Split up form creation and input handling, require a `.model`

fsp_feeds
Tyler Goodlet 2021-08-11 16:29:56 -04:00
parent 1d7300577e
commit cebfe9dca3
3 changed files with 140 additions and 100 deletions

View File

@ -33,6 +33,7 @@ from PyQt5.QtWidgets import (
)
import numpy as np
import pyqtgraph as pg
from pydantic import BaseModel
import tractor
import trio
@ -75,8 +76,9 @@ from .. import fsp
from ..data import feed
from ._forms import (
FieldsForm,
open_form,
mk_form,
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
# XXX: since the pp config is a singleton widget we have to
# 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(
self.pp_config,
self.pp_pane,
alignment=Qt.AlignTop
)
@ -377,7 +379,7 @@ class LinkedSplits(QWidget):
style=style,
_is_main=True,
sidepane=self.godwidget.pp_config,
sidepane=self.godwidget.pp_pane,
)
# add crosshair graphic
self.chart.addItem(self.cursor)
@ -438,7 +440,7 @@ class LinkedSplits(QWidget):
self.xaxis = xaxis
# TODO: probably should formalize and call this something else?
class LambdaQFrame(QFrame):
class ChartnPane(QFrame):
'''One-off ``QFrame`` composite which pairs a chart + sidepane
``FieldsForm`` (if provided).
@ -465,7 +467,7 @@ class LinkedSplits(QWidget):
hbox.setContentsMargins(0, 0, 0, 0)
hbox.setSpacing(3)
qframe = LambdaQFrame(self.splitter)
qframe = ChartnPane(self.splitter)
cpw = ChartPlotWidget(
@ -517,7 +519,9 @@ class LinkedSplits(QWidget):
# XXX: gives us outline on backside of y-axis
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)
# add to cross-hair's known plots
@ -1373,6 +1377,36 @@ async def run_fsp(
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 (
portal.open_stream_from(
@ -1388,23 +1422,8 @@ async def run_fsp(
) as stream,
open_form(
parent=linkedsplits.godwidget,
fields_schema={
'name': {
'label': '**fsp**:',
'type': 'select',
'default_value': [
f'{display_name}'
],
},
'period': {
'label': '**period**:',
'type': 'edit',
'default_value': 14,
},
},
) as sidepane,
# TODO:
# open_form_input_handling(sidepane),
):
@ -1840,56 +1859,14 @@ async def _async_main(
starting_done = sbar.open_status('starting ze sexy chartz')
# 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 (
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
# that run cached in the bg
godwidget._root_n = root_n
@ -1932,9 +1909,10 @@ async def _async_main(
await order_mode_ready.wait()
# start handling search bar kb inputs
# start handling peripherals input for top level widgets
async with (
# search bar kb inputs
_event.open_handlers(
[search.bar],
event_types={QEvent.KeyPress},
@ -1942,6 +1920,9 @@ async def _async_main(
# let key repeats pass through for search
filter_auto_repeats=False,
),
# pp pane kb inputs
open_form_input_handling(pp_pane),
):
# remove startup status text
starting_done()

View File

@ -41,6 +41,7 @@ from PyQt5.QtWidgets import (
QStyledItemDelegate,
QStyleOptionViewItem,
)
import pydantic
from ._event import open_handlers
from ._style import hcolor, _font, _font_small, DpiAwareFont
@ -330,10 +331,9 @@ class FieldsForm(QWidget):
async def handle_field_input(
widget: QWidget,
# last_widget: QWidget, # had focus prior
recv_chan: trio.abc.ReceiveChannel,
fields: FieldsForm,
allocator: Allocator, # noqa
form: FieldsForm,
model: pydantic.BaseModel, # noqa
) -> None:
@ -356,39 +356,38 @@ async def handle_field_input(
}:
widget.clearFocus()
fields.godwidget.focus()
form.godwidget.focus()
continue
# process field input
if key in (Qt.Key_Enter, Qt.Key_Return):
value = widget.text()
key = widget._key
setattr(allocator, key, value)
print(allocator.dict())
setattr(model, key, value)
print(model.dict())
@asynccontextmanager
async def open_form(
def mk_form(
model: pydantic.BaseModel,
parent: QWidget,
fields_schema: dict,
# alloc: Allocator,
# orientation: str = 'horizontal',
) -> FieldsForm:
fields = FieldsForm(parent=parent)
from ._position import mk_pp_alloc
alloc = mk_pp_alloc()
fields.model = alloc
form = FieldsForm(parent=parent)
# TODO: generate components from model
# instead of schema dict (aka use an ORM)
form.model = model
# generate sub-components from schema dict
for name, config in fields_schema.items():
wtype = config['type']
label = str(config.get('label', name))
# plain (line) edit field
if wtype == 'edit':
w = fields.add_edit_field(
w = form.add_edit_field(
label,
config['default_value']
)
@ -396,36 +395,48 @@ async def open_form(
# drop-down selection
elif wtype == 'select':
values = list(config['default_value'])
w = fields.add_select_field(
w = form.add_select_field(
label,
values
)
def write_model(text: str):
print(f'{text}')
setattr(alloc, name, text)
setattr(form.model, name, text)
w.currentTextChanged.connect(write_model)
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(
list(fields.fields.values()),
list(form.fields.values()),
event_types={
QEvent.KeyPress,
},
async_handler=partial(
handle_field_input,
fields=fields,
allocator=alloc,
form=form,
model=form.model,
),
# block key repeats?
filter_auto_repeats=True,
):
yield fields
yield form
def mk_fill_status_bar(
@ -568,30 +579,73 @@ def mk_fill_status_bar(
def mk_order_pane_layout(
fields: FieldsForm,
parent: QWidget,
font_size: int = _font_small.px_size - 2
) -> 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
# and expect an async ctx entry?
fields._font_size = font_size
form = mk_form(
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
# XXX: see ``FieldsForm.__init__()`` for why we can't do basic
# 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}')
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
vbox.addLayout(hbox, stretch=1/3)
feed_label = fields.add_field_label(
feed_label = form.add_field_label(
dedent("""
brokerd.ib\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
vbox.setSpacing(36)
return fields
form.show()
return form

View File

@ -105,12 +105,15 @@ def mk_pp_alloc(
slots: int
_position: Position = None
_widget: QWidget = None
def get_order_info(
self,
price: float,
) -> dict:
size = self.size / self.slots
units, r = divmod(
round((self.size / self.slots)),
price,