Further refinement and shimming of `MktPair`

Prepping to entirely replace `Symbol`; this adds a buncha docs/comments,
better implementation for representing and parsing the FQME: "fully
qualified market endpoint".

Deatz:
- make `.src` an optional field until we figure out how we're going
  to support loading source assets from all backends sensibly..
- implement `MktPair.fqme: str` (what was previously called `fqsn`)
  using a new util func: `maybe_cons_tokens()`.
- `Symbol.brokers` and expect only `.broker` usage.
- remap anything with `fqsn` in the name to `fqme` with aliases from the
  old name.
- implement `unpack_fqme()` with `match:` syntax B)
- add `MktPair.tick_size_digits`, `.lot_size_digits`, `.fqsn`, `.key` for
  backward compat.
- make all fqme generation related fields empty `str`s by default.
- add `MktPair.resolved: bool` a flag indicating whether or not `.dst`
  is an `Asset` instance or just a string and, `.bs_mktid` the field
  to hold the "backend system market id" per broker.
pre_overruns_ctxcancelled
Tyler Goodlet 2023-03-15 19:31:44 -04:00
parent 2485bc803b
commit e5eb317b47
1 changed files with 166 additions and 112 deletions

View File

@ -43,8 +43,8 @@ from ..data.types import Struct
_underlyings: list[str] = [ _underlyings: list[str] = [
'stock', 'stock',
'bond', 'bond',
'crypto_currency', 'crypto',
'fiat_currency', 'fiat',
'commodity', 'commodity',
] ]
@ -102,7 +102,8 @@ def digits_to_dec(
class Asset(Struct, frozen=True): class Asset(Struct, frozen=True):
''' '''
Container type describing any transactable asset's technology. Container type describing any transactable asset and its
contract-like and/or underlying technology meta-info.
''' '''
name: str name: str
@ -116,6 +117,9 @@ class Asset(Struct, frozen=True):
# should not be explicitly required in our generic API. # should not be explicitly required in our generic API.
info: dict = {} # make it frozen? info: dict = {} # make it frozen?
# TODO?
# _to_dict_skip = {'info'}
def __str__(self) -> str: def __str__(self) -> str:
return self.name return self.name
@ -137,6 +141,18 @@ class Asset(Struct, frozen=True):
) )
def maybe_cons_tokens(
tokens: list[Any],
delim_char: str = '.',
) -> str:
'''
Construct `str` output from a maybe-concatenation of input
sequence of elements in ``tokens``.
'''
return '.'.join(filter(bool, tokens)).lower()
class MktPair(Struct, frozen=True): class MktPair(Struct, frozen=True):
''' '''
Market description for a pair of assets which are tradeable: Market description for a pair of assets which are tradeable:
@ -144,52 +160,50 @@ class MktPair(Struct, frozen=True):
buy: source asset -> destination asset buy: source asset -> destination asset
sell: destination asset -> source asset sell: destination asset -> source asset
The main intention of this type is for a cross-asset, venue, broker The main intention of this type is for a **simple** cross-asset
normalized descriptive data type from which all market-auctions can venue/broker normalized descrption type from which all
be mapped, simply. market-auctions can be mapped from FQME identifiers.
TODO: our eventual target fqme format/schema is:
<dst>/<src>.<expiry>.<con_info_1>.<con_info_2>. -> .<venue>.<broker>
^ -- optional tokens ------------------------------- ^
''' '''
# "source asset" (name) used to buy *from* dst: str | Asset
# (or used to sell *to*)
src: str | Asset
# "destination asset" (name) used to buy *to* # "destination asset" (name) used to buy *to*
# (or used to sell *from*) # (or used to sell *from*)
dst: str | Asset
@property
def key(self) -> str:
'''
The "endpoint key" for this market.
In most other tina platforms this is referred to as the
"symbol".
'''
return f'{self.src}{self.dst}'
price_tick: Decimal # minimum price increment
size_tick: Decimal # minimum size (aka vlm) increment
# the tick size is the number describing the smallest step in value # the tick size is the number describing the smallest step in value
# available in this market between the source and destination # available in this market between the source and destination
# assets. # assets.
# https://en.wikipedia.org/wiki/Tick_size # https://en.wikipedia.org/wiki/Tick_size
# https://en.wikipedia.org/wiki/Commodity_tick # https://en.wikipedia.org/wiki/Commodity_tick
# https://en.wikipedia.org/wiki/Percentage_in_point # https://en.wikipedia.org/wiki/Percentage_in_point
price_tick: Decimal # minimum price increment value increment
size_tick: Decimal # minimum size (aka vlm) increment value increment
# @property # unique "broker id" since every market endpoint provider
# def size_tick_digits(self) -> int: # has their own nomenclature and schema for market maps.
# return float_digits(self.size_tick) bs_mktid: str
broker: str # the middle man giving access
broker: str | None = None # the middle man giving access # NOTE: to start this field is optional but should eventually be
venue: str | None = None # market venue provider name # required; the reason is for backward compat since more positioning
expiry: str | None = None # for derivs, expiry datetime parseable str # calculations were not originally stored with a src asset..
src: str | Asset | None = None
# "source asset" (name) used to buy *from*
# (or used to sell *to*).
venue: str = '' # market venue provider name
expiry: str = '' # for derivs, expiry datetime parseable str
# destination asset's financial type/classification name # destination asset's financial type/classification name
# NOTE: this is required for the order size allocator system, # NOTE: this is required for the order size allocator system,
# since we use different default settings based on the type # since we use different default settings based on the type
# of the destination asset, eg. futes use a units limits vs. # of the destination asset, eg. futes use a units limits vs.
# equities a $limit. # equities a $limit.
dst_type: AssetTypeName | None = None # dst_type: AssetTypeName | None = None
# source asset's financial type/classification name # source asset's financial type/classification name
# TODO: is a src type required for trading? # TODO: is a src type required for trading?
@ -211,21 +225,101 @@ class MktPair(Struct, frozen=True):
Constructor for a received msg-dict normally received over IPC. Constructor for a received msg-dict normally received over IPC.
''' '''
... raise NotImplementedError
# fqa, fqma, .. etc. see issue: @property
# https://github.com/pikers/piker/issues/467 def resolved(self) -> bool:
return isinstance(self.dst, Asset)
@classmethod
def from_fqme(
cls,
fqme: str,
price_tick: float | str,
size_tick: float | str,
bs_mktid: str,
) -> MktPair:
broker, key, suffix = unpack_fqme(fqme)
# XXX: loading from a fqme string will
# leave this pair as "un resolved" meaning
# we don't yet have `.dst` set as an `Asset`
# which we expect to be filled in by some
# backend client with access to that data-info.
return cls(
dst=key, # not resolved
price_tick=price_tick,
size_tick=size_tick,
bs_mktid=bs_mktid,
broker=broker,
)
@property
def key(self) -> str:
'''
The "endpoint key" for this market.
Eg. mnq/usd or btc/usdt or xmr/btc
In most other tina platforms this is referred to as the
"symbol".
'''
return maybe_cons_tokens([self.dst, self.src])
# NOTE: the main idea behind an fqme is to map a "market address"
# to some endpoint from a transaction provider (eg. a broker) such
# that we build a table of `fqme: str -> bs_mktid: Any` where any "piker
# market address" maps 1-to-1 to some broker trading endpoint.
# @cached_property
@property @property
def fqme(self) -> str: def fqme(self) -> str:
''' '''
Return the fully qualified market endpoint-address for the Return the fully qualified market endpoint-address for the
pair of transacting assets. pair of transacting assets.
Yes, you can pronounce it colloquially as "f#$%-me".. fqme = "fully qualified market endpoint"
And yes, you pronounce it colloquially as read..
Basically the idea here is for all client code (consumers of piker's
APIs which query the data/broker-provider agnostic layer(s)) should be
able to tell which backend / venue / derivative each data feed/flow is
from by an explicit string-key of the current form:
<market-instrument-name>
.<venue>
.<expiry>
.<derivative-suffix-info>
.<brokerbackendname>
eg. for an explicit daq mini futes contract: mnq.cme.20230317.ib
TODO: I have thoughts that we should actually change this to be
more like an "attr lookup" (like how the web should have done
urls, but marketting peeps ruined it etc. etc.)
<broker>.<venue>.<instrumentname>.<suffixwithmetadata>
TODO:
See community discussion on naming and nomenclature, order
of addressing hierarchy, general schema, internal representation:
https://github.com/pikers/piker/issues/467
''' '''
return maybe_cons_tokens([
self.key, # final "pair name" (eg. qqq[/usd], btcusdt)
self.venue,
self.expiry,
self.broker,
])
# fqsn = fqme @property
def fqsn(self) -> str:
return self.fqme
def quantize( def quantize(
self, self,
@ -250,35 +344,27 @@ class MktPair(Struct, frozen=True):
rounding=ROUND_HALF_EVEN rounding=ROUND_HALF_EVEN
) )
# TODO: remove this? # @property
# def size_tick_digits(self) -> int:
# return float_digits(self.size_tick)
# TODO: BACKWARD COMPAT, TO REMOVE?
@property @property
def type_key(self) -> str: def type_key(self) -> str:
return list(self.broker_info.values())[0]['asset_type'] return str(self.dst.atype)
# @classmethod @property
# def from_fqme( def tick_size_digits(self) -> int:
# cls, return float_digits(self.price_tick)
# fqme: str,
# **kwargs,
# ) -> MktPair: @property
# broker, key, suffix = unpack_fqme(fqme) def lot_size_digits(self) -> int:
return float_digits(self.size_tick)
def mk_fqsn( def unpack_fqme(
provider: str, fqme: str,
symbol: str, ) -> tuple[str, str, str]:
) -> str:
'''
Generate a "fully qualified symbol name" which is
a reverse-hierarchical cross broker/provider symbol
'''
return '.'.join([symbol, provider]).lower()
def unpack_fqsn(fqsn: str) -> tuple[str, str, str]:
''' '''
Unpack a fully-qualified-symbol-name to ``tuple``. Unpack a fully-qualified-symbol-name to ``tuple``.
@ -287,37 +373,38 @@ def unpack_fqsn(fqsn: str) -> tuple[str, str, str]:
suffix = '' suffix = ''
# TODO: probably reverse the order of all this XD # TODO: probably reverse the order of all this XD
tokens = fqsn.split('.') tokens = fqme.split('.')
if len(tokens) < 3:
match tokens:
case [mkt_ep, broker]:
# probably crypto # probably crypto
symbol, broker = tokens # mkt_ep, broker = tokens
return ( return (
broker, broker,
symbol, mkt_ep,
'', '',
) )
elif len(tokens) > 3: # TODO: swap venue and suffix/deriv-info here?
symbol, venue, suffix, broker = tokens case [mkt_ep, venue, suffix, broker]:
else: pass
symbol, venue, broker = tokens
case [mkt_ep, venue, broker]:
suffix = '' suffix = ''
# head, _, broker = fqsn.rpartition('.') case _:
# symbol, _, suffix = head.rpartition('.') raise ValueError(f'Invalid fqme: {fqme}')
return ( return (
broker, broker,
'.'.join([symbol, venue]), '.'.join([mkt_ep, venue]),
suffix, suffix,
) )
unpack_fqme = unpack_fqsn unpack_fqsn = unpack_fqme
# TODO: rework the below `Symbol` (which was originally inspired and
# derived from stuff in quantdom) into a simpler, ipc msg ready, market
# endpoint meta-data container type as per the drafted interace above.
class Symbol(Struct): class Symbol(Struct):
''' '''
I guess this is some kinda container thing for dealing with I guess this is some kinda container thing for dealing with
@ -343,13 +430,8 @@ class Symbol(Struct):
return Symbol( return Symbol(
key=key, key=key,
tick_size=tick_size, tick_size=tick_size,
lot_tick_size=lot_size, lot_tick_size=lot_size,
# tick_size_digits=float_digits(tick_size),
# lot_size_digits=float_digits(lot_size),
suffix=suffix, suffix=suffix,
broker_info={broker: info}, broker_info={broker: info},
) )
@ -361,10 +443,6 @@ class Symbol(Struct):
def type_key(self) -> str: def type_key(self) -> str:
return list(self.broker_info.values())[0]['asset_type'] return list(self.broker_info.values())[0]['asset_type']
@property
def brokers(self) -> list[str]:
return list(self.broker_info.keys())
@property @property
def tick_size_digits(self) -> int: def tick_size_digits(self) -> int:
return float_digits(self.lot_tick_size) return float_digits(self.lot_tick_size)
@ -379,23 +457,6 @@ class Symbol(Struct):
@property @property
def fqsn(self) -> str: def fqsn(self) -> str:
'''
fqsn = "fully qualified symbol name"
Basically the idea here is for all client-ish code (aka programs/actors
that ask the provider agnostic layers in the stack for data) should be
able to tell which backend / venue / derivative each data feed/flow is
from by an explicit string key of the current form:
<instrumentname>.<venue>.<suffixwithmetadata>.<brokerbackendname>
TODO: I have thoughts that we should actually change this to be
more like an "attr lookup" (like how the web should have done
urls, but marketting peeps ruined it etc. etc.):
<broker>.<venue>.<instrumentname>.<suffixwithmetadata>
'''
broker = self.broker broker = self.broker
key = self.key key = self.key
if self.suffix: if self.suffix:
@ -410,14 +471,7 @@ class Symbol(Struct):
def quantize( def quantize(
self, self,
size: float, size: float,
) -> Decimal: ) -> Decimal:
'''
Truncate input ``size: float`` using ``Decimal``
quantized form of the digit precision defined
by ``self.lot_tick_size``.
'''
digits = float_digits(self.lot_tick_size) digits = float_digits(self.lot_tick_size)
return Decimal(size).quantize( return Decimal(size).quantize(
Decimal(f'1.{"0".ljust(digits, "0")}'), Decimal(f'1.{"0".ljust(digits, "0")}'),