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, Qt.Key_P,
} }
): ):
pp_pane = order_mode.pp.pane pp_pane = order_mode.current_pp.pane
if pp_pane.isHidden(): if pp_pane.isHidden():
pp_pane.show() pp_pane.show()
else: else:
@ -213,7 +213,7 @@ async def handle_viewmode_kb_inputs(
if order_keys_pressed: if order_keys_pressed:
# show the pp size label # show the pp size label
order_mode.pp.show() order_mode.current_pp.show()
# TODO: show pp config mini-params in status bar widget # TODO: show pp config mini-params in status bar widget
# mode.pp_config.show() # mode.pp_config.show()
@ -259,20 +259,23 @@ async def handle_viewmode_kb_inputs(
) and ) and
key in NUMBER_LINE 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) num = int(text)
pp_pane = order_mode.pane pp_pane = order_mode.pane
pp_pane.on_ui_settings_change('slots', num) pp_pane.on_ui_settings_change('slots', num)
edit = pp_pane.form.fields['slots'] edit = pp_pane.form.fields['slots']
edit.selectAll() edit.selectAll()
# un-highlight on ctrl release
on_next_release = edit.deselect on_next_release = edit.deselect
pp_pane.update_status_ui() pp_pane.update_status_ui()
else: # none active else: # none active
# hide pp label # hide pp label
order_mode.pp.hide_info() order_mode.current_pp.hide_info()
# if none are pressed, remove "staged" level # if none are pressed, remove "staged" level
# line under cursor position # line under cursor position

View File

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

View File

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