Support real-time account switch and status update

Make a pp tracker per account and load on order mode boot.
Only show details on the pp tracker for the selected account.
Make the settings pane assign a `.current_pp` state on the order mode
instance (for the charted symbol) on account selection switches and no
longer keep a ref to a single pp tracker and allocator in the pane.

`SettingsPane.update_status_ui()` now expects an explicit tracker
reference as input. Still need to figure out the pnl update task logic
despite the intermittent account changes.
fsp_feeds
Tyler Goodlet 2021-09-10 11:50:24 -04:00
parent d25aec53e3
commit f16591612e
3 changed files with 159 additions and 105 deletions

View File

@ -198,7 +198,7 @@ async def handle_viewmode_kb_inputs(
Qt.Key_P,
}
):
pp_pane = order_mode.pp.pane
pp_pane = order_mode.current_pp.pane
if pp_pane.isHidden():
pp_pane.show()
else:
@ -213,7 +213,7 @@ async def handle_viewmode_kb_inputs(
if order_keys_pressed:
# show the pp size label
order_mode.pp.show()
order_mode.current_pp.show()
# TODO: show pp config mini-params in status bar widget
# mode.pp_config.show()
@ -259,20 +259,23 @@ async def handle_viewmode_kb_inputs(
) and
key in NUMBER_LINE
):
# hot key to set order slots size
# hot key to set order slots size.
# change edit field to current number line value,
# update the pp allocator bar, unhighlight the
# field when ctrl is released.
num = int(text)
pp_pane = order_mode.pane
pp_pane.on_ui_settings_change('slots', num)
edit = pp_pane.form.fields['slots']
edit.selectAll()
# un-highlight on ctrl release
on_next_release = edit.deselect
pp_pane.update_status_ui()
else: # none active
# hide pp label
order_mode.pp.hide_info()
order_mode.current_pp.hide_info()
# if none are pressed, remove "staged" level
# line under cursor position

View File

