def set_main_currency(self, given_currency: str) -> None: currency = Asset(given_currency) msg = 'main currency checks should have happened at rotkehlchen.set_settings()' assert currency.is_fiat(), msg self.profit_currency = currency self.events.profit_currency = currency
def add_sell_and_corresponding_buy( self, selling_asset: Asset, selling_amount: FVal, receiving_asset: Asset, receiving_amount: FVal, gain_in_profit_currency: FVal, total_fee_in_profit_currency: Fee, trade_rate: FVal, rate_in_profit_currency: FVal, timestamp: Timestamp, ) -> None: """ Add and process a selling event to the events list Args: selling_asset (str): The ticker representation of the asset we sell. selling_amount (FVal): The amount of `selling_asset` for sale. receiving_asset (str): The ticker representation of the asset we receive in exchange for `selling_asset`. receiving_amount (FVal): The amount of `receiving_asset` we receive. gain_in_profit_currency (FVal): This is the amount of `profit_currency` equivalent we receive after doing this trade. Fees are not counted in this. total_fee_in_profit_currency (FVal): This is the amount of `profit_currency` equivalent we pay in fees after doing this trade. trade_rate (FVal): How much does 1 unit of `receiving_asset` cost in `selling_asset` rate_in_profit_currency (FVal): The equivalent of `trade_rate` in `profit_currency` timestamp (int): The timestamp for the trade """ self.add_sell( selling_asset, selling_amount, receiving_asset, receiving_amount, gain_in_profit_currency, total_fee_in_profit_currency, trade_rate, rate_in_profit_currency, timestamp, is_virtual=False, ) if receiving_asset.is_fiat() or not self.include_crypto2crypto: return log.debug( f'Selling {selling_asset} for {receiving_asset} also introduces a virtual buy event', ) # else then you are also buying some other asset through your sell self.add_buy( bought_asset=receiving_asset, bought_amount=receiving_amount, paid_with_asset=selling_asset, trade_rate=1 / trade_rate, fee_in_profit_currency=total_fee_in_profit_currency, fee_currency=receiving_asset, # does not matter timestamp=timestamp, is_virtual=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: - 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 """ 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() return instance._cryptocompare.query_historical_price( from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, historical_data_start=instance._historical_data_start, )
def set_main_currency(self, currency: Asset) -> None: if not currency.is_fiat(): raise ValueError( f'Attempted to set unsupported "{str(currency)}" as main currency.', ) self.profit_currency = currency self.events.profit_currency = currency
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' # Check cache price_cache_entry = GlobalDBHandler().get_historical_price( from_asset=from_fiat_currency, to_asset=to_fiat_currency, timestamp=timestamp, max_seconds_distance=DAY_IN_SECONDS, ) if price_cache_entry: return price_cache_entry.price try: prices_map = get_historical_xratescom_exchange_rates( from_asset=from_fiat_currency, time=timestamp, ) except RemoteError: return None # Since xratecoms has daily rates let's save at timestamp of UTC day start for asset, asset_price in prices_map.items(): GlobalDBHandler().add_historical_prices(entries=[ HistoricalPrice( from_asset=from_fiat_currency, to_asset=asset, source=HistoricalPriceOracle.XRATESCOM, timestamp=timestamp_to_daystart_timestamp(timestamp), price=asset_price, ) ]) if asset == to_fiat_currency: rate = asset_price log.debug('Historical fiat exchange rate query succesful', rate=rate) return rate
def _save_forex_rate( date: str, from_currency: Asset, to_currency: Asset, price: FVal, ) -> None: assert from_currency.is_fiat(), 'fiat currency should have been provided' assert to_currency.is_fiat(), 'fiat currency should have been provided' instance = Inquirer() if date not in instance._cached_forex_data: instance._cached_forex_data[date] = {} if from_currency not in instance._cached_forex_data[date]: instance._cached_forex_data[date][from_currency] = {} msg = 'Cached value should not already exist' assert to_currency not in instance._cached_forex_data[date][from_currency], msg instance._cached_forex_data[date][from_currency][to_currency] = price instance.save_historical_forex_data()
def add_fiat_balance(self, currency: Asset, amount: FVal) -> None: assert currency.is_fiat() cursor = self.conn.cursor() # We don't care about previous value so this simple insert or replace should work cursor.execute( 'INSERT OR REPLACE INTO current_balances(asset, amount) VALUES (?, ?)', (currency.identifier, str(amount)), ) self.conn.commit() self.update_last_write()
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 """ 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=FiatAsset(from_asset.identifier), to_fiat_currency=FiatAsset(to_asset.identifier), timestamp=timestamp, ) if price is not None: return price # else cryptocompare also has historical fiat to fiat data instance = PriceHistorian() return instance._cryptocompare.query_historical_price( from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, historical_data_start=instance._historical_data_start, )
def _query_currency_converterapi(base: Asset, quote: Asset) -> Optional[Price]: assert base.is_fiat(), 'fiat currency should have been provided' assert quote.is_fiat(), 'fiat currency should have been provided' log.debug( 'Query free.currencyconverterapi.com fiat pair', base_currency=base.identifier, quote_currency=quote.identifier, ) pair = f'{base.identifier}_{quote.identifier}' querystr = (f'https://free.currconv.com/api/v7/convert?' f'q={pair}&compact=ultra&apiKey={CURRENCYCONVERTER_API_KEY}') try: resp = request_get_dict(querystr) return Price(FVal(resp[pair])) except (ValueError, RemoteError, KeyError, UnableToDecryptRemoteData): log.error( 'Querying free.currencyconverterapi.com fiat pair failed', base_currency=base.identifier, quote_currency=quote.identifier, ) return None
def find_usd_price( asset: Asset, ignore_cache: bool = False, ) -> Price: """Returns the current USD price of the asset Returns Price(ZERO) if all options have been exhausted and errors are logged in the logs """ if asset == A_USD: return Price(FVal(1)) instance = Inquirer() cache_key = (asset, A_USD) if ignore_cache is False: cache = instance.get_cached_current_price_entry( cache_key=cache_key) if cache is not None: return cache.price if asset.is_fiat(): try: return instance._query_fiat_pair(base=asset, quote=A_USD) except RemoteError: pass # continue, a price can be found by one of the oracles (CC for example) if asset in instance.special_tokens: ethereum = instance._ethereum assert ethereum, 'Inquirer should never be called before the injection of ethereum' token = EthereumToken.from_asset(asset) assert token, 'all assets in special tokens are already ethereum tokens' underlying_asset_price = get_underlying_asset_price(token) usd_price = handle_defi_price_query( ethereum=ethereum, token=token, underlying_asset_price=underlying_asset_price, ) if usd_price is None: price = Price(ZERO) else: price = Price(usd_price) Inquirer._cached_current_price[cache_key] = CachedPriceEntry( price=price, time=ts_now()) # noqa: E501 return price return instance._query_oracle_instances(from_asset=asset, to_asset=A_USD)
def set_settings(self, settings: Dict[str, Any]) -> Tuple[bool, str]: log.info('Add new settings') message = '' with self.lock: if 'eth_rpc_endpoint' in settings: result, msg = self.blockchain.set_eth_rpc_endpoint( settings['eth_rpc_endpoint']) if not result: # Don't save it in the DB del settings['eth_rpc_endpoint'] message += "\nEthereum RPC endpoint not set: " + msg if 'main_currency' in settings: given_symbol = settings['main_currency'] try: main_currency = Asset(given_symbol) except UnknownAsset: return False, f'Unknown fiat currency {given_symbol} provided' except DeserializationError: return False, 'Non string type given for fiat currency' if not main_currency.is_fiat(): msg = ( f'Provided symbol for main currency {given_symbol} is ' f'not a fiat currency') return False, msg if main_currency != A_USD: self.usd_to_main_currency_rate = Inquirer( ).query_fiat_pair( A_USD, main_currency, ) res, msg = self.accountant.customize(settings) if not res: message += '\n' + msg return False, message _, msg, = self.data.set_settings(settings, self.accountant) if msg != '': message += '\n' + msg # Always return success here but with a message return True, message
def set_main_currency(self, currency_string: str) -> Tuple[bool, str]: """Takes a currency string from the API and sets it as the main currency for rotki Returns True and empty string for success and False and error string for error """ try: currency = Asset(currency_string) except UnknownAsset: msg = f'An unknown asset {currency_string} was given for main currency' log.critical(msg) return False, msg if not currency.is_fiat(): msg = f'A non-fiat asset {currency_string} was given for main currency' log.critical(msg) return False, msg fiat_currency = FiatAsset(currency.identifier) with self.lock: self.data.set_main_currency(fiat_currency, self.accountant) return True, ''
def get_asset_balance_total(asset_symbol: str, setup: BalancesTestSetup) -> FVal: conversion_function = satoshis_to_btc if asset_symbol == 'BTC' else from_wei total = ZERO asset = Asset(asset_symbol) if asset.is_fiat(): total += setup.fiat_balances.get(asset_symbol, ZERO) elif asset_symbol in ('ETH', 'BTC'): asset_balances = getattr(setup, f'{asset_symbol.lower()}_balances') total += sum(conversion_function(FVal(b)) for b in asset_balances) elif EthereumToken(asset_symbol): asset_balances = setup.token_balances[EthereumToken(asset_symbol)] total += sum(conversion_function(FVal(b)) for b in asset_balances) else: raise AssertionError(f'not implemented for asset {asset_symbol}') total += setup.binance_balances.get(asset_symbol, ZERO) total += setup.poloniex_balances.get(asset_symbol, ZERO) if setup.manually_tracked_balances: for entry in setup.manually_tracked_balances: if entry.asset.identifier == asset_symbol: total += entry.amount return total
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 """ # 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): return price try: data = self.get_historical_data( from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, only_check_cache=True, ) except UnsupportedAsset as e: raise PriceQueryUnsupportedAsset(e.asset_name) from e price = self._retrieve_price_from_data( data=data, from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, ) if price == Price(ZERO): 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 == Price(ZERO): 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 cryptocompare', from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, price=price, ) return price
def add_buy_and_corresponding_sell( self, bought_asset: Asset, bought_amount: FVal, paid_with_asset: Asset, trade_rate: FVal, fee_in_profit_currency: Fee, fee_currency: Asset, timestamp: Timestamp, ) -> None: self.add_buy( 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, is_virtual=False, ) if paid_with_asset.is_fiat() or not self.include_crypto2crypto: return # else you are also selling some other asset to buy the bought asset log.debug( f'Buying {bought_asset} with {paid_with_asset} also introduces a virtual sell event', ) try: bought_asset_rate_in_profit_currency = self.get_rate_in_profit_currency( bought_asset, timestamp, ) except (NoPriceForGivenTimestamp, PriceQueryUnknownFromAsset): bought_asset_rate_in_profit_currency = FVal(-1) if bought_asset_rate_in_profit_currency != FVal(-1): # The asset bought does not have a price yet # Can happen for Token sales, presales e.t.c. with_bought_asset_gain = bought_asset_rate_in_profit_currency * bought_amount receiving_asset = bought_asset receiving_amount = bought_amount rate_in_profit_currency = bought_asset_rate_in_profit_currency / trade_rate gain_in_profit_currency = with_bought_asset_gain sold_amount = trade_rate * bought_amount sold_asset_rate_in_profit_currency = self.get_rate_in_profit_currency( paid_with_asset, timestamp, ) with_sold_asset_gain = sold_asset_rate_in_profit_currency * sold_amount # Consider as value of the sell what would give the least profit if (bought_asset_rate_in_profit_currency == -1 or with_sold_asset_gain < with_bought_asset_gain): receiving_asset = self.profit_currency receiving_amount = with_sold_asset_gain trade_rate = sold_asset_rate_in_profit_currency rate_in_profit_currency = sold_asset_rate_in_profit_currency gain_in_profit_currency = with_sold_asset_gain self.add_sell( selling_asset=paid_with_asset, selling_amount=sold_amount, receiving_asset=receiving_asset, receiving_amount=receiving_amount, trade_rate=trade_rate, rate_in_profit_currency=rate_in_profit_currency, gain_in_profit_currency=gain_in_profit_currency, total_fee_in_profit_currency=fee_in_profit_currency, timestamp=timestamp, is_virtual=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: - 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 _scrape_xratescom_exchange_rates(url: str) -> Dict[Asset, Price]: """ Scrapes x-rates.com website for the exchange rates tables May raise: - RemoteError if we can't query x-rates.com """ log.debug(f'Querying x-rates.com stats: {url}') prices = {} try: response = requests.get(url=url, timeout=DEFAULT_TIMEOUT_TUPLE) except requests.exceptions.RequestException as e: raise RemoteError(f'x-rates.com request {url} failed due to {str(e)}') from e if response.status_code != 200: raise RemoteError( f'x-rates.com request {url} failed with code: {response.status_code}' f' and response: {response.text}', ) soup = BeautifulSoup( response.text, 'html.parser', parse_only=SoupStrainer('table', {'class': 'tablesorter ratesTable'}), ) if soup is None: raise RemoteError('Could not find <table> while parsing x-rates stats page') try: tr = soup.table.tbody.tr except AttributeError as e: raise RemoteError('Could not find first <tr> while parsing x-rates.com page') from e while tr is not None: secondtd = tr.select('td:nth-of-type(2)')[0] try: href = secondtd.a['href'] except (AttributeError, KeyError) as e: raise RemoteError('Could not find a href of 2nd td while parsing x-rates.com page') from e # noqa: E501 parts = href.split('to=') if len(parts) != 2: raise RemoteError(f'Could not find to= in {href} while parsing x-rates.com page') try: to_asset = Asset(parts[1]) if not to_asset.is_fiat(): raise ValueError except (UnknownAsset, ValueError): log.debug(f'Skipping {parts[1]} asset because its not a known fiat asset while parsing x-rates.com page') # noqa: E501 tr = tr.find_next_sibling() continue try: price = deserialize_price(secondtd.a.text) except DeserializationError as e: log.debug(f'Could not parse x-rates.com rate of {to_asset.identifier} due to {str(e)}. Skipping ...') # noqa: E501 tr = tr.find_next_sibling() continue prices[to_asset] = price tr = tr.find_next_sibling() return prices
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 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 find_usd_price( asset: Asset, ignore_cache: bool = False, ) -> Price: """Returns the current USD price of the asset Returns Price(ZERO) if all options have been exhausted and errors are logged in the logs """ if asset == A_USD: return Price(FVal(1)) instance = Inquirer() cache_key = (asset, A_USD) if ignore_cache is False: cache = instance.get_cached_current_price_entry( cache_key=cache_key) if cache is not None: return cache.price if asset.is_fiat(): try: return instance._query_fiat_pair(base=asset, quote=A_USD) except RemoteError: pass # continue, a price can be found by one of the oracles (CC for example) # Try and check if it is an ethereum token with specified protocol or underlying tokens is_known_protocol = False underlying_tokens = None try: token = EthereumToken.from_asset(asset) if token is not None: if token.protocol is not None: is_known_protocol = token.protocol in KnownProtocolsAssets underlying_tokens = GlobalDBHandler( ).get_ethereum_token( # type: ignore token.ethereum_address, ).underlying_tokens except UnknownAsset: pass # Check if it is a special token if asset in instance.special_tokens: ethereum = instance._ethereum assert ethereum, 'Inquirer should never be called before the injection of ethereum' assert token, 'all assets in special tokens are already ethereum tokens' underlying_asset_price = get_underlying_asset_price(token) usd_price = handle_defi_price_query( ethereum=ethereum, token=token, underlying_asset_price=underlying_asset_price, ) if usd_price is None: price = Price(ZERO) else: price = Price(usd_price) Inquirer._cached_current_price[cache_key] = CachedPriceEntry( price=price, time=ts_now()) # noqa: E501 return price if is_known_protocol is True or underlying_tokens is not None: assert token is not None result = get_underlying_asset_price(token) if result is None: usd_price = Price(ZERO) if instance._ethereum is not None: instance._ethereum.msg_aggregator.add_warning( f'Could not find price for {token}', ) else: usd_price = Price(result) Inquirer._cached_current_price[cache_key] = CachedPriceEntry( price=usd_price, time=ts_now(), ) return usd_price # BSQ is a special asset that doesnt have oracle information but its custom API if asset == A_BSQ: try: price_in_btc = get_bisq_market_price(asset) btc_price = Inquirer().find_usd_price(A_BTC) usd_price = Price(price_in_btc * btc_price) Inquirer._cached_current_price[cache_key] = CachedPriceEntry( price=usd_price, time=ts_now(), ) return usd_price except (RemoteError, DeserializationError) as e: msg = f'Could not find price for BSQ. {str(e)}' if instance._ethereum is not None: instance._ethereum.msg_aggregator.add_warning(msg) return Price(BTC_PER_BSQ * price_in_btc) if asset == A_KFEE: # KFEE is a kraken special asset where 1000 KFEE = 10 USD return Price(FVal(0.01)) return instance._query_oracle_instances(from_asset=asset, to_asset=A_USD)
def add_buy_and_corresponding_sell( self, location: Location, bought_asset: Asset, bought_amount: FVal, paid_with_asset: Asset, trade_rate: FVal, fee_in_profit_currency: Fee, timestamp: Timestamp, ) -> None: """ Account for the given buy and the corresponding sell if it's a crypto to crypto 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 cryptocompare - RemoteError if there is a problem reaching the price oracle server or with reading the response returned by the server """ self.add_buy( 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, timestamp=timestamp, is_virtual=False, ) if paid_with_asset.is_fiat() or not self.include_crypto2crypto: return # else you are also selling some other asset to buy the bought asset log.debug( f'Buying {bought_asset} with {paid_with_asset} also introduces a virtual sell event', ) try: bought_asset_rate_in_profit_currency = self.get_rate_in_profit_currency( bought_asset, timestamp, ) except (NoPriceForGivenTimestamp, PriceQueryUnsupportedAsset): bought_asset_rate_in_profit_currency = FVal(-1) if bought_asset_rate_in_profit_currency != FVal(-1): # The asset bought does not have a price yet # Can happen for Token sales, presales e.t.c. with_bought_asset_gain = bought_asset_rate_in_profit_currency * bought_amount receiving_asset = bought_asset receiving_amount = bought_amount rate_in_profit_currency = bought_asset_rate_in_profit_currency / trade_rate gain_in_profit_currency = with_bought_asset_gain sold_amount = trade_rate * bought_amount if sold_amount == ZERO: logger.error( f'Not adding a virtual sell event. Could not calculate it from ' f'trade_rate * bought_amount = {trade_rate} * {bought_amount}', ) return sold_asset_rate_in_profit_currency = self.get_rate_in_profit_currency( paid_with_asset, timestamp, ) with_sold_asset_gain = sold_asset_rate_in_profit_currency * sold_amount # Consider as value of the sell what would give the least profit if (bought_asset_rate_in_profit_currency == -1 or with_sold_asset_gain < with_bought_asset_gain): receiving_asset = self.profit_currency receiving_amount = with_sold_asset_gain rate_in_profit_currency = sold_asset_rate_in_profit_currency gain_in_profit_currency = with_sold_asset_gain self.add_sell( location=location, selling_asset=paid_with_asset, selling_amount=sold_amount, receiving_asset=receiving_asset, receiving_amount=receiving_amount, rate_in_profit_currency=rate_in_profit_currency, gain_in_profit_currency=gain_in_profit_currency, total_fee_in_profit_currency=fee_in_profit_currency, timestamp=timestamp, is_virtual=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 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 find_usd_price( asset: Asset, ignore_cache: bool = False, ) -> Price: """Returns the current USD price of the asset Returns Price(ZERO) if all options have been exhausted and errors are logged in the logs """ if asset == A_USD: return Price(FVal(1)) instance = Inquirer() cache_key = (asset, A_USD) if ignore_cache is False: cache = instance.get_cached_current_price_entry( cache_key=cache_key) if cache is not None: return cache.price if asset.is_fiat(): try: return instance._query_fiat_pair(base=asset, quote=A_USD) except RemoteError: pass # continue, a price can be found by one of the oracles (CC for example) # Try and check if it is an ethereum token with specified protocol or underlying tokens is_known_protocol = False underlying_tokens = None try: token = EthereumToken.from_asset(asset) if token is not None: if token.protocol is not None: is_known_protocol = token.protocol in KnownProtocolsAssets underlying_tokens = GlobalDBHandler( ).get_ethereum_token( # type: ignore token.ethereum_address, ).underlying_tokens except UnknownAsset: pass # Check if it is a special token if asset in instance.special_tokens: ethereum = instance._ethereum assert ethereum, 'Inquirer should never be called before the injection of ethereum' assert token, 'all assets in special tokens are already ethereum tokens' underlying_asset_price = get_underlying_asset_price(token) usd_price = handle_defi_price_query( ethereum=ethereum, token=token, underlying_asset_price=underlying_asset_price, ) if usd_price is None: price = Price(ZERO) else: price = Price(usd_price) Inquirer._cached_current_price[cache_key] = CachedPriceEntry( price=price, time=ts_now()) # noqa: E501 return price if is_known_protocol is True or underlying_tokens is not None: assert token is not None result = get_underlying_asset_price(token) usd_price = Price(ZERO) if result is None else Price(result) Inquirer._cached_current_price[cache_key] = CachedPriceEntry( price=usd_price, time=ts_now(), ) return usd_price return instance._query_oracle_instances(from_asset=asset, to_asset=A_USD)
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 add_sell( self, location: Location, selling_asset: Asset, selling_amount: FVal, receiving_asset: Optional[Asset], receiving_amount: Optional[FVal], gain_in_profit_currency: FVal, total_fee_in_profit_currency: Fee, rate_in_profit_currency: FVal, timestamp: Timestamp, loan_settlement: bool = False, is_virtual: bool = False, ) -> None: """Account for the given sell action 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 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 selling_asset.is_fiat() and receiving_asset and not receiving_asset.is_fiat()) if skip_trade: return if selling_amount == ZERO: logger.error( f'Skipping sell trade of {selling_asset.identifier} for ' f'{receiving_asset.identifier if receiving_asset else "nothing"} at {timestamp}' f' since the selling amount is 0', ) return if selling_asset.is_fiat(): # Should be handled by a virtual buy logger.debug( f'Skipping sell trade of {selling_asset.identifier} for ' f'{receiving_asset.identifier if receiving_asset else "nothing"} at {timestamp} ' f'since selling of FIAT of something will just be treated as a buy.', ) return logger.debug( f'Processing sell trade of {selling_asset.identifier} for ' f'{receiving_asset.identifier if receiving_asset else "nothing"} at {timestamp}', ) self.cost_basis.spend_asset( location=location, timestamp=timestamp, asset=selling_asset, amount=selling_amount, rate=rate_in_profit_currency, fee_in_profit_currency=total_fee_in_profit_currency, gain_in_profit_currency=gain_in_profit_currency, ) self.handle_prefork_asset_sells(selling_asset, selling_amount, timestamp) # now search the acquisitions for `paid_with_asset` and calculate profit/loss cost_basis_info = self.cost_basis.calculate_spend_cost_basis( spending_amount=selling_amount, spending_asset=selling_asset, timestamp=timestamp, ) general_profit_loss = ZERO taxable_profit_loss = ZERO # If we don't include crypto2crypto and we sell for crypto, stop here if receiving_asset and not receiving_asset.is_fiat( ) 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=cost_basis_info.taxable_amount, rate_in_profit_currency=rate_in_profit_currency, total_fee_in_profit_currency=total_fee_in_profit_currency, selling_amount=selling_amount, ) total_bought_cost_in_profit_currency = ( cost_basis_info.taxfree_bought_cost + cost_basis_info.taxable_bought_cost + total_fee_in_profit_currency) general_profit_loss = gain_in_profit_currency - total_bought_cost_in_profit_currency taxable_profit_loss = taxable_gain - cost_basis_info.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 expected = rate_in_profit_currency * selling_amount + total_fee_in_profit_currency msg = ( f'Expected settlement loss mismatch. rate_in_profit_currency' f' ({rate_in_profit_currency}) * selling_amount' f' ({selling_amount}) + total_fee_in_profit_currency' f' ({total_fee_in_profit_currency}) != settlement_loss ' f'({settlement_loss})') assert expected == settlement_loss, msg self.settlement_losses += settlement_loss log.debug( 'Loan Settlement Loss', settlement_loss=settlement_loss, profit_currency=self.profit_currency, ) else: log.debug( "After Sell Profit/Loss", taxable_profit_loss=taxable_profit_loss, general_profit_loss=general_profit_loss, profit_currency=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( location=location, 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, cost_basis_info=cost_basis_info, ) else: assert receiving_asset, 'Here receiving asset should have a value' self.csv_exporter.add_sell( location=location, 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=cost_basis_info.taxable_amount, taxable_bought_cost=cost_basis_info.taxable_bought_cost, timestamp=timestamp, cost_basis_info=cost_basis_info, is_virtual=is_virtual, total_bought_cost=total_bought_cost_in_profit_currency, )
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
def add_buy( self, location: Location, bought_asset: Asset, bought_amount: FVal, paid_with_asset: Asset, trade_rate: FVal, fee_in_profit_currency: Fee, timestamp: Timestamp, is_virtual: bool = False, is_from_prefork_virtual_buy: 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, timestamp=timestamp, is_from_prefork_virtual_buy=is_from_prefork_virtual_buy, ) gross_cost = bought_amount * buy_rate cost_in_profit_currency = gross_cost + fee_in_profit_currency self.cost_basis.obtain_asset( location=location, timestamp=timestamp, description='trade', asset=bought_asset, amount=bought_amount, rate=buy_rate, fee_in_profit_currency=fee_in_profit_currency, ) if timestamp >= self.query_start_ts: self.csv_exporter.add_buy( location=location, bought_asset=bought_asset, rate_in_profit_currency=buy_rate, fee_cost=fee_in_profit_currency, amount=bought_amount, cost_in_profit_currency=cost_in_profit_currency, paid_with_asset=paid_with_asset, paid_with_asset_rate=paid_with_asset_rate, paid_with_asset_amount=trade_rate * bought_amount, timestamp=timestamp, is_virtual=is_virtual, )
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 add_sell_and_corresponding_buy( self, location: Location, selling_asset: Asset, selling_amount: FVal, receiving_asset: Asset, receiving_amount: FVal, gain_in_profit_currency: FVal, total_fee_in_profit_currency: Fee, trade_rate: FVal, rate_in_profit_currency: FVal, timestamp: Timestamp, ) -> None: """ Account for the given sell and the corresponding buy if it's a crypto to crypto Args: selling_asset (str): The ticker representation of the asset we sell. selling_amount (FVal): The amount of `selling_asset` for sale. receiving_asset (str): The ticker representation of the asset we receive in exchange for `selling_asset`. receiving_amount (FVal): The amount of `receiving_asset` we receive. gain_in_profit_currency (FVal): This is the amount of `profit_currency` equivalent we receive after doing this trade. Fees are not counted in this. total_fee_in_profit_currency (FVal): This is the amount of `profit_currency` equivalent we pay in fees after doing this trade. trade_rate (FVal): How much does 1 unit of `receiving_asset` cost in `selling_asset` rate_in_profit_currency (FVal): The equivalent of `trade_rate` in `profit_currency` timestamp (int): The timestamp for the trade 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 cryptocompare - RemoteError if there is a problem reaching the price oracle server or with reading the response returned by the server """ self.add_sell( location=location, selling_asset=selling_asset, selling_amount=selling_amount, receiving_asset=receiving_asset, receiving_amount=receiving_amount, gain_in_profit_currency=gain_in_profit_currency, total_fee_in_profit_currency=total_fee_in_profit_currency, rate_in_profit_currency=rate_in_profit_currency, timestamp=timestamp, is_virtual=False, ) if receiving_asset.is_fiat() or not self.include_crypto2crypto: return log.debug( f'Selling {selling_asset} for {receiving_asset} also introduces a virtual buy event', ) # else then you are also buying some other asset through your sell assert trade_rate != 0, 'Trade rate should not be zero at this point' self.add_buy( location=location, bought_asset=receiving_asset, bought_amount=receiving_amount, paid_with_asset=selling_asset, trade_rate=1 / trade_rate, fee_in_profit_currency=total_fee_in_profit_currency, timestamp=timestamp, is_virtual=True, )
def add_sell( self, location: Location, selling_asset: Asset, selling_amount: FVal, receiving_asset: Optional[Asset], receiving_amount: Optional[FVal], gain_in_profit_currency: FVal, total_fee_in_profit_currency: Fee, trade_rate: FVal, rate_in_profit_currency: FVal, timestamp: Timestamp, loan_settlement: bool = False, is_virtual: bool = False, ) -> None: """Account for the given sell action 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 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 selling_asset.is_fiat() and receiving_asset and not receiving_asset.is_fiat()) if skip_trade: return if selling_asset not in self.events: self.events[selling_asset] = Events([], []) 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, ), ) self.handle_prefork_asset_sells(selling_asset, selling_amount, timestamp) if loan_settlement: log.debug( 'Loan Settlement Selling Event', sensitive_log=True, selling_amount=selling_amount, selling_asset=selling_asset, gain_in_profit_currency=gain_in_profit_currency, profit_currency=self.profit_currency, timestamp=timestamp, ) else: log.debug( 'Selling Event', sensitive_log=True, selling_amount=selling_amount, selling_asset=selling_asset, receiving_amount=receiving_amount, receiving_asset=receiving_asset, rate=trade_rate, rate_in_profit_currency=rate_in_profit_currency, profit_currency=self.profit_currency, gain_in_profit_currency=gain_in_profit_currency, fee_in_profit_currency=total_fee_in_profit_currency, timestamp=timestamp, ) # 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 = ZERO taxable_profit_loss = ZERO # If we don't include crypto2crypto and we sell for crypto, stop here if receiving_asset and not receiving_asset.is_fiat( ) 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 expected = rate_in_profit_currency * selling_amount + total_fee_in_profit_currency msg = ( f'Expected settlement loss mismatch. rate_in_profit_currency' f' ({rate_in_profit_currency}) * selling_amount' f' ({selling_amount}) + total_fee_in_profit_currency' f' ({total_fee_in_profit_currency}) != settlement_loss ' f'({settlement_loss})') assert expected == settlement_loss, msg self.settlement_losses += settlement_loss log.debug( 'Loan Settlement Loss', sensitive_log=True, settlement_loss=settlement_loss, profit_currency=self.profit_currency, ) else: log.debug( "After Sell Profit/Loss", sensitive_log=True, taxable_profit_loss=taxable_profit_loss, general_profit_loss=general_profit_loss, profit_currency=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( location=location, 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: assert receiving_asset, 'Here receiving asset should have a value' self.csv_exporter.add_sell( location=location, 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, )