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