@ -50,10 +50,6 @@ class SettingsPane:
order entry sizes and position limits per tradable instrument.
'''
# config for and underlying validation model
tracker: PositionTracker
alloc: Allocator
# input fields
form: FieldsForm
@ -64,9 +60,8 @@ class SettingsPane:
pnl_label: QLabel
limit_label: QLabel
def transform_to(self, size_unit: str) -> None:
if self.alloc.size_unit == size_unit:
return
# encompasing high level namespace
order_mode: Optional['OrderMode'] = None # typing: ignore # noqa
def on_selection_change(
self,
@ -79,7 +74,6 @@ class SettingsPane:
'''
log.info(f'selection input: {text}')
setattr(self.alloc, key, text)
self.on_ui_settings_change(key, text)
def on_ui_settings_change(
@ -92,10 +86,34 @@ class SettingsPane:
'''Called on any order pane edit field value change.
'''
alloc = self.alloc
mode = self.order_mode
if key == 'account':
# an account switch request
# hide detail on the old pp
old_tracker = mode.current_pp
old_tracker.hide_info()
# re-assign the order mode tracker
account_name = value
tracker = mode.trackers[account_name]
self.order_mode.current_pp = tracker
assert tracker.alloc.account_name() == account_name
self.form.fields['account'].setCurrentText(account_name)
tracker.show()
tracker.hide_info()
# load the new account's allocator
alloc = tracker.alloc
else:
tracker = mode.current_pp
alloc = tracker.alloc
size_unit = alloc.size_unit
# write any passed settings to allocator
# WRITE any settings to current pp's allocator
if key == 'limit':
if size_unit == 'currency':
alloc.currency_limit = float(value)
@ -110,14 +128,10 @@ class SettingsPane:
# the current settings in the new units
pass
elif key == 'account':
account_name = value or 'paper'
alloc.account = account_name
else:
elif key != 'account':
raise ValueError(f'Unknown setting {key}')
# read out settings and update UI
# READ out settings and update UI
log.info(f'settings change: {key}: {value}')
suffix = {'currency': ' $', 'units': ' u'}[size_unit]
@ -125,7 +139,7 @@ class SettingsPane:
# 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()
self.update_status_ui(pp=tracker)
step_size, currency_per_slot = alloc.step_sizes()
@ -143,7 +157,6 @@ class SettingsPane:
self.form.fields['size_unit'].setCurrentText(
alloc._size_units[alloc.size_unit]
)
self.form.fields['account'].setCurrentText(alloc.account_name())
self.form.fields['slots'].setText(str(alloc.slots))
self.form.fields['limit'].setText(str(limit))
@ -154,13 +167,14 @@ class SettingsPane:
def update_status_ui(
self,
size: float = None,
pp: PositionTracker,
) -> None:
alloc = self.alloc
alloc = pp.alloc
slots = alloc.slots
used = alloc.slots_used(self.tracker.live_pp)
used = alloc.slots_used(pp.live_pp)
# calculate proportion of position size limit
# that exists and display in fill bar
@ -173,12 +187,17 @@ class SettingsPane:
min(used, slots)
)
# TODO: move to order mode module! doesn't need to be a method since
# we partial in all the state
def on_level_change_update_next_order_info(
self,
level: float,
# these are all ``partial``-ed in at callback assignment time.
line: LevelLine,
order: Order,
tracker: PositionTracker,
) -> None:
'''A callback applied for each level change to the line
@ -187,9 +206,9 @@ class SettingsPane:
``OrderMode.line_from_order()``
'''
order_info = self.alloc.next_order_info(
startup_pp=self.tracker.startup_pp,
live_pp=self.tracker.live_pp,
order_info = tracker.alloc.next_order_info(
startup_pp=tracker.startup_pp,
live_pp=tracker.live_pp,
price=level,
action=order.action,
)
@ -267,8 +286,8 @@ def position_line(
class PositionTracker:
'''Track and display a real-time position for a single symbol
on a chart.
'''Track and display real-time positions for a single symbol
over multiple accounts on a single chart.
Graphically composed of a level line and marker as well as labels
for indcating current position information. Updates are made to the
@ -277,11 +296,12 @@ class PositionTracker:
'''
# inputs
chart: 'ChartPlotWidget' # noqa
alloc: Allocator
startup_pp: Position
live_pp: Position
# allocated
live_pp: Position
pp_label: Label
size_label: Label
line: Optional[LevelLine] = None
@ -297,6 +317,7 @@ class PositionTracker:
) -> None:
self.chart = chart
self.alloc = alloc
self.startup_pp = startup_pp
self.live_pp = startup_pp.copy()
@ -375,7 +396,6 @@ class PositionTracker:
'''
# live pp updates
pp = position or self.live_pp
# pp.update_from_msg(msg)
self.update_line(
pp.avg_price,
@ -413,7 +433,6 @@ class PositionTracker:
def show(self) -> None:
if self.live_pp.size:
self.line.show()
self.line.show_labels()

View File

@ -27,7 +27,6 @@ import time
from typing import Optional, Dict, Callable, Any
import uuid
from bidict import bidict
from pydantic import BaseModel
import tractor
import trio
@ -36,7 +35,6 @@ from .. import config
from ..calc import pnl
from ..clearing._client import open_ems, OrderBook
from ..clearing._allocate import (
Allocator,
mk_allocator,
Position,
)
@ -102,12 +100,12 @@ class OrderMode:
lines: LineEditor
arrows: ArrowEditor
multistatus: MultiStatus
pp: PositionTracker
alloc: 'Allocator' # noqa
pane: SettingsPane
trackers: dict[str, PositionTracker]
# switched state, the current position
current_pp: Optional[PositionTracker] = None
active: bool = False
name: str = 'order'
dialogs: dict[str, OrderDialog] = field(default_factory=dict)
@ -155,6 +153,7 @@ class OrderMode:
self.pane.on_level_change_update_next_order_info,
line=line,
order=order,
tracker=self.current_pp,
)
else:
@ -193,7 +192,7 @@ class OrderMode:
order = self._staged_order = Order(
action=action,
price=price,
account=self.alloc.account_name(),
account=self.current_pp.alloc.account_name(),
size=0,
symbol=symbol,
brokers=symbol.brokers,
@ -499,7 +498,7 @@ async def open_order_mode(
book: OrderBook
trades_stream: tractor.MsgStream
positions: dict
position_msgs: dict
# spawn EMS actor-service
async with (
@ -507,7 +506,7 @@ async def open_order_mode(
open_ems(brokername, symbol) as (
book,
trades_stream,
positions
position_msgs
),
trio.open_nursery() as tn,
@ -520,64 +519,91 @@ async def open_order_mode(
lines = LineEditor(chart=chart)
arrows = ArrowEditor(chart, {})
# allocation and account settings side pane
form = chart.sidepane
# update any from exising positions received from ``brokerd``
# symbol id
symbol = chart.linked.symbol
symkey = chart.linked._symbol.key
symkey = symbol.key
# NOTE: requires that the backend exactly specifies
# the expected symbol key in it's positions msg.
pp_msg = positions.get(symkey)
# net-zero pp
startup_pp = Position(
symbol=symbol,
size=0,
avg_price=0,
)
# map of per-provider account keys to position tracker instances
trackers: dict[str, PositionTracker] = {}
# load account names from ``brokers.toml``
accounts = bidict(config.load_accounts())
# process pps back from broker, only present
# account names reported back from ``brokerd``.
pp_account = None
accounts = config.load_accounts(providers=symbol.brokers)
if accounts:
# first account listed is the one we select at startup
# (aka order based selection).
pp_account = next(iter(accounts.keys()))
else:
pp_account = 'paper'
if pp_msg:
log.info(f'Loading pp for {symkey}:\n{pformat(pp_msg)}')
startup_pp.update_from_msg(pp_msg)
pp_account = accounts.inverse.get(pp_msg.get('account'))
# for each account with this broker, allocate pp tracker components
for config_name, account in accounts.items():
# net-zero pp
startup_pp = Position(
symbol=symbol,
size=0,
avg_price=0,
)
# lookup account for this pp or load the user default
# for this backend
# allocator
alloc = mk_allocator(
symbol=symbol,
accounts=accounts,
account=pp_account,
startup_pp=startup_pp,
)
form.model = alloc
pp_tracker = PositionTracker(
chart,
alloc,
startup_pp
)
pp_tracker.update_from_pp(startup_pp)
if startup_pp.size == 0:
# if no position, don't show pp tracking graphics
# allocator
alloc = mk_allocator(
symbol=symbol,
accounts=accounts,
account=config_name,
startup_pp=startup_pp,
)
pp_tracker = PositionTracker(
chart,
alloc,
startup_pp
)
pp_tracker.hide()
trackers[config_name] = pp_tracker
# TODO: preparse and account-map these msgs and do it all in ONE LOOP!
# NOTE: requires the backend exactly specifies
# the expected symbol key in its positions msg.
pp_msgs = position_msgs.get(symkey, ())
# update all pp trackers with existing data relayed
# from ``brokerd``.
for msg in pp_msgs:
log.info(f'Loading pp for {symkey}:\n{pformat(msg)}')
account_name = accounts.inverse.get(msg.get('account'))
tracker = trackers[account_name]
# TODO: do we even really need the "startup pp" or can we
# just take the max and pass that into the some state / the
# alloc?
tracker.startup_pp.update_from_msg(msg)
tracker.live_pp.update_from_msg(msg)
# TODO:
# if this startup size is greater the allocator limit,
# increase the limit to the current pp size which is done
# in this alloc factory..
# tracker.alloc = mk_allocator(
# symbol=symbol,
# accounts=accounts,
# account=account_name,
# startup_pp=tracker.live_pp,
# )
# tracker.update_from_pp(tracker.startup_pp)
tracker.update_from_pp(tracker.live_pp)
if tracker.startup_pp.size != 0:
# if no position, don't show pp tracking graphics
tracker.show()
tracker.hide_info()
# order pane widgets and allocation model
order_pane = SettingsPane(
tracker=pp_tracker,
form=form,
alloc=alloc,
# XXX: ugh, so hideous...
fill_bar=form.fill_bar,
pnl_label=form.left_label,
@ -585,13 +611,6 @@ async def open_order_mode(
limit_label=form.top_label,
)
# set startup limit value read during alloc init
order_pane.on_ui_settings_change('limit', alloc.limit())
order_pane.on_ui_settings_change('account', pp_account)
# make fill bar and positioning snapshot
order_pane.update_status_ui(size=startup_pp.size)
# top level abstraction which wraps all this crazyness into
# a namespace..
mode = OrderMode(
@ -600,10 +619,18 @@ async def open_order_mode(
lines,
arrows,
multistatus,
pp_tracker,
alloc=alloc,
pane=order_pane,
trackers=trackers,
)
# XXX: MUST be set
order_pane.order_mode = mode
# select a pp to track
tracker = trackers[pp_account]
mode.current_pp = tracker
tracker.show()
tracker.hide_info()
# XXX: would love to not have to do this separate from edit
# fields (which are done in an async loop - see below)
@ -619,13 +646,19 @@ async def open_order_mode(
)
)
# make fill bar and positioning snapshot
order_pane.on_ui_settings_change('limit', tracker.alloc.limit())
order_pane.on_ui_settings_change('account', pp_account)
# order_pane.update_status_ui(pp=tracker.startup_pp)
order_pane.update_status_ui(pp=tracker)
# TODO: create a mode "manager" of sorts?
# -> probably just call it "UxModes" err sumthin?
# so that view handlers can access it
view.order_mode = mode
# real-time pnl display task allocation
live_pp = mode.pp.live_pp
live_pp = mode.current_pp.live_pp
size = live_pp.size
if size:
global _pnl_tasks
@ -697,7 +730,7 @@ async def display_pnl(
'''
global _pnl_tasks
pp = order_mode.pp
pp = order_mode.current_pp
live = pp.live_pp
sym = live.symbol.key
@ -727,7 +760,7 @@ async def display_pnl(
for tick in iterticks(quote, types):
# print(f'{1/period} Hz')
size = live.size
size = order_mode.current_pp.live_pp.size
if size == 0:
# terminate this update task since we're
# no longer in a pp
@ -738,7 +771,8 @@ async def display_pnl(
# compute and display pnl status
order_mode.pane.pnl_label.format(
pnl=copysign(1, size) * pnl(
live.avg_price,
# live.avg_price,
order_mode.current_pp.live_pp.avg_price,
tick['price'],
),
)
@ -760,7 +794,6 @@ async def process_trades_and_update_ui(
) -> None:
get_index = mode.chart.get_index
tracker = mode.pp
global _pnl_tasks
# this is where we receive **back** messages
@ -774,16 +807,15 @@ async def process_trades_and_update_ui(
if name in (
'position',
):
# show line label once order is live
sym = mode.chart.linked.symbol
if msg['symbol'].lower() in sym.key:
tracker = mode.trackers[msg['account']]
tracker.live_pp.update_from_msg(msg)
tracker.update_from_pp()
# update order pane widgets
mode.pane.update_status_ui()
mode.pane.update_status_ui(tracker.live_pp)
if (
tracker.live_pp.size and
@ -795,7 +827,7 @@ async def process_trades_and_update_ui(
mode,
)
# short circuit to next msg to avoid
# uncessary msg content lookups
# unnecessary msg content lookups
continue
resp = msg['resp']