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