def test_reduce_asset_amount(accountant): asset = 'BTC' events = accountant.events.events events[asset] = Events(list(), list()) events[asset].buys.append( BuyEvent( amount=FVal(1), timestamp=1446979735, # 08/11/2015 rate=FVal(268.1), fee_rate=FVal(0.0001), ), ) events['BTC'].buys.append( BuyEvent( amount=FVal(1), timestamp=1467378304, # 31/06/2016 rate=FVal(612.45), fee_rate=FVal(0.0019), ), ) events['BTC'].buys.append( BuyEvent( amount=FVal(3), # 25/10/2016 timestamp=1477378304, rate=FVal(603.415), fee_rate=FVal(0.0017), ), ) assert accountant.events.reduce_asset_amount(asset, FVal(1.5)) assert (len(accountant.events.events[asset].buys)) == 2, '1 buy should be used' remaining_amount = accountant.events.events[asset].buys[0].amount assert remaining_amount == FVal(0.5), '0.5 of 2nd buy should remain'
def test_search_buys_calculate_profit_1_buy_consumed_by_1_sell(accountant): """ Assert bought_cost is correct when 1 buy is completely consumed by 1 sell Regression test for part of https://github.com/rotki/rotki/issues/223 """ asset = 'BTC' events = accountant.events.events events[asset] = Events(list(), list()) events[asset].buys.append( BuyEvent( amount=FVal(5), timestamp=1446979735, # 08/11/2015 rate=FVal(268.1), fee_rate=FVal(0.0001), ), ) ( taxable_amount, taxable_bought_cost, taxfree_bought_cost, ) = accountant.events.search_buys_calculate_profit( selling_amount=FVal(5), selling_asset=asset, timestamp=1467378304, # 31/06/2016 ) assert taxable_amount == 5, '5 out of 5 should be taxable (within a year)' assert taxfree_bought_cost.is_close(FVal('0')) assert taxable_bought_cost.is_close(FVal('1340.5005')) assert (len(accountant.events.events[asset].buys)) == 0, 'only buy should have been used'
def add_loan_gain( self, location: Location, gained_asset: Asset, gained_amount: FVal, fee_in_asset: Fee, lent_amount: FVal, open_time: Timestamp, close_time: Timestamp, ) -> None: """Account for gains from the given loan 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 external service. - RemoteError if there is a problem reaching the price oracle server or with reading the response returned by the server """ timestamp = close_time rate = self.get_rate_in_profit_currency(gained_asset, timestamp) if gained_asset not in self.events: self.events[gained_asset] = Events([], []) net_gain_amount = gained_amount - fee_in_asset gain_in_profit_currency = net_gain_amount * rate assert gain_in_profit_currency > 0, "Loan profit is negative. Should never happen" self.events[gained_asset].buys.append( BuyEvent( amount=net_gain_amount, timestamp=timestamp, rate=rate, fee_rate=ZERO, ), ) # count profits if we are inside the query period if timestamp >= self.query_start_ts: log.debug( 'Accounting for loan profit', sensitive_log=True, location=location, gained_asset=gained_asset, gained_amount=gained_amount, gain_in_profit_currency=gain_in_profit_currency, lent_amount=lent_amount, open_time=open_time, close_time=close_time, ) self.loan_profit += gain_in_profit_currency self.csv_exporter.add_loan_profit( location=location, gained_asset=gained_asset, gained_amount=gained_amount, gain_in_profit_currency=gain_in_profit_currency, lent_amount=lent_amount, open_time=open_time, close_time=close_time, )
def test_search_buys_calculate_profit_after_year(accountant): asset = 'BTC' events = accountant.events.events events[asset] = Events(list(), list()) events[asset].buys.append( BuyEvent( amount=FVal(5), timestamp=1446979735, # 08/11/2015 rate=FVal(268.1), fee_rate=FVal(0.0001), ), ) events['BTC'].buys.append( BuyEvent( amount=FVal(15), timestamp=1467378304, # 31/06/2016 rate=FVal(612.45), fee_rate=FVal(0.0019), ), ) events['BTC'].buys.append( BuyEvent( amount=FVal(3), # 25/10/2016 timestamp=1477378304, rate=FVal(603.415), fee_rate=FVal(0.0017), ), ) ( taxable_amount, taxable_bought_cost, taxfree_bought_cost, ) = accountant.events.search_buys_calculate_profit( selling_amount=FVal(8), selling_asset=asset, timestamp=1480683904, # 02/12/2016 ) assert taxable_amount == 3, '3 out of 8 should be taxable (within a year)' assert taxfree_bought_cost.is_close(FVal('1340.5005')) assert taxable_bought_cost.is_close(FVal('1837.3557')) assert (len(accountant.events.events[asset].buys)) == 2, 'first buy should have been used' remaining_amount = accountant.events.events[asset].buys[0].amount assert remaining_amount == FVal(12), '3 of 15 should have been consumed'
def add_margin_position( self, gain_loss_asset: Asset, gain_loss_amount: FVal, fee_in_asset: Fee, margin_notes: str, timestamp: Timestamp, ) -> None: rate = self.get_rate_in_profit_currency(gain_loss_asset, timestamp) if gain_loss_asset not in self.events: self.events[gain_loss_asset] = Events(list(), list()) net_gain_loss_amount = gain_loss_amount - fee_in_asset gain_loss_in_profit_currency = net_gain_loss_amount * rate if net_gain_loss_amount > 0: self.events[gain_loss_asset].buys.append( BuyEvent( amount=net_gain_loss_amount, timestamp=timestamp, rate=rate, fee_rate=ZERO, ), ) elif net_gain_loss_amount < 0: result = self.reduce_asset_amount( asset=gain_loss_asset, amount=-gain_loss_amount, ) if not result: log.critical( f'No documented buy found for {gain_loss_asset} before ' f'{timestamp_to_date(timestamp, formatstr="%d/%m/%Y %H:%M:%S")}', ) # count profit/loss if we are inside the query period if timestamp >= self.query_start_ts: self.margin_positions_profit_loss += gain_loss_in_profit_currency log.debug( 'Accounting for margin position', sensitive_log=True, notes=margin_notes, gain_loss_asset=gain_loss_asset, net_gain_loss_amount=net_gain_loss_amount, gain_loss_in_profit_currency=gain_loss_in_profit_currency, timestamp=timestamp, ) self.csv_exporter.add_margin_position( margin_notes=margin_notes, gain_loss_asset=gain_loss_asset, net_gain_loss_amount=net_gain_loss_amount, gain_loss_in_profit_currency=gain_loss_in_profit_currency, timestamp=timestamp, )
def test_search_buys_calculate_profit_1_buy_used_by_2_sells_taxable( accountant): """ Make sure that when 1 buy is used by 2 sells bought cost is correct Regression test for taxable part of: https://github.com/rotkehlchenio/rotkehlchen/issues/223 """ asset = 'BTC' events = accountant.events.events events[asset] = Events(list(), list()) events[asset].buys.append( BuyEvent( amount=FVal(5), timestamp=1446979735, # 08/11/2015 rate=FVal(268.1), fee_rate=FVal(0.0001), ), ) ( taxable_amount, taxable_bought_cost, taxfree_bought_cost, ) = accountant.events.search_buys_calculate_profit( selling_amount=FVal(3), selling_asset=asset, timestamp=1467378304, # 31/06/2016 ) assert taxable_amount == 3, '3 out of 3 should be taxable (within a year)' assert taxfree_bought_cost.is_close(FVal('0')) assert taxable_bought_cost.is_close(FVal('804.3003')) assert (len( accountant.events.events[asset].buys)) == 1, 'whole buy was not used' remaining_amount = accountant.events.events[asset].buys[0].amount assert remaining_amount == FVal(2), '3 of 5 should have been consumed' # now eat up all the rest ( taxable_amount, taxable_bought_cost, taxfree_bought_cost, ) = accountant.events.search_buys_calculate_profit( selling_amount=FVal(2), selling_asset=asset, timestamp=1467378404, # bit after previous sell ) assert taxable_amount == 2, '2 out of 2 should be taxable (within a year)' assert taxfree_bought_cost.is_close(FVal('0')) assert taxable_bought_cost.is_close(FVal('536.2002')) assert (len(accountant.events.events[asset].buys) ) == 0, 'the buy should have been used'
def test_reduce_asset_amount_more_that_bought(accountant): asset = 'BTC' events = accountant.events.events events[asset] = Events(list(), list()) events[asset].buys.append( BuyEvent( amount=FVal(1), timestamp=1446979735, # 08/11/2015 rate=FVal(268.1), fee_rate=FVal(0.0001), ), ) events['BTC'].buys.append( BuyEvent( amount=FVal(1), timestamp=1467378304, # 31/06/2016 rate=FVal(612.45), fee_rate=FVal(0.0019), ), ) assert not accountant.events.reduce_asset_amount(asset, FVal(3)) assert (len( accountant.events.events[asset].buys)) == 0, 'all buys should be used'
def test_search_buys_calculate_profit_sell_more_than_bought_after_year(accountant): asset = 'BTC' events = accountant.events.events events[asset] = Events(list(), list()) events[asset].buys.append( BuyEvent( amount=FVal(1), timestamp=1446979735, # 08/11/2015 rate=FVal(268.1), fee_rate=FVal(0.0001), ), ) events['BTC'].buys.append( BuyEvent( amount=FVal(1), timestamp=1467378304, # 31/06/2016 rate=FVal(612.45), fee_rate=FVal(0.0019), ), ) ( taxable_amount, taxable_bought_cost, taxfree_bought_cost, ) = accountant.events.search_buys_calculate_profit( selling_amount=FVal(3), selling_asset=asset, timestamp=1523399409, # 10/04/2018 ) assert taxable_amount == 1, '1 out of 3 should be taxable (after a year)' assert taxfree_bought_cost.is_close(FVal('880.552')) assert taxable_bought_cost.is_close(FVal('0')) assert (len(accountant.events.events[asset].buys)) == 0, 'only buy should have been used'
def add_loan_gain( self, gained_asset: Asset, gained_amount: FVal, fee_in_asset: Fee, lent_amount: FVal, open_time: Timestamp, close_time: Timestamp, ) -> None: timestamp = close_time rate = self.get_rate_in_profit_currency(gained_asset, timestamp) if gained_asset not in self.events: self.events[gained_asset] = Events(list(), list()) net_gain_amount = gained_amount - fee_in_asset gain_in_profit_currency = net_gain_amount * rate assert gain_in_profit_currency > 0, "Loan profit is negative. Should never happen" self.events[gained_asset].buys.append( BuyEvent( amount=net_gain_amount, timestamp=timestamp, rate=rate, fee_rate=ZERO, ), ) # count profits if we are inside the query period if timestamp >= self.query_start_ts: log.debug( 'Accounting for loan profit', sensitive_log=True, gained_asset=gained_asset, gained_amount=gained_amount, gain_in_profit_currency=gain_in_profit_currency, lent_amount=lent_amount, open_time=open_time, close_time=close_time, ) self.loan_profit += gain_in_profit_currency self.csv_exporter.add_loan_profit( gained_asset=gained_asset, gained_amount=gained_amount, gain_in_profit_currency=gain_in_profit_currency, lent_amount=lent_amount, open_time=open_time, close_time=close_time, )
def add_ledger_action(self, action: LedgerAction) -> None: log.debug( 'Accounting for LedgerAction', sensitive_log=True, action=action, ) # should never happen, should be stopped at the main loop assert action.timestamp <= self.query_end_ts, ( 'Ledger action time > query_end_ts found in processing') rate = self.get_rate_in_profit_currency(action.asset, action.timestamp) profit_loss = action.amount * rate if action.asset not in self.events: self.events[action.asset] = Events([], []) if action.is_profitable(): if action.timestamp > self.query_start_ts: self.ledger_actions_profit_loss += profit_loss self.events[action.asset].buys.append( BuyEvent( amount=action.amount, timestamp=action.timestamp, rate=rate, fee_rate=ZERO, ), ) else: if action.timestamp > self.query_start_ts: self.ledger_actions_profit_loss -= profit_loss result = self.reduce_asset_amount( asset=action.asset, amount=action.amount, ) if not result: log.critical( f'No documented buy found for {action.asset} before ' f'{self.csv_exporter.timestamp_to_date(action.timestamp)}', ) if action.timestamp > self.query_start_ts: self.csv_exporter.add_ledger_action( action=action, profit_loss_in_profit_currency=profit_loss, )
def add_margin_position(self, margin: MarginPosition) -> None: """Account for the given margin position May raise: - PriceQueryUnknownFromAsset if the from asset is known to miss from cryptocompare - NoPriceForGivenTimestamp if we can't find a price for the asset in the given timestamp from the external service. - RemoteError if there is a problem reaching the price oracle server or with reading the response returned by the server """ if margin.pl_currency not in self.events: self.events[margin.pl_currency] = Events([], []) if margin.fee_currency not in self.events: self.events[margin.fee_currency] = Events([], []) pl_currency_rate = self.get_rate_in_profit_currency( margin.pl_currency, margin.close_time) fee_currency_rate = self.get_rate_in_profit_currency( margin.pl_currency, margin.close_time) net_gain_loss_in_profit_currency = ( margin.profit_loss * pl_currency_rate - margin.fee * fee_currency_rate) # Add or remove to the pl_currency asset if margin.profit_loss > 0: self.events[margin.pl_currency].buys.append( BuyEvent( amount=margin.profit_loss, timestamp=margin.close_time, rate=pl_currency_rate, fee_rate=ZERO, ), ) elif margin.profit_loss < 0: result = self.reduce_asset_amount( asset=margin.pl_currency, amount=-margin.profit_loss, ) if not result: log.critical( f'No documented buy found for {margin.pl_currency} before ' f'{timestamp_to_date(margin.close_time, formatstr="%d/%m/%Y %H:%M:%S")}', ) # Reduce the fee_currency asset result = self.reduce_asset_amount(asset=margin.fee_currency, amount=margin.fee) if not result: log.critical( f'No documented buy found for {margin.fee_currency} before ' f'{timestamp_to_date(margin.close_time, formatstr="%d/%m/%Y %H:%M:%S")}', ) # count profit/loss if we are inside the query period if margin.close_time >= self.query_start_ts: self.margin_positions_profit_loss += net_gain_loss_in_profit_currency log.debug( 'Accounting for margin position', sensitive_log=True, notes=margin.notes, gain_loss_asset=margin.pl_currency, gain_loss_amount=margin.profit_loss, net_gain_loss_in_profit_currency= net_gain_loss_in_profit_currency, timestamp=margin.close_time, ) self.csv_exporter.add_margin_position( margin_notes=margin.notes, gain_loss_asset=margin.pl_currency, gain_loss_amount=margin.profit_loss, gain_loss_in_profit_currency=net_gain_loss_in_profit_currency, timestamp=margin.close_time, )
def add_buy( self, bought_asset: Asset, bought_amount: FVal, paid_with_asset: Asset, trade_rate: FVal, fee_in_profit_currency: Fee, fee_currency: Asset, timestamp: Timestamp, is_virtual: bool = False, ) -> None: """ Account for the given buy May raise: - PriceQueryUnknownFromAsset if the from asset is known to miss from cryptocompare - NoPriceForGivenTimestamp if we can't find a price for the asset in the given timestamp from cryptocompare - RemoteError if there is a problem reaching the price oracle server or with reading the response returned by the server """ paid_with_asset_rate = self.get_rate_in_profit_currency( paid_with_asset, timestamp) buy_rate = paid_with_asset_rate * trade_rate self.handle_prefork_asset_buys( bought_asset=bought_asset, bought_amount=bought_amount, paid_with_asset=paid_with_asset, trade_rate=trade_rate, fee_in_profit_currency=fee_in_profit_currency, fee_currency=fee_currency, timestamp=timestamp, ) if bought_asset not in self.events: self.events[bought_asset] = Events([], []) gross_cost = bought_amount * buy_rate cost_in_profit_currency = gross_cost + fee_in_profit_currency self.events[bought_asset].buys.append( BuyEvent( amount=bought_amount, timestamp=timestamp, rate=buy_rate, fee_rate=fee_in_profit_currency / bought_amount, ), ) log.debug( 'Buy Event', sensitive_log=True, bought_amount=bought_amount, bought_asset=bought_asset, paid_with_asset=paid_with_asset, rate=trade_rate, rate_in_profit_currency=buy_rate, profit_currency=self.profit_currency, timestamp=timestamp, ) if timestamp >= self.query_start_ts: self.csv_exporter.add_buy( bought_asset=bought_asset, rate=buy_rate, fee_cost=fee_in_profit_currency, amount=bought_amount, cost=cost_in_profit_currency, paid_with_asset=paid_with_asset, paid_with_asset_rate=paid_with_asset_rate, timestamp=timestamp, is_virtual=is_virtual, )
def add_buy( self, location: Location, bought_asset: Asset, bought_amount: FVal, paid_with_asset: Asset, trade_rate: FVal, fee_in_profit_currency: Fee, fee_currency: Asset, timestamp: Timestamp, is_virtual: bool = False, ) -> None: """ Account for the given buy May raise: - PriceQueryUnsupportedAsset if from/to asset is missing from all price oracles - NoPriceForGivenTimestamp if we can't find a price for the asset in the given timestamp from cryptocompare - RemoteError if there is a problem reaching the price oracle server or with reading the response returned by the server """ skip_trade = (not self.include_crypto2crypto and not bought_asset.is_fiat() and not paid_with_asset.is_fiat()) if skip_trade: return logger.debug( f'Processing buy trade of {bought_asset.identifier} with ' f'{paid_with_asset.identifier} at {timestamp}', ) paid_with_asset_rate = self.get_rate_in_profit_currency( paid_with_asset, timestamp) buy_rate = paid_with_asset_rate * trade_rate self.handle_prefork_asset_buys( location=location, bought_asset=bought_asset, bought_amount=bought_amount, paid_with_asset=paid_with_asset, trade_rate=trade_rate, fee_in_profit_currency=fee_in_profit_currency, fee_currency=fee_currency, timestamp=timestamp, ) if bought_asset not in self.events: self.events[bought_asset] = Events([], []) gross_cost = bought_amount * buy_rate cost_in_profit_currency = gross_cost + fee_in_profit_currency self.events[bought_asset].buys.append( BuyEvent( amount=bought_amount, timestamp=timestamp, rate=buy_rate, fee_rate=fee_in_profit_currency / bought_amount, ), ) log.debug( 'Buy Event', sensitive_log=True, location=str(location), bought_amount=bought_amount, bought_asset=bought_asset, paid_with_asset=paid_with_asset, rate=trade_rate, rate_in_profit_currency=buy_rate, profit_currency=self.profit_currency, timestamp=timestamp, ) if timestamp >= self.query_start_ts: self.csv_exporter.add_buy( location=location, bought_asset=bought_asset, rate=buy_rate, fee_cost=fee_in_profit_currency, amount=bought_amount, cost=cost_in_profit_currency, paid_with_asset=paid_with_asset, paid_with_asset_rate=paid_with_asset_rate, timestamp=timestamp, is_virtual=is_virtual, )
def add_buy( self, bought_asset: Asset, bought_amount: FVal, paid_with_asset: Asset, trade_rate: FVal, fee_in_profit_currency: Fee, fee_currency: Asset, timestamp: Timestamp, is_virtual: bool = False, ) -> None: paid_with_asset_rate = self.get_rate_in_profit_currency( paid_with_asset, timestamp) buy_rate = paid_with_asset_rate * trade_rate self.handle_prefork_asset_buys( bought_asset=bought_asset, bought_amount=bought_amount, paid_with_asset=paid_with_asset, trade_rate=trade_rate, fee_in_profit_currency=fee_in_profit_currency, fee_currency=fee_currency, timestamp=timestamp, ) if bought_asset not in self.events: self.events[bought_asset] = Events(list(), list()) gross_cost = bought_amount * buy_rate cost_in_profit_currency = gross_cost + fee_in_profit_currency self.events[bought_asset].buys.append( BuyEvent( amount=bought_amount, timestamp=timestamp, rate=buy_rate, fee_rate=fee_in_profit_currency / bought_amount, ), ) log.debug( 'Buy Event', sensitive_log=True, bought_amount=bought_amount, bought_asset=bought_asset, paid_with_asset=paid_with_asset, rate=trade_rate, rate_in_profit_currency=buy_rate, profit_currency=self.profit_currency, timestamp=timestamp, ) if timestamp >= self.query_start_ts: self.csv_exporter.add_buy( bought_asset=bought_asset, rate=buy_rate, fee_cost=fee_in_profit_currency, amount=bought_amount, cost=cost_in_profit_currency, paid_with_asset=paid_with_asset, paid_with_asset_rate=paid_with_asset_rate, timestamp=timestamp, is_virtual=is_virtual, )
def add_margin_position(self, margin: MarginPosition) -> None: if margin.pl_currency not in self.events: self.events[margin.pl_currency] = Events(list(), list()) if margin.fee_currency not in self.events: self.events[margin.fee_currency] = Events(list(), list()) pl_currency_rate = self.get_rate_in_profit_currency( margin.pl_currency, margin.close_time) fee_currency_rate = self.get_rate_in_profit_currency( margin.pl_currency, margin.close_time) net_gain_loss_in_profit_currency = ( margin.profit_loss * pl_currency_rate - margin.fee * fee_currency_rate) # Add or remove to the pl_currency asset if margin.profit_loss > 0: self.events[margin.pl_currency].buys.append( BuyEvent( amount=margin.profit_loss, timestamp=margin.close_time, rate=pl_currency_rate, fee_rate=ZERO, ), ) elif margin.profit_loss < 0: result = self.reduce_asset_amount( asset=margin.pl_currency, amount=-margin.profit_loss, ) if not result: log.critical( f'No documented buy found for {margin.pl_currency} before ' f'{timestamp_to_date(margin.close_time, formatstr="%d/%m/%Y %H:%M:%S")}', ) # Reduce the fee_currency asset result = self.reduce_asset_amount(asset=margin.fee_currency, amount=margin.fee) if not result: log.critical( f'No documented buy found for {margin.fee_currency} before ' f'{timestamp_to_date(margin.close_time, formatstr="%d/%m/%Y %H:%M:%S")}', ) # count profit/loss if we are inside the query period if margin.close_time >= self.query_start_ts: self.margin_positions_profit_loss += net_gain_loss_in_profit_currency log.debug( 'Accounting for margin position', sensitive_log=True, notes=margin.notes, gain_loss_asset=margin.pl_currency, gain_loss_amount=margin.profit_loss, net_gain_loss_in_profit_currency= net_gain_loss_in_profit_currency, timestamp=margin.close_time, ) self.csv_exporter.add_margin_position( margin_notes=margin.notes, gain_loss_asset=margin.pl_currency, gain_loss_amount=margin.profit_loss, gain_loss_in_profit_currency=net_gain_loss_in_profit_currency, timestamp=margin.close_time, )