Add an a pane composite and throw ui update methods on it

fsp_feeds
Tyler Goodlet 2021-08-23 14:21:26 -04:00
parent 2b8c3f69b1
commit b09d0d7129
1 changed files with 107 additions and 104 deletions

View File

@ -19,6 +19,7 @@ Position info and display
""" """
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from enum import Enum from enum import Enum
from functools import partial from functools import partial
from math import floor from math import floor
@ -26,7 +27,7 @@ from math import floor
from typing import Optional from typing import Optional
from PyQt5.QtWidgets import QWidget # from PyQt5.QtWidgets import QWidget
from bidict import bidict from bidict import bidict
from pyqtgraph import functions as fn from pyqtgraph import functions as fn
from pydantic import BaseModel, validator from pydantic import BaseModel, validator
@ -43,7 +44,7 @@ from ._lines import LevelLine, level_line
from ._style import _font from ._style import _font
from ._forms import FieldsForm, FillStatusBar, QLabel from ._forms import FieldsForm, FillStatusBar, QLabel
from ..log import get_logger from ..log import get_logger
from ..clearing._messages import Order
log = get_logger(__name__) log = get_logger(__name__)
@ -78,11 +79,12 @@ SizeUnit = Enum(
class Allocator(BaseModel): class Allocator(BaseModel):
symbol: Symbol
class Config: class Config:
validate_assignment = True validate_assignment = True
copy_on_model_validation = False copy_on_model_validation = False
extra = 'allow' arbitrary_types_allowed = True
# underscore_attrs_are_private = False
account: Optional[str] = 'paper' account: Optional[str] = 'paper'
_accounts: bidict[str, Optional[str]] _accounts: bidict[str, Optional[str]]
@ -102,39 +104,15 @@ class Allocator(BaseModel):
disti_weight: str = 'uniform' disti_weight: str = 'uniform'
size: float units_size: float
currency_size: float
slots: int slots: int
_position: Position = None def next_order_info(
_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(
self, self,
# TODO: apply the symbol when the chart it is selected startup_pp: Position,
symbol: Symbol, live_pp: Position,
price: float, price: float,
action: str, action: str,
@ -143,30 +121,30 @@ class Allocator(BaseModel):
depending on position / order entry config. depending on position / order entry config.
''' '''
tracker = self._position sym = self.symbol
pp_size = tracker.live_pp.size ld = sym.lot_size_digits
ld = symbol.lot_size_digits
startup_size = startup_pp.size
live_size = live_pp.size
if ( if (
action == 'buy' and pp_size > 0 or action == 'buy' and startup_size > 0 or
action == 'sell' and pp_size < 0 or action == 'sell' and startup_size < 0 or
pp_size == 0 live_size == 0
): # an entry ): # an entry
# try to read existing position and compute size_step = self.units_size / self.slots
# next entry/exit size from distribution weight policy
# (and possibly TODO: commissions info). if self.size_unit == 'currency':
entry_size = self._sizers[self.size_unit]( size_step = self.currency_size / self.slots / price
self, symbol, self.size, price
)
if ld == 0: if ld == 0:
# in discrete units case (eg. stocks, futures, opts) # in discrete units case (eg. stocks, futures, opts)
# we always round down # we always round down
units = floor(entry_size) units = floor(size_step)
else: else:
# we can return a float lot size rounded to nearest tick # we can return a float lot size rounded to nearest tick
units = round(entry_size, ndigits=ld) units = round(size_step, ndigits=ld)
return { return {
'size': units, 'size': units,
@ -175,13 +153,12 @@ class Allocator(BaseModel):
elif action != 'alert': # an exit elif action != 'alert': # an exit
pp_size = tracker.startup_pp.size
if ld == 0: if ld == 0:
# exit at the slot size worth of units or the remaining # exit at the slot size worth of units or the remaining
# units left for the position to be net-zero, whichever # units left for the position to be net-zero, whichever
# is smaller # is smaller
evenly, r = divmod(pp_size, self.slots) evenly, r = divmod(startup_size, self.slots)
exit_size = min(evenly, pp_size) exit_size = min(evenly, startup_size)
# "front" weight the exit order sizes # "front" weight the exit order sizes
# TODO: make this configurable? # TODO: make this configurable?
@ -190,8 +167,8 @@ class Allocator(BaseModel):
else: # we can return a float lot size rounded to nearest tick else: # we can return a float lot size rounded to nearest tick
exit_size = min( exit_size = min(
round(pp_size / self.slots, ndigits=ld), round(startup_size / self.slots, ndigits=ld),
pp_size startup_size
) )
return { return {
@ -205,56 +182,108 @@ class Allocator(BaseModel):
def mk_alloc( def mk_alloc(
accounts: dict[str, Optional[str]] = { symbol: Symbol,
'paper': None, accounts: dict[str, Optional[str]],
},
) -> 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
return Allocator( return Allocator(
symbol=symbol,
account=None, account=None,
_accounts=bidict(accounts), _accounts=bidict(accounts),
size_unit=_size_units['currency'], size_unit=_size_units['currency'],
size=5e3, units_size=400,
currency_size=5e3,
slots=4, slots=4,
) )
class OrderPane(BaseModel): @dataclass
'''Set of widgets plus an allocator model class OrderModePane:
for configuring order entry sizes. '''Composite set of widgets plus an allocator model for configuring
order entry sizes and position limits per tradable instrument.
''' '''
class Config:
arbitrary_types_allowed = True
# underscore_attrs_are_private = False
# config for and underlying validation model # config for and underlying validation model
form: FieldsForm tracker: PositionTracker
model: BaseModel alloc: Allocator
# input fields
form: FieldsForm
# output fill status and labels
fill_bar: FillStatusBar
# fill status + labels
fill_status_bar: FillStatusBar
step_label: QLabel step_label: QLabel
pnl_label: QLabel pnl_label: QLabel
limit_label: QLabel limit_label: QLabel
def config_ui_from_model(self) -> None: def update_ui_from_alloc(self) -> None:
... ...
def transform_to(self, size_unit: str) -> None: def transform_to(self, size_unit: str) -> None:
... ...
def init_status_ui(
self,
):
pp = self.tracker.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,
) -> None:
alloc = self.alloc
prop = size / alloc.units_size
slots = alloc.slots
if alloc.size_unit == 'currency':
used = floor(prop * slots)
else:
used = alloc.units_size
self.fill_bar.set_slots(
slots,
min(used, slots)
)
def on_level_change_update_next_order_info(
self,
level: float,
line: LevelLine,
order: Order,
) -> None:
'''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()``
'''
order_info = self.alloc.next_order_info(
startup_pp=self.tracker.startup_pp,
live_pp=self.tracker.live_pp,
price=level,
action=order.action,
)
line.update_labels(order_info)
# update bound-in staged order
order.price = level
order.size = order_info['size']
class PositionTracker: class PositionTracker:
'''Track and display a real-time position for a single symbol '''Track and display a real-time position for a single symbol
@ -567,29 +596,3 @@ 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))