commit
600636784c
228
piker/types.py
228
piker/types.py
|
|
@ -21,230 +21,6 @@ Extensions to built-in or (heavily used but 3rd party) friend-lib
|
||||||
types.
|
types.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from tractor.msg.pretty_struct import (
|
||||||
from collections import UserList
|
Struct as Struct,
|
||||||
from pprint import (
|
|
||||||
saferepr,
|
|
||||||
)
|
)
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from msgspec import (
|
|
||||||
msgpack,
|
|
||||||
Struct as _Struct,
|
|
||||||
structs,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DiffDump(UserList):
|
|
||||||
'''
|
|
||||||
Very simple list delegator that repr() dumps (presumed) tuple
|
|
||||||
elements of the form `tuple[str, Any, Any]` in a nice
|
|
||||||
multi-line readable form for analyzing `Struct` diffs.
|
|
||||||
|
|
||||||
'''
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
if not len(self):
|
|
||||||
return super().__repr__()
|
|
||||||
|
|
||||||
# format by displaying item pair's ``repr()`` on multiple,
|
|
||||||
# indented lines such that they are more easily visually
|
|
||||||
# comparable when printed to console when printed to
|
|
||||||
# console.
|
|
||||||
repstr: str = '[\n'
|
|
||||||
for k, left, right in self:
|
|
||||||
repstr += (
|
|
||||||
f'({k},\n'
|
|
||||||
f'\t{repr(left)},\n'
|
|
||||||
f'\t{repr(right)},\n'
|
|
||||||
')\n'
|
|
||||||
)
|
|
||||||
repstr += ']\n'
|
|
||||||
return repstr
|
|
||||||
|
|
||||||
|
|
||||||
class Struct(
|
|
||||||
_Struct,
|
|
||||||
|
|
||||||
# https://jcristharif.com/msgspec/structs.html#tagged-unions
|
|
||||||
# tag='pikerstruct',
|
|
||||||
# tag=True,
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
A "human friendlier" (aka repl buddy) struct subtype.
|
|
||||||
|
|
||||||
'''
|
|
||||||
def _sin_props(self) -> Iterator[
|
|
||||||
tuple[
|
|
||||||
structs.FieldIinfo,
|
|
||||||
str,
|
|
||||||
Any,
|
|
||||||
]
|
|
||||||
]:
|
|
||||||
'''
|
|
||||||
Iterate over all non-@property fields of this struct.
|
|
||||||
|
|
||||||
'''
|
|
||||||
fi: structs.FieldInfo
|
|
||||||
for fi in structs.fields(self):
|
|
||||||
key: str = fi.name
|
|
||||||
val: Any = getattr(self, key)
|
|
||||||
yield fi, key, val
|
|
||||||
|
|
||||||
def to_dict(
|
|
||||||
self,
|
|
||||||
include_non_members: bool = True,
|
|
||||||
|
|
||||||
) -> dict:
|
|
||||||
'''
|
|
||||||
Like it sounds.. direct delegation to:
|
|
||||||
https://jcristharif.com/msgspec/api.html#msgspec.structs.asdict
|
|
||||||
|
|
||||||
BUT, by default we pop all non-member (aka not defined as
|
|
||||||
struct fields) fields by default.
|
|
||||||
|
|
||||||
'''
|
|
||||||
asdict: dict = structs.asdict(self)
|
|
||||||
if include_non_members:
|
|
||||||
return asdict
|
|
||||||
|
|
||||||
# only return a dict of the struct members
|
|
||||||
# which were provided as input, NOT anything
|
|
||||||
# added as type-defined `@property` methods!
|
|
||||||
sin_props: dict = {}
|
|
||||||
fi: structs.FieldInfo
|
|
||||||
for fi, k, v in self._sin_props():
|
|
||||||
sin_props[k] = asdict[k]
|
|
||||||
|
|
||||||
return sin_props
|
|
||||||
|
|
||||||
def pformat(
|
|
||||||
self,
|
|
||||||
field_indent: int = 2,
|
|
||||||
indent: int = 0,
|
|
||||||
|
|
||||||
) -> str:
|
|
||||||
'''
|
|
||||||
Recursion-safe `pprint.pformat()` style formatting of
|
|
||||||
a `msgspec.Struct` for sane reading by a human using a REPL.
|
|
||||||
|
|
||||||
'''
|
|
||||||
# global whitespace indent
|
|
||||||
ws: str = ' '*indent
|
|
||||||
|
|
||||||
# field whitespace indent
|
|
||||||
field_ws: str = ' '*(field_indent + indent)
|
|
||||||
|
|
||||||
# qtn: str = ws + self.__class__.__qualname__
|
|
||||||
qtn: str = self.__class__.__qualname__
|
|
||||||
|
|
||||||
obj_str: str = '' # accumulator
|
|
||||||
fi: structs.FieldInfo
|
|
||||||
k: str
|
|
||||||
v: Any
|
|
||||||
for fi, k, v in self._sin_props():
|
|
||||||
|
|
||||||
# TODO: how can we prefer `Literal['option1', 'option2,
|
|
||||||
# ..]` over .__name__ == `Literal` but still get only the
|
|
||||||
# latter for simple types like `str | int | None` etc..?
|
|
||||||
ft: type = fi.type
|
|
||||||
typ_name: str = getattr(ft, '__name__', str(ft))
|
|
||||||
|
|
||||||
# recurse to get sub-struct's `.pformat()` output Bo
|
|
||||||
if isinstance(v, Struct):
|
|
||||||
val_str: str = v.pformat(
|
|
||||||
indent=field_indent + indent,
|
|
||||||
field_indent=indent + field_indent,
|
|
||||||
)
|
|
||||||
|
|
||||||
else: # the `pprint` recursion-safe format:
|
|
||||||
# https://docs.python.org/3.11/library/pprint.html#pprint.saferepr
|
|
||||||
val_str: str = saferepr(v)
|
|
||||||
|
|
||||||
obj_str += (field_ws + f'{k}: {typ_name} = {val_str},\n')
|
|
||||||
|
|
||||||
return (
|
|
||||||
f'{qtn}(\n'
|
|
||||||
f'{obj_str}'
|
|
||||||
f'{ws})'
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: use a pprint.PrettyPrinter instance around ONLY rendering
|
|
||||||
# inside a known tty?
|
|
||||||
# def __repr__(self) -> str:
|
|
||||||
# ...
|
|
||||||
|
|
||||||
# __str__ = __repr__ = pformat
|
|
||||||
__repr__ = pformat
|
|
||||||
|
|
||||||
def copy(
|
|
||||||
self,
|
|
||||||
update: dict | None = None,
|
|
||||||
|
|
||||||
) -> Struct:
|
|
||||||
'''
|
|
||||||
Validate-typecast all self defined fields, return a copy of
|
|
||||||
us with all such fields.
|
|
||||||
|
|
||||||
NOTE: This is kinda like the default behaviour in
|
|
||||||
`pydantic.BaseModel` except a copy of the object is
|
|
||||||
returned making it compat with `frozen=True`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
if update:
|
|
||||||
for k, v in update.items():
|
|
||||||
setattr(self, k, v)
|
|
||||||
|
|
||||||
# NOTE: roundtrip serialize to validate
|
|
||||||
# - enode to msgpack binary format,
|
|
||||||
# - decode that back to a struct.
|
|
||||||
return msgpack.Decoder(type=type(self)).decode(
|
|
||||||
msgpack.Encoder().encode(self)
|
|
||||||
)
|
|
||||||
|
|
||||||
def typecast(
|
|
||||||
self,
|
|
||||||
|
|
||||||
# TODO: allow only casting a named subset?
|
|
||||||
# fields: set[str] | None = None,
|
|
||||||
|
|
||||||
) -> None:
|
|
||||||
'''
|
|
||||||
Cast all fields using their declared type annotations
|
|
||||||
(kinda like what `pydantic` does by default).
|
|
||||||
|
|
||||||
NOTE: this of course won't work on frozen types, use
|
|
||||||
``.copy()`` above in such cases.
|
|
||||||
|
|
||||||
'''
|
|
||||||
# https://jcristharif.com/msgspec/api.html#msgspec.structs.fields
|
|
||||||
fi: structs.FieldInfo
|
|
||||||
for fi in structs.fields(self):
|
|
||||||
setattr(
|
|
||||||
self,
|
|
||||||
fi.name,
|
|
||||||
fi.type(getattr(self, fi.name)),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __sub__(
|
|
||||||
self,
|
|
||||||
other: Struct,
|
|
||||||
|
|
||||||
) -> DiffDump[tuple[str, Any, Any]]:
|
|
||||||
'''
|
|
||||||
Compare fields/items key-wise and return a ``DiffDump``
|
|
||||||
for easy visual REPL comparison B)
|
|
||||||
|
|
||||||
'''
|
|
||||||
diffs: DiffDump[tuple[str, Any, Any]] = DiffDump()
|
|
||||||
for fi in structs.fields(self):
|
|
||||||
attr_name: str = fi.name
|
|
||||||
ours: Any = getattr(self, attr_name)
|
|
||||||
theirs: Any = getattr(other, attr_name)
|
|
||||||
if ours != theirs:
|
|
||||||
diffs.append((
|
|
||||||
attr_name,
|
|
||||||
ours,
|
|
||||||
theirs,
|
|
||||||
))
|
|
||||||
|
|
||||||
return diffs
|
|
||||||
|
|
|
||||||
|
|
@ -27,15 +27,15 @@ import trio
|
||||||
from piker.ui.qt import (
|
from piker.ui.qt import (
|
||||||
QEvent,
|
QEvent,
|
||||||
)
|
)
|
||||||
from ..service import maybe_spawn_brokerd
|
from . import _chart
|
||||||
from . import _event
|
from . import _event
|
||||||
from ._exec import run_qtractor
|
|
||||||
from ..data.feed import install_brokerd_search
|
|
||||||
from ..data._symcache import open_symcache
|
|
||||||
from ..accounting import unpack_fqme
|
|
||||||
from . import _search
|
from . import _search
|
||||||
from ._chart import GodWidget
|
from ..accounting import unpack_fqme
|
||||||
|
from ..data._symcache import open_symcache
|
||||||
|
from ..data.feed import install_brokerd_search
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
|
from ..service import maybe_spawn_brokerd
|
||||||
|
from ._exec import run_qtractor
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
@ -73,8 +73,8 @@ async def load_provider_search(
|
||||||
|
|
||||||
async def _async_main(
|
async def _async_main(
|
||||||
|
|
||||||
# implicit required argument provided by ``qtractor_run()``
|
# implicit required argument provided by `qtractor_run()`
|
||||||
main_widget: GodWidget,
|
main_widget: _chart.GodWidget,
|
||||||
|
|
||||||
syms: list[str],
|
syms: list[str],
|
||||||
brokers: dict[str, ModuleType],
|
brokers: dict[str, ModuleType],
|
||||||
|
|
@ -87,6 +87,9 @@ async def _async_main(
|
||||||
Provision the "main" widget with initial symbol data and root nursery.
|
Provision the "main" widget with initial symbol data and root nursery.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# set as singleton
|
||||||
|
_chart._godw = main_widget
|
||||||
|
|
||||||
from . import _display
|
from . import _display
|
||||||
from ._pg_overrides import _do_overrides
|
from ._pg_overrides import _do_overrides
|
||||||
_do_overrides()
|
_do_overrides()
|
||||||
|
|
@ -201,6 +204,6 @@ def _main(
|
||||||
brokermods,
|
brokermods,
|
||||||
piker_loglevel,
|
piker_loglevel,
|
||||||
),
|
),
|
||||||
main_widget_type=GodWidget,
|
main_widget_type=_chart.GodWidget,
|
||||||
tractor_kwargs=tractor_kwargs,
|
tractor_kwargs=tractor_kwargs,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ from typing import (
|
||||||
)
|
)
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
import trio
|
|
||||||
|
|
||||||
from piker.ui.qt import (
|
from piker.ui.qt import (
|
||||||
QtCore,
|
QtCore,
|
||||||
|
|
@ -41,6 +40,7 @@ from piker.ui.qt import (
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QSplitter,
|
QSplitter,
|
||||||
)
|
)
|
||||||
|
from ._widget import GodWidget
|
||||||
from ._axes import (
|
from ._axes import (
|
||||||
DynamicDateAxis,
|
DynamicDateAxis,
|
||||||
PriceAxis,
|
PriceAxis,
|
||||||
|
|
@ -61,10 +61,6 @@ from ._style import (
|
||||||
_xaxis_at,
|
_xaxis_at,
|
||||||
# _min_points_to_show,
|
# _min_points_to_show,
|
||||||
)
|
)
|
||||||
from ..data.feed import (
|
|
||||||
Feed,
|
|
||||||
Flume,
|
|
||||||
)
|
|
||||||
from ..accounting import (
|
from ..accounting import (
|
||||||
MktPair,
|
MktPair,
|
||||||
)
|
)
|
||||||
|
|
@ -78,286 +74,12 @@ from . import _pg_overrides as pgo
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._display import DisplayState
|
from ._display import DisplayState
|
||||||
|
from ..data.flows import Flume
|
||||||
|
from ..data.feed import Feed
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class GodWidget(QWidget):
|
|
||||||
'''
|
|
||||||
"Our lord and savior, the holy child of window-shua, there is no
|
|
||||||
widget above thee." - 6|6
|
|
||||||
|
|
||||||
The highest level composed widget which contains layouts for
|
|
||||||
organizing charts as well as other sub-widgets used to control or
|
|
||||||
modify them.
|
|
||||||
|
|
||||||
'''
|
|
||||||
search: SearchWidget
|
|
||||||
mode_name: str = 'god'
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
|
|
||||||
self,
|
|
||||||
parent=None,
|
|
||||||
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
self.search: SearchWidget | None = None
|
|
||||||
|
|
||||||
self.hbox = QHBoxLayout(self)
|
|
||||||
self.hbox.setContentsMargins(0, 0, 0, 0)
|
|
||||||
self.hbox.setSpacing(6)
|
|
||||||
self.hbox.setAlignment(Qt.AlignTop)
|
|
||||||
|
|
||||||
self.vbox = QVBoxLayout()
|
|
||||||
self.vbox.setContentsMargins(0, 0, 0, 0)
|
|
||||||
self.vbox.setSpacing(2)
|
|
||||||
self.vbox.setAlignment(Qt.AlignTop)
|
|
||||||
|
|
||||||
self.hbox.addLayout(self.vbox)
|
|
||||||
|
|
||||||
self._chart_cache: dict[
|
|
||||||
str,
|
|
||||||
tuple[LinkedSplits, LinkedSplits],
|
|
||||||
] = {}
|
|
||||||
|
|
||||||
self.hist_linked: LinkedSplits | None = None
|
|
||||||
self.rt_linked: LinkedSplits | None = None
|
|
||||||
self._active_cursor: Cursor | None = None
|
|
||||||
|
|
||||||
# assigned in the startup func `_async_main()`
|
|
||||||
self._root_n: trio.Nursery = None
|
|
||||||
|
|
||||||
self._widgets: dict[str, QWidget] = {}
|
|
||||||
self._resizing: bool = False
|
|
||||||
|
|
||||||
# TODO: do we need this, when would god get resized
|
|
||||||
# and the window does not? Never right?!
|
|
||||||
# self.reg_for_resize(self)
|
|
||||||
|
|
||||||
# TODO: strat loader/saver that we don't need yet.
|
|
||||||
# def init_strategy_ui(self):
|
|
||||||
# self.toolbar_layout = QHBoxLayout()
|
|
||||||
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
# self.vbox.addLayout(self.toolbar_layout)
|
|
||||||
# self.strategy_box = StrategyBoxWidget(self)
|
|
||||||
# self.toolbar_layout.addWidget(self.strategy_box)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def linkedsplits(self) -> LinkedSplits:
|
|
||||||
return self.rt_linked
|
|
||||||
|
|
||||||
def set_chart_symbols(
|
|
||||||
self,
|
|
||||||
group_key: tuple[str], # of form <fqme>.<providername>
|
|
||||||
all_linked: tuple[LinkedSplits, LinkedSplits], # type: ignore
|
|
||||||
|
|
||||||
) -> None:
|
|
||||||
# re-sort org cache symbol list in LIFO order
|
|
||||||
cache = self._chart_cache
|
|
||||||
cache.pop(group_key, None)
|
|
||||||
cache[group_key] = all_linked
|
|
||||||
|
|
||||||
def get_chart_symbols(
|
|
||||||
self,
|
|
||||||
symbol_key: str,
|
|
||||||
|
|
||||||
) -> tuple[LinkedSplits, LinkedSplits]: # type: ignore
|
|
||||||
return self._chart_cache.get(symbol_key)
|
|
||||||
|
|
||||||
async def load_symbols(
|
|
||||||
self,
|
|
||||||
fqmes: list[str],
|
|
||||||
loglevel: str,
|
|
||||||
reset: bool = False,
|
|
||||||
|
|
||||||
) -> trio.Event:
|
|
||||||
'''
|
|
||||||
Load a new contract into the charting app.
|
|
||||||
|
|
||||||
Expects a ``numpy`` structured array containing all the ohlcv fields.
|
|
||||||
|
|
||||||
'''
|
|
||||||
# NOTE: for now we use the first symbol in the set as the "key"
|
|
||||||
# for the overlay of feeds on the chart.
|
|
||||||
group_key: tuple[str] = tuple(fqmes)
|
|
||||||
|
|
||||||
all_linked = self.get_chart_symbols(group_key)
|
|
||||||
order_mode_started = trio.Event()
|
|
||||||
|
|
||||||
if not self.vbox.isEmpty():
|
|
||||||
|
|
||||||
# XXX: seems to make switching slower?
|
|
||||||
# qframe = self.hist_linked.chart.qframe
|
|
||||||
# if qframe.sidepane is self.search:
|
|
||||||
# qframe.hbox.removeWidget(self.search)
|
|
||||||
|
|
||||||
for linked in [self.rt_linked, self.hist_linked]:
|
|
||||||
# XXX: this is CRITICAL especially with pixel buffer caching
|
|
||||||
linked.hide()
|
|
||||||
linked.unfocus()
|
|
||||||
|
|
||||||
# XXX: pretty sure we don't need this
|
|
||||||
# remove any existing plots?
|
|
||||||
# XXX: ahh we might want to support cache unloading..
|
|
||||||
# self.vbox.removeWidget(linked)
|
|
||||||
|
|
||||||
# switching to a new viewable chart
|
|
||||||
if all_linked is None or reset:
|
|
||||||
from ._display import display_symbol_data
|
|
||||||
|
|
||||||
# we must load a fresh linked charts set
|
|
||||||
self.rt_linked = rt_charts = LinkedSplits(self)
|
|
||||||
self.hist_linked = hist_charts = LinkedSplits(self)
|
|
||||||
|
|
||||||
# spawn new task to start up and update new sub-chart instances
|
|
||||||
self._root_n.start_soon(
|
|
||||||
display_symbol_data,
|
|
||||||
self,
|
|
||||||
fqmes,
|
|
||||||
loglevel,
|
|
||||||
order_mode_started,
|
|
||||||
)
|
|
||||||
|
|
||||||
# self.vbox.addWidget(hist_charts)
|
|
||||||
self.vbox.addWidget(rt_charts)
|
|
||||||
self.set_chart_symbols(
|
|
||||||
group_key,
|
|
||||||
(hist_charts, rt_charts),
|
|
||||||
)
|
|
||||||
|
|
||||||
for linked in [hist_charts, rt_charts]:
|
|
||||||
linked.show()
|
|
||||||
linked.focus()
|
|
||||||
|
|
||||||
await trio.sleep(0)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# symbol is already loaded and ems ready
|
|
||||||
order_mode_started.set()
|
|
||||||
|
|
||||||
self.hist_linked, self.rt_linked = all_linked
|
|
||||||
|
|
||||||
for linked in all_linked:
|
|
||||||
# TODO:
|
|
||||||
# - we'll probably want per-instrument/provider state here?
|
|
||||||
# change the order config form over to the new chart
|
|
||||||
|
|
||||||
# chart is already in memory so just focus it
|
|
||||||
linked.show()
|
|
||||||
linked.focus()
|
|
||||||
linked.graphics_cycle()
|
|
||||||
await trio.sleep(0)
|
|
||||||
|
|
||||||
# resume feeds *after* rendering chart view asap
|
|
||||||
chart = linked.chart
|
|
||||||
if chart:
|
|
||||||
chart.resume_all_feeds()
|
|
||||||
|
|
||||||
# TODO: we need a check to see if the chart
|
|
||||||
# last had the xlast in view, if so then shift so it's
|
|
||||||
# still in view, if the user was viewing history then
|
|
||||||
# do nothing yah?
|
|
||||||
self.rt_linked.chart.main_viz.default_view(
|
|
||||||
do_min_bars=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# if a history chart instance is already up then
|
|
||||||
# set the search widget as its sidepane.
|
|
||||||
hist_chart = self.hist_linked.chart
|
|
||||||
if hist_chart:
|
|
||||||
hist_chart.qframe.set_sidepane(self.search)
|
|
||||||
|
|
||||||
# NOTE: this is really stupid/hard to follow.
|
|
||||||
# we have to reposition the active position nav
|
|
||||||
# **AFTER** applying the search bar as a sidepane
|
|
||||||
# to the newly switched to symbol.
|
|
||||||
await trio.sleep(0)
|
|
||||||
|
|
||||||
# TODO: probably stick this in some kinda `LooknFeel` API?
|
|
||||||
for tracker in self.rt_linked.mode.trackers.values():
|
|
||||||
pp_nav = tracker.nav
|
|
||||||
if tracker.live_pp.cumsize:
|
|
||||||
pp_nav.show()
|
|
||||||
pp_nav.hide_info()
|
|
||||||
else:
|
|
||||||
pp_nav.hide()
|
|
||||||
|
|
||||||
# set window titlebar info
|
|
||||||
symbol = self.rt_linked.mkt
|
|
||||||
if symbol is not None:
|
|
||||||
self.window.setWindowTitle(
|
|
||||||
f'{symbol.fqme} '
|
|
||||||
f'tick:{symbol.size_tick}'
|
|
||||||
)
|
|
||||||
|
|
||||||
return order_mode_started
|
|
||||||
|
|
||||||
def focus(self) -> None:
|
|
||||||
'''
|
|
||||||
Focus the top level widget which in turn focusses the chart
|
|
||||||
ala "view mode".
|
|
||||||
|
|
||||||
'''
|
|
||||||
# go back to view-mode focus (aka chart focus)
|
|
||||||
self.clearFocus()
|
|
||||||
chart = self.rt_linked.chart
|
|
||||||
if chart:
|
|
||||||
chart.setFocus()
|
|
||||||
|
|
||||||
def reg_for_resize(
|
|
||||||
self,
|
|
||||||
widget: QWidget,
|
|
||||||
) -> None:
|
|
||||||
getattr(widget, 'on_resize')
|
|
||||||
self._widgets[widget.mode_name] = widget
|
|
||||||
|
|
||||||
def on_win_resize(self, event: QtCore.QEvent) -> None:
|
|
||||||
'''
|
|
||||||
Top level god widget handler from window (the real yaweh) resize
|
|
||||||
events such that any registered widgets which wish to be
|
|
||||||
notified are invoked using our pythonic `.on_resize()` method
|
|
||||||
api.
|
|
||||||
|
|
||||||
Where we do UX magic to make things not suck B)
|
|
||||||
|
|
||||||
'''
|
|
||||||
if self._resizing:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._resizing = True
|
|
||||||
|
|
||||||
log.info('God widget resize')
|
|
||||||
for name, widget in self._widgets.items():
|
|
||||||
widget.on_resize()
|
|
||||||
|
|
||||||
self._resizing = False
|
|
||||||
|
|
||||||
# on_resize = on_win_resize
|
|
||||||
|
|
||||||
def get_cursor(self) -> Cursor:
|
|
||||||
return self._active_cursor
|
|
||||||
|
|
||||||
def iter_linked(self) -> Iterator[LinkedSplits]:
|
|
||||||
for linked in [self.hist_linked, self.rt_linked]:
|
|
||||||
yield linked
|
|
||||||
|
|
||||||
def resize_all(self) -> None:
|
|
||||||
'''
|
|
||||||
Dynamic resize sequence: adjusts all sub-widgets/charts to
|
|
||||||
sensible default ratios of what space is detected as available
|
|
||||||
on the display / window.
|
|
||||||
|
|
||||||
'''
|
|
||||||
rt_linked = self.rt_linked
|
|
||||||
rt_linked.set_split_sizes()
|
|
||||||
self.rt_linked.resize_sidepanes()
|
|
||||||
self.hist_linked.resize_sidepanes(from_linked=rt_linked)
|
|
||||||
self.search.on_resize()
|
|
||||||
|
|
||||||
|
|
||||||
class ChartnPane(QFrame):
|
class ChartnPane(QFrame):
|
||||||
'''
|
'''
|
||||||
One-off ``QFrame`` composite which pairs a chart
|
One-off ``QFrame`` composite which pairs a chart
|
||||||
|
|
@ -369,9 +91,9 @@ class ChartnPane(QFrame):
|
||||||
https://doc.qt.io/qt-5/qwidget.html#composite-widgets
|
https://doc.qt.io/qt-5/qwidget.html#composite-widgets
|
||||||
|
|
||||||
'''
|
'''
|
||||||
sidepane: FieldsForm | SearchWidget
|
sidepane: FieldsForm|SearchWidget
|
||||||
hbox: QHBoxLayout
|
hbox: QHBoxLayout
|
||||||
chart: ChartPlotWidget | None = None
|
chart: ChartPlotWidget|None = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
@ -387,13 +109,13 @@ class ChartnPane(QFrame):
|
||||||
self.chart = None
|
self.chart = None
|
||||||
|
|
||||||
hbox = self.hbox = QHBoxLayout(self)
|
hbox = self.hbox = QHBoxLayout(self)
|
||||||
hbox.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
hbox.setAlignment(Qt.AlignTop|Qt.AlignLeft)
|
||||||
hbox.setContentsMargins(0, 0, 0, 0)
|
hbox.setContentsMargins(0, 0, 0, 0)
|
||||||
hbox.setSpacing(3)
|
hbox.setSpacing(3)
|
||||||
|
|
||||||
def set_sidepane(
|
def set_sidepane(
|
||||||
self,
|
self,
|
||||||
sidepane: FieldsForm | SearchWidget,
|
sidepane: FieldsForm|SearchWidget,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
# add sidepane **after** chart; place it on axis side
|
# add sidepane **after** chart; place it on axis side
|
||||||
|
|
@ -404,7 +126,7 @@ class ChartnPane(QFrame):
|
||||||
self._sidepane = sidepane
|
self._sidepane = sidepane
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sidepane(self) -> FieldsForm | SearchWidget:
|
def sidepane(self) -> FieldsForm|SearchWidget:
|
||||||
return self._sidepane
|
return self._sidepane
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -419,7 +141,6 @@ class LinkedSplits(QWidget):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
||||||
self,
|
self,
|
||||||
godwidget: GodWidget,
|
godwidget: GodWidget,
|
||||||
|
|
||||||
|
|
@ -450,7 +171,7 @@ class LinkedSplits(QWidget):
|
||||||
# chart-local graphics state that can be passed to
|
# chart-local graphics state that can be passed to
|
||||||
# a ``graphic_update_cycle()`` call by any task wishing to
|
# a ``graphic_update_cycle()`` call by any task wishing to
|
||||||
# update the UI for a given "chart instance".
|
# update the UI for a given "chart instance".
|
||||||
self.display_state: DisplayState | None = None
|
self.display_state: DisplayState|None = None
|
||||||
|
|
||||||
self._mkt: MktPair = None
|
self._mkt: MktPair = None
|
||||||
|
|
||||||
|
|
@ -486,7 +207,7 @@ class LinkedSplits(QWidget):
|
||||||
|
|
||||||
def set_split_sizes(
|
def set_split_sizes(
|
||||||
self,
|
self,
|
||||||
prop: float | None = None,
|
prop: float|None = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -567,8 +288,8 @@ class LinkedSplits(QWidget):
|
||||||
|
|
||||||
# style?
|
# style?
|
||||||
self.chart.setFrameStyle(
|
self.chart.setFrameStyle(
|
||||||
QFrame.Shape.StyledPanel |
|
QFrame.Shape.StyledPanel
|
||||||
QFrame.Shadow.Plain
|
|QFrame.Shadow.Plain
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.chart
|
return self.chart
|
||||||
|
|
@ -580,11 +301,11 @@ class LinkedSplits(QWidget):
|
||||||
shm: ShmArray,
|
shm: ShmArray,
|
||||||
flume: Flume,
|
flume: Flume,
|
||||||
|
|
||||||
array_key: str | None = None,
|
array_key: str|None = None,
|
||||||
style: str = 'line',
|
style: str = 'line',
|
||||||
_is_main: bool = False,
|
_is_main: bool = False,
|
||||||
|
|
||||||
sidepane: QWidget | None = None,
|
sidepane: QWidget|None = None,
|
||||||
draw_kwargs: dict = {},
|
draw_kwargs: dict = {},
|
||||||
|
|
||||||
**cpw_kwargs,
|
**cpw_kwargs,
|
||||||
|
|
@ -687,7 +408,7 @@ class LinkedSplits(QWidget):
|
||||||
cpw.plotItem.vb.linked = self
|
cpw.plotItem.vb.linked = self
|
||||||
cpw.setFrameStyle(
|
cpw.setFrameStyle(
|
||||||
QFrame.Shape.StyledPanel
|
QFrame.Shape.StyledPanel
|
||||||
# | QFrame.Shadow.Plain
|
# |QFrame.Shadow.Plain
|
||||||
)
|
)
|
||||||
|
|
||||||
# don't show the little "autoscale" A label.
|
# don't show the little "autoscale" A label.
|
||||||
|
|
@ -800,7 +521,7 @@ class LinkedSplits(QWidget):
|
||||||
|
|
||||||
def resize_sidepanes(
|
def resize_sidepanes(
|
||||||
self,
|
self,
|
||||||
from_linked: LinkedSplits | None = None,
|
from_linked: LinkedSplits|None = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -874,7 +595,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# TODO: load from config
|
# TODO: load from config
|
||||||
use_open_gl: bool = False,
|
use_open_gl: bool = False,
|
||||||
|
|
||||||
static_yrange: tuple[float, float] | None = None,
|
static_yrange: tuple[float, float]|None = None,
|
||||||
|
|
||||||
parent=None,
|
parent=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
@ -889,7 +610,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
# NOTE: must be set bfore calling ``.mk_vb()``
|
# NOTE: must be set bfore calling ``.mk_vb()``
|
||||||
self.linked = linkedsplits
|
self.linked = linkedsplits
|
||||||
self.sidepane: FieldsForm | None = None
|
self.sidepane: FieldsForm|None = None
|
||||||
|
|
||||||
# source of our custom interactions
|
# source of our custom interactions
|
||||||
self.cv = self.mk_vb(name)
|
self.cv = self.mk_vb(name)
|
||||||
|
|
@ -923,7 +644,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
self.useOpenGL(use_open_gl)
|
self.useOpenGL(use_open_gl)
|
||||||
self.name = name
|
self.name = name
|
||||||
self.data_key = data_key or name
|
self.data_key = data_key or name
|
||||||
self.qframe: ChartnPane | None = None
|
self.qframe: ChartnPane|None = None
|
||||||
|
|
||||||
# scene-local placeholder for book graphics
|
# scene-local placeholder for book graphics
|
||||||
# sizing to avoid overlap with data contents
|
# sizing to avoid overlap with data contents
|
||||||
|
|
@ -934,7 +655,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# registry of overlay curve names
|
# registry of overlay curve names
|
||||||
self._vizs: dict[str, Viz] = {}
|
self._vizs: dict[str, Viz] = {}
|
||||||
|
|
||||||
self.feed: Feed | None = None
|
self.feed: Feed|None = None
|
||||||
|
|
||||||
self._labels = {} # registry of underlying graphics
|
self._labels = {} # registry of underlying graphics
|
||||||
self._ysticks = {} # registry of underlying graphics
|
self._ysticks = {} # registry of underlying graphics
|
||||||
|
|
@ -1027,11 +748,11 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
def increment_view(
|
def increment_view(
|
||||||
self,
|
self,
|
||||||
datums: int = 1,
|
datums: int = 1,
|
||||||
vb: ChartView | None = None,
|
vb: ChartView|None = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Increment the data view ``datums``` steps toward y-axis thus
|
Increment the data view `datums`` steps toward y-axis thus
|
||||||
"following" the current time slot/step/bar.
|
"following" the current time slot/step/bar.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
@ -1041,7 +762,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
x_shift = viz.index_step() * datums
|
x_shift = viz.index_step() * datums
|
||||||
|
|
||||||
if datums >= 300:
|
if datums >= 300:
|
||||||
print("FUCKING FIX THE GLOBAL STEP BULLSHIT")
|
log.warning('FUCKING FIX THE GLOBAL STEP BULLSHIT')
|
||||||
# breakpoint()
|
# breakpoint()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -1058,8 +779,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
def overlay_plotitem(
|
def overlay_plotitem(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
index: int | None = None,
|
index: int|None = None,
|
||||||
axis_title: str | None = None,
|
axis_title: str|None = None,
|
||||||
axis_side: str = 'right',
|
axis_side: str = 'right',
|
||||||
axis_kwargs: dict = {},
|
axis_kwargs: dict = {},
|
||||||
|
|
||||||
|
|
@ -1147,14 +868,14 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
shm: ShmArray,
|
shm: ShmArray,
|
||||||
flume: Flume,
|
flume: Flume,
|
||||||
|
|
||||||
array_key: str | None = None,
|
array_key: str|None = None,
|
||||||
overlay: bool = False,
|
overlay: bool = False,
|
||||||
color: str | None = None,
|
color: str|None = None,
|
||||||
add_label: bool = True,
|
add_label: bool = True,
|
||||||
pi: pg.PlotItem | None = None,
|
pi: pg.PlotItem|None = None,
|
||||||
step_mode: bool = False,
|
step_mode: bool = False,
|
||||||
is_ohlc: bool = False,
|
is_ohlc: bool = False,
|
||||||
add_sticky: None | str = 'right',
|
add_sticky: None|str = 'right',
|
||||||
|
|
||||||
**graphics_kwargs,
|
**graphics_kwargs,
|
||||||
|
|
||||||
|
|
@ -1252,7 +973,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# use the tick size precision for display
|
# use the tick size precision for display
|
||||||
name = name or pi.name
|
name = name or pi.name
|
||||||
mkt: MktPair = self.linked.mkt
|
mkt: MktPair = self.linked.mkt
|
||||||
digits: int | None = None
|
digits: int|None = None
|
||||||
if name in mkt.fqme:
|
if name in mkt.fqme:
|
||||||
digits = mkt.price_tick_digits
|
digits = mkt.price_tick_digits
|
||||||
|
|
||||||
|
|
@ -1286,7 +1007,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
shm: ShmArray,
|
shm: ShmArray,
|
||||||
flume: Flume,
|
flume: Flume,
|
||||||
|
|
||||||
array_key: str | None = None,
|
array_key: str|None = None,
|
||||||
**draw_curve_kwargs,
|
**draw_curve_kwargs,
|
||||||
|
|
||||||
) -> Viz:
|
) -> Viz:
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ import pyqtgraph as pg
|
||||||
|
|
||||||
from piker.ui.qt import (
|
from piker.ui.qt import (
|
||||||
QtWidgets,
|
QtWidgets,
|
||||||
QGraphicsItem,
|
|
||||||
Qt,
|
Qt,
|
||||||
QLineF,
|
QLineF,
|
||||||
QRectF,
|
QRectF,
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,11 @@ from ._style import (
|
||||||
from ._lines import LevelLine
|
from ._lines import LevelLine
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
|
|
||||||
|
# TODO, rm the cycle here!
|
||||||
|
from ._widget import (
|
||||||
|
GodWidget,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._chart import (
|
from ._chart import (
|
||||||
GodWidget,
|
GodWidget,
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ from . import _style
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._chart import GodWidget
|
from ._widget import GodWidget
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
@ -91,6 +91,10 @@ def run_qtractor(
|
||||||
window_type: QMainWindow = None,
|
window_type: QMainWindow = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
'''
|
||||||
|
Run the Qt event loop and embed `trio` via guest mode on it.
|
||||||
|
|
||||||
|
'''
|
||||||
# avoids annoying message when entering debugger from qt loop
|
# avoids annoying message when entering debugger from qt loop
|
||||||
pyqtRemoveInputHook()
|
pyqtRemoveInputHook()
|
||||||
|
|
||||||
|
|
@ -170,7 +174,7 @@ def run_qtractor(
|
||||||
# hook into app focus change events
|
# hook into app focus change events
|
||||||
app.focusChanged.connect(window.on_focus_change)
|
app.focusChanged.connect(window.on_focus_change)
|
||||||
|
|
||||||
instance = main_widget_type()
|
instance: GodWidget = main_widget_type()
|
||||||
instance.window = window
|
instance.window = window
|
||||||
|
|
||||||
# override tractor's defaults
|
# override tractor's defaults
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ log = get_logger(__name__)
|
||||||
def update_fsp_chart(
|
def update_fsp_chart(
|
||||||
viz,
|
viz,
|
||||||
graphics_name: str,
|
graphics_name: str,
|
||||||
array_key: str | None,
|
array_key: str|None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -87,7 +87,11 @@ def update_fsp_chart(
|
||||||
|
|
||||||
# guard against unreadable case
|
# guard against unreadable case
|
||||||
if not last_row:
|
if not last_row:
|
||||||
log.warning(f'Read-race on shm array: {graphics_name}@{shm.token}')
|
log.warning(
|
||||||
|
f'Read-race on shm array,\n'
|
||||||
|
f'graphics_name: {graphics_name!r}\n'
|
||||||
|
f'shm.token: {shm.token}\n'
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# update graphics
|
# update graphics
|
||||||
|
|
@ -203,7 +207,6 @@ async def open_fsp_actor_cluster(
|
||||||
|
|
||||||
|
|
||||||
async def run_fsp_ui(
|
async def run_fsp_ui(
|
||||||
|
|
||||||
linkedsplits: LinkedSplits,
|
linkedsplits: LinkedSplits,
|
||||||
flume: Flume,
|
flume: Flume,
|
||||||
started: trio.Event,
|
started: trio.Event,
|
||||||
|
|
@ -471,7 +474,7 @@ class FspAdmin:
|
||||||
target: Fsp,
|
target: Fsp,
|
||||||
conf: dict[str, dict[str, Any]],
|
conf: dict[str, dict[str, Any]],
|
||||||
|
|
||||||
worker_name: str | None = None,
|
worker_name: str|None = None,
|
||||||
loglevel: str = 'info',
|
loglevel: str = 'info',
|
||||||
|
|
||||||
) -> (Flume, trio.Event):
|
) -> (Flume, trio.Event):
|
||||||
|
|
@ -623,8 +626,10 @@ async def open_fsp_admin(
|
||||||
event.set()
|
event.set()
|
||||||
|
|
||||||
|
|
||||||
|
# TODO, passing in `pikerd` related settings here!
|
||||||
|
# [ ] read in the `tractor` setting for `enable_transports: list`
|
||||||
|
# from the root `conf.toml`!
|
||||||
async def open_vlm_displays(
|
async def open_vlm_displays(
|
||||||
|
|
||||||
linked: LinkedSplits,
|
linked: LinkedSplits,
|
||||||
flume: Flume,
|
flume: Flume,
|
||||||
dvlm: bool = True,
|
dvlm: bool = True,
|
||||||
|
|
@ -634,12 +639,12 @@ async def open_vlm_displays(
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Volume subchart displays.
|
Vlm (volume) subchart displays.
|
||||||
|
|
||||||
Since "volume" is often included directly alongside OHLCV price
|
Since "volume" is often included directly alongside OHLCV price
|
||||||
data, we don't really need a separate FSP-actor + shm array for it
|
data, we don't really need a separate FSP-actor + shm array for
|
||||||
since it's likely already directly adjacent to OHLC samples from the
|
it since it's likely already directly adjacent to OHLC samples
|
||||||
data provider.
|
from the data provider.
|
||||||
|
|
||||||
Further only if volume data is detected (it sometimes isn't provided
|
Further only if volume data is detected (it sometimes isn't provided
|
||||||
eg. forex, certain commodities markets) will volume dependent FSPs
|
eg. forex, certain commodities markets) will volume dependent FSPs
|
||||||
|
|
|
||||||
|
|
@ -237,8 +237,8 @@ class LevelLabel(YAxisLabel):
|
||||||
class L1Label(LevelLabel):
|
class L1Label(LevelLabel):
|
||||||
|
|
||||||
text_flags = (
|
text_flags = (
|
||||||
QtCore.Qt.TextDontClip
|
QtCore.Qt.TextFlag.TextDontClip
|
||||||
| QtCore.Qt.AlignLeft
|
| QtCore.Qt.AlignmentFlag.AlignLeft
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_label_str(
|
def set_label_str(
|
||||||
|
|
|
||||||
|
|
@ -308,6 +308,7 @@ def hcolor(name: str) -> str:
|
||||||
'cool_green': '#33b864',
|
'cool_green': '#33b864',
|
||||||
'dull_green': '#74a662',
|
'dull_green': '#74a662',
|
||||||
'hedge_green': '#518360',
|
'hedge_green': '#518360',
|
||||||
|
'lilypad_green': '#839c84',
|
||||||
|
|
||||||
# orders and alerts
|
# orders and alerts
|
||||||
'alert_yellow': '#e2d083',
|
'alert_yellow': '#e2d083',
|
||||||
|
|
@ -335,6 +336,7 @@ def hcolor(name: str) -> str:
|
||||||
'sell_red': '#b6003f',
|
'sell_red': '#b6003f',
|
||||||
# 'sell_red': '#d00048',
|
# 'sell_red': '#d00048',
|
||||||
'sell_red_light': '#f85462',
|
'sell_red_light': '#f85462',
|
||||||
|
'wine': '#69212d',
|
||||||
|
|
||||||
# 'sell_red': '#f85462',
|
# 'sell_red': '#f85462',
|
||||||
# 'sell_red_light': '#ff4d5c',
|
# 'sell_red_light': '#ff4d5c',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,346 @@
|
||||||
|
# piker: trading gear for hackers
|
||||||
|
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
|
||||||
|
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
'''
|
||||||
|
Root-most (what they call a "central widget") of every Qt-UI-app's
|
||||||
|
window.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import (
|
||||||
|
Iterator,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
)
|
||||||
|
|
||||||
|
import trio
|
||||||
|
|
||||||
|
from piker.ui.qt import (
|
||||||
|
QtCore,
|
||||||
|
Qt,
|
||||||
|
QWidget,
|
||||||
|
QHBoxLayout,
|
||||||
|
QVBoxLayout,
|
||||||
|
)
|
||||||
|
from ..log import get_logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ._search import SearchWidget
|
||||||
|
from ._chart import (
|
||||||
|
LinkedSplits,
|
||||||
|
)
|
||||||
|
from ._cursor import (
|
||||||
|
Cursor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
_godw: GodWidget|None = None
|
||||||
|
|
||||||
|
def get_godw() -> GodWidget:
|
||||||
|
'''
|
||||||
|
Get the top level "god widget", the root/central-most Qt
|
||||||
|
widget-object set as `QMainWindow.setCentralWidget(_godw)`.
|
||||||
|
|
||||||
|
See `piker.ui._exec` for the runtime init details and all the
|
||||||
|
machinery for running `trio` on the Qt event loop in guest mode.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if _godw is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
'No god-widget initialized ??\n'
|
||||||
|
'Have you called `run_qtractor()` yet?\n'
|
||||||
|
)
|
||||||
|
return _godw
|
||||||
|
|
||||||
|
|
||||||
|
class GodWidget(QWidget):
|
||||||
|
'''
|
||||||
|
"Our lord and savior, the holy child of window-shua, there is no
|
||||||
|
widget above thee." - 6|6
|
||||||
|
|
||||||
|
The highest level composed widget which contains layouts for
|
||||||
|
organizing charts as well as other sub-widgets used to control or
|
||||||
|
modify them.
|
||||||
|
|
||||||
|
'''
|
||||||
|
search: SearchWidget
|
||||||
|
mode_name: str = 'god'
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
|
||||||
|
self,
|
||||||
|
parent=None,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.search: SearchWidget|None = None
|
||||||
|
|
||||||
|
self.hbox = QHBoxLayout(self)
|
||||||
|
self.hbox.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.hbox.setSpacing(6)
|
||||||
|
self.hbox.setAlignment(Qt.AlignTop)
|
||||||
|
|
||||||
|
self.vbox = QVBoxLayout()
|
||||||
|
self.vbox.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.vbox.setSpacing(2)
|
||||||
|
self.vbox.setAlignment(Qt.AlignTop)
|
||||||
|
|
||||||
|
self.hbox.addLayout(self.vbox)
|
||||||
|
|
||||||
|
self._chart_cache: dict[
|
||||||
|
str,
|
||||||
|
tuple[LinkedSplits, LinkedSplits],
|
||||||
|
] = {}
|
||||||
|
|
||||||
|
self.hist_linked: LinkedSplits|None = None
|
||||||
|
self.rt_linked: LinkedSplits|None = None
|
||||||
|
self._active_cursor: Cursor|None = None
|
||||||
|
|
||||||
|
# assigned in the startup func `_async_main()`
|
||||||
|
self._root_n: trio.Nursery = None
|
||||||
|
|
||||||
|
self._widgets: dict[str, QWidget] = {}
|
||||||
|
self._resizing: bool = False
|
||||||
|
|
||||||
|
# TODO: do we need this, when would god get resized
|
||||||
|
# and the window does not? Never right?!
|
||||||
|
# self.reg_for_resize(self)
|
||||||
|
|
||||||
|
# TODO: strat loader/saver that we don't need yet.
|
||||||
|
# def init_strategy_ui(self):
|
||||||
|
# self.toolbar_layout = QHBoxLayout()
|
||||||
|
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
# self.vbox.addLayout(self.toolbar_layout)
|
||||||
|
# self.strategy_box = StrategyBoxWidget(self)
|
||||||
|
# self.toolbar_layout.addWidget(self.strategy_box)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def linkedsplits(self) -> LinkedSplits:
|
||||||
|
return self.rt_linked
|
||||||
|
|
||||||
|
def set_chart_symbols(
|
||||||
|
self,
|
||||||
|
group_key: tuple[str], # of form <fqme>.<providername>
|
||||||
|
all_linked: tuple[LinkedSplits, LinkedSplits], # type: ignore
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
# re-sort org cache symbol list in LIFO order
|
||||||
|
cache = self._chart_cache
|
||||||
|
cache.pop(group_key, None)
|
||||||
|
cache[group_key] = all_linked
|
||||||
|
|
||||||
|
def get_chart_symbols(
|
||||||
|
self,
|
||||||
|
symbol_key: str,
|
||||||
|
|
||||||
|
) -> tuple[LinkedSplits, LinkedSplits]: # type: ignore
|
||||||
|
return self._chart_cache.get(symbol_key)
|
||||||
|
|
||||||
|
async def load_symbols(
|
||||||
|
self,
|
||||||
|
fqmes: list[str],
|
||||||
|
loglevel: str,
|
||||||
|
reset: bool = False,
|
||||||
|
|
||||||
|
) -> trio.Event:
|
||||||
|
'''
|
||||||
|
Load a new contract into the charting app.
|
||||||
|
|
||||||
|
Expects a ``numpy`` structured array containing all the ohlcv fields.
|
||||||
|
|
||||||
|
'''
|
||||||
|
# NOTE: for now we use the first symbol in the set as the "key"
|
||||||
|
# for the overlay of feeds on the chart.
|
||||||
|
group_key: tuple[str] = tuple(fqmes)
|
||||||
|
|
||||||
|
all_linked = self.get_chart_symbols(group_key)
|
||||||
|
order_mode_started = trio.Event()
|
||||||
|
|
||||||
|
if not self.vbox.isEmpty():
|
||||||
|
|
||||||
|
# XXX: seems to make switching slower?
|
||||||
|
# qframe = self.hist_linked.chart.qframe
|
||||||
|
# if qframe.sidepane is self.search:
|
||||||
|
# qframe.hbox.removeWidget(self.search)
|
||||||
|
|
||||||
|
for linked in [self.rt_linked, self.hist_linked]:
|
||||||
|
# XXX: this is CRITICAL especially with pixel buffer caching
|
||||||
|
linked.hide()
|
||||||
|
linked.unfocus()
|
||||||
|
|
||||||
|
# XXX: pretty sure we don't need this
|
||||||
|
# remove any existing plots?
|
||||||
|
# XXX: ahh we might want to support cache unloading..
|
||||||
|
# self.vbox.removeWidget(linked)
|
||||||
|
|
||||||
|
# switching to a new viewable chart
|
||||||
|
if all_linked is None or reset:
|
||||||
|
from ._display import display_symbol_data
|
||||||
|
|
||||||
|
# we must load a fresh linked charts set
|
||||||
|
from ._chart import LinkedSplits
|
||||||
|
self.rt_linked = rt_charts = LinkedSplits(self)
|
||||||
|
self.hist_linked = hist_charts = LinkedSplits(self)
|
||||||
|
|
||||||
|
# spawn new task to start up and update new sub-chart instances
|
||||||
|
self._root_n.start_soon(
|
||||||
|
display_symbol_data,
|
||||||
|
self,
|
||||||
|
fqmes,
|
||||||
|
loglevel,
|
||||||
|
order_mode_started,
|
||||||
|
)
|
||||||
|
|
||||||
|
# self.vbox.addWidget(hist_charts)
|
||||||
|
self.vbox.addWidget(rt_charts)
|
||||||
|
self.set_chart_symbols(
|
||||||
|
group_key,
|
||||||
|
(hist_charts, rt_charts),
|
||||||
|
)
|
||||||
|
|
||||||
|
for linked in [hist_charts, rt_charts]:
|
||||||
|
linked.show()
|
||||||
|
linked.focus()
|
||||||
|
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# symbol is already loaded and ems ready
|
||||||
|
order_mode_started.set()
|
||||||
|
|
||||||
|
self.hist_linked, self.rt_linked = all_linked
|
||||||
|
|
||||||
|
for linked in all_linked:
|
||||||
|
# TODO:
|
||||||
|
# - we'll probably want per-instrument/provider state here?
|
||||||
|
# change the order config form over to the new chart
|
||||||
|
|
||||||
|
# chart is already in memory so just focus it
|
||||||
|
linked.show()
|
||||||
|
linked.focus()
|
||||||
|
linked.graphics_cycle()
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
|
# resume feeds *after* rendering chart view asap
|
||||||
|
chart = linked.chart
|
||||||
|
if chart:
|
||||||
|
chart.resume_all_feeds()
|
||||||
|
|
||||||
|
# TODO: we need a check to see if the chart
|
||||||
|
# last had the xlast in view, if so then shift so it's
|
||||||
|
# still in view, if the user was viewing history then
|
||||||
|
# do nothing yah?
|
||||||
|
self.rt_linked.chart.main_viz.default_view(
|
||||||
|
do_min_bars=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# if a history chart instance is already up then
|
||||||
|
# set the search widget as its sidepane.
|
||||||
|
hist_chart = self.hist_linked.chart
|
||||||
|
if hist_chart:
|
||||||
|
hist_chart.qframe.set_sidepane(self.search)
|
||||||
|
|
||||||
|
# NOTE: this is really stupid/hard to follow.
|
||||||
|
# we have to reposition the active position nav
|
||||||
|
# **AFTER** applying the search bar as a sidepane
|
||||||
|
# to the newly switched to symbol.
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
|
# TODO: probably stick this in some kinda `LooknFeel` API?
|
||||||
|
for tracker in self.rt_linked.mode.trackers.values():
|
||||||
|
pp_nav = tracker.nav
|
||||||
|
if tracker.live_pp.cumsize:
|
||||||
|
pp_nav.show()
|
||||||
|
pp_nav.hide_info()
|
||||||
|
else:
|
||||||
|
pp_nav.hide()
|
||||||
|
|
||||||
|
# set window titlebar info
|
||||||
|
symbol = self.rt_linked.mkt
|
||||||
|
if symbol is not None:
|
||||||
|
self.window.setWindowTitle(
|
||||||
|
f'{symbol.fqme} '
|
||||||
|
f'tick:{symbol.size_tick}'
|
||||||
|
)
|
||||||
|
|
||||||
|
return order_mode_started
|
||||||
|
|
||||||
|
def focus(self) -> None:
|
||||||
|
'''
|
||||||
|
Focus the top level widget which in turn focusses the chart
|
||||||
|
ala "view mode".
|
||||||
|
|
||||||
|
'''
|
||||||
|
# go back to view-mode focus (aka chart focus)
|
||||||
|
self.clearFocus()
|
||||||
|
chart = self.rt_linked.chart
|
||||||
|
if chart:
|
||||||
|
chart.setFocus()
|
||||||
|
|
||||||
|
def reg_for_resize(
|
||||||
|
self,
|
||||||
|
widget: QWidget,
|
||||||
|
) -> None:
|
||||||
|
getattr(widget, 'on_resize')
|
||||||
|
self._widgets[widget.mode_name] = widget
|
||||||
|
|
||||||
|
def on_win_resize(self, event: QtCore.QEvent) -> None:
|
||||||
|
'''
|
||||||
|
Top level god widget handler from window (the real yaweh) resize
|
||||||
|
events such that any registered widgets which wish to be
|
||||||
|
notified are invoked using our pythonic `.on_resize()` method
|
||||||
|
api.
|
||||||
|
|
||||||
|
Where we do UX magic to make things not suck B)
|
||||||
|
|
||||||
|
'''
|
||||||
|
if self._resizing:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._resizing = True
|
||||||
|
|
||||||
|
log.info('God widget resize')
|
||||||
|
for name, widget in self._widgets.items():
|
||||||
|
widget.on_resize()
|
||||||
|
|
||||||
|
self._resizing = False
|
||||||
|
|
||||||
|
# on_resize = on_win_resize
|
||||||
|
|
||||||
|
def get_cursor(self) -> Cursor:
|
||||||
|
return self._active_cursor
|
||||||
|
|
||||||
|
def iter_linked(self) -> Iterator[LinkedSplits]:
|
||||||
|
for linked in [self.hist_linked, self.rt_linked]:
|
||||||
|
yield linked
|
||||||
|
|
||||||
|
def resize_all(self) -> None:
|
||||||
|
'''
|
||||||
|
Dynamic resize sequence: adjusts all sub-widgets/charts to
|
||||||
|
sensible default ratios of what space is detected as available
|
||||||
|
on the display / window.
|
||||||
|
|
||||||
|
'''
|
||||||
|
rt_linked = self.rt_linked
|
||||||
|
rt_linked.set_split_sizes()
|
||||||
|
self.rt_linked.resize_sidepanes()
|
||||||
|
self.hist_linked.resize_sidepanes(from_linked=rt_linked)
|
||||||
|
self.search.on_resize()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -40,7 +40,7 @@ from piker.ui.qt import (
|
||||||
)
|
)
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from ._style import _font_small, hcolor
|
from ._style import _font_small, hcolor
|
||||||
from ._chart import GodWidget
|
from ._widget import GodWidget
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
@ -61,9 +61,9 @@ class MultiStatus:
|
||||||
|
|
||||||
self,
|
self,
|
||||||
msg: str,
|
msg: str,
|
||||||
final_msg: str | None = None,
|
final_msg: str|None = None,
|
||||||
clear_on_next: bool = False,
|
clear_on_next: bool = False,
|
||||||
group_key: Union[bool, str] | None = False,
|
group_key: Union[bool, str]|None = False,
|
||||||
|
|
||||||
) -> Union[Callable[..., None], str]:
|
) -> Union[Callable[..., None], str]:
|
||||||
'''
|
'''
|
||||||
|
|
@ -175,11 +175,11 @@ class MainWindow(QMainWindow):
|
||||||
self.setWindowTitle(self.title)
|
self.setWindowTitle(self.title)
|
||||||
|
|
||||||
# set by runtime after `trio` is engaged.
|
# set by runtime after `trio` is engaged.
|
||||||
self.godwidget: GodWidget | None = None
|
self.godwidget: GodWidget|None = None
|
||||||
|
|
||||||
self._status_bar: QStatusBar = None
|
self._status_bar: QStatusBar = None
|
||||||
self._status_label: QLabel = None
|
self._status_label: QLabel = None
|
||||||
self._size: tuple[int, int] | None = None
|
self._size: tuple[int, int]|None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mode_label(self) -> QLabel:
|
def mode_label(self) -> QLabel:
|
||||||
|
|
@ -202,7 +202,7 @@ class MainWindow(QMainWindow):
|
||||||
label.setMargin(2)
|
label.setMargin(2)
|
||||||
label.setAlignment(
|
label.setAlignment(
|
||||||
QtCore.Qt.AlignVCenter
|
QtCore.Qt.AlignVCenter
|
||||||
| QtCore.Qt.AlignRight
|
|QtCore.Qt.AlignRight
|
||||||
)
|
)
|
||||||
self.statusBar().addPermanentWidget(label)
|
self.statusBar().addPermanentWidget(label)
|
||||||
label.show()
|
label.show()
|
||||||
|
|
@ -288,7 +288,7 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
def configure_to_desktop(
|
def configure_to_desktop(
|
||||||
self,
|
self,
|
||||||
size: tuple[int, int] | None = None,
|
size: tuple[int, int]|None = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,14 @@ from piker.data import (
|
||||||
from piker.types import Struct
|
from piker.types import Struct
|
||||||
from piker.log import get_logger
|
from piker.log import get_logger
|
||||||
from piker.ui.qt import Qt
|
from piker.ui.qt import Qt
|
||||||
from ._editors import LineEditor, ArrowEditor
|
from ._editors import (
|
||||||
from ._lines import order_line, LevelLine
|
LineEditor,
|
||||||
|
ArrowEditor,
|
||||||
|
)
|
||||||
|
from ._lines import (
|
||||||
|
order_line,
|
||||||
|
LevelLine,
|
||||||
|
)
|
||||||
from ._position import (
|
from ._position import (
|
||||||
PositionTracker,
|
PositionTracker,
|
||||||
SettingsPane,
|
SettingsPane,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue