Finally, correct "next size" allocation logic

Handling the edge cases in this was "fun", namely:
- entering with less then a slot's worth of units to purchase
  before hitting the pp limit or, less then a slots worth when exiting
  toward a net-zero position.
- round pp msg updates using the symbol tick and lot size digits to
  avoid super small (1e-30 lel) positions lingering in the ems (happens
  moreso with the paper engine).
- don't expect the next size method to be called for alert level changes
fsp_feeds
Tyler Goodlet 2021-08-25 10:27:58 -04:00
parent 80b01ed8cf
commit 16c1f727c7
1 changed files with 139 additions and 88 deletions

View File

@ -23,11 +23,9 @@ 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
# from pprint import pprint
from typing import Optional from typing import Optional
# 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
@ -102,10 +100,12 @@ class Allocator(BaseModel):
# apply the corresponding enum key for the text "description" value # apply the corresponding enum key for the text "description" value
return v.name return v.name
disti_weight: str = 'uniform' # TODO: if we ever want ot support non-uniform entry-slot-proportion
# "sizes"
# disti_weight: str = 'uniform'
units_size: float units_limit: float
currency_size: float currency_limit: float
slots: int slots: int
def next_order_info( def next_order_info(
@ -124,8 +124,18 @@ class Allocator(BaseModel):
sym = self.symbol sym = self.symbol
ld = sym.lot_size_digits ld = sym.lot_size_digits
startup_size = startup_pp.size
live_size = live_pp.size live_size = live_pp.size
startup_size = startup_pp.size
step_size = self.units_limit / self.slots
l_sub_pp = self.units_limit - live_size
if self.size_unit == 'currency':
startup_size = startup_pp.size * startup_pp.avg_price / price
step_size = self.currency_limit / self.slots / price
l_sub_pp = (
self.currency_limit - live_size * live_pp.avg_price
) / price
if ( if (
action == 'buy' and startup_size > 0 or action == 'buy' and startup_size > 0 or
@ -133,70 +143,20 @@ class Allocator(BaseModel):
live_size == 0 live_size == 0
): # an entry ): # an entry
size_step = self.units_size / self.slots units_size = min(step_size, l_sub_pp)
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(size_step)
else: else:
# we can return a float lot size rounded to nearest tick # exit at the slot size worth of units or the remaining
units = round(size_step, ndigits=ld) # units left for the position to be net-zero, whichever
# is smaller
units_size = min(step_size, live_size)
units = round(units_size, ndigits=ld)
return { return {
'size': units, 'size': units,
'size_digits': ld 'size_digits': ld
} }
elif action != 'alert': # an exit
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(startup_size, self.slots)
exit_size = min(evenly, startup_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(startup_size / self.slots, ndigits=ld),
startup_size
)
return {
'size': exit_size,
'size_digits': ld
}
else: # likely an alert
return {'size': 0}
def mk_alloc(
symbol: Symbol,
accounts: dict[str, Optional[str]],
) -> Allocator: # noqa
return Allocator(
symbol=symbol,
account=None,
_accounts=bidict(accounts),
size_unit=_size_units['currency'],
units_size=400,
currency_size=5e3,
slots=4,
)
@dataclass @dataclass
class OrderModePane: class OrderModePane:
@ -218,44 +178,125 @@ class OrderModePane:
pnl_label: QLabel pnl_label: QLabel
limit_label: QLabel limit_label: QLabel
def update_ui_from_alloc(self) -> None:
...
def transform_to(self, size_unit: str) -> None: def transform_to(self, size_unit: str) -> None:
... if self.alloc.size_unit == size_unit:
return
def on_selection_change(
self,
key: str,
text: str,
) -> None:
'''Called on any order pane drop down selection change.
'''
print(f'{text}')
setattr(self.alloc, key, text)
print(self.alloc.dict())
async def on_ui_settings_change(
self,
key: str,
value: str,
) -> bool:
'''Called on any order pane edit field value change.
'''
print(f'settings change: {key}:{value}')
# TODO: maybe return a diff of settings so if we can an error we
# can have general input handling code to report it through the
# UI in some way?
return True
def init_status_ui( def init_status_ui(
self, self,
): ):
pp = self.tracker.startup_pp alloc = self.alloc
asset_type = alloc.symbol.type_key
form = self.form
# calculate proportion of position size limit # TODO: pull from piker.toml
# that exists and display in fill bar # default config
size = pp.size slots = 4
currency_limit = 5e3
if self.alloc.size_unit == 'currency': startup_pp = self.tracker.startup_pp
size = size * pp.avg_price
self.update_status_ui(size) alloc.slots = slots
alloc.currency_limit = currency_limit
# default entry sizing
if asset_type in ('stock', 'crypto', 'forex'):
alloc.size_unit = '$ size'
elif asset_type in ('future', 'option', 'futures_option'):
# since it's harder to know how currency "applies" in this case
# given leverage properties
alloc.size_unit = '# units'
# set units limit to slots size thus making make the next
# entry step 1.0
alloc.units_limit = slots
# if the current position is already greater then the limit
# settings, increase the limit to the current position
if alloc.size_unit == 'currency':
startup_size = startup_pp.size * startup_pp.avg_price
if startup_size > alloc.currency_limit:
alloc.currency_limit = round(startup_size, ndigits=2)
limit_text = alloc.currency_limit
else:
startup_size = startup_pp.size
if startup_size > alloc.units_limit:
alloc.units_limit = startup_size
limit_text = alloc.units_limit
# update size unit in UI
form.fields['size_unit'].setCurrentText(
alloc._size_units[alloc.size_unit]
)
form.fields['slots'].setText(str(alloc.slots))
form.fields['limit'].setText(str(limit_text))
# TODO: a reverse look up from the position to the equivalent
# account(s), if none then look to user config for default?
self.update_status_ui(size=startup_size)
def update_status_ui( def update_status_ui(
self, self,
size: float = None, size: float = None,
) -> None: ) -> None:
alloc = self.alloc
prop = size / alloc.units_size alloc = self.alloc
live_pp = self.tracker.live_pp
slots = alloc.slots slots = alloc.slots
if alloc.size_unit == 'currency': if alloc.size_unit == 'currency':
used = floor(prop * slots) live_currency_size = size or (live_pp.size * live_pp.avg_price)
else: prop = live_currency_size / alloc.currency_limit
used = alloc.units_size
else:
prop = size or live_pp.size / alloc.units_limit
# calculate proportion of position size limit
# that exists and display in fill bar
# TODO: what should we do for fractional slot pps?
self.fill_bar.set_slots( self.fill_bar.set_slots(
slots, slots,
min(used, slots) min(round(prop * slots), slots)
) )
def on_level_change_update_next_order_info( def on_level_change_update_next_order_info(
@ -284,6 +325,14 @@ class OrderModePane:
order.price = level order.price = level
order.size = order_info['size'] order.size = order_info['size']
def on_settings_change_update_ui(
self,
) -> None:
# TODO:
# - recompute both units_limit and currency_limit
# - update fill bar slotting if necessary (only on slots?)
...
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
@ -292,7 +341,6 @@ class PositionTracker:
''' '''
# inputs # inputs
chart: 'ChartPlotWidget' # noqa chart: 'ChartPlotWidget' # noqa
# alloc: 'Allocator'
# allocated # allocated
startup_pp: Position startup_pp: Position
@ -306,12 +354,10 @@ 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.alloc = alloc
self.live_pp = Position( self.live_pp = Position(
symbol=chart.linked.symbol, symbol=chart.linked.symbol,
size=0, size=0,
@ -418,7 +464,12 @@ class PositionTracker:
'''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'] # XXX: better place to do this?
symbol = self.chart.linked.symbol
avg_price, size = (
round(msg['avg_price'], ndigits=symbol.tick_size_digits),
round(msg['size'], ndigits=symbol.lot_size_digits),
)
# live pp updates # live pp updates
pp = position or self.live_pp pp = position or self.live_pp