diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index d40f9221e..99d06dde0 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -7,7 +7,7 @@ import logging import time from typing import Dict, List -from coinmarketcap import Market +from pycoingecko import CoinGeckoAPI from freqtrade.constants import SUPPORTED_FIAT @@ -38,8 +38,8 @@ class CryptoFiat: # Private attributes self._expiration = 0.0 - self.crypto_symbol = crypto_symbol.upper() - self.fiat_symbol = fiat_symbol.upper() + self.crypto_symbol = crypto_symbol.lower() + self.fiat_symbol = fiat_symbol.lower() self.set_price(price=price) def set_price(self, price: float) -> None: @@ -67,17 +67,20 @@ class CryptoToFiatConverter: This object is also a Singleton """ __instance = None - _coinmarketcap: Market = None + _coingekko: CoinGeckoAPI = None _cryptomap: Dict = {} def __new__(cls): + """ + This class is a singleton - should not be instanciated twice. + """ if CryptoToFiatConverter.__instance is None: CryptoToFiatConverter.__instance = object.__new__(cls) try: - CryptoToFiatConverter._coinmarketcap = Market() + CryptoToFiatConverter._coingekko = CoinGeckoAPI() except BaseException: - CryptoToFiatConverter._coinmarketcap = None + CryptoToFiatConverter._coingekko = None return CryptoToFiatConverter.__instance def __init__(self) -> None: @@ -86,14 +89,12 @@ class CryptoToFiatConverter: def _load_cryptomap(self) -> None: try: - coinlistings = self._coinmarketcap.listings() - self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])), - coinlistings["data"])) - except (BaseException) as exception: + coinlistings = self._coingekko.get_coins_list() + # Create mapping table from synbol to coingekko_id + self._cryptomap = {x['symbol']: x['id'] for x in coinlistings} + except (Exception) as exception: logger.error( - "Could not load FIAT Cryptocurrency map for the following problem: %s", - type(exception).__name__ - ) + f"Could not load FIAT Cryptocurrency map for the following problem: {exception}") def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float: """ @@ -115,8 +116,8 @@ class CryptoToFiatConverter: :param fiat_symbol: FIAT currency you want to convert to (e.g USD) :return: Price in FIAT """ - crypto_symbol = crypto_symbol.upper() - fiat_symbol = fiat_symbol.upper() + crypto_symbol = crypto_symbol.lower() + fiat_symbol = fiat_symbol.lower() # Check if the fiat convertion you want is supported if not self._is_supported_fiat(fiat=fiat_symbol): @@ -170,15 +171,13 @@ class CryptoToFiatConverter: :return: bool, True supported, False not supported """ - fiat = fiat.upper() - - return fiat in SUPPORTED_FIAT + return fiat.upper() in SUPPORTED_FIAT def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float: """ - Call CoinMarketCap API to retrieve the price in the FIAT - :param crypto_symbol: Crypto-currency you want to convert (e.g BTC) - :param fiat_symbol: FIAT currency you want to convert to (e.g USD) + Call CoinGekko API to retrieve the price in the FIAT + :param crypto_symbol: Crypto-currency you want to convert (e.g btc) + :param fiat_symbol: FIAT currency you want to convert to (e.g usd) :return: float, price of the crypto-currency in Fiat """ # Check if the fiat convertion you want is supported @@ -195,12 +194,13 @@ class CryptoToFiatConverter: return 0.0 try: + _gekko_id = self._cryptomap[crypto_symbol] return float( - self._coinmarketcap.ticker( - currency=self._cryptomap[crypto_symbol], - convert=fiat_symbol - )['data']['quotes'][fiat_symbol.upper()]['price'] + self._coingekko.get_price( + ids=_gekko_id, + vs_currencies=fiat_symbol + )[_gekko_id][fiat_symbol] ) - except BaseException as exception: + except Exception as exception: logger.error("Error in _find_price: %s", exception) return 0.0 diff --git a/tests/conftest.py b/tests/conftest.py index 000f62868..e8e3fe9e3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -167,23 +167,23 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: @pytest.fixture(autouse=True) -def patch_coinmarketcap(mocker) -> None: +def patch_coingekko(mocker) -> None: """ - Mocker to coinmarketcap to speed up tests - :param mocker: mocker to patch coinmarketcap class + Mocker to coingekko to speed up tests + :param mocker: mocker to patch coingekko class :return: None """ - tickermock = MagicMock(return_value={'price_usd': 12345.0}) - listmock = MagicMock(return_value={'data': [{'id': 1, 'name': 'Bitcoin', 'symbol': 'BTC', - 'website_slug': 'bitcoin'}, - {'id': 1027, 'name': 'Ethereum', 'symbol': 'ETH', - 'website_slug': 'ethereum'} - ]}) + tickermock = MagicMock(return_value={'bitcoin': {'usd': 12345.0}, 'ethereum': {'usd': 12345.0}}) + listmock = MagicMock(return_value=[{'id': 'bitcoin', 'name': 'Bitcoin', 'symbol': 'btc', + 'website_slug': 'bitcoin'}, + {'id': 'ethereum', 'name': 'Ethereum', 'symbol': 'eth', + 'website_slug': 'ethereum'} + ]) mocker.patch.multiple( - 'freqtrade.rpc.fiat_convert.Market', - ticker=tickermock, - listings=listmock, + 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', + get_price=tickermock, + get_coins_list=listmock, ) diff --git a/tests/rpc/test_fiat_convert.py b/tests/rpc/test_fiat_convert.py index 05760ce25..ed21bc516 100644 --- a/tests/rpc/test_fiat_convert.py +++ b/tests/rpc/test_fiat_convert.py @@ -8,7 +8,7 @@ import pytest from requests.exceptions import RequestException from freqtrade.rpc.fiat_convert import CryptoFiat, CryptoToFiatConverter -from tests.conftest import log_has +from tests.conftest import log_has, log_has_re def test_pair_convertion_object(): @@ -22,8 +22,8 @@ def test_pair_convertion_object(): assert pair_convertion.CACHE_DURATION == 6 * 60 * 60 # Check a regular usage - assert pair_convertion.crypto_symbol == 'BTC' - assert pair_convertion.fiat_symbol == 'USD' + assert pair_convertion.crypto_symbol == 'btc' + assert pair_convertion.fiat_symbol == 'usd' assert pair_convertion.price == 12345.0 assert pair_convertion.is_expired() is False @@ -57,15 +57,15 @@ def test_fiat_convert_add_pair(mocker): fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='usd', price=12345.0) pair_len = len(fiat_convert._pairs) assert pair_len == 1 - assert fiat_convert._pairs[0].crypto_symbol == 'BTC' - assert fiat_convert._pairs[0].fiat_symbol == 'USD' + assert fiat_convert._pairs[0].crypto_symbol == 'btc' + assert fiat_convert._pairs[0].fiat_symbol == 'usd' assert fiat_convert._pairs[0].price == 12345.0 fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='Eur', price=13000.2) pair_len = len(fiat_convert._pairs) assert pair_len == 2 - assert fiat_convert._pairs[1].crypto_symbol == 'BTC' - assert fiat_convert._pairs[1].fiat_symbol == 'EUR' + assert fiat_convert._pairs[1].crypto_symbol == 'btc' + assert fiat_convert._pairs[1].fiat_symbol == 'eur' assert fiat_convert._pairs[1].price == 13000.2 @@ -100,15 +100,15 @@ def test_fiat_convert_get_price(mocker): fiat_convert = CryptoToFiatConverter() - with pytest.raises(ValueError, match=r'The fiat US DOLLAR is not supported.'): - fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='US Dollar') + with pytest.raises(ValueError, match=r'The fiat us dollar is not supported.'): + fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='US Dollar') # Check the value return by the method pair_len = len(fiat_convert._pairs) assert pair_len == 0 - assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0 - assert fiat_convert._pairs[0].crypto_symbol == 'BTC' - assert fiat_convert._pairs[0].fiat_symbol == 'USD' + assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 28000.0 + assert fiat_convert._pairs[0].crypto_symbol == 'btc' + assert fiat_convert._pairs[0].fiat_symbol == 'usd' assert fiat_convert._pairs[0].price == 28000.0 assert fiat_convert._pairs[0]._expiration != 0 assert len(fiat_convert._pairs) == 1 @@ -116,13 +116,13 @@ def test_fiat_convert_get_price(mocker): # Verify the cached is used fiat_convert._pairs[0].price = 9867.543 expiration = fiat_convert._pairs[0]._expiration - assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 9867.543 + assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 9867.543 assert fiat_convert._pairs[0]._expiration == expiration # Verify the cache expiration expiration = time.time() - 2 * 60 * 60 fiat_convert._pairs[0]._expiration = expiration - assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0 + assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 28000.0 assert fiat_convert._pairs[0]._expiration is not expiration @@ -143,15 +143,15 @@ def test_loadcryptomap(mocker): fiat_convert = CryptoToFiatConverter() assert len(fiat_convert._cryptomap) == 2 - assert fiat_convert._cryptomap["BTC"] == "1" + assert fiat_convert._cryptomap["btc"] == "bitcoin" def test_fiat_init_network_exception(mocker): # Because CryptoToFiatConverter is a Singleton we reset the listings listmock = MagicMock(side_effect=RequestException) mocker.patch.multiple( - 'freqtrade.rpc.fiat_convert.Market', - listings=listmock, + 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', + get_coins_list=listmock, ) # with pytest.raises(RequestEsxception): fiat_convert = CryptoToFiatConverter() @@ -163,24 +163,24 @@ def test_fiat_init_network_exception(mocker): def test_fiat_convert_without_network(mocker): - # Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap + # Because CryptoToFiatConverter is a Singleton we reset the value of _coingekko fiat_convert = CryptoToFiatConverter() - cmc_temp = CryptoToFiatConverter._coinmarketcap - CryptoToFiatConverter._coinmarketcap = None + cmc_temp = CryptoToFiatConverter._coingekko + CryptoToFiatConverter._coingekko = None - assert fiat_convert._coinmarketcap is None - assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0 - CryptoToFiatConverter._coinmarketcap = cmc_temp + assert fiat_convert._coingekko is None + assert fiat_convert._find_price(crypto_symbol='btc', fiat_symbol='usd') == 0.0 + CryptoToFiatConverter._coingekko = cmc_temp def test_fiat_invalid_response(mocker, caplog): # Because CryptoToFiatConverter is a Singleton we reset the listings listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}") mocker.patch.multiple( - 'freqtrade.rpc.fiat_convert.Market', - listings=listmock, + 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', + get_coins_list=listmock, ) # with pytest.raises(RequestEsxception): fiat_convert = CryptoToFiatConverter() @@ -189,8 +189,8 @@ def test_fiat_invalid_response(mocker, caplog): length_cryptomap = len(fiat_convert._cryptomap) assert length_cryptomap == 0 - assert log_has('Could not load FIAT Cryptocurrency map for the following problem: TypeError', - caplog) + assert log_has_re('Could not load FIAT Cryptocurrency map for the following problem: .*', + caplog) def test_convert_amount(mocker):