Merge pull request #448 from pikers/axis_sticky_api
Axis sticky api, `PlotItem` is the new "chart"kraken_deposits_fixes
commit
11ba706797
|
@ -118,17 +118,10 @@ async def _async_main(
|
|||
# godwidget.hbox.addWidget(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
|
||||
order_mode_ready = await godwidget.load_symbols(
|
||||
provider,
|
||||
symbols,
|
||||
loglevel
|
||||
fqsns=syms,
|
||||
loglevel=loglevel,
|
||||
)
|
||||
|
||||
# spin up a search engine for the local cached symbol set
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
Chart axes graphics and behavior.
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from functools import lru_cache
|
||||
from typing import Optional, Callable
|
||||
from math import floor
|
||||
|
@ -27,6 +28,7 @@ import pyqtgraph as pg
|
|||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from PyQt5.QtCore import QPointF
|
||||
|
||||
from . import _pg_overrides as pgo
|
||||
from ..data._source import float_digits
|
||||
from ._label import Label
|
||||
from ._style import DpiAwareFont, hcolor, _font
|
||||
|
@ -46,7 +48,7 @@ class Axis(pg.AxisItem):
|
|||
'''
|
||||
def __init__(
|
||||
self,
|
||||
linkedsplits,
|
||||
plotitem: pgo.PlotItem,
|
||||
typical_max_str: str = '100 000.000',
|
||||
text_color: str = 'bracket',
|
||||
lru_cache_tick_strings: bool = True,
|
||||
|
@ -61,27 +63,32 @@ class Axis(pg.AxisItem):
|
|||
# XXX: pretty sure this makes things slower
|
||||
# self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
|
||||
|
||||
self.linkedsplits = linkedsplits
|
||||
self.pi = plotitem
|
||||
self._dpi_font = _font
|
||||
|
||||
self.setTickFont(_font.font)
|
||||
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',):
|
||||
text_offset = floor(0.25 * font_size)
|
||||
|
||||
elif self.orientation in ('left', 'right'):
|
||||
text_offset = floor(font_size / 2)
|
||||
|
||||
self.setStyle(**{
|
||||
'textFillLimits': [(0, 0.5)],
|
||||
'tickFont': self._dpi_font.font,
|
||||
|
||||
# offset of text *away from* axis line in px
|
||||
# use approx. half the font pixel size (height)
|
||||
'tickTextOffset': text_offset,
|
||||
})
|
||||
if text_offset:
|
||||
style_conf.update({
|
||||
# offset of text *away from* axis line in px
|
||||
# use approx. half the font pixel size (height)
|
||||
'tickTextOffset': text_offset,
|
||||
})
|
||||
|
||||
self.setStyle(**style_conf)
|
||||
self.setTickFont(_font.font)
|
||||
|
||||
# NOTE: this is for surrounding "border"
|
||||
|
@ -102,6 +109,9 @@ class Axis(pg.AxisItem):
|
|||
maxsize=2**20
|
||||
)(self.tickStrings)
|
||||
|
||||
# axis "sticky" labels
|
||||
self._stickies: dict[str, YAxisLabel] = {}
|
||||
|
||||
# NOTE: only overriden to cast tick values entries into tuples
|
||||
# for use with the lru caching.
|
||||
def tickValues(
|
||||
|
@ -139,6 +149,40 @@ class Axis(pg.AxisItem):
|
|||
def txt_offsets(self) -> tuple[int, int]:
|
||||
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):
|
||||
|
||||
|
@ -255,7 +299,9 @@ class DynamicDateAxis(Axis):
|
|||
|
||||
) -> list[str]:
|
||||
|
||||
chart = self.linkedsplits.chart
|
||||
# XX: ARGGGGG AG:LKSKDJF:LKJSDFD
|
||||
chart = self.pi.chart_widget
|
||||
|
||||
flow = chart._flows[chart.name]
|
||||
shm = flow.shm
|
||||
bars = shm.array
|
||||
|
@ -522,7 +568,7 @@ class XAxisLabel(AxisLabel):
|
|||
|
||||
|
||||
class YAxisLabel(AxisLabel):
|
||||
_y_margin = 4
|
||||
_y_margin: int = 4
|
||||
|
||||
text_flags = (
|
||||
QtCore.Qt.AlignLeft
|
||||
|
@ -533,19 +579,19 @@ class YAxisLabel(AxisLabel):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
chart,
|
||||
pi: pgo.PlotItem,
|
||||
*args,
|
||||
**kwargs
|
||||
) -> None:
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._chart = chart
|
||||
|
||||
chart.sigRangeChanged.connect(self.update_on_resize)
|
||||
self._pi = pi
|
||||
pi.sigRangeChanged.connect(self.update_on_resize)
|
||||
|
||||
self._last_datum = (None, None)
|
||||
|
||||
self.x_offset = 0
|
||||
# pull text offset from axis from parent axis
|
||||
if getattr(self._parent, 'txt_offsets', False):
|
||||
self.x_offset, y_offset = self._parent.txt_offsets()
|
||||
|
@ -564,7 +610,8 @@ class YAxisLabel(AxisLabel):
|
|||
value: float, # data for text
|
||||
|
||||
# on odd dimension and/or adds nice black line
|
||||
x_offset: Optional[int] = None
|
||||
x_offset: int = 0,
|
||||
|
||||
) -> None:
|
||||
|
||||
# this is read inside ``.paint()``
|
||||
|
@ -610,7 +657,7 @@ class YAxisLabel(AxisLabel):
|
|||
self._last_datum = (index, value)
|
||||
|
||||
self.update_label(
|
||||
self._chart.mapFromView(QPointF(index, value)),
|
||||
self._pi.mapFromView(QPointF(index, value)),
|
||||
value
|
||||
)
|
||||
|
||||
|
|
|
@ -45,7 +45,6 @@ import trio
|
|||
from ._axes import (
|
||||
DynamicDateAxis,
|
||||
PriceAxis,
|
||||
YAxisLabel,
|
||||
)
|
||||
from ._cursor import (
|
||||
Cursor,
|
||||
|
@ -168,18 +167,18 @@ class GodWidget(QWidget):
|
|||
# self.strategy_box = StrategyBoxWidget(self)
|
||||
# self.toolbar_layout.addWidget(self.strategy_box)
|
||||
|
||||
def set_chart_symbol(
|
||||
def set_chart_symbols(
|
||||
self,
|
||||
symbol_key: str, # of form <fqsn>.<providername>
|
||||
group_key: tuple[str], # of form <fqsn>.<providername>
|
||||
all_linked: tuple[LinkedSplits, LinkedSplits], # type: ignore
|
||||
|
||||
) -> None:
|
||||
# re-sort org cache symbol list in LIFO order
|
||||
cache = self._chart_cache
|
||||
cache.pop(symbol_key, None)
|
||||
cache[symbol_key] = all_linked
|
||||
cache.pop(group_key, None)
|
||||
cache[group_key] = all_linked
|
||||
|
||||
def get_chart_symbol(
|
||||
def get_chart_symbols(
|
||||
self,
|
||||
symbol_key: str,
|
||||
|
||||
|
@ -188,8 +187,7 @@ class GodWidget(QWidget):
|
|||
|
||||
async def load_symbols(
|
||||
self,
|
||||
providername: str,
|
||||
symbol_keys: list[str],
|
||||
fqsns: list[str],
|
||||
loglevel: str,
|
||||
reset: bool = False,
|
||||
|
||||
|
@ -200,20 +198,11 @@ class GodWidget(QWidget):
|
|||
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"
|
||||
# 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()
|
||||
|
||||
if not self.vbox.isEmpty():
|
||||
|
@ -245,7 +234,6 @@ class GodWidget(QWidget):
|
|||
self._root_n.start_soon(
|
||||
display_symbol_data,
|
||||
self,
|
||||
providername,
|
||||
fqsns,
|
||||
loglevel,
|
||||
order_mode_started,
|
||||
|
@ -253,8 +241,8 @@ class GodWidget(QWidget):
|
|||
|
||||
# self.vbox.addWidget(hist_charts)
|
||||
self.vbox.addWidget(rt_charts)
|
||||
self.set_chart_symbol(
|
||||
fqsn,
|
||||
self.set_chart_symbols(
|
||||
group_key,
|
||||
(hist_charts, rt_charts),
|
||||
)
|
||||
|
||||
|
@ -495,7 +483,10 @@ class LinkedSplits(QWidget):
|
|||
from . import _display
|
||||
ds = self.display_state
|
||||
if ds:
|
||||
return _display.graphics_update_cycle(ds, **kwargs)
|
||||
return _display.graphics_update_cycle(
|
||||
ds,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@property
|
||||
def symbol(self) -> Symbol:
|
||||
|
@ -548,7 +539,7 @@ class LinkedSplits(QWidget):
|
|||
shm: ShmArray,
|
||||
sidepane: FieldsForm,
|
||||
|
||||
style: str = 'bar',
|
||||
style: str = 'ohlc_bar',
|
||||
|
||||
) -> ChartPlotWidget:
|
||||
'''
|
||||
|
@ -568,12 +559,10 @@ class LinkedSplits(QWidget):
|
|||
# be no distinction since we will have multiple symbols per
|
||||
# view as part of "aggregate feeds".
|
||||
self.chart = self.add_plot(
|
||||
|
||||
name=symbol.key,
|
||||
name=symbol.fqsn,
|
||||
shm=shm,
|
||||
style=style,
|
||||
_is_main=True,
|
||||
|
||||
sidepane=sidepane,
|
||||
)
|
||||
# add crosshair graphic
|
||||
|
@ -615,12 +604,13 @@ class LinkedSplits(QWidget):
|
|||
# TODO: we gotta possibly assign this back
|
||||
# to the last subplot on removal of some last subplot
|
||||
xaxis = DynamicDateAxis(
|
||||
None,
|
||||
orientation='bottom',
|
||||
linkedsplits=self
|
||||
)
|
||||
axes = {
|
||||
'right': PriceAxis(linkedsplits=self, orientation='right'),
|
||||
'left': PriceAxis(linkedsplits=self, orientation='left'),
|
||||
'right': PriceAxis(None, orientation='right'),
|
||||
'left': PriceAxis(None, orientation='left'),
|
||||
'bottom': xaxis,
|
||||
}
|
||||
|
||||
|
@ -645,6 +635,11 @@ class LinkedSplits(QWidget):
|
|||
axisItems=axes,
|
||||
**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('bottom')
|
||||
|
||||
|
@ -707,7 +702,7 @@ class LinkedSplits(QWidget):
|
|||
anchor_at = ('top', 'left')
|
||||
|
||||
# draw curve graphics
|
||||
if style == 'bar':
|
||||
if style == 'ohlc_bar':
|
||||
|
||||
graphics, data_key = cpw.draw_ohlc(
|
||||
name,
|
||||
|
@ -744,30 +739,33 @@ class LinkedSplits(QWidget):
|
|||
else:
|
||||
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
|
||||
self.subplots[name] = cpw
|
||||
if qframe is not None:
|
||||
self.splitter.addWidget(qframe)
|
||||
|
||||
else:
|
||||
assert style == 'bar', 'main chart must be OHLC'
|
||||
|
||||
# add to cross-hair's known plots
|
||||
# NOTE: add **AFTER** creating the underlying ``PlotItem``s
|
||||
# since we require that global (linked charts wide) axes have
|
||||
# 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':
|
||||
self.cursor.add_curve_cursor(cpw, graphics)
|
||||
|
||||
if add_label:
|
||||
self.cursor.contents_labels.add_label(
|
||||
cpw,
|
||||
data_key,
|
||||
anchor_at=anchor_at,
|
||||
)
|
||||
if add_label:
|
||||
self.cursor.contents_labels.add_label(
|
||||
cpw,
|
||||
data_key,
|
||||
anchor_at=anchor_at,
|
||||
)
|
||||
|
||||
self.resize_sidepanes()
|
||||
return cpw
|
||||
|
@ -860,7 +858,12 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# source of our custom interactions
|
||||
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__(
|
||||
background=hcolor(view_color),
|
||||
viewBox=cv,
|
||||
|
@ -913,18 +916,20 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
self._on_screen: bool = False
|
||||
|
||||
def resume_all_feeds(self):
|
||||
try:
|
||||
for feed in self._feeds.values():
|
||||
for flume in feed.flumes.values():
|
||||
self.linked.godwidget._root_n.start_soon(feed.resume)
|
||||
except RuntimeError:
|
||||
# TODO: cancel the qtractor runtime here?
|
||||
raise
|
||||
...
|
||||
# try:
|
||||
# for feed in self._feeds.values():
|
||||
# for flume in feed.flumes.values():
|
||||
# self.linked.godwidget._root_n.start_soon(flume.resume)
|
||||
# except RuntimeError:
|
||||
# # TODO: cancel the qtractor runtime here?
|
||||
# raise
|
||||
|
||||
def pause_all_feeds(self):
|
||||
for feed in self._feeds.values():
|
||||
for flume in feed.flumes.values():
|
||||
self.linked.godwidget._root_n.start_soon(feed.pause)
|
||||
...
|
||||
# for feed in self._feeds.values():
|
||||
# for flume in feed.flumes.values():
|
||||
# self.linked.godwidget._root_n.start_soon(flume.pause)
|
||||
|
||||
@property
|
||||
def view(self) -> ChartView:
|
||||
|
@ -1116,43 +1121,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
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(
|
||||
self,
|
||||
name: str,
|
||||
|
@ -1172,8 +1140,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
raise ValueError(f'``axis_side``` must be in {allowed_sides}')
|
||||
|
||||
yaxis = PriceAxis(
|
||||
plotitem=None,
|
||||
orientation=axis_side,
|
||||
linkedsplits=self.linked,
|
||||
**axis_kwargs,
|
||||
)
|
||||
|
||||
|
@ -1188,6 +1156,9 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
},
|
||||
default_axes=[],
|
||||
)
|
||||
# pi.vb.background.setOpacity(0)
|
||||
yaxis.pi = pi
|
||||
pi.chart_widget = self
|
||||
pi.hideButtons()
|
||||
|
||||
# compose this new plot's graphics with the current chart's
|
||||
|
@ -1231,43 +1202,56 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
add_label: bool = True,
|
||||
pi: Optional[pg.PlotItem] = None,
|
||||
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
|
||||
the input shm array ``shm``.
|
||||
|
||||
'''
|
||||
color = color or self.pen_color or 'default_light'
|
||||
pdi_kwargs.update({
|
||||
'color': color
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
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(
|
||||
name=name,
|
||||
plot=pi,
|
||||
_shm=shm,
|
||||
is_ohlc=False,
|
||||
is_ohlc=is_ohlc,
|
||||
# register curve graphics with this flow
|
||||
graphics=curve,
|
||||
graphics=graphics,
|
||||
)
|
||||
|
||||
# TODO: this probably needs its own method?
|
||||
|
@ -1278,12 +1262,41 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
f'{overlay} must be from `.plotitem_overlay()`'
|
||||
)
|
||||
pi = overlay
|
||||
else:
|
||||
# anchor_at = ('top', 'left')
|
||||
|
||||
# TODO: something instead of stickies for overlays
|
||||
# (we need something that avoids clutter on x-axis).
|
||||
self._add_sticky(name, bg_color=color)
|
||||
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')
|
||||
|
||||
# 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
|
||||
# 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
|
||||
# updates are implicit and require a bit of digging to
|
||||
# understand.
|
||||
pi.addItem(curve)
|
||||
pi.addItem(graphics)
|
||||
|
||||
return curve, data_key
|
||||
return graphics, data_key
|
||||
|
||||
# TODO: make this a ctx mngr
|
||||
def _add_sticky(
|
||||
def draw_ohlc(
|
||||
self,
|
||||
|
||||
name: str,
|
||||
bg_color='bracket',
|
||||
shm: ShmArray,
|
||||
|
||||
) -> YAxisLabel:
|
||||
array_key: Optional[str] = None,
|
||||
**draw_curve_kwargs,
|
||||
|
||||
# if the sticky is for our symbol
|
||||
# use the tick size precision for display
|
||||
sym = self.linked.symbol
|
||||
if name == sym.key:
|
||||
digits = sym.tick_size_digits
|
||||
else:
|
||||
digits = 2
|
||||
) -> (pg.GraphicsObject, str):
|
||||
'''
|
||||
Draw OHLC datums to chart.
|
||||
|
||||
# add y-axis "last" value label
|
||||
last = self._ysticks[name] = YAxisLabel(
|
||||
chart=self,
|
||||
# parent=self.getAxis('right'),
|
||||
parent=self.pi_overlay.get_axis(self.plotItem, 'right'),
|
||||
# TODO: pass this from symbol data
|
||||
digits=digits,
|
||||
opacity=1,
|
||||
bg_color=bg_color,
|
||||
'''
|
||||
return self.draw_curve(
|
||||
name=name,
|
||||
shm=shm,
|
||||
array_key=array_key,
|
||||
is_ohlc=True,
|
||||
**draw_curve_kwargs,
|
||||
)
|
||||
return last
|
||||
|
||||
def update_graphics_from_flow(
|
||||
self,
|
||||
|
|
|
@ -418,7 +418,7 @@ class Cursor(pg.GraphicsObject):
|
|||
hl.hide()
|
||||
|
||||
yl = YAxisLabel(
|
||||
chart=plot,
|
||||
pi=plot.plotItem,
|
||||
# parent=plot.getAxis('right'),
|
||||
parent=plot.pi_overlay.get_axis(plot.plotItem, 'right'),
|
||||
digits=digits or self.digits,
|
||||
|
|
|
@ -260,12 +260,14 @@ async def graphics_update_loop(
|
|||
hist_ohlcv = flume.hist_shm
|
||||
|
||||
# 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(
|
||||
*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_ohlcv.array[-1][['index', 'close']]
|
||||
)
|
||||
|
@ -289,7 +291,7 @@ async def graphics_update_loop(
|
|||
symbol = fast_chart.linked.symbol
|
||||
|
||||
l1 = L1Labels(
|
||||
fast_chart,
|
||||
fast_chart.plotItem,
|
||||
# determine precision/decimal lengths
|
||||
digits=symbol.tick_size_digits,
|
||||
size_digits=symbol.lot_size_digits,
|
||||
|
@ -333,7 +335,8 @@ async def graphics_update_loop(
|
|||
})
|
||||
|
||||
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_sticky = vlm_sticky
|
||||
|
||||
|
@ -947,7 +950,6 @@ async def link_views_with_region(
|
|||
|
||||
async def display_symbol_data(
|
||||
godwidget: GodWidget,
|
||||
provider: str,
|
||||
fqsns: list[str],
|
||||
loglevel: str,
|
||||
order_mode_started: trio.Event,
|
||||
|
@ -999,6 +1001,7 @@ async def display_symbol_data(
|
|||
|
||||
symbol = flume.symbol
|
||||
fqsn = symbol.fqsn
|
||||
brokername = symbol.brokers[0]
|
||||
|
||||
step_size_s = 1
|
||||
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 (
|
||||
not symbol.broker_info[provider].get('no_vlm', False)
|
||||
not symbol.broker_info[brokername].get('no_vlm', False)
|
||||
and has_vlm(ohlcv)
|
||||
):
|
||||
vlm_chart = await ln.start(
|
||||
|
|
|
@ -110,7 +110,8 @@ def update_fsp_chart(
|
|||
# sub-charts reference it under different 'named charts'.
|
||||
|
||||
# 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:
|
||||
last = last_row[array_key]
|
||||
last_val_sticky.update_from_data(-1, last)
|
||||
|
@ -685,7 +686,8 @@ async def open_vlm_displays(
|
|||
assert chart.name != linked.chart.name
|
||||
|
||||
# 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
|
||||
value = shm.array['volume'][-1]
|
||||
|
|
|
@ -26,6 +26,7 @@ from PyQt5.QtCore import QPointF
|
|||
|
||||
from ._axes import YAxisLabel
|
||||
from ._style import hcolor
|
||||
from ._pg_overrides import PlotItem
|
||||
|
||||
|
||||
class LevelLabel(YAxisLabel):
|
||||
|
@ -132,7 +133,7 @@ class LevelLabel(YAxisLabel):
|
|||
level = self.fields['level']
|
||||
|
||||
# map "level" to local coords
|
||||
abs_xy = self._chart.mapFromView(QPointF(0, level))
|
||||
abs_xy = self._pi.mapFromView(QPointF(0, level))
|
||||
|
||||
self.update_label(
|
||||
abs_xy,
|
||||
|
@ -149,7 +150,7 @@ class LevelLabel(YAxisLabel):
|
|||
h, w = self.set_label_str(fields)
|
||||
|
||||
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._h_shift * (w + self._x_offset),
|
||||
|
@ -236,10 +237,10 @@ class L1Label(LevelLabel):
|
|||
# Set a global "max L1 label length" so we can
|
||||
# look it up on order lines and adjust their
|
||||
# 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,
|
||||
w
|
||||
w,
|
||||
)
|
||||
|
||||
return h, w
|
||||
|
@ -251,17 +252,17 @@ class L1Labels:
|
|||
"""
|
||||
def __init__(
|
||||
self,
|
||||
chart: 'ChartPlotWidget', # noqa
|
||||
plotitem: PlotItem,
|
||||
digits: int = 2,
|
||||
size_digits: int = 3,
|
||||
font_size: str = 'small',
|
||||
) -> None:
|
||||
|
||||
self.chart = chart
|
||||
chart = self.chart = plotitem.chart_widget
|
||||
|
||||
raxis = chart.getAxis('right')
|
||||
raxis = plotitem.getAxis('right')
|
||||
kwargs = {
|
||||
'chart': chart,
|
||||
'chart': plotitem,
|
||||
'parent': raxis,
|
||||
|
||||
'opacity': 1,
|
||||
|
|
|
@ -98,7 +98,7 @@ class BarItems(pg.GraphicsObject):
|
|||
self,
|
||||
linked: LinkedSplits,
|
||||
plotitem: 'pg.PlotItem', # noqa
|
||||
pen_color: str = 'bracket',
|
||||
color: str = 'bracket',
|
||||
last_bar_color: str = 'bracket',
|
||||
|
||||
name: Optional[str] = None,
|
||||
|
@ -108,8 +108,8 @@ class BarItems(pg.GraphicsObject):
|
|||
self.linked = linked
|
||||
# XXX: for the mega-lulz increasing width here increases draw
|
||||
# latency... so probably don't do it until we figure that out.
|
||||
self._color = pen_color
|
||||
self.bars_pen = pg.mkPen(hcolor(pen_color), width=1)
|
||||
self._color = color
|
||||
self.bars_pen = pg.mkPen(hcolor(color), width=1)
|
||||
self.last_bar_pen = pg.mkPen(hcolor(last_bar_color), width=2)
|
||||
self._name = name
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ from typing import Optional
|
|||
|
||||
import pyqtgraph as pg
|
||||
|
||||
from ._axes import Axis
|
||||
|
||||
|
||||
def invertQTransform(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
|
||||
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__(
|
||||
self,
|
||||
|
@ -86,6 +102,8 @@ class PlotItem(pg.PlotItem):
|
|||
enableMenu=enableMenu,
|
||||
kargs=kargs,
|
||||
)
|
||||
self.name = name
|
||||
self.chart_widget = None
|
||||
# self.setAxisItems(
|
||||
# axisItems,
|
||||
# default_axes=default_axes,
|
||||
|
@ -209,7 +227,12 @@ class PlotItem(pg.PlotItem):
|
|||
# adding this is without it there's some weird
|
||||
# ``ViewBox`` geometry bug.. where a gap for the
|
||||
# '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)
|
||||
|
||||
|
|
|
@ -416,12 +416,26 @@ class CompleterView(QTreeView):
|
|||
section: str,
|
||||
values: Sequence[str],
|
||||
clear_all: bool = False,
|
||||
reverse: bool = False,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
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()
|
||||
if clear_all:
|
||||
# XXX: rewrite the model from scratch if caller requests it
|
||||
|
@ -598,22 +612,34 @@ class SearchWidget(QtWidgets.QWidget):
|
|||
self.show()
|
||||
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
|
||||
loaded with active data) feeds in the results section.
|
||||
|
||||
'''
|
||||
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(
|
||||
'cache',
|
||||
list(reversed(godw._chart_cache)),
|
||||
list(fqsns),
|
||||
# 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]]:
|
||||
'''Return the current completer tree selection as
|
||||
'''
|
||||
Return the current completer tree selection as
|
||||
a tuple ``(parent: str, child: str)`` if valid, else ``None``.
|
||||
|
||||
'''
|
||||
|
@ -663,12 +689,13 @@ class SearchWidget(QtWidgets.QWidget):
|
|||
provider, symbol = value
|
||||
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(
|
||||
provider,
|
||||
[symbol],
|
||||
'info',
|
||||
fqsns=[fqsn],
|
||||
loglevel='info',
|
||||
)
|
||||
|
||||
# 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
|
||||
# LIFO order. this is normally only done internally by
|
||||
# the chart on new symbols being loaded into memory
|
||||
godw.set_chart_symbol(
|
||||
fqsn, (
|
||||
godw.set_chart_symbols(
|
||||
(fqsn,), (
|
||||
godw.hist_linked,
|
||||
godw.rt_linked,
|
||||
)
|
||||
)
|
||||
self.show_only_cache_entries()
|
||||
self.show_cache_entries(only=True)
|
||||
|
||||
self.bar.focus()
|
||||
return fqsn
|
||||
|
@ -757,9 +784,10 @@ async def pack_matches(
|
|||
with trio.CancelScope() as cs:
|
||||
task_status.started(cs)
|
||||
# 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
|
||||
|
||||
# print(f'results from {provider}: {results}')
|
||||
|
@ -806,7 +834,7 @@ async def fill_results(
|
|||
has_results: defaultdict[str, set[str]] = defaultdict(set)
|
||||
|
||||
# show cached feed list at startup
|
||||
search.show_only_cache_entries()
|
||||
search.show_cache_entries()
|
||||
search.on_resize()
|
||||
|
||||
while True:
|
||||
|
@ -860,8 +888,9 @@ async def fill_results(
|
|||
# it hasn't already been searched with the current
|
||||
# input pattern (in which case just look up the old
|
||||
# results).
|
||||
if (period >= pause) and (
|
||||
provider not in already_has_results
|
||||
if (
|
||||
period >= pause
|
||||
and provider not in already_has_results
|
||||
):
|
||||
|
||||
# TODO: it may make more sense TO NOT search the
|
||||
|
@ -869,7 +898,9 @@ async def fill_results(
|
|||
# cpu-bound.
|
||||
if provider != 'cache':
|
||||
view.clear_section(
|
||||
provider, status_field='-> searchin..')
|
||||
provider,
|
||||
status_field='-> searchin..',
|
||||
)
|
||||
|
||||
await n.start(
|
||||
pack_matches,
|
||||
|
@ -890,11 +921,20 @@ async def fill_results(
|
|||
# re-searching it's ``dict`` since it's easier
|
||||
# but it also causes it to be slower then cached
|
||||
# results from other providers on occasion.
|
||||
if results and provider != 'cache':
|
||||
view.set_section_entries(
|
||||
section=provider,
|
||||
values=results,
|
||||
)
|
||||
if (
|
||||
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:
|
||||
view.clear_section(provider)
|
||||
|
||||
|
@ -937,7 +977,7 @@ async def handle_keyboard_input(
|
|||
)
|
||||
|
||||
bar.focus()
|
||||
search.show_only_cache_entries()
|
||||
search.show_cache_entries()
|
||||
await trio.sleep(0)
|
||||
|
||||
async for kbmsg in recv_chan:
|
||||
|
@ -949,20 +989,21 @@ async def handle_keyboard_input(
|
|||
if mods == Qt.ControlModifier:
|
||||
ctl = True
|
||||
|
||||
if key in (Qt.Key_Enter, Qt.Key_Return):
|
||||
if key in (
|
||||
Qt.Key_Enter,
|
||||
Qt.Key_Return
|
||||
):
|
||||
_search_enabled = False
|
||||
await search.chart_current_item(clear_to_cache=True)
|
||||
search.show_only_cache_entries()
|
||||
search.show_cache_entries(only=True)
|
||||
view.show_matches()
|
||||
search.focus()
|
||||
|
||||
elif not ctl and not bar.text():
|
||||
# if nothing in search text show the cache
|
||||
view.set_section_entries(
|
||||
'cache',
|
||||
list(reversed(godwidget._chart_cache)),
|
||||
clear_all=True,
|
||||
)
|
||||
|
||||
# TODO: really should factor this somewhere..bc
|
||||
# we're doin it in another spot as well..
|
||||
search.show_cache_entries(only=True)
|
||||
continue
|
||||
|
||||
# cancel and close
|
||||
|
@ -1025,6 +1066,8 @@ async def handle_keyboard_input(
|
|||
if parent_item and parent_item.text() == 'cache':
|
||||
await search.chart_current_item(clear_to_cache=False)
|
||||
|
||||
# ACTUAL SEARCH BLOCK #
|
||||
# where we fuzzy complete and fill out sections.
|
||||
elif not ctl:
|
||||
# relay to completer task
|
||||
_search_enabled = True
|
||||
|
@ -1035,13 +1078,21 @@ async def handle_keyboard_input(
|
|||
async def search_simple_dict(
|
||||
text: str,
|
||||
source: dict,
|
||||
|
||||
) -> 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
|
||||
# as in the case of the current app's local symbol cache
|
||||
matches = fuzzy.extractBests(
|
||||
text,
|
||||
source.keys(),
|
||||
tokens,
|
||||
score_cutoff=90,
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue