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
parent
d25aec53e3
commit
f16591612e
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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,16 +519,27 @@ 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)
|
||||
# map of per-provider account keys to position tracker instances
|
||||
trackers: dict[str, PositionTracker] = {}
|
||||
|
||||
# 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
|
||||
startup_pp = Position(
|
||||
symbol=symbol,
|
||||
|
@ -537,47 +547,63 @@ async def open_order_mode(
|
|||
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
|
||||
alloc = mk_allocator(
|
||||
symbol=symbol,
|
||||
accounts=accounts,
|
||||
account=pp_account,
|
||||
account=config_name,
|
||||
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
|
||||
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']
|
||||
|
|
Loading…
Reference in New Issue