Formalize a ledger type + API: `TransactionLedger`
Add a new `class TransactionLedger(collections.UserDict)` for managing ledger (files) from a `dict`-like API. The main motivations being easy conversion between `dict` <-> `Transaction` obj forms as well as dynamic (toml) file updates via a set of methods: - `.write_config()` to render and write state to the local toml file. - `.iter_trans()` to allow iterator style conversion to `Transaction` form for each entry. - `.to_trans()` for the dict output from the above. Some adjustments to `Transaction` namely making `.sym/.sys` optional for now so that paper engine entries can be loaded (offline) without connecting to the emulated broker backend. Move to using `pathlib.Path` throughout for bootyful toml file mgmt B)pre_overruns_ctxcancelled
							parent
							
								
									055025c64c
								
							
						
					
					
						commit
						2f31e40d3b
					
				| 
						 | 
				
			
			@ -21,21 +21,23 @@ for tendiez.
 | 
			
		|||
'''
 | 
			
		||||
from ..log import get_logger
 | 
			
		||||
 | 
			
		||||
from ._pos import (
 | 
			
		||||
from ._ledger import (
 | 
			
		||||
    Transaction,
 | 
			
		||||
    TransactionLedger,
 | 
			
		||||
    open_trade_ledger,
 | 
			
		||||
    PpTable,
 | 
			
		||||
)
 | 
			
		||||
from ._pos import (
 | 
			
		||||
    open_pps,
 | 
			
		||||
    load_pps_from_ledger,
 | 
			
		||||
    open_pps,
 | 
			
		||||
    Position,
 | 
			
		||||
    PpTable,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
log = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
    'Transaction',
 | 
			
		||||
    'TransactionLedger',
 | 
			
		||||
    'open_trade_ledger',
 | 
			
		||||
    'PpTable',
 | 
			
		||||
    'open_pps',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,9 +19,9 @@ Trade and transaction ledger processing.
 | 
			
		|||
 | 
			
		||||
'''
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
from collections import UserDict
 | 
			
		||||
from contextlib import contextmanager as cm
 | 
			
		||||
import os
 | 
			
		||||
from os import path
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import time
 | 
			
		||||
