From d08886dcebfc623e867db5b879efe018cf417e80 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 12 Sep 2021 19:29:42 -0400 Subject: [PATCH 01/18] Try to set icons on RHS, store combo box entries in map --- piker/ui/_forms.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/piker/ui/_forms.py b/piker/ui/_forms.py index 9e7e40ea..a97ae762 100644 --- a/piker/ui/_forms.py +++ b/piker/ui/_forms.py @@ -157,6 +157,13 @@ class FontScaledDelegate(QStyledItemDelegate): else: return super().sizeHint(option, index) + # 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 + super().paint(painter, option, index) + # slew of resources which helped get this where it is: # https://stackoverflow.com/questions/20648210/qcombobox-adjusttocontents-changing-height @@ -280,10 +287,16 @@ class FieldsForm(QWidget): label = self.add_field_label(label_name) select = QComboBox(self) + select._key = key + select._items: dict[str, int] = {} for i, value in enumerate(values): - select.insertItem(i, str(value)) + strkey = str(value) + select.insertItem(i, strkey) + + # store map of entry keys to row indexes + select._items[strkey] = i select.setStyleSheet( f"""QComboBox {{ @@ -293,14 +306,15 @@ class FieldsForm(QWidget): """ ) 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)) + # TODO: this doesn't seem to work for the currently selected item? + select.setItemDelegate(FontScaledDelegate(self)) # compute maximum item size so that the weird # "style item delegate" thing can then specify @@ -309,6 +323,8 @@ class FieldsForm(QWidget): br = _font.boundingRect(str(values[-1])) _, h = br.width(), br.height() + select.setIconSize(QSize(h, h)) + # TODO: something better then this monkey patch # view._max_item_size = w, h From bbcdb88263051a8a819fd39db48db12c8dd5de5e Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 12 Sep 2021 19:35:36 -0400 Subject: [PATCH 02/18] Add account icon setter method --- piker/ui/_position.py | 52 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/piker/ui/_position.py b/piker/ui/_position.py index 07f8c76e..71c850af 100644 --- a/piker/ui/_position.py +++ b/piker/ui/_position.py @@ -25,6 +25,8 @@ from math import floor, copysign from typing import Optional +from PyQt5.QtWidgets import QStyle +from PyQt5.QtGui import QIcon from pyqtgraph import functions as fn from ._annotate import LevelMarker @@ -109,6 +111,19 @@ async def display_pnl( assert _pnl_tasks.pop(key) +# https://www.pythonguis.com/faq/built-in-qicons-pyqt/ +# account status icons taken from built-in set +_icon_names: dict[str, str] = { + # 'connected': 'SP_DialogYesButton', + 'unavailable': 'SP_DockWidgetCloseButton', + 'long_pp': 'SP_TitleBarShadeButton', + 'short_pp': 'SP_TitleBarUnshadeButton', + 'ready': 'SP_MediaPlay', +} +_icons: dict[str, QIcon] = {} + + + @dataclass class SettingsPane: '''Composite set of widgets plus an allocator model for configuring @@ -128,6 +143,41 @@ class SettingsPane: # encompasing high level namespace order_mode: Optional['OrderMode'] = None # typing: ignore # noqa + def update_accounts_icon( + self, + status: str, # one of the values in ``_icons`` above + keys: Optional[list[str]] = None, + + ) -> None: + + form = self.form + + global _icons, _icon_name + if not _icons: + # load account selection using current style + for name, icon_name in _icon_names.items(): + pixmap = getattr(QStyle, icon_name) + + # TODO: set color to our default pp color + # https://stackoverflow.com/questions/13350631/simple-color-fill-qicons-in-qt + # mask = pixmap.createMaskFromColor( + # QColor('transparent'), + # Qt.MaskOutColor + # ) + # pixmap.fill((QColor(hcolor('default_light')))) + # pixmap.setMask(mask) + + _icons[name] = form.style().standardIcon(pixmap) + + acct_select = form.fields['account'] + keys = list(keys or acct_select._items.keys()) + + # breakpoint() + for key in keys: + i = acct_select._items[key] + icon = _icons[status] + acct_select.setItemIcon(i, icon) + def on_selection_change( self, @@ -206,7 +256,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}') From 4f9827c070a5b2cd1362ba581af852fae1c18817 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 12 Sep 2021 19:36:14 -0400 Subject: [PATCH 03/18] Try out account icons from order mode --- piker/ui/order_mode.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index 40f8a3a3..d8b862a3 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -549,9 +549,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 @@ -645,7 +642,9 @@ async def open_order_mode( pp_tracker.hide() trackers[account_name] = pp_tracker - # order pane widgets and allocation model + # setup order mode sidepane widgets + form = chart.sidepane + order_pane = SettingsPane( form=form, # XXX: ugh, so hideous... @@ -654,6 +653,17 @@ async def open_order_mode( step_label=form.bottom_label, limit_label=form.top_label, ) + # set all entries as unavailable at startup and then fill out + # positions and ready icons + # order_pane.update_accounts_icon('unavailable') + + for name, tracker in trackers.items(): + if tracker.live_pp.size > 0: + order_pane.update_accounts_icon('long_pp', [name]) + elif tracker.live_pp.size < 0: + order_pane.update_accounts_icon('short_pp', [name]) + # else: + # order_pane.update_accounts_icon('ready', [name]) # top level abstraction which wraps all this crazyness into # a namespace.. From 6ac092d618c5d02bba9ec82c6321ac1112cca9ea Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 12 Sep 2021 19:36:50 -0400 Subject: [PATCH 04/18] Scale search results indent to font size --- piker/ui/_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 3de4b9afbbf2efb9d13d42e75cb7a78661ae5799 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 13 Sep 2021 17:39:19 -0400 Subject: [PATCH 05/18] Scale down icons size, add RHS icons theory code --- piker/ui/_forms.py | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/piker/ui/_forms.py b/piker/ui/_forms.py index a97ae762..3d0b2398 100644 --- a/piker/ui/_forms.py +++ b/piker/ui/_forms.py @@ -157,12 +157,33 @@ 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 - super().paint(painter, option, index) + # 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) + + +# NOTE: in theory we can put icons on the RHS side with this hackery: +# https://stackoverflow.com/a/64256969 +# class ComboBox(QComboBox): +# def __init__( +# self, +# parent=None, +# ) -> None: +# super().__init__(parent=parent) + +# 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: @@ -171,7 +192,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 @@ -287,7 +307,6 @@ class FieldsForm(QWidget): label = self.add_field_label(label_name) select = QComboBox(self) - select._key = key select._items: dict[str, int] = {} @@ -313,6 +332,7 @@ class FieldsForm(QWidget): ) view = select.view() view.setUniformItemSizes(True) + # TODO: this doesn't seem to work for the currently selected item? select.setItemDelegate(FontScaledDelegate(self)) @@ -323,7 +343,8 @@ class FieldsForm(QWidget): br = _font.boundingRect(str(values[-1])) _, h = br.width(), br.height() - select.setIconSize(QSize(h, h)) + icon_size = round(h * 0.75) + select.setIconSize(QSize(icon_size, icon_size)) # TODO: something better then this monkey patch # view._max_item_size = w, h @@ -647,8 +668,7 @@ def mk_order_pane_layout( ) -> FieldsForm: - # font_size: int = _font_small.px_size - 2 - font_size: int = _font.px_size - 2 + font_size: int = _font.px_size - 1 accounts = config.load_accounts() # TODO: maybe just allocate the whole fields form here From 66199bfe6f931fad4ef39422ceb729a4f320f6b9 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 13 Sep 2021 17:40:14 -0400 Subject: [PATCH 06/18] Implement the pixmap mask hack for long/short pp icons --- piker/ui/_position.py | 52 ++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/piker/ui/_position.py b/piker/ui/_position.py index 71c850af..608ed402 100644 --- a/piker/ui/_position.py +++ b/piker/ui/_position.py @@ -26,7 +26,9 @@ from typing import Optional from PyQt5.QtWidgets import QStyle -from PyQt5.QtGui import QIcon +from PyQt5.QtGui import ( + QIcon, QPixmap, QColor +) from pyqtgraph import functions as fn from ._annotate import LevelMarker @@ -40,7 +42,7 @@ from ..data._normalize import iterticks from ..data.feed import Feed from ._label import Label from ._lines import LevelLine, order_line -from ._style import _font +from ._style import _font, hcolor from ._forms import FieldsForm, FillStatusBar, QLabel from ..log import get_logger @@ -114,8 +116,8 @@ async def display_pnl( # https://www.pythonguis.com/faq/built-in-qicons-pyqt/ # account status icons taken from built-in set _icon_names: dict[str, str] = { - # 'connected': 'SP_DialogYesButton', - 'unavailable': 'SP_DockWidgetCloseButton', + # 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', @@ -123,7 +125,6 @@ _icon_names: dict[str, str] = { _icons: dict[str, QIcon] = {} - @dataclass class SettingsPane: '''Composite set of widgets plus an allocator model for configuring @@ -156,18 +157,39 @@ class SettingsPane: if not _icons: # load account selection using current style for name, icon_name in _icon_names.items(): - pixmap = getattr(QStyle, icon_name) - # TODO: set color to our default pp color - # https://stackoverflow.com/questions/13350631/simple-color-fill-qicons-in-qt - # mask = pixmap.createMaskFromColor( - # QColor('transparent'), - # Qt.MaskOutColor - # ) - # pixmap.fill((QColor(hcolor('default_light')))) - # pixmap.setMask(mask) + stdpixmap = getattr(QStyle, icon_name) + stdicon = form.style().standardIcon(stdpixmap) + combo = form.fields['account'] + size = combo.iconSize() + pixmap = stdicon.pixmap(combo.iconSize()) - _icons[name] = form.style().standardIcon(pixmap) + # 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 acct_select = form.fields['account'] keys = list(keys or acct_select._items.keys()) From 335e72bf32b65e7ac321c9c8f2b6df2b27d081a8 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 13 Sep 2021 18:40:12 -0400 Subject: [PATCH 07/18] Move icons generatino to new module --- piker/ui/_icons.py | 88 +++++++++++++++++++++++++++++++++++++++++++ piker/ui/_position.py | 70 ++++++---------------------------- 2 files changed, 99 insertions(+), 59 deletions(-) create mode 100644 piker/ui/_icons.py diff --git a/piker/ui/_icons.py b/piker/ui/_icons.py new file mode 100644 index 00000000..57947251 --- /dev/null +++ b/piker/ui/_icons.py @@ -0,0 +1,88 @@ +# 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 + + # 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/_position.py b/piker/ui/_position.py index 608ed402..f9ee4e2b 100644 --- a/piker/ui/_position.py +++ b/piker/ui/_position.py @@ -25,10 +25,10 @@ from math import floor, copysign from typing import Optional -from PyQt5.QtWidgets import QStyle -from PyQt5.QtGui import ( - QIcon, QPixmap, QColor -) +# from PyQt5.QtWidgets import QStyle +# from PyQt5.QtGui import ( +# QIcon, QPixmap, QColor +# ) from pyqtgraph import functions as fn from ._annotate import LevelMarker @@ -42,7 +42,8 @@ from ..data._normalize import iterticks from ..data.feed import Feed from ._label import Label from ._lines import LevelLine, order_line -from ._style import _font, hcolor +from ._icons import mk_icons +from ._style import _font from ._forms import FieldsForm, FillStatusBar, QLabel from ..log import get_logger @@ -113,18 +114,6 @@ async def display_pnl( assert _pnl_tasks.pop(key) -# 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] = {} - - @dataclass class SettingsPane: '''Composite set of widgets plus an allocator model for configuring @@ -152,52 +141,15 @@ class SettingsPane: ) -> None: form = self.form - - global _icons, _icon_name - if not _icons: - # load account selection using current style - for name, icon_name in _icon_names.items(): - - stdpixmap = getattr(QStyle, icon_name) - stdicon = form.style().standardIcon(stdpixmap) - combo = form.fields['account'] - size = combo.iconSize() - pixmap = stdicon.pixmap(combo.iconSize()) - - # 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 - + icons = mk_icons( + form.style(), + form.fields['account'].iconSize() + ) acct_select = form.fields['account'] keys = list(keys or acct_select._items.keys()) - - # breakpoint() for key in keys: i = acct_select._items[key] - icon = _icons[status] + icon = icons[status] acct_select.setItemIcon(i, icon) def on_selection_change( From 6a31c4e160f74bbb6454094413ac9e8774b8b341 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 13 Sep 2021 19:08:30 -0400 Subject: [PATCH 08/18] Fix missing tracker to ui update call --- piker/ui/_interaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 75e1bf3f6e82dda6d95cf4671cdbdf87a6fe8e47 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 14 Sep 2021 10:34:36 -0400 Subject: [PATCH 09/18] Factor combobox logic into a new `Selection` subtype --- piker/ui/_forms.py | 176 ++++++++++++++++++++++++++------------------- 1 file changed, 102 insertions(+), 74 deletions(-) diff --git a/piker/ui/_forms.py b/piker/ui/_forms.py index 3d0b2398..cf324ca5 100644 --- a/piker/ui/_forms.py +++ b/piker/ui/_forms.py @@ -42,12 +42,10 @@ from PyQt5.QtWidgets import ( QStyledItemDelegate, QStyleOptionViewItem, ) -# import pydantic from ._event import open_handlers from ._style import hcolor, _font, _font_small, DpiAwareFont from ._label import FormatLabel -from .. import config class FontAndChartAwareLineEdit(QLineEdit): @@ -71,17 +69,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 +101,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( @@ -167,23 +169,93 @@ class FontScaledDelegate(QStyledItemDelegate): # QStyledItemDelegate.paint(self, painter, option, index) -# NOTE: in theory we can put icons on the RHS side with this hackery: -# https://stackoverflow.com/a/64256969 -# class ComboBox(QComboBox): -# def __init__( -# self, -# parent=None, -# ) -> None: -# super().__init__(parent=parent) +class Selection(QComboBox): -# def showPopup(self): -# print('show') -# QComboBox.showPopup(self) + def __init__( + self, + parent=None, -# def hidePopup(self): -# # self.setItemDelegate(FontScaledDelegate(self.parent())) -# print('hide') -# QComboBox.hidePopup(self) + ) -> 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)) + + def set_style( + self, + + color: str, + font_size: int, + + ) -> None: + + self.setStyleSheet( + f"""QComboBox {{ + color : {hcolor(color)}; + font-size : {font_size}px; + }} + """ + ) + + 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() + br = _font.boundingRect(str(keys[-1])) + _, 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)) + + # # 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: @@ -279,6 +351,7 @@ class FieldsForm(QWidget): edit = FontAndChartAwareLineEdit( parent=self, + # width_in_chars=6, ) edit.setStyleSheet( f"""QLineEdit {{ @@ -301,66 +374,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._items: dict[str, int] = {} - - for i, value in enumerate(values): - strkey = str(value) - select.insertItem(i, strkey) - - # store map of entry keys to row indexes - select._items[strkey] = i - - select.setStyleSheet( - f"""QComboBox {{ - color : {hcolor('gunmetal')}; - font-size : {self._font_size}px; - }} - """ - ) - select.setSizeAdjustPolicy(QComboBox.AdjustToContents) + select.set_items(values) self.setSizePolicy( QSizePolicy.Fixed, QSizePolicy.Fixed, ) - view = select.view() - view.setUniformItemSizes(True) - - # TODO: this doesn't seem to work for the currently selected item? - select.setItemDelegate(FontScaledDelegate(self)) - - # 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() - - icon_size = round(h * 0.75) - select.setIconSize(QSize(icon_size, icon_size)) - - # 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 @@ -669,7 +699,6 @@ def mk_order_pane_layout( ) -> FieldsForm: font_size: int = _font.px_size - 1 - accounts = config.load_accounts() # TODO: maybe just allocate the whole fields form here # and expect an async ctx entry? @@ -679,7 +708,7 @@ def mk_order_pane_layout( 'account': { 'label': '**account**:', 'type': 'select', - 'default_value': accounts.keys(), + 'default_value': ['paper'], }, 'size_unit': { 'label': '**allocate**:', @@ -721,7 +750,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... From b04645aa47e7020629efd13c228217d181a35e90 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 14 Sep 2021 10:36:13 -0400 Subject: [PATCH 10/18] Expect `accounts: set[str]` startup msg through clearing system --- piker/brokers/ib.py | 4 +++- piker/clearing/_client.py | 4 ++-- piker/clearing/_ems.py | 41 ++++++++++++++++++++------------- piker/clearing/_paper_engine.py | 2 +- 4 files changed, 31 insertions(+), 20 deletions(-) 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/_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, From 9e15401ddc769325bed4f8c40d8e0a04d6031b14 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 14 Sep 2021 10:36:44 -0400 Subject: [PATCH 11/18] Add an accounts list setter --- piker/ui/_position.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/piker/ui/_position.py b/piker/ui/_position.py index f9ee4e2b..54ca8605 100644 --- a/piker/ui/_position.py +++ b/piker/ui/_position.py @@ -133,6 +133,15 @@ 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 update_accounts_icon( self, status: str, # one of the values in ``_icons`` above From be5a8e66d88e7ef2eb48a0bb629274d0ad215922 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 14 Sep 2021 10:37:30 -0400 Subject: [PATCH 12/18] Only show accounts reported from clearing sys --- piker/ui/order_mode.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index d8b862a3..3d9941d9 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -26,6 +26,7 @@ import time from typing import Optional, Dict, Callable, Any import uuid +from bidict import bidict from pydantic import BaseModel import tractor import trio @@ -536,7 +537,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, @@ -557,7 +559,15 @@ async def open_order_mode( trackers: dict[str, PositionTracker] = {} # load account names from ``brokers.toml`` - accounts = config.load_accounts(providers=symbol.brokers).copy() + accounts_def = config.load_accounts( + providers=symbol.brokers + ) + + # use only loaded accounts according to brokerd + accounts = bidict({}) + for name in brokerd_accounts: + accounts[name] = accounts_def[name] + if accounts: # first account listed is the one we select at startup # (aka order based selection). @@ -653,6 +663,8 @@ async def open_order_mode( step_label=form.bottom_label, limit_label=form.top_label, ) + order_pane.set_accounts(list(trackers.keys())) + # set all entries as unavailable at startup and then fill out # positions and ready icons # order_pane.update_accounts_icon('unavailable') @@ -662,8 +674,6 @@ async def open_order_mode( order_pane.update_accounts_icon('long_pp', [name]) elif tracker.live_pp.size < 0: order_pane.update_accounts_icon('short_pp', [name]) - # else: - # order_pane.update_accounts_icon('ready', [name]) # top level abstraction which wraps all this crazyness into # a namespace.. From 9e41dfb735a63a1494f5229e0785a1cad735451b Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 14 Sep 2021 12:25:30 -0400 Subject: [PATCH 13/18] Add an icon setter api to `Selection` --- piker/ui/_forms.py | 55 ++++++++++++++++++++++++++++++++++------------ piker/ui/_icons.py | 2 ++ 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/piker/ui/_forms.py b/piker/ui/_forms.py index cf324ca5..5aed6f9c 100644 --- a/piker/ui/_forms.py +++ b/piker/ui/_forms.py @@ -44,6 +44,7 @@ from PyQt5.QtWidgets import ( ) from ._event import open_handlers +from ._icons import mk_icons from ._style import hcolor, _font, _font_small, DpiAwareFont from ._label import FormatLabel @@ -178,7 +179,6 @@ class Selection(QComboBox): ) -> None: self._items: dict[str, int] = {} - super().__init__(parent=parent) self.setSizeAdjustPolicy(QComboBox.AdjustToContents) # make line edit expand to surrounding frame @@ -192,6 +192,13 @@ class Selection(QComboBox): # 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, @@ -208,6 +215,25 @@ class Selection(QComboBox): """ ) + 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], @@ -231,23 +257,24 @@ class Selection(QComboBox): # "style item delegate" thing can then specify # that size on each item... keys.sort() - br = _font.boundingRect(str(keys[-1])) - _, h = br.width(), br.height() + self.resize(keys[-1]) - # TODO: something better then this monkey patch - view = self.view() + def set_icon( + self, + key: str, + icon_name: Optional[str], - # XXX: see size policy settings of line edit - # view._max_item_size = w, h + ) -> None: + self.setItemIcon( + self._items[key], + self._icons[icon_name], + ) - self.setMinimumHeight(h) # at least one entry in view - view.setMaximumHeight(6*h) # limit to 6 items max in view + def items(self) -> list[(str, int)]: + return list(self._items.items()) - icon_size = round(h * 0.75) - self.setIconSize(QSize(icon_size, icon_size)) - - # # NOTE: in theory we can put icons on the RHS side with this hackery: - # # https://stackoverflow.com/a/64256969 + # 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) diff --git a/piker/ui/_icons.py b/piker/ui/_icons.py index 57947251..feb0cbb6 100644 --- a/piker/ui/_icons.py +++ b/piker/ui/_icons.py @@ -51,6 +51,8 @@ def mk_icons( if _icons: return _icons + _icons[None] = QIcon() # the "null" icon + # load account selection using current style for name, icon_name in _icon_names.items(): From 9c60aa19286929c6b92f75cf65f8f1faabe5b3d5 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 14 Sep 2021 12:26:06 -0400 Subject: [PATCH 14/18] Add account icon updater method to sidepane --- piker/ui/_position.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/piker/ui/_position.py b/piker/ui/_position.py index 54ca8605..8632effd 100644 --- a/piker/ui/_position.py +++ b/piker/ui/_position.py @@ -42,7 +42,6 @@ from ..data._normalize import iterticks from ..data.feed import Feed from ._label import Label from ._lines import LevelLine, order_line -from ._icons import mk_icons from ._style import _font from ._forms import FieldsForm, FillStatusBar, QLabel from ..log import get_logger @@ -142,28 +141,8 @@ class SettingsPane: combo = self.form.fields['account'] return combo.set_items(names) - def update_accounts_icon( - self, - status: str, # one of the values in ``_icons`` above - keys: Optional[list[str]] = None, - - ) -> None: - - form = self.form - icons = mk_icons( - form.style(), - form.fields['account'].iconSize() - ) - acct_select = form.fields['account'] - keys = list(keys or acct_select._items.keys()) - for key in keys: - i = acct_select._items[key] - icon = icons[status] - acct_select.setItemIcon(i, icon) - def on_selection_change( self, - text: str, key: str, @@ -299,6 +278,26 @@ class SettingsPane: # min(round(prop * slots), slots) min(used, slots) ) + self.update_account_icons({alloc.account_name(): 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, From 4afafce297ae09bc1482b382fcc5b49253b57e25 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 14 Sep 2021 12:26:26 -0400 Subject: [PATCH 15/18] Update icons from pps at order mode startup --- piker/ui/order_mode.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index 3d9941d9..7f169a22 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -665,15 +665,9 @@ async def open_order_mode( ) order_pane.set_accounts(list(trackers.keys())) - # set all entries as unavailable at startup and then fill out - # positions and ready icons - # order_pane.update_accounts_icon('unavailable') - + # update pp icons for name, tracker in trackers.items(): - if tracker.live_pp.size > 0: - order_pane.update_accounts_icon('long_pp', [name]) - elif tracker.live_pp.size < 0: - order_pane.update_accounts_icon('short_pp', [name]) + order_pane.update_account_icons({name: tracker.live_pp}) # top level abstraction which wraps all this crazyness into # a namespace.. From f4740da6a22589a4842220ad9bb8439023dc3f62 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 14 Sep 2021 13:10:39 -0400 Subject: [PATCH 16/18] Drop `.accounts` field from allocator --- piker/clearing/_allocate.py | 14 +------------- piker/ui/_position.py | 10 +++++----- piker/ui/order_mode.py | 4 +--- 3 files changed, 7 insertions(+), 21 deletions(-) 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/ui/_position.py b/piker/ui/_position.py index 8632effd..12cb3dfb 100644 --- a/piker/ui/_position.py +++ b/piker/ui/_position.py @@ -185,11 +185,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() @@ -278,7 +278,7 @@ class SettingsPane: # min(round(prop * slots), slots) min(used, slots) ) - self.update_account_icons({alloc.account_name(): pp.live_pp}) + self.update_account_icons({alloc.account: pp.live_pp}) def update_account_icons( self, @@ -332,7 +332,7 @@ class SettingsPane: ) log.info( - f'Starting pnl display for {tracker.alloc.account_name()}') + f'Starting pnl display for {tracker.alloc.account}') self.order_mode.nursery.start_soon( display_pnl, feed, @@ -654,7 +654,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/order_mode.py b/piker/ui/order_mode.py index 7f169a22..f7b5a836 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -223,7 +223,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, @@ -600,7 +600,6 @@ async def open_order_mode( # allocator alloc = mk_allocator( symbol=symbol, - accounts=accounts, account=account_name, # if this startup size is greater the allocator limit, @@ -640,7 +639,6 @@ async def open_order_mode( # allocator alloc = mk_allocator( symbol=symbol, - accounts=accounts, account=account_name, startup_pp=startup_pp, ) From 67de83afa97c691783468d1cb92b1a1abab29266 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 14 Sep 2021 14:26:15 -0400 Subject: [PATCH 17/18] Create all trackers in one pass of the accounts --- piker/ui/order_mode.py | 53 ++++++++++-------------------------------- 1 file changed, 12 insertions(+), 41 deletions(-) diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index f7b5a836..69a627fa 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -26,7 +26,6 @@ import time from typing import Optional, Dict, Callable, Any import uuid -from bidict import bidict from pydantic import BaseModel import tractor import trio @@ -564,30 +563,22 @@ async def open_order_mode( ) # use only loaded accounts according to brokerd - accounts = bidict({}) + accounts = {} for name in brokerd_accounts: + # ensure name is in ``brokers.toml`` accounts[name] = accounts_def[name] - 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' + # 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( @@ -595,7 +586,10 @@ 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( @@ -627,29 +621,6 @@ 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, - ) - - # allocator - alloc = mk_allocator( - symbol=symbol, - account=account_name, - startup_pp=startup_pp, - ) - pp_tracker = PositionTracker( - chart, - alloc, - startup_pp - ) - pp_tracker.hide() - trackers[account_name] = pp_tracker - # setup order mode sidepane widgets form = chart.sidepane From aa91055a16c25324e4171f2b456e709b04ef03a1 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 14 Sep 2021 18:31:49 -0400 Subject: [PATCH 18/18] Fix logic to display pnl in status label immediately --- piker/ui/_position.py | 50 +++++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/piker/ui/_position.py b/piker/ui/_position.py index 12cb3dfb..ab6fc0b6 100644 --- a/piker/ui/_position.py +++ b/piker/ui/_position.py @@ -50,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 @@ -67,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') @@ -303,7 +305,7 @@ class SettingsPane: 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 @@ -314,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}') - 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(