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 pydantic import BaseModel, validator
from msgspec import Struct
from ..data._source import Symbol
from ._messages import BrokerdPosition, Status
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
from ..pp import Position
_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 copy import copy
from dataclasses import dataclass
from functools import partial
from math import floor, copysign
@ -476,7 +477,7 @@ class PositionTracker:
self.alloc = alloc
self.startup_pp = startup_pp
self.live_pp = startup_pp.copy()
self.live_pp = copy(startup_pp)
view = chart.getViewBox()

View File

@ -59,7 +59,8 @@ log = get_logger(__name__)
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.
'''
@ -87,7 +88,8 @@ def on_level_change_update_next_order_info(
tracker: PositionTracker,
) -> 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
settings. this is assigned inside
``OrderMode.line_from_order()``