from typing import (
 | 
			
		||||
    Any,
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +32,7 @@ from typing import (
 | 
			
		|||
 | 
			
		||||
from pendulum import (
 | 
			
		||||
    datetime,
 | 
			
		||||
    parse,
 | 
			
		||||
)
 | 
			
		||||
import tomli
 | 
			
		||||
import toml
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +49,145 @@ from ._mktinfo import (
 | 
			
		|||
log = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Transaction(Struct, frozen=True):
 | 
			
		||||
 | 
			
		||||
    # TODO: unify this with the `MktPair`,
 | 
			
		||||
    # once we have that as a required field,
 | 
			
		||||
    # we don't really need the fqsn any more..
 | 
			
		||||
    fqsn: str
 | 
			
		||||
 | 
			
		||||
    tid: Union[str, int]  # unique transaction id
 | 
			
		||||
    size: float
 | 
			
		||||
    price: float
 | 
			
		||||
    cost: float  # commisions or other additional costs
 | 
			
		||||
    dt: datetime
 | 
			
		||||
 | 
			
		||||
    # TODO: we can drop this right since we
 | 
			
		||||
    # can instead expect the backend to provide this
 | 
			
		||||
    # via the `MktPair`?
 | 
			
		||||
    expiry: datetime | None = None
 | 
			
		||||
 | 
			
		||||
    # remap for back-compat
 | 
			
		||||
    @property
 | 
			
		||||
    def fqme(self) -> str:
 | 
			
		||||
        return self.fqsn
 | 
			
		||||
 | 
			
		||||
    # TODO: drop the Symbol type
 | 
			
		||||
 | 
			
		||||
    # the underlying "transaction system", normally one of a ``MktPair``
 | 
			
		||||
    # (a description of a tradable double auction) or a ledger-recorded
 | 
			
		||||
    # ("ledger" in any sense as long as you can record transfers) of any
 | 
			
		||||
    # sort) ``Asset``.
 | 
			
		||||
    sym: MktPair | Asset | Symbol | None = None
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def sys(self) -> Symbol:
 | 
			
		||||
        return self.sym
 | 
			
		||||
 | 
			
		||||
    # (optional) key-id defined by the broker-service backend which
 | 
			
		||||
    # ensures the instrument-symbol market key for this record is unique
 | 
			
		||||
    # in the "their backend/system" sense; i.e. this uid for the market
 | 
			
		||||
    # as defined (internally) in some namespace defined by the broker
 | 
			
		||||
    # service.
 | 
			
		||||
    bs_mktid: str | int | None = None
 | 
			
		||||
 | 
			
		||||
    def to_dict(self) -> dict:
 | 
			
		||||
        dct = super().to_dict()
 | 
			
		||||
        # ensure we use a pendulum formatted
 | 
			
		||||
        # ISO style str here!@
 | 
			
		||||
        dct['dt'] = str(self.dt)
 | 
			
		||||
        return dct
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionLedger(UserDict):
 | 
			
		||||
    '''
 | 
			
		||||
    Very simple ``dict`` wrapper + ``pathlib.Path`` handle to
 | 
			
		||||
    a TOML formatted transaction file for enabling file writes
 | 
			
		||||
    dynamically whilst still looking exactly like a ``dict`` from the
 | 
			
		||||
    outside.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
        ledger_dict: dict,
 | 
			
		||||
        file_path: Path,
 | 
			
		||||
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        self.file_path = file_path
 | 
			
		||||
        super().__init__(ledger_dict)
 | 
			
		||||
 | 
			
		||||
    def write_config(self) -> None:
 | 
			
		||||
        '''
 | 
			
		||||
        Render the self.data ledger dict to it's TML file form.
 | 
			
		||||
 | 
			
		||||
        '''
 | 
			
		||||
        with self.file_path.open(mode='w') as fp:
 | 
			
		||||
            toml.dump(self.data, fp)
 | 
			
		||||
 | 
			
		||||
    def iter_trans(
 | 
			
		||||
        self,
 | 
			
		||||
        broker: str = 'paper',
 | 
			
		||||
 | 
			
		||||
    ) -> Generator[
 | 
			
		||||
        tuple[str, Transaction],
 | 
			
		||||
        None,
 | 
			
		||||
        None,
 | 
			
		||||
    ]:
 | 
			
		||||
        '''
 | 
			
		||||
        Deliver trades records in ``(key: str, t: Transaction)``
 | 
			
		||||
        form via generator.
 | 
			
		||||
 | 
			
		||||
        '''
 | 
			
		||||
        if broker != 'paper':
 | 
			
		||||
            raise NotImplementedError('Per broker support not dun yet!')
 | 
			
		||||
 | 
			
		||||
            # TODO: lookup some standard normalizer
 | 
			
		||||
            # func in the backend?
 | 
			
		||||
            # from ..brokers import get_brokermod
 | 
			
		||||
            # mod = get_brokermod(broker)
 | 
			
		||||
            # trans_dict = mod.norm_trade_records(self.data)
 | 
			
		||||
 | 
			
		||||
            # NOTE: instead i propose the normalizer is
 | 
			
		||||
            # a one shot routine (that can be lru cached)
 | 
			
		||||
            # and instead call it for each entry incrementally:
 | 
			
		||||
            # normer = mod.norm_trade_record(txdict)
 | 
			
		||||
 | 
			
		||||
        for tid, txdict in self.data.items():
 | 
			
		||||
            # special field handling for datetimes
 | 
			
		||||
            # to ensure pendulum is used!
 | 
			
		||||
            fqme = txdict.get('fqme', txdict['fqsn'])
 | 
			
		||||
            dt = parse(txdict['dt'])
 | 
			
		||||
            expiry = txdict.get('expiry')
 | 
			
		||||
 | 
			
		||||
            yield (
 | 
			
		||||
                tid,
 | 
			
		||||
                Transaction(
 | 
			
		||||
                    fqsn=fqme,
 | 
			
		||||
                    tid=txdict['tid'],
 | 
			
		||||
                    dt=dt,
 | 
			
		||||
                    price=txdict['price'],
 | 
			
		||||
                    size=txdict['size'],
 | 
			
		||||
                    cost=txdict.get('cost', 0),
 | 
			
		||||
                    bs_mktid=txdict['bs_mktid'],
 | 
			
		||||
 | 
			
		||||
                    # optional
 | 
			
		||||
                    sym=None,
 | 
			
		||||
                    expiry=parse(expiry) if expiry else None,
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def to_trans(
 | 
			
		||||
        self,
 | 
			
		||||
        broker: str = 'paper',
 | 
			
		||||
 | 
			
		||||
    ) -> dict[str, Transaction]:
 | 
			
		||||
        '''
 | 
			
		||||
        Return the entire output from ``.iter_trans()`` in a ``dict``.
 | 
			
		||||
 | 
			
		||||
        '''
 | 
			
		||||
        return dict(self.iter_trans())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cm
 | 
			
		||||
def open_trade_ledger(
 | 
			
		||||
    broker: str,
 | 
			
		||||
| 
						 | 
				
			
			@ -63,82 +203,39 @@ def open_trade_ledger(
 | 
			
		|||
    name as defined in the user's ``brokers.toml`` config.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    ldir = path.join(config._config_dir, 'ledgers')
 | 
			
		||||
    if not path.isdir(ldir):
 | 
			
		||||
        os.makedirs(ldir)
 | 
			
		||||
    ldir: Path = config._config_dir / 'ledgers'
 | 
			
		||||
    if not ldir.is_dir():
 | 
			
		||||
        ldir.mkdir()
 | 
			
		||||
 | 
			
		||||
    fname = f'trades_{broker}_{account}.toml'
 | 
			
		||||
    tradesfile = path.join(ldir, fname)
 | 
			
		||||
    tradesfile: Path = ldir / fname
 | 
			
		||||
 | 
			
		||||
    if not path.isfile(tradesfile):
 | 
			
		||||
    if not tradesfile.is_file():
 | 
			
		||||
        log.info(
 | 
			
		||||
            f'Creating new local trades ledger: {tradesfile}'
 | 
			
		||||
        )
 | 
			
		||||
        with open(tradesfile, 'w') as cf:
 | 
			
		||||
            pass  # touch
 | 
			
		||||
    with open(tradesfile, 'rb') as cf:
 | 
			
		||||
        tradesfile.touch()
 | 
			
		||||
 | 
			
		||||
    with tradesfile.open(mode='rb') as cf:
 | 
			
		||||
        start = time.time()
 | 
			
		||||
        ledger = tomli.load(cf)
 | 
			
		||||
        ledger_dict = tomli.load(cf)
 | 
			
		||||
        log.info(f'Ledger load took {time.time() - start}s')
 | 
			
		||||
        cpy = ledger.copy()
 | 
			
		||||
        cpy = ledger_dict.copy()
 | 
			
		||||
 | 
			
		||||
    ledger = TransactionLedger(
 | 
			
		||||
        ledger_dict=cpy,
 | 
			
		||||
        file_path=tradesfile,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        yield cpy
 | 
			
		||||
        yield ledger
 | 
			
		||||
    finally:
 | 
			
		||||
        if cpy != ledger:
 | 
			
		||||
        if ledger.data != ledger_dict:
 | 
			
		||||
 | 
			
		||||
            # 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)
 | 
			
		||||
 | 
			
		||||
            # we write on close the mutated ledger data
 | 
			
		||||
            with open(tradesfile, 'w') as cf:
 | 
			
		||||
                toml.dump(ledger, cf)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Transaction(Struct, frozen=True):
 | 
			
		||||
 | 
			
		||||
    # TODO: unify this with the `MktPair`,
 | 
			
		||||
    # once we have that as a required field,
 | 
			
		||||
    # we don't really need the fqsn any more..
 | 
			
		||||
    fqsn: str
 | 
			
		||||
 | 
			
		||||
    # TODO: drop the Symbol type
 | 
			
		||||
 | 
			
		||||
    # the underlying "transaction system", normally one of a ``MktPair``
 | 
			
		||||
    # (a description of a tradable double auction) or a ledger-recorded
 | 
			
		||||
    # ("ledger" in any sense as long as you can record transfers) of any
 | 
			
		||||
    # sort) ``Asset``.
 | 
			
		||||
    sym: MktPair | Asset | Symbol
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def sys(self) -> Symbol:
 | 
			
		||||
        return self.sym
 | 
			
		||||
 | 
			
		||||
    tid: Union[str, int]  # unique transaction id
 | 
			
		||||
    size: float
 | 
			
		||||
    price: float
 | 
			
		||||
    cost: float  # commisions or other additional costs
 | 
			
		||||
    dt: datetime
 | 
			
		||||
    expiry: datetime | None = None
 | 
			
		||||
 | 
			
		||||
    # remap for back-compat
 | 
			
		||||
    @property
 | 
			
		||||
    def fqme(self) -> str:
 | 
			
		||||
        return self.fqsn
 | 
			
		||||
 | 
			
		||||
    # (optional) key-id defined by the broker-service backend which
 | 
			
		||||
    # ensures the instrument-symbol market key for this record is unique
 | 
			
		||||
    # in the "their backend/system" sense; i.e. this uid for the market
 | 
			
		||||
    # as defined (internally) in some namespace defined by the broker
 | 
			
		||||
    # service.
 | 
			
		||||
    bs_mktid: str | int | None = None
 | 
			
		||||
 | 
			
		||||
    # XXX NOTE: this will come from the `MktPair`
 | 
			
		||||
    # instead of defined here right?
 | 
			
		||||
    # optional fqsn for the source "asset"/money symbol?
 | 
			
		||||
    # from: Optional[str] = None
 | 
			
		||||
            ledger.write_config()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def iter_by_dt(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue