Simplify fiat_convert and handle multi-mappings

This commit is contained in:
Matthias 2021-08-17 20:41:08 +02:00
parent 81715d0b9d
commit 4164f93853
2 changed files with 49 additions and 30 deletions

View File

@ -5,7 +5,7 @@ e.g BTC to USD
import datetime import datetime
import logging import logging
from typing import Dict from typing import Dict, List
from cachetools.ttl import TTLCache from cachetools.ttl import TTLCache
from pycoingecko import CoinGeckoAPI from pycoingecko import CoinGeckoAPI
@ -25,8 +25,7 @@ class CryptoToFiatConverter:
""" """
__instance = None __instance = None
_coingekko: CoinGeckoAPI = None _coingekko: CoinGeckoAPI = None
_coinlistings: List[Dict] = []
_cryptomap: Dict = {}
_backoff: float = 0.0 _backoff: float = 0.0
def __new__(cls): def __new__(cls):
@ -49,9 +48,8 @@ class CryptoToFiatConverter:
def _load_cryptomap(self) -> None: def _load_cryptomap(self) -> None:
try: try:
coinlistings = self._coingekko.get_coins_list() # Use list-comprehension to ensure we get a list.
# Create mapping table from symbol to coingekko_id self._coinlistings = [x for x in self._coingekko.get_coins_list()]
self._cryptomap = {x['symbol']: x['id'] for x in coinlistings}
except RequestException as request_exception: except RequestException as request_exception:
if "429" in str(request_exception): if "429" in str(request_exception):
logger.warning( logger.warning(
@ -69,6 +67,24 @@ class CryptoToFiatConverter:
logger.error( logger.error(
f"Could not load FIAT Cryptocurrency map for the following problem: {exception}") f"Could not load FIAT Cryptocurrency map for the following problem: {exception}")
def _get_gekko_id(self, crypto_symbol):
if not self._coinlistings:
if self._backoff <= datetime.datetime.now().timestamp():
self._load_cryptomap()
# Still not loaded.
if not self._coinlistings:
return None
else:
return None
found = [x for x in self._coinlistings if x['symbol'] == crypto_symbol]
if len(found) == 1:
return found[0]['id']
if len(found) > 0:
# Wrong!
logger.warning(f"Found multiple mappings in goingekko for {crypto_symbol}.")
return None
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float: def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
""" """
Convert an amount of crypto-currency to fiat Convert an amount of crypto-currency to fiat
@ -143,22 +159,14 @@ class CryptoToFiatConverter:
if crypto_symbol == fiat_symbol: if crypto_symbol == fiat_symbol:
return 1.0 return 1.0
if self._cryptomap == {}: _gekko_id = self._get_gekko_id(crypto_symbol)
if self._backoff <= datetime.datetime.now().timestamp():
self._load_cryptomap()
# return 0.0 if we still don't 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: if not _gekko_id:
# return 0 for unsupported stake currencies (fiat-convert should not break the bot) # return 0 for unsupported stake currencies (fiat-convert should not break the bot)
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol) logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
return 0.0 return 0.0
try: try:
_gekko_id = self._cryptomap[crypto_symbol]
return float( return float(
self._coingekko.get_price( self._coingekko.get_price(
ids=_gekko_id, ids=_gekko_id,

View File

@ -22,7 +22,7 @@ def test_fiat_convert_is_supported(mocker):
def test_fiat_convert_find_price(mocker): def test_fiat_convert_find_price(mocker):
fiat_convert = CryptoToFiatConverter() fiat_convert = CryptoToFiatConverter()
fiat_convert._cryptomap = {} fiat_convert._coinlistings = {}
fiat_convert._backoff = 0 fiat_convert._backoff = 0
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._load_cryptomap', mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._load_cryptomap',
return_value=None) return_value=None)
@ -44,7 +44,7 @@ def test_fiat_convert_find_price(mocker):
def test_fiat_convert_unsupported_crypto(mocker, caplog): def test_fiat_convert_unsupported_crypto(mocker, caplog):
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[]) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._coinlistings', return_value=[])
fiat_convert = CryptoToFiatConverter() fiat_convert = CryptoToFiatConverter()
assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0 assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0
assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog) assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog)
@ -88,9 +88,9 @@ def test_fiat_convert_two_FIAT(mocker):
def test_loadcryptomap(mocker): def test_loadcryptomap(mocker):
fiat_convert = CryptoToFiatConverter() fiat_convert = CryptoToFiatConverter()
assert len(fiat_convert._cryptomap) == 2 assert len(fiat_convert._coinlistings) == 2
assert fiat_convert._cryptomap["btc"] == "bitcoin" assert fiat_convert._get_gekko_id("btc") == "bitcoin"
def test_fiat_init_network_exception(mocker): def test_fiat_init_network_exception(mocker):
@ -102,11 +102,10 @@ def test_fiat_init_network_exception(mocker):
) )
# with pytest.raises(RequestEsxception): # with pytest.raises(RequestEsxception):
fiat_convert = CryptoToFiatConverter() fiat_convert = CryptoToFiatConverter()
fiat_convert._cryptomap = {} fiat_convert._coinlistings = {}
fiat_convert._load_cryptomap() fiat_convert._load_cryptomap()
length_cryptomap = len(fiat_convert._cryptomap) assert len(fiat_convert._coinlistings) == 0
assert length_cryptomap == 0
def test_fiat_convert_without_network(mocker): def test_fiat_convert_without_network(mocker):
@ -132,11 +131,10 @@ def test_fiat_too_many_requests_response(mocker, caplog):
) )
# with pytest.raises(RequestEsxception): # with pytest.raises(RequestEsxception):
fiat_convert = CryptoToFiatConverter() fiat_convert = CryptoToFiatConverter()
fiat_convert._cryptomap = {} fiat_convert._coinlistings = {}
fiat_convert._load_cryptomap() fiat_convert._load_cryptomap()
length_cryptomap = len(fiat_convert._cryptomap) assert len(fiat_convert._coinlistings) == 0
assert length_cryptomap == 0
assert fiat_convert._backoff > datetime.datetime.now().timestamp() assert fiat_convert._backoff > datetime.datetime.now().timestamp()
assert log_has( assert log_has(
'Too many requests for Coingecko API, backing off and trying again later.', 'Too many requests for Coingecko API, backing off and trying again later.',
@ -144,20 +142,33 @@ def test_fiat_too_many_requests_response(mocker, caplog):
) )
def test_fiat_multiple_coins(mocker, caplog):
fiat_convert = CryptoToFiatConverter()
fiat_convert._coinlistings = [
{'id': 'helium', 'symbol': 'hnt', 'name': 'Helium'},
{'id': 'hymnode', 'symbol': 'hnt', 'name': 'Hymnode'},
{'id': 'bitcoin', 'symbol': 'btc', 'name': 'Bitcoin'},
]
assert fiat_convert._get_gekko_id('btc') == 'bitcoin'
assert fiat_convert._get_gekko_id('hnt') is None
assert log_has('Found multiple mappings in goingekko for hnt.', caplog)
def test_fiat_invalid_response(mocker, caplog): def test_fiat_invalid_response(mocker, caplog):
# Because CryptoToFiatConverter is a Singleton we reset the listings # Because CryptoToFiatConverter is a Singleton we reset the listings
listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}") listmock = MagicMock(return_value=None)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.fiat_convert.CoinGeckoAPI', 'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
get_coins_list=listmock, get_coins_list=listmock,
) )
# with pytest.raises(RequestEsxception): # with pytest.raises(RequestEsxception):
fiat_convert = CryptoToFiatConverter() fiat_convert = CryptoToFiatConverter()
fiat_convert._cryptomap = {} fiat_convert._coinlistings = []
fiat_convert._load_cryptomap() fiat_convert._load_cryptomap()
length_cryptomap = len(fiat_convert._cryptomap) assert len(fiat_convert._coinlistings) == 0
assert length_cryptomap == 0
assert log_has_re('Could not load FIAT Cryptocurrency map for the following problem: .*', assert log_has_re('Could not load FIAT Cryptocurrency map for the following problem: .*',
caplog) caplog)