Add draft `pydantic`-`QWidget` ORM system

Move all the ``pydantic`` finagling to an `_orm.py` and
just keep an `Allocator` as the backing model for our pp controls
in the position module. This all needs to be tied together in some sane
with with facility for multiple symbols/streams per chart for when we
get to charting-trading aggregate feeds.
fsp_feeds
Tyler Goodlet 2021-08-04 13:37:51 -04:00
parent 2d1deb7ab7
commit c982634839
2 changed files with 200 additions and 2 deletions

129
piker/ui/_orm.py 100644
View File

@ -0,0 +1,129 @@
# piker: trading gear for hackers
# Copyright (C) Tyler Goodlet
# 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/>.
"""
micro-ORM for coupling ``pydantic`` models with Qt input/output widgets.
"""
from __future__ import annotations
from typing import (
Optional, Generic,
TypeVar, Callable,
Literal,
)
import enum
import sys
from pydantic import BaseModel, validator
from pydantic.generics import GenericModel
from PyQt5.QtWidgets import (
QWidget,
QComboBox,
)
from ._forms import (
# FontScaledDelegate,
FontAndChartAwareLineEdit,
)
DataType = TypeVar('DataType')
class Field(GenericModel, Generic[DataType]):
widget_factory: Optional[
Callable[
[QWidget, 'Field'],
QWidget
]
]
value: Optional[DataType] = None
class Selection(Field[DataType], Generic[DataType]):
'''Model which maps to a finite set of drop down entries declared as
a ``dict[str, DataType]``.
'''
widget_factory = QComboBox
options: dict[str, DataType]
# value: DataType = None
@validator('value') # , always=True)
def set_value_first(
cls,
v: DataType,
values: dict[str, DataType],
) -> DataType:
'''If no initial value is set, use the first in
the ``options`` dict.
'''
# breakpoint()
options = values['options']
if v is None:
return next(options.values())
else:
assert v in options, f'{v} is not in {options}'
return v
# class SizeUnit(Enum):
# currency = '$ size'
# percent_of_port = '% of port'
# shares = '# shares'
# class Weighter(str, Enum):
# uniform = 'uniform'
class Edit(Field[DataType], Generic[DataType]):
'''An edit field which takes a number.
'''
widget_factory = FontAndChartAwareLineEdit
class AllocatorPane(BaseModel):
account = Selection[str](
options=dict.fromkeys(
['paper', 'ib.paper', 'ib.margin'],
'paper',
),
)
allocate = Selection[str](
# options=list(Size),
options={
'$ size': 'currency',
'% of port': 'percent_of_port',
'# shares': 'shares',
},
# TODO: save/load from config and/or last session
# value='currency'
)
weight = Selection[str](
options={
'uniform': 'uniform',
},
# value='uniform',
)
size = Edit[float](value=1000)
slots = Edit[int](value=4)

View File

@ -18,12 +18,17 @@
Position info and display
"""
from typing import Optional
from __future__ import annotations
import enum
from functools import partial
from math import floor
import sys
from typing import Optional
from bidict import bidict
from pyqtgraph import functions as fn
from pydantic import BaseModel
from pydantic import BaseModel, validator
# from pydantic.generics import GenericModel
# from PyQt5.QtCore import QPointF
# from PyQt5.QtGui import QGraphicsPathItem
@ -56,6 +61,70 @@ class Position(BaseModel):
fills: list[Status] = []
def mk_pp_alloc(
accounts: dict[str, Optional[str]] = {
'paper': None,
'ib.paper': 'DU1435481',
'ib.margin': 'U10983%',
},
) -> Allocator: # noqa
# lol we have to do this module patching bc ``pydantic``
# needs types to exist at module level:
# https://pydantic-docs.helpmanual.io/usage/postponed_annotations/
mod = sys.modules[__name__]
accounts = bidict(accounts)
Account = mod.Account = enum.Enum('Account', accounts)
size_units = bidict({
'$ size': 'currency',
'% of port': 'percent_of_port',
'# shares': 'shares',
})
SizeUnit = mod.SizeUnit = enum.Enum(
'SizeUnit',
size_units.inverse
)
class Allocator(BaseModel):
account: Account = None
_accounts: dict[str, Optional[str]] = accounts
size_unit: SizeUnit = 'currency'
_size_units: dict[str, Optional[str]] = size_units
disti_weight: str = 'uniform'
size: float
slots: int
_position: Position = None
def get_order_info(
self,
price: float,
) -> dict:
units, r = divmod(
round((self.size / self.slots)),
price,
)
print(f'# shares: {units}, r: {r}')
# Allocator.update_forward_refs()
return Allocator(
account=None,
size_unit=size_units.inverse['currency'],
size=2000,
slots=4,
)
class PositionTracker:
'''Track and display a real-time position for a single symbol
on a chart.