Fix size quantization and closed position popping..
Turns out we actually had further pp entry bugs due to *not quantizing* the size inside `.minimize_clears()` method calcs; fix that using `Position.sys.mkt.quantize()` as is done in `Position.calc_size()`. Fix `PpTable.write_config()` to drop from the TOML config any `closed: dict[str, Position]` entries delivered by `.dump_active()`. Add a more detailed doc string for our position type and a little todo for the `.bep` B)rekt_pps
parent
bba1ee43ff
commit
f106472bcb
|
@ -63,19 +63,42 @@ log = get_logger(__name__)
|
||||||
|
|
||||||
class Position(Struct):
|
class Position(Struct):
|
||||||
'''
|
'''
|
||||||
Basic pp (personal/piker position) model with attached clearing
|
An asset "position" model with attached clearing transaction history.
|
||||||
transaction history.
|
|
||||||
|
A financial "position" in `piker` terms is a summary of accounting
|
||||||
|
metrics computed from a transaction ledger; generally it describes
|
||||||
|
some acumulative "size" and "average price" from the summarized
|
||||||
|
underlying transaction set.
|
||||||
|
|
||||||
|
In piker we focus on the `.ppu` (price per unit) and the `.bep`
|
||||||
|
(break even price) including all transaction entries and exits since
|
||||||
|
the last "net-zero" size of the destination asset's holding.
|
||||||
|
|
||||||
|
This interface serves as an object API for computing and tracking
|
||||||
|
positions as well as supports serialization for storage in the local
|
||||||
|
file system (in TOML) and to interchange as a msg over IPC.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
symbol: Symbol | MktPair
|
symbol: Symbol | MktPair
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mkt(self) -> MktPair:
|
||||||
|
return self.symbol
|
||||||
|
|
||||||
# can be +ve or -ve for long/short
|
# can be +ve or -ve for long/short
|
||||||
size: float
|
size: float
|
||||||
|
|
||||||
# "breakeven price" above or below which pnl moves above and below
|
# "price-per-unit price" above or below which pnl moves above and
|
||||||
# zero for the entirety of the current "trade state".
|
# below zero for the entirety of the current "trade state". The ppu
|
||||||
|
# is only modified on "increases of" the absolute size of a position
|
||||||
|
# in one of a long/short "direction" (i.e. abs(.size_i) > 0 after
|
||||||
|
# the next transaction given .size was > 0 before that tx, and vice
|
||||||
|
# versa for -ve sized positions).
|
||||||
ppu: float
|
ppu: float
|
||||||
|
|
||||||
|
# TODO: break-even-price support!
|
||||||
|
# bep: float
|
||||||
|
|
||||||
# unique "backend system market id"
|
# unique "backend system market id"
|
||||||
bs_mktid: str
|
bs_mktid: str
|
||||||
|
|
||||||
|
@ -164,7 +187,8 @@ class Position(Struct):
|
||||||
inline_table = toml.TomlDecoder().get_empty_inline_table()
|
inline_table = toml.TomlDecoder().get_empty_inline_table()
|
||||||
|
|
||||||
# serialize datetime to parsable `str`
|
# serialize datetime to parsable `str`
|
||||||
inline_table['dt'] = str(data['dt'])
|
dtstr = inline_table['dt'] = str(data['dt'])
|
||||||
|
assert 'Datetime' not in dtstr
|
||||||
|
|
||||||
# insert optional clear fields in column order
|
# insert optional clear fields in column order
|
||||||
for k in ['ppu', 'accum_size']:
|
for k in ['ppu', 'accum_size']:
|
||||||
|
@ -191,7 +215,9 @@ class Position(Struct):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
clears = list(self.clears.values())
|
clears = list(self.clears.values())
|
||||||
self.first_clear_dt = min(list(entry['dt'] for entry in clears))
|
self.first_clear_dt = min(
|
||||||
|
list(entry['dt'] for entry in clears)
|
||||||
|
)
|
||||||
last_clear = clears[-1]
|
last_clear = clears[-1]
|
||||||
|
|
||||||
csize = self.calc_size()
|
csize = self.calc_size()
|
||||||
|
@ -413,15 +439,21 @@ class Position(Struct):
|
||||||
asset using the clears/trade event table; zero if expired.
|
asset using the clears/trade event table; zero if expired.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
size: float = 0
|
size: float = 0.
|
||||||
|
|
||||||
# time-expired pps (normally derivatives) are "closed"
|
# time-expired pps (normally derivatives) are "closed"
|
||||||
# and have a zero size.
|
# and have a zero size.
|
||||||
if self.expired():
|
if self.expired():
|
||||||
return 0
|
return 0.
|
||||||
|
|
||||||
for tid, entry in self.clears.items():
|
for tid, entry in self.clears.items():
|
||||||
size += entry['size']
|
size += entry['size']
|
||||||
|
# XXX: do we need it every step?
|
||||||
|
# no right since rounding is an LT?
|
||||||
|
# size = self.mkt.quantize(
|
||||||
|
# size + entry['size'],
|
||||||
|
# quantity_type='size',
|
||||||
|
# )
|
||||||
|
|
||||||
if self.split_ratio is not None:
|
if self.split_ratio is not None:
|
||||||
size = round(size * self.split_ratio)
|
size = round(size * self.split_ratio)
|
||||||
|
@ -450,7 +482,9 @@ class Position(Struct):
|
||||||
# scan for the last "net zero" position by iterating
|
# scan for the last "net zero" position by iterating
|
||||||
# transactions until the next net-zero size, rinse, repeat.
|
# transactions until the next net-zero size, rinse, repeat.
|
||||||
for tid, clear in self.clears.items():
|
for tid, clear in self.clears.items():
|
||||||
size += clear['size']
|
size = float(
|
||||||
|
self.mkt.quantize(size + clear['size'])
|
||||||
|
)
|
||||||
clears_since_zero.append((tid, clear))
|
clears_since_zero.append((tid, clear))
|
||||||
|
|
||||||
if size == 0:
|
if size == 0:
|
||||||
|
@ -504,8 +538,6 @@ class PpTable(Struct):
|
||||||
trans: dict[str, Transaction],
|
trans: dict[str, Transaction],
|
||||||
cost_scalar: float = 2,
|
cost_scalar: float = 2,
|
||||||
|
|
||||||
force_mkt: MktPair | None = None,
|
|
||||||
|
|
||||||
) -> dict[str, Position]:
|
) -> dict[str, Position]:
|
||||||
|
|
||||||
pps = self.pps
|
pps = self.pps
|
||||||
|
@ -523,15 +555,7 @@ class PpTable(Struct):
|
||||||
|
|
||||||
# template the mkt-info presuming a legacy market ticks
|
# template the mkt-info presuming a legacy market ticks
|
||||||
# if no info exists in the transactions..
|
# if no info exists in the transactions..
|
||||||
mkt: MktPair | Symbol | None = force_mkt or t.sys
|
mkt: MktPair = t.sys
|
||||||
if not mkt:
|
|
||||||
mkt = MktPair.from_fqme(
|
|
||||||
fqme,
|
|
||||||
price_tick='0.01',
|
|
||||||
size_tick='0.0',
|
|
||||||
bs_mktid=bs_mktid,
|
|
||||||
)
|
|
||||||
|
|
||||||
pp = pps.get(bs_mktid)
|
pp = pps.get(bs_mktid)
|
||||||
if not pp:
|
if not pp:
|
||||||
# if no existing pp, allocate fresh one.
|
# if no existing pp, allocate fresh one.
|
||||||
|
@ -633,14 +657,18 @@ class PpTable(Struct):
|
||||||
|
|
||||||
def to_toml(
|
def to_toml(
|
||||||
self,
|
self,
|
||||||
|
active: dict[str, Position] | None = None,
|
||||||
|
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
|
||||||
active, closed = self.dump_active()
|
if active is None:
|
||||||
|
active, _ = self.dump_active()
|
||||||
|
|
||||||
# ONLY dict-serialize all active positions; those that are
|
# ONLY dict-serialize all active positions; those that are
|
||||||
# closed we don't store in the ``pps.toml``.
|
# closed we don't store in the ``pps.toml``.
|
||||||
to_toml_dict = {}
|
to_toml_dict = {}
|
||||||
|
|
||||||
|
pos: Position
|
||||||
for bs_mktid, pos in active.items():
|
for bs_mktid, pos in active.items():
|
||||||
|
|
||||||
# keep the minimal amount of clears that make up this
|
# keep the minimal amount of clears that make up this
|
||||||
|
@ -650,6 +678,8 @@ class PpTable(Struct):
|
||||||
|
|
||||||
# serialize to pre-toml form
|
# serialize to pre-toml form
|
||||||
fqme, asdict = pos.to_pretoml()
|
fqme, asdict = pos.to_pretoml()
|
||||||
|
|
||||||
|
# assert 'Datetime' not in asdict['dt']
|
||||||
log.info(f'Updating active pp: {fqme}')
|
log.info(f'Updating active pp: {fqme}')
|
||||||
|
|
||||||
# XXX: ugh, it's cuz we push the section under
|
# XXX: ugh, it's cuz we push the section under
|
||||||
|
@ -667,7 +697,9 @@ class PpTable(Struct):
|
||||||
# TODO: show diff output?
|
# TODO: show diff output?
|
||||||
# https://stackoverflow.com/questions/12956957/print-diff-of-python-dictionaries
|
# https://stackoverflow.com/questions/12956957/print-diff-of-python-dictionaries
|
||||||
# active, closed_pp_objs = table.dump_active()
|
# active, closed_pp_objs = table.dump_active()
|
||||||
pp_entries = self.to_toml()
|
|
||||||
|
active, closed = self.dump_active()
|
||||||
|
pp_entries = self.to_toml(active=active)
|
||||||
if pp_entries:
|
if pp_entries:
|
||||||
log.info(
|
log.info(
|
||||||
f'Updating positions in ``{self.conf_path}``:\n'
|
f'Updating positions in ``{self.conf_path}``:\n'
|
||||||
|
@ -688,6 +720,12 @@ class PpTable(Struct):
|
||||||
|
|
||||||
self.conf.update(pp_entries)
|
self.conf.update(pp_entries)
|
||||||
|
|
||||||
|
# drop any entries that are computed as net-zero
|
||||||
|
# we don't care about storing in the pps file.
|
||||||
|
if closed:
|
||||||
|
for fqme in closed:
|
||||||
|
self.conf.pop(fqme, None)
|
||||||
|
|
||||||
# if there are no active position entries according
|
# if there are no active position entries according
|
||||||
# to the toml dump output above, then clear the config
|
# to the toml dump output above, then clear the config
|
||||||
# file of all entries.
|
# file of all entries.
|
||||||
|
|
Loading…
Reference in New Issue