def test_kfee_price_in_accounting(accountant): """ Test that KFEEs are correctly handled during accounting """ history = [ { 'timestamp': 1609537953, 'base_asset': 'ETH', 'quote_asset': A_USDT.identifier, 'trade_type': 'sell', 'rate': 1000, 'fee': '30', 'fee_currency': A_KFEE.identifier, 'amount': 0.02, 'location': 'kraken', }, ] ledger_actions_list = [ LedgerAction( identifier=0, timestamp=Timestamp(1539713238), action_type=LedgerActionType.INCOME, location=Location.KRAKEN, amount=FVal(1), asset=A_ETH, rate=None, rate_asset=None, link=None, notes='', ), LedgerAction( identifier=0, timestamp=Timestamp(1539713238), action_type=LedgerActionType.INCOME, location=Location.KRAKEN, amount=FVal(1000), asset=A_KFEE, rate=None, rate_asset=None, link=None, notes='', ), ] report, _ = accounting_history_process( accountant, start_ts=1539713238, end_ts=1624395187, history_list=history, ledger_actions_list=ledger_actions_list, ) warnings = accountant.msg_aggregator.consume_warnings() assert len(warnings) == 0 errors = accountant.msg_aggregator.consume_errors() assert len(errors) == 0 # The ledger actions income doesn't count for the income as the 1 # year rule is applied. Only the sell is computed. # The expected PnL without the fee is 14.2277000 # counting the fee is 14.2277000 - 30 * 0.01 * 0.82411 assert FVal(report['total_profit_loss']) == FVal(13.980467)
def add_ledger_action( self, action: LedgerAction, profit_loss_in_profit_currency: FVal, ) -> None: if not self.create_csv: return self.ledger_actions_csv.append({ 'time': self.timestamp_to_date(action.timestamp), 'type': str(action.action_type), 'location': str(action.location), 'asset': str(action.asset), 'amount': str(action.amount), f'profit_loss_in_{self.profit_currency.symbol}': profit_loss_in_profit_currency, }) paid_asset: Optional[Asset] received_asset: Optional[Asset] if action.is_profitable(): paid_in_profit_currency = ZERO paid_in_asset = ZERO paid_asset = None received_asset = action.asset received_in_asset = action.amount received_in_profit_currency = profit_loss_in_profit_currency else: paid_in_profit_currency = profit_loss_in_profit_currency paid_in_asset = action.amount paid_asset = action.asset received_asset = None received_in_asset = AssetAmount(ZERO) received_in_profit_currency = ZERO self.add_to_allevents( event_type=EV_LEDGER_ACTION, location=action.location, paid_in_profit_currency=paid_in_profit_currency, paid_asset=paid_asset, paid_in_asset=paid_in_asset, received_asset=received_asset, received_in_asset=received_in_asset, taxable_received_in_profit_currency=received_in_profit_currency, total_received_in_profit_currency=received_in_profit_currency, timestamp=action.timestamp, link=action.link, notes=action.notes, )
def add_ledger_action(self, action: LedgerAction) -> int: """Adds a new ledger action to the DB and returns its identifier for success""" cursor = self.db.conn.cursor() query = """ INSERT INTO ledger_actions( timestamp, type, location, amount, asset, rate, rate_asset, link, notes ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);""" cursor.execute(query, action.serialize_for_db()) identifier = cursor.lastrowid self.db.conn.commit() return identifier
def _deserialize_transaction(grant_id: int, rawtx: Dict[str, Any]) -> LedgerAction: """May raise: - DeserializationError - KeyError - UnknownAsset """ timestamp = deserialize_timestamp_from_date( date=rawtx['timestamp'], formatstr='%Y-%m-%dT%H:%M:%S', location='Gitcoin API', skip_milliseconds=True, ) asset = get_gitcoin_asset(symbol=rawtx['asset'], token_address=rawtx['token_address']) raw_amount = deserialize_int_from_str(symbol=rawtx['amount'], location='gitcoin api') amount = asset_normalized_value(raw_amount, asset) if amount == ZERO: raise ZeroGitcoinAmount() # let's use gitcoin's calculated rate for now since they include it in the response usd_value = Price( ZERO) if rawtx['usd_value'] is None else deserialize_price( rawtx['usd_value']) # noqa: E501 rate = Price(ZERO) if usd_value == ZERO else Price(usd_value / amount) raw_txid = rawtx['tx_hash'] tx_type, tx_id = process_gitcoin_txid(key='tx_hash', entry=rawtx) # until we figure out if we can use it https://github.com/gitcoinco/web/issues/9255#issuecomment-874537144 # noqa: E501 clr_round = _calculate_clr_round(timestamp, rawtx) notes = f'Gitcoin grant {grant_id} event' if not clr_round else f'Gitcoin grant {grant_id} event in clr_round {clr_round}' # noqa: E501 return LedgerAction( identifier=1, # whatever -- does not end up in the DB timestamp=timestamp, action_type=LedgerActionType.DONATION_RECEIVED, location=Location.GITCOIN, amount=AssetAmount(amount), asset=asset, rate=rate, rate_asset=A_USD, link=raw_txid, notes=notes, extra_data=GitcoinEventData( tx_id=tx_id, grant_id=grant_id, clr_round=clr_round, tx_type=tx_type, ), )
def test_fees_in_received_asset(accountant): """ Test the sell trade where the fee is nominated in the asset received. We had a bug where the PnL report said that there was no documented adquisition. """ history = [ { 'timestamp': 1609537953, 'base_asset': 'ETH', 'quote_asset': A_USDT.identifier, 'trade_type': 'sell', 'rate': 1000, 'fee': '0.10', 'fee_currency': A_USDT.identifier, 'amount': 0.02, 'location': 'binance', }, ] ledger_actions_list = [ LedgerAction( identifier=0, timestamp=Timestamp(1539713238), action_type=LedgerActionType.INCOME, location=Location.BINANCE, amount=FVal(1), asset=A_ETH, rate=None, rate_asset=None, link=None, notes='', ), ] report, _ = accounting_history_process( accountant, start_ts=1539713238, end_ts=1624395187, history_list=history, ledger_actions_list=ledger_actions_list, ) warnings = accountant.msg_aggregator.consume_warnings() assert len(warnings) == 0 errors = accountant.msg_aggregator.consume_errors() assert len(errors) == 0 assert accountant.events.cost_basis.get_calculated_asset_amount( A_USDT.identifier).is_close('19.90') # noqa: E501 # The ethereum income doesn't count for the income as the 1 # year rule is applied. Only the sell is computed. assert FVal(report['total_profit_loss']) == FVal(14.13870)
def edit_ledger_action(self, action: LedgerAction) -> Optional[str]: """Edits a ledger action from the DB by identifier Returns None for success or an error message for error """ error_msg = None cursor = self.db.conn.cursor() query = """ UPDATE ledger_actions SET timestamp=?, type=?, location=?, amount=?, asset=?, rate=?, rate_asset=?, link=?, notes=? WHERE identifier=?""" db_action_tuple = action.serialize_for_db() cursor.execute(query, (*db_action_tuple, action.identifier)) if cursor.rowcount != 1: error_msg = ( f'Tried to edit ledger action with identifier {action.identifier} ' f'but it was not found in the DB') self.db.conn.commit() return error_msg
def get_ledger_actions( self, from_ts: Optional[Timestamp], to_ts: Optional[Timestamp], location: Optional[Location], ) -> List[LedgerAction]: cursor = self.db.conn.cursor() query = ('SELECT identifier,' ' timestamp,' ' type,' ' location,' ' amount,' ' asset,' ' rate,' ' rate_asset,' ' link,' ' notes FROM ledger_actions ') if location is not None: query += f'WHERE location="{location.serialize_for_db()}" ' query, bindings = form_query_to_filter_timestamps( query, 'timestamp', from_ts, to_ts) results = cursor.execute(query, bindings) actions = [] for result in results: try: action = LedgerAction.deserialize_from_db(result) except DeserializationError as e: self.msg_aggregator.add_error( f'Error deserializing Ledger Action from the DB. Skipping it.' f'Error was: {str(e)}', ) continue except UnknownAsset as e: self.msg_aggregator.add_error( f'Error deserializing Ledger Action from the DB. Skipping it. ' f'Unknown asset {e.asset_name} found', ) continue actions.append(action) return actions