Split up form creation and input handling, require a `.model`
parent
1d7300577e
commit
cebfe9dca3
|
@ -33,6 +33,7 @@ from PyQt5.QtWidgets import (
|
||||||
)
|
)
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
from pydantic import BaseModel
|
||||||
import tractor
|
import tractor
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
|
@ -75,8 +76,9 @@ from .. import fsp
|
||||||
from ..data import feed
|
from ..data import feed
|
||||||
from ._forms import (
|
from ._forms import (
|
||||||
FieldsForm,
|
FieldsForm,
|
||||||
open_form,
|
mk_form,
|
||||||
mk_order_pane_layout,
|
mk_order_pane_layout,
|
||||||
|
open_form_input_handling,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -228,9 +230,9 @@ class GodWidget(QWidget):
|
||||||
# change the order config form over to the new chart
|
# change the order config form over to the new chart
|
||||||
# XXX: since the pp config is a singleton widget we have to
|
# XXX: since the pp config is a singleton widget we have to
|
||||||
# also switch it over to the new chart's interal-layout
|
# also switch it over to the new chart's interal-layout
|
||||||
self.linkedsplits.chart.qframe.hbox.removeWidget(self.pp_config)
|
self.linkedsplits.chart.qframe.hbox.removeWidget(self.pp_pane)
|
||||||
linkedsplits.chart.qframe.hbox.addWidget(
|
linkedsplits.chart.qframe.hbox.addWidget(
|
||||||
self.pp_config,
|
self.pp_pane,
|
||||||
alignment=Qt.AlignTop
|
alignment=Qt.AlignTop
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -377,7 +379,7 @@ class LinkedSplits(QWidget):
|
||||||
style=style,
|
style=style,
|
||||||
_is_main=True,
|
_is_main=True,
|
||||||
|
|
||||||
sidepane=self.godwidget.pp_config,
|
sidepane=self.godwidget.pp_pane,
|
||||||
)
|
)
|
||||||
# add crosshair graphic
|
# add crosshair graphic
|
||||||
self.chart.addItem(self.cursor)
|
self.chart.addItem(self.cursor)
|
||||||
|
@ -438,7 +440,7 @@ class LinkedSplits(QWidget):
|
||||||
self.xaxis = xaxis
|
self.xaxis = xaxis
|
||||||
|
|
||||||
# TODO: probably should formalize and call this something else?
|
# TODO: probably should formalize and call this something else?
|
||||||
class LambdaQFrame(QFrame):
|
class ChartnPane(QFrame):
|
||||||
'''One-off ``QFrame`` composite which pairs a chart + sidepane
|
'''One-off ``QFrame`` composite which pairs a chart + sidepane
|
||||||
``FieldsForm`` (if provided).
|
``FieldsForm`` (if provided).
|
||||||
|
|
||||||
|
@ -465,7 +467,7 @@ class LinkedSplits(QWidget):
|
||||||
hbox.setContentsMargins(0, 0, 0, 0)
|
hbox.setContentsMargins(0, 0, 0, 0)
|
||||||
hbox.setSpacing(3)
|
hbox.setSpacing(3)
|
||||||
|
|
||||||
qframe = LambdaQFrame(self.splitter)
|
qframe = ChartnPane(self.splitter)
|
||||||
|
|
||||||
cpw = ChartPlotWidget(
|
cpw = ChartPlotWidget(
|
||||||
|
|
||||||
|
@ -517,7 +519,9 @@ class LinkedSplits(QWidget):
|
||||||
# XXX: gives us outline on backside of y-axis
|
# XXX: gives us outline on backside of y-axis
|
||||||
cpw.getPlotItem().setContentsMargins(*CHART_MARGINS)
|
cpw.getPlotItem().setContentsMargins(*CHART_MARGINS)
|
||||||
|
|
||||||
# link chart x-axis to main quotes chart
|
# link chart x-axis to main chart
|
||||||
|
# this is 1/2 of where the `Link` in ``LinkedSplit``
|
||||||
|
# comes from ;)
|
||||||
cpw.setXLink(self.chart)
|
cpw.setXLink(self.chart)
|
||||||
|
|
||||||
# add to cross-hair's known plots
|
# add to cross-hair's known plots
|
||||||
|
@ -1373,6 +1377,36 @@ async def run_fsp(
|
||||||
group_key=group_status_key,
|
group_key=group_status_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class FspConfig(BaseModel):
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
validate_assignment = True
|
||||||
|
|
||||||
|
name: str
|
||||||
|
period: int
|
||||||
|
|
||||||
|
sidepane: FieldsForm = mk_form(
|
||||||
|
model=FspConfig(
|
||||||
|
name=display_name,
|
||||||
|
period=14,
|
||||||
|
),
|
||||||
|
parent=linkedsplits.godwidget,
|
||||||
|
fields_schema={
|
||||||
|
'name': {
|
||||||
|
'label': '**fsp**:',
|
||||||
|
'type': 'select',
|
||||||
|
'default_value': [
|
||||||
|
f'{display_name}'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'period': {
|
||||||
|
'label': '**period**:',
|
||||||
|
'type': 'edit',
|
||||||
|
'default_value': 14,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
portal.open_stream_from(
|
portal.open_stream_from(
|
||||||
|
|
||||||
|
@ -1388,23 +1422,8 @@ async def run_fsp(
|
||||||
|
|
||||||
) as stream,
|
) as stream,
|
||||||
|
|
||||||
open_form(
|
# TODO:
|
||||||
parent=linkedsplits.godwidget,
|
# open_form_input_handling(sidepane),
|
||||||
fields_schema={
|
|
||||||
'name': {
|
|
||||||
'label': '**fsp**:',
|
|
||||||
'type': 'select',
|
|
||||||
'default_value': [
|
|
||||||
f'{display_name}'
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'period': {
|
|
||||||
'label': '**period**:',
|
|
||||||
'type': 'edit',
|
|
||||||
'default_value': 14,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
) as sidepane,
|
|
||||||
|
|
||||||
):
|
):
|
||||||
|
|
||||||
|
@ -1840,56 +1859,14 @@ async def _async_main(
|
||||||
starting_done = sbar.open_status('starting ze sexy chartz')
|
starting_done = sbar.open_status('starting ze sexy chartz')
|
||||||
|
|
||||||
# generate order mode side-pane UI
|
# generate order mode side-pane UI
|
||||||
|
# A ``FieldsForm`` form to configure order entry
|
||||||
|
pp_pane: FieldsForm = mk_order_pane_layout(godwidget)
|
||||||
|
# add as next-to-y-axis singleton pane
|
||||||
|
godwidget.pp_pane = pp_pane
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
trio.open_nursery() as root_n,
|
trio.open_nursery() as root_n,
|
||||||
|
|
||||||
# fields form to configure order entry
|
|
||||||
open_form(
|
|
||||||
parent=godwidget,
|
|
||||||
fields_schema={
|
|
||||||
'account': {
|
|
||||||
'type': 'select',
|
|
||||||
'default_value': [
|
|
||||||
'paper',
|
|
||||||
# 'ib.margin',
|
|
||||||
# 'ib.paper',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'size_unit': {
|
|
||||||
'label': '**allocate**:',
|
|
||||||
'type': 'select',
|
|
||||||
'default_value': [
|
|
||||||
'$ size',
|
|
||||||
'% of port',
|
|
||||||
'# shares'
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'disti_weight': {
|
|
||||||
'label': '**weight**:',
|
|
||||||
'type': 'select',
|
|
||||||
'default_value': ['uniform'],
|
|
||||||
},
|
|
||||||
'size': {
|
|
||||||
'label': '**size**:',
|
|
||||||
'type': 'edit',
|
|
||||||
'default_value': 5000,
|
|
||||||
},
|
|
||||||
'slots': {
|
|
||||||
'type': 'edit',
|
|
||||||
'default_value': 4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
) as pp_config,
|
|
||||||
):
|
):
|
||||||
pp_config: FieldsForm
|
|
||||||
mk_order_pane_layout(pp_config)
|
|
||||||
pp_config.show()
|
|
||||||
|
|
||||||
# add as next-to-y-axis pane
|
|
||||||
godwidget.pp_config = pp_config
|
|
||||||
|
|
||||||
# set root nursery and task stack for spawning other charts/feeds
|
# set root nursery and task stack for spawning other charts/feeds
|
||||||
# that run cached in the bg
|
# that run cached in the bg
|
||||||
godwidget._root_n = root_n
|
godwidget._root_n = root_n
|
||||||
|
@ -1932,9 +1909,10 @@ async def _async_main(
|
||||||
|
|
||||||
await order_mode_ready.wait()
|
await order_mode_ready.wait()
|
||||||
|
|
||||||
# start handling search bar kb inputs
|
# start handling peripherals input for top level widgets
|
||||||
async with (
|
async with (
|
||||||
|
|
||||||
|
# search bar kb inputs
|
||||||
_event.open_handlers(
|
_event.open_handlers(
|
||||||
[search.bar],
|
[search.bar],
|
||||||
event_types={QEvent.KeyPress},
|
event_types={QEvent.KeyPress},
|
||||||
|
@ -1942,6 +1920,9 @@ async def _async_main(
|
||||||
# let key repeats pass through for search
|
# let key repeats pass through for search
|
||||||
filter_auto_repeats=False,
|
filter_auto_repeats=False,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
# pp pane kb inputs
|
||||||
|
open_form_input_handling(pp_pane),
|
||||||
):
|
):
|
||||||
# remove startup status text
|
# remove startup status text
|
||||||
starting_done()
|
starting_done()
|
||||||
|
|
|
@ -41,6 +41,7 @@ from PyQt5.QtWidgets import (
|
||||||
QStyledItemDelegate,
|
QStyledItemDelegate,
|
||||||
QStyleOptionViewItem,
|
QStyleOptionViewItem,
|
||||||
)
|
)
|
||||||
|
import pydantic
|
||||||
|
|
||||||
from ._event import open_handlers
|
from ._event import open_handlers
|
||||||
from ._style import hcolor, _font, _font_small, DpiAwareFont
|
from ._style import hcolor, _font, _font_small, DpiAwareFont
|
||||||
|
@ -330,10 +331,9 @@ class FieldsForm(QWidget):
|
||||||
async def handle_field_input(
|
async def handle_field_input(
|
||||||
|
|
||||||
widget: QWidget,
|
widget: QWidget,
|
||||||
# last_widget: QWidget, # had focus prior
|
|
||||||
recv_chan: trio.abc.ReceiveChannel,
|
recv_chan: trio.abc.ReceiveChannel,
|
||||||
fields: FieldsForm,
|
form: FieldsForm,
|
||||||
allocator: Allocator, # noqa
|
model: pydantic.BaseModel, # noqa
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
|
@ -356,39 +356,38 @@ async def handle_field_input(
|
||||||
}:
|
}:
|
||||||
|
|
||||||
widget.clearFocus()
|
widget.clearFocus()
|
||||||
fields.godwidget.focus()
|
form.godwidget.focus()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# process field input
|
# process field input
|
||||||
if key in (Qt.Key_Enter, Qt.Key_Return):
|
if key in (Qt.Key_Enter, Qt.Key_Return):
|
||||||
value = widget.text()
|
value = widget.text()
|
||||||
key = widget._key
|
key = widget._key
|
||||||
setattr(allocator, key, value)
|
setattr(model, key, value)
|
||||||
print(allocator.dict())
|
print(model.dict())
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
def mk_form(
|
||||||
async def open_form(
|
|
||||||
|
|
||||||
|
model: pydantic.BaseModel,
|
||||||
parent: QWidget,
|
parent: QWidget,
|
||||||
fields_schema: dict,
|
fields_schema: dict,
|
||||||
# alloc: Allocator,
|
|
||||||
# orientation: str = 'horizontal',
|
|
||||||
|
|
||||||
) -> FieldsForm:
|
) -> FieldsForm:
|
||||||
|
|
||||||
fields = FieldsForm(parent=parent)
|
form = FieldsForm(parent=parent)
|
||||||
from ._position import mk_pp_alloc
|
# TODO: generate components from model
|
||||||
alloc = mk_pp_alloc()
|
# instead of schema dict (aka use an ORM)
|
||||||
fields.model = alloc
|
form.model = model
|
||||||
|
|
||||||
|
# generate sub-components from schema dict
|
||||||
for name, config in fields_schema.items():
|
for name, config in fields_schema.items():
|
||||||
wtype = config['type']
|
wtype = config['type']
|
||||||
label = str(config.get('label', name))
|
label = str(config.get('label', name))
|
||||||
|
|
||||||
# plain (line) edit field
|
# plain (line) edit field
|
||||||
if wtype == 'edit':
|
if wtype == 'edit':
|
||||||
w = fields.add_edit_field(
|
w = form.add_edit_field(
|
||||||
label,
|
label,
|
||||||
config['default_value']
|
config['default_value']
|
||||||
)
|
)
|
||||||
|
@ -396,36 +395,48 @@ async def open_form(
|
||||||
# drop-down selection
|
# drop-down selection
|
||||||
elif wtype == 'select':
|
elif wtype == 'select':
|
||||||
values = list(config['default_value'])
|
values = list(config['default_value'])
|
||||||
w = fields.add_select_field(
|
w = form.add_select_field(
|
||||||
label,
|
label,
|
||||||
values
|
values
|
||||||
)
|
)
|
||||||
|
|
||||||
def write_model(text: str):
|
def write_model(text: str):
|
||||||
print(f'{text}')
|
print(f'{text}')
|
||||||
setattr(alloc, name, text)
|
setattr(form.model, name, text)
|
||||||
|
|
||||||
w.currentTextChanged.connect(write_model)
|
w.currentTextChanged.connect(write_model)
|
||||||
|
|
||||||
w._key = name
|
w._key = name
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def open_form_input_handling(
|
||||||
|
|
||||||
|
form: FieldsForm
|
||||||
|
|
||||||
|
) -> FieldsForm:
|
||||||
|
|
||||||
|
assert form.model, f'{form} must define a `.model`'
|
||||||
|
|
||||||
async with open_handlers(
|
async with open_handlers(
|
||||||
|
|
||||||
list(fields.fields.values()),
|
list(form.fields.values()),
|
||||||
event_types={
|
event_types={
|
||||||
QEvent.KeyPress,
|
QEvent.KeyPress,
|
||||||
},
|
},
|
||||||
|
|
||||||
async_handler=partial(
|
async_handler=partial(
|
||||||
handle_field_input,
|
handle_field_input,
|
||||||
fields=fields,
|
form=form,
|
||||||
allocator=alloc,
|
model=form.model,
|
||||||
),
|
),
|
||||||
|
|
||||||
# block key repeats?
|
# block key repeats?
|
||||||
filter_auto_repeats=True,
|
filter_auto_repeats=True,
|
||||||
):
|
):
|
||||||
yield fields
|
yield form
|
||||||
|
|
||||||
|
|
||||||
def mk_fill_status_bar(
|
def mk_fill_status_bar(
|
||||||
|
@ -568,30 +579,73 @@ def mk_fill_status_bar(
|
||||||
|
|
||||||
def mk_order_pane_layout(
|
def mk_order_pane_layout(
|
||||||
|
|
||||||
fields: FieldsForm,
|
parent: QWidget,
|
||||||
font_size: int = _font_small.px_size - 2
|
font_size: int = _font_small.px_size - 2
|
||||||
|
|
||||||
) -> FieldsForm:
|
) -> FieldsForm:
|
||||||
|
|
||||||
|
from ._position import mk_pp_alloc
|
||||||
|
# TODO: some kinda pydantic sub-type
|
||||||
|
# that enforces a composite widget attr er sumthin..
|
||||||
|
# as part of our ORM thingers.
|
||||||
|
allocator = mk_pp_alloc()
|
||||||
|
|
||||||
# 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?
|
||||||
|
form = mk_form(
|
||||||
fields._font_size = font_size
|
parent=parent,
|
||||||
|
model=allocator,
|
||||||
|
fields_schema={
|
||||||
|
'account': {
|
||||||
|
'type': 'select',
|
||||||
|
'default_value': [
|
||||||
|
'paper',
|
||||||
|
# 'ib.margin',
|
||||||
|
# 'ib.paper',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'size_unit': {
|
||||||
|
'label': '**allocate**:',
|
||||||
|
'type': 'select',
|
||||||
|
'default_value': [
|
||||||
|
'$ size',
|
||||||
|
'% of port',
|
||||||
|
'# shares'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'disti_weight': {
|
||||||
|
'label': '**weight**:',
|
||||||
|
'type': 'select',
|
||||||
|
'default_value': ['uniform'],
|
||||||
|
},
|
||||||
|
'size': {
|
||||||
|
'label': '**size**:',
|
||||||
|
'type': 'edit',
|
||||||
|
'default_value': 5000,
|
||||||
|
},
|
||||||
|
'slots': {
|
||||||
|
'type': 'edit',
|
||||||
|
'default_value': 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
form._font_size = font_size
|
||||||
|
allocator._widget = form
|
||||||
|
|
||||||
# top level pane layout
|
# top level pane layout
|
||||||
# XXX: see ``FieldsForm.__init__()`` for why we can't do basic
|
# XXX: see ``FieldsForm.__init__()`` for why we can't do basic
|
||||||
# config of the vbox here
|
# config of the vbox here
|
||||||
vbox = fields.vbox
|
vbox = form.vbox
|
||||||
|
|
||||||
# _, h = fields.width(), fields.height()
|
# _, h = form.width(), form.height()
|
||||||
# print(f'w, h: {w, h}')
|
# print(f'w, h: {w, h}')
|
||||||
|
|
||||||
hbox, bar = mk_fill_status_bar(fields, pane_vbox=vbox)
|
hbox, bar = mk_fill_status_bar(form, pane_vbox=vbox)
|
||||||
|
|
||||||
# add pp fill bar + spacing
|
# add pp fill bar + spacing
|
||||||
vbox.addLayout(hbox, stretch=1/3)
|
vbox.addLayout(hbox, stretch=1/3)
|
||||||
|
|
||||||
feed_label = fields.add_field_label(
|
feed_label = form.add_field_label(
|
||||||
dedent("""
|
dedent("""
|
||||||
brokerd.ib\n
|
brokerd.ib\n
|
||||||
|_@localhost:8509\n
|
|_@localhost:8509\n
|
||||||
|
@ -613,4 +667,6 @@ def mk_order_pane_layout(
|
||||||
# https://doc.qt.io/qt-5/layout.html#adding-widgets-to-a-layout
|
# https://doc.qt.io/qt-5/layout.html#adding-widgets-to-a-layout
|
||||||
vbox.setSpacing(36)
|
vbox.setSpacing(36)
|
||||||
|
|
||||||
return fields
|
form.show()
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
|
@ -105,12 +105,15 @@ def mk_pp_alloc(
|
||||||
slots: int
|
slots: int
|
||||||
|
|
||||||
_position: Position = None
|
_position: Position = None
|
||||||
|
_widget: QWidget = None
|
||||||
|
|
||||||
def get_order_info(
|
def get_order_info(
|
||||||
self,
|
self,
|
||||||
price: float,
|
price: float,
|
||||||
|
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
size = self.size / self.slots
|
||||||
|
|
||||||
units, r = divmod(
|
units, r = divmod(
|
||||||
round((self.size / self.slots)),
|
round((self.size / self.slots)),
|
||||||
price,
|
price,
|
||||||
|
|
Loading…
Reference in New Issue