Initial chart widget adjustments for agg feeds

Main "public" API change is to make `GodWidget.get/set_chart_symbol()`
accept and cache-on fqsn tuples to allow handling overlayed chart groups
and adjust method names to be plural to match.

Wrt `LinkedSplits`,
- create all chart widget axes with a `None` plotitem argument and set
  the `.pi` field after axis creation (since apparently we have another
  object reference causality dilemma..)
- set a monkeyed `PlotItem.chart_widget` for use in axes that still need
  the widget reference.
- drop feed pause/resume for now since it's leaking feed tasks on the
  `brokerd` side and we probably don't really need it any more, and if
  we still do it should be done on the feed not the flume.

Wrt `ChartPlotItem`,
- drop `._add_sticky()` and use the `Axis` method instead and add some
  overlay + axis sanity checks.
- refactor `.draw_ohlc()` to be a lighter wrapper around a call to
  `.add_plot()`.
axis_sticky_api
Tyler Goodlet 2022-11-14 16:25:19 -05:00
parent 9217610734
commit 00be100e71
1 changed files with 126 additions and 124 deletions

View File

@ -45,7 +45,6 @@ import trio
from ._axes import ( from ._axes import (
DynamicDateAxis, DynamicDateAxis,
PriceAxis, PriceAxis,
YAxisLabel,
) )
from ._cursor import ( from ._cursor import (
Cursor, Cursor,
@ -168,18 +167,18 @@ class GodWidget(QWidget):
# self.strategy_box = StrategyBoxWidget(self) # self.strategy_box = StrategyBoxWidget(self)
# self.toolbar_layout.addWidget(self.strategy_box) # self.toolbar_layout.addWidget(self.strategy_box)
def set_chart_symbol( def set_chart_symbols(
self, self,
symbol_key: str, # of form <fqsn>.<providername> group_key: tuple[str], # of form <fqsn>.<providername>
all_linked: tuple[LinkedSplits, LinkedSplits], # type: ignore all_linked: tuple[LinkedSplits, LinkedSplits], # type: ignore
) -> None: ) -> None:
# re-sort org cache symbol list in LIFO order # re-sort org cache symbol list in LIFO order
cache = self._chart_cache cache = self._chart_cache
cache.pop(symbol_key, None) cache.pop(group_key, None)
cache[symbol_key] = all_linked cache[group_key] = all_linked
def get_chart_symbol( def get_chart_symbols(
self, self,
symbol_key: str, symbol_key: str,
@ -188,8 +187,7 @@ class GodWidget(QWidget):
async def load_symbols( async def load_symbols(
self, self,
providername: str, fqsns: list[str],
symbol_keys: list[str],
loglevel: str, loglevel: str,
reset: bool = False, reset: bool = False,
@ -200,20 +198,11 @@ class GodWidget(QWidget):
Expects a ``numpy`` structured array containing all the ohlcv fields. Expects a ``numpy`` structured array containing all the ohlcv fields.
''' '''
fqsns: list[str] = []
# our symbol key style is always lower case
for key in list(map(str.lower, symbol_keys)):
# fully qualified symbol name (SNS i guess is what we're making?)
fqsn = '.'.join([key, providername])
fqsns.append(fqsn)
# NOTE: for now we use the first symbol in the set as the "key" # NOTE: for now we use the first symbol in the set as the "key"
# for the overlay of feeds on the chart. # for the overlay of feeds on the chart.
group_key = fqsns[0] group_key: tuple[str] = tuple(fqsns)
all_linked = self.get_chart_symbol(group_key) all_linked = self.get_chart_symbols(group_key)
order_mode_started = trio.Event() order_mode_started = trio.Event()
if not self.vbox.isEmpty(): if not self.vbox.isEmpty():
@ -245,7 +234,6 @@ class GodWidget(QWidget):
self._root_n.start_soon( self._root_n.start_soon(
display_symbol_data, display_symbol_data,
self, self,
providername,
fqsns, fqsns,
loglevel, loglevel,
order_mode_started, order_mode_started,
@ -253,8 +241,8 @@ class GodWidget(QWidget):
# self.vbox.addWidget(hist_charts) # self.vbox.addWidget(hist_charts)
self.vbox.addWidget(rt_charts) self.vbox.addWidget(rt_charts)
self.set_chart_symbol( self.set_chart_symbols(
fqsn, group_key,
(hist_charts, rt_charts), (hist_charts, rt_charts),
) )
@ -568,12 +556,10 @@ class LinkedSplits(QWidget):
# be no distinction since we will have multiple symbols per # be no distinction since we will have multiple symbols per
# view as part of "aggregate feeds". # view as part of "aggregate feeds".
self.chart = self.add_plot( self.chart = self.add_plot(
name=symbol.fqsn,
name=symbol.key,
shm=shm, shm=shm,
style=style, style=style,
_is_main=True, _is_main=True,
sidepane=sidepane, sidepane=sidepane,
) )
# add crosshair graphic # add crosshair graphic
@ -615,12 +601,13 @@ class LinkedSplits(QWidget):
# TODO: we gotta possibly assign this back # TODO: we gotta possibly assign this back
# to the last subplot on removal of some last subplot # to the last subplot on removal of some last subplot
xaxis = DynamicDateAxis( xaxis = DynamicDateAxis(
None,
orientation='bottom', orientation='bottom',
linkedsplits=self linkedsplits=self
) )
axes = { axes = {
'right': PriceAxis(linkedsplits=self, orientation='right'), 'right': PriceAxis(None, orientation='right'),
'left': PriceAxis(linkedsplits=self, orientation='left'), 'left': PriceAxis(None, orientation='left'),
'bottom': xaxis, 'bottom': xaxis,
} }
@ -645,6 +632,11 @@ class LinkedSplits(QWidget):
axisItems=axes, axisItems=axes,
**cpw_kwargs, **cpw_kwargs,
) )
# TODO: wow i can't believe how confusing garbage all this axes
# stuff iss..
for axis in axes.values():
axis.pi = cpw.plotItem
cpw.hideAxis('left') cpw.hideAxis('left')
cpw.hideAxis('bottom') cpw.hideAxis('bottom')
@ -860,7 +852,12 @@ class ChartPlotWidget(pg.PlotWidget):
# source of our custom interactions # source of our custom interactions
self.cv = cv = self.mk_vb(name) self.cv = cv = self.mk_vb(name)
pi = pgo.PlotItem(viewBox=cv, **kwargs) pi = pgo.PlotItem(
viewBox=cv,
name=name,
**kwargs,
)
pi.chart_widget = self
super().__init__( super().__init__(
background=hcolor(view_color), background=hcolor(view_color),
viewBox=cv, viewBox=cv,
@ -913,18 +910,20 @@ class ChartPlotWidget(pg.PlotWidget):
self._on_screen: bool = False self._on_screen: bool = False
def resume_all_feeds(self): def resume_all_feeds(self):
try: ...
for feed in self._feeds.values(): # try:
for flume in feed.flumes.values(): # for feed in self._feeds.values():
self.linked.godwidget._root_n.start_soon(feed.resume) # for flume in feed.flumes.values():
except RuntimeError: # self.linked.godwidget._root_n.start_soon(flume.resume)
# TODO: cancel the qtractor runtime here? # except RuntimeError:
raise # # TODO: cancel the qtractor runtime here?
# raise
def pause_all_feeds(self): def pause_all_feeds(self):
for feed in self._feeds.values(): ...
for flume in feed.flumes.values(): # for feed in self._feeds.values():
self.linked.godwidget._root_n.start_soon(feed.pause) # for flume in feed.flumes.values():
# self.linked.godwidget._root_n.start_soon(flume.pause)
@property @property
def view(self) -> ChartView: def view(self) -> ChartView:
@ -1116,43 +1115,6 @@ class ChartPlotWidget(pg.PlotWidget):
padding=0, padding=0,
) )
def draw_ohlc(
self,
name: str,
shm: ShmArray,
array_key: Optional[str] = None,
) -> (pg.GraphicsObject, str):
'''
Draw OHLC datums to chart.
'''
graphics = BarItems(
self.linked,
self.plotItem,
pen_color=self.pen_color,
name=name,
)
# adds all bar/candle graphics objects for each data point in
# the np array buffer to be drawn on next render cycle
self.plotItem.addItem(graphics)
data_key = array_key or name
self._flows[data_key] = Flow(
name=name,
plot=self.plotItem,
_shm=shm,
is_ohlc=True,
graphics=graphics,
)
self._add_sticky(name, bg_color='davies')
return graphics, data_key
def overlay_plotitem( def overlay_plotitem(
self, self,
name: str, name: str,
@ -1172,8 +1134,8 @@ class ChartPlotWidget(pg.PlotWidget):
raise ValueError(f'``axis_side``` must be in {allowed_sides}') raise ValueError(f'``axis_side``` must be in {allowed_sides}')
yaxis = PriceAxis( yaxis = PriceAxis(
plotitem=None,
orientation=axis_side, orientation=axis_side,
linkedsplits=self.linked,
**axis_kwargs, **axis_kwargs,
) )
@ -1188,6 +1150,8 @@ class ChartPlotWidget(pg.PlotWidget):
}, },
default_axes=[], default_axes=[],
) )
yaxis.pi = pi
pi.chart_widget = self
pi.hideButtons() pi.hideButtons()
# compose this new plot's graphics with the current chart's # compose this new plot's graphics with the current chart's
@ -1231,22 +1195,40 @@ class ChartPlotWidget(pg.PlotWidget):
add_label: bool = True, add_label: bool = True,
pi: Optional[pg.PlotItem] = None, pi: Optional[pg.PlotItem] = None,
step_mode: bool = False, step_mode: bool = False,
is_ohlc: bool = False,
add_sticky: None | str = 'right',
**pdi_kwargs, **graphics_kwargs,
) -> (pg.PlotDataItem, str): ) -> tuple[
pg.GraphicsObject,
str,
]:
''' '''
Draw a "curve" (line plot graphics) for the provided data in Draw a "curve" (line plot graphics) for the provided data in
the input shm array ``shm``. the input shm array ``shm``.
''' '''
color = color or self.pen_color or 'default_light' color = color or self.pen_color or 'default_light'
pdi_kwargs.update({ # graphics_kwargs.update({
'color': color # 'color': color
}) # })
data_key = array_key or name data_key = array_key or name
pi = pi or self.plotItem
if is_ohlc:
graphics = BarItems(
linked=self.linked,
plotitem=pi,
# pen_color=self.pen_color,
color=color,
name=name,
**graphics_kwargs,
)
else:
curve_type = { curve_type = {
None: Curve, None: Curve,
'step': StepCurve, 'step': StepCurve,
@ -1254,20 +1236,19 @@ class ChartPlotWidget(pg.PlotWidget):
# 'bars': BarsItems # 'bars': BarsItems
}['step' if step_mode else None] }['step' if step_mode else None]
curve = curve_type( graphics = curve_type(
name=name, name=name,
**pdi_kwargs, color=color,
**graphics_kwargs,
) )
pi = pi or self.plotItem
self._flows[data_key] = Flow( self._flows[data_key] = Flow(
name=name, name=name,
plot=pi, plot=pi,
_shm=shm, _shm=shm,
is_ohlc=False, is_ohlc=is_ohlc,
# register curve graphics with this flow # register curve graphics with this flow
graphics=curve, graphics=graphics,
) )
# TODO: this probably needs its own method? # TODO: this probably needs its own method?
@ -1278,12 +1259,41 @@ class ChartPlotWidget(pg.PlotWidget):
f'{overlay} must be from `.plotitem_overlay()`' f'{overlay} must be from `.plotitem_overlay()`'
) )
pi = overlay pi = overlay
else:
if add_sticky:
axis = pi.getAxis(add_sticky)
if pi.name not in axis._stickies:
if pi is not self.plotItem:
overlay = self.pi_overlay
assert pi in overlay.overlays
overlay_axis = overlay.get_axis(
pi,
add_sticky,
)
assert overlay_axis is axis
# TODO: UGH! just make this not here! we should
# be making the sticky from code which has access
# to the ``Symbol`` instance..
# if the sticky is for our symbol
# use the tick size precision for display
name = name or pi.name
sym = self.linked.symbol
digits = None
if name == sym.key:
digits = sym.tick_size_digits
# anchor_at = ('top', 'left') # anchor_at = ('top', 'left')
# TODO: something instead of stickies for overlays # TODO: something instead of stickies for overlays
# (we need something that avoids clutter on x-axis). # (we need something that avoids clutter on x-axis).
self._add_sticky(name, bg_color=color) axis.add_sticky(
pi=pi,
bg_color=color,
digits=digits,
)
# NOTE: this is more or less the RENDER call that tells Qt to # NOTE: this is more or less the RENDER call that tells Qt to
# start showing the generated graphics-curves. This is kind of # start showing the generated graphics-curves. This is kind of
@ -1294,38 +1304,30 @@ class ChartPlotWidget(pg.PlotWidget):
# the next render cycle; just note a lot of the real-time # the next render cycle; just note a lot of the real-time
# updates are implicit and require a bit of digging to # updates are implicit and require a bit of digging to
# understand. # understand.
pi.addItem(curve) pi.addItem(graphics)
return curve, data_key return graphics, data_key
# TODO: make this a ctx mngr def draw_ohlc(
def _add_sticky(
self, self,
name: str, name: str,
bg_color='bracket', shm: ShmArray,
) -> YAxisLabel: array_key: Optional[str] = None,
**draw_curve_kwargs,
# if the sticky is for our symbol ) -> (pg.GraphicsObject, str):
# use the tick size precision for display '''
sym = self.linked.symbol Draw OHLC datums to chart.
if name == sym.key:
digits = sym.tick_size_digits
else:
digits = 2
# add y-axis "last" value label '''
last = self._ysticks[name] = YAxisLabel( return self.draw_curve(
chart=self, name=name,
# parent=self.getAxis('right'), shm=shm,
parent=self.pi_overlay.get_axis(self.plotItem, 'right'), array_key=array_key,
# TODO: pass this from symbol data is_ohlc=True,
digits=digits, **draw_curve_kwargs,
opacity=1,
bg_color=bg_color,
) )
return last
def update_graphics_from_flow( def update_graphics_from_flow(
self, self,