`binance`: use `MktPair` in live feed setup

Turns out `binance` is pretty great with their schema  since they have
more or less the same data schema for their exchange info ep which we
wrap in a `Pair` struct:
https://binance-docs.github.io/apidocs/spot/en/#exchange-information

That makes it super easy to provide the most general case for filling
out a `MktPair` with both `.src/dst: Asset` to maintain maximum
meta-data B)

Deatz:
- adjust `Pair` to have `.size/price_tick: Decimal` by parsing out
  the values from the filters field; TODO: we should probably just rewrite
  the input `.filter` at init time so we can keep the frozen style.
- rename `Client.mkt_info()` (was `.symbol_info` to `.exch_info()`
  better matching the ep name and have it build, cache, and return
  a `dict[str, Pair]`; allows dropping `.cache_symbols()`
- only pass the `mkt_info: MktPair` field in the init msg!
rekt_pps
Tyler Goodlet 2023-03-23 16:32:20 -04:00
parent 8f79c37b99
commit b718b5634e
1 changed files with 98 additions and 54 deletions

View File

@ -37,14 +37,19 @@ import numpy as np
import tractor import tractor
import wsproto import wsproto
from ..accounting._mktinfo import (
Asset,
MktPair,
digits_to_dec,
)
from .._cacheables import open_cached_client from .._cacheables import open_cached_client
from ._util import ( from ._util import (
resproc, resproc,
SymbolNotFound, SymbolNotFound,
DataUnavailable, DataUnavailable,
) )
from ..log import ( from ._util import (
get_logger, log,
get_console_log, get_console_log,
) )
from ..data.types import Struct from ..data.types import Struct
@ -53,8 +58,6 @@ from ..data._web_bs import (
NoBsWs, NoBsWs,
) )
log = get_logger(__name__)
_url = 'https://api.binance.com' _url = 'https://api.binance.com'
@ -89,7 +92,10 @@ _show_wap_in_history = False
# https://binance-docs.github.io/apidocs/spot/en/#exchange-information # https://binance-docs.github.io/apidocs/spot/en/#exchange-information
class Pair(Struct, frozen=True):
# TODO: make this frozen again by pre-processing the
# filters list to a dict at init time?
class Pair(Struct): # , frozen=True):
symbol: str symbol: str
status: str status: str
@ -115,9 +121,41 @@ class Pair(Struct, frozen=True):
defaultSelfTradePreventionMode: str defaultSelfTradePreventionMode: str
allowedSelfTradePreventionModes: list[str] allowedSelfTradePreventionModes: list[str]
filters: list[dict[str, Union[str, int, float]]] filters: list[
dict[
str,
Union[str, int, float]
]
]
permissions: list[str] permissions: list[str]
_filtersbykey: dict | None = None
def get_filter(self) -> dict[str, dict]:
filters = self._filtersbykey
if self._filtersbykey:
return filters
filters = self._filtersbykey = {}
for entry in self.filters:
ftype = entry['filterType']
filters[ftype] = entry
return filters
def size_tick(self) -> Decimal:
# XXX: lul, after manually inspecting the response format we
# just directly pick out the info we need
return Decimal(
self.get_filter()['PRICE_FILTER']['tickSize'].rstrip('0')
)
def price_tick(self) -> Decimal:
return Decimal(
self.get_filter()['LOT_SIZE']['stepSize'].rstrip('0')
)
class OHLC(Struct): class OHLC(Struct):
''' '''
@ -160,7 +198,7 @@ class Client:
def __init__(self) -> None: def __init__(self) -> None:
self._sesh = asks.Session(connections=4) self._sesh = asks.Session(connections=4)
self._sesh.base_location = _url self._sesh.base_location = _url
self._pairs: dict[str, Any] = {} self._pairs: dict[str, Pair] = {}
async def _api( async def _api(
self, self,
@ -174,50 +212,43 @@ class Client:
) )
return resproc(resp, log) return resproc(resp, log)
async def mkt_info( async def exch_info(
self, self,
sym: str | None = None, sym: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Pair] | Pair:
'''Get symbol info for the exchange. '''
Fresh exchange-pairs info query for symbol ``sym: str``:
https://binance-docs.github.io/apidocs/spot/en/#exchange-information
''' '''
# TODO: we can load from our self._pairs cache cached_pair = self._pairs.get(sym)
# on repeat calls... if cached_pair:
return cached_pair
# will retrieve all symbols by default # retrieve all symbols by default
params = {} params = {}
if sym is not None: if sym is not None:
sym = sym.lower() sym = sym.lower()
params = {'symbol': sym} params = {'symbol': sym}
resp = await self._api( resp = await self._api('exchangeInfo', params=params)
'exchangeInfo',
params=params,
)
entries = resp['symbols'] entries = resp['symbols']
if not entries: if not entries:
raise SymbolNotFound(f'{sym} not found') raise SymbolNotFound(f'{sym} not found:\n{resp}')
syms = {item['symbol']: item for item in entries} pairs = {
item['symbol']: Pair(**item) for item in entries
}
self._pairs.update(pairs)
if sym is not None: if sym is not None:
return syms[sym] return pairs[sym]
else: else:
return syms return self._pairs
symbol_info = mkt_info symbol_info = exch_info
async def cache_symbols(
self,
) -> dict:
if not self._pairs:
self._pairs = await self.mkt_info()
return self._pairs
async def search_symbols( async def search_symbols(
self, self,
@ -227,7 +258,7 @@ class Client:
if self._pairs is not None: if self._pairs is not None:
data = self._pairs data = self._pairs
else: else:
data = await self.mkt_info() data = await self.exch_info()
matches = fuzzy.extractBests( matches = fuzzy.extractBests(
pattern, pattern,
@ -302,7 +333,7 @@ class Client:
@acm @acm
async def get_client() -> Client: async def get_client() -> Client:
client = Client() client = Client()
await client.cache_symbols() await client.exch_info()
yield client yield client
@ -465,27 +496,38 @@ async def stream_quotes(
): ):
# keep client cached for real-time section # keep client cached for real-time section
cache = await client.cache_symbols() pairs = await client.exch_info()
sym_infos: dict[str, dict] = {}
mkt_infos: dict[str, MktPair] = {}
for sym in symbols: for sym in symbols:
d = cache[sym.upper()]
syminfo = Pair(**d) # validation
si = sym_infos[sym] = syminfo.to_dict() pair: Pair = pairs[sym.upper()]
filters = {} price_tick = pair.price_tick()
for entry in syminfo.filters: size_tick = pair.size_tick()
ftype = entry['filterType']
filters[ftype] = entry
# XXX: after manually inspecting the response format we mkt_infos[sym] = MktPair(
# just directly pick out the info we need dst=Asset(
si['price_tick_size'] = Decimal( name=pair.baseAsset,
filters['PRICE_FILTER']['tickSize'].rstrip('0') atype='crypto',
tx_tick=digits_to_dec(pair.baseAssetPrecision),
),
src=Asset(
name=pair.quoteAsset,
atype='crypto',
tx_tick=digits_to_dec(pair.quoteAssetPrecision),
),
price_tick=price_tick,
size_tick=size_tick,
bs_mktid=pair.symbol,
broker='binance',
) )
si['lot_tick_size'] = Decimal(
filters['LOT_SIZE']['stepSize'].rstrip('0') sym_infos[sym] = {
) 'price_tick_size': price_tick,
si['asset_type'] = 'crypto' 'lot_tick_size': size_tick,
'asset_type': 'crypto',
}
symbol = symbols[0] symbol = symbols[0]
@ -493,9 +535,11 @@ async def stream_quotes(
# pass back token, and bool, signalling if we're the writer # pass back token, and bool, signalling if we're the writer
# and that history has been written # and that history has been written
symbol: { symbol: {
'symbol_info': sym_infos[sym],
'shm_write_opts': {'sum_tick_vml': False},
'fqsn': sym, 'fqsn': sym,
# 'symbol_info': sym_infos[sym],
'mkt_info': mkt_infos[sym],
'shm_write_opts': {'sum_tick_vml': False},
}, },
} }
@ -582,13 +626,13 @@ async def open_symbol_search(
async with open_cached_client('binance') as client: async with open_cached_client('binance') as client:
# load all symbols locally for fast search # load all symbols locally for fast search
cache = await client.cache_symbols() cache = await client.exch_info()
await ctx.started() await ctx.started()
async with ctx.open_stream() as stream: async with ctx.open_stream() as stream:
async for pattern in stream: async for pattern in stream:
# results = await client.mkt_info(sym=pattern.upper()) # results = await client.exch_info(sym=pattern.upper())
matches = fuzzy.extractBests( matches = fuzzy.extractBests(
pattern, pattern,