def add_loan_profit( self, gained_asset: Asset, gained_amount: FVal, gain_in_profit_currency: FVal, lent_amount: FVal, open_time: Timestamp, close_time: Timestamp, ) -> None: if not self.create_csv: return self.loan_profits_csv.append({ 'open_time': timestamp_to_date(open_time, formatstr='%d/%m/%Y %H:%M:%S'), 'close_time': timestamp_to_date(close_time, formatstr='%d/%m/%Y %H:%M:%S'), 'gained_asset': gained_asset.identifier, 'gained_amount': gained_amount, 'lent_amount': lent_amount, f'profit_in_{self.profit_currency.identifier}': 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, taxable_received_in_profit_currency=gain_in_profit_currency, timestamp=close_time, )
def __init__(self, from_asset: Asset, to_asset: Asset, time: Timestamp) -> None: self.from_asset = from_asset self.to_asset = to_asset self.time = time super().__init__( 'Unable to query a historical price for "{}" to "{}" at {}'.format( from_asset.identifier, to_asset.identifier, timestamp_to_date( ts=time, formatstr='%d/%m/%Y, %H:%M:%S', treat_as_local=True, ), ), )
def query_fiat_pair(base: Asset, quote: Asset) -> Price: if base == quote: return Price(FVal('1')) instance = Inquirer() now = ts_now() date = timestamp_to_date(ts_now(), formatstr='%Y-%m-%d') price = instance._get_cached_forex_data(date, base, quote) if price: return price price = _query_exchanges_rateapi(base, quote) # TODO: Find another backup API for fiat exchange rates 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 = timestamp_to_date(now, formatstr='%Y-%m-%d') price = instance._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.identifier, quote_currency=quote.identifier, price=price, ) return price raise ValueError( 'Could not find a "{}" price for "{}"'.format( base.identifier, quote.identifier), ) instance._save_forex_rate(date, base, quote, price) return price
def add_defi_event(self, event: DefiEvent, profit_loss_in_profit_currency: FVal) -> None: if not self.create_csv: return self.defi_events_csv.append({ 'time': timestamp_to_date(event.timestamp, formatstr='%d/%m/%Y %H:%M:%S'), 'type': str(event.event_type), 'asset': str(event.asset), 'amount': str(event.amount), f'profit_loss_in_{self.profit_currency.identifier}': profit_loss_in_profit_currency, }) paid_asset: Union[EmptyStr, Asset] received_asset: Union[EmptyStr, Asset] if event.event_type == DefiEventType.DSR_LOAN_GAIN: paid_in_profit_currency = ZERO paid_in_asset = ZERO paid_asset = S_EMPTYSTR received_asset = event.asset received_in_asset = event.amount received_in_profit_currency = profit_loss_in_profit_currency elif event.event_type == DefiEventType.MAKERDAO_VAULT_LOSS: paid_in_profit_currency = profit_loss_in_profit_currency paid_in_asset = event.amount paid_asset = event.asset received_asset = S_EMPTYSTR received_in_asset = ZERO received_in_profit_currency = ZERO else: raise NotImplementedError( 'Not implemented Defi event encountered at csv export') self.add_to_allevents( event_type=EV_DEFI, paid_in_profit_currency=paid_in_profit_currency, paid_asset=paid_asset, paid_in_asset=paid_in_asset, received_asset=received_asset, received_in_asset=received_in_asset, taxable_received_in_profit_currency=received_in_profit_currency, timestamp=event.timestamp, )
def add_defi_event(self, event: DefiEvent, profit_loss_in_profit_currency: FVal) -> None: if not self.create_csv: return self.defi_events_csv.append({ 'time': timestamp_to_date(event.timestamp, formatstr='%d/%m/%Y %H:%M:%S'), 'type': str(event.event_type), 'asset': str(event.asset), 'amount': str(event.amount), f'profit_loss_in_{self.profit_currency.identifier}': profit_loss_in_profit_currency, }) paid_asset: Union[EmptyStr, Asset] received_asset: Union[EmptyStr, Asset] if event.is_profitable(): paid_in_profit_currency = ZERO paid_in_asset = ZERO paid_asset = S_EMPTYSTR received_asset = event.asset received_in_asset = event.amount received_in_profit_currency = profit_loss_in_profit_currency else: paid_in_profit_currency = profit_loss_in_profit_currency paid_in_asset = event.amount paid_asset = event.asset received_asset = S_EMPTYSTR received_in_asset = ZERO received_in_profit_currency = ZERO self.add_to_allevents( event_type=EV_DEFI, location=Location.BLOCKCHAIN, paid_in_profit_currency=paid_in_profit_currency, paid_asset=paid_asset, paid_in_asset=paid_in_asset, received_asset=received_asset, received_in_asset=received_in_asset, taxable_received_in_profit_currency=received_in_profit_currency, timestamp=event.timestamp, )
def historical_price(self, from_asset: Asset, to_asset: Asset, time: Timestamp) -> Price: vs_currency = Coingecko.check_vs_currencies( from_asset=from_asset, to_asset=to_asset, location='historical price', ) if not vs_currency: return Price(ZERO) if from_asset.coingecko is None: log.warning( f'Tried to query coingecko historical price from {from_asset.identifier} ' f'to {to_asset.identifier}. But from_asset is not supported in coingecko', ) return Price(ZERO) date = timestamp_to_date(time, formatstr='%d-%m-%Y') cached_price = self._get_cached_price(from_asset=from_asset, to_asset=to_asset, date=date) if cached_price is not None: return cached_price result = self._query( module='coins', subpath=f'{from_asset.coingecko}/history', options={ 'date': date, 'localization': False, }, ) try: price = Price( FVal(result['market_data']['current_price'][vs_currency])) except KeyError as e: log.warning( f'Queried coingecko historical price from {from_asset.identifier} ' f'to {to_asset.identifier}. But got key error for {str(e)} when ' f'processing the result.', ) return Price(ZERO) self._save_cached_price(from_asset, to_asset, date, price) return price
def decompress_and_decrypt_db(self, password: str, encrypted_data: B64EncodedString) -> None: """Decrypt and decompress the encrypted data we receive from the server If successful then replace our local Database Can raise UnableToDecryptRemoteData due to decrypt(). """ log.info('Decompress and decrypt DB') # First make a backup of the DB we are about to replace date = timestamp_to_date(ts=ts_now(), formatstr='%Y_%m_%d_%H_%M_%S') shutil.copyfile( os.path.join(self.data_directory, self.username, 'rotkehlchen.db'), os.path.join(self.data_directory, self.username, f'rotkehlchen_db_{date}.backup'), ) decrypted_data = decrypt(password.encode(), encrypted_data) decompressed_data = zlib.decompress(decrypted_data) self.db.import_unencrypted(decompressed_data, password)
def add_buy( self, bought_asset: Asset, rate: FVal, fee_cost: Fee, amount: FVal, cost: FVal, paid_with_asset: Asset, paid_with_asset_rate: FVal, timestamp: 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': timestamp_to_date(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 serialize( self, export_data: Optional[Tuple[Asset, Price]] = None) -> Dict[str, str]: if export_data: return { 'timestamp': timestamp_to_date(self.time, '%Y-%m-%d %H:%M:%S'), 'location': Location.deserialize_from_db(self.location).serialize(), f'{export_data[0].symbol.lower()}_value': str(FVal(self.usd_value) * export_data[1]), # noqa: 501 } return { 'timestamp': str(self.time), 'location': Location.deserialize_from_db(self.location).serialize(), 'usd_value': self.usd_value, }
def add_loan_settlement( self, location: Location, asset: Asset, amount: FVal, rate_in_profit_currency: FVal, total_fee_in_profit_currency: FVal, timestamp: Timestamp, ) -> None: if not self.create_csv: return row = len(self.loan_settlements_csv) + 2 loss_formula = '=C{}*D{}+E{}'.format(row, row, row) self.loan_settlements_csv.append({ 'asset': asset.identifier, 'location': str(location), 'amount': amount, f'price_in_{self.profit_currency.identifier}': rate_in_profit_currency, f'fee_in_{self.profit_currency.identifier}': total_fee_in_profit_currency, f'loss_in_{self.profit_currency.identifier}': loss_formula, 'time': timestamp_to_date(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, location=location, 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_margin_position( self, location: Location, margin_notes: str, gain_loss_asset: Asset, gain_loss_amount: FVal, gain_loss_in_profit_currency: FVal, timestamp: Timestamp, ) -> None: if not self.create_csv: return # Note: We are not getting the fee info in here but they are not needed # in the final CSV export. self.margin_positions_csv.append({ 'name': margin_notes, 'location': str(location), 'time': timestamp_to_date(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), 'gain_loss_asset': gain_loss_asset.identifier, 'gain_loss_amount': gain_loss_amount, f'profit_loss_in_{self.profit_currency.identifier}': gain_loss_in_profit_currency, }) self.add_to_allevents( event_type=EV_MARGIN_CLOSE, location=location, paid_in_profit_currency=FVal(0), paid_asset=S_EMPTYSTR, paid_in_asset=FVal(0), received_asset=gain_loss_asset, received_in_asset=gain_loss_amount, taxable_received_in_profit_currency=gain_loss_in_profit_currency, timestamp=timestamp, )
def main() -> None: """Goes through the assets template, reads the built-in assets DB and generates assets.py with initialization of all constant assets""" root_dir = Path(__file__).resolve().parent.parent.parent constants_dir = root_dir / 'rotkehlchen' / 'constants' template_file = constants_dir / 'assets.py.template' date = timestamp_to_date(ts_now()) generated_text = ( f'# This python file was generated automatically by\n' f'# {__file__} at {date}.\n' f'# Do not edit manually!\n' f'\n' ) ctx = ContextManager() with open(template_file, 'r') as f: for line in f: line = line.strip('\n\r') if 'Asset(\'' in line: initial_split = line.split(' = Asset(\'') var_name = initial_split[0] identifier = initial_split[1].split('\'')[0] generated_text += ctx.add_asset_initialization(var_name, identifier) continue if 'EthereumToken(\'' in line: initial_split = line.split(' = EthereumToken(\'') var_name = initial_split[0] identifier = initial_split[1].split('\'')[0] generated_text += ctx.add_ethtoken_initialization(var_name, identifier) continue # else just copy text generated_text += line + '\n' assets_file = constants_dir / 'assets.py' with open(assets_file, 'w') as f: f.write(generated_text) print('constants/assets.py generated succesfully!')
def decompress_and_decrypt_db(self, password: str, encrypted_data: B64EncodedString) -> None: """Decrypt and decompress the encrypted data we receive from the server If successful then replace our local Database May Raise: - UnableToDecryptRemoteData due to decrypt() - DBUpgradeError if the rotki DB version is newer than the software or there is a DB upgrade and there is an error. - SystemPermissionError if the DB file permissions are not correct """ log.info('Decompress and decrypt DB') # First make a backup of the DB we are about to replace date = timestamp_to_date(ts=ts_now(), formatstr='%Y_%m_%d_%H_%M_%S') shutil.copyfile( self.data_directory / self.username / 'rotkehlchen.db', self.data_directory / self.username / f'rotkehlchen_db_{date}.backup', ) decrypted_data = decrypt(password.encode(), encrypted_data) decompressed_data = zlib.decompress(decrypted_data) self.db.import_unencrypted(decompressed_data, password)
def add_asset_movement( self, exchange: Location, category: AssetMovementCategory, asset: Asset, fee: Fee, rate: FVal, timestamp: Timestamp, ) -> None: if not self.create_csv: return self.asset_movements_csv.append({ 'time': timestamp_to_date(timestamp, formatstr='%d/%m/%Y %H:%M:%S'), 'exchange': str(exchange), 'type': str(category), 'moving_asset': asset.identifier, 'fee_in_asset': fee, f'fee_in_{self.profit_currency.identifier}': fee * rate, }) self.add_to_allevents( event_type=EV_ASSET_MOVE, location=exchange, paid_in_profit_currency=fee * rate, paid_asset=asset, paid_in_asset=fee, received_asset=S_EMPTYSTR, received_in_asset=FVal(0), taxable_received_in_profit_currency=FVal(0), timestamp=timestamp, )
def serialize( self, export_data: Optional[Tuple[Asset, Price]] = None) -> Dict[str, str]: if export_data: return { 'timestamp': timestamp_to_date(self.time, '%Y-%m-%d %H:%M:%S'), 'category': self.category.serialize(), 'asset': str(self.asset), 'amount': self.amount, f'{export_data[0].symbol.lower()}_value': str(FVal(self.usd_value) * export_data[1]), # noqa: 501 } return { 'timestamp': str(self.time), 'category': self.category.serialize(), 'asset_identifier': str(self.asset.identifier), 'amount': self.amount, 'usd_value': self.usd_value, }
def query_historical_price( self, from_asset: Asset, to_asset: Asset, timestamp: Timestamp, historical_data_start: Timestamp, ) -> Price: """ Query the historical price on `timestamp` for `from_asset` in `to_asset`. So how much `to_asset` does 1 unit of `from_asset` cost. May raise: - PriceQueryUnsupportedAsset if from/to 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 cryptocompare server or with reading the response returned by the server """ try: data = self.get_historical_data( from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, historical_data_start=historical_data_start, ) except UnsupportedAsset as e: raise PriceQueryUnsupportedAsset(e.asset_name) # all data are sorted and timestamps are always increasing by 1 hour # find the closest entry to the provided timestamp if timestamp >= data[0].time: # convert_to_int can't raise here due to its input 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 alternatives price = Price(ZERO) else: price = Price((data[index].high + data[index].low) / 2) else: # no price found in the historical data from/to asset, try alternatives price = Price(ZERO) if price == 0: if from_asset != 'BTC' and to_asset != 'BTC': log.debug( f"Couldn't find historical price from {from_asset} to " f"{to_asset} at timestamp {timestamp}. Comparing with BTC...", ) # Just get the BTC price asset_btc_price = PriceHistorian().query_historical_price( from_asset=from_asset, to_asset=A_BTC, timestamp=timestamp, ) btc_to_asset_price = PriceHistorian().query_historical_price( from_asset=A_BTC, to_asset=to_asset, timestamp=timestamp, ) price = Price(asset_btc_price * btc_to_asset_price) else: log.debug( f"Couldn't find historical price from {from_asset} to " f"{to_asset} at timestamp {timestamp} through cryptocompare." f" Attempting to get daily price...", ) price = self.query_endpoint_pricehistorical( from_asset, to_asset, timestamp) comparison_to_nonusd_fiat = ((to_asset.is_fiat() and to_asset != A_USD) or (from_asset.is_fiat() and from_asset != A_USD)) if comparison_to_nonusd_fiat: price = self._adjust_to_cryptocompare_price_incosistencies( price=price, from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, ) if price == 0: raise NoPriceForGivenTimestamp( from_asset=from_asset, to_asset=to_asset, date=timestamp_to_date(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 query_historical_price( from_asset: Asset, to_asset: Asset, timestamp: Timestamp, ) -> Price: """ 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: The ticker symbol of the asset for which we want to know the price. to_asset: The ticker symbol of the asset against which we want to know the price. timestamp: The timestamp at which to query the price May raise: - NoPriceForGivenTimestamp if we can't find a price for the asset in the given timestamp from the external service. """ log.debug( 'Querying historical price', from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, ) if from_asset == to_asset: return Price(FVal('1')) # Querying historical forex data is attempted first via exchangerates API, # and then via any price oracle that has fiat to fiat. if from_asset.is_fiat() and to_asset.is_fiat(): price = Inquirer().query_historical_fiat_exchange_rates( from_fiat_currency=from_asset, to_fiat_currency=to_asset, timestamp=timestamp, ) if price is not None: return price # else cryptocompare also has historical fiat to fiat data instance = PriceHistorian() oracles = instance._oracles oracle_instances = instance._oracle_instances assert isinstance(oracles, list) and isinstance( oracle_instances, list ), ('PriceHistorian should never be called before the setting the oracles' ) for oracle, oracle_instance in zip(oracles, oracle_instances): can_query_history = oracle_instance.can_query_history( from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, ) if can_query_history is False: continue try: price = oracle_instance.query_historical_price( from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, ) except (PriceQueryUnsupportedAsset, NoPriceForGivenTimestamp, RemoteError) as e: log.warning( f'Historical price oracle {oracle} failed to request ' f'due to: {str(e)}.', from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, ) continue if price != Price(ZERO): log.debug( f'Historical price oracle {oracle} got price', price=price, from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, ) return price raise NoPriceForGivenTimestamp( from_asset=from_asset, to_asset=to_asset, date=timestamp_to_date(timestamp, formatstr='%d/%m/%Y, %H:%M:%S', treat_as_local=True), )
def query_historical_price(from_asset: Asset, to_asset: Asset, timestamp: Timestamp) -> Price: """ 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: The ticker symbol of the asset for which we want to know the price. to_asset: The ticker symbol of the asset against which we want to know the price. timestamp: The timestamp at which to query the price 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 """ log.debug( 'Querying historical price', from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, ) if from_asset == to_asset: return Price(FVal('1')) if from_asset.is_fiat() and to_asset.is_fiat(): # if we are querying historical forex data then try something other than cryptocompare price = Inquirer().query_historical_fiat_exchange_rates( from_fiat_currency=from_asset, to_fiat_currency=to_asset, timestamp=timestamp, ) if price is not None: return price # else cryptocompare also has historical fiat to fiat data instance = PriceHistorian() price = None if Inquirer()._cryptocompare.rate_limited_in_last() is False: try: price = instance._cryptocompare.query_historical_price( from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, ) except (PriceQueryUnsupportedAsset, NoPriceForGivenTimestamp, RemoteError): # then use coingecko pass if price and price != Price(ZERO): return price try: price = instance._coingecko.historical_price( from_asset=from_asset, to_asset=to_asset, time=timestamp, ) if price != Price(ZERO): return price except RemoteError: pass # nothing found in any price oracle raise NoPriceForGivenTimestamp( from_asset=from_asset, to_asset=to_asset, date=timestamp_to_date(timestamp, formatstr='%d/%m/%Y, %H:%M:%S'), )
def add_to_allevents( self, event_type: EventType, location: Location, paid_in_profit_currency: FVal, paid_asset: Union[Asset, EmptyStr], paid_in_asset: FVal, received_asset: Union[Asset, EmptyStr], received_in_asset: FVal, taxable_received_in_profit_currency: FVal, timestamp: Timestamp, is_virtual: bool = False, taxable_amount: FVal = ZERO, taxable_bought_cost: FVal = ZERO, ) -> 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' elif event_type == EV_SELL: if taxable_amount == 0: net_profit_or_loss = FVal(0) else: net_profit_or_loss = taxable_received_in_profit_currency - taxable_bought_cost net_profit_or_loss_csv = '=IF(E{}=0,0,L{}-M{})'.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 = '=-K{}'.format(row) elif event_type in (EV_INTEREST_PAYMENT, EV_MARGIN_CLOSE, EV_DEFI): net_profit_or_loss = taxable_received_in_profit_currency net_profit_or_loss_csv = '=L{}'.format(row) else: raise ValueError( 'Illegal event type "{}" at add_to_allevents'.format( event_type)) exported_paid_asset = (paid_asset if isinstance(paid_asset, str) else paid_asset.identifier) exported_received_asset = (received_asset if isinstance( received_asset, str) else received_asset.identifier) entry = { 'type': event_type, 'location': str(location), 'paid_in_profit_currency': paid_in_profit_currency, 'paid_asset': exported_paid_asset, 'paid_in_asset': paid_in_asset, 'taxable_amount': taxable_amount, 'taxable_bought_cost_in_profit_currency': taxable_bought_cost, 'received_asset': exported_received_asset, 'taxable_received_in_profit_currency': taxable_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'] = timestamp_to_date(timestamp, formatstr='%d/%m/%Y %H:%M:%S') new_entry[ f'paid_in_{self.profit_currency.identifier}'] = paid_in_profit_currency key = f'taxable_received_in_{self.profit_currency.identifier}' new_entry[key] = taxable_received_in_profit_currency key = f'taxable_bought_cost_in_{self.profit_currency.identifier}' new_entry[key] = taxable_bought_cost del new_entry['paid_in_profit_currency'] del new_entry['taxable_received_in_profit_currency'] del new_entry['taxable_bought_cost_in_profit_currency'] self.all_events_csv.append(new_entry)
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 = ZERO taxable_bought_cost = ZERO taxable_amount = ZERO taxfree_amount = ZERO remaining_amount_from_last_buy = FVal('-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, timestamp_to_date(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, ZERO, ZERO # 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 != FVal('-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, timestamp_to_date(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 query_historical_price( self, from_asset: Asset, to_asset: Asset, timestamp: Timestamp, historical_data_start: Timestamp, ) -> Price: """ Query the historical price on `timestamp` for `from_asset` in `to_asset`. So how much `to_asset` does 1 unit of `from_asset` cost. May raise: - PriceQueryUnsupportedAsset if from/to 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 cryptocompare server or with reading the response returned by the server """ try: data = self.get_historical_data( from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, historical_data_start=historical_data_start, ) except UnsupportedAsset as e: raise PriceQueryUnsupportedAsset(e.asset_name) price = Price(ZERO) # all data are sorted and timestamps are always increasing by 1 hour # find the closest entry to the provided timestamp if timestamp >= data[0].time: index_in_bounds = True # convert_to_int can't raise here due to its input index = convert_to_int((timestamp - data[0].time) / 3600, accept_only_exact=False) if index > len(data) - 1: # index out of bounds # Try to see if index - 1 is there and if yes take it if index > len(data): index = index - 1 else: # give up. This happened: https://github.com/rotki/rotki/issues/1534 log.error( f'Expected data index in cryptocompare historical hour price ' f'not found. Queried price of: {from_asset.identifier} in ' f'{to_asset.identifier} at {timestamp}. Data ' f'index: {index}. Length of returned data: {len(data)}. ' f'https://github.com/rotki/rotki/issues/1534. Attempting other methods...', ) index_in_bounds = False if index_in_bounds: 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 not None and data[index].low is not None: price = Price((data[index].high + data[index].low) / 2) else: # no price found in the historical data from/to asset, try alternatives price = Price(ZERO) if price == 0: if from_asset != 'BTC' and to_asset != 'BTC': log.debug( f"Couldn't find historical price from {from_asset} to " f"{to_asset} at timestamp {timestamp}. Comparing with BTC...", ) # Just get the BTC price asset_btc_price = PriceHistorian().query_historical_price( from_asset=from_asset, to_asset=A_BTC, timestamp=timestamp, ) btc_to_asset_price = PriceHistorian().query_historical_price( from_asset=A_BTC, to_asset=to_asset, timestamp=timestamp, ) price = Price(asset_btc_price * btc_to_asset_price) else: log.debug( f"Couldn't find historical price from {from_asset} to " f"{to_asset} at timestamp {timestamp} through cryptocompare." f" Attempting to get daily price...", ) price = self.query_endpoint_pricehistorical( from_asset, to_asset, timestamp) comparison_to_nonusd_fiat = ((to_asset.is_fiat() and to_asset != A_USD) or (from_asset.is_fiat() and from_asset != A_USD)) if comparison_to_nonusd_fiat: price = self._adjust_to_cryptocompare_price_incosistencies( price=price, from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, ) if price == 0: raise NoPriceForGivenTimestamp( from_asset=from_asset, to_asset=to_asset, date=timestamp_to_date(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 _can_sync_data_from_server(self, new_account: bool) -> SyncCheckResult: """ Checks if the remote data can be pulled from the server. Returns a SyncCheckResult denoting whether we can pull for sure, whether we can't pull or whether the user should be asked. If the user should be asked a message is also returned """ log.debug('can sync data from server -- start') if self.premium is None: return SyncCheckResult(can_sync=CanSync.NO, message='', payload=None) b64_encoded_data, our_hash = self.data.compress_and_encrypt_db( self.password) try: metadata = self.premium.query_last_data_metadata() except RemoteError as e: log.debug('can sync data from server failed', error=str(e)) return SyncCheckResult(can_sync=CanSync.NO, message='', payload=None) if new_account: return SyncCheckResult(can_sync=CanSync.YES, message='', payload=None) if not self.data.db.get_premium_sync(): # If it's not a new account and the db setting for premium syncing is off stop return SyncCheckResult(can_sync=CanSync.NO, message='', payload=None) log.debug( 'CAN_PULL', ours=our_hash, theirs=metadata.data_hash, ) if our_hash == metadata.data_hash: log.debug('sync from server stopped -- same hash') # same hash -- no need to get anything return SyncCheckResult(can_sync=CanSync.NO, message='', payload=None) our_last_write_ts = self.data.db.get_last_write_ts() data_bytes_size = len(base64.b64decode(b64_encoded_data)) local_more_recent = our_last_write_ts >= metadata.last_modify_ts local_bigger = data_bytes_size >= metadata.data_size if local_more_recent and local_bigger: log.debug( 'sync from server stopped -- local is both newer and bigger') return SyncCheckResult(can_sync=CanSync.NO, message='', payload=None) if local_more_recent is False: # remote is more recent message = ( 'Detected remote database with more recent modification timestamp ' 'than the local one. ') else: # remote is bigger message = 'Detected remote database with bigger size than the local one. ' return SyncCheckResult( can_sync=CanSync.ASK_USER, message=message, payload={ 'local_size': data_bytes_size, 'remote_size': metadata.data_size, 'local_last_modified': timestamp_to_date(our_last_write_ts), 'remote_last_modified': timestamp_to_date(metadata.last_modify_ts), }, )
def add_sell( self, location: Location, selling_asset: Asset, rate_in_profit_currency: FVal, total_fee_in_profit_currency: Fee, gain_in_profit_currency: FVal, selling_amount: FVal, receiving_asset: Optional[Asset], receiving_amount: Optional[FVal], receiving_asset_rate_in_profit_currency: FVal, taxable_amount: FVal, taxable_bought_cost: FVal, timestamp: Timestamp, is_virtual: bool, ) -> None: if not self.create_csv: return processed_receiving_asset: Union[EmptyStr, Asset] = ( EmptyStr('') if receiving_asset is None else receiving_asset) exported_receiving_asset = '' if receiving_asset is None else receiving_asset.identifier processed_receiving_amount = FVal( 0) if not receiving_amount else receiving_amount exchange_rate_key = f'exchanged_asset_{self.profit_currency.identifier}_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(H{}=0,0,L{}-K{})'.format(row, row, row) self.trades_csv.append({ 'type': 'sell', 'location': str(location), 'asset': selling_asset.identifier, f'price_in_{self.profit_currency.identifier}': rate_in_profit_currency, f'fee_in_{self.profit_currency.identifier}': total_fee_in_profit_currency, f'gained_or_invested_{self.profit_currency.identifier}': gain_in_profit_currency, 'amount': selling_amount, 'taxable_amount': taxable_amount, 'exchanged_for': exported_receiving_asset, exchange_rate_key: receiving_asset_rate_in_profit_currency, f'taxable_bought_cost_in_{self.profit_currency.identifier}': taxable_bought_cost, f'taxable_gain_in_{self.profit_currency.identifier}': taxable_profit_received, f'taxable_profit_loss_in_{self.profit_currency.identifier}': taxable_profit_formula, 'time': timestamp_to_date(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, location=location, paid_in_profit_currency=paid_in_profit_currency, paid_asset=selling_asset, paid_in_asset=selling_amount, received_asset=processed_receiving_asset, received_in_asset=processed_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 configure_logging(args: argparse.Namespace) -> None: loglevel = args.loglevel.upper() formatters = { 'default': { 'format': '[%(asctime)s] %(levelname)s %(name)s: %(message)s', 'datefmt': '%d/%m/%Y %H:%M:%S %Z', }, } handlers = { 'console': { 'class': 'logging.StreamHandler', 'level': loglevel, 'formatter': 'default', }, } if args.max_logfiles_num < 0: backups_num = 0 else: backups_num = args.max_logfiles_num - 1 if args.logtarget == 'file': given_filepath = Path(args.logfile) filepath = given_filepath if not getattr(sys, 'frozen', False): # not packaged -- must be in develop mode. Append date to each file date = timestamp_to_date( ts=ts_now(), formatstr='%Y%m%d_%H%M%S', treat_as_local=True, ) filepath = given_filepath.parent / f'{date}_{given_filepath.name}' selected_handlers = ['file'] single_log_max_bytes = int( (args.max_size_in_mb_all_logs * 1024 * 1000) / args.max_logfiles_num, ) handlers['file'] = { 'class': 'logging.handlers.RotatingFileHandler', 'filename': filepath, 'mode': 'a', 'maxBytes': single_log_max_bytes, 'backupCount': backups_num, 'level': loglevel, 'formatter': 'default', } else: selected_handlers = ['console'] filters = { 'pywsgi': { '()': PywsgiFilter, }, } loggers = { '': { # root logger 'level': loglevel, 'handlers': selected_handlers, }, 'rotkehlchen.api.server.pywsgi': { 'level': loglevel, 'handlers': selected_handlers, 'filters': ['pywsgi'], 'propagate': False, }, } logging.config.dictConfig({ 'version': 1, 'disable_existing_loggers': False, 'filters': filters, 'formatters': formatters, 'handlers': handlers, 'loggers': loggers, }) if not args.logfromothermodules: logging.getLogger('urllib3').setLevel(logging.CRITICAL) logging.getLogger('urllib3.connectionpool').setLevel(logging.CRITICAL) logging.getLogger('substrateinterface.base').setLevel(logging.CRITICAL)
def query_historical_price( self, from_asset: Asset, to_asset: Asset, timestamp: Timestamp, historical_data_start: Timestamp, ) -> Price: if from_asset in KNOWN_TO_MISS_FROM_CRYPTOCOMPARE: raise PriceQueryUnknownFromAsset(from_asset) data = self.get_historical_data( from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, historical_data_start=historical_data_start, ) # all data are sorted and timestamps are always increasing by 1 hour # find the closest entry to the provided timestamp if 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 alternatives price = Price(ZERO) else: price = (data[index].high + data[index].low) / 2 else: # no price found in the historical data from/to asset, try alternatives price = Price(ZERO) if price == 0: if from_asset != 'BTC' and to_asset != 'BTC': log.debug( f"Couldn't find historical price from {from_asset} to " f"{to_asset} at timestamp {timestamp}. Comparing with BTC...", ) # Just get the BTC price asset_btc_price = PriceHistorian().query_historical_price( from_asset=from_asset, to_asset=A_BTC, timestamp=timestamp, ) btc_to_asset_price = PriceHistorian().query_historical_price( from_asset=A_BTC, to_asset=to_asset, timestamp=timestamp, ) price = asset_btc_price * btc_to_asset_price else: log.debug( f"Couldn't find historical price from {from_asset} to " f"{to_asset} at timestamp {timestamp} through cryptocompare." f" Attempting to get daily price...", ) price = self.query_endpoint_pricehistorical( from_asset, to_asset, timestamp) comparison_to_nonusd_fiat = ((to_asset.is_fiat() and to_asset != A_USD) or (from_asset.is_fiat() and from_asset != A_USD)) if comparison_to_nonusd_fiat: price = self._adjust_to_cryptocompare_price_incosistencies( price=price, from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, ) if price == 0: raise NoPriceForGivenTimestamp( from_asset, to_asset, timestamp_to_date(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: Asset, rate_in_profit_currency: FVal, total_fee_in_profit_currency: Fee, gain_in_profit_currency: FVal, selling_amount: FVal, receiving_asset: Asset, receiving_amount: FVal, receiving_asset_rate_in_profit_currency: FVal, taxable_amount: FVal, taxable_bought_cost: FVal, timestamp: 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': timestamp_to_date(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 timestamp_to_date(self, timestamp: Timestamp) -> str: return timestamp_to_date( timestamp, formatstr=self.dateformat, treat_as_local=self.datelocaltime, )
def query_historical_price( self, from_asset: Asset, to_asset: Asset, timestamp: Timestamp, ) -> Price: """ Query the historical price on `timestamp` for `from_asset` in `to_asset`. So how much `to_asset` does 1 unit of `from_asset` cost. This tries to: 1. Find cached cryptocompare values and return them 2. If none exist at the moment try the normal historical price endpoint 3. Else fail May raise: - PriceQueryUnsupportedAsset if from/to 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 cryptocompare server or with reading the response returned by the server """ # TODO: Figure out a better way to log and return. Only thing I can imagine # is nested ifs (ugly af) or a different function (meh + performance). # NB: check if the from..to asset price (or viceversa) is a special # histohour API case. price = self._check_and_get_special_histohour_price( from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, ) if price != Price(ZERO): log.debug('Got historical price from cryptocompare', from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, price=price) # noqa: E501 return price # check DB cache price_cache_entry = GlobalDBHandler().get_historical_price( from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, max_seconds_distance=3600, source=HistoricalPriceOracle.CRYPTOCOMPARE, ) if price_cache_entry and price_cache_entry.price != Price(ZERO): log.debug('Got historical price from cryptocompare', from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, price=price) # noqa: E501 return price_cache_entry.price # else log.debug( f"Couldn't find historical price from {from_asset} to " f"{to_asset} at timestamp {timestamp} through cryptocompare." f" Attempting to get daily price...", ) price = self.query_endpoint_pricehistorical(from_asset, to_asset, timestamp) if price == Price(ZERO): raise NoPriceForGivenTimestamp( from_asset=from_asset, to_asset=to_asset, date=timestamp_to_date( timestamp, formatstr='%d/%m/%Y, %H:%M:%S', treat_as_local=True, ), ) log.debug('Got historical price from cryptocompare', from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, price=price) # noqa: E501 return price
def query_historical_price( self, from_asset: Asset, to_asset: Asset, timestamp: Timestamp, ) -> Price: vs_currency = Coingecko.check_vs_currencies( from_asset=from_asset, to_asset=to_asset, location='historical price', ) if not vs_currency: return Price(ZERO) try: from_coingecko_id = from_asset.to_coingecko() except UnsupportedAsset: log.warning( f'Tried to query coingecko historical price from {from_asset.identifier} ' f'to {to_asset.identifier}. But from_asset is not supported in coingecko', ) return Price(ZERO) # check DB cache price_cache_entry = GlobalDBHandler().get_historical_price( from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, max_seconds_distance=DAY_IN_SECONDS, source=HistoricalPriceOracle.COINGECKO, ) if price_cache_entry: return price_cache_entry.price # no cache, query coingecko for daily price date = timestamp_to_date(timestamp, formatstr='%d-%m-%Y') result = self._query( module='coins', subpath=f'{from_coingecko_id}/history', options={ 'date': date, 'localization': False, }, ) try: price = Price( FVal(result['market_data']['current_price'][vs_currency])) except KeyError as e: log.warning( f'Queried coingecko historical price from {from_asset.identifier} ' f'to {to_asset.identifier}. But got key error for {str(e)} when ' f'processing the result.', ) return Price(ZERO) # save result in the DB and return date_timestamp = create_timestamp(date, formatstr='%d-%m-%Y') GlobalDBHandler().add_historical_prices(entries=[ HistoricalPrice( from_asset=from_asset, to_asset=to_asset, source=HistoricalPriceOracle.COINGECKO, timestamp=date_timestamp, price=price, ) ]) return price
def query_historical_fiat_exchange_rates( from_fiat_currency: Asset, to_fiat_currency: Asset, timestamp: Timestamp, ) -> Optional[Price]: assert from_fiat_currency.is_fiat( ), 'fiat currency should have been provided' assert to_fiat_currency.is_fiat( ), 'fiat currency should have been provided' date = timestamp_to_date(timestamp, formatstr='%Y-%m-%d') instance = Inquirer() rate = instance._get_cached_forex_data(date, from_fiat_currency, to_fiat_currency) if rate: return rate log.debug( 'Querying exchangeratesapi', from_fiat_currency=from_fiat_currency.identifier, to_fiat_currency=to_fiat_currency.identifier, timestamp=timestamp, ) query_str = (f'https://api.exchangeratesapi.io/{date}?' f'base={from_fiat_currency.identifier}') resp = retry_calls( times=5, location='query_exchangeratesapi', handle_429=False, backoff_in_seconds=0, method_name='requests.get', function=requests.get, # function's arguments url=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_fiat_currency.identifier not in result[ 'rates']: return None if date not in instance._cached_forex_data: instance._cached_forex_data[date] = {} if from_fiat_currency not in instance._cached_forex_data[date]: instance._cached_forex_data[date][from_fiat_currency] = {} for key, value in result['rates'].items(): instance._cached_forex_data[date][from_fiat_currency][key] = FVal( value) rate = Price(FVal(result['rates'][to_fiat_currency.identifier])) log.debug('Exchangeratesapi query succesful', rate=rate) return rate