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 changesfsp_feeds
parent
80b01ed8cf
commit
16c1f727c7
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue