From 5ae16bf73e57d8055c25e25497acee4bfd6810a2 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 4 Aug 2021 13:37:51 -0400 Subject: [PATCH] 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. --- piker/ui/_orm.py | 129 ++++++++++++++++++++++++++++++++++++++++++ piker/ui/_position.py | 73 +++++++++++++++++++++++- 2 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 piker/ui/_orm.py diff --git a/piker/ui/_orm.py b/piker/ui/_orm.py new file mode 100644 index 00000000..152da505 --- /dev/null +++ b/piker/ui/_orm.py @@ -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 . + +""" +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) diff --git a/piker/ui/_position.py b/piker/ui/_position.py index c9b68a02..6c1325ce 100644 --- a/piker/ui/_position.py +++ b/piker/ui/_position.py @@ -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.