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