From 3a4794e9d1c9215a03fc7ce3a1e5bc0571730a07 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 2 Mar 2023 19:03:40 -0500 Subject: [PATCH] Backward-compat: don't require `'lot_tick_size'` In order to support existing `pps.toml` files in the wild which don't have the `asset_type, price_tick_size, lot_tick_size` fields, we need to only optionally read them and instead expect that backends will write the fields going forward (coming in follow patches). Further this makes some small asset-size (vlm accounting) quantization related adjustments: - rename `Symbol.decimal_quant()` -> `.quantize_size()` since that is explicitly what this method is doing. - and expect an input `size: float` which we cast to decimal instead of doing it inside the `.calc_size()` caller code. - drop `Symbol.iterfqsns()` which wasn't being used anywhere at all.. Additionally, this drafts out a new replacement market-trading-pair data type to eventually replace `.data._source.Symbol` -> `MktPair` which we aren't using yet, but serves as the documentation-driven motivator ;) and, it relates to https://github.com/pikers/piker/issues/467. --- piker/data/_source.py | 83 +++++++++++++++++++++++++++++++++++-------- piker/pp.py | 30 +++++++++++----- 2 files changed, 89 insertions(+), 24 deletions(-) diff --git a/piker/data/_source.py b/piker/data/_source.py index e8f09484..d358cd96 100644 --- a/piker/data/_source.py +++ b/piker/data/_source.py @@ -18,7 +18,10 @@ numpy data source coversion helpers. """ from __future__ import annotations -from decimal import Decimal, ROUND_HALF_EVEN +from decimal import ( + Decimal, + ROUND_HALF_EVEN, +) from typing import Any from bidict import bidict @@ -77,6 +80,10 @@ def mk_fqsn( def float_digits( value: float, ) -> int: + ''' + Return the number of precision digits read from a float value. + + ''' if value == 0: return 0 @@ -127,6 +134,56 @@ def unpack_fqsn(fqsn: str) -> tuple[str, str, str]: ) +class MktPair(Struct, frozen=True): + + 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. + + 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 + price_tick_digits: int # required decimal digits for above + + size_tick: float # minimum size (aka vlm) increment value increment + size_tick_digits: int # required decimal digits for above + + venue: str | None = None # market venue provider name + expiry: str | None = None # for derivs, expiry datetime parseable str + + # for derivs, info describing contract, egs. + # strike price, call or put, swap type, exercise model, etc. + contract_info: str | None = None + + @classmethod + def from_msg( + self, + msg: dict[str, Any], + + ) -> MktPair: + ''' + Constructor for a received msg-dict normally received over IPC. + + ''' + ... + + # fqa, fqma, .. etc. see issue: + # https://github.com/pikers/piker/issues/467 + @property + def fqsn(self) -> str: + ''' + Return the fully qualified market (endpoint) name for the + pair of transacting assets. + + ''' + ... + + +# 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): ''' I guess this is some kinda container thing for dealing with @@ -141,10 +198,6 @@ class Symbol(Struct): suffix: str = '' broker_info: dict[str, dict[str, Any]] = {} - # specifies a "class" of financial instrument - # ex. stock, futer, option, bond etc. - - # @validate_arguments @classmethod def from_broker_info( cls, @@ -244,23 +297,23 @@ class Symbol(Struct): fqsn = '.'.join(map(str.lower, tokens)) return fqsn - def iterfqsns(self) -> list[str]: - keys = [] - for broker in self.broker_info.keys(): - fqsn = mk_fqsn(self.key, broker) - if self.suffix: - fqsn += f'.{self.suffix}' - keys.append(fqsn) + def quantize_size( + self, + size: float, - return keys + ) -> Decimal: + ''' + Truncate input ``size: float`` using ``Decimal`` + and ``.lot_size_digits``. - def decimal_quant(self, d: Decimal): + ''' digits = self.lot_size_digits - return d.quantize( + return Decimal(size).quantize( Decimal(f'1.{"0".ljust(digits, "0")}'), rounding=ROUND_HALF_EVEN ) + def _nan_to_closest_num(array: np.ndarray): """Return interpolated values instead of NaN. diff --git a/piker/pp.py b/piker/pp.py index 1a2f5e6b..38ff1566 100644 --- a/piker/pp.py +++ b/piker/pp.py @@ -22,8 +22,6 @@ that doesn't try to cuk most humans who prefer to not lose their moneys.. ''' from __future__ import annotations from contextlib import contextmanager as cm -from pathlib import Path -from decimal import Decimal, ROUND_HALF_EVEN from pprint import pformat import os from os import path @@ -91,10 +89,12 @@ def open_trade_ledger( yield cpy finally: if cpy != ledger: + # TODO: show diff output? # https://stackoverflow.com/questions/12956957/print-diff-of-python-dictionaries log.info(f'Updating ledger for {tradesfile}:\n') - ledger.update(cpy) + ledger.update(cpy) + # we write on close the mutated ledger data with open(tradesfile, 'w') as cf: toml.dump(ledger, cf) @@ -476,7 +476,7 @@ class Position(Struct): if self.split_ratio is not None: size = round(size * self.split_ratio) - return float(self.symbol.decimal_quant(Decimal(size))) + return float(self.symbol.quantize_size(size)) def minimize_clears( self, @@ -934,10 +934,18 @@ def open_pps( for fqsn, entry in pps.items(): bsuid = entry['bsuid'] symbol = Symbol.from_fqsn( - fqsn, info={ - 'asset_type': entry['asset_type'], - 'price_tick_size': entry['price_tick_size'], - 'lot_tick_size': entry['lot_tick_size'] + fqsn, + + # NOTE & TODO: right now we fill in the defaults from + # `.data._source.Symbol` but eventually these should always + # either be already written to the pos table or provided at + # write time to ensure always having these values somewhere + # and thus allowing us to get our pos sizing precision + # correct! + info={ + 'asset_type': entry.get('asset_type', ''), + 'price_tick_size': entry.get('price_tick_size', 0.01), + 'lot_tick_size': entry.get('lot_tick_size', 0.0), } ) @@ -977,7 +985,11 @@ def open_pps( size = entry['size'] # TODO: remove but, handle old field name for now - ppu = entry.get('ppu', entry.get('be_price', 0)) + ppu = entry.get( + 'ppu', + entry.get('be_price', 0), + ) + split_ratio = entry.get('split_ratio') expiry = entry.get('expiry')