Fill out allocator calcs for $size and #units, draft pp ui tracking

fsp_feeds
Tyler Goodlet 2021-08-18 08:55:42 -04:00
parent 17fbe6a6ab
commit 07b20a5e68
1 changed files with 166 additions and 29 deletions

View File

@ -19,10 +19,10 @@ Position info and display
""" """
from __future__ import annotations from __future__ import annotations
import enum from enum import Enum
from functools import partial from functools import partial
from math import floor from math import floor
from pprint import pprint # from pprint import pprint
import sys import sys
from typing import Optional from typing import Optional
@ -42,6 +42,11 @@ from ..data._source import Symbol
from ._label import Label from ._label import Label
from ._lines import LevelLine, level_line from ._lines import LevelLine, level_line
from ._style import _font from ._style import _font
from ._forms import FieldsForm
from ..log import get_logger
log = get_logger(__name__)
class Position(BaseModel): class Position(BaseModel):
@ -60,46 +65,65 @@ class Position(BaseModel):
fills: list[Status] = [] fills: list[Status] = []
def mk_pp_alloc( def mk_alloc(
accounts: dict[str, Optional[str]] = { accounts: dict[str, Optional[str]] = {
'paper': None, 'paper': None,
'ib.paper': 'DU1435481',
'ib.margin': 'U10983%',
}, },
) -> Allocator: # noqa ) -> Allocator: # noqa
from ..brokers import config
conf, path = config.load()
section = conf.get('accounts')
if section is None:
log.warning('No accounts config found?')
for brokername, account_labels in section.items():
for name, value in account_labels.items():
accounts[f'{brokername}.{name}'] = value
# lol we have to do this module patching bc ``pydantic`` # lol we have to do this module patching bc ``pydantic``
# needs types to exist at module level: # needs types to exist at module level:
# https://pydantic-docs.helpmanual.io/usage/postponed_annotations/ # https://pydantic-docs.helpmanual.io/usage/postponed_annotations/
mod = sys.modules[__name__] mod = sys.modules[__name__]
accounts = bidict(accounts) accounts = bidict(accounts)
Account = mod.Account = enum.Enum('Account', accounts) Account = mod.Account = Enum('Account', accounts)
size_units = bidict({ size_units = bidict({
'$ size': 'currency', 'currency': '$ size',
'% of port': 'percent_of_port', 'units': '# units',
'# shares': 'shares', # 'percent_of_port': '% of port', # TODO:
}) })
SizeUnit = mod.SizeUnit = enum.Enum( SizeUnit = mod.SizeUnit = Enum(
'SizeUnit', 'SizeUnit',
size_units.inverse size_units,
) )
class Allocator(BaseModel): class Allocator(BaseModel):
class Config: class Config:
validate_assignment = True validate_assignment = True
copy_on_model_validation = False
extra = 'allow' extra = 'allow'
account: Account = None account: Account = None
_accounts: dict[str, Optional[str]] = accounts _accounts: dict[str, Optional[str]] = accounts
@validator('account', pre=True)
def set_account(cls, v):
if v:
return cls._accounts[v]
size_unit: SizeUnit = 'currency' size_unit: SizeUnit = 'currency'
_size_units: dict[str, Optional[str]] = size_units _size_units: dict[str, Optional[str]] = size_units
@validator('size_unit')
def lookup_key(cls, v):
# apply the corresponding enum key for the text "description" value
return v.name
disti_weight: str = 'uniform' disti_weight: str = 'uniform'
size: float size: float
@ -108,31 +132,104 @@ def mk_pp_alloc(
_position: Position = None _position: Position = None
_widget: QWidget = None _widget: QWidget = None
def slotted_units(
self,
symbol: Symbol,
size: float,
price: float,
) -> float:
return size / self.slots
def size_from_currency_limit(
self,
symbol: Symbol,
size: float,
price: float,
) -> float:
return size / self.slots / price
_sizers = {
'currency': size_from_currency_limit,
'units': slotted_units,
# 'percent_of_port': lambda: 0,
}
def get_order_info( def get_order_info(
self, self,
# TODO: apply the symbol when the chart it is selected # TODO: apply the symbol when the chart it is selected
symbol: Symbol, symbol: Symbol,
price: float, price: float,
action: str,
) -> dict: ) -> dict:
'''Generate order request info for the "next" submittable order
depending on position / order entry config.
# pprint(self._position.info.dict()) '''
tracker = self._position
fiat_size = self.size / self.slots pp_size = tracker.live_pp.size
ld = symbol.lot_digits() ld = symbol.lot_digits()
units = round(fiat_size / price, ndigits=ld)
return {'size': units, 'size_digits': ld} if (
action == 'buy' and pp_size > 0 or
action == 'sell' and pp_size < 0 or
pp_size == 0
): # an entry
def pp_update(self) -> None: # try to read existing position and compute
# update fill bar and labels # next entry/exit size from distribution weight policy
... # (and possibly TODO: commissions info).
entry_size = self._sizers[self.size_unit](
self, symbol, self.size, price
)
if ld == 0:
# in discrete units case (eg. stocks, futures, opts)
# we always round down
units = floor(entry_size)
else:
# we can return a float lot size rounded to nearest tick
units = round(entry_size, ndigits=ld)
return {
'size': units,
'size_digits': ld
}
elif action != 'alert': # an exit
pp_size = tracker.startup_pp.size
if ld == 0:
# exit at the slot size worth of units or the remaining
# units left for the position to be net-zero, whichever
# is smaller
evenly, r = divmod(pp_size, self.slots)
exit_size = min(evenly, pp_size)
# "front" weight the exit order sizes
# TODO: make this configurable?
if r:
exit_size += 1
else: # we can return a float lot size rounded to nearest tick
exit_size = min(
round(pp_size / self.slots, ndigits=ld),
pp_size
)
return {
'size': exit_size,
'size_digits': ld
}
else: # likely an alert
return {'size': 0}
return Allocator( return Allocator(
account=None, account=None,
size_unit=size_units.inverse['currency'], size_unit=size_units['currency'],
size=2000, size=5e3,
slots=4, slots=4,
) )
@ -144,10 +241,11 @@ class PositionTracker:
''' '''
# inputs # inputs
chart: 'ChartPlotWidget' # noqa chart: 'ChartPlotWidget' # noqa
alloc: 'Allocator' # alloc: 'Allocator'
# allocated # allocated
info: Position startup_pp: Position
live_pp: Position
pp_label: Label pp_label: Label
size_label: Label size_label: Label
line: Optional[LevelLine] = None line: Optional[LevelLine] = None
@ -157,15 +255,18 @@ class PositionTracker:
def __init__( def __init__(
self, self,
chart: 'ChartPlotWidget', # noqa chart: 'ChartPlotWidget', # noqa
alloc: 'Allocator', # noqa
) -> None: ) -> None:
self.chart = chart self.chart = chart
self.info = Position( self.alloc = alloc
self.live_pp = Position(
symbol=chart.linked.symbol, symbol=chart.linked.symbol,
size=0, size=0,
avg_price=0, avg_price=0,
) )
self.startup_pp = self.live_pp.copy()
view = chart.getViewBox() view = chart.getViewBox()
@ -197,7 +298,7 @@ class PositionTracker:
# this is "static" label # this is "static" label
# update_on_range_change=False, # update_on_range_change=False,
fmt_str='\n'.join(( fmt_str='\n'.join((
':{entry_size:.{size_digits}f}', ':{entry_size:.{size_digits}f}x',
)), )),
fields={ fields={
@ -236,6 +337,13 @@ class PositionTracker:
# }, # },
# ) # )
@property
def pane(self) -> FieldsForm:
'''Return handle to pp side pane form.
'''
return self.chart.linked.godwidget.pp_pane
def update_graphics( def update_graphics(
self, self,
marker: LevelMarker marker: LevelMarker
@ -253,15 +361,18 @@ class PositionTracker:
def update( def update(
self, self,
msg: BrokerdPosition, msg: BrokerdPosition,
position: Optional[Position] = None,
) -> None: ) -> None:
'''Update graphics and data from average price and size. '''Update graphics and data from average price and size.
''' '''
avg_price, size = msg['avg_price'], msg['size'] avg_price, size = msg['avg_price'], msg['size']
# info updates
self.info.avg_price = avg_price # live pp updates
self.info.size = size pp = position or self.live_pp
pp.avg_price = avg_price
pp.size = size
self.update_line(avg_price, size) self.update_line(avg_price, size)
@ -293,7 +404,7 @@ class PositionTracker:
return 0 return 0
def show(self) -> None: def show(self) -> None:
if self.info.size: if self.live_pp.size:
self.line.show() self.line.show()
self._level_marker.show() self._level_marker.show()
self.pp_label.show() self.pp_label.show()
@ -433,3 +544,29 @@ class PositionTracker:
# remove pp line from view # remove pp line from view
line.delete() line.delete()
self.line = None self.line = None
def init_status_ui(
self,
):
pp = self.startup_pp
# calculate proportion of position size limit
# that exists and display in fill bar
size = pp.size
if self.alloc.size_unit == 'currency':
size = size * pp.avg_price
self.update_status_ui(size)
def update_status_ui(
self,
size: float,
) -> None:
alloc = self.alloc
prop = size / alloc.size
slots = alloc.slots
pane = self.pane
pane.fill_bar.set_slots(slots, min(floor(prop * slots), slots))