def add_loan_profit( self, gained_asset: typing.Asset, gained_amount: FVal, gain_in_profit_currency: FVal, lent_amount: FVal, open_time: typing.Timestamp, close_time: typing.Timestamp, ) -> None: if not self.create_csv: return self.loan_profits_csv.append({ 'open_time': tsToDate(open_time, formatstr='%d/%m/%Y %H:%M:%S'), 'close_time': tsToDate(close_time, formatstr='%d/%m/%Y %H:%M:%S'), 'gained_asset': gained_asset, 'gained_amount': gained_amount, 'lent_amount': lent_amount, 'profit_in_{}'.format(self.profit_currency): gain_in_profit_currency }) self.add_to_allevents( event_type=EV_INTEREST_PAYMENT, paid_in_profit_currency=FVal(0), paid_asset=S_EMPTYSTR, paid_in_asset=FVal(0), received_asset=gained_asset, received_in_asset=gained_amount, received_in_profit_currency=gain_in_profit_currency, timestamp=close_time, )
def query_fiat_pair(self, base: FiatAsset, quote: FiatAsset) -> FVal: if base == quote: return FVal(1.0) now = ts_now() date = tsToDate(ts_now(), formatstr='%Y-%m-%d') price = self._get_cached_forex_data(date, base, quote) if price: return price price = _query_exchanges_rateapi(base, quote) if not price: price = _query_currency_converterapi(base, quote) if not price: # Search the cache for any price in the last month for i in range(1, 31): now = Timestamp(now - Timestamp(86401)) date = tsToDate(now, formatstr='%Y-%m-%d') price = self._get_cached_forex_data(date, base, quote) if price: log.debug( f'Could not query online apis for a fiat price. ' f'Used cached value from {i} days ago.', base_currency=base, quote_currency=quote, price=price, ) return price raise ValueError('Could not find a "{}" price for "{}"'.format( base, quote)) self._save_forex_rate(date, base, quote, price) return price
def add_loan_settlement( self, asset, amount, rate_in_profit_currency, total_fee_in_profit_currency, timestamp, ): if not self.create_csv: return self.loan_settlements_csv.append({ 'asset': asset, "amount": amount, "price_in_{}".format(self.profit_currency): rate_in_profit_currency, "fee_in_{}".format(self.profit_currency): total_fee_in_profit_currency, "time": tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), }) self.add_to_allevents( event_type='loan_settlement', paid_in_profit_currency=amount * rate_in_profit_currency + total_fee_in_profit_currency, paid_asset=asset, paid_in_asset=amount, received_asset='', received_in_asset=0, received_in_profit_currency=0, timestamp=timestamp, )
def add_tx_gas_cost( self, transaction_hash: bytes, eth_burned_as_gas: FVal, rate: FVal, timestamp: typing.Timestamp, ) -> None: if not self.create_csv: return self.tx_gas_costs_csv.append({ 'time': tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), 'transaction_hash': transaction_hash, 'eth_burned_as_gas': eth_burned_as_gas, 'cost_in_{}'.format(self.profit_currency): eth_burned_as_gas * rate, }) self.add_to_allevents( event_type=EV_TX_GAS_COST, paid_in_profit_currency=eth_burned_as_gas * rate, paid_asset=S_ETH, paid_in_asset=eth_burned_as_gas, received_asset=S_EMPTYSTR, received_in_asset=FVal(0), taxable_received_in_profit_currency=FVal(0), timestamp=timestamp, )
def add_margin_position( self, margin_notes: str, gain_loss_asset: typing.Asset, net_gain_loss_amount: FVal, gain_loss_in_profit_currency: FVal, timestamp: typing.Timestamp, ) -> None: if not self.create_csv: return self.margin_positions_csv.append({ 'name': margin_notes, 'time': tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), 'gain_loss_asset': gain_loss_asset, 'gain_loss_amount': net_gain_loss_amount, 'profit_loss_in_{}'.format(self.profit_currency): gain_loss_in_profit_currency, }) self.add_to_allevents( event_type=EV_MARGIN_CLOSE, paid_in_profit_currency=FVal(0), paid_asset=S_EMPTYSTR, paid_in_asset=FVal(0), received_asset=gain_loss_asset, received_in_asset=net_gain_loss_amount, taxable_received_in_profit_currency=gain_loss_in_profit_currency, timestamp=timestamp, )
def add_margin_position( self, margin_notes, gained_asset, net_gain_amount, gain_in_profit_currency, timestamp, ): if not self.create_csv: return self.margin_positions_csv.append({ 'name': margin_notes, 'time': tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), 'gained_asset': gained_asset, 'gained_amount': net_gain_amount, 'profit_in_{}'.format(self.profit_currency): gain_in_profit_currency }) self.add_to_allevents( event_type='margin_position_close', paid_in_profit_currency=0, paid_asset='', paid_in_asset=0, received_asset=gained_asset, received_in_asset=net_gain_amount, received_in_profit_currency=gain_in_profit_currency, timestamp=timestamp, )
def add_tx_gas_cost( self, transaction_hash, eth_burned_as_gas, rate, timestamp, ): if not self.create_csv: return self.tx_gas_costs_csv.append({ 'time': tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), 'transaction_hash': transaction_hash, 'eth_burned_as_gas': eth_burned_as_gas, 'cost_in_{}'.format(self.profit_currency): eth_burned_as_gas * rate, }) self.add_to_allevents( event_type='tx_gas_cost', paid_in_profit_currency=eth_burned_as_gas * rate, paid_asset='ETH', paid_in_asset=eth_burned_as_gas, received_asset='', received_in_asset=0, received_in_profit_currency=0, timestamp=timestamp, )
def add_asset_movement( self, exchange: str, category: str, asset: typing.Asset, fee: FVal, rate: FVal, timestamp: typing.Timestamp, ) -> None: if not self.create_csv: return self.asset_movements_csv.append({ 'time': tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), 'exchange': exchange, 'type': category, 'moving_asset': asset, 'fee_in_asset': fee, 'fee_in_{}'.format(self.profit_currency): fee * rate, }) self.add_to_allevents( event_type=EV_ASSET_MOVE, paid_in_profit_currency=fee * rate, paid_asset=asset, paid_in_asset=fee, received_asset=S_EMPTYSTR, received_in_asset=FVal(0), received_in_profit_currency=FVal(0), timestamp=timestamp, )
def add_loan_settlement( self, asset: typing.Asset, amount: FVal, rate_in_profit_currency: FVal, total_fee_in_profit_currency: FVal, timestamp: typing.Timestamp, ) -> None: if not self.create_csv: return self.loan_settlements_csv.append({ 'asset': asset, "amount": amount, "price_in_{}".format(self.profit_currency): rate_in_profit_currency, "fee_in_{}".format(self.profit_currency): total_fee_in_profit_currency, "time": tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), }) paid_in_profit_currency = amount * rate_in_profit_currency + total_fee_in_profit_currency self.add_to_allevents( event_type=EV_LOAN_SETTLE, paid_in_profit_currency=paid_in_profit_currency, paid_asset=asset, paid_in_asset=amount, received_asset=S_EMPTYSTR, received_in_asset=FVal(0), received_in_profit_currency=FVal(0), timestamp=timestamp, )
def add_buy(self, bought_asset, bought_amount, paid_with_asset, trade_rate, trade_fee, fee_currency, timestamp, is_virtual=False): paid_with_asset_rate = self.get_rate_in_profit_currency( paid_with_asset, timestamp) buy_rate = paid_with_asset_rate * trade_rate fee_price_in_profit_currency = 0 if trade_fee != 0: fee_price_in_profit_currency = self.price_historian.query_historical_price( fee_currency, self.profit_currency, timestamp) self.handle_prefork_acquisitions(bought_asset=bought_asset, bought_amount=bought_amount, paid_with_asset=paid_with_asset, trade_rate=trade_rate, trade_fee=trade_fee, fee_currency=fee_currency, timestamp=timestamp) if bought_asset not in self.events: self.events[bought_asset] = Events(list(), list()) fee_cost = fee_price_in_profit_currency * trade_fee gross_cost = bought_amount * buy_rate cost = gross_cost + fee_cost self.events[bought_asset].buys.append( BuyEvent(amount=bought_amount, timestamp=timestamp, rate=buy_rate, fee_rate=fee_cost / bought_amount, cost=cost)) if logger.isEnabledFor(logging.DEBUG): logger.debug( 'Buying {} "{}" for {} "{}" ({} "{}" per "{}" or {} "{}" per ' '"{}") at {}'.format( bought_amount, bought_asset, bought_amount * trade_rate, paid_with_asset, trade_rate, paid_with_asset, bought_asset, buy_rate, self.profit_currency, bought_asset, tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'))) self.csv_exporter.add_buy( bought_asset=bought_asset, rate=buy_rate, fee_cost=fee_cost, amount=bought_amount, gross_cost=gross_cost, cost=cost, paid_with_asset=paid_with_asset, paid_with_asset_rate=paid_with_asset_rate, timestamp=timestamp, is_virtual=is_virtual, )
def add_to_allevents( self, event_type, paid_in_profit_currency, paid_asset, paid_in_asset, received_asset, received_in_asset, received_in_profit_currency, timestamp, is_virtual=False, taxable_amount='', taxable_bought_cost='', ): row = len(self.all_events_csv) + 2 if event_type == 'buy': net_profit_or_loss = 0 # no profit by buying net_profit_or_loss_csv = 0 # no profit by buying elif event_type == 'sell': net_profit_or_loss = 0 if taxable_amount == 0 else received_in_asset - taxable_bought_cost net_profit_or_loss_csv = '=IF(E{}=0,0,H{}-F{})'.format( row, row, row) elif event_type in ('tx_gas_cost', 'asset_movement', 'loan_settlement'): net_profit_or_loss = paid_in_profit_currency net_profit_or_loss_csv = '=-B{}'.format(row) elif event_type in ('interest_rate_payment', 'margin_position_close'): net_profit_or_loss = received_in_profit_currency net_profit_or_loss_csv = '=H{}'.format(row) else: raise ValueError( 'Illegal event type "{}" at add_to_allevents'.format( event_type)) entry = { 'type': event_type, 'paid_in_profit_currency': paid_in_profit_currency, 'paid_asset': paid_asset, 'paid_in_asset': paid_in_asset, 'taxable_amount': taxable_amount, 'taxable_bought_cost': taxable_bought_cost, 'received_asset': received_asset, 'received_in_profit_currency': received_in_profit_currency, 'received_in_asset': received_in_asset, 'net_profit_or_loss': net_profit_or_loss, 'time': timestamp, 'is_virtual': is_virtual } self.all_events.append(entry) new_entry = entry.copy() new_entry['net_profit_or_loss'] = net_profit_or_loss_csv new_entry['time'] = tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), new_entry['paid_in_{}'.format( self.profit_currency)] = paid_in_profit_currency new_entry['received_in_{}'.format( self.profit_currency)] = received_in_profit_currency del new_entry['paid_in_profit_currency'] del new_entry['received_in_profit_currency'] self.all_events_csv.append(new_entry)
def add_buy( self, bought_asset: typing.Asset, rate: FVal, fee_cost: typing.Fee, amount: FVal, gross_cost: FVal, cost: FVal, paid_with_asset: typing.Asset, paid_with_asset_rate: FVal, timestamp: typing.Timestamp, is_virtual: bool, ) -> None: if not self.create_csv: return exchange_rate_key = f'exchanged_asset_{self.profit_currency}_exchange_rate' self.trades_csv.append({ 'type': 'buy', 'asset': bought_asset, 'price_in_{}'.format(self.profit_currency): rate, 'fee_in_{}'.format(self.profit_currency): fee_cost, 'gained_or_invested_{}'.format(self.profit_currency): cost, 'amount': amount, 'taxable_amount': 'not applicable', # makes no difference for buying 'exchanged_for': paid_with_asset, exchange_rate_key: paid_with_asset_rate, 'taxable_bought_cost_in_{}'.format(self.profit_currency): 'not applicable', 'taxable_gain_in_{}'.format(self.profit_currency): FVal(0), 'taxable_profit_loss_in_{}'.format(self.profit_currency): FVal(0), 'time': tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), 'is_virtual': is_virtual, }) self.add_to_allevents( event_type=EV_BUY, paid_in_profit_currency=cost, paid_asset=self.profit_currency, paid_in_asset=cost, received_asset=bought_asset, received_in_asset=amount, taxable_received_in_profit_currency=FVal(0), timestamp=timestamp, is_virtual=is_virtual, )
def add_to_allevents( self, event_type: typing.EventType, paid_in_profit_currency: FVal, paid_asset: Union[typing.Asset, typing.EmptyStr], paid_in_asset: FVal, received_asset: Union[typing.Asset, typing.EmptyStr], received_in_asset: FVal, received_in_profit_currency: FVal, timestamp: typing.Timestamp, is_virtual: bool = False, taxable_amount: FVal = FVal(0), taxable_bought_cost: FVal = FVal(0), ) -> None: row = len(self.all_events_csv) + 2 if event_type == EV_BUY: net_profit_or_loss = FVal(0) # no profit by buying net_profit_or_loss_csv = '0' # no profit by buying elif event_type == EV_SELL: if taxable_amount == 0: net_profit_or_loss = FVal(0) else: net_profit_or_loss = received_in_asset - taxable_bought_cost net_profit_or_loss_csv = '=IF(E{}=0,0,H{}-F{})'.format(row, row, row) elif event_type in (EV_TX_GAS_COST, EV_ASSET_MOVE, EV_LOAN_SETTLE): net_profit_or_loss = paid_in_profit_currency net_profit_or_loss_csv = '=-B{}'.format(row) elif event_type in (EV_INTEREST_PAYMENT, EV_MARGIN_CLOSE): net_profit_or_loss = received_in_profit_currency net_profit_or_loss_csv = '=H{}'.format(row) else: raise ValueError('Illegal event type "{}" at add_to_allevents'.format(event_type)) entry = { 'type': event_type, 'paid_in_profit_currency': paid_in_profit_currency, 'paid_asset': paid_asset, 'paid_in_asset': paid_in_asset, 'taxable_amount': taxable_amount, 'taxable_bought_cost': taxable_bought_cost, 'received_asset': received_asset, 'received_in_profit_currency': received_in_profit_currency, 'received_in_asset': received_in_asset, 'net_profit_or_loss': net_profit_or_loss, 'time': timestamp, 'is_virtual': is_virtual } log.debug('csv event', **make_sensitive(entry)) self.all_events.append(entry) new_entry = entry.copy() new_entry['net_profit_or_loss'] = net_profit_or_loss_csv new_entry['time'] = tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S') new_entry['paid_in_{}'.format(self.profit_currency)] = paid_in_profit_currency new_entry['received_in_{}'.format(self.profit_currency)] = received_in_profit_currency del new_entry['paid_in_profit_currency'] del new_entry['received_in_profit_currency'] self.all_events_csv.append(new_entry)
def handle_prefork_asset_sells( self, sold_asset: Asset, sold_amount: FVal, timestamp: Timestamp, ) -> None: if sold_asset == S_ETH and timestamp < ETH_DAO_FORK_TS: if not self.reduce_asset_amount(asset=S_ETC, amount=sold_amount): log.critical( 'No documented buy found for ETC (ETH equivalent) before {}' .format(tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), ), ) if sold_asset == S_BTC and timestamp < BTC_BCH_FORK_TS: if not self.reduce_asset_amount(asset=S_BCH, amount=sold_amount): log.critical( 'No documented buy found for BCH (BTC equivalent) before {}' .format(tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), ), )
def test_fallback_to_cached_values_within_a_month(inquirer): def mock_api_remote_fail(uri): return MockResponse(500, '{"msg": "shit hit the fan"') # Get a date 15 days ago and insert a cached entry for EUR JPY then now = ts_now() eurjpy_val = FVal('124.123') date = tsToDate(now - 86400 * 15, formatstr='%Y-%m-%d') inquirer._save_forex_rate(date, 'EUR', 'JPY', eurjpy_val) # Get a date 31 days ago and insert a cache entry for EUR CNY then date = tsToDate(now - 86400 * 31, formatstr='%Y-%m-%d') inquirer._save_forex_rate(date, 'EUR', 'CNY', FVal('7.719')) with patch('requests.get', side_effect=mock_api_remote_fail): # We fail to find a response but then go back 15 days and find the cached response result = inquirer.query_fiat_pair('EUR', 'JPY') assert result == eurjpy_val # The cached response for EUR CNY is too old so we will fail here with pytest.raises(ValueError): result = inquirer.query_fiat_pair('EUR', 'CNY')
def add_sell( self, selling_asset: typing.Asset, rate_in_profit_currency: FVal, total_fee_in_profit_currency: FVal, gain_in_profit_currency: FVal, selling_amount: FVal, receiving_asset: typing.Asset, receiving_amount: FVal, receiving_asset_rate_in_profit_currency: FVal, taxable_amount: FVal, taxable_bought_cost: FVal, timestamp: typing.Timestamp, is_virtual: bool, ): if not self.create_csv: return gross_key = 'gross_gained_or_invested_{}'.format(self.profit_currency) self.trades_csv.append({ 'type': 'sell', 'asset': selling_asset, 'price_in_{}'.format(self.profit_currency): rate_in_profit_currency, 'fee_in_{}'.format(self.profit_currency): total_fee_in_profit_currency, gross_key: gain_in_profit_currency + total_fee_in_profit_currency, 'net_gained_or_invested_{}'.format(self.profit_currency): gain_in_profit_currency, 'amount': selling_amount, 'exchanged_for': receiving_asset, 'exchanged_asset_euro_exchange_rate': receiving_asset_rate_in_profit_currency, 'time': tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), 'is_virtual': is_virtual, }) paid_in_profit_currency = ( selling_amount * rate_in_profit_currency + total_fee_in_profit_currency ) self.add_to_allevents( event_type=EV_SELL, paid_in_profit_currency=paid_in_profit_currency, paid_asset=selling_asset, paid_in_asset=selling_amount, received_asset=receiving_asset, received_in_asset=receiving_amount, received_in_profit_currency=taxable_gain_for_sell( taxable_amount=taxable_amount, rate_in_profit_currency=rate_in_profit_currency, total_fee_in_profit_currency=total_fee_in_profit_currency, selling_amount=selling_amount, ), timestamp=timestamp, is_virtual=is_virtual, taxable_amount=taxable_amount, taxable_bought_cost=taxable_bought_cost, )
def query_historical_fiat_exchange_rates( self, from_currency: FiatAsset, to_currency: FiatAsset, timestamp: Timestamp, ) -> Optional[FVal]: date = tsToDate(timestamp, formatstr='%Y-%m-%d') rate = self._get_cached_forex_data(date, from_currency, to_currency) if rate: return rate log.debug( 'Querying exchangeratesapi', from_currency=from_currency, to_currency=to_currency, timestamp=timestamp, ) query_str = (f'https://api.exchangeratesapi.io/{date}?' f'base={from_currency}') resp = retry_calls( 5, 'query_exchangeratesapi', 'requests.get', requests.get, query_str, ) if resp.status_code != 200: return None try: result = rlk_jsonloads_dict(resp.text) except JSONDecodeError: return None if 'rates' not in result or to_currency not in result['rates']: return None if date not in self.cached_forex_data: self.cached_forex_data[date] = {} if from_currency not in self.cached_forex_data[date]: self.cached_forex_data[date][from_currency] = {} for key, value in result['rates'].items(): self.cached_forex_data[date][from_currency][key] = FVal(value) rate = FVal(result['rates'][to_currency]) log.debug('Exchangeratesapi query succesful', rate=rate) return rate
def add_buy( self, bought_asset, rate, fee_cost, amount, gross_cost, cost, paid_with_asset, paid_with_asset_rate, timestamp, is_virtual, ): if not self.create_csv: return self.trades_csv.append({ 'type': 'buy', 'asset': bought_asset, "price_in_{}".format(self.profit_currency): rate, "fee_in_{}".format(self.profit_currency): fee_cost, "amount": amount, "gross_gained_or_invested_{}".format(self.profit_currency): gross_cost, "net_gained_or_invested_{}".format(self.profit_currency): cost, "exchanged_for": paid_with_asset, "exchanged_asset_euro_exchange_rate": paid_with_asset_rate, "time": tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), "is_virtual": is_virtual }) self.add_to_allevents(event_type='buy', paid_in_profit_currency=cost, paid_asset=self.profit_currency, paid_in_asset=cost, received_asset=bought_asset, received_in_asset=amount, received_in_profit_currency=0, timestamp=timestamp, is_virtual=is_virtual)
def add_loan_profit( self, gained_asset, gained_amount, gain_in_profit_currency, lent_amount, open_time, close_time, ): if not self.create_csv: return self.loan_profits_csv.append({ 'open_time': tsToDate(open_time, formatstr='%d/%m/%Y %H:%M:%S'), 'close_time': tsToDate(close_time, formatstr='%d/%m/%Y %H:%M:%S'), 'gained_asset': gained_asset, 'gained_amount': gained_amount, 'lent_amount': lent_amount, 'profit_in_{}'.format(self.profit_currency): gain_in_profit_currency }) self.add_to_allevents( event_type='interest_rate_payment', paid_in_profit_currency=0, paid_asset='', paid_in_asset=0, received_asset=gained_asset, received_in_asset=gained_amount, received_in_profit_currency=gain_in_profit_currency, timestamp=close_time, )
def add_loan_settlement( self, asset: typing.Asset, amount: FVal, rate_in_profit_currency: FVal, total_fee_in_profit_currency: FVal, timestamp: typing.Timestamp, ) -> None: if not self.create_csv: return row = len(self.loan_settlements_csv) + 2 loss_formula = '=B{}*C{}+D{}'.format(row, row, row) self.loan_settlements_csv.append({ 'asset': asset, 'amount': amount, 'price_in_{}'.format(self.profit_currency): rate_in_profit_currency, 'fee_in_{}'.format(self.profit_currency): total_fee_in_profit_currency, 'loss_in_{}'.format(self.profit_currency): loss_formula, 'time': tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), }) paid_in_profit_currency = amount * rate_in_profit_currency + total_fee_in_profit_currency self.add_to_allevents( event_type=EV_LOAN_SETTLE, paid_in_profit_currency=paid_in_profit_currency, paid_asset=asset, paid_in_asset=amount, received_asset=S_EMPTYSTR, received_in_asset=FVal(0), taxable_received_in_profit_currency=FVal(0), timestamp=timestamp, )
def add_asset_movement( self, exchange, category, asset, fee, rate, timestamp, ): if not self.create_csv: return self.asset_movements_csv.append({ 'time': tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), 'exchange': exchange, 'type': category, 'moving_asset': asset, 'fee_in_asset': fee, 'fee_in_{}'.format(self.profit_currency): fee * rate, }) self.add_to_allevents( event_type='asset_movement', paid_in_profit_currency=fee * rate, paid_asset=asset, paid_in_asset=fee, received_asset='', received_in_asset=0, received_in_profit_currency=0, timestamp=timestamp, )
def query_historical_price(self, from_asset, to_asset, timestamp): """ Query the historical price on `timestamp` for `from_asset` in `to_asset`. So how much `to_asset` does 1 unit of `from_asset` cost. Args: from_asset (str): The ticker symbol of the asset for which we want to know the price. to_asset (str): The ticker symbol of the asset against which we want to know the price. timestamp (int): The timestamp at which to query the price """ if from_asset == to_asset: return 1 if from_asset not in self.cryptocompare_coin_list: raise PriceQueryUnknownFromAsset(from_asset) data = self.get_historical_data(from_asset, to_asset, timestamp) # all data are sorted and timestamps are always increasing by 1 hour # find the closest entry to the provided timestamp # print("loaded {}_{}".format(from_asset, to_asset)) assert timestamp > data[0]['time'] index = convert_to_int((timestamp - data[0]['time']) / 3600, accept_only_exact=False) # print("timestamp: {} index: {} data_length: {}".format(timestamp, index, len(data))) diff = abs(data[index]['time'] - timestamp) if index + 1 <= len(data) - 1: diff_p1 = abs(data[index + 1]['time'] - timestamp) if diff_p1 < diff: index = index + 1 if data[index]['high'] is None or data[index]['low'] is None: # If we get some None in the hourly set price to 0 so that we check daily price price = FVal(0) else: price = FVal((data[index]['high'] + data[index]['low'])) / 2 if price == 0: if from_asset != 'BTC' and to_asset != 'BTC': # Just get the BTC price asset_btc_price = self.query_historical_price(from_asset, 'BTC', timestamp) btc_to_asset_price = self.query_historical_price('BTC', to_asset, timestamp) price = asset_btc_price * btc_to_asset_price else: # attempt to get the daily price by timestamp query_string = ( 'https://min-api.cryptocompare.com/data/pricehistorical?' 'fsym={}&tsyms={}&ts={}'.format( from_asset, to_asset, timestamp )) if to_asset == 'BTC': query_string += '&tryConversion=false' resp = urlopen(Request(query_string)) resp = rlk_jsonloads(resp.read()) print('DAILY PRICE OF ASSET: "{}"'.format(resp)) if from_asset not in resp: error_message = 'Failed to query cryptocompare for: "{}"'.format(query_string) raise ValueError(error_message) price = FVal(resp[from_asset][to_asset]) if price == 0: raise NoPriceForGivenTimestamp( from_asset, to_asset, tsToDate(timestamp, formatstr='%d/%m/%Y, %H:%M:%S') ) return price
def add_sell( self, selling_asset: typing.Asset, rate_in_profit_currency: FVal, total_fee_in_profit_currency: typing.Fee, gain_in_profit_currency: FVal, selling_amount: FVal, receiving_asset: typing.Asset, receiving_amount: FVal, receiving_asset_rate_in_profit_currency: FVal, taxable_amount: FVal, taxable_bought_cost: FVal, timestamp: typing.Timestamp, is_virtual: bool, ): if not self.create_csv: return exchange_rate_key = f'exchanged_asset_{self.profit_currency}_exchange_rate' taxable_profit_received = taxable_gain_for_sell( taxable_amount=taxable_amount, rate_in_profit_currency=rate_in_profit_currency, total_fee_in_profit_currency=total_fee_in_profit_currency, selling_amount=selling_amount, ) row = len(self.trades_csv) + 2 taxable_profit_formula = '=IF(G{}=0,0,K{}-J{})'.format(row, row, row) self.trades_csv.append({ 'type': 'sell', 'asset': selling_asset, 'price_in_{}'.format(self.profit_currency): rate_in_profit_currency, 'fee_in_{}'.format(self.profit_currency): total_fee_in_profit_currency, 'gained_or_invested_{}'.format(self.profit_currency): gain_in_profit_currency, 'amount': selling_amount, 'taxable_amount': taxable_amount, 'exchanged_for': receiving_asset, exchange_rate_key: receiving_asset_rate_in_profit_currency, 'taxable_bought_cost_in_{}'.format(self.profit_currency): taxable_bought_cost, 'taxable_gain_in_{}'.format(self.profit_currency): taxable_profit_received, 'taxable_profit_loss_in_{}'.format(self.profit_currency): taxable_profit_formula, 'time': tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), 'is_virtual': is_virtual, }) paid_in_profit_currency = ZERO self.add_to_allevents( event_type=EV_SELL, paid_in_profit_currency=paid_in_profit_currency, paid_asset=selling_asset, paid_in_asset=selling_amount, received_asset=receiving_asset, received_in_asset=receiving_amount, taxable_received_in_profit_currency=taxable_profit_received, timestamp=timestamp, is_virtual=is_virtual, taxable_amount=taxable_amount, taxable_bought_cost=taxable_bought_cost, )
def query_historical_price(self, from_asset, to_asset, timestamp): """ Query the historical price on `timestamp` for `from_asset` in `to_asset`. So how much `to_asset` does 1 unit of `from_asset` cost. Args: from_asset (str): The ticker symbol of the asset for which we want to know the price. to_asset (str): The ticker symbol of the asset against which we want to know the price. timestamp (int): The timestamp at which to query the price """ log.debug( 'Querying historical price', from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, ) if from_asset == to_asset: return 1 if from_asset not in self.cryptocompare_coin_list: raise PriceQueryUnknownFromAsset(from_asset) data = self.get_historical_data(from_asset, to_asset, timestamp) # all data are sorted and timestamps are always increasing by 1 hour # find the closest entry to the provided timestamp assert timestamp > data[0]['time'] index = convert_to_int((timestamp - data[0]['time']) / 3600, accept_only_exact=False) # print("timestamp: {} index: {} data_length: {}".format(timestamp, index, len(data))) diff = abs(data[index]['time'] - timestamp) if index + 1 <= len(data) - 1: diff_p1 = abs(data[index + 1]['time'] - timestamp) if diff_p1 < diff: index = index + 1 if data[index]['high'] is None or data[index]['low'] is None: # If we get some None in the hourly set price to 0 so that we check daily price price = FVal(0) else: price = FVal((data[index]['high'] + data[index]['low'])) / 2 if price == 0: if from_asset != 'BTC' and to_asset != 'BTC': log.debug( f"Coudn't find historical price from {from_asset} to " f"{to_asset}. Comparing with BTC...", ) # Just get the BTC price asset_btc_price = self.query_historical_price( from_asset, 'BTC', timestamp) btc_to_asset_price = self.query_historical_price( 'BTC', to_asset, timestamp) price = asset_btc_price * btc_to_asset_price else: log.debug( f"Coudn't find historical price from {from_asset} to " f"{to_asset}. Attempting to get daily price...", ) # attempt to get the daily price by timestamp cc_from_asset = world_to_cryptocompare(from_asset) cc_to_asset = world_to_cryptocompare(to_asset) log.debug( 'Querying cryptocompare for daily historical price', from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, ) query_string = ( 'https://min-api.cryptocompare.com/data/pricehistorical?' 'fsym={}&tsyms={}&ts={}'.format( cc_from_asset, cc_to_asset, timestamp, )) if to_asset == 'BTC': query_string += '&tryConversion=false' resp = request_get(query_string) if cc_from_asset not in resp: error_message = 'Failed to query cryptocompare for: "{}"'.format( query_string) log.error( 'Cryptocompare query for daily historical price failed', from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, error=error_message, ) raise ValueError(error_message) price = FVal(resp[cc_from_asset][cc_to_asset]) if price == 0: raise NoPriceForGivenTimestamp( from_asset, to_asset, tsToDate(timestamp, formatstr='%d/%m/%Y, %H:%M:%S')) log.debug('Got historical price', from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, price=price) return price
def add_sell( self, selling_asset, rate_in_profit_currency, total_fee_in_profit_currency, gain_in_profit_currency, selling_amount, receiving_asset, receiving_amount, receiving_asset_rate_in_profit_currency, taxable_amount, taxable_bought_cost, timestamp, is_virtual, ): if not self.create_csv: return self.trades_csv.append({ 'type': 'sell', 'asset': selling_asset, "price_in_{}".format(self.profit_currency): rate_in_profit_currency, "fee_in_{}".format(self.profit_currency): total_fee_in_profit_currency, "gross_gained_or_invested_{}".format(self.profit_currency): gain_in_profit_currency + total_fee_in_profit_currency, "net_gained_or_invested_{}".format(self.profit_currency): gain_in_profit_currency, "amount": selling_amount, "exchanged_for": receiving_asset, "exchanged_asset_euro_exchange_rate": receiving_asset_rate_in_profit_currency, "time": tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), "is_virtual": is_virtual, }) self.add_to_allevents( event_type='sell', paid_in_profit_currency=selling_amount * rate_in_profit_currency + total_fee_in_profit_currency, paid_asset=selling_asset, paid_in_asset=selling_amount, received_asset=receiving_asset, received_in_asset=receiving_amount, received_in_profit_currency=taxable_gain_for_sell( taxable_amount=taxable_amount, rate_in_profit_currency=rate_in_profit_currency, total_fee_in_profit_currency=total_fee_in_profit_currency, selling_amount=selling_amount, ), timestamp=timestamp, is_virtual=is_virtual, taxable_amount=taxable_amount, taxable_bought_cost=taxable_bought_cost, )
def search_buys_calculate_profit( self, selling_amount: FVal, selling_asset: Asset, timestamp: Timestamp, ) -> Tuple[FVal, FVal, FVal]: """ When selling `selling_amount` of `selling_asset` at `timestamp` this function calculates using the first-in-first-out rule the corresponding buy/s from which to do profit calculation. Also applies the one year rule after which a sell is not taxable in Germany. Returns a tuple of 3 values: - `taxable_amount`: The amount out of `selling_amount` that is taxable, calculated from the 1 year rule. - `taxable_bought_cost`: How much it cost in `profit_currency` to buy the `taxable_amount` - `taxfree_bought_cost`: How much it cost in `profit_currency` to buy the taxfree_amount (selling_amount - taxable_amount) """ remaining_sold_amount = selling_amount stop_index = -1 taxfree_bought_cost = FVal(0) taxable_bought_cost = FVal(0) taxable_amount = FVal(0) taxfree_amount = FVal(0) remaining_amount_from_last_buy = -1 for idx, buy_event in enumerate(self.events[selling_asset].buys): if self.taxfree_after_period is None: at_taxfree_period = False else: at_taxfree_period = (buy_event.timestamp + self.taxfree_after_period < timestamp) if remaining_sold_amount < buy_event.amount: stop_index = idx buying_cost = remaining_sold_amount.fma( buy_event.rate, (buy_event.fee_rate * remaining_sold_amount), ) if at_taxfree_period: taxfree_amount += remaining_sold_amount taxfree_bought_cost += buying_cost else: taxable_amount += remaining_sold_amount taxable_bought_cost += buying_cost remaining_amount_from_last_buy = buy_event.amount - remaining_sold_amount log.debug( 'Sell uses up part of historical buy', sensitive_log=True, tax_status='TAX-FREE' if at_taxfree_period else 'TAXABLE', used_amount=remaining_sold_amount, from_amount=buy_event.amount, asset=selling_asset, trade_buy_rate=buy_event.rate, profit_currency=self.profit_currency, trade_timestamp=buy_event.timestamp, ) # stop iterating since we found all buys to satisfy this sell break else: buying_cost = buy_event.amount.fma( buy_event.rate, (buy_event.fee_rate * buy_event.amount), ) remaining_sold_amount -= buy_event.amount if at_taxfree_period: taxfree_amount += buy_event.amount taxfree_bought_cost += buying_cost else: taxable_amount += buy_event.amount taxable_bought_cost += buying_cost log.debug( 'Sell uses up entire historical buy', sensitive_log=True, tax_status='TAX-FREE' if at_taxfree_period else 'TAXABLE', bought_amount=buy_event.amount, asset=selling_asset, trade_buy_rate=buy_event.rate, profit_currency=self.profit_currency, trade_timestamp=buy_event.timestamp, ) # If the sell used up the last historical buy if idx == len(self.events[selling_asset].buys) - 1: stop_index = idx + 1 if len(self.events[selling_asset].buys) == 0: log.critical( 'No documented buy found for "{}" before {}'.format( selling_asset, tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), ), ) # That means we had no documented buy for that asset. This is not good # because we can't prove a corresponding buy and as such we are burdened # calculating the entire sell as profit which needs to be taxed return selling_amount, FVal(0), FVal(0) # Otherwise, delete all the used up buys from the list del self.events[selling_asset].buys[:stop_index] # and modify the amount of the buy where we stopped if there is one if remaining_amount_from_last_buy != -1: self.events[selling_asset].buys[ 0].amount = remaining_amount_from_last_buy elif remaining_sold_amount != ZERO: # if we still have sold amount but no buys to satisfy it then we only # found buys to partially satisfy the sell adjusted_amount = selling_amount - taxfree_amount log.critical( 'Not enough documented buys found for "{}" before {}.' 'Only found buys for {} {}'.format( selling_asset, tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), taxable_amount + taxfree_amount, selling_asset, ), ) return adjusted_amount, taxable_bought_cost, taxfree_bought_cost return taxable_amount, taxable_bought_cost, taxfree_bought_cost
def search_buys_calculate_profit(self, selling_amount, selling_asset, timestamp): """ When selling `selling_amount` of `selling_asset` at `timestamp` this function calculates using the first-in-first-out rule the corresponding buy/s from which to do profit calculation. Also applies the one year rule after which a sell is not taxable in Germany. Returns a tuple of 3 values: - `taxable_amount`: The amount out of `selling_amount` that is taxable, calculated from the 1 year rule. - `taxable_bought_cost`: How much it cost in `profit_currency` to buy the `taxable_amount` - `taxfree_bought_cost`: How much it cost in `profit_currency` to buy the taxfree_amount (selling_amount - taxable_amount) """ remaining_sold_amount = selling_amount stop_index = -1 taxfree_bought_cost = 0 taxable_bought_cost = 0 taxable_amount = 0 taxfree_amount = 0 debug_enabled = logger.isEnabledFor(logging.DEBUG) for idx, buy_event in enumerate(self.events[selling_asset].buys): if self.taxfree_after_period is None: at_taxfree_period = False else: at_taxfree_period = (buy_event.timestamp + self.taxfree_after_period < timestamp) if remaining_sold_amount < buy_event.amount: stop_index = idx buying_cost = remaining_sold_amount.fma( buy_event.rate, -(buy_event.fee_rate * remaining_sold_amount)) if at_taxfree_period: taxfree_amount += remaining_sold_amount taxfree_bought_cost += buying_cost else: taxable_amount += remaining_sold_amount taxable_bought_cost += buying_cost remaining_amount_from_last_buy = buy_event.amount - remaining_sold_amount if debug_enabled: logger.debug( '[{}] Using up {}/{} "{}" from the buy for {} "{}" per "{}" at {}' .format( 'TAX-FREE' if at_taxfree_period else 'TAXABLE', remaining_sold_amount, buy_event.amount, selling_asset, buy_event.rate, self.profit_currency, selling_asset, tsToDate(buy_event.timestamp, formatstr='%d/%m/%Y %H:%M:%S'))) # stop iterating since we found all buys to satisfy this sell break else: remaining_sold_amount -= buy_event.amount if at_taxfree_period: taxfree_amount += buy_event.amount taxfree_bought_cost += buy_event.cost else: taxable_amount += buy_event.amount taxable_bought_cost += buy_event.cost if debug_enabled: logger.debug( '[{}] Using up the entire buy of {} "{}" for {} "{}" per {} at {}' .format( 'TAX-FREE' if at_taxfree_period else 'TAXABLE', buy_event.amount, selling_asset, buy_event.rate, self.profit_currency, selling_asset, tsToDate(buy_event.timestamp, formatstr='%d/%m/%Y %H:%M:%S'))) if stop_index == -1: logger.critical( 'No documented buy found for "{}" before {}'.format( selling_asset, tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'))) # That means we had no documented buy for that asset. This is not good # because we can't prove a corresponding buy and as such we are burdened # calculating the entire sell as profit which needs to be taxed return selling_amount, 0, 0 # Otherwise, delete all the used up buys from the list del self.events[selling_asset].buys[:stop_index] # and modify the amount of the buy where we stopped self.events[selling_asset].buys[0] = self.events[selling_asset].buys[ 0]._replace(amount=remaining_amount_from_last_buy) return taxable_amount, taxable_bought_cost, taxfree_bought_cost
def add_sell(self, selling_asset, selling_amount, receiving_asset, receiving_amount, gain_in_profit_currency, total_fee_in_profit_currency, trade_rate, rate_in_profit_currency, timestamp, loan_settlement=False, is_virtual=False): if selling_asset not in self.events: self.events[selling_asset] = Events(list(), list()) self.events[selling_asset].sells.append( SellEvent( amount=selling_amount, timestamp=timestamp, rate=rate_in_profit_currency, fee_rate=total_fee_in_profit_currency / selling_amount, gain=gain_in_profit_currency, )) debug_enabled = logger.isEnabledFor(logging.DEBUG) if debug_enabled: if loan_settlement: logger.debug( 'Loan Settlement Selling {} of "{}" for {} "{}" at {}'. format(selling_amount, selling_asset, gain_in_profit_currency, self.profit_currency, tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'))) else: logger.debug( 'Selling {} of "{}" for {} "{}" ({} "{}" per "{}" or {} "{}" ' 'per "{}") for a gain of {} "{}" and a fee of {} "{} at {}' .format(selling_amount, selling_asset, receiving_amount, receiving_asset, trade_rate, receiving_asset, selling_asset, rate_in_profit_currency, self.profit_currency, selling_asset, gain_in_profit_currency, self.profit_currency, total_fee_in_profit_currency, self.profit_currency, tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'))) # now search the buys for `paid_with_asset` and calculate profit/loss (taxable_amount, taxable_bought_cost, taxfree_bought_cost) = self.search_buys_calculate_profit( selling_amount, selling_asset, timestamp) general_profit_loss = 0 taxable_profit_loss = 0 # If we don't include crypto2crypto and we sell for crypto, stop here if receiving_asset not in FIAT_CURRENCIES and not self.include_crypto2crypto: return # calculate profit/loss if not loan_settlement or (loan_settlement and self.count_profit_for_settlements): taxable_gain = taxable_gain_for_sell( taxable_amount=taxable_amount, rate_in_profit_currency=rate_in_profit_currency, total_fee_in_profit_currency=total_fee_in_profit_currency, selling_amount=selling_amount, ) general_profit_loss = gain_in_profit_currency - ( taxfree_bought_cost + taxable_bought_cost + total_fee_in_profit_currency) taxable_profit_loss = taxable_gain - taxable_bought_cost # should never happen, should be stopped at the main loop assert timestamp <= self.query_end_ts, ( "Trade time > query_end_ts found in adding to sell event") # count profit/losses if we are inside the query period if timestamp >= self.query_start_ts: if loan_settlement: # If it's a loan settlement we are charged both the fee and the gain settlement_loss = gain_in_profit_currency + total_fee_in_profit_currency self.settlement_losses += settlement_loss if debug_enabled: logger.debug("Loan Settlement Loss: {} {}".format( settlement_loss, self.profit_currency)) elif debug_enabled: logger.debug("Taxable P/L: {} {} General P/L: {} {}".format( taxable_profit_loss, self.profit_currency, general_profit_loss, self.profit_currency, )) self.general_trade_profit_loss += general_profit_loss self.taxable_trade_profit_loss += taxable_profit_loss if loan_settlement: self.csv_exporter.add_loan_settlement( asset=selling_asset, amount=selling_amount, rate_in_profit_currency=rate_in_profit_currency, total_fee_in_profit_currency=total_fee_in_profit_currency, timestamp=timestamp, ) else: self.csv_exporter.add_sell( selling_asset=selling_asset, rate_in_profit_currency=rate_in_profit_currency, total_fee_in_profit_currency=total_fee_in_profit_currency, gain_in_profit_currency=gain_in_profit_currency, selling_amount=selling_amount, receiving_asset=receiving_asset, receiving_amount=receiving_amount, receiving_asset_rate_in_profit_currency=self. get_rate_in_profit_currency( receiving_asset, timestamp, ), taxable_amount=taxable_amount, taxable_bought_cost=taxable_bought_cost, timestamp=timestamp, is_virtual=is_virtual, )