@ -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 '