Merge pull request #259 from pikers/overlayed_dvlm

Overlayed $vlm
py3.10_support
goodboy 2022-01-26 13:47:52 -05:00 committed by GitHub
commit 8fe2bd6614
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 373 additions and 195 deletions

View File

@ -1,5 +1,5 @@
# piker: trading gear for hackers # piker: trading gear for hackers
# Copyright (C) 2018-present Tyler Goodlet (in stewardship of piker0) # Copyright (C) Tyler Goodlet (in stewardship of pikers)
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by

View File

@ -18,21 +18,26 @@
Anchor funtions for UI placement of annotions. Anchor funtions for UI placement of annotions.
''' '''
from typing import Callable from __future__ import annotations
from typing import Callable, TYPE_CHECKING
from PyQt5.QtCore import QPointF from PyQt5.QtCore import QPointF
from PyQt5.QtWidgets import QGraphicsPathItem from PyQt5.QtWidgets import QGraphicsPathItem
from ._label import Label if TYPE_CHECKING:
from ._axes import PriceAxis
from ._chart import ChartPlotWidget
from ._label import Label
def marker_right_points( def marker_right_points(
chart: ChartPlotWidget, # noqa
chart: 'ChartPlotWidget', # noqa
marker_size: int = 20, marker_size: int = 20,
) -> (float, float, float): ) -> (float, float, float):
'''Return x-dimension, y-axis-aware, level-line marker oriented scene values. '''
Return x-dimension, y-axis-aware, level-line marker oriented scene
values.
X values correspond to set the end of a level line, end of X values correspond to set the end of a level line, end of
a paried level line marker, and the right most side of the "right" a paried level line marker, and the right most side of the "right"
@ -57,16 +62,17 @@ def vbr_left(
label: Label, label: Label,
) -> Callable[..., float]: ) -> Callable[..., float]:
"""Return a closure which gives the scene x-coordinate for the '''
leftmost point of the containing view box. Return a closure which gives the scene x-coordinate for the leftmost
point of the containing view box.
""" '''
return label.vbr().left return label.vbr().left
def right_axis( def right_axis(
chart: 'ChartPlotWidget', # noqa chart: ChartPlotWidget, # noqa
label: Label, label: Label,
side: str = 'left', side: str = 'left',
@ -141,13 +147,13 @@ def gpath_pin(
return path_br.bottomRight() - QPointF(label.w, label.h / 6) return path_br.bottomRight() - QPointF(label.w, label.h / 6)
def pp_tight_and_right( def pp_tight_and_right(
label: Label label: Label
) -> QPointF: ) -> QPointF:
'''Place *just* right of the pp label. '''
Place *just* right of the pp label.
''' '''
txt = label.txt # txt = label.txt
return label.txt.pos() + QPointF(label.w - label.h/3, 0) return label.txt.pos() + QPointF(label.w - label.h/3, 0)

View File

@ -18,8 +18,8 @@
Chart axes graphics and behavior. Chart axes graphics and behavior.
""" """
import functools from functools import lru_cache
from typing import List, Tuple, Optional from typing import List, Tuple, Optional, Callable
from math import floor from math import floor
import pandas as pd import pandas as pd
@ -27,8 +27,10 @@ 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 ._style import DpiAwareFont, hcolor, _font
from ..data._source import float_digits from ..data._source import float_digits
from ._label import Label
from ._style import DpiAwareFont, hcolor, _font
from ._interaction import ChartView
_axis_pen = pg.mkPen(hcolor('bracket')) _axis_pen = pg.mkPen(hcolor('bracket'))
@ -42,7 +44,6 @@ class Axis(pg.AxisItem):
self, self,
linkedsplits, linkedsplits,
typical_max_str: str = '100 000.000', typical_max_str: str = '100 000.000',
min_tick: int = 2,
**kwargs **kwargs
) -> None: ) -> None:
@ -52,7 +53,6 @@ class Axis(pg.AxisItem):
# self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) # self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
self.linkedsplits = linkedsplits self.linkedsplits = linkedsplits
self._min_tick = min_tick
self._dpi_font = _font self._dpi_font = _font
self.setTickFont(_font.font) self.setTickFont(_font.font)
@ -74,7 +74,10 @@ class Axis(pg.AxisItem):
}) })
self.setTickFont(_font.font) self.setTickFont(_font.font)
# NOTE: this is for surrounding "border"
self.setPen(_axis_pen) self.setPen(_axis_pen)
# this is the text color
self.setTextPen(_axis_pen)
self.typical_br = _font._qfm.boundingRect(typical_max_str) self.typical_br = _font._qfm.boundingRect(typical_max_str)
# size the pertinent axis dimension to a "typical value" # size the pertinent axis dimension to a "typical value"
@ -83,40 +86,102 @@ class Axis(pg.AxisItem):
def size_to_values(self) -> None: def size_to_values(self) -> None:
pass pass
def set_min_tick(self, size: int) -> None:
self._min_tick = size
def txt_offsets(self) -> Tuple[int, int]: def txt_offsets(self) -> Tuple[int, int]:
return tuple(self.style['tickTextOffset']) return tuple(self.style['tickTextOffset'])
class PriceAxis(Axis): class PriceAxis(Axis):
def __init__(
self,
*args,
min_tick: int = 2,
title: str = '',
formatter: Optional[Callable[[float], str]] = None,
**kwargs
) -> None:
super().__init__(*args, **kwargs)
self.formatter = formatter
self._min_tick: int = min_tick
self.title = None
def set_title(
self,
title: str,
view: Optional[ChartView] = None
) -> Label:
'''
Set a sane UX label using our built-in ``Label``.
'''
# XXX: built-in labels but they're huge, and placed weird..
# self.setLabel(title)
# self.showLabel()
label = self.title = Label(
view=view or self.linkedView(),
fmt_str=title,
color='bracket',
parent=self,
# update_on_range_change=False,
)
def below_axis() -> QPointF:
return QPointF(
0,
self.size().height(),
)
# XXX: doesn't work? have to pass it above
# label.txt.setParent(self)
label.scene_anchor = below_axis
label.render()
label.show()
label.update()
return label
def set_min_tick(
self,
size: int
) -> None:
self._min_tick = size
def size_to_values(self) -> None: def size_to_values(self) -> None:
# self.typical_br = _font._qfm.boundingRect(typical_max_str)
self.setWidth(self.typical_br.width()) self.setWidth(self.typical_br.width())
# XXX: drop for now since it just eats up h space # XXX: drop for now since it just eats up h space
def tickStrings( def tickStrings(
self, self,
vals, vals: tuple[float],
scale, scale: float,
spacing, spacing: float,
):
# TODO: figure out how to enforce min tick spacing by passing ) -> list[str]:
# it into the parent type # TODO: figure out how to enforce min tick spacing by passing it
digits = max(float_digits(spacing * scale), self._min_tick) # into the parent type
digits = max(
float_digits(spacing * scale),
self._min_tick,
)
if self.title:
self.title.update()
# print(f'vals: {vals}\nscale: {scale}\nspacing: {spacing}') # print(f'vals: {vals}\nscale: {scale}\nspacing: {spacing}')
# print(f'digits: {digits}') # print(f'digits: {digits}')
return [ if not self.formatter:
('{value:,.{digits}f}').format( return [
digits=digits, ('{value:,.{digits}f}').format(
value=v, digits=digits,
).replace(',', ' ') for v in vals value=v,
] ).replace(',', ' ') for v in vals
]
else:
return list(map(self.formatter, vals))
class DynamicDateAxis(Axis): class DynamicDateAxis(Axis):
@ -136,6 +201,7 @@ class DynamicDateAxis(Axis):
def _indexes_to_timestrs( def _indexes_to_timestrs(
self, self,
indexes: List[int], indexes: List[int],
) -> List[str]: ) -> List[str]:
chart = self.linkedsplits.chart chart = self.linkedsplits.chart
@ -165,9 +231,10 @@ class DynamicDateAxis(Axis):
def tickStrings( def tickStrings(
self, self,
values: tuple[float], values: tuple[float],
scale, scale: float,
spacing, spacing: float,
):
) -> list[str]:
# info = self.tickStrings.cache_info() # info = self.tickStrings.cache_info()
# print(info) # print(info)
return self._indexes_to_timestrs(values) return self._indexes_to_timestrs(values)
@ -220,6 +287,8 @@ class AxisLabel(pg.GraphicsObject):
self.path = None self.path = None
self.rect = None self.rect = None
self._pw = self.pixelWidth()
def paint( def paint(
self, self,
p: QtGui.QPainter, p: QtGui.QPainter,
@ -269,9 +338,10 @@ class AxisLabel(pg.GraphicsObject):
def boundingRect(self): # noqa def boundingRect(self): # noqa
"""Size the graphics space from the text contents. '''
Size the graphics space from the text contents.
""" '''
if self.label_str: if self.label_str:
self._size_br_from_str(self.label_str) self._size_br_from_str(self.label_str)
@ -287,23 +357,32 @@ class AxisLabel(pg.GraphicsObject):
return QtCore.QRectF() return QtCore.QRectF()
# return self.rect or QtCore.QRectF() # TODO: but the input probably needs to be the "len" of
# the current text value:
@lru_cache
def _size_br_from_str(
self,
value: str
def _size_br_from_str(self, value: str) -> None: ) -> tuple[float, float]:
"""Do our best to render the bounding rect to a set margin '''
Do our best to render the bounding rect to a set margin
around provided string contents. around provided string contents.
""" '''
# size the filled rect to text and/or parent axis # size the filled rect to text and/or parent axis
# if not self._txt_br: # if not self._txt_br:
# # XXX: this can't be c # # XXX: this can't be called until stuff is rendered?
# self._txt_br = self._dpifont.boundingRect(value) # self._txt_br = self._dpifont.boundingRect(value)
txt_br = self._txt_br = self._dpifont.boundingRect(value) txt_br = self._txt_br = self._dpifont.boundingRect(value)
txt_h, txt_w = txt_br.height(), txt_br.width() txt_h, txt_w = txt_br.height(), txt_br.width()
# print(f'wsw: {self._dpifont.boundingRect(" ")}')
# allow subtypes to specify a static width and height # allow subtypes to specify a static width and height
h, w = self.size_hint() h, w = self.size_hint()
# print(f'axis size: {self._parent.size()}')
# print(f'axis geo: {self._parent.geometry()}')
self.rect = QtCore.QRectF( self.rect = QtCore.QRectF(
0, 0, 0, 0,
@ -314,7 +393,7 @@ class AxisLabel(pg.GraphicsObject):
# hb = self.path.controlPointRect() # hb = self.path.controlPointRect()
# hb_size = hb.size() # hb_size = hb.size()
return self.rect return (self.rect.width(), self.rect.height())
# _common_text_flags = ( # _common_text_flags = (
# QtCore.Qt.TextDontClip | # QtCore.Qt.TextDontClip |
@ -342,6 +421,7 @@ class XAxisLabel(AxisLabel):
abs_pos: QPointF, # scene coords abs_pos: QPointF, # scene coords
value: float, # data for text value: float, # data for text
offset: int = 0 # if have margins, k? offset: int = 0 # if have margins, k?
) -> None: ) -> None:
timestrs = self._parent._indexes_to_timestrs([int(value)]) timestrs = self._parent._indexes_to_timestrs([int(value)])
@ -356,17 +436,19 @@ class XAxisLabel(AxisLabel):
w = self.boundingRect().width() w = self.boundingRect().width()
self.setPos(QPointF( self.setPos(
abs_pos.x() - w/2, QPointF(
y_offset/2, abs_pos.x() - w/2 - self._pw,
)) y_offset/2,
)
)
self.update() self.update()
def _draw_arrow_path(self): def _draw_arrow_path(self):
y_offset = self._parent.style['tickTextOffset'][1] y_offset = self._parent.style['tickTextOffset'][1]
path = QtGui.QPainterPath() path = QtGui.QPainterPath()
h, w = self.rect.height(), self.rect.width() h, w = self.rect.height(), self.rect.width()
middle = w/2 - 0.5 middle = w/2 - self._pw * 0.5
aw = h/2 aw = h/2
left = middle - aw left = middle - aw
right = middle + aw right = middle + aw
@ -410,8 +492,12 @@ class YAxisLabel(AxisLabel):
self.x_offset, y_offset = self._parent.txt_offsets() self.x_offset, y_offset = self._parent.txt_offsets()
def size_hint(self) -> Tuple[float, float]: def size_hint(self) -> Tuple[float, float]:
# size to parent axis width # size to parent axis width(-ish)
return None, self._parent.width() wsh = self._dpifont.boundingRect(' ').height() / 2
return (
None,
self._parent.size().width() - wsh,
)
def update_label( def update_label(
self, self,
@ -432,16 +518,19 @@ class YAxisLabel(AxisLabel):
br = self.boundingRect() br = self.boundingRect()
h = br.height() h = br.height()
self.setPos(QPointF( self.setPos(
x_offset, QPointF(
abs_pos.y() - h / 2 - self._y_margin / 2 x_offset,
)) abs_pos.y() - h / 2 - self._pw,
)
)
self.update() self.update()
def update_on_resize(self, vr, r): def update_on_resize(self, vr, r):
"""Tiis is a ``.sigRangeChanged()`` handler. '''
This is a ``.sigRangeChanged()`` handler.
""" '''
index, last = self._last_datum index, last = self._last_datum
if index is not None: if index is not None:
self.update_from_data(index, last) self.update_from_data(index, last)
@ -451,11 +540,13 @@ class YAxisLabel(AxisLabel):
index: int, index: int,
value: float, value: float,
_save_last: bool = True, _save_last: bool = True,
) -> None: ) -> None:
"""Update the label's text contents **and** position from '''
Update the label's text contents **and** position from
a view box coordinate datum. a view box coordinate datum.
""" '''
if _save_last: if _save_last:
self._last_datum = (index, value) self._last_datum = (index, value)
@ -469,7 +560,7 @@ class YAxisLabel(AxisLabel):
path = QtGui.QPainterPath() path = QtGui.QPainterPath()
h = self.rect.height() h = self.rect.height()
path.moveTo(0, 0) path.moveTo(0, 0)
path.lineTo(-x_offset - h/4, h/2.) path.lineTo(-x_offset - h/4, h/2. - self._pw/2)
path.lineTo(0, h) path.lineTo(0, h)
path.closeSubpath() path.closeSubpath()
self.path = path self.path = path

View File

@ -479,14 +479,20 @@ class LinkedSplits(QWidget):
axisItems=axes, axisItems=axes,
**cpw_kwargs, **cpw_kwargs,
) )
cpw.hideAxis('left')
cpw.hideAxis('bottom')
if self.xaxis_chart: if self.xaxis_chart:
self.xaxis_chart.hideAxis('bottom')
# presuming we only want it at the true bottom of all charts. # presuming we only want it at the true bottom of all charts.
# XXX: uses new api from our ``pyqtgraph`` fork. # XXX: uses new api from our ``pyqtgraph`` fork.
# https://github.com/pikers/pyqtgraph/tree/plotitemoverlay_onto_pg_master # https://github.com/pikers/pyqtgraph/tree/plotitemoverlay_onto_pg_master
_ = self.xaxis_chart.removeAxis('bottom', unlink=False) # _ = self.xaxis_chart.removeAxis('bottom', unlink=False)
assert 'bottom' not in self.xaxis_chart.plotItem.axes # assert 'bottom' not in self.xaxis_chart.plotItem.axes
self.xaxis_chart = cpw self.xaxis_chart = cpw
cpw.showAxis('bottom')
if self.xaxis_chart is None: if self.xaxis_chart is None:
self.xaxis_chart = cpw self.xaxis_chart = cpw
@ -726,11 +732,6 @@ class ChartPlotWidget(pg.PlotWidget):
self._static_yrange = static_yrange # for "known y-range style" self._static_yrange = static_yrange # for "known y-range style"
self._view_mode: str = 'follow' self._view_mode: str = 'follow'
# show only right side axes
self.hideAxis('left')
self.showAxis('right')
# self.showAxis('left')
# show background grid # show background grid
self.showGrid(x=False, y=True, alpha=0.3) self.showGrid(x=False, y=True, alpha=0.3)
@ -862,53 +863,58 @@ class ChartPlotWidget(pg.PlotWidget):
def overlay_plotitem( def overlay_plotitem(
self, self,
name: str, name: str,
index: Optional[int] = None,
axis_title: Optional[str] = None,
axis_side: str = 'right',
axis_kwargs: dict = {},
) -> pg.PlotItem: ) -> pg.PlotItem:
# Custom viewbox impl # Custom viewbox impl
cv = self.mk_vb(name) cv = self.mk_vb(name)
cv.chart = self cv.chart = self
# xaxis = DynamicDateAxis( allowed_sides = {'left', 'right'}
# orientation='bottom', if axis_side not in allowed_sides:
# linkedsplits=self.linked, raise ValueError(f'``axis_side``` must be in {allowed_sides}')
# )
yaxis = PriceAxis( yaxis = PriceAxis(
orientation='right', orientation=axis_side,
linkedsplits=self.linked, linkedsplits=self.linked,
**axis_kwargs,
) )
plotitem = pg.PlotItem( pi = pg.PlotItem(
parent=self.plotItem, parent=self.plotItem,
name=name, name=name,
enableMenu=False, enableMenu=False,
viewBox=cv, viewBox=cv,
axisItems={ axisItems={
# 'bottom': xaxis, # 'bottom': xaxis,
'right': yaxis, axis_side: yaxis,
}, },
default_axes=[], default_axes=[],
) )
# plotitem.setAxisItems( pi.hideButtons()
# add_to_layout=False,
# axisItems={
# 'bottom': xaxis,
# 'right': yaxis,
# },
# )
# plotite.hideAxis('right')
# plotite.hideAxis('bottom')
# plotitem.addItem(curve)
cv.enable_auto_yrange() cv.enable_auto_yrange()
# plotitem.enableAutoRange(axis='y') # compose this new plot's graphics with the current chart's
plotitem.hideButtons() # existing one but with separate axes as neede and specified.
self.pi_overlay.add_plotitem( self.pi_overlay.add_plotitem(
plotitem, pi,
index=index,
# only link x-axes, # only link x-axes,
link_axes=(0,), link_axes=(0,),
) )
return plotitem
# add axis title
# TODO: do we want this API to still work?
# raxis = pi.getAxis('right')
axis = self.pi_overlay.get_axis(pi, axis_side)
axis.set_title(axis_title or name, view=pi.getViewBox())
return pi
def draw_curve( def draw_curve(
self, self,
@ -1014,7 +1020,8 @@ class ChartPlotWidget(pg.PlotWidget):
# add y-axis "last" value label # add y-axis "last" value label
last = self._ysticks[name] = YAxisLabel( last = self._ysticks[name] = YAxisLabel(
chart=self, chart=self,
parent=self.getAxis('right'), # parent=self.getAxis('right'),
parent=self.pi_overlay.get_axis(self.plotItem, 'right'),
# TODO: pass this from symbol data # TODO: pass this from symbol data
digits=digits, digits=digits,
opacity=1, opacity=1,

View File

@ -43,8 +43,8 @@ log = get_logger(__name__)
# latency (in terms of perceived lag in cross hair) so really be sure # latency (in terms of perceived lag in cross hair) so really be sure
# there's an improvement if you want to change it! # there's an improvement if you want to change it!
_mouse_rate_limit = 58 # TODO; should we calc current screen refresh rate? _mouse_rate_limit = 120 # TODO; should we calc current screen refresh rate?
_debounce_delay = 1 / 60 _debounce_delay = 1 / 40
_ch_label_opac = 1 _ch_label_opac = 1
@ -369,7 +369,13 @@ class Cursor(pg.GraphicsObject):
self, self,
plot: 'ChartPlotWidget', # noqa plot: 'ChartPlotWidget', # noqa
digits: int = 0, digits: int = 0,
) -> None: ) -> None:
'''
Add chart to tracked set such that a cross-hair and possibly
curve tracking cursor can be drawn on the plot.
'''
# add ``pg.graphicsItems.InfiniteLine``s # add ``pg.graphicsItems.InfiniteLine``s
# vertical and horizonal lines and a y-axis label # vertical and horizonal lines and a y-axis label
@ -382,7 +388,8 @@ class Cursor(pg.GraphicsObject):
yl = YAxisLabel( yl = YAxisLabel(
chart=plot, chart=plot,
parent=plot.getAxis('right'), # parent=plot.getAxis('right'),
parent=plot.pi_overlay.get_axis(plot.plotItem, 'right'),
digits=digits or self.digits, digits=digits or self.digits,
opacity=_ch_label_opac, opacity=_ch_label_opac,
bg_color=self.label_color, bg_color=self.label_color,
@ -424,19 +431,25 @@ class Cursor(pg.GraphicsObject):
# ONLY create an x-axis label for the cursor # ONLY create an x-axis label for the cursor
# if this plot owns the 'bottom' axis. # if this plot owns the 'bottom' axis.
if 'bottom' in plot.plotItem.axes: # if 'bottom' in plot.plotItem.axes:
self.xaxis_label = XAxisLabel( if plot.linked.xaxis_chart is plot:
xlabel = self.xaxis_label = XAxisLabel(
parent=self.plots[plot_index].getAxis('bottom'), parent=self.plots[plot_index].getAxis('bottom'),
# parent=self.plots[plot_index].pi_overlay.get_axis(plot.plotItem, 'bottom'),
opacity=_ch_label_opac, opacity=_ch_label_opac,
bg_color=self.label_color, bg_color=self.label_color,
) )
# place label off-screen during startup # place label off-screen during startup
self.xaxis_label.setPos(self.plots[0].mapFromView(QPointF(0, 0))) xlabel.setPos(
self.plots[0].mapFromView(QPointF(0, 0))
)
xlabel.show()
def add_curve_cursor( def add_curve_cursor(
self, self,
plot: 'ChartPlotWidget', # noqa plot: 'ChartPlotWidget', # noqa
curve: 'PlotCurveItem', # noqa curve: 'PlotCurveItem', # noqa
) -> LineDot: ) -> LineDot:
# if this plot contains curves add line dot "cursors" to denote # if this plot contains curves add line dot "cursors" to denote
# the current sample under the mouse # the current sample under the mouse
@ -493,24 +506,27 @@ class Cursor(pg.GraphicsObject):
ix = round(x) # since bars are centered around index ix = round(x) # since bars are centered around index
# px perfect...
line_offset = self._lw / 2
# round y value to nearest tick step # round y value to nearest tick step
m = self._y_incr_mult m = self._y_incr_mult
iy = round(y * m) / m iy = round(y * m) / m
vl_y = iy - line_offset
# px perfect...
line_offset = self._lw / 2
# update y-range items # update y-range items
if iy != last_iy: if iy != last_iy:
if self._y_label_update: if self._y_label_update:
self.graphics[self.active_plot]['yl'].update_label( self.graphics[self.active_plot]['yl'].update_label(
abs_pos=plot.mapFromView(QPointF(ix, iy)), # abs_pos=plot.mapFromView(QPointF(ix, iy)),
abs_pos=plot.mapFromView(QPointF(ix, vl_y)),
value=iy value=iy
) )
# only update horizontal xhair line if label is enabled # only update horizontal xhair line if label is enabled
self.graphics[plot]['hl'].setY(iy) # self.graphics[plot]['hl'].setY(iy)
self.graphics[plot]['hl'].setY(vl_y)
# update all trackers # update all trackers
for item in self._trackers: for item in self._trackers:
@ -541,21 +557,18 @@ class Cursor(pg.GraphicsObject):
# left axis offset width for calcuating # left axis offset width for calcuating
# absolute x-axis label placement. # absolute x-axis label placement.
left_axis_width = 0 left_axis_width = 0
left = axes.get('left')
if left:
left_axis_width = left['item'].width()
if 'bottom' in axes: # map back to abs (label-local) coordinates
self.xaxis_label.update_label(
left = axes.get('left') abs_pos=(
if left: plot.mapFromView(QPointF(vl_x, iy)) -
left_axis_width = left['item'].width() QPointF(left_axis_width, 0)
),
# map back to abs (label-local) coordinates value=ix,
self.xaxis_label.update_label( )
abs_pos=(
plot.mapFromView(QPointF(vl_x, iy)) -
QPointF(left_axis_width, 0)
),
value=ix,
)
self._datum_xy = ix, iy self._datum_xy = ix, iy

View File

@ -115,13 +115,14 @@ async def update_linked_charts_graphics(
vlm_chart: Optional[ChartPlotWidget] = None, vlm_chart: Optional[ChartPlotWidget] = None,
) -> None: ) -> None:
'''The 'main' (price) chart real-time update loop. '''
The 'main' (price) chart real-time update loop.
Receive from the primary instrument quote stream and update the OHLC Receive from the primary instrument quote stream and update the OHLC
chart. chart.
''' '''
# TODO: bunch of stuff: # TODO: bunch of stuff (some might be done already, can't member):
# - I'm starting to think all this logic should be # - I'm starting to think all this logic should be
# done in one place and "graphics update routines" # done in one place and "graphics update routines"
# should not be doing any length checking and array diffing. # should not be doing any length checking and array diffing.
@ -181,13 +182,34 @@ async def update_linked_charts_graphics(
view = chart.view view = chart.view
last_quote = time.time() last_quote = time.time()
# async def iter_drain_quotes():
# # NOTE: all code below this loop is expected to be synchronous
# # and thus draw instructions are not picked up jntil the next
# # wait / iteration.
# async for quotes in stream:
# while True:
# try:
# moar = stream.receive_nowait()
# except trio.WouldBlock:
# yield quotes
# break
# else:
# for sym, quote in moar.items():
# ticks_frame = quote.get('ticks')
# if ticks_frame:
# quotes[sym].setdefault(
# 'ticks', []).extend(ticks_frame)
# print('pulled extra')
# yield quotes
# async for quotes in iter_drain_quotes():
async for quotes in stream: async for quotes in stream:
now = time.time()
quote_period = time.time() - last_quote quote_period = time.time() - last_quote
quote_rate = round( quote_rate = round(
1/quote_period, 1) if quote_period > 0 else float('inf') 1/quote_period, 1) if quote_period > 0 else float('inf')
if ( if (
quote_period <= 1/_quote_throttle_rate quote_period <= 1/_quote_throttle_rate
@ -196,7 +218,8 @@ async def update_linked_charts_graphics(
and quote_rate >= _quote_throttle_rate * 1.5 and quote_rate >= _quote_throttle_rate * 1.5
): ):
log.warning(f'High quote rate {symbol.key}: {quote_rate}') log.warning(f'High quote rate {symbol.key}: {quote_rate}')
last_quote = now
last_quote = time.time()
# chart isn't active/shown so skip render cycle and pause feed(s) # chart isn't active/shown so skip render cycle and pause feed(s)
if chart.linked.isHidden(): if chart.linked.isHidden():
@ -621,9 +644,15 @@ async def display_symbol_data(
await trio.sleep(0) await trio.sleep(0)
linkedsplits.resize_sidepanes() linkedsplits.resize_sidepanes()
# NOTE: we pop the volume chart from the subplots set so
# that it isn't double rendered in the display loop
# above since we do a maxmin calc on the volume data to
# determine if auto-range adjustements should be made.
linkedsplits.subplots.pop('volume', None)
# TODO: make this not so shit XD # TODO: make this not so shit XD
# close group status # close group status
sbar._status_groups[loading_sym_key][1]() sbar._status_groups[loading_sym_key][1]()
# let the app run. # let the app run.. bby
await trio.sleep_forever() await trio.sleep_forever()

View File

@ -33,7 +33,9 @@ import pyqtgraph as pg
import trio import trio
from trio_typing import TaskStatus from trio_typing import TaskStatus
from ._axes import PriceAxis
from .._cacheables import maybe_open_context from .._cacheables import maybe_open_context
from ..calc import humanize
from ..data._sharedmem import ( from ..data._sharedmem import (
ShmArray, ShmArray,
maybe_open_shm_array, maybe_open_shm_array,
@ -653,7 +655,7 @@ async def open_vlm_displays(
last_val_sticky.update_from_data(-1, value) last_val_sticky.update_from_data(-1, value)
chart.update_curve_from_array( vlm_curve = chart.update_curve_from_array(
'volume', 'volume',
shm.array, shm.array,
) )
@ -661,73 +663,100 @@ async def open_vlm_displays(
# size view to data once at outset # size view to data once at outset
chart.view._set_yrange() chart.view._set_yrange()
if not dvlm: # add axis title
return axis = chart.getAxis('right')
axis.set_title(' vlm')
# spawn and overlay $ vlm on the same subchart if dvlm:
shm, started = await admin.start_engine_task(
'dolla_vlm', # spawn and overlay $ vlm on the same subchart
# linked.symbol.front_feed(), # data-feed symbol key shm, started = await admin.start_engine_task(
{ # fsp engine conf 'dolla_vlm',
'func_name': 'dolla_vlm', # linked.symbol.front_feed(), # data-feed symbol key
'zero_on_step': True, { # fsp engine conf
'params': { 'func_name': 'dolla_vlm',
'price_func': { 'zero_on_step': True,
'default_value': 'chl3', 'params': {
'price_func': {
'default_value': 'chl3',
},
}, },
}, },
}, # loglevel,
# loglevel, )
) # profiler(f'created shm for fsp actor: {display_name}')
# profiler(f'created shm for fsp actor: {display_name}')
await started.wait() await started.wait()
pi = chart.overlay_plotitem( pi = chart.overlay_plotitem(
'dolla_vlm', 'dolla_vlm',
) index=0, # place axis on inside (nearest to chart)
# add custom auto range handler axis_title=' $vlm',
pi.vb._maxmin = partial(maxmin, name='dolla_vlm') axis_side='right',
axis_kwargs={
'typical_max_str': ' 100.0 M ',
'formatter': partial(
humanize,
digits=2,
),
},
curve, _ = chart.draw_curve( )
name='dolla_vlm', # add custom auto range handler
data=shm.array, pi.vb._maxmin = partial(maxmin, name='dolla_vlm')
array_key='dolla_vlm', curve, _ = chart.draw_curve(
overlay=pi,
color='charcoal',
step_mode=True,
# **conf.get('chart_kwargs', {})
)
# TODO: is there a way to "sync" the dual axes such that only
# one curve is needed?
# curve.hide()
# TODO: we need a better API to do this.. name='dolla_vlm',
# specially store ref to shm for lookup in display loop data=shm.array,
# since only a placeholder of `None` is entered in
# ``.draw_curve()``.
chart._overlays['dolla_vlm'] = shm
# XXX: old dict-style config before it was moved into the helper task array_key='dolla_vlm',
# 'dolla_vlm': { overlay=pi,
# 'func_name': 'dolla_vlm', # color='bracket',
# 'zero_on_step': True, # TODO: this color or dark volume
# 'overlay': 'volume', # color='charcoal',
# 'separate_axes': True, step_mode=True,
# 'params': { # **conf.get('chart_kwargs', {})
# 'price_func': { )
# 'default_value': 'chl3', # TODO: is there a way to "sync" the dual axes such that only
# # tell target ``Edit`` widget to not allow # one curve is needed?
# # edits for now. # hide the original vlm curve since the $vlm one is now
# 'widget_kwargs': {'readonly': True}, # displayed and the curves are effectively the same minus
# }, # liquidity events (well at least on low OHLC periods - 1s).
# }, vlm_curve.hide()
# 'chart_kwargs': {'step_mode': True}
# },
# } # TODO: we need a better API to do this..
# specially store ref to shm for lookup in display loop
# since only a placeholder of `None` is entered in
# ``.draw_curve()``.
chart._overlays['dolla_vlm'] = shm
# XXX: old dict-style config before it was moved into the
# helper task
# 'dolla_vlm': {
# 'func_name': 'dolla_vlm',
# 'zero_on_step': True,
# 'overlay': 'volume',
# 'separate_axes': True,
# 'params': {
# 'price_func': {
# 'default_value': 'chl3',
# # tell target ``Edit`` widget to not allow
# # edits for now.
# 'widget_kwargs': {'readonly': True},
# },
# },
# 'chart_kwargs': {'step_mode': True}
# },
# }
for name, axis_info in pi.axes.items():
# lol this sux XD
axis = axis_info['item']
if isinstance(axis, PriceAxis):
axis.size_to_values()
# built-in vlm fsps # built-in vlm fsps
for display_name, conf in { for display_name, conf in {

View File

@ -34,7 +34,7 @@ from ._style import (
class Label: class Label:
""" '''
A plain ol' "scene label" using an underlying ``QGraphicsTextItem``. A plain ol' "scene label" using an underlying ``QGraphicsTextItem``.
After hacking for many days on multiple "label" systems inside After hacking for many days on multiple "label" systems inside
@ -50,10 +50,8 @@ class Label:
small, re-usable label components that can actually be used to build small, re-usable label components that can actually be used to build
production grade UIs... production grade UIs...
""" '''
def __init__( def __init__(
self, self,
view: pg.ViewBox, view: pg.ViewBox,
fmt_str: str, fmt_str: str,
@ -63,6 +61,7 @@ class Label:
font_size: str = 'small', font_size: str = 'small',
opacity: float = 1, opacity: float = 1,
fields: dict = {}, fields: dict = {},
parent: pg.GraphicsObject = None,
update_on_range_change: bool = True, update_on_range_change: bool = True,
) -> None: ) -> None:
@ -71,11 +70,13 @@ class Label:
self._fmt_str = fmt_str self._fmt_str = fmt_str
self._view_xy = QPointF(0, 0) self._view_xy = QPointF(0, 0)
self.scene_anchor: Optional[Callable[..., QPointF]] = None self.scene_anchor: Optional[
Callable[..., QPointF]
] = None
self._x_offset = x_offset self._x_offset = x_offset
txt = self.txt = QtWidgets.QGraphicsTextItem() txt = self.txt = QtWidgets.QGraphicsTextItem(parent=parent)
txt.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) txt.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
vb.scene().addItem(txt) vb.scene().addItem(txt)
@ -86,7 +87,6 @@ class Label:
) )
dpi_font.configure_to_dpi() dpi_font.configure_to_dpi()
txt.setFont(dpi_font.font) txt.setFont(dpi_font.font)
txt.setOpacity(opacity) txt.setOpacity(opacity)
# register viewbox callbacks # register viewbox callbacks
@ -109,7 +109,7 @@ class Label:
# self.setTextInteractionFlags(QtGui.Qt.TextEditorInteraction) # self.setTextInteractionFlags(QtGui.Qt.TextEditorInteraction)
@property @property
def color(self): def color(self) -> str:
return self._hcolor return self._hcolor
@color.setter @color.setter
@ -118,9 +118,10 @@ class Label:
self._hcolor = color self._hcolor = color
def update(self) -> None: def update(self) -> None:
'''Update this label either by invoking its '''
user defined anchoring function, or by positioning Update this label either by invoking its user defined anchoring
to the last recorded data view coordinates. function, or by positioning to the last recorded data view
coordinates.
''' '''
# move label in scene coords to desired position # move label in scene coords to desired position
@ -234,7 +235,8 @@ class Label:
class FormatLabel(QLabel): class FormatLabel(QLabel):
'''Kinda similar to above but using the widget apis. '''
Kinda similar to above but using the widget apis.
''' '''
def __init__( def __init__(
@ -273,8 +275,8 @@ class FormatLabel(QLabel):
QSizePolicy.Expanding, QSizePolicy.Expanding,
QSizePolicy.Expanding, QSizePolicy.Expanding,
) )
self.setAlignment(Qt.AlignVCenter self.setAlignment(
| Qt.AlignLeft Qt.AlignVCenter | Qt.AlignLeft
) )
self.setText(self.fmt_str) self.setText(self.fmt_str)

View File

@ -334,10 +334,11 @@ class LevelLine(pg.InfiniteLine):
w: QtWidgets.QWidget w: QtWidgets.QWidget
) -> None: ) -> None:
"""Core paint which we override (yet again) '''
Core paint which we override (yet again)
from pg.. from pg..
""" '''
p.setRenderHint(p.Antialiasing) p.setRenderHint(p.Antialiasing)
# these are in viewbox coords # these are in viewbox coords