commit
3aadd49e07
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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...
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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..
|
||||
|
|
Loading…
Reference in New Issue