def assert_pnl_totals_close(expected: PnlTotals, got: PnlTotals) -> None: # ignore prefork acquisitions for these tests got.pop(AccountingEventType.PREFORK_ACQUISITION) assert len(expected) == len(got) for event_type, expected_pnl in expected.items(): assert expected_pnl.free.is_close(got[event_type].free) assert expected_pnl.taxable.is_close(got[event_type].taxable)
def test_receiving_value_from_tx(accountant, google_service): """ Test that receiving a transaction that provides value works fine """ addr2 = make_ethereum_address() tx_hash = '0x5cc0e6e62753551313412492296d5e57bea0a9d1ce507cc96aa4aa076c5bde7a' history = [ HistoryBaseEntry( event_identifier=tx_hash, sequence_index=0, timestamp=1569924574000, location=Location.BLOCKCHAIN, location_label=make_ethereum_address(), asset=A_ETH, balance=Balance(amount=FVal('1.5')), notes=f'Received 1.5 ETH from {addr2}', event_type=HistoryEventType.RECEIVE, event_subtype=HistoryEventSubType.NONE, counterparty=addr2, ) ] accounting_history_process( accountant, start_ts=0, end_ts=1640493376, history_list=history, ) no_message_errors(accountant.msg_aggregator) expected_pnls = PnlTotals({ AccountingEventType.TRANSACTION_EVENT: PNL(taxable=FVal('242.385'), free=ZERO), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def add_report_overview( self, report_id: int, last_processed_timestamp: Timestamp, processed_actions: int, total_actions: int, pnls: PnlTotals, ) -> None: """Inserts the report overview data May raise: - InputError if the given report id does not exist """ cursor = self.db.conn_transient.cursor() cursor.execute( 'UPDATE pnl_reports SET last_processed_timestamp=?,' ' processed_actions=?, total_actions=? WHERE identifier=?', (last_processed_timestamp, processed_actions, total_actions, report_id), ) if cursor.rowcount != 1: raise InputError( f'Could not insert overview for {report_id}. ' f'Report id could not be found in the DB', ) tuples = [] for event_type, entry in pnls.items(): tuples.append( (report_id, event_type.serialize(), str(entry.taxable), str(entry.free))) # noqa: E501 cursor.executemany( 'INSERT OR IGNORE INTO pnl_report_totals(report_id, name, taxable_value, free_value) VALUES(?, ?, ?, ?)', # noqa: E501 tuples, ) self.db.conn_transient.commit()
def test_no_corresponding_buy_for_sell(accountant, google_service): """Test that if there is no corresponding buy for a sell, the entire sell counts as profit""" history = [ Trade( timestamp=1476979735, location=Location.KRAKEN, base_asset=A_BTC, quote_asset=A_EUR, trade_type=TradeType.SELL, amount=FVal(1), rate=FVal('2519.62'), fee=FVal('0.02'), fee_currency=A_EUR, link=None, ) ] accounting_history_process( accountant=accountant, start_ts=1436979735, end_ts=1519693374, history_list=history, ) expected_pnls = PnlTotals({ AccountingEventType.TRADE: PNL(taxable=FVal('2519.62'), free=ZERO), AccountingEventType.FEE: PNL(taxable=FVal('-0.02'), free=ZERO), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_buying_selling_eth_before_daofork(accountant, google_service): history3 = [ Trade( timestamp=1446979735, # 11/08/2015 location=Location.EXTERNAL, base_asset=A_ETH, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=FVal(1450), rate=FVal('0.2315893'), fee=None, fee_currency=None, link=None, ), Trade( # selling ETH prefork should also reduce our ETC amount timestamp=1461021812, # 18/04/2016 (taxable) location=Location.KRAKEN, base_asset=A_ETH, # cryptocompare hourly ETC/EUR price: 7.88 quote_asset=A_EUR, trade_type=TradeType.SELL, amount=FVal(50), rate=FVal('7.88'), fee=FVal('0.5215'), fee_currency=A_EUR, link=None, ), Trade( # selling ETC after the fork timestamp=1481979135, # 17/12/2016 location=Location.KRAKEN, base_asset=A_ETC, # cryptocompare hourly ETC/EUR price: 7.88 quote_asset=A_EUR, trade_type=TradeType.SELL, # not-taxable -- considered bought with ETH so after year amount=FVal(550), rate=FVal('1.78'), fee=FVal('0.9375'), fee_currency=A_EUR, link=None, ), Trade( # selling ETH after the fork timestamp=1482138141, # 19/12/2016 location=Location.KRAKEN, base_asset=A_ETH, # cryptocompare hourly ETH/EUR price: 7.45 quote_asset=A_EUR, trade_type=TradeType.SELL, # not-taxable -- after 1 year amount=FVal(10), rate=FVal('7.45'), fee=FVal('0.12'), fee_currency=A_EUR, link=None, ), ] accounting_history_process(accountant, 1436979735, 1495751688, history3) no_message_errors(accountant.msg_aggregator) # make sure that the intermediate ETH sell before the fork reduced our ETC assert accountant.pots[0].cost_basis.get_calculated_asset_amount('ETC') == FVal(850) assert accountant.pots[0].cost_basis.get_calculated_asset_amount('ETH') == FVal(1390) expected_pnls = PnlTotals({ AccountingEventType.TRADE: PNL(taxable=FVal('382.4205350'), free=FVal('923.8099920')), AccountingEventType.FEE: PNL(taxable=FVal('-1.579'), free=ZERO), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_taxable_ledger_action_setting(accountant, expected, google_service): """Test that ledger actions respect the taxable setting""" history = [ LedgerAction( identifier=1, timestamp=1476979735, action_type=LedgerActionType.INCOME, location=Location.EXTERNAL, amount=FVal(1), # 578.505 EUR/BTC from mocked prices asset=A_BTC, rate=None, rate_asset=None, link=None, notes=None, ), LedgerAction( identifier=2, timestamp=1491062063, action_type=LedgerActionType.AIRDROP, location=Location.EXTERNAL, amount=FVal(10), # 47.865 EUR/ETH from mocked prices asset=A_ETH, rate=None, rate_asset=None, link='foo', notes='boo', ), LedgerAction( identifier=3, timestamp=1501062063, action_type=LedgerActionType.LOSS, location=Location.BLOCKCHAIN, amount=FVal(2), # 175.44 EUR/ETH from mocked prices asset=A_ETH, rate=FVal(400), # but should use the given rate of 400 EUR rate_asset=A_EUR, link='goo', notes='hoo', ), LedgerAction( # include a non taxed ledger action too identifier=4, timestamp=1501062064, action_type=LedgerActionType.EXPENSE, location=Location.BLOCKCHAIN, amount=FVal(1), asset=A_ETH, rate=FVal(400), rate_asset=A_EUR, link='goo2', notes='hoo2', ), ] accounting_history_process( accountant=accountant, start_ts=1436979735, end_ts=1519693374, history_list=history, ) expected_pnls = PnlTotals() if expected != 0: expected_pnls[AccountingEventType.LEDGER_ACTION] = PNL(taxable=FVal(expected), free=ZERO) check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_selling_crypto_bought_with_crypto(accountant, google_service): history = [ Trade( timestamp=1446979735, location=Location.EXTERNAL, base_asset=A_BTC, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=FVal(82), rate=FVal('268.678317859'), fee=None, fee_currency=None, link=None, ), Trade( timestamp=1449809536, # cryptocompare hourly BTC/EUR price: 386.175 location=Location.POLONIEX, base_asset=A_XMR, # cryptocompare hourly XMR/EUR price: 0.39665 quote_asset=A_BTC, trade_type=TradeType.BUY, amount=FVal(375), rate=FVal('0.0010275'), fee=FVal('0.9375'), fee_currency=A_XMR, link=None, ), Trade( timestamp= 1458070370, # cryptocompare hourly rate XMR/EUR price: 1.0443027675 location=Location.KRAKEN, base_asset=A_XMR, quote_asset=A_EUR, trade_type=TradeType.SELL, amount=FVal(45), rate=FVal('1.0443027675'), fee=FVal('0.117484061344'), fee_currency=A_XMR, link=None, ), ] accounting_history_process(accountant, 1436979735, 1495751688, history) no_message_errors(accountant.msg_aggregator) # Make sure buying XMR with BTC also creates a BTC sell sells = accountant.pots[0].cost_basis.get_events(A_BTC).spends assert len(sells) == 1 assert sells[0].timestamp == 1449809536 assert sells[0].amount.is_close(FVal('0.3853125')) assert sells[0].rate.is_close(FVal('386.03406326')) expected_pnls = PnlTotals({ AccountingEventType.TRADE: PNL(taxable=FVal('74.3118704999540625'), free=ZERO), AccountingEventType.FEE: PNL(taxable=FVal('-0.419658351381311222'), free=ZERO), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_no_taxfree_period(accountant, google_service): accounting_history_process(accountant, 1436979735, 1519693374, history5) no_message_errors(accountant.msg_aggregator) expected_pnls = PnlTotals({ AccountingEventType.TRADE: PNL(taxable=FVal('265253.1283582327833875'), free=ZERO), AccountingEventType.FEE: PNL(taxable=FVal('-0.238868129979988140934107'), free=ZERO), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_nocrypto2crypto(accountant, google_service): accounting_history_process(accountant, 1436979735, 1519693374, history5) no_message_errors(accountant.msg_aggregator) expected_pnls = PnlTotals({ AccountingEventType.TRADE: PNL(taxable=ZERO, free=FVal('264693.433642820')), AccountingEventType.FEE: PNL(taxable=FVal('-1.1708853227087498964'), free=ZERO), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def __init__( self, database: 'DBHandler', evm_accounting_aggregator: 'EVMAccountingAggregator', msg_aggregator: MessagesAggregator, ) -> None: super().__init__(database=database) self.profit_currency = self.settings.main_currency self.cost_basis = CostBasisCalculator( database=database, msg_aggregator=msg_aggregator, ) self.pnls = PnlTotals() self.processed_events: List[ProcessedAccountingEvent] = [] self.transactions = TransactionsAccountant( evm_accounting_aggregator=evm_accounting_aggregator, pot=self, ) self.query_start_ts = self.query_end_ts = Timestamp(0)
def test_simple_accounting(accountant, google_service): accounting_history_process(accountant, 1436979735, 1495751688, history1) no_message_errors(accountant.msg_aggregator) expected_pnls = PnlTotals({ AccountingEventType.TRADE: PNL(taxable=FVal('559.6947154'), free=ZERO), AccountingEventType.FEE: PNL(taxable=FVal('-0.23886813'), free=ZERO), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_fees_count_in_cost_basis(accountant, google_service): """Make sure that asset amounts used in fees are reduced.""" history = [ Trade( timestamp=1609537953, location=Location.KRAKEN, base_asset=A_ETH, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=ONE, rate=FVal('598.26'), fee=ONE, fee_currency=A_EUR, link=None, ), Trade( # PNL: 0.5 * 1862.06 - 0.5 * 599.26 -> 631.4 # fee: -0.5 * 1862.06 + 0.5 * 1862.06 - 0.5 * 599.26 -> -299.63 timestamp=1624395186, location=Location.KRAKEN, base_asset=A_ETH, quote_asset=A_EUR, trade_type=TradeType.SELL, amount=FVal('0.5'), rate=FVal('1862.06'), fee=FVal('0.5'), fee_currency=A_ETH, link=None, ), Trade( timestamp=1625001464, location=Location.KRAKEN, base_asset=A_ETH, quote_asset=A_EUR, trade_type=TradeType.SELL, amount=FVal('0.5'), rate=FVal('1837.31'), fee=None, fee_currency=None, link=None, ), ] accounting_history_process( accountant=accountant, start_ts=1436979735, end_ts=1625001466, history_list=history, ) expected_pnls = PnlTotals({ AccountingEventType.TRADE: PNL(taxable=FVal('1550.055'), free=ZERO), AccountingEventType.FEE: PNL(taxable=FVal('-300.630'), free=ZERO), }) assert accountant.pots[0].cost_basis.get_calculated_asset_amount(A_ETH) is None warnings = accountant.msg_aggregator.consume_warnings() assert len(warnings) == 0 check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_kfee_price_in_accounting(accountant, google_service): """ Test that KFEEs are correctly handled during accounting KFEE price is fixed at $0.01 """ history = [ LedgerAction( identifier=0, timestamp=Timestamp(1539713238), # 178.615 EUR/ETH 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), # 0.8612 USD/EUR. 1 KFEE = $0.01 so 8.612 EUR action_type=LedgerActionType.INCOME, location=Location.KRAKEN, amount=FVal(1000), asset=A_KFEE, rate=None, rate_asset=None, link=None, notes='', ), Trade( timestamp=1609537953, location=Location.KRAKEN, # 0.89 USDT/EUR -> PNL: 20 * 0.89 - 0.02*178.615 -> 14.2277 base_asset=A_ETH, quote_asset=A_USDT, trade_type=TradeType.SELL, amount=FVal('0.02'), rate=FVal(1000), fee=FVal(30), # KFEE should not be taken into account fee_currency=A_KFEE, link=None, ), ] accounting_history_process( accountant, start_ts=1539713238, end_ts=1624395187, history_list=history, ) no_message_errors(accountant.msg_aggregator) expected_pnls = PnlTotals({ AccountingEventType.TRADE: PNL(taxable=ZERO, free=FVal('14.2277')), AccountingEventType.LEDGER_ACTION: PNL(taxable=FVal('187.227'), free=ZERO), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_accounting_works_for_empty_history(accountant, google_service): history = [] accounting_history_process( accountant=accountant, start_ts=1436979735, end_ts=1519693374, history_list=history, ) no_message_errors(accountant.msg_aggregator) expected_pnls = PnlTotals() check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_kraken_staking_events(accountant, google_service): """ Test that staking events from kraken are correctly processed """ history = [ HistoryBaseEntry( event_identifier='XXX', sequence_index=0, timestamp=1640493374000, location=Location.KRAKEN, location_label='Kraken 1', asset=A_ETH2, balance=Balance( amount=FVal(0.0000541090), usd_value=FVal(0.212353475950), ), notes=None, event_type=HistoryEventType.STAKING, event_subtype=HistoryEventSubType.REWARD, ), HistoryBaseEntry( event_identifier='YYY', sequence_index=0, timestamp=1636638550000, location=Location.KRAKEN, location_label='Kraken 1', asset=A_ETH2, balance=Balance( amount=FVal(0.0000541090), usd_value=FVal(0.212353475950), ), notes=None, event_type=HistoryEventType.STAKING, event_subtype=HistoryEventSubType.REWARD, ) ] _, events = accounting_history_process( accountant, start_ts=1636638549, end_ts=1640493376, history_list=history, ) no_message_errors(accountant.msg_aggregator) expected_pnls = PnlTotals({ AccountingEventType.STAKING: PNL(taxable=FVal('0.471505826'), free=ZERO), }) check_pnls_and_csv(accountant, expected_pnls, google_service) assert len(events) == 2 expected_pnls = [FVal('0.25114638241'), FVal('0.22035944359')] for idx, event in enumerate(events): assert event.pnl.taxable == expected_pnls[idx] assert event.type == AccountingEventType.STAKING
def test_big_taxfree_period(accountant, google_service): accounting_history_process(accountant, 1436979735, 1519693374, history5) no_message_errors(accountant.msg_aggregator) expected_pnls = PnlTotals({ AccountingEventType.TRADE: PNL(taxable=ZERO, free=FVal('265253.1283582327833875')), AccountingEventType.FEE: PNL( taxable=FVal('-1.170885322708749896'), free=FVal('0.932017192728761755465893'), ), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_fees_in_received_asset(accountant, google_service): """ 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 acquisition. """ history = [ LedgerAction( identifier=0, timestamp=Timestamp(1539713238), # 178.615 EUR/ETH action_type=LedgerActionType.INCOME, location=Location.BINANCE, amount=ONE, asset=A_ETH, rate=None, rate_asset=None, link=None, notes='', ), Trade( # Sell 0.02 ETH for USDT with rate 1000 USDT/ETH and 0.10 USDT fee # So acquired 20 USDT for 0.02 ETH + 0.10 USDT # So acquired 20 USDT for 0.02 * 598.26 + 0.10 * 0.89 -> 12.0542 EUR # So paid 12.0542/20 -> 0.60271 EUR/USDT timestamp=1609537953, # 0.89 EUR/USDT location=Location.BINANCE, base_asset=A_ETH, # 598.26 EUR/ETH quote_asset=A_USDT, trade_type=TradeType.SELL, amount=FVal('0.02'), rate=FVal(1000), fee=FVal('0.10'), fee_currency=A_USDT, link=None, ), ] accounting_history_process( accountant, start_ts=1539713238, end_ts=1624395187, history_list=history, ) no_message_errors(accountant.msg_aggregator) assert accountant.pots[0].cost_basis.get_calculated_asset_amount(A_USDT.identifier).is_close('19.90') # noqa: E501 expected_pnls = PnlTotals({ AccountingEventType.TRADE: PNL(taxable=ZERO, free=FVal('14.2277')), AccountingEventType.FEE: PNL(taxable=FVal('-0.060271'), free=ZERO), AccountingEventType.LEDGER_ACTION: PNL(taxable=FVal('178.615'), free=ZERO), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_report_settings(database): dbreport = DBAccountingReports(database) settings = DBSettings( main_currency=A_GBP, calculate_past_cost_basis=False, include_gas_costs=False, account_for_assets_movements=True, pnl_csv_have_summary=False, pnl_csv_with_formulas=True, taxfree_after_period=15, ) start_ts = 1 first_processed_timestamp = 4 last_processed_timestamp = 9 end_ts = 10 report_id = dbreport.add_report( first_processed_timestamp=first_processed_timestamp, start_ts=start_ts, end_ts=end_ts, settings=settings, ) total_actions = 10 processed_actions = 2 dbreport.add_report_overview( report_id=report_id, last_processed_timestamp=last_processed_timestamp, processed_actions=processed_actions, total_actions=total_actions, pnls=PnlTotals(), ) data, entries_num = dbreport.get_reports(report_id=report_id, with_limit=False) assert len(data) == 1 assert entries_num == 1 report = data[0] assert report['identifier'] == report_id assert report['start_ts'] == start_ts assert report['first_processed_timestamp'] == first_processed_timestamp assert report['last_processed_timestamp'] == last_processed_timestamp assert report['processed_actions'] == processed_actions assert report['total_actions'] == total_actions returned_settings = report['settings'] assert len(returned_settings) == 6 for x in ('account_for_assets_movements', 'calculate_past_cost_basis', 'include_crypto2crypto', 'include_gas_costs', 'profit_currency', 'taxfree_after_period'): # noqa: E501 setting_name = 'main_currency' if x == 'profit_currency' else x assert returned_settings[x] == getattr(settings, setting_name)
def test_eth2_staking(accountant, google_service): """Test that ethereum 2 staking is accounted for properly""" history = [ ValidatorDailyStats( validator_index=1, timestamp=1607727600, # ETH price: 449.68 ETH/EUR start_amount=FVal('32'), end_amount=FVal('32.05'), pnl=FVal('0.05'), # 0.05 * 449.68 = 22.484 ), ValidatorDailyStats( validator_index=1, timestamp=1607814000, # ETH price: 469.82 ETH/EUR start_amount=FVal('32.05'), end_amount=FVal('32.045'), pnl=FVal( '-0.005' ), # -0.005 * 469.82 + 0.005 * 469.82 - 0.005*449.68 = -2.2484 ), ValidatorDailyStats( validator_index=1, timestamp=1607900400, # ETH price: 486.57 ETH/EUR start_amount=FVal('32.045'), end_amount=FVal('32.085'), pnl=FVal('0.04'), # 0.04 * 486.57 = 19.4628 ), ValidatorDailyStats( validator_index=2, timestamp=1607900400, start_amount=FVal('32'), end_amount=FVal('32.045'), pnl=FVal('0.045'), # 0.045 * 486.57 = 21.89565 ), ] accounting_history_process( accountant, start_ts=1606727600, end_ts=1640493376, history_list=history, ) no_message_errors(accountant.msg_aggregator) expected_pnls = PnlTotals({ # 22.484 - 2.2484 + 19.4628 + 21.89565 AccountingEventType.STAKING: PNL(taxable=FVal('61.59405'), free=ZERO), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_gas_fees_after_year(accountant, google_service): """ Test that for an expense like gas fees after year the "selling" part is tax free PnL, and the expense part is taxable pnl. """ tx_hash = '0x5cc0e6e62753551313412492296d5e57bea0a9d1ce507cc96aa4aa076c5bde7a' history = [ LedgerAction( identifier=0, timestamp=Timestamp(1539713238), # 178.615 EUR/ETH action_type=LedgerActionType. GIFT, # gift so not counting as income location=Location.KRAKEN, amount=FVal(1), asset=A_ETH, rate=None, rate_asset=None, link=None, notes='', ), HistoryBaseEntry( event_identifier=tx_hash, sequence_index=0, timestamp=1640493374000, # 4072.51 EUR/ETH location=Location.BLOCKCHAIN, location_label=make_ethereum_address(), asset=A_ETH, balance=Balance(amount=FVal('0.01')), notes='Burned 0.01 ETH in gas', event_type=HistoryEventType.SPEND, event_subtype=HistoryEventSubType.FEE, counterparty=CPT_GAS, ) ] accounting_history_process( accountant, start_ts=0, end_ts=1640493376, history_list=history, ) no_message_errors(accountant.msg_aggregator) expected_pnls = PnlTotals({ AccountingEventType.TRANSACTION_EVENT: PNL(taxable=FVal('-40.7251'), free=FVal('38.93895')), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_include_gas_costs(accountant, google_service): addr1 = '0x2B888954421b424C5D3D9Ce9bB67c9bD47537d12' tx_hash = '0x5cc0e6e62753551313412492296d5e57bea0a9d1ce507cc96aa4aa076c5bde7a' history = [ Trade( timestamp=1539388574, location=Location.EXTERNAL, base_asset=A_ETH, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=FVal(10), rate=FVal('168.7'), fee=None, fee_currency=None, link=None, ), HistoryBaseEntry( event_identifier=tx_hash, sequence_index=0, timestamp=1569924574000, location=Location.BLOCKCHAIN, location_label=addr1, asset=A_ETH, balance=Balance(amount=FVal('0.000030921')), notes=f'Burned 0.000030921 ETH in gas from {addr1}', event_type=HistoryEventType.SPEND, event_subtype=HistoryEventSubType.FEE, counterparty=CPT_GAS, ) ] accounting_history_process(accountant, start_ts=1436979735, end_ts=1619693374, history_list=history) # noqa: E501 no_message_errors(accountant.msg_aggregator) expected = ZERO expected_pnls = PnlTotals() if accountant.pots[0].settings.include_gas_costs: expected = FVal('-0.0052163727') expected_pnls[AccountingEventType.TRANSACTION_EVENT] = PNL( taxable=expected, free=ZERO) check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_ignored_assets(accountant, google_service): history = history1 + [ Trade( timestamp=1476979735, location=Location.KRAKEN, base_asset=A_DASH, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=FVal(10), rate=FVal('9.76775956284'), fee=FVal('0.0011'), fee_currency=A_DASH, link=None, ), Trade( timestamp=1496979735, location=Location.KRAKEN, base_asset=A_DASH, quote_asset=A_EUR, trade_type=TradeType.SELL, amount=FVal(5), rate=FVal('128.09'), fee=FVal('0.015'), fee_currency=A_EUR, link=None, ), ] accounting_history_process(accountant, 1436979735, 1519693374, history) no_message_errors(accountant.msg_aggregator) expected_pnls = PnlTotals({ AccountingEventType.TRADE: PNL(taxable=FVal('559.6947154127833875'), free=ZERO), AccountingEventType.FEE: PNL(taxable=FVal('-0.238868129979988140934107'), free=ZERO), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_ledger_actions_accounting(accountant, google_service): """Test for accounting for ledger actions Makes sure that Ledger actions are processed in accounting, range is respected and that they contribute to the "bought" amount per asset and that also if a rate is given then that is used instead of the queried price """ history = [LedgerAction( # before range - read only for amount not profit identifier=1, timestamp=1435979735, # 0.1 EUR per ETH action_type=LedgerActionType.INCOME, location=Location.EXTERNAL, asset=A_ETH, amount=AssetAmount(FVal(1)), rate=None, rate_asset=None, link=None, notes=None, ), LedgerAction( identifier=2, timestamp=1437279735, # 250 EUR per BTC action_type=LedgerActionType.INCOME, location=Location.BLOCKCHAIN, asset=A_BTC, amount=AssetAmount(FVal(1)), rate=FVal('400'), rate_asset=A_EUR, link='foo', notes='we give a rate here', ), LedgerAction( identifier=3, timestamp=1447279735, # 0.4 EUR per XMR action_type=LedgerActionType.DIVIDENDS_INCOME, location=Location.KRAKEN, asset=A_XMR, amount=AssetAmount(FVal(10)), rate=None, rate_asset=None, link=None, notes=None, ), LedgerAction( identifier=4, timestamp=1457279735, # 1 EUR per ETH action_type=LedgerActionType.EXPENSE, location=Location.EXTERNAL, asset=A_ETH, amount=AssetAmount(FVal('0.1')), rate=None, rate_asset=None, link=None, notes=None, ), LedgerAction( identifier=5, timestamp=1467279735, # 420 EUR per BTC action_type=LedgerActionType.LOSS, location=Location.EXTERNAL, asset=A_BTC, amount=AssetAmount(FVal('0.1')), rate=FVal(500), rate_asset=A_USD, link='foo2', notes='we give a rate here', ), LedgerAction( # after range and should be completely ignored identifier=6, timestamp=1529693374, action_type=LedgerActionType.EXPENSE, location=Location.EXTERNAL, asset=A_ETH, amount=AssetAmount(FVal('0.5')), rate=FVal(400), rate_asset=A_EUR, link='foo3', notes='we give a rate here too but doesnt matter', )] accounting_history_process( accountant=accountant, start_ts=1436979735, end_ts=1519693374, history_list=history, ) assert accountant.pots[0].cost_basis.get_calculated_asset_amount(A_BTC).is_close('0.9') assert accountant.pots[0].cost_basis.get_calculated_asset_amount(A_ETH).is_close('0.9') assert accountant.pots[0].cost_basis.get_calculated_asset_amount(A_XMR).is_close('10') expected_pnls = PnlTotals({ # 400 + 0.4*10 - 1*0.1 + 1*0.1 - 1*0.01 - 0.1*500*0.9004 + 0.1*500*0.9004 - 0.1* 400 AccountingEventType.LEDGER_ACTION: PNL(taxable=FVal('363.99'), free=ZERO), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_assets_movements_not_accounted_for(accountant, expected, google_service): # asset_movements_list partially copied from # rotkehlchen/tests/integration/test_end_to_end_tax_report.py history = [ Trade( timestamp=1446979735, location=Location.EXTERNAL, base_asset=A_BTC, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=FVal(82), rate=FVal('268.678317859'), fee=None, fee_currency=None, link=None, ), Trade( timestamp=1446979735, location=Location.EXTERNAL, base_asset=A_ETH, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=FVal(1450), rate=FVal('0.2315893'), fee=None, fee_currency=None, link=None, ), AssetMovement( # before query period -- 8.915 * 0.001 = 8.915e-3 location=Location.KRAKEN, category=AssetMovementCategory.WITHDRAWAL, address=None, transaction_id=None, timestamp=Timestamp(1479510304), # 18/11/2016, asset=A_ETH, # cryptocompare hourly ETH/EUR: 8.915 amount=FVal('95'), fee_asset=A_ETH, fee=Fee(FVal('0.001')), link='krakenid1', ), AssetMovement( # 0.00029*1964.685 = 0.56975865 location=Location.POLONIEX, address='foo', transaction_id='0xfoo', category=AssetMovementCategory.WITHDRAWAL, timestamp=Timestamp(1495969504), # 28/05/2017, asset=A_BTC, # cryptocompare hourly BTC/EUR: 1964.685 amount=FVal('8.5'), fee_asset=A_BTC, fee=Fee(FVal('0.00029')), link='poloniexid1', ), ] accounting_history_process( accountant=accountant, start_ts=1436979735, end_ts=1519693374, history_list=history, ) no_message_errors(accountant.msg_aggregator) expected_pnls = PnlTotals() if expected != ZERO: expected_pnls[AccountingEventType.ASSET_MOVEMENT] = PNL( taxable=expected, free=ZERO) # noqa: E501 check_pnls_and_csv(accountant, expected_pnls, google_service)
class AccountingPot(CustomizableDateMixin): """ Represents a single accounting depot for which events are processed under a specific set of rules """ def __init__( self, database: 'DBHandler', evm_accounting_aggregator: 'EVMAccountingAggregator', msg_aggregator: MessagesAggregator, ) -> None: super().__init__(database=database) self.profit_currency = self.settings.main_currency self.cost_basis = CostBasisCalculator( database=database, msg_aggregator=msg_aggregator, ) self.pnls = PnlTotals() self.processed_events: List[ProcessedAccountingEvent] = [] self.transactions = TransactionsAccountant( evm_accounting_aggregator=evm_accounting_aggregator, pot=self, ) self.query_start_ts = self.query_end_ts = Timestamp(0) def _add_processed_event(self, event: ProcessedAccountingEvent) -> None: dbpnl = DBAccountingReports(self.database) self.processed_events.append(event) try: dbpnl.add_report_data( report_id=self.report_id, time=event.timestamp, ts_converter=self.timestamp_to_date, event=event, ) except (DeserializationError, InputError) as e: log.error(str(e)) return log.debug(event.to_string(self.timestamp_to_date)) def get_rate_in_profit_currency(self, asset: Asset, timestamp: Timestamp) -> Price: """Get the profit_currency price of asset in the given timestamp May raise: - PriceQueryUnsupportedAsset if from/to asset is missing from price oracles - NoPriceForGivenTimestamp if we can't find a price for the asset in the given timestamp from the price oracle - RemoteError if there is a problem reaching the price oracle server or with reading the response returned by the server """ if asset == self.profit_currency: rate = Price(FVal(1)) else: rate = PriceHistorian().query_historical_price( from_asset=asset, to_asset=self.profit_currency, timestamp=timestamp, ) return rate def reset( self, settings: DBSettings, start_ts: Timestamp, end_ts: Timestamp, report_id: int, ) -> None: self.settings = settings self.report_id = report_id self.profit_currency = self.settings.main_currency self.query_start_ts = start_ts self.query_end_ts = end_ts self.pnls.reset() self.cost_basis.reset(settings) self.transactions.reset() self.processed_events = [] def add_acquisition( self, # pylint: disable=unused-argument event_type: AccountingEventType, notes: str, location: Location, timestamp: Timestamp, asset: Asset, amount: FVal, taxable: bool, given_price: Optional[Price] = None, extra_data: Optional[Dict] = None, **kwargs: Any, # to be able to consume args given by add_asset_change_event ) -> None: """Add an asset acquisition event for the pot and count it in PnL if needed. If a custom price for the asset should be used it can be passed here via given_price. Price is always in profit currency during accounting.""" if amount == ZERO: # do nothing for zero acquisitions return if given_price is not None: price = given_price else: price = self.get_rate_in_profit_currency(asset=asset, timestamp=timestamp) prefork_events = handle_prefork_asset_acquisitions( cost_basis=self.cost_basis, location=location, timestamp=timestamp, asset=asset, amount=amount, price=price, starting_index=len(self.processed_events), ) for prefork_event in prefork_events: self._add_processed_event(prefork_event) event = ProcessedAccountingEvent( type=event_type, notes=notes, location=location, timestamp=timestamp, asset=asset, taxable_amount=amount, free_amount=ZERO, price=price, pnl=PNL(), # filled out later cost_basis=None, index=len(self.processed_events), ) if extra_data: event.extra_data = extra_data self.cost_basis.obtain_asset(event) # count profit/losses if we are inside the query period if timestamp >= self.query_start_ts and taxable: self.pnls[event_type] += event.calculate_pnl( count_entire_amount_spend=False, count_cost_basis_pnl=True, ) self._add_processed_event(event) def add_spend( self, event_type: AccountingEventType, notes: str, location: Location, timestamp: Timestamp, asset: Asset, amount: FVal, taxable: bool, given_price: Optional[Price] = None, taxable_amount_ratio: FVal = ONE, count_entire_amount_spend: bool = True, count_cost_basis_pnl: bool = True, extra_data: Optional[Dict[str, Any]] = None, ) -> Tuple[FVal, FVal]: """Add an asset spend event for the pot and count it in PnL if needed If a custom price for the asset should be used it can be passed here via given_price. Price is always in profit currency during accounting. If taxable_ratio is given then this is how we initialize the taxable and free amounts in the case of missing cost basis. By default it's all taxable. If count_entire_amount_spend is True then the entire amount is counted as a spend. Which means an expense (negative pnl). If count_cost_basis_pnl is True then we also count any profit/loss the asset may have had compared to when it was acquired. Returns (free, taxable) amounts. """ if amount == ZERO: # do nothing for zero spends return ZERO, ZERO if asset.is_fiat() and event_type != AccountingEventType.FEE: taxable = False handle_prefork_asset_spends( cost_basis=self.cost_basis, asset=asset, amount=amount, timestamp=timestamp, ) if given_price is not None: price = given_price else: price = self.get_rate_in_profit_currency( asset=asset, timestamp=timestamp, ) if asset == A_KFEE: count_cost_basis_pnl = False taxable = False spend_cost = None if count_cost_basis_pnl: spend_cost = self.cost_basis.spend_asset( location=location, timestamp=timestamp, asset=asset, amount=amount, rate=price, taxable_spend=taxable, ) taxable_amount = taxable_amount_ratio * amount free_amount = amount - taxable_amount if spend_cost: taxable_amount = spend_cost.taxable_amount free_amount = amount - spend_cost.taxable_amount spend_event = ProcessedAccountingEvent( type=event_type, notes=notes, location=location, timestamp=timestamp, asset=asset, taxable_amount=taxable_amount, free_amount=free_amount, price=price, pnl=PNL(), # filled out later cost_basis=spend_cost, index=len(self.processed_events), ) if extra_data: spend_event.extra_data = extra_data # count profit/losses if we are inside the query period if timestamp >= self.query_start_ts and taxable: self.pnls[event_type] += spend_event.calculate_pnl( count_entire_amount_spend=count_entire_amount_spend, count_cost_basis_pnl=count_cost_basis_pnl, ) self._add_processed_event(spend_event) return free_amount, taxable_amount def add_asset_change_event( self, method: Literal['acquisition', 'spend'], event_type: AccountingEventType, notes: str, location: Location, timestamp: Timestamp, asset: Asset, amount: FVal, taxable: bool, given_price: Optional[Price] = None, **kwargs: Any, ) -> None: fn = getattr(self, f'add_{method}') return fn( event_type=event_type, notes=notes, location=location, timestamp=timestamp, asset=asset, amount=amount, taxable=taxable, given_price=given_price, **kwargs, ) def get_prices_for_swap( self, timestamp: Timestamp, amount_in: FVal, asset_in: Asset, amount_out: FVal, asset_out: Asset, fee: Optional[FVal], fee_asset: Optional[Asset], ) -> Optional[Tuple[Price, Price]]: """Calculates the prices for assets going in and out of a swap/trade. The rules are: - For the asset_in we get the equivalent rate from asset_out + fee if any. If there is no price found for fee_currency we ignore it. If there is no price for asset_out then we switch to using the asset_in price itself. If neither of the 2 assets can have their price known, we bail. - For the asset_out we get the equivalent rate from asset_in. if there is no price found for asset_in then we switch to using the asset_out price. If neither of the 2 assets can have their price known we bail. Returns (out_price, in_price) or None if it can't find proper prices """ if ZERO in (amount_in, amount_out): log.error( f'At get_prices_for_swap got a zero amount. {asset_in=} {amount_in=} ' f'{asset_out=} {amount_out=}. Skipping ...') return None try: out_price = self.get_rate_in_profit_currency( asset=asset_out, timestamp=timestamp, ) except (PriceQueryUnsupportedAsset, NoPriceForGivenTimestamp, RemoteError): out_price = None fee_price = None if fee is not None and fee_asset is not None and fee != ZERO: # also checking fee_asset != None due to https://github.com/rotki/rotki/issues/4172 try: fee_price = self.get_rate_in_profit_currency( asset=fee_asset, timestamp=timestamp, ) except (PriceQueryUnsupportedAsset, NoPriceForGivenTimestamp, RemoteError): fee_price = None try: in_price = self.get_rate_in_profit_currency( asset=asset_in, timestamp=timestamp, ) except (PriceQueryUnsupportedAsset, NoPriceForGivenTimestamp, RemoteError): in_price = None if out_price is None and in_price is None: return None if out_price is not None: paid = amount_out * out_price if fee_price is not None: paid += fee_price * fee # type: ignore # fee should exist here calculated_in = Price(paid / amount_in) else: calculated_in = in_price # type: ignore # in_price should exist here if in_price is not None: calculated_out = Price((amount_in * in_price) / amount_out) else: calculated_out = out_price # type: ignore # out_price should exist here return (calculated_out, calculated_in)
def test_buying_selling_bch_before_bsvfork(accountant, google_service): history = [ Trade( # 6.5 BTC 6.5 BCH 6.5 BSV timestamp=1491593374, # 04/07/2017 location=Location.EXTERNAL, base_asset=A_BTC, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=FVal('6.5'), rate=FVal('1128.905'), fee=FVal('0.55'), fee_currency=A_EUR, link=None, ), Trade( # selling BTC prefork should also reduce the BCH and BSV equivalent -- taxable # 6 BTC 6 BCH 6 BSV timestamp=1500595200, # 21/07/2017 location=Location.EXTERNAL, base_asset=A_BTC, quote_asset=A_EUR, trade_type=TradeType.SELL, amount=FVal('0.5'), rate=FVal('2380.835'), fee=FVal('0.15'), fee_currency=A_EUR, link=None, ), Trade( # selling BCH after the fork should also reduce BSV equivalent -- taxable # 6 BTC 3.9 BCH 3.9 BSV timestamp=1512693374, # 08/12/2017 location=Location.KRAKEN, base_asset=A_BCH, # cryptocompare hourly BCH/EUR price: 995.935 quote_asset=A_EUR, trade_type=TradeType.SELL, amount=FVal('2.1'), rate=FVal('995.935'), fee=FVal('0.26'), fee_currency=A_EUR, link=None, ), Trade( # 4.8 BTC 3.9 BCH 3.9 BSV timestamp=1514937600, # 03/01/2018 location=Location.KRAKEN, base_asset=A_BTC, # cryptocompare hourly BCH/EUR price: 995.935 quote_asset=A_EUR, trade_type=TradeType.SELL, amount=FVal('1.2'), rate=FVal('12404.88'), fee=FVal('0.52'), fee_currency=A_EUR, link=None, ), Trade( # buying BCH before the BSV fork should increase BSV equivalent # 4.8 BTC 4.9 BCH 4.9 BSV timestamp=1524937600, location=Location.KRAKEN, base_asset=A_BCH, # cryptocompare hourly BCH/EUR price: 1146.98 quote_asset=A_EUR, trade_type=TradeType.BUY, amount=ONE, rate=FVal('1146.98'), fee=FVal('0.52'), fee_currency=A_EUR, link=None, ), Trade( # selling BCH before the BSV fork should decrease the BSV equivalent # 4.8 BTC 4.6 BCH 4.6 BSV timestamp=1525937600, location=Location.KRAKEN, base_asset=A_BCH, # cryptocompare hourly BCH/EUR price: 1146.98 quote_asset=A_EUR, trade_type=TradeType.SELL, amount=FVal('0.3'), rate=FVal('1272.05'), fee=FVal('0.52'), fee_currency=A_EUR, link=None, ), Trade( # selling BCH after the BSV fork should not affect the BSV equivalent # 4.8 BTC 4.1 BCH 4.6 BSV timestamp=1552304352, location=Location.KRAKEN, base_asset=A_BCH, # cryptocompare hourly BCH/EUR price: 114.27 quote_asset=A_EUR, trade_type=TradeType.SELL, amount=FVal('0.5'), rate=FVal('114.27'), fee=FVal('0.52'), fee_currency=A_EUR, link=None, ), ] accounting_history_process(accountant, 1436979735, 1569693374, history) no_message_errors(accountant.msg_aggregator) amount_btc = FVal(4.8) amount_bch = FVal(4.1) amount_bsv = FVal(4.6) bch_buys = accountant.pots[0].cost_basis.get_events(A_BCH).acquisitions assert len(bch_buys) == 2 assert sum(x.remaining_amount for x in bch_buys) == amount_bch assert bch_buys[0].timestamp == 1491593374 assert bch_buys[1].timestamp == 1524937600 bsv_buys = accountant.pots[0].cost_basis.get_events(A_BSV).acquisitions assert len(bsv_buys) == 2 assert sum(x.remaining_amount for x in bsv_buys) == amount_bsv assert bsv_buys[0].timestamp == 1491593374 assert bsv_buys[1].timestamp == 1524937600 assert accountant.pots[0].cost_basis.get_calculated_asset_amount(A_BCH) == amount_bch assert accountant.pots[0].cost_basis.get_calculated_asset_amount(A_BTC) == amount_btc assert accountant.pots[0].cost_basis.get_calculated_asset_amount(A_BSV) == amount_bsv expected_pnls = PnlTotals({ AccountingEventType.TRADE: PNL( taxable=FVal('13877.57646153846153846153846'), free=FVal('-464.4416923076923076923076920'), ), AccountingEventType.FEE: PNL(taxable=FVal('-3.04'), free=ZERO), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def _maybe_add_summary(self, events: List[Dict[str, Any]], pnls: PnlTotals) -> None: """Depending on given settings, adds a few summary lines at the end of the all events PnL report""" if self.settings.pnl_csv_have_summary is False: return length = len(events) + 1 template: Dict[str, Any] = { 'type': '', 'notes': '', 'location': '', 'timestamp': '', 'asset': '', 'free_amount': '', 'taxable_amount': '', 'price': '', 'pnl_taxable': '', 'cost_basis_taxable': '', 'pnl_free': '', 'cost_basis_free': '', } events.append(template) # separate with 2 new lines events.append(template) entry = template.copy() entry['taxable_amount'] = 'TAXABLE' entry['price'] = 'FREE' events.append(entry) start_sums_index = length + 4 sums = 0 for name, value in pnls.items(): if value.taxable == ZERO and value.free == ZERO: continue sums += 1 entry = template.copy() entry['free_amount'] = f'{str(name)} total' entry['taxable_amount'] = self._add_sumif_formula( check_range=f'A2:A{length}', condition=f'"{str(name)}"', sum_range=f'I2:I{length}', actual_value=value.taxable, ) entry['price'] = self._add_sumif_formula( check_range=f'A2:A{length}', condition=f'"{str(name)}"', sum_range=f'J2:J{length}', actual_value=value.free, ) events.append(entry) entry = template.copy() entry['free_amount'] = 'TOTAL' if sums != 0: entry[ 'taxable_amount'] = f'=SUM(G{start_sums_index}:G{start_sums_index+sums-1})' entry[ 'price'] = f'=SUM(H{start_sums_index}:H{start_sums_index+sums-1})' else: entry['taxable_amount'] = entry['price'] = 0 events.append(entry) events.append(template) # separate with 2 new lines events.append(template) version_result = get_current_version(check_for_updates=False) entry = template.copy() entry['free_amount'] = 'rotki version' entry['taxable_amount'] = version_result.our_version events.append(entry) for setting in ACCOUNTING_SETTINGS: entry = template.copy() entry['free_amount'] = setting entry['taxable_amount'] = str(getattr(self.settings, setting)) events.append(entry)
def test_margin_events_affect_gained_lost_amount(accountant, google_service): history = [ Trade( timestamp=1476979735, location=Location.KRAKEN, base_asset=A_BTC, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=FVal(5), rate=FVal('578.505'), fee=FVal('0.0012'), fee_currency=A_BTC, link=None, ), Trade( # 2519.62 - 0.02 - ((0.0012*578.505)/5 + 578.505) timestamp=1476979735, location=Location.KRAKEN, base_asset=A_BTC, quote_asset=A_EUR, trade_type=TradeType.SELL, amount=FVal(1), rate=FVal('2519.62'), fee=FVal('0.02'), fee_currency=A_EUR, link=None, ), ] history += [ MarginPosition( location=Location.POLONIEX, # BTC/EUR: 810.49 open_time=1484438400, # 15/01/2017 close_time=1484629704, # 17/01/2017 profit_loss=FVal('-0.5'), pl_currency=A_BTC, fee=FVal('0.001'), fee_currency=A_BTC, link='1', notes='margin1', ), MarginPosition( location=Location.POLONIEX, # BTC/EUR: 979.39 open_time=1487116800, # 15/02/2017 close_time=1487289600, # 17/02/2017 profit_loss=FVal('0.25'), pl_currency=A_BTC, fee=FVal('0.001'), fee_currency=A_BTC, link='2', notes='margin2', ) ] accounting_history_process( accountant=accountant, start_ts=1436979735, end_ts=1519693374, history_list=history, ) no_message_errors(accountant.msg_aggregator) assert accountant.pots[0].cost_basis.get_calculated_asset_amount( 'BTC').is_close('3.7468') expected_pnls = PnlTotals({ AccountingEventType.TRADE: PNL(taxable=FVal('1940.9761588'), free=ZERO), AccountingEventType.FEE: PNL(taxable=FVal('-1.87166029184'), free=ZERO), AccountingEventType.MARGIN_POSITION: PNL(taxable=FVal('-44.47442060'), free=ZERO), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def test_buying_selling_btc_before_bchfork(accountant, google_service): history = [ Trade( timestamp=1491593374, # 04/07/2017 location=Location.EXTERNAL, base_asset=A_BTC, quote_asset=A_EUR, trade_type=TradeType.BUY, amount=FVal('6.5'), rate=FVal('1128.905'), fee=FVal('0.55'), fee_currency=A_EUR, link=None, ), Trade( # selling BTC prefork should also reduce the BCH equivalent -- taxable timestamp=1500595200, # 21/07/2017 location=Location.EXTERNAL, base_asset=A_BTC, quote_asset=A_EUR, trade_type=TradeType.SELL, amount=FVal('0.5'), rate=FVal('2380.835'), fee=FVal('0.15'), fee_currency=A_EUR, link=None, ), Trade( # selling BCH after the fork -- taxable timestamp=1512693374, # 08/12/2017 location=Location.KRAKEN, base_asset=A_BCH, # cryptocompare hourly BCH/EUR price: 995.935 quote_asset=A_EUR, trade_type=TradeType.SELL, amount=FVal('2.1'), rate=FVal('995.935'), fee=FVal('0.26'), fee_currency=A_EUR, link=None, ), Trade( timestamp=1514937600, # 03/01/2018 location=Location.KRAKEN, base_asset=A_BTC, # cryptocompare hourly BCH/EUR price: 995.935 quote_asset=A_EUR, trade_type=TradeType.SELL, amount=FVal('1.2'), rate=FVal('12404.88'), fee=FVal('0.52'), fee_currency=A_EUR, link=None, ), ] accounting_history_process(accountant, 1436979735, 1519693374, history) no_message_errors(accountant.msg_aggregator) amount_bch = FVal(3.9) amount_btc = FVal(4.8) buys = accountant.pots[0].cost_basis.get_events(A_BCH).acquisitions assert len(buys) == 1 assert buys[0].remaining_amount == amount_bch assert buys[0].timestamp == 1491593374 assert buys[0].rate.is_close('1128.98961538') assert accountant.pots[0].cost_basis.get_calculated_asset_amount(A_BCH) == amount_bch assert accountant.pots[0].cost_basis.get_calculated_asset_amount(A_BTC) == amount_btc expected_pnls = PnlTotals({ AccountingEventType.TRADE: PNL(taxable=FVal('13877.57646153846153846153846'), free=ZERO), AccountingEventType.FEE: PNL(taxable=FVal('-1.48'), free=ZERO), }) check_pnls_and_csv(accountant, expected_pnls, google_service)
def assert_csv_export( accountant: 'Accountant', expected_pnls: PnlTotals, google_service: Optional['GoogleService'] = None, ) -> None: """Test the contents of the csv export match the actual result If google_service exists then it's also uploaded to a sheet to check the formular rendering """ csvexporter = accountant.csvexporter if len(accountant.pots[0].processed_events) == 0: return # nothing to do for no events as no csv is generated with tempfile.TemporaryDirectory() as tmpdirname: tmpdir = Path(tmpdirname) # first make sure we export without formulas csvexporter.settings = csvexporter.settings._replace( pnl_csv_with_formulas=False) accountant.csvexporter.export( events=accountant.pots[0].processed_events, pnls=accountant.pots[0].pnls, directory=tmpdir, ) calculated_pnls = PnlTotals() expected_csv_data = [] with open(tmpdir / FILENAME_ALL_CSV, newline='') as csvfile: reader = csv.DictReader(csvfile) for row in reader: expected_csv_data.append(row) if row['type'] == '': continue # have summaries and reached the end event_type = AccountingEventType.deserialize(row['type']) taxable = FVal(row['pnl_taxable']) free = FVal(row['pnl_free']) if taxable != ZERO or free != ZERO: calculated_pnls[event_type] += PNL(taxable=taxable, free=free) assert_pnl_totals_close(expected_pnls, calculated_pnls) # export with formulas and summary csvexporter.settings = csvexporter.settings._replace( pnl_csv_with_formulas=True, pnl_csv_have_summary=True) # noqa: E501 accountant.csvexporter.export( events=accountant.pots[0].processed_events, pnls=accountant.pots[0].pnls, directory=tmpdir, ) index = CSV_INDEX_OFFSET at_summaries = False to_upload_data = [] with open(tmpdir / FILENAME_ALL_CSV, newline='') as csvfile: reader = csv.DictReader(csvfile) for row in reader: to_upload_data.append(row) if at_summaries: _check_summaries_row(row, accountant) continue if row['type'] == '': at_summaries = True continue # have summaries and reached the end if row['pnl_taxable'] != '0': value = f'G{index}*H{index}' if row['type'] == AccountingEventType.TRADE and 'Amount out' in row[ 'notes']: assert row['pnl_taxable'] == f'={value}-J{index}' elif row['type'] == AccountingEventType.FEE: assert row[ 'pnl_taxable'] == f'={value}+{value}-J{index}' if row['pnl_free'] != '0': value = f'F{index}*H{index}' if row['type'] == AccountingEventType.TRADE and 'Amount out' in row[ 'notes']: assert row['pnl_free'] == f'={value}-L{index}' elif row['type'] == AccountingEventType.FEE: assert row['pnl_free'] == f'={value}+{value}-:{index}' index += 1 if google_service is not None: upload_csv_and_check( service=google_service, csv_data=to_upload_data, expected_csv_data=expected_csv_data, expected_pnls=expected_pnls, )