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