diff --git a/piker/brokers/ib.py b/piker/brokers/ib.py index 21946802..51cf4a39 100644 --- a/piker/brokers/ib.py +++ b/piker/brokers/ib.py @@ -1469,6 +1469,7 @@ async def trades_dialogue( # deliver positions to subscriber before anything else all_positions = [] + accounts = set() clients: list[tuple[Client, trio.MemoryReceiveChannel]] = [] for account, client in _accounts2clients.items(): @@ -1484,9 +1485,10 @@ async def trades_dialogue( for pos in client.positions(): msg = pack_position(pos) msg.account = accounts_def.inverse[msg.account] + accounts.add(msg.account) all_positions.append(msg.dict()) - await ctx.started(all_positions) + await ctx.started((all_positions, accounts)) async with ( ctx.open_stream() as ems_stream, diff --git a/piker/clearing/_allocate.py b/piker/clearing/_allocate.py index 94c7af12..f6c9eee1 100644 --- a/piker/clearing/_allocate.py +++ b/piker/clearing/_allocate.py @@ -86,14 +86,7 @@ class Allocator(BaseModel): underscore_attrs_are_private = False symbol: Symbol - accounts: bidict[str, Optional[str]] account: Optional[str] = 'paper' - - @validator('account', pre=False) - def set_account(cls, v, values): - if v: - return values['accounts'][v] - size_unit: SizeUnit = 'currency' _size_units: dict[str, Optional[str]] = _size_units @@ -128,9 +121,6 @@ class Allocator(BaseModel): else: return self.units_limit - def account_name(self) -> str: - return self.accounts.inverse[self.account] - def next_order_info( self, @@ -234,7 +224,7 @@ class Allocator(BaseModel): 'slots_used': slots_used, # update line LHS label with account name - 'account': self.account_name(), + 'account': self.account, } def slots_used( @@ -264,7 +254,6 @@ class Allocator(BaseModel): def mk_allocator( symbol: Symbol, - accounts: dict[str, str], startup_pp: Position, # default allocation settings @@ -293,7 +282,6 @@ def mk_allocator( alloc = Allocator( symbol=symbol, - accounts=accounts, **defaults, ) diff --git a/piker/clearing/_client.py b/piker/clearing/_client.py index a23fdb5e..4f766daf 100644 --- a/piker/clearing/_client.py +++ b/piker/clearing/_client.py @@ -210,7 +210,7 @@ async def open_ems( broker=broker, symbol=symbol.key, - ) as (ctx, positions), + ) as (ctx, (positions, accounts)), # open 2-way trade command stream ctx.open_stream() as trades_stream, @@ -222,4 +222,4 @@ async def open_ems( trades_stream ) - yield book, trades_stream, positions + yield book, trades_stream, positions, accounts diff --git a/piker/clearing/_ems.py b/piker/clearing/_ems.py index 397ef48b..583e2509 100644 --- a/piker/clearing/_ems.py +++ b/piker/clearing/_ems.py @@ -268,6 +268,9 @@ class TradesRelay: # map of symbols to dicts of accounts to pp msgs positions: dict[str, dict[str, BrokerdPosition]] + # allowed account names + accounts: set[str] + # count of connected ems clients for this ``brokerd`` consumers: int = 0 @@ -410,8 +413,7 @@ async def open_brokerd_trades_dialogue( try: async with ( - - open_trades_endpoint as (brokerd_ctx, positions), + open_trades_endpoint as (brokerd_ctx, (positions, accounts,)), brokerd_ctx.open_stream() as brokerd_trades_stream, ): @@ -433,15 +435,20 @@ async def open_brokerd_trades_dialogue( # locally cache and track positions per account. pps = {} for msg in positions: + + account = msg['account'] + assert account in accounts + pps.setdefault( msg['symbol'], {} - )[msg['account']] = msg + )[account] = msg relay = TradesRelay( brokerd_dialogue=brokerd_trades_stream, positions=pps, - consumers=1 + accounts=set(accounts), + consumers=1, ) _router.relays[broker] = relay @@ -936,11 +943,11 @@ async def _emsd_main( ) -> None: '''EMS (sub)actor entrypoint providing the execution management (micro)service which conducts broker - order control on behalf of clients. + order clearing control on behalf of clients. This is the daemon (child) side routine which starts an EMS runtime - (one per broker-feed) and and begins streaming back alerts from - broker executions/fills. + task (one per broker-feed) and and begins streaming back alerts from + each broker's executions/fills. ``send_order_cmds()`` is called here to execute in a task back in the actor which started this service (spawned this actor), presuming @@ -964,8 +971,8 @@ async def _emsd_main( reponse" proxy-broker. | - ``process_client_order_cmds()``: - accepts order cmds from requesting piker clients, registers - execs with exec loop + accepts order cmds from requesting clients, registers dark orders and + alerts with clearing loop. ''' global _router @@ -1015,13 +1022,15 @@ async def _emsd_main( brokerd_stream = relay.brokerd_dialogue # .clone() - # signal to client that we're started - # TODO: we could eventually send back **all** brokerd - # positions here? - await ems_ctx.started( - {sym: list(pps.values()) - for sym, pps in relay.positions.items()} - ) + # flatten out collected pps from brokerd for delivery + pp_msgs = { + sym: list(pps.values()) + for sym, pps in relay.positions.items() + } + + # signal to client that we're started and deliver + # all known pps and accounts for this ``brokerd``. + await ems_ctx.started((pp_msgs, relay.accounts)) # establish 2-way stream with requesting order-client and # begin handling inbound order requests and updates diff --git a/piker/clearing/_paper_engine.py b/piker/clearing/_paper_engine.py index 628f58b9..892087c4 100644 --- a/piker/clearing/_paper_engine.py +++ b/piker/clearing/_paper_engine.py @@ -463,7 +463,7 @@ async def trades_dialogue( # TODO: load paper positions per broker from .toml config file # and pass as symbol to position data mapping: ``dict[str, dict]`` # await ctx.started(all_positions) - await ctx.started({}) + await ctx.started(({}, {'paper',})) async with ( ctx.open_stream() as ems_stream, diff --git a/piker/ui/_forms.py b/piker/ui/_forms.py index 9e7e40ea..5aed6f9c 100644 --- a/piker/ui/_forms.py +++ b/piker/ui/_forms.py @@ -42,12 +42,11 @@ from PyQt5.QtWidgets import ( QStyledItemDelegate, QStyleOptionViewItem, ) -# import pydantic from ._event import open_handlers +from ._icons import mk_icons from ._style import hcolor, _font, _font_small, DpiAwareFont from ._label import FormatLabel -from .. import config class FontAndChartAwareLineEdit(QLineEdit): @@ -71,17 +70,21 @@ class FontAndChartAwareLineEdit(QLineEdit): if width_in_chars: self._chars = int(width_in_chars) + x_size_policy = QSizePolicy.Fixed else: # chart count which will be used to calculate # width of input field. - self._chars: int = 9 + self._chars: int = 6 + # fit to surroundingn frame width + x_size_policy = QSizePolicy.Expanding super().__init__(parent) + # size it as we specify # https://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum self.setSizePolicy( - QSizePolicy.Expanding, + x_size_policy, QSizePolicy.Fixed, ) self.setFont(font.font) @@ -99,11 +102,11 @@ class FontAndChartAwareLineEdit(QLineEdit): dpi_font = self.dpi_font psh.setHeight(dpi_font.px_size) - # space for ``._chars: int`` - char_w_pxs = dpi_font.boundingRect(self.text()).width() - chars_w = char_w_pxs + 6 # * dpi_font.scale() * self._chars - psh.setWidth(chars_w) - + # make space for ``._chars: int`` for of characters in view + # TODO: somehow this math ain't right? + chars_w_pxs = dpi_font.boundingRect('0'*self._chars).width() + scale = round(dpi_font.scale()) + psh.setWidth(chars_w_pxs * scale) return psh def set_width_in_chars( @@ -157,6 +160,130 @@ class FontScaledDelegate(QStyledItemDelegate): else: return super().sizeHint(option, index) + # NOTE: hack to display icons on RHS + # TODO: is there a way to set this stype option once? + # def paint(self, painter, option, index): + # # display icons on RHS + # # https://stackoverflow.com/a/39943629 + # option.decorationPosition = QtGui.QStyleOptionViewItem.Right + # option.decorationAlignment = Qt.AlignRight | Qt.AlignVCenter + # QStyledItemDelegate.paint(self, painter, option, index) + + +class Selection(QComboBox): + + def __init__( + self, + parent=None, + + ) -> None: + + self._items: dict[str, int] = {} + super().__init__(parent=parent) + self.setSizeAdjustPolicy(QComboBox.AdjustToContents) + # make line edit expand to surrounding frame + self.setSizePolicy( + QSizePolicy.Expanding, + QSizePolicy.Fixed, + ) + view = self.view() + view.setUniformItemSizes(True) + + # TODO: this doesn't seem to work for the currently selected item? + self.setItemDelegate(FontScaledDelegate(self)) + + self.resize() + + self._icons = mk_icons( + self.style(), + self.iconSize() + ) + + def set_style( + self, + + color: str, + font_size: int, + + ) -> None: + + self.setStyleSheet( + f"""QComboBox {{ + color : {hcolor(color)}; + font-size : {font_size}px; + }} + """ + ) + + def resize( + self, + char: str = 'W', + ) -> None: + br = _font.boundingRect(str(char)) + _, h = br.width(), br.height() + + # TODO: something better then this monkey patch + view = self.view() + + # XXX: see size policy settings of line edit + # view._max_item_size = w, h + + self.setMinimumHeight(h) # at least one entry in view + view.setMaximumHeight(6*h) # limit to 6 items max in view + + icon_size = round(h * 0.75) + self.setIconSize(QSize(icon_size, icon_size)) + + def set_items( + self, + keys: list[str], + + ) -> None: + '''Write keys to the selection verbatim. + + All other items are cleared beforehand. + ''' + self.clear() + self._items.clear() + + for i, key in enumerate(keys): + strkey = str(key) + self.insertItem(i, strkey) + + # store map of entry keys to row indexes + self._items[strkey] = i + + # compute max item size so that the weird + # "style item delegate" thing can then specify + # that size on each item... + keys.sort() + self.resize(keys[-1]) + + def set_icon( + self, + key: str, + icon_name: Optional[str], + + ) -> None: + self.setItemIcon( + self._items[key], + self._icons[icon_name], + ) + + def items(self) -> list[(str, int)]: + return list(self._items.items()) + + # NOTE: in theory we can put icons on the RHS side with this hackery: + # https://stackoverflow.com/a/64256969 + # def showPopup(self): + # print('show') + # QComboBox.showPopup(self) + + # def hidePopup(self): + # # self.setItemDelegate(FontScaledDelegate(self.parent())) + # print('hide') + # QComboBox.hidePopup(self) + # slew of resources which helped get this where it is: # https://stackoverflow.com/questions/20648210/qcombobox-adjusttocontents-changing-height @@ -164,7 +291,6 @@ class FontScaledDelegate(QStyledItemDelegate): # https://stackoverflow.com/questions/6337589/qlistwidget-adjust-size-to-content#6370892 # https://stackoverflow.com/questions/25304267/qt-resize-of-qlistview # https://stackoverflow.com/questions/28227406/how-to-set-qlistview-rows-height-permanently - class FieldsForm(QWidget): vbox: QVBoxLayout @@ -252,6 +378,7 @@ class FieldsForm(QWidget): edit = FontAndChartAwareLineEdit( parent=self, + # width_in_chars=6, ) edit.setStyleSheet( f"""QLineEdit {{ @@ -274,56 +401,23 @@ class FieldsForm(QWidget): label_name: str, values: list[str], - ) -> QComboBox: + ) -> Selection: # TODO: maybe a distint layout per "field" item? label = self.add_field_label(label_name) - select = QComboBox(self) + select = Selection(self) + select.set_style(color='gunmetal', font_size=self._font_size) select._key = key + select.set_items(values) - for i, value in enumerate(values): - select.insertItem(i, str(value)) - - select.setStyleSheet( - f"""QComboBox {{ - color : {hcolor('gunmetal')}; - font-size : {self._font_size}px; - }} - """ - ) - select.setSizeAdjustPolicy(QComboBox.AdjustToContents) - select.setIconSize(QSize(0, 0)) self.setSizePolicy( QSizePolicy.Fixed, QSizePolicy.Fixed, ) - view = select.view() - view.setUniformItemSizes(True) - view.setItemDelegate(FontScaledDelegate(view)) - - # compute maximum item size so that the weird - # "style item delegate" thing can then specify - # that size on each item... - values.sort() - br = _font.boundingRect(str(values[-1])) - _, h = br.width(), br.height() - - # TODO: something better then this monkey patch - # view._max_item_size = w, h - - # limit to 6 items? - view.setMaximumHeight(6*h) - - # one entry in view - select.setMinimumHeight(h) - select.show() - self.form.addRow(label, select) - self.fields[key] = select - return select @@ -631,9 +725,7 @@ def mk_order_pane_layout( ) -> FieldsForm: - # font_size: int = _font_small.px_size - 2 - font_size: int = _font.px_size - 2 - accounts = config.load_accounts() + font_size: int = _font.px_size - 1 # TODO: maybe just allocate the whole fields form here # and expect an async ctx entry? @@ -643,7 +735,7 @@ def mk_order_pane_layout( 'account': { 'label': '**account**:', 'type': 'select', - 'default_value': accounts.keys(), + 'default_value': ['paper'], }, 'size_unit': { 'label': '**allocate**:', @@ -685,7 +777,6 @@ def mk_order_pane_layout( form, pane_vbox=vbox, label_font_size=font_size, - ) # TODO: would be nice to have some better way of reffing these over # monkey patching... diff --git a/piker/ui/_icons.py b/piker/ui/_icons.py new file mode 100644 index 00000000..feb0cbb6 --- /dev/null +++ b/piker/ui/_icons.py @@ -0,0 +1,90 @@ +# piker: trading gear for hackers +# Copyright (C) Tyler Goodlet (in stewardship for pikers) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +''' +``QIcon`` hackery. + +''' +from PyQt5.QtWidgets import QStyle +from PyQt5.QtGui import ( + QIcon, QPixmap, QColor +) +from PyQt5.QtCore import QSize + +from ._style import hcolor + +# https://www.pythonguis.com/faq/built-in-qicons-pyqt/ +# account status icons taken from built-in set +_icon_names: dict[str, str] = { + # these two seem to work for our mask hack done below to + # change the coloring. + 'long_pp': 'SP_TitleBarShadeButton', + 'short_pp': 'SP_TitleBarUnshadeButton', + 'ready': 'SP_MediaPlay', +} +_icons: dict[str, QIcon] = {} + + +def mk_icons( + + style: QStyle, + size: QSize, + +) -> dict[str, QIcon]: + '''This helper is indempotent. + + ''' + global _icons, _icon_names + if _icons: + return _icons + + _icons[None] = QIcon() # the "null" icon + + # load account selection using current style + for name, icon_name in _icon_names.items(): + + stdpixmap = getattr(QStyle, icon_name) + stdicon = style.standardIcon(stdpixmap) + pixmap = stdicon.pixmap(size) + + # fill hack from SO to change icon color: + # https://stackoverflow.com/a/38369468 + out_pixmap = QPixmap(size) + out_pixmap.fill(QColor(hcolor('default_spotlight'))) + out_pixmap.setMask(pixmap.createHeuristicMask()) + + # TODO: not idea why this doesn't work / looks like + # trash. Sure would be nice to just generate our own + # pixmaps on the fly.. + # p = QPainter(out_pixmap) + # p.setOpacity(1) + # p.setBrush(QColor(hcolor('papas_special'))) + # p.setPen(QColor(hcolor('default_lightest'))) + # path = mk_marker_path(style='|<') + # p.scale(6, 6) + # # p.translate(0, 0) + # p.drawPath(path) + # p.save() + # p.end() + # del p + # icon = QIcon(out_pixmap) + + icon = QIcon() + icon.addPixmap(out_pixmap) + + _icons[name] = icon + + return _icons diff --git a/piker/ui/_interaction.py b/piker/ui/_interaction.py index d33d553e..97222065 100644 --- a/piker/ui/_interaction.py +++ b/piker/ui/_interaction.py @@ -270,7 +270,7 @@ async def handle_viewmode_kb_inputs( edit.selectAll() # un-highlight on ctrl release on_next_release = edit.deselect - pp_pane.update_status_ui() + pp_pane.update_status_ui(pp_pane.order_mode.current_pp) else: # none active diff --git a/piker/ui/_position.py b/piker/ui/_position.py index 07f8c76e..ab6fc0b6 100644 --- a/piker/ui/_position.py +++ b/piker/ui/_position.py @@ -25,6 +25,10 @@ from math import floor, copysign from typing import Optional +# from PyQt5.QtWidgets import QStyle +# from PyQt5.QtGui import ( +# QIcon, QPixmap, QColor +# ) from pyqtgraph import functions as fn from ._annotate import LevelMarker @@ -46,7 +50,7 @@ log = get_logger(__name__) _pnl_tasks: dict[str, bool] = {} -async def display_pnl( +async def update_pnl_from_feed( feed: Feed, order_mode: OrderMode, # noqa @@ -63,6 +67,8 @@ async def display_pnl( live = pp.live_pp key = live.symbol.key + log.info(f'Starting pnl display for {pp.alloc.account}') + if live.size < 0: types = ('ask', 'last', 'last', 'utrade') @@ -128,9 +134,17 @@ class SettingsPane: # encompasing high level namespace order_mode: Optional['OrderMode'] = None # typing: ignore # noqa + def set_accounts( + self, + names: list[str], + sizes: Optional[list[float]] = None, + ) -> None: + + combo = self.form.fields['account'] + return combo.set_items(names) + def on_selection_change( self, - text: str, key: str, @@ -173,11 +187,11 @@ class SettingsPane: f'Account `{account_name}` can not be set for {sym}' ) self.form.fields['account'].setCurrentText( - old_tracker.alloc.account_name()) + old_tracker.alloc.account) return self.order_mode.current_pp = tracker - assert tracker.alloc.account_name() == account_name + assert tracker.alloc.account == account_name self.form.fields['account'].setCurrentText(account_name) tracker.show() tracker.hide_info() @@ -206,7 +220,7 @@ class SettingsPane: elif key == 'size_unit': # TODO: if there's a limit size unit change re-compute # the current settings in the new units - pass + alloc.size_unit = value elif key != 'account': raise ValueError(f'Unknown setting {key}') @@ -266,12 +280,32 @@ class SettingsPane: # min(round(prop * slots), slots) min(used, slots) ) + self.update_account_icons({alloc.account: pp.live_pp}) + + def update_account_icons( + self, + pps: dict[str, Position], + + ) -> None: + + form = self.form + accounts = form.fields['account'] + + for account_name, pp in pps.items(): + icon_name = None + + if pp.size > 0: + icon_name = 'long_pp' + elif pp.size < 0: + icon_name = 'short_pp' + + accounts.set_icon(account_name, icon_name) def display_pnl( self, tracker: PositionTracker, - ) -> bool: + ) -> None: '''Display the PnL for the current symbol and personal positioning (pp). If a position is open start a background task which will @@ -282,36 +316,28 @@ class SettingsPane: sym = mode.chart.linked.symbol size = tracker.live_pp.size feed = mode.quote_feed - global _pnl_tasks + pnl_value = 0 - if ( - size and - sym.key not in _pnl_tasks - ): - _pnl_tasks[sym.key] = True - - # immediately compute and display pnl status from last quote - self.pnl_label.format( - pnl=copysign(1, size) * pnl( - tracker.live_pp.avg_price, - # last historical close price - feed.shm.array[-1][['close']][0], - ), + if size: + # last historical close price + last = feed.shm.array[-1][['close']][0] + pnl_value = copysign(1, size) * pnl( + tracker.live_pp.avg_price, + last, ) - log.info( - f'Starting pnl display for {tracker.alloc.account_name()}') - self.order_mode.nursery.start_soon( - display_pnl, - feed, - mode, - ) - return True + # maybe start update task + global _pnl_tasks + if sym.key not in _pnl_tasks: + _pnl_tasks[sym.key] = True + self.order_mode.nursery.start_soon( + update_pnl_from_feed, + feed, + mode, + ) - else: - # set 0% pnl - self.pnl_label.format(pnl=0) - return False + # immediately display in status label + self.pnl_label.format(pnl=pnl_value) def position_line( @@ -622,7 +648,7 @@ class PositionTracker: 'fiat_size': round(price * size, ndigits=2), # TODO: per account lines on a single (or very related) symbol - 'account': self.alloc.account_name(), + 'account': self.alloc.account, }) line.show() diff --git a/piker/ui/_search.py b/piker/ui/_search.py index 98ed9d0b..48561a24 100644 --- a/piker/ui/_search.py +++ b/piker/ui/_search.py @@ -118,7 +118,7 @@ class CompleterView(QTreeView): self.setModel(model) self.setAlternatingRowColors(True) # TODO: size this based on DPI font - self.setIndentation(20) + self.setIndentation(_font.px_size) # self.setUniformRowHeights(True) # self.setColumnWidth(0, 3) diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index 40f8a3a3..69a627fa 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -222,7 +222,7 @@ class OrderMode: order = self._staged_order = Order( action=action, price=price, - account=self.current_pp.alloc.account_name(), + account=self.current_pp.alloc.account, size=0, symbol=symbol, brokers=symbol.brokers, @@ -536,7 +536,8 @@ async def open_order_mode( open_ems(brokername, symbol) as ( book, trades_stream, - position_msgs + position_msgs, + brokerd_accounts, ), trio.open_nursery() as tn, @@ -549,9 +550,6 @@ async def open_order_mode( lines = LineEditor(chart=chart) arrows = ArrowEditor(chart, {}) - # allocation and account settings side pane - form = chart.sidepane - # symbol id symbol = chart.linked.symbol symkey = symbol.key @@ -560,27 +558,27 @@ async def open_order_mode( trackers: dict[str, PositionTracker] = {} # load account names from ``brokers.toml`` - accounts = config.load_accounts(providers=symbol.brokers).copy() - 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' + accounts_def = config.load_accounts( + providers=symbol.brokers + ) + + # use only loaded accounts according to brokerd + accounts = {} + for name in brokerd_accounts: + # ensure name is in ``brokers.toml`` + accounts[name] = accounts_def[name] + + # first account listed is the one we select at startup + # (aka order based selection). + pp_account = next(iter(accounts.keys())) if accounts else 'paper' # NOTE: requires the backend exactly specifies # the expected symbol key in its positions msg. pp_msgs = position_msgs.get(symkey, ()) + pps_by_account = {msg['account']: msg for msg in pp_msgs} - # 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 = msg.get('account') - account_value = accounts.get(account_name) - if not account_name and account_value == 'paper': - account_name = 'paper' + # update pp trackers with data relayed from ``brokerd``. + for account_name in accounts: # net-zero pp startup_pp = Position( @@ -588,12 +586,14 @@ async def open_order_mode( size=0, avg_price=0, ) - startup_pp.update_from_msg(msg) + msg = pps_by_account.get(account_name) + if msg: + log.info(f'Loading pp for {symkey}:\n{pformat(msg)}') + startup_pp.update_from_msg(msg) # allocator alloc = mk_allocator( symbol=symbol, - accounts=accounts, account=account_name, # if this startup size is greater the allocator limit, @@ -621,31 +621,9 @@ async def open_order_mode( pp_tracker.show() pp_tracker.hide_info() - # fill out trackers for accounts with net-zero pps - zero_pp_accounts = set(accounts) - set(trackers) - for account_name in zero_pp_accounts: - startup_pp = Position( - symbol=symbol, - size=0, - avg_price=0, - ) + # setup order mode sidepane widgets + form = chart.sidepane - # allocator - alloc = mk_allocator( - symbol=symbol, - accounts=accounts, - account=account_name, - startup_pp=startup_pp, - ) - pp_tracker = PositionTracker( - chart, - alloc, - startup_pp - ) - pp_tracker.hide() - trackers[account_name] = pp_tracker - - # order pane widgets and allocation model order_pane = SettingsPane( form=form, # XXX: ugh, so hideous... @@ -654,6 +632,11 @@ async def open_order_mode( step_label=form.bottom_label, limit_label=form.top_label, ) + order_pane.set_accounts(list(trackers.keys())) + + # update pp icons + for name, tracker in trackers.items(): + order_pane.update_account_icons({name: tracker.live_pp}) # top level abstraction which wraps all this crazyness into # a namespace..