Move `open_trade_ledger()` to pp mod, add `get_pps()`

lifo_pps_ib
Tyler Goodlet 2022-06-13 14:11:37 -04:00
parent de77c7d209
commit 5d774bef90
2 changed files with 106 additions and 53 deletions

View File

@ -18,7 +18,6 @@
Broker configuration mgmt. Broker configuration mgmt.
""" """
from contextlib import contextmanager as cm
import platform import platform
import sys import sys
import os import os
@ -173,51 +172,6 @@ def get_conf_path(
) )
@cm
def open_trade_ledger(
broker: str,
account: str,
) -> str:
'''
Indempotently create and read in a trade log file from the
``<configuration_dir>/ledgers/`` directory.
Files are named per broker account of the form
``<brokername>_<accountname>.toml``. The ``accountname`` here is the
name as defined in the user's ``brokers.toml`` config.
'''
ldir = path.join(_config_dir, 'ledgers')
if not path.isdir(ldir):
os.makedirs(ldir)
fname = f'trades_{broker}_{account}.toml'
tradesfile = path.join(ldir, fname)
if not path.isfile(tradesfile):
log.info(
f'Creating new local trades ledger: {tradesfile}'
)
with open(tradesfile, 'w') as cf:
pass # touch
try:
with open(tradesfile, 'r') as cf:
ledger = toml.load(tradesfile)
cpy = ledger.copy()
yield cpy
finally:
if cpy != ledger:
# TODO: show diff output?
# https://stackoverflow.com/questions/12956957/print-diff-of-python-dictionaries
print(f'Updating ledger for {tradesfile}:\n')
ledger.update(cpy)
# we write on close the mutated ledger data
with open(tradesfile, 'w') as cf:
return toml.dump(ledger, cf)
def repodir(): def repodir():
''' '''
Return the abspath to the repo directory. Return the abspath to the repo directory.

View File

@ -20,17 +20,70 @@ that doesn't try to cuk most humans who prefer to not lose their moneys..
(looking at you `ib` and shitzy friends) (looking at you `ib` and shitzy friends)
''' '''
from contextlib import contextmanager as cm
import os
from os import path
from typing import ( from typing import (
Any,
Optional, Optional,
Union, Union,
) )
from msgspec import Struct from msgspec import Struct
import toml
from . import config from . import config
from .brokers import get_brokermod
from .clearing._messages import BrokerdPosition, Status from .clearing._messages import BrokerdPosition, Status
from .data._source import Symbol from .data._source import Symbol
from .brokers import get_brokermod from .log import get_logger
log = get_logger(__name__)
@cm
def open_trade_ledger(
broker: str,
account: str,
) -> str:
'''
Indempotently create and read in a trade log file from the
``<configuration_dir>/ledgers/`` directory.
Files are named per broker account of the form
``<brokername>_<accountname>.toml``. The ``accountname`` here is the
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)
fname = f'trades_{broker}_{account}.toml'
tradesfile = path.join(ldir, fname)
if not path.isfile(tradesfile):
log.info(
f'Creating new local trades ledger: {tradesfile}'
)
with open(tradesfile, 'w') as cf:
pass # touch
try:
with open(tradesfile, 'r') as cf:
ledger = toml.load(tradesfile)
cpy = ledger.copy()
yield cpy
finally:
if cpy != ledger:
# TODO: show diff output?
# https://stackoverflow.com/questions/12956957/print-diff-of-python-dictionaries
print(f'Updating ledger for {tradesfile}:\n')
ledger.update(cpy)
# we write on close the mutated ledger data
with open(tradesfile, 'w') as cf:
return toml.dump(ledger, cf)
class TradeRecord(Struct): class TradeRecord(Struct):
@ -40,6 +93,8 @@ class TradeRecord(Struct):
price: float price: float
cost: float # commisions or other additional costs cost: float # commisions or other additional costs
# dt: datetime
# optional key normally derived from the broker # optional key normally derived from the broker
# backend which ensures the instrument-symbol this record # backend which ensures the instrument-symbol this record
# is for is truly unique. # is for is truly unique.
@ -106,6 +161,16 @@ class Position(Struct):
size: float, size: float,
price: float, price: float,
# TODO: idea: "real LIFO" dynamic positioning.
# - when a trade takes place where the pnl for
# the (set of) trade(s) is below the breakeven price
# it may be that the trader took a +ve pnl on a short(er)
# term trade in the same account.
# - in this case we could recalc the be price to
# be reverted back to it's prior value before the nearest term
# trade was opened.?
dynamic_breakeven_price: bool = False,
) -> (float, float): ) -> (float, float):
''' '''
Incremental update using a LIFO-style weighted mean. Incremental update using a LIFO-style weighted mean.
@ -191,7 +256,10 @@ def update_pps(
def _split_active( def _split_active(
pps: dict[str, Position], pps: dict[str, Position],
) -> tuple[dict, dict]: ) -> tuple[
dict[str, Any],
dict[str, Any],
]:
''' '''
Split pps into those that are "active" (non-zero size) and "closed" Split pps into those that are "active" (non-zero size) and "closed"
(zero size) and return in 2 dicts. (zero size) and return in 2 dicts.
@ -229,7 +297,7 @@ def load_pps_from_ledger(
and deliver the two dict-sets of the active and closed pps. and deliver the two dict-sets of the active and closed pps.
''' '''
with config.open_trade_ledger( with open_trade_ledger(
brokername, brokername,
acctname, acctname,
) as ledger: ) as ledger:
@ -242,18 +310,34 @@ def load_pps_from_ledger(
return _split_active(pps) return _split_active(pps)
def get_pps(
brokername: str,
) -> dict[str, Any]:
'''
Read out broker-specific position entries from
incremental update file: ``pps.toml``.
'''
conf, path = config.load('pps')
return conf.setdefault(brokername, {})
def update_pps_conf( def update_pps_conf(
brokername: str, brokername: str,
acctid: str, acctid: str,
trade_records: Optional[list[TradeRecord]] = None, trade_records: Optional[list[TradeRecord]] = None,
):
) -> dict[str, Position]:
conf, path = config.load('pps') conf, path = config.load('pps')
brokersection = conf.setdefault(brokername, {}) brokersection = conf.setdefault(brokername, {})
entries = brokersection.setdefault(acctid, {}) entries = brokersection.setdefault(acctid, {})
if not entries: if not entries:
# no pps entry yet for this broker/account # no pps entry yet for this broker/account so parse
# any available ledgers to build a pps state.
active, closed = load_pps_from_ledger( active, closed = load_pps_from_ledger(
brokername, brokername,
acctid, acctid,
@ -286,6 +370,9 @@ def update_pps_conf(
) )
active, closed = _split_active(pps) active, closed = _split_active(pps)
else:
raise ValueError('wut wut')
for fqsn in closed: for fqsn in closed:
print(f'removing closed pp: {fqsn}') print(f'removing closed pp: {fqsn}')
entries.pop(fqsn, None) entries.pop(fqsn, None)
@ -295,11 +382,16 @@ def update_pps_conf(
# normalize to a simpler flat dict format # normalize to a simpler flat dict format
_ = pp_dict.pop('symbol') _ = pp_dict.pop('symbol')
entries[fqsn.rstrip(f'.{brokername}')] = pp_dict
# XXX: ugh, it's cuz we push the section under
# the broker name.. maybe we need to rethink this?
brokerless_key = fqsn.rstrip(f'.{brokername}')
entries[brokerless_key] = pp_dict
config.write( config.write(
conf, conf,
'pps', 'pps',
# TODO: make nested tables and/or inline tables work? # TODO: make nested tables and/or inline tables work?
# encoder=config.toml.Encoder(preserve=True), # encoder=config.toml.Encoder(preserve=True),
) )
@ -308,4 +400,11 @@ def update_pps_conf(
if __name__ == '__main__': if __name__ == '__main__':
update_pps_conf('ib', 'algopaper') import sys
args = sys.argv
assert len(args) > 1, 'Specifiy account(s) from `brokers.toml`'
args = args[1:]
for acctid in args:
broker, name = acctid.split('.')
update_pps_conf(broker, name)