Simplify `Symbol` extend `MktPair`, add `Asset`

Drop everything we can in terms of methods and attrs from `Symbol`:
- kill `.tokens()`, `.front_feed()`, `.tokens()`, `.nearest_tick()`,
  `.front_fqsn()`, instead moving logic from these methods into
  dependents (and obviously removing any usage from rest of code base,
  coming in follow up commits).
- rename `.quantize_size()` -> `.quantize()`.
- re-implement `.brokers`, `.lot_size_digits`, `.tick_size_digits` as
  `@property` methods; for the latter two, allows us to minimize to only
  accepting min tick decimal values on alternative constructor class
  methods and to drop the equivalent instance vars.
- map `_fqsn` related variable names to new and preferred `_fqme`.

We also juggle around some utility functions, moving limited precision
related `decimal.Decimal` routines to the top of module and soon-to-be
legacy `fqsn` related routines to the bottom.

`MktPair` draft type extensions:
- drop requirements for `src_type`, and offer the optional `.dst_type`
  field as either a `str` or (new `typing.Literal`) `AssetTypeName`.
- define an equivalent `.quantize()` as (re)defined in `Symbol` but with
  `quantity_type: str` field which specifies whether to use the price or
  the size precision.
- add a lot more docs, a `.key` property for the "symbol" name, draft
  property for a `.fqme: str`
- allow `.src` and `.dst` to be of type `str | Asset`

Add a new `Asset` to capture "things which can be used in markets and/or
transactions" XD
- defines a `.name`, `.atype: AssetTypeName` a financial category tag, `tx_tick:
  Decimal` the precision limit for transactions and of course
  a `.quantime()` method for doing accounting arithmetic on a given tech
  stack.
- define the `atype: AssetTypeName` type as a finite set of `str`s
  expected to be used in various ways for default settings in other
  parts of the data and order control layers..
rekt_pps
Tyler Goodlet 2023-03-14 19:40:47 -04:00
parent 9f03484c4d
commit 91dda3020e
1 changed files with 235 additions and 112 deletions

View File

