`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!pre_overruns_ctxcancelled
							parent
							
								
									2142c13228
								
							
						
					
					
						commit
						a50452dbfd
					
				| 
						 | 
					@ -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,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue