First (untested) draft remote annotation ctl API
Since we can and want to eventually allow remote control of pretty much all UIs, this drafts out a new `.ui._remote_ctl` module with a new `@tractor.context` called `remote_annotate()` which simply starts a msg loop which allows for (eventual) initial control of a `SelectRect` through IPC msgs. Remote controller impl deats: - make `._display.graphics_update_loop()` set a `._remote_ctl._dss: dict` for all chart actor-global `DisplayState` instances which can then be controlled from the `remote_annotate()` handler task. - also stash any remote client controller `tractor.Context` handles in a module var for broadband IPC cancellation on any display loop shutdown. - draft a further global map to track graphics object instances since likely we'll want to support remote mutation where the client can use the `id(obj): int` key as an IPC handle/uuid. - just draft out a client-side `@acm` for now: `open_annots_client()` to be filled out in up coming commits. UI component tweaks in support of the above: - change/add `SelectRect.set_view_pos()` and `.set_scene_pos()` to allow specifying the rect coords in either of the scene or viewbox domains. - use these new apis in the interaction loop. - add a `SelectRect.add_to_view()` to avoid having annotation client code knowing "how" a graphics obj needs to be added and can instead just pass only the target `ChartView` during init. - drop all the status label updates from the display loop since they don't really work all the time, and probably it's not a feature we want to keep in the longer term (over just console output and/or using the status bar for simpler "current state / mkt" infos). - allows a bit of simplification of `.ui._fsp` method APIs to not pass around status (bar) callbacks as well!distribute_dis
parent
ab84303da7
commit
8d324acf91
|
@ -56,8 +56,8 @@ _line_styles: dict[str, int] = {
|
|||
|
||||
class FlowGraphic(pg.GraphicsObject):
|
||||
'''
|
||||
Base class with minimal interface for `QPainterPath` implemented,
|
||||
real-time updated "data flow" graphics.
|
||||
Base class with minimal interface for `QPainterPath`
|
||||
implemented, real-time updated "data flow" graphics.
|
||||
|
||||
See subtypes below.
|
||||
|
||||
|
@ -167,11 +167,12 @@ class FlowGraphic(pg.GraphicsObject):
|
|||
return None
|
||||
|
||||
# XXX: due to a variety of weird jitter bugs and "smearing"
|
||||
# artifacts when click-drag panning and viewing history time series,
|
||||
# we offer this ctx-mngr interface to allow temporarily disabling
|
||||
# Qt's graphics caching mode; this is now currently used from
|
||||
# ``ChartView.start/signal_ic()`` methods which also disable the
|
||||
# rt-display loop when the user is moving around a view.
|
||||
# artifacts when click-drag panning and viewing history time
|
||||
# series, we offer this ctx-mngr interface to allow temporarily
|
||||
# disabling Qt's graphics caching mode; this is now currently
|
||||
# used from ``ChartView.start/signal_ic()`` methods which also
|
||||
# disable the rt-display loop when the user is moving around
|
||||
# a view.
|
||||
@cm
|
||||
def reset_cache(self) -> None:
|
||||
try:
|
||||
|
|
|
@ -563,7 +563,8 @@ class Viz(Struct):
|
|||
|
||||
def view_range(self) -> tuple[int, int]:
|
||||
'''
|
||||
Return the start and stop x-indexes for the managed ``ViewBox``.
|
||||
Return the start and stop x-indexes for the managed
|
||||
``ViewBox``.
|
||||
|
||||
'''
|
||||
vr = self.plot.viewRect()
|
||||
|
|
|
@ -470,54 +470,64 @@ async def graphics_update_loop(
|
|||
if ds.hist_vars['i_last'] < ds.hist_vars['i_last_append']:
|
||||
await tractor.pause()
|
||||
|
||||
# main real-time quotes update loop
|
||||
stream: tractor.MsgStream
|
||||
async with feed.open_multi_stream() as stream:
|
||||
assert stream
|
||||
async for quotes in stream:
|
||||
quote_period = time.time() - last_quote_s
|
||||
quote_rate = round(
|
||||
1/quote_period, 1) if quote_period > 0 else float('inf')
|
||||
if (
|
||||
quote_period <= 1/_quote_throttle_rate
|
||||
try:
|
||||
from . import _remote_ctl
|
||||
_remote_ctl._dss = dss
|
||||
|
||||
# in the absolute worst case we shouldn't see more then
|
||||
# twice the expected throttle rate right!?
|
||||
# and quote_rate >= _quote_throttle_rate * 2
|
||||
and quote_rate >= display_rate
|
||||
):
|
||||
pass
|
||||
# log.warning(f'High quote rate {mkt.fqme}: {quote_rate}')
|
||||
|
||||
last_quote_s = time.time()
|
||||
|
||||
for fqme, quote in quotes.items():
|
||||
ds = dss[fqme]
|
||||
ds.quotes = quote
|
||||
rt_pi, hist_pi = pis[fqme]
|
||||
|
||||
# chart isn't active/shown so skip render cycle and
|
||||
# pause feed(s)
|
||||
# main real-time quotes update loop
|
||||
stream: tractor.MsgStream
|
||||
async with feed.open_multi_stream() as stream:
|
||||
assert stream
|
||||
async for quotes in stream:
|
||||
quote_period = time.time() - last_quote_s
|
||||
quote_rate = round(
|
||||
1/quote_period, 1) if quote_period > 0 else float('inf')
|
||||
if (
|
||||
fast_chart.linked.isHidden()
|
||||
or not rt_pi.isVisible()
|
||||
quote_period <= 1/_quote_throttle_rate
|
||||
|
||||
# in the absolute worst case we shouldn't see more then
|
||||
# twice the expected throttle rate right!?
|
||||
# and quote_rate >= _quote_throttle_rate * 2
|
||||
and quote_rate >= display_rate
|
||||
):
|
||||
print(f'{fqme} skipping update for HIDDEN CHART')
|
||||
fast_chart.pause_all_feeds()
|
||||
continue
|
||||
pass
|
||||
# log.warning(f'High quote rate {mkt.fqme}: {quote_rate}')
|
||||
|
||||
ic = fast_chart.view._in_interact
|
||||
if ic:
|
||||
fast_chart.pause_all_feeds()
|
||||
print(f'{fqme} PAUSING DURING INTERACTION')
|
||||
await ic.wait()
|
||||
fast_chart.resume_all_feeds()
|
||||
last_quote_s = time.time()
|
||||
|
||||
# sync call to update all graphics/UX components.
|
||||
graphics_update_cycle(
|
||||
ds,
|
||||
quote,
|
||||
)
|
||||
for fqme, quote in quotes.items():
|
||||
ds = dss[fqme]
|
||||
ds.quotes = quote
|
||||
rt_pi, hist_pi = pis[fqme]
|
||||
|
||||
# chart isn't active/shown so skip render cycle and
|
||||
# pause feed(s)
|
||||
if (
|
||||
fast_chart.linked.isHidden()
|
||||
or not rt_pi.isVisible()
|
||||
):
|
||||
print(f'{fqme} skipping update for HIDDEN CHART')
|
||||
fast_chart.pause_all_feeds()
|
||||
continue
|
||||
|
||||
ic = fast_chart.view._in_interact
|
||||
if ic:
|
||||
fast_chart.pause_all_feeds()
|
||||
print(f'{fqme} PAUSING DURING INTERACTION')
|
||||
await ic.wait()
|
||||
fast_chart.resume_all_feeds()
|
||||
|
||||
# sync call to update all graphics/UX components.
|
||||
graphics_update_cycle(
|
||||
ds,
|
||||
quote,
|
||||
)
|
||||
|
||||
finally:
|
||||
# XXX: cancel any remote annotation control ctxs
|
||||
_remote_ctl._dss = None
|
||||
for ctx in _remote_ctl._ctxs:
|
||||
await ctx.cancel()
|
||||
|
||||
|
||||
def graphics_update_cycle(
|
||||
|
@ -1235,7 +1245,7 @@ async def display_symbol_data(
|
|||
fast from a cached watch-list.
|
||||
|
||||
'''
|
||||
sbar = godwidget.window.status_bar
|
||||
# sbar = godwidget.window.status_bar
|
||||
# historical data fetch
|
||||
# brokermod = brokers.get_brokermod(provider)
|
||||
|
||||
|
@ -1245,11 +1255,11 @@ async def display_symbol_data(
|
|||
# group_key=loading_sym_key,
|
||||
# )
|
||||
|
||||
for fqme in fqmes:
|
||||
loading_sym_key = sbar.open_status(
|
||||
f'loading {fqme} ->',
|
||||
group_key=True
|
||||
)
|
||||
# for fqme in fqmes:
|
||||
# loading_sym_key = sbar.open_status(
|
||||
# f'loading {fqme} ->',
|
||||
# group_key=True
|
||||
# )
|
||||
|
||||
# (TODO: make this not so shit XD)
|
||||
# close group status once a symbol feed fully loads to view.
|
||||
|
@ -1422,7 +1432,7 @@ async def display_symbol_data(
|
|||
start_fsp_displays,
|
||||
rt_linked,
|
||||
flume,
|
||||
loading_sym_key,
|
||||
# loading_sym_key,
|
||||
loglevel,
|
||||
)
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ from ..log import get_logger
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from ._chart import GodWidget
|
||||
from ._interaction import ChartView
|
||||
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
@ -261,7 +262,7 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
|||
super().__init__(0, 0, 1, 1)
|
||||
|
||||
# self.rbScaleBox = QGraphicsRectItem(0, 0, 1, 1)
|
||||
self.vb = viewbox
|
||||
self.vb: ViewBox = viewbox
|
||||
self._chart: 'ChartPlotWidget' = None # noqa
|
||||
|
||||
# override selection box color
|
||||
|
@ -297,6 +298,19 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
|||
'sigma: {std:.2f}',
|
||||
]
|
||||
|
||||
def add_to_view(
|
||||
self,
|
||||
view: ChartView,
|
||||
) -> None:
|
||||
'''
|
||||
Self-defined view hookup impl.
|
||||
|
||||
'''
|
||||
view.addItem(
|
||||
self,
|
||||
ignoreBounds=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def chart(self) -> 'ChartPlotWidget': # noqa
|
||||
return self._chart
|
||||
|
@ -324,21 +338,40 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
|||
self.vb.mapFromView(self._abs_top_right)
|
||||
)
|
||||
|
||||
def set_pos(
|
||||
def set_scen_pos(
|
||||
self,
|
||||
p1: QPointF,
|
||||
p2: QPointF
|
||||
|
||||
) -> None:
|
||||
"""Set position of selection rect and accompanying label, move
|
||||
label to match.
|
||||
'''
|
||||
Set position from scene coords of selection rect (normally
|
||||
from mouse position) and accompanying label, move label to
|
||||
match.
|
||||
|
||||
"""
|
||||
'''
|
||||
# map to view coords
|
||||
self.set_view_pos(
|
||||
self.vb.mapToView(p1),
|
||||
self.vb.mapToView(p2),
|
||||
)
|
||||
|
||||
def set_view_pos(
|
||||
self,
|
||||
start_pos: QPointF,
|
||||
end_pos: QPointF,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Set position from `ViewBox` coords (i.e. from the actual
|
||||
data domain) of rect (and any accompanying label which is
|
||||
moved to match).
|
||||
|
||||
'''
|
||||
# https://doc.qt.io/qt-5/qgraphicsproxywidget.html
|
||||
if self._label_proxy is None:
|
||||
# https://doc.qt.io/qt-5/qgraphicsproxywidget.html
|
||||
self._label_proxy = self.vb.scene().addWidget(self._label)
|
||||
|
||||
start_pos = self.vb.mapToView(p1)
|
||||
end_pos = self.vb.mapToView(p2)
|
||||
self._label_proxy = self.vb.scene(
|
||||
).addWidget(self._label)
|
||||
|
||||
# map to view coords and update area
|
||||
r = QtCore.QRectF(start_pos, end_pos)
|
||||
|
@ -398,8 +431,9 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
|||
# self._label.show()
|
||||
|
||||
def clear(self):
|
||||
"""Clear the selection box from view.
|
||||
'''
|
||||
Clear the selection box from view.
|
||||
|
||||
"""
|
||||
'''
|
||||
self._label.hide()
|
||||
self.hide()
|
||||
|
|
|
@ -560,7 +560,7 @@ class FspAdmin:
|
|||
conf: dict, # yeah probably dumb..
|
||||
loglevel: str = 'error',
|
||||
|
||||
) -> (trio.Event, ChartPlotWidget):
|
||||
) -> trio.Event:
|
||||
|
||||
flume, started = await self.start_engine_task(
|
||||
target,
|
||||
|
@ -927,7 +927,7 @@ async def start_fsp_displays(
|
|||
|
||||
linked: LinkedSplits,
|
||||
flume: Flume,
|
||||
group_status_key: str,
|
||||
# group_status_key: str,
|
||||
loglevel: str,
|
||||
|
||||
) -> None:
|
||||
|
@ -974,21 +974,23 @@ async def start_fsp_displays(
|
|||
flume,
|
||||
) as admin,
|
||||
):
|
||||
statuses = []
|
||||
statuses: list[trio.Event] = []
|
||||
for target, conf in fsp_conf.items():
|
||||
started = await admin.open_fsp_chart(
|
||||
started: trio.Event = await admin.open_fsp_chart(
|
||||
target,
|
||||
conf,
|
||||
)
|
||||
done = linked.window().status_bar.open_status(
|
||||
f'loading fsp, {target}..',
|
||||
group_key=group_status_key,
|
||||
)
|
||||
statuses.append((started, done))
|
||||
# done = linked.window().status_bar.open_status(
|
||||
# f'loading fsp, {target}..',
|
||||
# group_key=group_status_key,
|
||||
# )
|
||||
# statuses.append((started, done))
|
||||
statuses.append(started)
|
||||
|
||||
for fsp_loaded, status_cb in statuses:
|
||||
# for fsp_loaded, status_cb in statuses:
|
||||
for fsp_loaded in statuses:
|
||||
await fsp_loaded.wait()
|
||||
profiler(f'attached to fsp portal: {target}')
|
||||
status_cb()
|
||||
# status_cb()
|
||||
|
||||
# blocks on nursery until all fsp actors complete
|
||||
|
|
|
@ -500,7 +500,11 @@ class ChartView(ViewBox):
|
|||
|
||||
# add our selection box annotator
|
||||
self.select_box = SelectRect(self)
|
||||
self.addItem(self.select_box, ignoreBounds=True)
|
||||
self.select_box.add_to_view(self)
|
||||
# self.addItem(
|
||||
# self.select_box,
|
||||
# ignoreBounds=True,
|
||||
# )
|
||||
|
||||
self.mode = None
|
||||
self.order_mode: bool = False
|
||||
|
@ -761,14 +765,17 @@ class ChartView(ViewBox):
|
|||
# This is the final position in the drag
|
||||
if ev.isFinish():
|
||||
|
||||
self.select_box.mouse_drag_released(down_pos, pos)
|
||||
self.select_box.mouse_drag_released(
|
||||
down_pos,
|
||||
pos,
|
||||
)
|
||||
|
||||
# NOTE: think of this as a `.mouse_drag_release()`
|
||||
# (bc HINT that's what i called the shit ass
|
||||
# method that wrapped this call [yes, as a single
|
||||
# fucking call] originally.. you bish, guille)
|
||||
# Bo.. oraleeee
|
||||
self.select_box.set_pos(
|
||||
self.select_box.set_scen_pos(
|
||||
down_pos,
|
||||
pos,
|
||||
)
|
||||
|
@ -786,7 +793,10 @@ class ChartView(ViewBox):
|
|||
|
||||
else:
|
||||
print('drag finish?')
|
||||
self.select_box.set_pos(down_pos, pos)
|
||||
self.select_box.set_scen_pos(
|
||||
down_pos,
|
||||
pos,
|
||||
)
|
||||
|
||||
# update shape of scale box
|
||||
# self.updateScaleBox(ev.buttonDownPos(), ev.pos())
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
# 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/>.
|
||||
|
||||
'''
|
||||
Remote control tasks for sending annotations (and maybe more cmds)
|
||||
to a chart from some other actor.
|
||||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
from contextlib import asynccontextmanager as acm
|
||||
|
||||
import tractor
|
||||
# import trio
|
||||
from PyQt5.QtWidgets import (
|
||||
QGraphicsItem,
|
||||
)
|
||||
|
||||
from piker.log import get_logger
|
||||
from ._display import DisplayState
|
||||
from ._editors import SelectRect
|
||||
from ._chart import ChartPlotWidget
|
||||
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
# NOTE: this is set by the `._display.graphics_update_loop()` once
|
||||
# all chart widgets / Viz per flume have been initialized allowing
|
||||
# for remote annotation (control) of any chart-actor's mkt feed by
|
||||
# fqme lookup Bo
|
||||
_dss: dict[str, DisplayState] | None = None
|
||||
|
||||
# stash each and every client connection so that they can all
|
||||
# be cancelled on shutdown/error.
|
||||
_ctxs: set[tractor.Context] = set()
|
||||
|
||||
# global map of all uniquely created annotation-graphics
|
||||
# so that they can be mutated (eventually) by a client.
|
||||
_annots: dict[int, QGraphicsItem] = {}
|
||||
|
||||
|
||||
@tractor.context
|
||||
async def remote_annotate(
|
||||
ctx: tractor.Context,
|
||||
) -> None:
|
||||
|
||||
global _dss, _ctxs
|
||||
assert _dss
|
||||
|
||||
_ctxs.add(ctx)
|
||||
|
||||
# send back full fqme symbology to caller
|
||||
await ctx.started(list(_dss))
|
||||
|
||||
async with ctx.open_stream() as annot_req_stream:
|
||||
async for msg in annot_req_stream:
|
||||
match msg:
|
||||
case {
|
||||
'fqme': fqme,
|
||||
'cmd': 'SelectRect',
|
||||
'color': color,
|
||||
'timeframe': timeframe,
|
||||
# 'meth': str(meth),
|
||||
'meth': 'set_view_pos',
|
||||
'kwargs': {
|
||||
'start_pos': tuple(start_pos),
|
||||
'end_pos': tuple(end_pos),
|
||||
},
|
||||
}:
|
||||
ds: DisplayState = _dss[fqme]
|
||||
chart: ChartPlotWidget = {
|
||||
60: ds.hist_chart,
|
||||
1: ds.chart,
|
||||
}[timeframe]
|
||||
|
||||
# annot type lookup from cmd
|
||||
rect = SelectRect(
|
||||
chart.cv,
|
||||
|
||||
# TODO: pull from conf.toml?
|
||||
color=color or 'dad_blue',
|
||||
)
|
||||
rect.set_view_pos(
|
||||
start_pos=start_pos,
|
||||
end_pos=end_pos,
|
||||
)
|
||||
await annot_req_stream.send(id(rect))
|
||||
|
||||
case _:
|
||||
log.error(
|
||||
'Unknown remote annotation cmd:\n'
|
||||
f'{msg}'
|
||||
)
|
||||
|
||||
|
||||
@acm
|
||||
async def open_annots_client(
|
||||
uid: tuple[str, str],
|
||||
|
||||
) -> 'AnnotClient':
|
||||
# TODO: load connetion to a specific chart actor
|
||||
# -[ ] pull from either service scan or config
|
||||
# -[ ] return some kinda client/proxy thinger?
|
||||
# -[ ] maybe we should finally just provide this as
|
||||
# a `tractor.hilevel.CallableProxy` or wtv?
|
||||
# -[ ] use this from the storage.cli stuff to mark up gaps!
|
||||
...
|
Loading…
Reference in New Issue