Drop `.lifo_upate()` add `.audit_sizing()`

Use the new `.calc_[be_price/size]()` methods when serializing to and
from the `pps.toml` format and add an audit method which will warn about
mismatched values and assign the clears table calculated values pre-write.

Drop the `.lifo_update()` method and instead allow both
`.size`/`.be_price` properties to exist (for non-ledger related uses of
`Position`) alongside the new calc methods and only get fussy about
*what* the properties are set to in the case of ledger audits.

Also changes `Position.update()` -> `.add_clear()`.
basic_pp_audit
Tyler Goodlet 2022-07-25 11:57:57 -04:00
parent 927bbc7258
commit 958e542f7d
1 changed files with 72 additions and 88 deletions

View File

@ -160,6 +160,10 @@ class Position(Struct):
clears = d.pop('clears') clears = d.pop('clears')
expiry = d.pop('expiry') expiry = d.pop('expiry')
size = d.pop('size')
be_price = d.pop('be_price')
d['size'], d['be_price'] = self.audit_sizing(size, be_price)
if expiry: if expiry:
d['expiry'] = str(expiry) d['expiry'] = str(expiry)
@ -178,6 +182,36 @@ class Position(Struct):
return d return d
def audit_sizing(
self,
size: Optional[float] = None,
be_price: Optional[float] = None,
) -> tuple[float, float]:
'''
Audit either the `.size` and `.be_price` values or equvialent
passed in values against the clears table calculations and
return the calc-ed values if they differ and log warnings to
console.
'''
size = size or self.size
be_price = be_price or self.be_price
csize = self.calc_size()
cbe_price = self.calc_be_price()
if size != csize:
log.warning(f'size != calculated size: {size} != {csize}')
size = csize
if be_price != cbe_price:
log.warning(
f'be_price != calculated be_price: {be_price} != {cbe_price}'
)
be_price = cbe_price
return size, be_price
def update_from_msg( def update_from_msg(
self, self,
msg: BrokerdPosition, msg: BrokerdPosition,
@ -211,7 +245,7 @@ class Position(Struct):
''' '''
return self.be_price * self.size return self.be_price * self.size
def update( def add_clear(
self, self,
t: Transaction, t: Transaction,
@ -223,12 +257,6 @@ class Position(Struct):
'dt': str(t.dt), 'dt': str(t.dt),
} }
def lifo_update(
self,
size: float,
price: float,
cost: float = 0,
# TODO: idea: "real LIFO" dynamic positioning. # TODO: idea: "real LIFO" dynamic positioning.
# - when a trade takes place where the pnl for # - when a trade takes place where the pnl for
# the (set of) trade(s) is below the breakeven price # the (set of) trade(s) is below the breakeven price
@ -237,47 +265,18 @@ class Position(Struct):
# - in this case we could recalc the be price to # - in this case we could recalc the be price to
# be reverted back to it's prior value before the nearest term # be reverted back to it's prior value before the nearest term
# trade was opened.? # trade was opened.?
# dynamic_breakeven_price: bool = False, # def lifo_price() -> float:
# ...
) -> (float, float): def calc_be_price(
''' self,
Incremental update using a LIFO-style weighted mean. # include transaction cost in breakeven price
# and presume the worst case of the same cost
# to exit this transaction (even though in reality
# it will be dynamic based on exit stratetgy).
cost_scalar: float = 2,
''' ) -> float:
# "avg position price" calcs
# TODO: eventually it'd be nice to have a small set of routines
# to do this stuff from a sequence of cleared orders to enable
# so called "contextual positions".
new_size = self.size + size
# old size minus the new size gives us size diff with
# +ve -> increase in pp size
# -ve -> decrease in pp size
size_diff = abs(new_size) - abs(self.size)
if new_size == 0:
self.be_price = 0
elif size_diff > 0:
# XXX: LOFI incremental update:
# only update the "average price" when
# the size increases not when it decreases (i.e. the
# position is being made smaller)
self.be_price = (
# weight of current exec = (size * price) + cost
(abs(size) * price)
+
(copysign(1, new_size) * cost) # transaction cost
+
# weight of existing be price
self.be_price * abs(self.size) # weight of previous pp
) / abs(new_size) # normalized by the new size: weighted mean.
self.size = new_size
return new_size, self.be_price
def calc_be_price(self) -> float:
size: float = 0 size: float = 0
cb_tot_size: float = 0 cb_tot_size: float = 0
@ -299,16 +298,22 @@ class Position(Struct):
cb_tot_size = 0 cb_tot_size = 0
be_price = 0 be_price = 0
# XXX: LIFO breakeven price update. only an increaze in size
# of the position contributes the breakeven price,
# a decrease does not (i.e. the position is being made
# smaller).
elif size_diff > 0: elif size_diff > 0:
# only an increaze in size of the position contributes
# the breakeven price, a decrease does not.
cost_basis += ( cost_basis += (
# weighted price per unit of # weighted price per unit of
clear_price * abs(clear_size) clear_price * abs(clear_size)
+ +
# transaction cost # transaction cost
(copysign(1, new_size) * entry['cost'] * 2) (copysign(1, new_size)
*
cost_scalar
*
entry['cost'])
) )
cb_tot_size += abs(clear_size) cb_tot_size += abs(clear_size)
be_price = cost_basis / cb_tot_size be_price = cost_basis / cb_tot_size
@ -404,21 +409,8 @@ class PpTable(Struct):
# "double count" these in pp calculations. # "double count" these in pp calculations.
continue continue
# lifo style "breakeven" price calc
pp.lifo_update(
r.size,
r.price,
# include transaction cost in breakeven price
# and presume the worst case of the same cost
# to exit this transaction (even though in reality
# it will be dynamic based on exit stratetgy).
cost=cost_scalar*r.cost,
)
# track clearing data # track clearing data
pp.update(r) pp.add_clear(r)
updated[r.bsuid] = pp updated[r.bsuid] = pp
return updated return updated
@ -454,11 +446,13 @@ class PpTable(Struct):
# if bsuid == qqqbsuid: # if bsuid == qqqbsuid:
# breakpoint() # breakpoint()
size, be_price = pp.audit_sizing()
pp.minimize_clears() pp.minimize_clears()
if ( if (
# "net-zero" is a "closed" position # "net-zero" is a "closed" position
pp.size == 0 size == 0
# time-expired pps (normally derivatives) are "closed" # time-expired pps (normally derivatives) are "closed"
or (pp.expiry and pp.expiry < now()) or (pp.expiry and pp.expiry < now())
@ -789,29 +783,16 @@ def open_pps(
clears[tid] = clears_table clears[tid] = clears_table
size = entry['size'] size = entry['size']
be_price = entry['be_price']
# TODO: an audit system for existing pps entries?
# if not len(clears) == abs(size):
# pp_objs = load_pps_from_ledger(
# brokername,
# acctid,
# filter_by=reload_records,
# )
# reason = 'size <-> len(clears) mismatch'
# raise ValueError(
# '`pps.toml` entry is invalid:\n'
# f'{fqsn}\n'
# f'{pformat(entry)}'
# )
expiry = entry.get('expiry') expiry = entry.get('expiry')
if expiry: if expiry:
expiry = pendulum.parse(expiry) expiry = pendulum.parse(expiry)
pp_objs[bsuid] = Position( pp = pp_objs[bsuid] = Position(
Symbol.from_fqsn(fqsn, info={}), Symbol.from_fqsn(fqsn, info={}),
size=size, size=size,
be_price=entry['be_price'], be_price=be_price,
expiry=expiry, expiry=expiry,
bsuid=entry['bsuid'], bsuid=entry['bsuid'],
@ -823,6 +804,9 @@ def open_pps(
clears=clears, clears=clears,
) )
# audit entries loaded from toml
pp.size, pp.be_price = pp.audit_sizing()
try: try:
yield table yield table
finally: finally: