diff --git a/piker/accounting/__init__.py b/piker/accounting/__init__.py index e27dc4bf..1b776714 100644 --- a/piker/accounting/__init__.py +++ b/piker/accounting/__init__.py @@ -42,7 +42,6 @@ from ._mktinfo import ( dec_digits, digits_to_dec, MktPair, - Symbol, unpack_fqme, _derivs as DerivTypes, ) @@ -60,7 +59,6 @@ __all__ = [ 'Asset', 'MktPair', 'Position', - 'Symbol', 'Transaction', 'TransactionLedger', 'dec_digits', diff --git a/piker/accounting/_ledger.py b/piker/accounting/_ledger.py index 82a77107..d2e56bf0 100644 --- a/piker/accounting/_ledger.py +++ b/piker/accounting/_ledger.py @@ -40,7 +40,7 @@ import tomli_w # for fast ledger writing from piker.types import Struct from piker import config -from ..log import get_logger +from piker.log import get_logger from .calc import ( iter_by_dt, ) @@ -239,7 +239,9 @@ class TransactionLedger(UserDict): symcache: SymbologyCache = self._symcache towrite: dict[str, Any] = {} - for tid, txdict in self.tx_sort(self.data.copy()): + for tid, txdict in self.tx_sort( + self.data.copy() + ): # write blank-str expiry for non-expiring assets if ( 'expiry' in txdict @@ -377,7 +379,7 @@ def open_trade_ledger( account, dirpath=_fp, ) - cpy = ledger_dict.copy() + cpy: dict = ledger_dict.copy() # XXX NOTE: if not provided presume we are being called from # sync code and need to maybe run `trio` to generate.. @@ -406,7 +408,13 @@ def open_trade_ledger( account=account, mod=mod, symcache=symcache, - tx_sort=getattr(mod, 'tx_sort', tx_sort), + + # NOTE: allow backends to provide custom ledger sorting + tx_sort=getattr( + mod, + 'tx_sort', + tx_sort, + ), ) try: yield ledger diff --git a/piker/accounting/_mktinfo.py b/piker/accounting/_mktinfo.py index a8c719da..0af20c3b 100644 --- a/piker/accounting/_mktinfo.py +++ b/piker/accounting/_mktinfo.py @@ -305,8 +305,8 @@ class MktPair(Struct, frozen=True): # config right? # src_type: AssetTypeName - # for derivs, info describing contract, egs. - # strike price, call or put, swap type, exercise model, etc. + # for derivs, info describing contract, egs. strike price, call + # or put, swap type, exercise model, etc. contract_info: list[str] | None = None # TODO: rename to sectype since all of these can @@ -390,8 +390,8 @@ class MktPair(Struct, frozen=True): cls, fqme: str, - price_tick: float | str, - size_tick: float | str, + price_tick: float|str, + size_tick: float|str, bs_mktid: str, broker: str | None = None, @@ -677,90 +677,3 @@ def unpack_fqme( # '.'.join([mkt_ep, venue]), suffix, ) - - -class Symbol(Struct): - ''' - I guess this is some kinda container thing for dealing with - all the different meta-data formats from brokers? - - ''' - key: str - - broker: str = '' - venue: str = '' - - # precision descriptors for price and vlm - tick_size: Decimal = Decimal('0.01') - lot_tick_size: Decimal = Decimal('0.0') - - suffix: str = '' - broker_info: dict[str, dict[str, Any]] = {} - - @classmethod - def from_fqme( - cls, - fqsn: str, - info: dict[str, Any], - - ) -> Symbol: - broker, mktep, venue, suffix = unpack_fqme(fqsn) - tick_size = info.get('price_tick_size', 0.01) - lot_size = info.get('lot_tick_size', 0.0) - - return Symbol( - broker=broker, - key=mktep, - tick_size=tick_size, - lot_tick_size=lot_size, - venue=venue, - suffix=suffix, - broker_info={broker: info}, - ) - - @property - def type_key(self) -> str: - return list(self.broker_info.values())[0]['asset_type'] - - @property - def tick_size_digits(self) -> int: - return float_digits(self.tick_size) - - @property - def lot_size_digits(self) -> int: - return float_digits(self.lot_tick_size) - - @property - def price_tick(self) -> Decimal: - return Decimal(str(self.tick_size)) - - @property - def size_tick(self) -> Decimal: - return Decimal(str(self.lot_tick_size)) - - @property - def broker(self) -> str: - return list(self.broker_info.keys())[0] - - @property - def fqme(self) -> str: - return maybe_cons_tokens([ - self.key, # final "pair name" (eg. qqq[/usd], btcusdt) - self.venue, - self.suffix, # includes expiry and other con info - self.broker, - ]) - - def quantize( - self, - size: float, - ) -> Decimal: - digits = float_digits(self.lot_tick_size) - return Decimal(size).quantize( - Decimal(f'1.{"0".ljust(digits, "0")}'), - rounding=ROUND_HALF_EVEN - ) - - # NOTE: when cast to `str` return fqme - def __str__(self) -> str: - return self.fqme diff --git a/piker/accounting/_pos.py b/piker/accounting/_pos.py index 1b305009..5952418f 100644 --- a/piker/accounting/_pos.py +++ b/piker/accounting/_pos.py @@ -30,7 +30,8 @@ from types import ModuleType from typing import ( Any, Iterator, - Generator + Generator, + TYPE_CHECKING, ) import pendulum @@ -59,8 +60,10 @@ from ..clearing._messages import ( BrokerdPosition, ) from piker.types import Struct -from piker.data._symcache import SymbologyCache -from ..log import get_logger +from piker.log import get_logger + +if TYPE_CHECKING: + from piker.data._symcache import SymbologyCache log = get_logger(__name__) @@ -493,6 +496,17 @@ class Account(Struct): _mktmap_table: dict[str, MktPair] | None = None, + only_require: list[str]|True = True, + # ^list of fqmes that are "required" to be processed from + # this ledger pass; we often don't care about others and + # definitely shouldn't always error in such cases. + # (eg. broker backend loaded that doesn't yet supsport the + # symcache but also, inside the paper engine we don't ad-hoc + # request `get_mkt_info()` for every symbol in the ledger, + # only the one for which we're simulating against). + # TODO, not sure if there's a better soln for this, ideally + # all backends get symcache support afap i guess.. + ) -> dict[str, Position]: ''' Update the internal `.pps[str, Position]` table from input @@ -535,11 +549,32 @@ class Account(Struct): if _mktmap_table is None: raise + required: bool = ( + only_require is True + or ( + only_require is not True + and + fqme in only_require + ) + ) # XXX: caller is allowed to provide a fallback # mktmap table for the case where a new position is # being added and the preloaded symcache didn't # have this entry prior (eg. with frickin IB..) - mkt = _mktmap_table[fqme] + if ( + not (mkt := _mktmap_table.get(fqme)) + and + required + ): + raise + + elif not required: + continue + + else: + # should be an entry retreived somewhere + assert mkt + if not (pos := pps.get(bs_mktid)): @@ -656,7 +691,7 @@ class Account(Struct): def write_config(self) -> None: ''' Write the current account state to the user's account TOML file, normally - something like ``pps.toml``. + something like `pps.toml`. ''' # TODO: show diff output? diff --git a/piker/accounting/calc.py b/piker/accounting/calc.py index ac4d9c22..5919c0d6 100644 --- a/piker/accounting/calc.py +++ b/piker/accounting/calc.py @@ -251,10 +251,16 @@ def iter_by_dt( for k in parsers: if ( isdict and k in tx - or getattr(tx, k, None) + or + getattr(tx, k, None) ): - v = tx[k] if isdict else tx.dt - assert v is not None, f'No valid value for `{k}`!?' + v = ( + tx[k] if isdict + else tx.dt + ) + assert v is not None, ( + f'No valid value for `{k}`!?' + ) # only call parser on the value if not None from # the `parsers` table above (when NOT using @@ -269,8 +275,21 @@ def iter_by_dt( return v else: - # XXX: should never get here.. - breakpoint() + # TODO: move to top? + from piker.log import get_logger + log = get_logger(__name__) + + # XXX: we should really never get here.. + # only if a ledger record has no expected sort(able) + # field will we likely hit this.. like with ze IB. + # if no sortable field just deliver epoch? + log.warning( + 'No (time) sortable field for TXN:\n' + f'{tx}\n' + ) + return from_timestamp(0) + # breakpoint() + entry: tuple[str, dict] | Transaction for entry in sorted( diff --git a/piker/accounting/cli.py b/piker/accounting/cli.py index 6a62f294..f68cdfca 100644 --- a/piker/accounting/cli.py +++ b/piker/accounting/cli.py @@ -300,7 +300,8 @@ def disect( assert not df.is_empty() # muck around in pdbp REPL - breakpoint() + # tractor.devx.mk_pdb().set_trace() + # breakpoint() # TODO: we REALLY need a better console REPL for this # kinda thing.. diff --git a/piker/clearing/_paper_engine.py b/piker/clearing/_paper_engine.py index e303d76c..60835598 100644 --- a/piker/clearing/_paper_engine.py +++ b/piker/clearing/_paper_engine.py @@ -655,6 +655,7 @@ async def open_trade_dialog( # in) use manually constructed table from calling # the `.get_mkt_info()` provider EP above. _mktmap_table=mkt_by_fqme, + only_require=list(mkt_by_fqme), ) pp_msgs: list[BrokerdPosition] = [] diff --git a/piker/data/_symcache.py b/piker/data/_symcache.py index fc1057a0..bcaa5844 100644 --- a/piker/data/_symcache.py +++ b/piker/data/_symcache.py @@ -31,6 +31,7 @@ from pathlib import Path from pprint import pformat from typing import ( Any, + Callable, Sequence, Hashable, TYPE_CHECKING, @@ -56,7 +57,7 @@ from piker.brokers import ( ) if TYPE_CHECKING: - from ..accounting import ( + from piker.accounting import ( Asset, MktPair, ) @@ -149,57 +150,68 @@ class SymbologyCache(Struct): 'Implement `Client.get_assets()`!' ) - if get_mkt_pairs := getattr(client, 'get_mkt_pairs', None): - - pairs: dict[str, Struct] = await get_mkt_pairs() - for bs_fqme, pair in pairs.items(): - - # NOTE: every backend defined pair should - # declare it's ns path for roundtrip - # serialization lookup. - if not getattr(pair, 'ns_path', None): - raise TypeError( - f'Pair-struct for {self.mod.name} MUST define a ' - '`.ns_path: str`!\n' - f'{pair}' - ) - - entry = await self.mod.get_mkt_info(pair.bs_fqme) - if not entry: - continue - - mkt: MktPair - pair: Struct - mkt, _pair = entry - assert _pair is pair, ( - f'`{self.mod.name}` backend probably has a ' - 'keying-symmetry problem between the pair-`Struct` ' - 'returned from `Client.get_mkt_pairs()`and the ' - 'module level endpoint: `.get_mkt_info()`\n\n' - "Here's the struct diff:\n" - f'{_pair - pair}' - ) - # NOTE XXX: this means backends MUST implement - # a `Struct.bs_mktid: str` field to provide - # a native-keyed map to their own symbol - # set(s). - self.pairs[pair.bs_mktid] = pair - - # NOTE: `MktPair`s are keyed here using piker's - # internal FQME schema so that search, - # accounting and feed init can be accomplished - # a sane, uniform, normalized basis. - self.mktmaps[mkt.fqme] = mkt - - self.pair_ns_path: str = tractor.msg.NamespacePath.from_ref( - pair, - ) - - else: + get_mkt_pairs: Callable|None = getattr( + client, + 'get_mkt_pairs', + None, + ) + if not get_mkt_pairs: log.warning( 'No symbology cache `Pair` support for `{provider}`..\n' 'Implement `Client.get_mkt_pairs()`!' ) + return self + + pairs: dict[str, Struct] = await get_mkt_pairs() + if not pairs: + log.warning( + 'No pairs from intial {provider!r} sym-cache request?\n\n' + '`Client.get_mkt_pairs()` -> {pairs!r} ?' + ) + return self + + for bs_fqme, pair in pairs.items(): + if not getattr(pair, 'ns_path', None): + # XXX: every backend defined pair must declare + # a `.ns_path: tractor.NamespacePath` to enable + # roundtrip serialization lookup from a local + # cache file. + raise TypeError( + f'Pair-struct for {self.mod.name} MUST define a ' + '`.ns_path: str`!\n\n' + f'{pair!r}' + ) + + entry = await self.mod.get_mkt_info(pair.bs_fqme) + if not entry: + continue + + mkt: MktPair + pair: Struct + mkt, _pair = entry + assert _pair is pair, ( + f'`{self.mod.name}` backend probably has a ' + 'keying-symmetry problem between the pair-`Struct` ' + 'returned from `Client.get_mkt_pairs()`and the ' + 'module level endpoint: `.get_mkt_info()`\n\n' + "Here's the struct diff:\n" + f'{_pair - pair}' + ) + # NOTE XXX: this means backends MUST implement + # a `Struct.bs_mktid: str` field to provide + # a native-keyed map to their own symbol + # set(s). + self.pairs[pair.bs_mktid] = pair + + # NOTE: `MktPair`s are keyed here using piker's + # internal FQME schema so that search, + # accounting and feed init can be accomplished + # a sane, uniform, normalized basis. + self.mktmaps[mkt.fqme] = mkt + + self.pair_ns_path: str = tractor.msg.NamespacePath.from_ref( + pair, + ) return self