diff --git a/environment.yml b/environment.yml index 4e8c1efcc..86ea03519 100644 --- a/environment.yml +++ b/environment.yml @@ -45,7 +45,7 @@ dependencies: - pip: # Required for app - cython - - coinmarketcap + - pycoingecko - ccxt - TA-Lib - py_find_1st diff --git a/freqtrade/constants.py b/freqtrade/constants.py index ac1a8a6a9..54f620631 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -43,7 +43,7 @@ SUPPORTED_FIAT = [ "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD", - "BTC", "XBT", "ETH", "XRP", "LTC", "BCH", "USDT" + "BTC", "ETH", "XRP", "LTC", "BCH" ] MINIMAL_CONFIG = { diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index d40f9221e..4e26432d4 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 - cannot be instantiated 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/requirements-common.txt b/requirements-common.txt index 10d567a96..a844e81bc 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -11,7 +11,7 @@ wrapt==1.12.0 jsonschema==3.2.0 TA-Lib==0.4.17 tabulate==0.8.6 -coinmarketcap==5.0.3 +pycoingecko==1.2.0 jinja2==2.11.1 # find first, C search in arrays diff --git a/setup.py b/setup.py index 63a595f32..7890f862e 100644 --- a/setup.py +++ b/setup.py @@ -73,7 +73,7 @@ setup(name='freqtrade', 'jsonschema', 'TA-Lib', 'tabulate', - 'coinmarketcap', + 'pycoingecko', 'py_find_1st', 'python-rapidjson', 'sdnotify', 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): diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 6319ab9e6..47ffb771b 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -95,8 +95,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: mocker.patch.multiple( - 'freqtrade.rpc.fiat_convert.Market', - ticker=MagicMock(return_value={'price_usd': 15000.0}), + 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', + get_price=MagicMock(return_value={'bitcoin': {'usd': 15000.0}}), ) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -178,7 +178,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, day[1] == '0.00006217 BTC') assert (day[2] == '0.000 USD' or - day[2] == '0.933 USD') + day[2] == '0.767 USD') # ensure first day is current date assert str(days[0][0]) == str(datetime.utcnow().date()) @@ -190,8 +190,8 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch.multiple( - 'freqtrade.rpc.fiat_convert.Market', - ticker=MagicMock(return_value={'price_usd': 15000.0}), + 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', + get_price=MagicMock(return_value={'bitcoin': {'usd': 15000.0}}), ) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -273,8 +273,8 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, ticker_sell_up, limit_buy_order, limit_sell_order): mocker.patch.multiple( - 'freqtrade.rpc.fiat_convert.Market', - ticker=MagicMock(return_value={'price_usd': 15000.0}), + 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', + get_price=MagicMock(return_value={'bitcoin': {'usd': 15000.0}}), ) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) @@ -341,8 +341,8 @@ def test_rpc_balance_handle_error(default_conf, mocker): # ETH will be skipped due to mocked Error below mocker.patch.multiple( - 'freqtrade.rpc.fiat_convert.Market', - ticker=MagicMock(return_value={'price_usd': 15000.0}), + 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', + get_price=MagicMock(return_value={'bitcoin': {'usd': 15000.0}}), ) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -380,8 +380,8 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): } mocker.patch.multiple( - 'freqtrade.rpc.fiat_convert.Market', - ticker=MagicMock(return_value={'price_usd': 15000.0}), + 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', + get_price=MagicMock(return_value={'bitcoin': {'usd': 15000.0}}), ) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index cea863ac8..d769016c4 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1210,7 +1210,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None: '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ - '*Total:* `(0.001000 BTC, 0.000 USD)`' + '*Total:* `(0.001000 BTC, 12.345 USD)`' def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: