Support pp expiries, datetimes on transactions

Since some positions obviously expire and thus shouldn't continually
exist inside a `pps.toml` add naive support for tracking and discarding
expired contracts:
- add `Transaction.expiry: Optional[pendulum.datetime]`.
- add `Position.expiry: Optional[pendulum.datetime]` which can be parsed
  from a transaction ledger.
- only write pps with a non-none expiry to the `pps.toml`
- change `Position.avg_price` -> `.be_price` (be is "breakeven")
  since it's a much less ambiguous name.
- change `load_pps_from_legder()` to *not* call `dump_active()` since
  for the only use case it ends up getting called later anyway.
lifo_pps_ib
Tyler Goodlet 2022-06-18 15:30:52 -04:00
parent 21153a0e1e
commit ff74f4302a
1 changed files with 71 additions and 40 deletions

View File

@ -31,6 +31,8 @@ from typing import (
) )
from msgspec import Struct from msgspec import Struct
import pendulum
from pendulum import datetime, now
import toml import toml
from . import config from . import config
@ -93,8 +95,8 @@ class Transaction(Struct):
size: float size: float
price: float price: float
cost: float # commisions or other additional costs cost: float # commisions or other additional costs
dt: datetime
# dt: datetime expiry: Optional[datetime] = None
# 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
@ -110,9 +112,14 @@ class Position(Struct):
''' '''
symbol: Symbol symbol: Symbol
# last size and avg entry price # can be +ve or -ve for long/short
size: float size: float
avg_price: float # TODO: contextual pricing
# "breakeven price" above or below which pnl moves above and below
# zero for the entirety of the current "trade state".
be_price: float
# unique backend symbol id
bsuid: str bsuid: str
# ordered record of known constituent trade messages # ordered record of known constituent trade messages
@ -121,6 +128,8 @@ class Position(Struct):
float, # cost float, # cost
] = {} ] = {}
expiry: Optional[datetime] = None
def to_dict(self) -> dict: def to_dict(self) -> dict:
return { return {
f: getattr(self, f) f: getattr(self, f)
@ -130,12 +139,16 @@ class Position(Struct):
def to_pretoml(self) -> dict: def to_pretoml(self) -> dict:
d = self.to_dict() d = self.to_dict()
clears = d.pop('clears') clears = d.pop('clears')
expiry = d.pop('expiry')
# if not expiry is None:
# breakpoint()
if expiry:
d['expiry'] = str(expiry)
# clears_list = [] # clears_list = []
inline_table = toml.TomlDecoder().get_empty_inline_table() inline_table = toml.TomlDecoder().get_empty_inline_table()
for tid, data in clears.items(): for tid, data in clears.items():
inline_table[tid] = data inline_table[f'{tid}'] = data
# clears_list.append(inline_table) # clears_list.append(inline_table)
@ -153,7 +166,7 @@ class Position(Struct):
symbol = self.symbol symbol = self.symbol
lot_size_digits = symbol.lot_size_digits lot_size_digits = symbol.lot_size_digits
avg_price, size = ( be_price, size = (
round( round(
msg['avg_price'], msg['avg_price'],
ndigits=symbol.tick_size_digits ndigits=symbol.tick_size_digits
@ -164,7 +177,7 @@ class Position(Struct):
), ),
) )
self.avg_price = avg_price self.be_price = be_price
self.size = size self.size = size
@property @property
@ -174,7 +187,7 @@ class Position(Struct):
terms. terms.
''' '''
return self.avg_price * self.size return self.be_price * self.size
def lifo_update( def lifo_update(
self, self,
@ -209,24 +222,24 @@ class Position(Struct):
size_diff = abs(new_size) - abs(self.size) size_diff = abs(new_size) - abs(self.size)
if new_size == 0: if new_size == 0:
self.avg_price = 0 self.be_price = 0
elif size_diff > 0: elif size_diff > 0:
# XXX: LOFI incremental update: # XXX: LOFI incremental update:
# only update the "average price" when # only update the "average price" when
# the size increases not when it decreases (i.e. the # the size increases not when it decreases (i.e. the
# position is being made smaller) # position is being made smaller)
self.avg_price = ( self.be_price = (
abs(size) * price # weight of current exec abs(size) * price # weight of current exec
+ +
cost # transaction cost cost # transaction cost
+ +
self.avg_price * abs(self.size) # weight of previous pp self.be_price * abs(self.size) # weight of previous pp
) / abs(new_size) ) / abs(new_size)
self.size = new_size self.size = new_size
return new_size, self.avg_price return new_size, self.be_price
def update_pps( def update_pps(
@ -253,10 +266,12 @@ def update_pps(
info={}, info={},
), ),
size=0.0, size=0.0,
avg_price=0.0, be_price=0.0,
bsuid=r.bsuid, bsuid=r.bsuid,
expiry=r.expiry,
) )
) )
# don't do updates for ledger records we already have # don't do updates for ledger records we already have
# included in the current pps state. # included in the current pps state.
if r.tid in pp.clears: if r.tid in pp.clears:
@ -307,8 +322,18 @@ def dump_active(
closed = {} closed = {}
for k, pp in pps.items(): for k, pp in pps.items():
asdict = pp.to_pretoml() asdict = pp.to_pretoml()
if pp.size == 0:
if pp.expiry is None:
asdict.pop('expiry', None)
if (
pp.size == 0
# drop time-expired positions (normally derivatives)
or (pp.expiry and pp.expiry < now())
):
closed[k] = asdict closed[k] = asdict
else: else:
active[k] = asdict active[k] = asdict
@ -321,7 +346,7 @@ def load_pps_from_ledger(
brokername: str, brokername: str,
acctname: str, acctname: str,
) -> tuple[dict, dict]: ) -> dict[str, Position]:
''' '''
Open a ledger file by broker name and account and read in and Open a ledger file by broker name and account and read in and
process any trade records into our normalized ``Transaction`` process any trade records into our normalized ``Transaction``
@ -341,8 +366,7 @@ def load_pps_from_ledger(
brokermod = get_brokermod(brokername) brokermod = get_brokermod(brokername)
records = brokermod.norm_trade_records(ledger) records = brokermod.norm_trade_records(ledger)
pps = update_pps(records) return update_pps(records)
return dump_active(pps)
def get_pps( def get_pps(
@ -509,7 +533,7 @@ def update_pps_conf(
if not pps: if not pps:
# no pps entry yet for this broker/account so parse # no pps entry yet for this broker/account so parse
# any available ledgers to build a pps state. # any available ledgers to build a pps state.
pps, closed = load_pps_from_ledger( pp_objs = load_pps_from_ledger(
brokername, brokername,
acctid, acctid,
) )
@ -518,30 +542,37 @@ def update_pps_conf(
f'No trade history could be loaded for {brokername}:{acctid}' f'No trade history could be loaded for {brokername}:{acctid}'
) )
# unmarshal/load ``pps.toml`` config entries into object form. else:
pp_objs = {} # unmarshal/load ``pps.toml`` config entries into object form.
for fqsn, entry in pps.items(): pp_objs = {}
for fqsn, entry in pps.items():
# convert clears sub-tables (only in this form # convert clears sub-tables (only in this form
# for toml re-presentation) back into a master table. # for toml re-presentation) back into a master table.
clears = entry['clears'] clears = entry['clears']
# clears = {} expiry = entry.get('expiry')
# for table in entry['clears']: if expiry:
# clears.update(table) expiry = pendulum.parse(expiry)
pp_objs[fqsn] = Position( # clears = {}
Symbol.from_fqsn(fqsn, info={}), # for k, v in clears.items():
size=entry['size'], # print((k, v))
avg_price=entry['avg_price'], # clears.update(table)
bsuid=entry['bsuid'],
# XXX: super critical, we need to be sure to include pp_objs[fqsn] = Position(
# all pps.toml clears to avoid reusing clears that were Symbol.from_fqsn(fqsn, info={}),
# already included in the current incremental update size=entry['size'],
# state, since today's records may have already been be_price=entry['be_price'],
# processed! expiry=expiry,
clears=clears, bsuid=entry['bsuid'],
)
# XXX: super critical, we need to be sure to include
# all pps.toml clears to avoid reusing clears that were
# already included in the current incremental update
# state, since today's records may have already been
# processed!
clears=clears,
)
# update all pp objects from any (new) trade records which # update all pp objects from any (new) trade records which
# were passed in (aka incremental update case). # were passed in (aka incremental update case).