Merge pull request #7955 from SomberNight/202208_fxrate_more_robust
exchange_rate: more robust spot price against temporary network issues
This commit is contained in:
@@ -48,12 +48,17 @@ def to_decimal(x: Union[str, float, int, Decimal]) -> Decimal:
|
||||
return Decimal(str(x))
|
||||
|
||||
|
||||
POLL_PERIOD_SPOT_RATE = 150 # approx. every 2.5 minutes, try to refresh spot price
|
||||
EXPIRY_SPOT_RATE = 600 # spot price becomes stale after 10 minutes
|
||||
|
||||
|
||||
class ExchangeBase(Logger):
|
||||
|
||||
def __init__(self, on_quotes, on_history):
|
||||
Logger.__init__(self)
|
||||
self._history = {} # type: Dict[str, Dict[str, str]]
|
||||
self.quotes = {} # type: Dict[str, Optional[Decimal]]
|
||||
self._quotes = {} # type: Dict[str, Optional[Decimal]]
|
||||
self._quotes_timestamp = 0 # type: Union[int, float]
|
||||
self.on_quotes = on_quotes
|
||||
self.on_history = on_history
|
||||
|
||||
@@ -89,16 +94,15 @@ class ExchangeBase(Logger):
|
||||
async def update_safe(self, ccy: str) -> None:
|
||||
try:
|
||||
self.logger.info(f"getting fx quotes for {ccy}")
|
||||
self.quotes = await self.get_rates(ccy)
|
||||
assert all(isinstance(rate, (Decimal, type(None))) for rate in self.quotes.values()), \
|
||||
f"fx rate must be Decimal, got {self.quotes}"
|
||||
self._quotes = await self.get_rates(ccy)
|
||||
assert all(isinstance(rate, (Decimal, type(None))) for rate in self._quotes.values()), \
|
||||
f"fx rate must be Decimal, got {self._quotes}"
|
||||
self._quotes_timestamp = time.time()
|
||||
self.logger.info("received fx quotes")
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
||||
self.logger.info(f"failed fx quotes: {repr(e)}")
|
||||
self.quotes = {}
|
||||
except Exception as e:
|
||||
self.logger.exception(f"failed fx quotes: {repr(e)}")
|
||||
self.quotes = {}
|
||||
self.on_quotes()
|
||||
|
||||
def read_historical_rates(self, ccy: str, cache_dir: str) -> Optional[dict]:
|
||||
@@ -167,6 +171,16 @@ class ExchangeBase(Logger):
|
||||
rates = await self.get_rates('')
|
||||
return sorted([str(a) for (a, b) in rates.items() if b is not None and len(a)==3])
|
||||
|
||||
def get_cached_spot_quote(self, ccy: str) -> Decimal:
|
||||
"""Returns the cached exchange rate as a Decimal"""
|
||||
rate = self._quotes.get(ccy)
|
||||
if rate is None:
|
||||
return Decimal('NaN')
|
||||
if self._quotes_timestamp + EXPIRY_SPOT_RATE < time.time():
|
||||
# Our rate is stale. Probably better to return no rate than an incorrect one.
|
||||
return Decimal('NaN')
|
||||
return Decimal(rate)
|
||||
|
||||
|
||||
class BitcoinAverage(ExchangeBase):
|
||||
# note: historical rates used to be freely available
|
||||
@@ -429,7 +443,7 @@ class Biscoint(ExchangeBase):
|
||||
class Walltime(ExchangeBase):
|
||||
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('s3.amazonaws.com',
|
||||
json = await self.get_json('s3.amazonaws.com',
|
||||
'/data-production-walltime-info/production/dynamic/walltime-info.json')
|
||||
return {'BRL': to_decimal(json['BRL_XBT']['last_inexact'])}
|
||||
|
||||
@@ -542,9 +556,9 @@ class FxThread(ThreadJob, EventListener):
|
||||
|
||||
async def run(self):
|
||||
while True:
|
||||
# approx. every 2.5 minutes, refresh spot price
|
||||
# every few minutes, refresh spot price
|
||||
try:
|
||||
async with timeout_after(150):
|
||||
async with timeout_after(POLL_PERIOD_SPOT_RATE):
|
||||
await self._trigger.wait()
|
||||
self._trigger.clear()
|
||||
# we were manually triggered, so get historical rates
|
||||
@@ -583,7 +597,7 @@ class FxThread(ThreadJob, EventListener):
|
||||
def set_fiat_address_config(self, b):
|
||||
self.config.set_key('fiat_address', bool(b))
|
||||
|
||||
def get_currency(self):
|
||||
def get_currency(self) -> str:
|
||||
'''Use when dynamic fetching is needed'''
|
||||
return self.config.get("currency", DEFAULT_CURRENCY)
|
||||
|
||||
@@ -625,10 +639,7 @@ class FxThread(ThreadJob, EventListener):
|
||||
"""Returns the exchange rate as a Decimal"""
|
||||
if not self.is_enabled():
|
||||
return Decimal('NaN')
|
||||
rate = self.exchange.quotes.get(self.ccy)
|
||||
if rate is None:
|
||||
return Decimal('NaN')
|
||||
return Decimal(rate)
|
||||
return self.exchange.get_cached_spot_quote(self.ccy)
|
||||
|
||||
def format_amount(self, btc_balance, *, timestamp: int = None) -> str:
|
||||
if timestamp is None:
|
||||
@@ -667,7 +678,7 @@ class FxThread(ThreadJob, EventListener):
|
||||
# Frequently there is no rate for today, until tomorrow :)
|
||||
# Use spot quotes in that case
|
||||
if rate.is_nan() and (datetime.today().date() - d_t.date()).days <= 2:
|
||||
rate = self.exchange.quotes.get(self.ccy, 'NaN')
|
||||
rate = self.exchange.get_cached_spot_quote(self.ccy)
|
||||
self.history_used_spot = True
|
||||
if rate is None:
|
||||
rate = 'NaN'
|
||||
|
||||
@@ -89,7 +89,8 @@ class TestWalletStorage(WalletTestCase):
|
||||
class FakeExchange(ExchangeBase):
|
||||
def __init__(self, rate):
|
||||
super().__init__(lambda self: None, lambda self: None)
|
||||
self.quotes = {'TEST': rate}
|
||||
self._quotes = {'TEST': rate}
|
||||
self._quotes_timestamp = float("inf") # spot price from the far future never becomes stale :P
|
||||
|
||||
class FakeFxThread:
|
||||
def __init__(self, exchange):
|
||||
|
||||
Reference in New Issue
Block a user