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))
|
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):
|
class ExchangeBase(Logger):
|
||||||
|
|
||||||
def __init__(self, on_quotes, on_history):
|
def __init__(self, on_quotes, on_history):
|
||||||
Logger.__init__(self)
|
Logger.__init__(self)
|
||||||
self._history = {} # type: Dict[str, Dict[str, str]]
|
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_quotes = on_quotes
|
||||||
self.on_history = on_history
|
self.on_history = on_history
|
||||||
|
|
||||||
@@ -89,16 +94,15 @@ class ExchangeBase(Logger):
|
|||||||
async def update_safe(self, ccy: str) -> None:
|
async def update_safe(self, ccy: str) -> None:
|
||||||
try:
|
try:
|
||||||
self.logger.info(f"getting fx quotes for {ccy}")
|
self.logger.info(f"getting fx quotes for {ccy}")
|
||||||
self.quotes = await self.get_rates(ccy)
|
self._quotes = await self.get_rates(ccy)
|
||||||
assert all(isinstance(rate, (Decimal, type(None))) for rate in self.quotes.values()), \
|
assert all(isinstance(rate, (Decimal, type(None))) for rate in self._quotes.values()), \
|
||||||
f"fx rate must be Decimal, got {self.quotes}"
|
f"fx rate must be Decimal, got {self._quotes}"
|
||||||
|
self._quotes_timestamp = time.time()
|
||||||
self.logger.info("received fx quotes")
|
self.logger.info("received fx quotes")
|
||||||
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
||||||
self.logger.info(f"failed fx quotes: {repr(e)}")
|
self.logger.info(f"failed fx quotes: {repr(e)}")
|
||||||
self.quotes = {}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(f"failed fx quotes: {repr(e)}")
|
self.logger.exception(f"failed fx quotes: {repr(e)}")
|
||||||
self.quotes = {}
|
|
||||||
self.on_quotes()
|
self.on_quotes()
|
||||||
|
|
||||||
def read_historical_rates(self, ccy: str, cache_dir: str) -> Optional[dict]:
|
def read_historical_rates(self, ccy: str, cache_dir: str) -> Optional[dict]:
|
||||||
@@ -167,6 +171,16 @@ class ExchangeBase(Logger):
|
|||||||
rates = await self.get_rates('')
|
rates = await self.get_rates('')
|
||||||
return sorted([str(a) for (a, b) in rates.items() if b is not None and len(a)==3])
|
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):
|
class BitcoinAverage(ExchangeBase):
|
||||||
# note: historical rates used to be freely available
|
# note: historical rates used to be freely available
|
||||||
@@ -429,7 +443,7 @@ class Biscoint(ExchangeBase):
|
|||||||
class Walltime(ExchangeBase):
|
class Walltime(ExchangeBase):
|
||||||
|
|
||||||
async def get_rates(self, ccy):
|
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')
|
'/data-production-walltime-info/production/dynamic/walltime-info.json')
|
||||||
return {'BRL': to_decimal(json['BRL_XBT']['last_inexact'])}
|
return {'BRL': to_decimal(json['BRL_XBT']['last_inexact'])}
|
||||||
|
|
||||||
@@ -542,9 +556,9 @@ class FxThread(ThreadJob, EventListener):
|
|||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
while True:
|
while True:
|
||||||
# approx. every 2.5 minutes, refresh spot price
|
# every few minutes, refresh spot price
|
||||||
try:
|
try:
|
||||||
async with timeout_after(150):
|
async with timeout_after(POLL_PERIOD_SPOT_RATE):
|
||||||
await self._trigger.wait()
|
await self._trigger.wait()
|
||||||
self._trigger.clear()
|
self._trigger.clear()
|
||||||
# we were manually triggered, so get historical rates
|
# we were manually triggered, so get historical rates
|
||||||
@@ -583,7 +597,7 @@ class FxThread(ThreadJob, EventListener):
|
|||||||
def set_fiat_address_config(self, b):
|
def set_fiat_address_config(self, b):
|
||||||
self.config.set_key('fiat_address', bool(b))
|
self.config.set_key('fiat_address', bool(b))
|
||||||
|
|
||||||
def get_currency(self):
|
def get_currency(self) -> str:
|
||||||
'''Use when dynamic fetching is needed'''
|
'''Use when dynamic fetching is needed'''
|
||||||
return self.config.get("currency", DEFAULT_CURRENCY)
|
return self.config.get("currency", DEFAULT_CURRENCY)
|
||||||
|
|
||||||
@@ -625,10 +639,7 @@ class FxThread(ThreadJob, EventListener):
|
|||||||
"""Returns the exchange rate as a Decimal"""
|
"""Returns the exchange rate as a Decimal"""
|
||||||
if not self.is_enabled():
|
if not self.is_enabled():
|
||||||
return Decimal('NaN')
|
return Decimal('NaN')
|
||||||
rate = self.exchange.quotes.get(self.ccy)
|
return self.exchange.get_cached_spot_quote(self.ccy)
|
||||||
if rate is None:
|
|
||||||
return Decimal('NaN')
|
|
||||||
return Decimal(rate)
|
|
||||||
|
|
||||||
def format_amount(self, btc_balance, *, timestamp: int = None) -> str:
|
def format_amount(self, btc_balance, *, timestamp: int = None) -> str:
|
||||||
if timestamp is None:
|
if timestamp is None:
|
||||||
@@ -667,7 +678,7 @@ class FxThread(ThreadJob, EventListener):
|
|||||||
# Frequently there is no rate for today, until tomorrow :)
|
# Frequently there is no rate for today, until tomorrow :)
|
||||||
# Use spot quotes in that case
|
# Use spot quotes in that case
|
||||||
if rate.is_nan() and (datetime.today().date() - d_t.date()).days <= 2:
|
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
|
self.history_used_spot = True
|
||||||
if rate is None:
|
if rate is None:
|
||||||
rate = 'NaN'
|
rate = 'NaN'
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ class TestWalletStorage(WalletTestCase):
|
|||||||
class FakeExchange(ExchangeBase):
|
class FakeExchange(ExchangeBase):
|
||||||
def __init__(self, rate):
|
def __init__(self, rate):
|
||||||
super().__init__(lambda self: None, lambda self: None)
|
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:
|
class FakeFxThread:
|
||||||
def __init__(self, exchange):
|
def __init__(self, exchange):
|
||||||
|
|||||||
Reference in New Issue
Block a user