Handle `Position.split_ratio` in state audits

This firstly changes `.audit_sizing()` => `.ensure_state()` and makes it
return `None` as well as only error when split ratio denoted (via
config) positions do not size as expected.

Further refinements,
- add an `.expired()` predicate method
- always return a size of zero from `.calc_size()` on expired assets
- load each `pps.toml` entry's clear tabe into `Transaction`s and use
  `.add_clear()` during from config init.
doin_the_splits
Tyler Goodlet 2022-08-17 08:49:26 -04:00
parent 23ba0e5e69
commit 999ae5a1c6
1 changed files with 82 additions and 38 deletions

View File

@ -175,16 +175,14 @@ class Position(Struct):
s = d.pop('symbol') s = d.pop('symbol')
fqsn = s.front_fqsn() fqsn = s.front_fqsn()
size = d.pop('size')
ppu = d.pop('ppu')
d['size'], d['ppu'] = self.audit_sizing(size, ppu)
if self.expiry is None: if self.expiry is None:
d.pop('expiry', None) d.pop('expiry', None)
elif expiry: elif expiry:
d['expiry'] = str(expiry) d['expiry'] = str(expiry)
toml_clears_list = [] toml_clears_list = []
# reverse sort so latest clears are at top of section?
for tid, data in sorted( for tid, data in sorted(
list(clears.items()), list(clears.items()),
@ -213,39 +211,51 @@ class Position(Struct):
return fqsn, d return fqsn, d
def audit_sizing( def ensure_state(self) -> None:
self,
size: Optional[float] = None,
ppu: Optional[float] = None,
) -> tuple[float, float]:
''' '''
Audit either the `.size` and `.ppu` values or equvialent Audit either the `.size` and `.ppu` local instance vars against
passed in values against the clears table calculations and the clears table calculations and return the calc-ed values if
return the calc-ed values if they differ and log warnings to they differ and log warnings to console.
console.
''' '''
size = size or self.size clears = list(self.clears.values())
ppu = ppu or self.ppu self.first_clear_dt = min(list(entry['dt'] for entry in clears))
last_clear = clears[-1]
csize = self.calc_size() csize = self.calc_size()
cppu = self.calc_ppu() accum = last_clear['accum_size']
if not self.expired():
if (
csize != accum
and csize != round(accum * self.split_ratio or 1)
):
raise ValueError(f'Size mismatch: {csize}')
else:
assert csize == 0, 'Contract is expired but non-zero size?'
if size != csize: if self.size != csize:
log.warning(f'size != calculated size: {size} != {csize}')
size = csize
if ppu != cppu:
log.warning( log.warning(
f'ppu != calculated ppu: {ppu} != {cppu}' 'Position state mismatch:\n'
f'{self.size} => {csize}'
) )
ppu = cppu self.size = csize
self.first_clear_dt = min( cppu = self.calc_ppu()
list(entry['dt'] for entry in self.clears.values()) ppu = last_clear['ppu']
) if (
cppu != ppu
and self.split_ratio is not None
# handle any split info entered (for now) manually by user
and cppu != (ppu / self.split_ratio)
):
raise ValueError(f'PPU mismatch: {cppu}')
return size, ppu if self.ppu != cppu:
log.warning(
'Position state mismatch:\n'
f'{self.ppu} => {cppu}'
)
self.ppu = cppu
def update_from_msg( def update_from_msg(
self, self,
@ -406,8 +416,26 @@ class Position(Struct):
return final_ppu return final_ppu
def expired(self) -> bool:
'''
Predicate which checks if the contract/instrument is past its expiry.
'''
return bool(self.expiry) and self.expiry < now()
def calc_size(self) -> float: def calc_size(self) -> float:
'''
Calculate the unit size of this position in the destination
asset using the clears/trade event table; zero if expired.
'''
size: float = 0 size: float = 0
# time-expired pps (normally derivatives) are "closed"
# and have a zero size.
if self.expired():
return 0
for tid, entry in self.clears.items(): for tid, entry in self.clears.items():
size += entry['size'] size += entry['size']
@ -473,6 +501,9 @@ class Position(Struct):
return clear return clear
def sugest_split(self) -> float:
...
class PpTable(Struct): class PpTable(Struct):
@ -532,7 +563,7 @@ class PpTable(Struct):
# minimize clears tables and update sizing. # minimize clears tables and update sizing.
for bsuid, pp in updated.items(): for bsuid, pp in updated.items():
pp.size, pp.ppu = pp.audit_sizing() pp.ensure_state()
return updated return updated
@ -565,7 +596,7 @@ class PpTable(Struct):
# if bsuid == qqqbsuid: # if bsuid == qqqbsuid:
# breakpoint() # breakpoint()
pp.size, pp.ppu = pp.audit_sizing() pp.ensure_state()
if ( if (
# "net-zero" is a "closed" position # "net-zero" is a "closed" position
@ -605,6 +636,7 @@ class PpTable(Struct):
# keep the minimal amount of clears that make up this # keep the minimal amount of clears that make up this
# position since the last net-zero state. # position since the last net-zero state.
pos.minimize_clears() pos.minimize_clears()
pos.ensure_state()
# serialize to pre-toml form # serialize to pre-toml form
fqsn, asdict = pos.to_pretoml() fqsn, asdict = pos.to_pretoml()
@ -872,11 +904,22 @@ def open_pps(
# convert "clear events table" from the toml config (list of # convert "clear events table" from the toml config (list of
# a dicts) and load it into object form for use in position # a dicts) and load it into object form for use in position
# processing of new clear events. # processing of new clear events.
trans: list[Transaction] = []
for clears_table in clears_list: for clears_table in clears_list:
tid = clears_table.pop('tid') tid = clears_table.pop('tid')
dtstr = clears_table['dt'] dtstr = clears_table['dt']
dt = pendulum.parse(dtstr) dt = pendulum.parse(dtstr)
clears_table['dt'] = dt clears_table['dt'] = dt
trans.append(Transaction(
fqsn=bsuid,
bsuid=bsuid,
tid=tid,
size=clears_table['size'],
price=clears_table['price'],
cost=clears_table['cost'],
dt=dt,
))
clears[tid] = clears_table clears[tid] = clears_table
size = entry['size'] size = entry['size']
@ -896,17 +939,18 @@ def open_pps(
split_ratio=split_ratio, split_ratio=split_ratio,
expiry=expiry, expiry=expiry,
bsuid=entry['bsuid'], 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,
) )
# 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!
for t in trans:
pp.add_clear(t)
# audit entries loaded from toml # audit entries loaded from toml
pp.size, pp.ppu = pp.audit_sizing() pp.ensure_state()
try: try:
yield table yield table