Merge pull request #5443 from freqtrade/fiat_convert_duplicate_symbols
Simplify fiat_convert and handle multi-mappings
This commit is contained in:
commit
9871268529
@ -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,
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user