From 1eb7e109e663ce43d50796048be50cb284ff8984 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 8 Jun 2022 11:25:17 -0400 Subject: [PATCH] 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. --- piker/clearing/_allocate.py | 46 +--------- piker/pp.py | 177 ++++++++++++++++++++++++++++++++++++ piker/ui/_position.py | 3 +- piker/ui/order_mode.py | 6 +- 4 files changed, 185 insertions(+), 47 deletions(-) create mode 100644 piker/pp.py diff --git a/piker/clearing/_allocate.py b/piker/clearing/_allocate.py index 71d7d9a0..f14728a1 100644 --- a/piker/clearing/_allocate.py +++ b/piker/clearing/_allocate.py @@ -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({ diff --git a/piker/pp.py b/piker/pp.py new file mode 100644 index 00000000..cdbcd0d5 --- /dev/null +++ b/piker/pp.py @@ -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 . +''' +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') diff --git a/piker/ui/_position.py b/piker/ui/_position.py index 0abb6459..844869b0 100644 --- a/piker/ui/_position.py +++ b/piker/ui/_position.py @@ -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() diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index a86fe816..5ee53bd4 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -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()``