Merge pull request #448 from pikers/axis_sticky_api

Axis sticky api, `PlotItem` is the new "chart"
kraken_deposits_fixes
goodboy 2023-02-05 15:32:22 -05:00 committed by GitHub
commit 11ba706797
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 347 additions and 222 deletions

View File

@ -118,17 +118,10 @@ async def _async_main(
# godwidget.hbox.addWidget(search) # godwidget.hbox.addWidget(search)
godwidget.search = search godwidget.search = search
symbols: list[str] = []
for sym in syms:
symbol, _, provider = sym.rpartition('.')
symbols.append(symbol)
# this internally starts a ``display_symbol_data()`` task above # this internally starts a ``display_symbol_data()`` task above
order_mode_ready = await godwidget.load_symbols( order_mode_ready = await godwidget.load_symbols(
provider, fqsns=syms,
symbols, loglevel=loglevel,
loglevel
) )
# spin up a search engine for the local cached symbol set # spin up a search engine for the local cached symbol set

View File

@ -18,6 +18,7 @@
Chart axes graphics and behavior. Chart axes graphics and behavior.
""" """
from __future__ import annotations
from functools import lru_cache from functools import lru_cache
from typing import Optional, Callable from typing import Optional, Callable
from math import floor from math import floor
@ -27,6 +28,7 @@ import pyqtgraph as pg
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QPointF from PyQt5.QtCore import QPointF
from . import _pg_overrides as pgo
from ..data._source import float_digits from ..data._source import float_digits
from ._label import Label from ._label import Label
from ._style import DpiAwareFont, hcolor, _font from ._style import DpiAwareFont, hcolor, _font
@ -46,7 +48,7 @@ class Axis(pg.AxisItem):
''' '''
def __init__( def __init__(
self, self,
linkedsplits, plotitem: pgo.PlotItem,
typical_max_str: str = '100 000.000', typical_max_str: str = '100 000.000',
text_color: str = 'bracket', text_color: str = 'bracket',
lru_cache_tick_strings: bool = True, lru_cache_tick_strings: bool = True,
@ -61,27 +63,32 @@ class Axis(pg.AxisItem):
# XXX: pretty sure this makes things slower # XXX: pretty sure this makes things slower
# self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) # self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
self.linkedsplits = linkedsplits self.pi = plotitem
self._dpi_font = _font self._dpi_font = _font
self.setTickFont(_font.font) self.setTickFont(_font.font)
font_size = self._dpi_font.font.pixelSize() font_size = self._dpi_font.font.pixelSize()
style_conf = {
'textFillLimits': [(0, 0.5)],
'tickFont': self._dpi_font.font,
}
text_offset = None
if self.orientation in ('bottom',): if self.orientation in ('bottom',):
text_offset = floor(0.25 * font_size) text_offset = floor(0.25 * font_size)
elif self.orientation in ('left', 'right'): elif self.orientation in ('left', 'right'):
text_offset = floor(font_size / 2) text_offset = floor(font_size / 2)
self.setStyle(**{ if text_offset:
'textFillLimits': [(0, 0.5)], style_conf.update({
'tickFont': self._dpi_font.font, # offset of text *away from* axis line in px
# use approx. half the font pixel size (height)
# offset of text *away from* axis line in px 'tickTextOffset': text_offset,
# use approx. half the font pixel size (height) })
'tickTextOffset': text_offset,
})
self.setStyle(**style_conf)
self.setTickFont(_font.font) self.setTickFont(_font.font)
# NOTE: this is for surrounding "border" # NOTE: this is for surrounding "border"
@ -102,6 +109,9 @@ class Axis(pg.AxisItem):
maxsize=2**20 maxsize=2**20
)(self.tickStrings) )(self.tickStrings)
# axis "sticky" labels
self._stickies: dict[str, YAxisLabel] = {}
# NOTE: only overriden to cast tick values entries into tuples # NOTE: only overriden to cast tick values entries into tuples
# for use with the lru caching. # for use with the lru caching.
def tickValues( def tickValues(
@ -139,6 +149,40 @@ class Axis(pg.AxisItem):
def txt_offsets(self) -> tuple[int, int]: def txt_offsets(self) -> tuple[int, int]:
return tuple(self.style['tickTextOffset']) return tuple(self.style['tickTextOffset'])
def add_sticky(
self,
pi: pgo.PlotItem,
name: None | str = None,
digits: None | int = 2,
# axis_name: str = 'right',
bg_color='bracket',
) -> YAxisLabel:
# if the sticky is for our symbol
# use the tick size precision for display
name = name or pi.name
digits = digits or 2
# TODO: ``._ysticks`` should really be an attr on each
# ``PlotItem`` no instead of the (containing because of
# overlays) widget?
# add y-axis "last" value label
sticky = self._stickies[name] = YAxisLabel(
pi=pi,
parent=self,
# TODO: pass this from symbol data
digits=digits,
opacity=1,
bg_color=bg_color,
)
pi.sigRangeChanged.connect(sticky.update_on_resize)
# pi.addItem(sticky)
# pi.addItem(last)
return sticky
class PriceAxis(Axis): class PriceAxis(Axis):
@ -255,7 +299,9 @@ class DynamicDateAxis(Axis):
) -> list[str]: ) -> list[str]:
chart = self.linkedsplits.chart # XX: ARGGGGG AG:LKSKDJF:LKJSDFD
chart = self.pi.chart_widget
flow = chart._flows[chart.name] flow = chart._flows[chart.name]
shm = flow.shm shm = flow.shm
bars = shm.array bars = shm.array
@ -522,7 +568,7 @@ class XAxisLabel(AxisLabel):
class YAxisLabel(AxisLabel): class YAxisLabel(AxisLabel):
_y_margin = 4 _y_margin: int = 4
text_flags = ( text_flags = (
QtCore.Qt.AlignLeft QtCore.Qt.AlignLeft
@ -533,19 +579,19 @@ class YAxisLabel(AxisLabel):
def __init__( def __init__(
self, self,
chart, pi: pgo.PlotItem,
*args, *args,
**kwargs **kwargs
) -> None: ) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._chart = chart self._pi = pi
pi.sigRangeChanged.connect(self.update_on_resize)
chart.sigRangeChanged.connect(self.update_on_resize)
self._last_datum = (None, None) self._last_datum = (None, None)
self.x_offset = 0
# pull text offset from axis from parent axis # pull text offset from axis from parent axis
if getattr(self._parent, 'txt_offsets', False): if getattr(self._parent, 'txt_offsets', False):
self.x_offset, y_offset = self._parent.txt_offsets() self.x_offset, y_offset = self._parent.txt_offsets()
@ -564,7 +610,8 @@ class YAxisLabel(AxisLabel):
value: float, # data for text value: float, # data for text
# on odd dimension and/or adds nice black line # on odd dimension and/or adds nice black line
x_offset: Optional[int] = None x_offset: int = 0,
) -> None: ) -> None:
# this is read inside ``.paint()`` # this is read inside ``.paint()``
@ -610,7 +657,7 @@ class YAxisLabel(AxisLabel):
self._last_datum = (index, value) self._last_datum = (index, value)
self.update_label( self.update_label(
self._chart.mapFromView(QPointF(index, value)), self._pi.mapFromView(QPointF(index, value)),
value value
) )

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),
) )
@ -495,7 +483,10 @@ class LinkedSplits(QWidget):
from . import _display from . import _display
ds = self.display_state ds = self.display_state
if ds: if ds:
return _display.graphics_update_cycle(ds, **kwargs) return _display.graphics_update_cycle(
ds,
**kwargs,
)
@property @property
def symbol(self) -> Symbol: def symbol(self) -> Symbol:
@ -548,7 +539,7 @@ class LinkedSplits(QWidget):
shm: ShmArray, shm: ShmArray,
sidepane: FieldsForm, sidepane: FieldsForm,
style: str = 'bar', style: str = 'ohlc_bar',
) -> ChartPlotWidget: ) -> ChartPlotWidget:
''' '''
@ -568,12 +559,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 +604,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 +635,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')
@ -707,7 +702,7 @@ class LinkedSplits(QWidget):
anchor_at = ('top', 'left') anchor_at = ('top', 'left')
# draw curve graphics # draw curve graphics
if style == 'bar': if style == 'ohlc_bar':
graphics, data_key = cpw.draw_ohlc( graphics, data_key = cpw.draw_ohlc(
name, name,
@ -744,30 +739,33 @@ class LinkedSplits(QWidget):
else: else:
raise ValueError(f"Chart style {style} is currently unsupported") raise ValueError(f"Chart style {style} is currently unsupported")
if not _is_main: if _is_main:
assert style == 'ohlc_bar', 'main chart must be OHLC'
else:
# track by name # track by name
self.subplots[name] = cpw self.subplots[name] = cpw
if qframe is not None: if qframe is not None:
self.splitter.addWidget(qframe) self.splitter.addWidget(qframe)
else:
assert style == 'bar', 'main chart must be OHLC'
# add to cross-hair's known plots # add to cross-hair's known plots
# NOTE: add **AFTER** creating the underlying ``PlotItem``s # NOTE: add **AFTER** creating the underlying ``PlotItem``s
# since we require that global (linked charts wide) axes have # since we require that global (linked charts wide) axes have
# been created! # been created!
self.cursor.add_plot(cpw) if self.cursor:
if (
_is_main
or style != 'ohlc_bar'
):
self.cursor.add_plot(cpw)
if style != 'ohlc_bar':
self.cursor.add_curve_cursor(cpw, graphics)
if self.cursor and style != 'bar': if add_label:
self.cursor.add_curve_cursor(cpw, graphics) self.cursor.contents_labels.add_label(
cpw,
if add_label: data_key,
self.cursor.contents_labels.add_label( anchor_at=anchor_at,
cpw, )
data_key,
anchor_at=anchor_at,
)
self.resize_sidepanes() self.resize_sidepanes()
return cpw return cpw
@ -860,7 +858,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 +916,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 +1121,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 +1140,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 +1156,9 @@ class ChartPlotWidget(pg.PlotWidget):
}, },
default_axes=[], default_axes=[],
) )
# pi.vb.background.setOpacity(0)
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,43 +1202,56 @@ 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({
'color': color
})
data_key = array_key or name data_key = array_key or name
curve_type = {
None: Curve,
'step': StepCurve,
# TODO:
# 'bars': BarsItems
}['step' if step_mode else None]
curve = curve_type(
name=name,
**pdi_kwargs,
)
pi = pi or self.plotItem 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 = {
None: Curve,
'step': StepCurve,
# TODO:
# 'bars': BarsItems
}['step' if step_mode else None]
graphics = curve_type(
name=name,
color=color,
**graphics_kwargs,
)
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 +1262,41 @@ class ChartPlotWidget(pg.PlotWidget):
f'{overlay} must be from `.plotitem_overlay()`' f'{overlay} must be from `.plotitem_overlay()`'
) )
pi = overlay pi = overlay
else:
# anchor_at = ('top', 'left')
# TODO: something instead of stickies for overlays if add_sticky:
# (we need something that avoids clutter on x-axis). axis = pi.getAxis(add_sticky)
self._add_sticky(name, bg_color=color) 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')
# TODO: something instead of stickies for overlays
# (we need something that avoids clutter on x-axis).
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 +1307,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,

View File

@ -418,7 +418,7 @@ class Cursor(pg.GraphicsObject):
hl.hide() hl.hide()
yl = YAxisLabel( yl = YAxisLabel(
chart=plot, pi=plot.plotItem,
# parent=plot.getAxis('right'), # parent=plot.getAxis('right'),
parent=plot.pi_overlay.get_axis(plot.plotItem, 'right'), parent=plot.pi_overlay.get_axis(plot.plotItem, 'right'),
digits=digits or self.digits, digits=digits or self.digits,

View File

@ -260,12 +260,14 @@ async def graphics_update_loop(
hist_ohlcv = flume.hist_shm hist_ohlcv = flume.hist_shm
# update last price sticky # update last price sticky
last_price_sticky = fast_chart._ysticks[fast_chart.name] last_price_sticky = fast_chart.plotItem.getAxis(
'right')._stickies.get(fast_chart.name)
last_price_sticky.update_from_data( last_price_sticky.update_from_data(
*ohlcv.array[-1][['index', 'close']] *ohlcv.array[-1][['index', 'close']]
) )
hist_last_price_sticky = hist_chart._ysticks[hist_chart.name] hist_last_price_sticky = hist_chart.plotItem.getAxis(
'right')._stickies.get(hist_chart.name)
hist_last_price_sticky.update_from_data( hist_last_price_sticky.update_from_data(
*hist_ohlcv.array[-1][['index', 'close']] *hist_ohlcv.array[-1][['index', 'close']]
) )
@ -289,7 +291,7 @@ async def graphics_update_loop(
symbol = fast_chart.linked.symbol symbol = fast_chart.linked.symbol
l1 = L1Labels( l1 = L1Labels(
fast_chart, fast_chart.plotItem,
# determine precision/decimal lengths # determine precision/decimal lengths
digits=symbol.tick_size_digits, digits=symbol.tick_size_digits,
size_digits=symbol.lot_size_digits, size_digits=symbol.lot_size_digits,
@ -333,7 +335,8 @@ async def graphics_update_loop(
}) })
if vlm_chart: if vlm_chart:
vlm_sticky = vlm_chart._ysticks['volume'] vlm_sticky = vlm_chart.plotItem.getAxis(
'right')._stickies.get('volume')
ds.vlm_chart = vlm_chart ds.vlm_chart = vlm_chart
ds.vlm_sticky = vlm_sticky ds.vlm_sticky = vlm_sticky
@ -947,7 +950,6 @@ async def link_views_with_region(
async def display_symbol_data( async def display_symbol_data(
godwidget: GodWidget, godwidget: GodWidget,
provider: str,
fqsns: list[str], fqsns: list[str],
loglevel: str, loglevel: str,
order_mode_started: trio.Event, order_mode_started: trio.Event,
@ -999,6 +1001,7 @@ async def display_symbol_data(
symbol = flume.symbol symbol = flume.symbol
fqsn = symbol.fqsn fqsn = symbol.fqsn
brokername = symbol.brokers[0]
step_size_s = 1 step_size_s = 1
tf_key = tf_in_1s[step_size_s] tf_key = tf_in_1s[step_size_s]
@ -1082,7 +1085,7 @@ async def display_symbol_data(
# if available load volume related built-in display(s) # if available load volume related built-in display(s)
if ( if (
not symbol.broker_info[provider].get('no_vlm', False) not symbol.broker_info[brokername].get('no_vlm', False)
and has_vlm(ohlcv) and has_vlm(ohlcv)
): ):
vlm_chart = await ln.start( vlm_chart = await ln.start(

View File

@ -110,7 +110,8 @@ def update_fsp_chart(
# sub-charts reference it under different 'named charts'. # sub-charts reference it under different 'named charts'.
# read from last calculated value and update any label # read from last calculated value and update any label
last_val_sticky = chart._ysticks.get(graphics_name) last_val_sticky = chart.plotItem.getAxis(
'right')._stickies.get(chart.name)
if last_val_sticky: if last_val_sticky:
last = last_row[array_key] last = last_row[array_key]
last_val_sticky.update_from_data(-1, last) last_val_sticky.update_from_data(-1, last)
@ -685,7 +686,8 @@ async def open_vlm_displays(
assert chart.name != linked.chart.name assert chart.name != linked.chart.name
# sticky only on sub-charts atm # sticky only on sub-charts atm
last_val_sticky = chart._ysticks[chart.name] last_val_sticky = chart.plotItem.getAxis(
'right')._stickies.get(chart.name)
# read from last calculated value # read from last calculated value
value = shm.array['volume'][-1] value = shm.array['volume'][-1]

View File

@ -26,6 +26,7 @@ from PyQt5.QtCore import QPointF
from ._axes import YAxisLabel from ._axes import YAxisLabel
from ._style import hcolor from ._style import hcolor
from ._pg_overrides import PlotItem
class LevelLabel(YAxisLabel): class LevelLabel(YAxisLabel):
@ -132,7 +133,7 @@ class LevelLabel(YAxisLabel):
level = self.fields['level'] level = self.fields['level']
# map "level" to local coords # map "level" to local coords
abs_xy = self._chart.mapFromView(QPointF(0, level)) abs_xy = self._pi.mapFromView(QPointF(0, level))
self.update_label( self.update_label(
abs_xy, abs_xy,
@ -149,7 +150,7 @@ class LevelLabel(YAxisLabel):
h, w = self.set_label_str(fields) h, w = self.set_label_str(fields)
if self._adjust_to_l1: if self._adjust_to_l1:
self._x_offset = self._chart._max_l1_line_len self._x_offset = self._pi.chart_widget._max_l1_line_len
self.setPos(QPointF( self.setPos(QPointF(
self._h_shift * (w + self._x_offset), self._h_shift * (w + self._x_offset),
@ -236,10 +237,10 @@ class L1Label(LevelLabel):
# Set a global "max L1 label length" so we can # Set a global "max L1 label length" so we can
# look it up on order lines and adjust their # look it up on order lines and adjust their
# labels not to overlap with it. # labels not to overlap with it.
chart = self._chart chart = self._pi.chart_widget
chart._max_l1_line_len: float = max( chart._max_l1_line_len: float = max(
chart._max_l1_line_len, chart._max_l1_line_len,
w w,
) )
return h, w return h, w
@ -251,17 +252,17 @@ class L1Labels:
""" """
def __init__( def __init__(
self, self,
chart: 'ChartPlotWidget', # noqa plotitem: PlotItem,
digits: int = 2, digits: int = 2,
size_digits: int = 3, size_digits: int = 3,
font_size: str = 'small', font_size: str = 'small',
) -> None: ) -> None:
self.chart = chart chart = self.chart = plotitem.chart_widget
raxis = chart.getAxis('right') raxis = plotitem.getAxis('right')
kwargs = { kwargs = {
'chart': chart, 'chart': plotitem,
'parent': raxis, 'parent': raxis,
'opacity': 1, 'opacity': 1,

View File

@ -98,7 +98,7 @@ class BarItems(pg.GraphicsObject):
self, self,
linked: LinkedSplits, linked: LinkedSplits,
plotitem: 'pg.PlotItem', # noqa plotitem: 'pg.PlotItem', # noqa
pen_color: str = 'bracket', color: str = 'bracket',
last_bar_color: str = 'bracket', last_bar_color: str = 'bracket',
name: Optional[str] = None, name: Optional[str] = None,
@ -108,8 +108,8 @@ class BarItems(pg.GraphicsObject):
self.linked = linked self.linked = linked
# XXX: for the mega-lulz increasing width here increases draw # XXX: for the mega-lulz increasing width here increases draw
# latency... so probably don't do it until we figure that out. # latency... so probably don't do it until we figure that out.
self._color = pen_color self._color = color
self.bars_pen = pg.mkPen(hcolor(pen_color), width=1) self.bars_pen = pg.mkPen(hcolor(color), width=1)
self.last_bar_pen = pg.mkPen(hcolor(last_bar_color), width=2) self.last_bar_pen = pg.mkPen(hcolor(last_bar_color), width=2)
self._name = name self._name = name

View File

@ -26,6 +26,8 @@ from typing import Optional
import pyqtgraph as pg import pyqtgraph as pg
from ._axes import Axis
def invertQTransform(tr): def invertQTransform(tr):
"""Return a QTransform that is the inverse of *tr*. """Return a QTransform that is the inverse of *tr*.
@ -62,6 +64,20 @@ class PlotItem(pg.PlotItem):
Overrides for the core plot object mostly pertaining to overlayed Overrides for the core plot object mostly pertaining to overlayed
multi-view management as it relates to multi-axis managment. multi-view management as it relates to multi-axis managment.
This object is the combination of a ``ViewBox`` and multiple
``AxisItem``s and so far we've added additional functionality and
APIs for:
- removal of axes
---
From ``pyqtgraph`` super type docs:
- Manage placement of ViewBox, AxisItems, and LabelItems
- Create and manage a list of PlotDataItems displayed inside the
ViewBox
- Implement a context menu with commonly used display and analysis
options
''' '''
def __init__( def __init__(
self, self,
@ -86,6 +102,8 @@ class PlotItem(pg.PlotItem):
enableMenu=enableMenu, enableMenu=enableMenu,
kargs=kargs, kargs=kargs,
) )
self.name = name
self.chart_widget = None
# self.setAxisItems( # self.setAxisItems(
# axisItems, # axisItems,
# default_axes=default_axes, # default_axes=default_axes,
@ -209,7 +227,12 @@ class PlotItem(pg.PlotItem):
# adding this is without it there's some weird # adding this is without it there's some weird
# ``ViewBox`` geometry bug.. where a gap for the # ``ViewBox`` geometry bug.. where a gap for the
# 'bottom' axis is somehow left in? # 'bottom' axis is somehow left in?
axis = pg.AxisItem(orientation=name, parent=self) # axis = pg.AxisItem(orientation=name, parent=self)
axis = Axis(
self,
orientation=name,
parent=self,
)
axis.linkToView(self.vb) axis.linkToView(self.vb)

View File

@ -416,12 +416,26 @@ class CompleterView(QTreeView):
section: str, section: str,
values: Sequence[str], values: Sequence[str],
clear_all: bool = False, clear_all: bool = False,
reverse: bool = False,
) -> None: ) -> None:
''' '''
Set result-rows for depth = 1 tree section ``section``. Set result-rows for depth = 1 tree section ``section``.
''' '''
if (
values
and not isinstance(values[0], str)
):
flattened: list[str] = []
for val in values:
flattened.extend(val)
values = flattened
if reverse:
values = reversed(values)
model = self.model() model = self.model()
if clear_all: if clear_all:
# XXX: rewrite the model from scratch if caller requests it # XXX: rewrite the model from scratch if caller requests it
@ -598,22 +612,34 @@ class SearchWidget(QtWidgets.QWidget):
self.show() self.show()
self.bar.focus() self.bar.focus()
def show_only_cache_entries(self) -> None: def show_cache_entries(
self,
only: bool = False,
) -> None:
''' '''
Clear the search results view and show only cached (aka recently Clear the search results view and show only cached (aka recently
loaded with active data) feeds in the results section. loaded with active data) feeds in the results section.
''' '''
godw = self.godwidget godw = self.godwidget
# first entry in the cache is the current symbol(s)
fqsns = []
for multi_fqsns in list(godw._chart_cache):
fqsns.extend(list(multi_fqsns))
self.view.set_section_entries( self.view.set_section_entries(
'cache', 'cache',
list(reversed(godw._chart_cache)), list(fqsns),
# remove all other completion results except for cache # remove all other completion results except for cache
clear_all=True, clear_all=only,
reverse=True,
) )
def get_current_item(self) -> Optional[tuple[str, str]]: def get_current_item(self) -> Optional[tuple[str, str]]:
'''Return the current completer tree selection as '''
Return the current completer tree selection as
a tuple ``(parent: str, child: str)`` if valid, else ``None``. a tuple ``(parent: str, child: str)`` if valid, else ``None``.
''' '''
@ -663,12 +689,13 @@ class SearchWidget(QtWidgets.QWidget):
provider, symbol = value provider, symbol = value
godw = self.godwidget godw = self.godwidget
log.info(f'Requesting symbol: {symbol}.{provider}') fqsn = f'{symbol}.{provider}'
log.info(f'Requesting symbol: {fqsn}')
# assert provider in symbol
await godw.load_symbols( await godw.load_symbols(
provider, fqsns=[fqsn],
[symbol], loglevel='info',
'info',
) )
# fully qualified symbol name (SNS i guess is what we're # fully qualified symbol name (SNS i guess is what we're
@ -682,13 +709,13 @@ class SearchWidget(QtWidgets.QWidget):
# Re-order the symbol cache on the chart to display in # Re-order the symbol cache on the chart to display in
# LIFO order. this is normally only done internally by # LIFO order. this is normally only done internally by
# the chart on new symbols being loaded into memory # the chart on new symbols being loaded into memory
godw.set_chart_symbol( godw.set_chart_symbols(
fqsn, ( (fqsn,), (
godw.hist_linked, godw.hist_linked,
godw.rt_linked, godw.rt_linked,
) )
) )
self.show_only_cache_entries() self.show_cache_entries(only=True)
self.bar.focus() self.bar.focus()
return fqsn return fqsn
@ -757,9 +784,10 @@ async def pack_matches(
with trio.CancelScope() as cs: with trio.CancelScope() as cs:
task_status.started(cs) task_status.started(cs)
# ensure ^ status is updated # ensure ^ status is updated
results = await search(pattern) results = list(await search(pattern))
if provider != 'cache': # XXX: don't cache the cache results xD # XXX: don't cache the cache results xD
if provider != 'cache':
matches[(provider, pattern)] = results matches[(provider, pattern)] = results
# print(f'results from {provider}: {results}') # print(f'results from {provider}: {results}')
@ -806,7 +834,7 @@ async def fill_results(
has_results: defaultdict[str, set[str]] = defaultdict(set) has_results: defaultdict[str, set[str]] = defaultdict(set)
# show cached feed list at startup # show cached feed list at startup
search.show_only_cache_entries() search.show_cache_entries()
search.on_resize() search.on_resize()
while True: while True:
@ -860,8 +888,9 @@ async def fill_results(
# it hasn't already been searched with the current # it hasn't already been searched with the current
# input pattern (in which case just look up the old # input pattern (in which case just look up the old
# results). # results).
if (period >= pause) and ( if (
provider not in already_has_results period >= pause
and provider not in already_has_results
): ):
# TODO: it may make more sense TO NOT search the # TODO: it may make more sense TO NOT search the
@ -869,7 +898,9 @@ async def fill_results(
# cpu-bound. # cpu-bound.
if provider != 'cache': if provider != 'cache':
view.clear_section( view.clear_section(
provider, status_field='-> searchin..') provider,
status_field='-> searchin..',
)
await n.start( await n.start(
pack_matches, pack_matches,
@ -890,11 +921,20 @@ async def fill_results(
# re-searching it's ``dict`` since it's easier # re-searching it's ``dict`` since it's easier
# but it also causes it to be slower then cached # but it also causes it to be slower then cached
# results from other providers on occasion. # results from other providers on occasion.
if results and provider != 'cache': if (
view.set_section_entries( results
section=provider, ):
values=results, if provider != 'cache':
) view.set_section_entries(
section=provider,
values=results,
)
else:
# if provider == 'cache':
# for the cache just show what we got
# that matches
search.show_cache_entries()
else: else:
view.clear_section(provider) view.clear_section(provider)
@ -937,7 +977,7 @@ async def handle_keyboard_input(
) )
bar.focus() bar.focus()
search.show_only_cache_entries() search.show_cache_entries()
await trio.sleep(0) await trio.sleep(0)
async for kbmsg in recv_chan: async for kbmsg in recv_chan:
@ -949,20 +989,21 @@ async def handle_keyboard_input(
if mods == Qt.ControlModifier: if mods == Qt.ControlModifier:
ctl = True ctl = True
if key in (Qt.Key_Enter, Qt.Key_Return): if key in (
Qt.Key_Enter,
Qt.Key_Return
):
_search_enabled = False _search_enabled = False
await search.chart_current_item(clear_to_cache=True) await search.chart_current_item(clear_to_cache=True)
search.show_only_cache_entries() search.show_cache_entries(only=True)
view.show_matches() view.show_matches()
search.focus() search.focus()
elif not ctl and not bar.text(): elif not ctl and not bar.text():
# if nothing in search text show the cache
view.set_section_entries( # TODO: really should factor this somewhere..bc
'cache', # we're doin it in another spot as well..
list(reversed(godwidget._chart_cache)), search.show_cache_entries(only=True)
clear_all=True,
)
continue continue
# cancel and close # cancel and close
@ -1025,6 +1066,8 @@ async def handle_keyboard_input(
if parent_item and parent_item.text() == 'cache': if parent_item and parent_item.text() == 'cache':
await search.chart_current_item(clear_to_cache=False) await search.chart_current_item(clear_to_cache=False)
# ACTUAL SEARCH BLOCK #
# where we fuzzy complete and fill out sections.
elif not ctl: elif not ctl:
# relay to completer task # relay to completer task
_search_enabled = True _search_enabled = True
@ -1035,13 +1078,21 @@ async def handle_keyboard_input(
async def search_simple_dict( async def search_simple_dict(
text: str, text: str,
source: dict, source: dict,
) -> dict[str, Any]: ) -> dict[str, Any]:
tokens = []
for key in source:
if not isinstance(key, str):
tokens.extend(key)
else:
tokens.append(key)
# search routine can be specified as a function such # search routine can be specified as a function such
# as in the case of the current app's local symbol cache # as in the case of the current app's local symbol cache
matches = fuzzy.extractBests( matches = fuzzy.extractBests(
text, text,
source.keys(), tokens,
score_cutoff=90, score_cutoff=90,
) )