Start `piker.pp` module, LIFO pp updates

Start a generic "position related" util mod and bring in the `Position`
type from the allocator , convert it to a `msgspec.Struct` and add
a `.lifo_update()` method. Implement a WIP pp parser from a trades
ledger and use the new lifo method to gather position entries.
lifo_pps_ib
Tyler Goodlet 2022-06-08 11:25:17 -04:00
parent 725909a94c
commit 1eb7e109e6
4 changed files with 185 additions and 47 deletions

View File

@ -23,53 +23,11 @@ from typing import Optional
from bidict import bidict from bidict import bidict
from pydantic import BaseModel, validator from pydantic import BaseModel, validator
from msgspec import Struct
from ..data._source import Symbol from ..data._source import Symbol
from ._messages import BrokerdPosition, Status from ._messages import BrokerdPosition, Status
from ..pp import Position
class Position(BaseModel):
'''
Basic pp (personal position) model with attached fills history.
This type should be IPC wire ready?
'''
symbol: Symbol
# last size and avg entry price
size: float
avg_price: float # TODO: contextual pricing
# ordered record of known constituent trade messages
fills: list[Status] = []
def update_from_msg(
self,
msg: BrokerdPosition,
) -> None:
# XXX: better place to do this?
symbol = self.symbol
lot_size_digits = symbol.lot_size_digits
avg_price, size = (
round(msg['avg_price'], ndigits=symbol.tick_size_digits),
round(msg['size'], ndigits=lot_size_digits),
)
self.avg_price = avg_price
self.size = size
@property
def dsize(self) -> float:
'''
The "dollar" size of the pp, normally in trading (fiat) unit
terms.
'''
return self.avg_price * self.size
_size_units = bidict({ _size_units = bidict({

177
piker/pp.py 100644
View File

@ -0,0 +1,177 @@
# piker: trading gear for hackers
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
Personal/Private position parsing, calculmating, summarizing in a way
that doesn't try to cuk most humans who prefer to not lose their moneys..
(looking at you `ib` and shitzy friends)
'''
from typing import (
Any,
Optional,
Union,
)
from msgspec import Struct
from . import config
from .clearing._messages import BrokerdPosition, Status
from .data._source import Symbol
class Position(Struct):
'''
Basic pp (personal position) model with attached fills history.
This type should be IPC wire ready?
'''
symbol: Symbol
# last size and avg entry price
size: float
avg_price: float # TODO: contextual pricing
# ordered record of known constituent trade messages
fills: list[Status] = []
def update_from_msg(
self,
msg: BrokerdPosition,
) -> None:
# XXX: better place to do this?
symbol = self.symbol
lot_size_digits = symbol.lot_size_digits
avg_price, size = (
round(
msg['avg_price'],
ndigits=symbol.tick_size_digits
),
round(
msg['size'],
ndigits=lot_size_digits
),
)
self.avg_price = avg_price
self.size = size
@property
def dsize(self) -> float:
'''
The "dollar" size of the pp, normally in trading (fiat) unit
terms.
'''
return self.avg_price * self.size
def lifo_update(
self,
size: float,
price: float,
) -> (float, float):
'''
Incremental update using a LIFO-style weighted mean.
'''
# "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.avg_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.avg_price = (
abs(size) * price # weight of current exec
+
self.avg_price * abs(self.size) # weight of previous pp
) / abs(new_size)
self.size = new_size
return new_size, self.avg_price
def parse_pps(
brokername: str,
acctname: str,
ledger: Optional[dict[str, Union[str, float]]] = None,
) -> dict[str, Any]:
pps: dict[str, Position] = {}
if not ledger:
with config.open_trade_ledger(
brokername,
acctname,
) as ledger:
pass # readonly
by_date = ledger[brokername]
for date, by_id in by_date.items():
for tid, record in by_id.items():
# ib specific record parsing
# date, time = record['dateTime']
# conid = record['condid']
# cost = record['cost']
# comms = record['ibCommission']
symbol = record['symbol']
price = record['tradePrice']
# action = record['buySell']
# NOTE: can be -ve on sells
size = float(record['quantity'])
pp = pps.setdefault(
symbol,
Position(
Symbol(key=symbol),
size=0.0,
avg_price=0.0,
)
)
# LOFI style average price calc
pp.lifo_update(size, price)
from pprint import pprint
pprint(pps)
if __name__ == '__main__':
parse_pps('ib', 'algopaper')

View File

@ -19,6 +19,7 @@ Position info and display
""" """
from __future__ import annotations from __future__ import annotations
from copy import copy
from dataclasses import dataclass from dataclasses import dataclass
from functools import partial from functools import partial
from math import floor, copysign from math import floor, copysign
@ -476,7 +477,7 @@ class PositionTracker:
self.alloc = alloc self.alloc = alloc
self.startup_pp = startup_pp self.startup_pp = startup_pp
self.live_pp = startup_pp.copy() self.live_pp = copy(startup_pp)
view = chart.getViewBox() view = chart.getViewBox()

View File

@ -59,7 +59,8 @@ log = get_logger(__name__)
class OrderDialog(BaseModel): class OrderDialog(BaseModel):
'''Trade dialogue meta-data describing the lifetime '''
Trade dialogue meta-data describing the lifetime
of an order submission to ``emsd`` from a chart. of an order submission to ``emsd`` from a chart.
''' '''
@ -87,7 +88,8 @@ def on_level_change_update_next_order_info(
tracker: PositionTracker, tracker: PositionTracker,
) -> None: ) -> None:
'''A callback applied for each level change to the line '''
A callback applied for each level change to the line
which will recompute the order size based on allocator which will recompute the order size based on allocator
settings. this is assigned inside settings. this is assigned inside
``OrderMode.line_from_order()`` ``OrderMode.line_from_order()``