binance.api: add venue qualified symcache support

Meaning we add the `Client.get_assets()` and `.get_mkt_pairs()` methods.
Also implement `.exch_info()` to take in a `expiry: str` to detect
whether to look up a derivative venue instead of spot.

In support of all this we now explicitly key all assets (via
`._cache_pairs() during the populate of `._venue2assets` sub-tables)
with their `.bs_dst_asset: str` value to ensure, for ex., a spot
`BTCUSDT` has a distinct value from any futures contracts with the same
`Pair.symbol: str` value!

Also, ensure we always create a `brokers.toml` (from template) if DNE
and binance is the user's first used backend XD
account_tests
Tyler Goodlet 2023-07-10 11:17:29 -04:00
parent 3c84ac326a
commit 19be8348e5
1 changed files with 104 additions and 20 deletions

View File

@ -78,7 +78,7 @@ def get_config() -> dict:
conf: dict conf: dict
path: Path path: Path
conf, path = config.load() conf, path = config.load(touch_if_dne=True)
section = conf.get('binance') section = conf.get('binance')
@ -396,7 +396,6 @@ class Client:
) -> None: ) -> None:
# lookup internal mkt-specific pair table to update # lookup internal mkt-specific pair table to update
pair_table: dict[str, Pair] = self._venue2pairs[venue] pair_table: dict[str, Pair] = self._venue2pairs[venue]
asset_table: dict[str, Asset] = self._venue2assets[venue]
# make API request(s) # make API request(s)
resp = await self._api( resp = await self._api(
@ -408,6 +407,7 @@ class Client:
venue=venue, venue=venue,
allow_testnet=False, # XXX: never use testnet for symbol lookups allow_testnet=False, # XXX: never use testnet for symbol lookups
) )
mkt_pairs = resp['symbols'] mkt_pairs = resp['symbols']
if not mkt_pairs: if not mkt_pairs:
raise SymbolNotFound(f'No market pairs found!?:\n{resp}') raise SymbolNotFound(f'No market pairs found!?:\n{resp}')
@ -432,21 +432,45 @@ class Client:
# `._pairs: ChainMap` for search B0 # `._pairs: ChainMap` for search B0
pairs_view_subtable[pair.bs_fqme] = pair pairs_view_subtable[pair.bs_fqme] = pair
# XXX WOW: TURNS OUT THIS ISN'T TRUE !?
# > (populate `Asset` table for spot mkts only since it
# > should be a superset of any other venues such as
# > futes or margin)
if venue == 'spot': if venue == 'spot':
if (name := pair.quoteAsset) not in asset_table: dst_sectype: str = 'crypto_currency'
asset_table[name] = Asset(
elif venue in {'usdtm_futes'}:
dst_sectype: str = 'future'
if pair.contractType == 'PERPETUAL':
dst_sectype: str = 'perpetual_future'
spot_asset_table: dict[str, Asset] = self._venue2assets['spot']
ven_asset_table: dict[str, Asset] = self._venue2assets[venue]
if (
(name := pair.quoteAsset) not in spot_asset_table
):
spot_asset_table[pair.bs_src_asset] = Asset(
name=name, name=name,
atype='crypto_currency', atype='crypto_currency',
tx_tick=digits_to_dec(pair.quoteAssetPrecision), tx_tick=digits_to_dec(pair.quoteAssetPrecision),
) )
if (name := pair.baseAsset) not in asset_table: if (
asset_table[name] = Asset( (name := pair.baseAsset) not in ven_asset_table
):
if venue != 'spot':
assert dst_sectype != 'crypto_currency'
ven_asset_table[pair.bs_dst_asset] = Asset(
name=name, name=name,
atype='crypto_currency', atype=dst_sectype,
tx_tick=digits_to_dec(pair.baseAssetPrecision), tx_tick=digits_to_dec(pair.baseAssetPrecision),
) )
# log.warning(
# f'Assets not YET found in spot set: `{pformat(dne)}`!?'
# )
# NOTE: make merged view of all market-type pairs but # NOTE: make merged view of all market-type pairs but
# use market specific `Pair.bs_fqme` for keys! # use market specific `Pair.bs_fqme` for keys!
# this allows searching for market pairs with different # this allows searching for market pairs with different
@ -458,16 +482,29 @@ class Client:
if venue == 'spot': if venue == 'spot':
return return
assets: list[dict] = resp.get('assets', ()) # TODO: maybe use this assets response for non-spot venues?
for entry in assets: # -> issue is we do the exch_info queries conc, so we can't
name: str = entry['asset'] # guarantee order for inter-table lookups..
asset_table[name] = self._venue2assets['spot'].get(name) # if venue ep delivers an explicit set of assets copy just
# ensure they are also already listed in the spot equivs.
# assets: list[dict] = resp.get('assets', ())
# for entry in assets:
# name: str = entry['asset']
# spot_asset_table: dict[str, Asset] = self._venue2assets['spot']
# if name not in spot_asset_table:
# log.warning(
# f'COULDNT FIND ASSET {name}\n{entry}\n'
# f'ADDING AS FUTES ONLY!?'
# )
# asset_table: dict[str, Asset] = self._venue2assets[venue]
# asset_table[name] = spot_asset_table.get(name)
async def exch_info( async def exch_info(
self, self,
sym: str | None = None, sym: str | None = None,
venue: MarketType | None = None, venue: MarketType | None = None,
expiry: str | None = None,
) -> dict[str, Pair] | Pair: ) -> dict[str, Pair] | Pair:
''' '''
@ -485,7 +522,16 @@ class Client:
pair_table: dict[str, Pair] = self._venue2pairs[ pair_table: dict[str, Pair] = self._venue2pairs[
venue or self.mkt_mode venue or self.mkt_mode
] ]
if cached_pair := pair_table.get(sym): if (
expiry
and 'perp' not in expiry.lower()
):
sym: str = f'{sym}_{expiry}'
if (
sym
and (cached_pair := pair_table.get(sym))
):
return cached_pair return cached_pair
venues: list[str] = ['spot', 'usdtm_futes'] venues: list[str] = ['spot', 'usdtm_futes']
@ -500,7 +546,45 @@ class Client:
ven, ven,
) )
return pair_table[sym] if sym else self._pairs if sym:
return pair_table[sym]
else:
self._pairs
async def get_assets(
self,
venue: str | None = None,
) -> dict[str, Asset]:
if (
venue
and venue != 'spot'
):
venues = [venue]
else:
venues = ['usdtm_futes']
ass_table: dict[str, Asset] = self._venue2assets['spot']
# merge in futes contracts with a sectype suffix
for venue in venues:
ass_table |= self._venue2assets[venue]
return ass_table
async def get_mkt_pairs(self) -> dict[str, Pair]:
'''
Flatten the multi-venue (chain) map of market pairs
to a fqme indexed table for data layer caching.
'''
flat: dict[str, Pair] = {}
for venmap in self._pairs.maps:
for bs_fqme, pair in venmap.items():
flat[pair.bs_fqme] = pair
return flat
# TODO: unused except by `brokers.core.search_symbols()`? # TODO: unused except by `brokers.core.search_symbols()`?
async def search_symbols( async def search_symbols(