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.rekt_pps
							parent
							
								
									85ddfc0f2d
								
							
						
					
					
						commit
						cf9442f4d5
					
				|  | @ -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: |  | ||||||
|         # probably crypto |  | ||||||
|         symbol, broker = tokens |  | ||||||
|         return ( |  | ||||||
|             broker, |  | ||||||
|             symbol, |  | ||||||
|             '', |  | ||||||
|         ) |  | ||||||
| 
 | 
 | ||||||
|     elif len(tokens) > 3: |     match tokens: | ||||||
|         symbol, venue, suffix, broker = tokens |         case [mkt_ep, broker]: | ||||||
|     else: |             # probably crypto | ||||||
|         symbol, venue, broker = tokens |             # mkt_ep, broker = tokens | ||||||
|         suffix = '' |             return ( | ||||||
|  |                 broker, | ||||||
|  |                 mkt_ep, | ||||||
|  |                 '', | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         # TODO: swap venue and suffix/deriv-info here? | ||||||
|  |         case [mkt_ep, venue, suffix, broker]: | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |         case [mkt_ep, venue, broker]: | ||||||
|  |             suffix = '' | ||||||
|  | 
 | ||||||
|  |         case _: | ||||||
|  |             raise ValueError(f'Invalid fqme: {fqme}') | ||||||
| 
 | 
 | ||||||
|     # head, _, broker = fqsn.rpartition('.') |  | ||||||
|     # symbol, _, suffix = head.rpartition('.') |  | ||||||
|     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")}'), | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue