Compare commits
5 Commits
feb25af8b8
...
9d01b5367b
| Author | SHA1 | Date |
|---|---|---|
|
|
9d01b5367b | |
|
|
3db0cf9054 | |
|
|
12c346d846 | |
|
|
5af7a82340 | |
|
|
5853bc2404 |
|
|
@ -24,6 +24,7 @@ from functools import partial
|
||||||
from typing import (
|
from typing import (
|
||||||
AsyncIterator,
|
AsyncIterator,
|
||||||
Callable,
|
Callable,
|
||||||
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -33,12 +34,12 @@ import tractor
|
||||||
from tractor.msg import NamespacePath
|
from tractor.msg import NamespacePath
|
||||||
|
|
||||||
from piker.types import Struct
|
from piker.types import Struct
|
||||||
from ..log import get_logger, get_console_log
|
from ..log import (
|
||||||
from .. import data
|
get_logger,
|
||||||
from ..data.feed import (
|
get_console_log,
|
||||||
Flume,
|
|
||||||
Feed,
|
|
||||||
)
|
)
|
||||||
|
from .. import data
|
||||||
|
from ..data.flows import Flume
|
||||||
from ..data._sharedmem import ShmArray
|
from ..data._sharedmem import ShmArray
|
||||||
from ..data._sampling import (
|
from ..data._sampling import (
|
||||||
_default_delay_s,
|
_default_delay_s,
|
||||||
|
|
@ -52,6 +53,9 @@ from ._api import (
|
||||||
)
|
)
|
||||||
from ..toolz import Profiler
|
from ..toolz import Profiler
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..data.feed import Feed
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -169,8 +173,10 @@ class Cascade(Struct):
|
||||||
if not synced:
|
if not synced:
|
||||||
fsp: Fsp = self.fsp
|
fsp: Fsp = self.fsp
|
||||||
log.warning(
|
log.warning(
|
||||||
'***DESYNCED FSP***\n'
|
f'***DESYNCED fsp***\n'
|
||||||
f'{fsp.ns_path}@{src_shm.token}\n'
|
f'------------------\n'
|
||||||
|
f'ns-path: {fsp.ns_path!r}\n'
|
||||||
|
f'shm-token: {src_shm.token}\n'
|
||||||
f'step_diff: {step_diff}\n'
|
f'step_diff: {step_diff}\n'
|
||||||
f'len_diff: {len_diff}\n'
|
f'len_diff: {len_diff}\n'
|
||||||
)
|
)
|
||||||
|
|
@ -398,7 +404,6 @@ async def connect_streams(
|
||||||
|
|
||||||
@tractor.context
|
@tractor.context
|
||||||
async def cascade(
|
async def cascade(
|
||||||
|
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
|
|
||||||
# data feed key
|
# data feed key
|
||||||
|
|
@ -412,7 +417,7 @@ async def cascade(
|
||||||
shm_registry: dict[str, _Token],
|
shm_registry: dict[str, _Token],
|
||||||
|
|
||||||
zero_on_step: bool = False,
|
zero_on_step: bool = False,
|
||||||
loglevel: str | None = None,
|
loglevel: str|None = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -426,7 +431,17 @@ async def cascade(
|
||||||
)
|
)
|
||||||
|
|
||||||
if loglevel:
|
if loglevel:
|
||||||
get_console_log(loglevel)
|
log = get_console_log(
|
||||||
|
loglevel,
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
# XXX TODO!
|
||||||
|
# figure out why this writes a dict to,
|
||||||
|
# `tractor._state._runtime_vars['_root_mailbox']`
|
||||||
|
# XD .. wtf
|
||||||
|
# TODO, solve this as reported in,
|
||||||
|
# https://www.pikers.dev/pikers/piker/issues/70
|
||||||
|
# await tractor.pause()
|
||||||
|
|
||||||
src: Flume = Flume.from_msg(src_flume_addr)
|
src: Flume = Flume.from_msg(src_flume_addr)
|
||||||
dst: Flume = Flume.from_msg(
|
dst: Flume = Flume.from_msg(
|
||||||
|
|
|
||||||
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
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,10 @@ from . import _search
|
||||||
from ..accounting import unpack_fqme
|
from ..accounting import unpack_fqme
|
||||||
from ..data._symcache import open_symcache
|
from ..data._symcache import open_symcache
|
||||||
from ..data.feed import install_brokerd_search
|
from ..data.feed import install_brokerd_search
|
||||||
from ..log import get_logger
|
from ..log import (
|
||||||
|
get_logger,
|
||||||
|
get_console_log,
|
||||||
|
)
|
||||||
from ..service import maybe_spawn_brokerd
|
from ..service import maybe_spawn_brokerd
|
||||||
from ._exec import run_qtractor
|
from ._exec import run_qtractor
|
||||||
|
|
||||||
|
|
@ -87,6 +90,13 @@ 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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# enable chart's console logging
|
||||||
|
if loglevel:
|
||||||
|
get_console_log(
|
||||||
|
level=loglevel,
|
||||||
|
name=__name__,
|
||||||
|
)
|
||||||
|
|
||||||
# set as singleton
|
# set as singleton
|
||||||
_chart._godw = main_widget
|
_chart._godw = main_widget
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,305 +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__)
|
||||||
|
|
||||||
|
|
||||||
_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
|
|
||||||
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
|
||||||
|
|
@ -438,7 +141,6 @@ class LinkedSplits(QWidget):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
||||||
self,
|
self,
|
||||||
godwidget: GodWidget,
|
godwidget: GodWidget,
|
||||||
|
|
||||||
|
|
@ -1050,7 +752,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
) -> 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.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
@ -1060,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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -413,9 +413,18 @@ class Cursor(pg.GraphicsObject):
|
||||||
self,
|
self,
|
||||||
item: pg.GraphicsObject,
|
item: pg.GraphicsObject,
|
||||||
) -> None:
|
) -> None:
|
||||||
assert getattr(item, 'delete'), f"{item} must define a ``.delete()``"
|
assert getattr(
|
||||||
|
item,
|
||||||
|
'delete',
|
||||||
|
), f"{item} must define a ``.delete()``"
|
||||||
self._hovered.add(item)
|
self._hovered.add(item)
|
||||||
|
|
||||||
|
def is_hovered(
|
||||||
|
self,
|
||||||
|
item: pg.GraphicsObject,
|
||||||
|
) -> bool:
|
||||||
|
return item in self._hovered
|
||||||
|
|
||||||
def add_plot(
|
def add_plot(
|
||||||
self,
|
self,
|
||||||
plot: ChartPlotWidget, # noqa
|
plot: ChartPlotWidget, # noqa
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ from piker.ui.qt import QLineF
|
||||||
from ..data._sharedmem import (
|
from ..data._sharedmem import (
|
||||||
ShmArray,
|
ShmArray,
|
||||||
)
|
)
|
||||||
from ..data.feed import Flume
|
from ..data.flows import Flume
|
||||||
from ..data._formatters import (
|
from ..data._formatters import (
|
||||||
IncrementalFormatter,
|
IncrementalFormatter,
|
||||||
OHLCBarsFmtr, # Plain OHLC renderer
|
OHLCBarsFmtr, # Plain OHLC renderer
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,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__)
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ from pyqtgraph import (
|
||||||
functions as fn,
|
functions as fn,
|
||||||
)
|
)
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import tractor
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
from piker.ui.qt import (
|
from piker.ui.qt import (
|
||||||
|
|
@ -72,7 +73,10 @@ if TYPE_CHECKING:
|
||||||
GodWidget,
|
GodWidget,
|
||||||
)
|
)
|
||||||
from ._dataviz import Viz
|
from ._dataviz import Viz
|
||||||
from .order_mode import OrderMode
|
from .order_mode import (
|
||||||
|
OrderMode,
|
||||||
|
Dialog,
|
||||||
|
)
|
||||||
from ._display import DisplayState
|
from ._display import DisplayState
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -130,7 +134,12 @@ async def handle_viewmode_kb_inputs(
|
||||||
|
|
||||||
async for kbmsg in recv_chan:
|
async for kbmsg in recv_chan:
|
||||||
event, etype, key, mods, text = kbmsg.to_tuple()
|
event, etype, key, mods, text = kbmsg.to_tuple()
|
||||||
log.debug(f'key: {key}, mods: {mods}, text: {text}')
|
log.debug(
|
||||||
|
f'View-mode kb-msg received,\n'
|
||||||
|
f'mods: {mods!r}\n'
|
||||||
|
f'key: {key!r}\n'
|
||||||
|
f'text: {text!r}\n'
|
||||||
|
)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
period = now - last
|
period = now - last
|
||||||
|
|
||||||
|
|
@ -158,8 +167,12 @@ async def handle_viewmode_kb_inputs(
|
||||||
# have no previous keys or we do and the min_tap period is
|
# have no previous keys or we do and the min_tap period is
|
||||||
# met
|
# met
|
||||||
if (
|
if (
|
||||||
not fast_key_seq or
|
not fast_key_seq
|
||||||
period <= min_tap and fast_key_seq
|
or (
|
||||||
|
period <= min_tap
|
||||||
|
and
|
||||||
|
fast_key_seq
|
||||||
|
)
|
||||||
):
|
):
|
||||||
fast_key_seq.append(text)
|
fast_key_seq.append(text)
|
||||||
log.debug(f'fast keys seqs {fast_key_seq}')
|
log.debug(f'fast keys seqs {fast_key_seq}')
|
||||||
|
|
@ -174,7 +187,8 @@ async def handle_viewmode_kb_inputs(
|
||||||
# UI REPL-shell, with ctrl-p (for "pause")
|
# UI REPL-shell, with ctrl-p (for "pause")
|
||||||
if (
|
if (
|
||||||
ctrl
|
ctrl
|
||||||
and key in {
|
and
|
||||||
|
key in {
|
||||||
Qt.Key_P,
|
Qt.Key_P,
|
||||||
}
|
}
|
||||||
):
|
):
|
||||||
|
|
@ -184,7 +198,6 @@ async def handle_viewmode_kb_inputs(
|
||||||
vlm_chart = chart.linked.subplots['volume'] # noqa
|
vlm_chart = chart.linked.subplots['volume'] # noqa
|
||||||
vlm_viz = vlm_chart.main_viz # noqa
|
vlm_viz = vlm_chart.main_viz # noqa
|
||||||
dvlm_pi = vlm_chart._vizs['dolla_vlm'].plot # noqa
|
dvlm_pi = vlm_chart._vizs['dolla_vlm'].plot # noqa
|
||||||
import tractor
|
|
||||||
await tractor.pause()
|
await tractor.pause()
|
||||||
view.interact_graphics_cycle()
|
view.interact_graphics_cycle()
|
||||||
|
|
||||||
|
|
@ -192,7 +205,8 @@ async def handle_viewmode_kb_inputs(
|
||||||
# shown data `Viz`s for the current chart app.
|
# shown data `Viz`s for the current chart app.
|
||||||
if (
|
if (
|
||||||
ctrl
|
ctrl
|
||||||
and key in {
|
and
|
||||||
|
key in {
|
||||||
Qt.Key_R,
|
Qt.Key_R,
|
||||||
}
|
}
|
||||||
):
|
):
|
||||||
|
|
@ -231,7 +245,8 @@ async def handle_viewmode_kb_inputs(
|
||||||
key == Qt.Key_Escape
|
key == Qt.Key_Escape
|
||||||
or (
|
or (
|
||||||
ctrl
|
ctrl
|
||||||
and key == Qt.Key_C
|
and
|
||||||
|
key == Qt.Key_C
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
# ctrl-c as cancel
|
# ctrl-c as cancel
|
||||||
|
|
@ -242,17 +257,35 @@ async def handle_viewmode_kb_inputs(
|
||||||
# cancel order or clear graphics
|
# cancel order or clear graphics
|
||||||
if (
|
if (
|
||||||
key == Qt.Key_C
|
key == Qt.Key_C
|
||||||
or key == Qt.Key_Delete
|
or
|
||||||
|
key == Qt.Key_Delete
|
||||||
):
|
):
|
||||||
|
# log.info('Handling <c> hotkey!')
|
||||||
|
try:
|
||||||
|
dialogs: list[Dialog] = order_mode.cancel_orders_under_cursor()
|
||||||
|
except BaseException:
|
||||||
|
log.exception('Failed to cancel orders !?\n')
|
||||||
|
await tractor.pause()
|
||||||
|
|
||||||
order_mode.cancel_orders_under_cursor()
|
if not dialogs:
|
||||||
|
log.warning(
|
||||||
|
'No orders were cancelled?\n'
|
||||||
|
'Is there an order-line under the cursor?\n'
|
||||||
|
'If you think there IS your DE might be "hiding the mouse" before '
|
||||||
|
'we rx the keyboard input via Qt..\n'
|
||||||
|
'=> Check your DE and/or TWM settings to be sure! <=\n'
|
||||||
|
)
|
||||||
|
# ^TODO?, some way to detect if there's lines and
|
||||||
|
# the DE is cuckin with things?
|
||||||
|
# await tractor.pause()
|
||||||
|
|
||||||
# View modes
|
# View modes
|
||||||
if (
|
if (
|
||||||
ctrl
|
ctrl
|
||||||
and (
|
and (
|
||||||
key == Qt.Key_Equal
|
key == Qt.Key_Equal
|
||||||
or key == Qt.Key_I
|
or
|
||||||
|
key == Qt.Key_I
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
view.wheelEvent(
|
view.wheelEvent(
|
||||||
|
|
@ -264,7 +297,8 @@ async def handle_viewmode_kb_inputs(
|
||||||
ctrl
|
ctrl
|
||||||
and (
|
and (
|
||||||
key == Qt.Key_Minus
|
key == Qt.Key_Minus
|
||||||
or key == Qt.Key_O
|
or
|
||||||
|
key == Qt.Key_O
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
view.wheelEvent(
|
view.wheelEvent(
|
||||||
|
|
@ -275,7 +309,8 @@ async def handle_viewmode_kb_inputs(
|
||||||
|
|
||||||
elif (
|
elif (
|
||||||
not ctrl
|
not ctrl
|
||||||
and key == Qt.Key_R
|
and
|
||||||
|
key == Qt.Key_R
|
||||||
):
|
):
|
||||||
# NOTE: seems that if we don't yield a Qt render
|
# NOTE: seems that if we don't yield a Qt render
|
||||||
# cycle then the m4 downsampled curves will show here
|
# cycle then the m4 downsampled curves will show here
|
||||||
|
|
@ -477,7 +512,8 @@ async def handle_viewmode_mouse(
|
||||||
# view.raiseContextMenu(event)
|
# view.raiseContextMenu(event)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
view.order_mode.active and
|
view.order_mode.active
|
||||||
|
and
|
||||||
button == QtCore.Qt.LeftButton
|
button == QtCore.Qt.LeftButton
|
||||||
):
|
):
|
||||||
# when in order mode, submit execution
|
# when in order mode, submit execution
|
||||||
|
|
@ -781,7 +817,8 @@ class ChartView(ViewBox):
|
||||||
|
|
||||||
# Scale or translate based on mouse button
|
# Scale or translate based on mouse button
|
||||||
if btn & (
|
if btn & (
|
||||||
QtCore.Qt.LeftButton | QtCore.Qt.MidButton
|
QtCore.Qt.LeftButton
|
||||||
|
| QtCore.Qt.MidButton
|
||||||
):
|
):
|
||||||
# zoom y-axis ONLY when click-n-drag on it
|
# zoom y-axis ONLY when click-n-drag on it
|
||||||
# if axis == 1:
|
# if axis == 1:
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,13 @@ from ._anchors import (
|
||||||
from ..calc import humanize
|
from ..calc import humanize
|
||||||
from ._label import Label
|
from ._label import Label
|
||||||
from ._style import hcolor, _font
|
from ._style import hcolor, _font
|
||||||
|
from ..log import get_logger
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._cursor import Cursor
|
from ._cursor import Cursor
|
||||||
|
|
||||||
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# TODO: probably worth investigating if we can
|
# TODO: probably worth investigating if we can
|
||||||
# make .boundingRect() faster:
|
# make .boundingRect() faster:
|
||||||
|
|
@ -346,7 +349,7 @@ class LevelLine(pg.InfiniteLine):
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
# TODO: enter labels edit mode
|
# TODO: enter labels edit mode
|
||||||
print(f'double click {ev}')
|
log.debug(f'double click {ev}')
|
||||||
|
|
||||||
def paint(
|
def paint(
|
||||||
self,
|
self,
|
||||||
|
|
@ -460,10 +463,19 @@ class LevelLine(pg.InfiniteLine):
|
||||||
# hovered
|
# hovered
|
||||||
if (
|
if (
|
||||||
not ev.isExit()
|
not ev.isExit()
|
||||||
and ev.acceptDrags(QtCore.Qt.LeftButton)
|
and
|
||||||
|
ev.acceptDrags(QtCore.Qt.LeftButton)
|
||||||
):
|
):
|
||||||
# if already hovered we don't need to run again
|
# if already hovered we don't need to run again
|
||||||
if self.mouseHovering is True:
|
if (
|
||||||
|
self.mouseHovering is True
|
||||||
|
and
|
||||||
|
cur.is_hovered(self)
|
||||||
|
):
|
||||||
|
log.debug(
|
||||||
|
f'Already hovering ??\n'
|
||||||
|
f'cur._hovered: {cur._hovered!r}\n'
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.only_show_markers_on_hover:
|
if self.only_show_markers_on_hover:
|
||||||
|
|
@ -480,6 +492,7 @@ class LevelLine(pg.InfiniteLine):
|
||||||
cur._y_label_update = False
|
cur._y_label_update = False
|
||||||
|
|
||||||
# add us to cursor state
|
# add us to cursor state
|
||||||
|
log.debug(f'Adding line {self!r}\n')
|
||||||
cur.add_hovered(self)
|
cur.add_hovered(self)
|
||||||
|
|
||||||
if self._hide_xhair_on_hover:
|
if self._hide_xhair_on_hover:
|
||||||
|
|
@ -507,6 +520,7 @@ class LevelLine(pg.InfiniteLine):
|
||||||
|
|
||||||
self.currentPen = self.pen
|
self.currentPen = self.pen
|
||||||
|
|
||||||
|
log.debug(f'Removing line {self!r}\n')
|
||||||
cur._hovered.remove(self)
|
cur._hovered.remove(self)
|
||||||
|
|
||||||
if self.only_show_markers_on_hover:
|
if self.only_show_markers_on_hover:
|
||||||
|
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,6 @@ from ._style import _font
|
||||||
from ._forms import open_form_input_handling
|
from ._forms import open_form_input_handling
|
||||||
from ._notify import notify_from_ems_status_msg
|
from ._notify import notify_from_ems_status_msg
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._chart import (
|
from ._chart import (
|
||||||
ChartPlotWidget,
|
ChartPlotWidget,
|
||||||
|
|
@ -436,7 +435,7 @@ class OrderMode:
|
||||||
lines=lines,
|
lines=lines,
|
||||||
last_status_close=self.multistatus.open_status(
|
last_status_close=self.multistatus.open_status(
|
||||||
f'submitting {order.exec_mode}-{order.action}',
|
f'submitting {order.exec_mode}-{order.action}',
|
||||||
final_msg=f'submitted {order.exec_mode}-{order.action}',
|
# final_msg=f'submitted {order.exec_mode}-{order.action}',
|
||||||
clear_on_next=True,
|
clear_on_next=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -528,7 +527,7 @@ class OrderMode:
|
||||||
# a submission is the start of a new order dialog
|
# a submission is the start of a new order dialog
|
||||||
dialog = self.dialogs[uuid]
|
dialog = self.dialogs[uuid]
|
||||||
dialog.lines = lines
|
dialog.lines = lines
|
||||||
cls: Callable | None = dialog.last_status_close
|
cls: Callable|None = dialog.last_status_close
|
||||||
if cls:
|
if cls:
|
||||||
cls()
|
cls()
|
||||||
|
|
||||||
|
|
@ -658,7 +657,7 @@ class OrderMode:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def cancel_orders_under_cursor(self) -> list[str]:
|
def cancel_orders_under_cursor(self) -> list[Dialog]:
|
||||||
return self.cancel_orders(
|
return self.cancel_orders(
|
||||||
self.oids_from_lines(
|
self.oids_from_lines(
|
||||||
self.lines.lines_under_cursor()
|
self.lines.lines_under_cursor()
|
||||||
|
|
@ -687,24 +686,28 @@ class OrderMode:
|
||||||
self,
|
self,
|
||||||
oids: list[str],
|
oids: list[str],
|
||||||
|
|
||||||
) -> None:
|
) -> list[Dialog]:
|
||||||
'''
|
'''
|
||||||
Cancel all orders from a list of order ids: `oids`.
|
Cancel all orders from a list of order ids: `oids`.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
key = self.multistatus.open_status(
|
# key = self.multistatus.open_status(
|
||||||
f'cancelling {len(oids)} orders',
|
# f'cancelling {len(oids)} orders',
|
||||||
final_msg=f'cancelled orders:\n{oids}',
|
# final_msg=f'cancelled orders:\n{oids}',
|
||||||
group_key=True
|
# group_key=True
|
||||||
)
|
# )
|
||||||
|
dialogs: list[Dialog] = []
|
||||||
for oid in oids:
|
for oid in oids:
|
||||||
if dialog := self.dialogs.get(oid):
|
if dialog := self.dialogs.get(oid):
|
||||||
self.client.cancel_nowait(uuid=oid)
|
self.client.cancel_nowait(uuid=oid)
|
||||||
cancel_status_close = self.multistatus.open_status(
|
# cancel_status_close = self.multistatus.open_status(
|
||||||
f'cancelling order {oid}',
|
# f'cancelling order {oid}',
|
||||||
group_key=key,
|
# group_key=key,
|
||||||
)
|
# )
|
||||||
dialog.last_status_close = cancel_status_close
|
# dialog.last_status_close = cancel_status_close
|
||||||
|
dialogs.append(dialog)
|
||||||
|
|
||||||
|
return dialogs
|
||||||
|
|
||||||
def cancel_all_orders(self) -> None:
|
def cancel_all_orders(self) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -776,7 +779,6 @@ class OrderMode:
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def open_order_mode(
|
async def open_order_mode(
|
||||||
|
|
||||||
feed: Feed,
|
feed: Feed,
|
||||||
godw: GodWidget,
|
godw: GodWidget,
|
||||||
fqme: str,
|
fqme: str,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue