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 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()

View File

@ -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

View File

@ -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,