def test_manual_oracle_correctly_returns_price(globaldb, fake_price_historian): """Test that the manual oracle correctly returns price for asset""" price_historian = fake_price_historian # Add price at timestamp globaldb.add_single_historical_price( HistoricalPrice( from_asset=A_BTC, to_asset=A_USD, price=30000, timestamp=Timestamp(1611595470), source=HistoricalPriceOracle.MANUAL, ), ) # Make the other oracles fail expected_price = Price(FVal('30000')) oracle_instances = price_historian._oracle_instances oracle_instances[ 1].query_historical_price.side_effect = PriceQueryUnsupportedAsset( 'bitcoin') oracle_instances[ 2].query_historical_price.side_effect = PriceQueryUnsupportedAsset( 'bitcoin') # Query price, should return the manual price price = price_historian.query_historical_price( from_asset=A_BTC, to_asset=A_USD, timestamp=Timestamp(1611595466), ) assert price == expected_price # Try to get manual price for a timestamp not in db with pytest.raises(NoPriceForGivenTimestamp): price = price_historian.query_historical_price( from_asset=A_BTC, to_asset=A_USD, timestamp=Timestamp(1610595466), )
def query_endpoint_price( self, from_asset: Asset, to_asset: Asset, handling_special_case: bool = False, ) -> Dict[str, Any]: """Returns the current price of an asset compared to another asset - May raise RemoteError if there is a problem reaching the cryptocompare server or with reading the response returned by the server - May raise PriceQueryUnsupportedAsset if from/to assets are not known to cryptocompare """ special_asset = (from_asset in CRYPTOCOMPARE_SPECIAL_CASES or to_asset in CRYPTOCOMPARE_SPECIAL_CASES) if special_asset and not handling_special_case: return self._special_case_handling( method_name='query_endpoint_price', from_asset=from_asset, to_asset=to_asset, ) try: cc_from_asset_symbol = from_asset.to_cryptocompare() cc_to_asset_symbol = to_asset.to_cryptocompare() except UnsupportedAsset as e: raise PriceQueryUnsupportedAsset(e.asset_name) query_path = f'price?fsym={cc_from_asset_symbol}&tsyms={cc_to_asset_symbol}' result = self._api_query(path=query_path) return result
def query_current_price( self, from_asset: Asset, to_asset: Asset, handling_special_case: bool = False, ) -> Price: """Returns the current price of an asset compared to another asset - May raise RemoteError if there is a problem reaching the cryptocompare server or with reading the response returned by the server - May raise PriceQueryUnsupportedAsset if from/to assets are not known to cryptocompare """ special_asset = (from_asset.identifier in CRYPTOCOMPARE_SPECIAL_CASES or to_asset.identifier in CRYPTOCOMPARE_SPECIAL_CASES) if special_asset and not handling_special_case: return self._special_case_handling( method_name='query_current_price', from_asset=from_asset, to_asset=to_asset, ) try: cc_from_asset_symbol = from_asset.to_cryptocompare() cc_to_asset_symbol = to_asset.to_cryptocompare() except UnsupportedAsset as e: raise PriceQueryUnsupportedAsset(e.asset_name) from e query_path = f'price?fsym={cc_from_asset_symbol}&tsyms={cc_to_asset_symbol}' result = self._api_query(path=query_path) # Up until 23/09/2020 cryptocompare may return {} due to bug. # Handle that case by assuming 0 if that happens if cc_to_asset_symbol not in result: return Price(ZERO) return Price(FVal(result[cc_to_asset_symbol]))
def query_endpoint_pricehistorical( self, from_asset: Asset, to_asset: Asset, timestamp: Timestamp, handling_special_case: bool = False, ) -> Price: """Queries the historical daily price of from_asset to to_asset for timestamp - May raise RemoteError if there is a problem reaching the cryptocompare server or with reading the response returned by the server - May raise PriceQueryUnsupportedAsset if from/to assets are not known to cryptocompare """ log.debug( 'Querying cryptocompare for daily historical price', from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, ) special_asset = ( from_asset in CRYPTOCOMPARE_SPECIAL_CASES or to_asset in CRYPTOCOMPARE_SPECIAL_CASES ) if special_asset and not handling_special_case: return self._special_case_handling( method_name='query_endpoint_pricehistorical', from_asset=from_asset, to_asset=to_asset, timestamp=timestamp, ) try: cc_from_asset_symbol = from_asset.to_cryptocompare() cc_to_asset_symbol = to_asset.to_cryptocompare() except UnsupportedAsset as e: raise PriceQueryUnsupportedAsset(e.asset_name) from e query_path = ( f'pricehistorical?fsym={cc_from_asset_symbol}&tsyms={cc_to_asset_symbol}' f'&ts={timestamp}' ) if to_asset == 'BTC': query_path += '&tryConversion=false' result = self._api_query(query_path) # Up until 23/09/2020 cryptocompare may return {} due to bug. # Handle that case by assuming 0 if that happens if ( cc_from_asset_symbol not in result or cc_to_asset_symbol not in result[cc_from_asset_symbol] ): return Price(ZERO) return Price(FVal(result[cc_from_asset_symbol][cc_to_asset_symbol]))
def test_token_to_fiat_via_second_oracle(fake_price_historian): """Test price is returned via the second oracle when the first oracle fails requesting the historical price from token to fiat. """ price_historian = fake_price_historian expected_price = Price(FVal('30000')) oracle_instances = price_historian._oracle_instances oracle_instances[0].query_historical_price.side_effect = PriceQueryUnsupportedAsset('bitcoin') oracle_instances[1].query_historical_price.return_value = expected_price price = price_historian.query_historical_price( from_asset=A_BTC, to_asset=A_USD, timestamp=Timestamp(1611595466), ) assert price == expected_price for oracle_instance in price_historian._oracle_instances[0:2]: assert oracle_instance.query_historical_price.call_count == 1
def query_endpoint_histohour( self, from_asset: Asset, to_asset: Asset, limit: int, to_timestamp: Timestamp, handling_special_case: bool = False, ) -> Dict[str, Any]: """Returns the full histohour response including TimeFrom and TimeTo - May raise RemoteError if there is a problem reaching the cryptocompare server or with reading the response returned by the server - May raise PriceQueryUnsupportedAsset if from/to assets are not known to cryptocompare """ special_asset = ( from_asset in CRYPTOCOMPARE_SPECIAL_CASES or to_asset in CRYPTOCOMPARE_SPECIAL_CASES ) if special_asset and not handling_special_case: return self._special_case_handling( method_name='query_endpoint_histohour', from_asset=from_asset, to_asset=to_asset, limit=limit, to_timestamp=to_timestamp, ) try: cc_from_asset_symbol = from_asset.to_cryptocompare() cc_to_asset_symbol = to_asset.to_cryptocompare() except UnsupportedAsset as e: raise PriceQueryUnsupportedAsset(e.asset_name) from e query_path = ( f'v2/histohour?fsym={cc_from_asset_symbol}&tsym={cc_to_asset_symbol}' f'&limit={limit}&toTs={to_timestamp}' ) result = self._api_query(path=query_path) return result
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( 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 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