diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 380070deb..5ae20afa1 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -3,11 +3,13 @@ Module that define classes to convert Crypto-currency to FIAT e.g BTC to USD """ +import datetime import logging from typing import Dict from cachetools.ttl import TTLCache from pycoingecko import CoinGeckoAPI +from requests.exceptions import RequestException from freqtrade.constants import SUPPORTED_FIAT @@ -25,6 +27,7 @@ class CryptoToFiatConverter: _coingekko: CoinGeckoAPI = None _cryptomap: Dict = {} + _backoff: float = 0.0 def __new__(cls): """ @@ -47,8 +50,21 @@ class CryptoToFiatConverter: def _load_cryptomap(self) -> None: try: coinlistings = self._coingekko.get_coins_list() - # Create mapping table from synbol to coingekko_id + # Create mapping table from symbol to coingekko_id self._cryptomap = {x['symbol']: x['id'] for x in coinlistings} + except RequestException as request_exception: + if "429" in str(request_exception): + logger.warning( + "Too many requests for Coingecko API, backing off and trying again later.") + # Set backoff timestamp to 60 seconds in the future + self._backoff = datetime.datetime.now().timestamp() + 60 + return + # If the request is not a 429 error we want to raise the normal error + logger.error( + "Could not load FIAT Cryptocurrency map for the following problem: {}".format( + request_exception + ) + ) except (Exception) as exception: logger.error( f"Could not load FIAT Cryptocurrency map for the following problem: {exception}") @@ -127,6 +143,15 @@ class CryptoToFiatConverter: if crypto_symbol == fiat_symbol: return 1.0 + if self._cryptomap == {}: + if self._backoff <= datetime.datetime.now().timestamp(): + self._load_cryptomap() + # return 0.0 if we still dont have data to check, no reason to proceed + if self._cryptomap == {}: + return 0.0 + else: + return 0.0 + if crypto_symbol not in self._cryptomap: # return 0 for unsupported stake currencies (fiat-convert should not break the bot) logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol) diff --git a/tests/rpc/test_fiat_convert.py b/tests/rpc/test_fiat_convert.py index 2d43addff..5174f9416 100644 --- a/tests/rpc/test_fiat_convert.py +++ b/tests/rpc/test_fiat_convert.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, # pragma pylint: disable=protected-access, C0103 +import datetime from unittest.mock import MagicMock import pytest @@ -21,6 +22,12 @@ def test_fiat_convert_is_supported(mocker): def test_fiat_convert_find_price(mocker): fiat_convert = CryptoToFiatConverter() + fiat_convert._cryptomap = {} + fiat_convert._backoff = 0 + mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._load_cryptomap', + return_value=None) + assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 0.0 + with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'): fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC') @@ -115,6 +122,28 @@ def test_fiat_convert_without_network(mocker): CryptoToFiatConverter._coingekko = cmc_temp +def test_fiat_too_many_requests_response(mocker, caplog): + # Because CryptoToFiatConverter is a Singleton we reset the listings + req_exception = "429 Too Many Requests" + listmock = MagicMock(return_value="{}", side_effect=RequestException(req_exception)) + mocker.patch.multiple( + 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', + get_coins_list=listmock, + ) + # with pytest.raises(RequestEsxception): + fiat_convert = CryptoToFiatConverter() + fiat_convert._cryptomap = {} + fiat_convert._load_cryptomap() + + length_cryptomap = len(fiat_convert._cryptomap) + assert length_cryptomap == 0 + assert fiat_convert._backoff > datetime.datetime.now().timestamp() + assert log_has( + 'Too many requests for Coingecko API, backing off and trying again later.', + caplog + ) + + def test_fiat_invalid_response(mocker, caplog): # Because CryptoToFiatConverter is a Singleton we reset the listings listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}")