diff --git a/piker/accounting/_ledger.py b/piker/accounting/_ledger.py index acfe97f7..b6f0ee74 100644 --- a/piker/accounting/_ledger.py +++ b/piker/accounting/_ledger.py @@ -32,9 +32,7 @@ from typing import ( ) from pendulum import ( - datetime, - # DateTime, - # parse, + DateTime, ) import tomli_w # for fast ledger writing @@ -70,7 +68,7 @@ class Transaction(Struct, frozen=True): size: float price: float cost: float # commisions or other additional costs - dt: datetime + dt: DateTime # the "event type" in terms of "market events" see # https://github.com/pikers/piker/issues/510 for where we're @@ -80,7 +78,7 @@ class Transaction(Struct, frozen=True): # TODO: we can drop this right since we # can instead expect the backend to provide this # via the `MktPair`? - expiry: datetime | None = None + expiry: DateTime | None = None # (optional) key-id defined by the broker-service backend which # ensures the instrument-symbol market key for this record is unique @@ -89,8 +87,11 @@ class Transaction(Struct, frozen=True): # service. bs_mktid: str | int | None = None - def to_dict(self) -> dict: - dct: dict[str, Any] = super().to_dict() + def to_dict( + self, + **kwargs, + ) -> dict: + dct: dict[str, Any] = super().to_dict(**kwargs) # ensure we use a pendulum formatted # ISO style str here!@ @@ -107,6 +108,8 @@ class TransactionLedger(UserDict): outside. ''' + # NOTE: see `open_trade_ledger()` for defaults, this should + # never be constructed manually! def __init__( self, ledger_dict: dict, @@ -138,6 +141,10 @@ class TransactionLedger(UserDict): @property def symcache(self) -> SymbologyCache: + ''' + Read-only ref to backend's ``SymbologyCache``. + + ''' return self._symcache def update_from_t( @@ -157,7 +164,7 @@ class TransactionLedger(UserDict): symcache: SymbologyCache | None = None, ) -> Generator[ - tuple[str, Transaction], + Transaction, None, None, ]: @@ -175,9 +182,13 @@ class TransactionLedger(UserDict): norm_trade = self.mod.norm_trade # datetime-sort and pack into txs - for txdict in self.tx_sort(self.data.values()): - txn = norm_trade(txdict) - yield txn.tid, txn + for tid, txdict in self.tx_sort(self.data.items()): + txn: Transaction = norm_trade( + tid, + txdict, + pairs=symcache.pairs, + ) + yield txn def to_txns( self, @@ -188,18 +199,19 @@ class TransactionLedger(UserDict): Return entire output from ``.iter_txns()`` in a ``dict``. ''' - return dict(self.iter_txns(symcache=symcache)) + return { + t.tid: t for t in self.iter_txns(symcache=symcache) + } - def write_config( - self, - - ) -> None: + def write_config(self) -> None: ''' Render the self.data ledger dict to its TOML file form. ALWAYS order datetime sorted! ''' + is_paper: bool = self.account == 'paper' + towrite: dict[str, Any] = {} for tid, txdict in self.tx_sort(self.data.copy()): # write blank-str expiry for non-expiring assets @@ -210,42 +222,44 @@ class TransactionLedger(UserDict): txdict['expiry'] = '' # (maybe) re-write old acro-key - fqme: str = txdict.pop('fqsn', None) or txdict['fqme'] - bs_mktid: str | None = txdict.get('bs_mktid') + if is_paper: + fqme: str = txdict.pop('fqsn', None) or txdict['fqme'] + bs_mktid: str | None = txdict.get('bs_mktid') - if ( - fqme not in self._symcache.mktmaps - or ( - # also try to see if this is maybe a paper - # engine ledger in which case the bs_mktid - # should be the fqme as well! - self.account == 'paper' - and bs_mktid - and fqme != bs_mktid - ) - ): - # always take any (paper) bs_mktid if defined and - # in the backend's cache key set. - if bs_mktid in self._symcache.mktmaps: - fqme: str = bs_mktid - else: - best_fqme: str = list(self._symcache.search(fqme))[0] - log.warning( - f'Could not find FQME: {fqme} in qualified set?\n' - f'Qualifying and expanding {fqme} -> {best_fqme}' + if ( + fqme not in self._symcache.mktmaps + or ( + # also try to see if this is maybe a paper + # engine ledger in which case the bs_mktid + # should be the fqme as well! + bs_mktid + and fqme != bs_mktid ) - fqme = best_fqme + ): + # always take any (paper) bs_mktid if defined and + # in the backend's cache key set. + if bs_mktid in self._symcache.mktmaps: + fqme: str = bs_mktid + else: + best_fqme: str = list(self._symcache.search(fqme))[0] + log.warning( + f'Could not find FQME: {fqme} in qualified set?\n' + f'Qualifying and expanding {fqme} -> {best_fqme}' + ) + fqme = best_fqme - if ( - self.account == 'paper' - and bs_mktid - and bs_mktid != fqme - ): - # in paper account case always make sure both the - # fqme and bs_mktid are fully qualified.. - txdict['bs_mktid'] = fqme + if ( + bs_mktid + and bs_mktid != fqme + ): + # in paper account case always make sure both the + # fqme and bs_mktid are fully qualified.. + txdict['bs_mktid'] = fqme + + # in paper ledgers always write the latest + # symbology key field: an FQME. + txdict['fqme'] = fqme - txdict['fqme'] = fqme towrite[tid] = txdict with self.file_path.open(mode='wb') as fp: @@ -256,6 +270,9 @@ def load_ledger( brokername: str, acctid: str, + # for testing or manual load from file + dirpath: Path | None = None, + ) -> tuple[dict, Path]: ''' Load a ledger (TOML) file from user's config directory: @@ -270,7 +287,11 @@ def load_ledger( except ModuleNotFoundError: import tomli as tomllib - ldir: Path = config._config_dir / 'accounting' / 'ledgers' + ldir: Path = ( + dirpath + or + config._config_dir / 'accounting' / 'ledgers' + ) if not ldir.is_dir(): ldir.mkdir() @@ -303,6 +324,9 @@ def open_trade_ledger( tx_sort: Callable = iter_by_dt, rewrite: bool = False, + # for testing or manual load from file + _fp: Path | None = None, + ) -> Generator[TransactionLedger, None, None]: ''' Indempotently create and read in a trade log file from the @@ -316,7 +340,11 @@ def open_trade_ledger( from ..brokers import get_brokermod mod: ModuleType = get_brokermod(broker) - ledger_dict, fpath = load_ledger(broker, account) + ledger_dict, fpath = load_ledger( + broker, + account, + dirpath=_fp, + ) cpy = ledger_dict.copy() # XXX NOTE: if not provided presume we are being called from