@ -34,34 +34,169 @@ from decimal import (
) )
from typing import ( from typing import (
Any, Any,
Literal,
) )
from ..data.types import Struct from ..data.types import Struct
class MktPair(Struct, frozen=True): _underlyings: list[str] = [
'stock',
'bond',
'crypto_currency',
'fiat_currency',
'commodity',
]
_derivs: list[str] = [
'swap',
'future',
'continuous_future',
'option',
'futures_option',
]
# NOTE: a tag for other subsystems to try
# and do default settings for certain things:
# - allocator does unit vs. dolla size limiting.
AssetTypeName: Literal[
_underlyings
+
_derivs
]
src: str # source asset name being used to buy
src_type: str # source asset's financial type/classification name
# ^ specifies a "class" of financial instrument
# egs. stock, futer, option, bond etc. # egs. stock, futer, option, bond etc.
dst: str # destination asset name being bought
dst_type: str # destination asset's financial type/classification name
price_tick: float # minimum price increment value increment def float_digits(
price_tick_digits: int # required decimal digits for above value: float,
) -> int:
'''
Return the number of precision digits read from a decimal or float
value.
size_tick: float # minimum size (aka vlm) increment value increment '''
if value == 0:
return 0
return int(
-Decimal(str(value)).as_tuple().exponent
)
def digits_to_dec(
ndigits: int,
) -> Decimal:
'''
Return the minimum float value for an input integer value.
eg. 3 -> 0.001
'''
if ndigits == 0:
return Decimal('0')
return Decimal('0.' + '0'*(ndigits-1) + '1')
class Asset(Struct, frozen=True):
'''
Container type describing any transactable asset's technology.
'''
name: str
atype: AssetTypeName
# minimum transaction size / precision.
# eg. for buttcoin this is a "satoshi".
tx_tick: Decimal
# NOTE: additional info optionally packed in by the backend, but
# should not be explicitly required in our generic API.
info: dict = {} # make it frozen?
def __str__(self) -> str:
return self.name
def quantize(
self,
size: float,
) -> Decimal:
'''
Truncate input ``size: float`` using ``Decimal``
quantized form of the digit precision defined
by ``self.lot_tick_size``.
'''
digits = float_digits(self.tx_tick)
return Decimal(size).quantize(
Decimal(f'1.{"0".ljust(digits, "0")}'),
rounding=ROUND_HALF_EVEN
)
class MktPair(Struct, frozen=True):
'''
Market description for a pair of assets which are tradeable:
a market which enables transactions of the form,
buy: source asset -> destination asset
sell: destination asset -> source asset
The main intention of this type is for a cross-asset, venue, broker
normalized descriptive data type from which all market-auctions can
be mapped, simply.
'''
# "source asset" (name) used to buy *from*
# (or used to sell *to*)
src: str | Asset
# "destination asset" (name) used to buy *to*
# (or used to sell *from*)
dst: str | Asset
# size_tick_digits: int # required decimal digits for above
@property @property
def size_tick_digits(self) -> int: def key(self) -> str:
return self.size_tick '''
The "endpoint key" for this market.
In most other tina platforms this is referred to as the
"symbol".
'''
return f'{self.src}{self.dst}'
# the tick size is the number describing the smallest step in value
# available in this market between the source and destination
# assets.
# https://en.wikipedia.org/wiki/Tick_size
# https://en.wikipedia.org/wiki/Commodity_tick
# 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
# def size_tick_digits(self) -> int:
# return float_digits(self.size_tick)
broker: str | None = None # the middle man giving access
venue: str | None = None # market venue provider name venue: str | None = None # market venue provider name
expiry: str | None = None # for derivs, expiry datetime parseable str expiry: str | None = None # for derivs, expiry datetime parseable str
# destination asset's financial type/classification name
# NOTE: this is required for the order size allocator system,
# since we use different default settings based on the type
# of the destination asset, eg. futes use a units limits vs.
# equities a $limit.
dst_type: AssetTypeName | None = None
# source asset's financial type/classification name
# TODO: is a src type required for trading?
# there's no reason to need any more then the one-way alloc-limiter
# config right?
# src_type: AssetTypeName
# for derivs, info describing contract, egs. # for derivs, info describing contract, egs.
# strike price, call or put, swap type, exercise model, etc. # strike price, call or put, swap type, exercise model, etc.
contract_info: str | None = None contract_info: str | None = None
@ -81,13 +216,53 @@ class MktPair(Struct, frozen=True):
# fqa, fqma, .. etc. see issue: # fqa, fqma, .. etc. see issue:
# https://github.com/pikers/piker/issues/467 # https://github.com/pikers/piker/issues/467
@property @property
def fqsn(self) -> str: def fqme(self) -> str:
''' '''
Return the fully qualified market (endpoint) name 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"..
''' '''
...
# fqsn = fqme
def quantize(
self,
size: float,
quantity_type: Literal['price', 'size'] = 'size',
) -> Decimal:
'''
Truncate input ``size: float`` using ``Decimal``
and ``.size_tick``'s # of digits.
'''
match quantity_type:
case 'price':
digits = float_digits(self.price_tick)
case 'size':
digits = float_digits(self.size_tick)
return Decimal(size).quantize(
Decimal(f'1.{"0".ljust(digits, "0")}'),
rounding=ROUND_HALF_EVEN
)
# TODO: remove this?
@property
def type_key(self) -> str:
return list(self.broker_info.values())[0]['asset_type']
# @classmethod
# def from_fqme(
# cls,
# fqme: str,
# **kwargs,
# ) -> MktPair:
# broker, key, suffix = unpack_fqme(fqme)
def mk_fqsn( def mk_fqsn(
@ -103,34 +278,6 @@ def mk_fqsn(
return '.'.join([symbol, provider]).lower() return '.'.join([symbol, provider]).lower()
def float_digits(
value: float,
) -> int:
'''
Return the number of precision digits read from a float value.
'''
if value == 0:
return 0
return int(-Decimal(str(value)).as_tuple().exponent)
def digits_to_dec(
ndigits: int,
) -> Decimal:
'''
Return the minimum float value for an input integer value.
eg. 3 -> 0.001
'''
if ndigits == 0:
return Decimal('0')
return Decimal('0.' + '0'*(ndigits-1) + '1')
def unpack_fqsn(fqsn: str) -> tuple[str, str, str]: 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``.
@ -164,6 +311,10 @@ def unpack_fqsn(fqsn: str) -> tuple[str, str, str]:
suffix, suffix,
) )
unpack_fqme = unpack_fqsn
# TODO: rework the below `Symbol` (which was originally inspired and # TODO: rework the below `Symbol` (which was originally inspired and
# derived from stuff in quantdom) into a simpler, ipc msg ready, market # derived from stuff in quantdom) into a simpler, ipc msg ready, market
# endpoint meta-data container type as per the drafted interace above. # endpoint meta-data container type as per the drafted interace above.
@ -176,37 +327,9 @@ class Symbol(Struct):
key: str key: str
tick_size: float = 0.01 tick_size: float = 0.01
lot_tick_size: float = 0.0 # "volume" precision as min step value lot_tick_size: float = 0.0 # "volume" precision as min step value
tick_size_digits: int = 2
lot_size_digits: int = 0
suffix: str = '' suffix: str = ''
broker_info: dict[str, dict[str, Any]] = {} broker_info: dict[str, dict[str, Any]] = {}
@classmethod
def from_broker_info(
cls,
broker: str,
symbol: str,
info: dict[str, Any],
suffix: str = '',
) -> Symbol:
tick_size = info.get('price_tick_size', 0.01)
lot_size = info.get('lot_tick_size', 0.0)
return Symbol(
key=symbol,
tick_size=tick_size,
lot_tick_size=lot_size,
tick_size_digits=float_digits(tick_size),
lot_size_digits=float_digits(lot_size),
suffix=suffix,
broker_info={broker: info},
)
@classmethod @classmethod
def from_fqsn( def from_fqsn(
cls, cls,
@ -215,13 +338,25 @@ class Symbol(Struct):
) -> Symbol: ) -> Symbol:
broker, key, suffix = unpack_fqsn(fqsn) broker, key, suffix = unpack_fqsn(fqsn)
return cls.from_broker_info( tick_size = info.get('price_tick_size', 0.01)
broker, lot_size = info.get('lot_tick_size', 0.0)
key,
info=info, return Symbol(
key=key,
tick_size=tick_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},
) )
# compat name mapping
from_fqme = from_fqsn
@property @property
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']
@ -230,38 +365,20 @@ class Symbol(Struct):
def brokers(self) -> list[str]: def brokers(self) -> list[str]:
return list(self.broker_info.keys()) return list(self.broker_info.keys())
def nearest_tick(self, value: float) -> float: @property
''' def tick_size_digits(self) -> int:
Return the nearest tick value based on mininum increment. return float_digits(self.lot_tick_size)
''' @property
mult = 1 / self.tick_size def lot_size_digits(self) -> int:
return round(value * mult) / mult return float_digits(self.lot_tick_size)
def front_feed(self) -> tuple[str, str]: @property
''' def broker(self) -> str:
Return the "current" feed key for this symbol. return list(self.broker_info.keys())[0]
(i.e. the broker + symbol key in a tuple).
'''
return (
list(self.broker_info.keys())[0],
self.key,
)
def tokens(self) -> tuple[str]:
broker, key = self.front_feed()
if self.suffix:
return (key, self.suffix, broker)
else:
return (key, broker)
@property @property
def fqsn(self) -> str: def fqsn(self) -> str:
return '.'.join(self.tokens()).lower()
def front_fqsn(self) -> str:
''' '''
fqsn = "fully qualified symbol name" fqsn = "fully qualified symbol name"
@ -279,24 +396,30 @@ class Symbol(Struct):
<broker>.<venue>.<instrumentname>.<suffixwithmetadata> <broker>.<venue>.<instrumentname>.<suffixwithmetadata>
''' '''
tokens = self.tokens() broker = self.broker
fqsn = '.'.join(map(str.lower, tokens)) key = self.key
return fqsn if self.suffix:
tokens = (key, self.suffix, broker)
else:
tokens = (key, broker)
def quantize_size( return '.'.join(tokens).lower()
fqme = fqsn
def quantize(
self, self,
size: float, size: float,
) -> Decimal: ) -> Decimal:
''' '''
Truncate input ``size: float`` using ``Decimal`` Truncate input ``size: float`` using ``Decimal``
and ``.lot_size_digits``. quantized form of the digit precision defined
by ``self.lot_tick_size``.
''' '''
digits = self.lot_size_digits 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")}'),
rounding=ROUND_HALF_EVEN rounding=ROUND_HALF_EVEN
) )