Merge pull request #226 from pikers/account_select_icons

Account select icons
fsp_feeds
goodboy 2021-09-15 11:36:16 -04:00 committed by GitHub
commit 3aadd49e07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 356 additions and 167 deletions

View File

@ -1469,6 +1469,7 @@ async def trades_dialogue(
# deliver positions to subscriber before anything else # deliver positions to subscriber before anything else
all_positions = [] all_positions = []
accounts = set()
clients: list[tuple[Client, trio.MemoryReceiveChannel]] = [] clients: list[tuple[Client, trio.MemoryReceiveChannel]] = []
for account, client in _accounts2clients.items(): for account, client in _accounts2clients.items():
@ -1484,9 +1485,10 @@ async def trades_dialogue(
for pos in client.positions(): for pos in client.positions():
msg = pack_position(pos) msg = pack_position(pos)
msg.account = accounts_def.inverse[msg.account] msg.account = accounts_def.inverse[msg.account]
accounts.add(msg.account)
all_positions.append(msg.dict()) all_positions.append(msg.dict())
await ctx.started(all_positions) await ctx.started((all_positions, accounts))
async with ( async with (
ctx.open_stream() as ems_stream, ctx.open_stream() as ems_stream,

View File

@ -86,14 +86,7 @@ class Allocator(BaseModel):
underscore_attrs_are_private = False underscore_attrs_are_private = False
symbol: Symbol symbol: Symbol
accounts: bidict[str, Optional[str]]
account: Optional[str] = 'paper' 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_unit: SizeUnit = 'currency'
_size_units: dict[str, Optional[str]] = _size_units _size_units: dict[str, Optional[str]] = _size_units
@ -128,9 +121,6 @@ class Allocator(BaseModel):
else: else:
return self.units_limit return self.units_limit
def account_name(self) -> str:
return self.accounts.inverse[self.account]
def next_order_info( def next_order_info(
self, self,
@ -234,7 +224,7 @@ class Allocator(BaseModel):
'slots_used': slots_used, 'slots_used': slots_used,
# update line LHS label with account name # update line LHS label with account name
'account': self.account_name(), 'account': self.account,
} }
def slots_used( def slots_used(
@ -264,7 +254,6 @@ class Allocator(BaseModel):
def mk_allocator( def mk_allocator(
symbol: Symbol, symbol: Symbol,
accounts: dict[str, str],
startup_pp: Position, startup_pp: Position,
# default allocation settings # default allocation settings
@ -293,7 +282,6 @@ def mk_allocator(
alloc = Allocator( alloc = Allocator(
symbol=symbol, symbol=symbol,
accounts=accounts,
**defaults, **defaults,
) )

View File

@ -210,7 +210,7 @@ async def open_ems(
broker=broker, broker=broker,
symbol=symbol.key, symbol=symbol.key,
) as (ctx, positions), ) as (ctx, (positions, accounts)),
# open 2-way trade command stream # open 2-way trade command stream
ctx.open_stream() as trades_stream, ctx.open_stream() as trades_stream,
@ -222,4 +222,4 @@ async def open_ems(
trades_stream trades_stream
) )
yield book, trades_stream, positions yield book, trades_stream, positions, accounts

View File

@ -268,6 +268,9 @@ class TradesRelay:
# map of symbols to dicts of accounts to pp msgs # map of symbols to dicts of accounts to pp msgs
positions: dict[str, dict[str, BrokerdPosition]] positions: dict[str, dict[str, BrokerdPosition]]
# allowed account names
accounts: set[str]
# count of connected ems clients for this ``brokerd`` # count of connected ems clients for this ``brokerd``
consumers: int = 0 consumers: int = 0
@ -410,8 +413,7 @@ async def open_brokerd_trades_dialogue(
try: try:
async with ( async with (
open_trades_endpoint as (brokerd_ctx, (positions, accounts,)),
open_trades_endpoint as (brokerd_ctx, positions),
brokerd_ctx.open_stream() as brokerd_trades_stream, 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. # locally cache and track positions per account.
pps = {} pps = {}
for msg in positions: for msg in positions:
account = msg['account']
assert account in accounts
pps.setdefault( pps.setdefault(
msg['symbol'], msg['symbol'],
{} {}
)[msg['account']] = msg )[account] = msg
relay = TradesRelay( relay = TradesRelay(
brokerd_dialogue=brokerd_trades_stream, brokerd_dialogue=brokerd_trades_stream,
positions=pps, positions=pps,
consumers=1 accounts=set(accounts),
consumers=1,
) )
_router.relays[broker] = relay _router.relays[broker] = relay
@ -936,11 +943,11 @@ async def _emsd_main(
) -> None: ) -> None:
'''EMS (sub)actor entrypoint providing the '''EMS (sub)actor entrypoint providing the
execution management (micro)service which conducts broker 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 This is the daemon (child) side routine which starts an EMS runtime
(one per broker-feed) and and begins streaming back alerts from task (one per broker-feed) and and begins streaming back alerts from
broker executions/fills. each broker's executions/fills.
``send_order_cmds()`` is called here to execute in a task back in ``send_order_cmds()`` is called here to execute in a task back in
the actor which started this service (spawned this actor), presuming the actor which started this service (spawned this actor), presuming
@ -964,8 +971,8 @@ async def _emsd_main(
reponse" proxy-broker. reponse" proxy-broker.
| |
- ``process_client_order_cmds()``: - ``process_client_order_cmds()``:
accepts order cmds from requesting piker clients, registers accepts order cmds from requesting clients, registers dark orders and
execs with exec loop alerts with clearing loop.
''' '''
global _router global _router
@ -1015,13 +1022,15 @@ async def _emsd_main(
brokerd_stream = relay.brokerd_dialogue # .clone() brokerd_stream = relay.brokerd_dialogue # .clone()
# signal to client that we're started # flatten out collected pps from brokerd for delivery
# TODO: we could eventually send back **all** brokerd pp_msgs = {
# positions here? sym: list(pps.values())
await ems_ctx.started( for sym, pps in relay.positions.items()
{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 # establish 2-way stream with requesting order-client and
# begin handling inbound order requests and updates # begin handling inbound order requests and updates

View File

@ -463,7 +463,7 @@ async def trades_dialogue(
# TODO: load paper positions per broker from .toml config file # TODO: load paper positions per broker from .toml config file
# and pass as symbol to position data mapping: ``dict[str, dict]`` # and pass as symbol to position data mapping: ``dict[str, dict]``
# await ctx.started(all_positions) # await ctx.started(all_positions)
await ctx.started({}) await ctx.started(({}, {'paper',}))
async with ( async with (
ctx.open_stream() as ems_stream, ctx.open_stream() as ems_stream,

View File

@ -42,12 +42,11 @@ from PyQt5.QtWidgets import (
QStyledItemDelegate, QStyledItemDelegate,
QStyleOptionViewItem, QStyleOptionViewItem,
) )
# import pydantic
from ._event import open_handlers from ._event import open_handlers
from ._icons import mk_icons
from ._style import hcolor, _font, _font_small, DpiAwareFont from ._style import hcolor, _font, _font_small, DpiAwareFont
from ._label import FormatLabel from ._label import FormatLabel
from .. import config
class FontAndChartAwareLineEdit(QLineEdit): class FontAndChartAwareLineEdit(QLineEdit):
@ -71,17 +70,21 @@ class FontAndChartAwareLineEdit(QLineEdit):
if width_in_chars: if width_in_chars:
self._chars = int(width_in_chars) self._chars = int(width_in_chars)
x_size_policy = QSizePolicy.Fixed
else: else:
# chart count which will be used to calculate # chart count which will be used to calculate
# width of input field. # 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) super().__init__(parent)
# size it as we specify # size it as we specify
# https://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum # https://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum
self.setSizePolicy( self.setSizePolicy(
QSizePolicy.Expanding, x_size_policy,
QSizePolicy.Fixed, QSizePolicy.Fixed,
) )
self.setFont(font.font) self.setFont(font.font)
@ -99,11 +102,11 @@ class FontAndChartAwareLineEdit(QLineEdit):
dpi_font = self.dpi_font dpi_font = self.dpi_font
psh.setHeight(dpi_font.px_size) psh.setHeight(dpi_font.px_size)
# space for ``._chars: int`` # make space for ``._chars: int`` for of characters in view
char_w_pxs = dpi_font.boundingRect(self.text()).width() # TODO: somehow this math ain't right?
chars_w = char_w_pxs + 6 # * dpi_font.scale() * self._chars chars_w_pxs = dpi_font.boundingRect('0'*self._chars).width()
psh.setWidth(chars_w) scale = round(dpi_font.scale())
psh.setWidth(chars_w_pxs * scale)
return psh return psh
def set_width_in_chars( def set_width_in_chars(
@ -157,6 +160,130 @@ class FontScaledDelegate(QStyledItemDelegate):
else: else:
return super().sizeHint(option, index) 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: # slew of resources which helped get this where it is:
# https://stackoverflow.com/questions/20648210/qcombobox-adjusttocontents-changing-height # 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/6337589/qlistwidget-adjust-size-to-content#6370892
# https://stackoverflow.com/questions/25304267/qt-resize-of-qlistview # https://stackoverflow.com/questions/25304267/qt-resize-of-qlistview
# https://stackoverflow.com/questions/28227406/how-to-set-qlistview-rows-height-permanently # https://stackoverflow.com/questions/28227406/how-to-set-qlistview-rows-height-permanently
class FieldsForm(QWidget): class FieldsForm(QWidget):
vbox: QVBoxLayout vbox: QVBoxLayout
@ -252,6 +378,7 @@ class FieldsForm(QWidget):
edit = FontAndChartAwareLineEdit( edit = FontAndChartAwareLineEdit(
parent=self, parent=self,
# width_in_chars=6,
) )
edit.setStyleSheet( edit.setStyleSheet(
f"""QLineEdit {{ f"""QLineEdit {{
@ -274,56 +401,23 @@ class FieldsForm(QWidget):
label_name: str, label_name: str,
values: list[str], values: list[str],
) -> QComboBox: ) -> Selection:
# TODO: maybe a distint layout per "field" item? # TODO: maybe a distint layout per "field" item?
label = self.add_field_label(label_name) 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._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( self.setSizePolicy(
QSizePolicy.Fixed, QSizePolicy.Fixed,
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() select.show()
self.form.addRow(label, select) self.form.addRow(label, select)
self.fields[key] = select self.fields[key] = select
return select return select
@ -631,9 +725,7 @@ def mk_order_pane_layout(
) -> FieldsForm: ) -> FieldsForm:
# font_size: int = _font_small.px_size - 2 font_size: int = _font.px_size - 1
font_size: int = _font.px_size - 2
accounts = config.load_accounts()
# TODO: maybe just allocate the whole fields form here # TODO: maybe just allocate the whole fields form here
# and expect an async ctx entry? # and expect an async ctx entry?
@ -643,7 +735,7 @@ def mk_order_pane_layout(
'account': { 'account': {
'label': '**account**:', 'label': '**account**:',
'type': 'select', 'type': 'select',
'default_value': accounts.keys(), 'default_value': ['paper'],
}, },
'size_unit': { 'size_unit': {
'label': '**allocate**:', 'label': '**allocate**:',
@ -685,7 +777,6 @@ def mk_order_pane_layout(
form, form,
pane_vbox=vbox, pane_vbox=vbox,
label_font_size=font_size, label_font_size=font_size,
) )
# TODO: would be nice to have some better way of reffing these over # TODO: would be nice to have some better way of reffing these over
# monkey patching... # monkey patching...

90
piker/ui/_icons.py 100644
View File

@ -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 <https://www.gnu.org/licenses/>.
'''
``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

View File

@ -270,7 +270,7 @@ async def handle_viewmode_kb_inputs(
edit.selectAll() edit.selectAll()
# un-highlight on ctrl release # 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(pp_pane.order_mode.current_pp)
else: # none active else: # none active

View File

@ -25,6 +25,10 @@ from math import floor, copysign
from typing import Optional from typing import Optional
# from PyQt5.QtWidgets import QStyle
# from PyQt5.QtGui import (
# QIcon, QPixmap, QColor
# )
from pyqtgraph import functions as fn from pyqtgraph import functions as fn
from ._annotate import LevelMarker from ._annotate import LevelMarker
@ -46,7 +50,7 @@ log = get_logger(__name__)
_pnl_tasks: dict[str, bool] = {} _pnl_tasks: dict[str, bool] = {}
async def display_pnl( async def update_pnl_from_feed(
feed: Feed, feed: Feed,
order_mode: OrderMode, # noqa order_mode: OrderMode, # noqa
@ -63,6 +67,8 @@ async def display_pnl(
live = pp.live_pp live = pp.live_pp
key = live.symbol.key key = live.symbol.key
log.info(f'Starting pnl display for {pp.alloc.account}')
if live.size < 0: if live.size < 0:
types = ('ask', 'last', 'last', 'utrade') types = ('ask', 'last', 'last', 'utrade')
@ -128,9 +134,17 @@ class SettingsPane:
# encompasing high level namespace # encompasing high level namespace
order_mode: Optional['OrderMode'] = None # typing: ignore # noqa 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( def on_selection_change(
self, self,
text: str, text: str,
key: str, key: str,
@ -173,11 +187,11 @@ class SettingsPane:
f'Account `{account_name}` can not be set for {sym}' f'Account `{account_name}` can not be set for {sym}'
) )
self.form.fields['account'].setCurrentText( self.form.fields['account'].setCurrentText(
old_tracker.alloc.account_name()) old_tracker.alloc.account)
return return
self.order_mode.current_pp = tracker 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) self.form.fields['account'].setCurrentText(account_name)
tracker.show() tracker.show()
tracker.hide_info() tracker.hide_info()
@ -206,7 +220,7 @@ class SettingsPane:
elif key == 'size_unit': elif key == 'size_unit':
# TODO: if there's a limit size unit change re-compute # TODO: if there's a limit size unit change re-compute
# the current settings in the new units # the current settings in the new units
pass alloc.size_unit = value
elif key != 'account': elif key != 'account':
raise ValueError(f'Unknown setting {key}') raise ValueError(f'Unknown setting {key}')
@ -266,12 +280,32 @@ class SettingsPane:
# min(round(prop * slots), slots) # min(round(prop * slots), slots)
min(used, 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( def display_pnl(
self, self,
tracker: PositionTracker, tracker: PositionTracker,
) -> bool: ) -> None:
'''Display the PnL for the current symbol and personal positioning (pp). '''Display the PnL for the current symbol and personal positioning (pp).
If a position is open start a background task which will If a position is open start a background task which will
@ -282,36 +316,28 @@ class SettingsPane:
sym = mode.chart.linked.symbol sym = mode.chart.linked.symbol
size = tracker.live_pp.size size = tracker.live_pp.size
feed = mode.quote_feed feed = mode.quote_feed
global _pnl_tasks pnl_value = 0
if ( if size:
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 # last historical close price
feed.shm.array[-1][['close']][0], last = feed.shm.array[-1][['close']][0]
), pnl_value = copysign(1, size) * pnl(
tracker.live_pp.avg_price,
last,
) )
log.info( # maybe start update task
f'Starting pnl display for {tracker.alloc.account_name()}') global _pnl_tasks
if sym.key not in _pnl_tasks:
_pnl_tasks[sym.key] = True
self.order_mode.nursery.start_soon( self.order_mode.nursery.start_soon(
display_pnl, update_pnl_from_feed,
feed, feed,
mode, mode,
) )
return True
else: # immediately display in status label
# set 0% pnl self.pnl_label.format(pnl=pnl_value)
self.pnl_label.format(pnl=0)
return False
def position_line( def position_line(
@ -622,7 +648,7 @@ class PositionTracker:
'fiat_size': round(price * size, ndigits=2), 'fiat_size': round(price * size, ndigits=2),
# TODO: per account lines on a single (or very related) symbol # TODO: per account lines on a single (or very related) symbol
'account': self.alloc.account_name(), 'account': self.alloc.account,
}) })
line.show() line.show()

View File

@ -118,7 +118,7 @@ class CompleterView(QTreeView):
self.setModel(model) self.setModel(model)
self.setAlternatingRowColors(True) self.setAlternatingRowColors(True)
# TODO: size this based on DPI font # TODO: size this based on DPI font
self.setIndentation(20) self.setIndentation(_font.px_size)
# self.setUniformRowHeights(True) # self.setUniformRowHeights(True)
# self.setColumnWidth(0, 3) # self.setColumnWidth(0, 3)

View File

@ -222,7 +222,7 @@ class OrderMode:
order = self._staged_order = Order( order = self._staged_order = Order(
action=action, action=action,
price=price, price=price,
account=self.current_pp.alloc.account_name(), account=self.current_pp.alloc.account,
size=0, size=0,
symbol=symbol, symbol=symbol,
brokers=symbol.brokers, brokers=symbol.brokers,
@ -536,7 +536,8 @@ async def open_order_mode(
open_ems(brokername, symbol) as ( open_ems(brokername, symbol) as (
book, book,
trades_stream, trades_stream,
position_msgs position_msgs,
brokerd_accounts,
), ),
trio.open_nursery() as tn, trio.open_nursery() as tn,
@ -549,9 +550,6 @@ 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
# symbol id # symbol id
symbol = chart.linked.symbol symbol = chart.linked.symbol
symkey = symbol.key symkey = symbol.key
@ -560,27 +558,27 @@ async def open_order_mode(
trackers: dict[str, PositionTracker] = {} trackers: dict[str, PositionTracker] = {}
# load account names from ``brokers.toml`` # load account names from ``brokers.toml``
accounts = config.load_accounts(providers=symbol.brokers).copy() accounts_def = config.load_accounts(
if 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 # first account listed is the one we select at startup
# (aka order based selection). # (aka order based selection).
pp_account = next(iter(accounts.keys())) pp_account = next(iter(accounts.keys())) if accounts else 'paper'
else:
pp_account = 'paper'
# NOTE: requires the backend exactly specifies # NOTE: requires the backend exactly specifies
# the expected symbol key in its positions msg. # the expected symbol key in its positions msg.
pp_msgs = position_msgs.get(symkey, ()) 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 # update pp trackers with data relayed from ``brokerd``.
# from ``brokerd``. for account_name in accounts:
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'
# net-zero pp # net-zero pp
startup_pp = Position( startup_pp = Position(
@ -588,12 +586,14 @@ async def open_order_mode(
size=0, size=0,
avg_price=0, avg_price=0,
) )
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) startup_pp.update_from_msg(msg)
# allocator # allocator
alloc = mk_allocator( alloc = mk_allocator(
symbol=symbol, symbol=symbol,
accounts=accounts,
account=account_name, account=account_name,
# if this startup size is greater the allocator limit, # if this startup size is greater the allocator limit,
@ -621,31 +621,9 @@ async def open_order_mode(
pp_tracker.show() pp_tracker.show()
pp_tracker.hide_info() pp_tracker.hide_info()
# fill out trackers for accounts with net-zero pps # setup order mode sidepane widgets
zero_pp_accounts = set(accounts) - set(trackers) form = chart.sidepane
for account_name in zero_pp_accounts:
startup_pp = Position(
symbol=symbol,
size=0,
avg_price=0,
)
# 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( order_pane = SettingsPane(
form=form, form=form,
# XXX: ugh, so hideous... # XXX: ugh, so hideous...
@ -654,6 +632,11 @@ async def open_order_mode(
step_label=form.bottom_label, step_label=form.bottom_label,
limit_label=form.top_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 # top level abstraction which wraps all this crazyness into
# a namespace.. # a namespace..