Add "contents" labels to charts
Add a default "contents label" (eg. OHLC values for bar charts) to each chart and update on crosshair interaction. Few technical changes to make this happen: - adjust bar graphics to have the HL line be in the "middle" of the underlying arrays' "index range" in the containing view. - add a label dict each chart's graphics name to a label + update routine - use symbol names instead of this "main" identifier crap for referring to particular price curves/graphicsbar_select
parent
61e460a422
commit
f46fa99a6e
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
High level Qt chart widgets.
|
||||
"""
|
||||
from typing import Optional, Tuple, Dict, Any
|
||||
from typing import Tuple, Dict, Any
|
||||
import time
|
||||
|
||||
from PyQt5 import QtCore, QtGui
|
||||
|
@ -172,14 +172,15 @@ class LinkedSplitCharts(QtGui.QWidget):
|
|||
|
||||
# add crosshairs
|
||||
self._ch = CrossHair(
|
||||
parent=self,
|
||||
linkedsplitcharts=self,
|
||||
digits=self.digits
|
||||
)
|
||||
self.chart = self.add_plot(
|
||||
name='main',
|
||||
name=symbol.key,
|
||||
array=array,
|
||||
xaxis=self.xaxis,
|
||||
ohlc=True,
|
||||
_is_main=True,
|
||||
)
|
||||
# add crosshair graphic
|
||||
self.chart.addItem(self._ch)
|
||||
|
@ -195,12 +196,13 @@ class LinkedSplitCharts(QtGui.QWidget):
|
|||
array: np.ndarray,
|
||||
xaxis: DynamicDateAxis = None,
|
||||
ohlc: bool = False,
|
||||
_is_main: bool = False,
|
||||
) -> 'ChartPlotWidget':
|
||||
"""Add (sub)plots to chart widget by name.
|
||||
|
||||
If ``name`` == ``"main"`` the chart will be the the primary view.
|
||||
"""
|
||||
if self.chart is None and name != 'main':
|
||||
if self.chart is None and not _is_main:
|
||||
raise RuntimeError(
|
||||
"A main plot must be created first with `.plot_main()`")
|
||||
|
||||
|
@ -230,14 +232,14 @@ class LinkedSplitCharts(QtGui.QWidget):
|
|||
|
||||
# draw curve graphics
|
||||
if ohlc:
|
||||
cpw.draw_ohlc(array)
|
||||
cpw.draw_ohlc(name, array)
|
||||
else:
|
||||
cpw.draw_curve(array)
|
||||
cpw.draw_curve(name, array)
|
||||
|
||||
# add to cross-hair's known plots
|
||||
self._ch.add_plot(cpw)
|
||||
|
||||
if name != "main":
|
||||
if not _is_main:
|
||||
# track by name
|
||||
self.subplots[name] = cpw
|
||||
|
||||
|
@ -279,13 +281,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
super().__init__(**kwargs)
|
||||
self._array = array # readonly view of data
|
||||
self._graphics = {} # registry of underlying graphics
|
||||
|
||||
# XXX: label setting doesn't seem to work?
|
||||
# likely custom graphics need special handling
|
||||
# label = pg.LabelItem(justify='right')
|
||||
# self.addItem(label)
|
||||
# label.setText("Yo yoyo")
|
||||
# label.setText("<span style='font-size: 12pt'>x=")
|
||||
self._labels = {} # registry of underlying graphics
|
||||
|
||||
# show only right side axes
|
||||
self.hideAxis('left')
|
||||
|
@ -303,6 +299,11 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# based on ohlc contents
|
||||
self.sigXRangeChanged.connect(self._set_yrange)
|
||||
|
||||
def _update_contents_label(self, index: int) -> None:
|
||||
if index > 0 and index < len(self._array):
|
||||
for name, (label, update) in self._labels.items():
|
||||
update(index)
|
||||
|
||||
def _set_xlimits(
|
||||
self,
|
||||
xfirst: int,
|
||||
|
@ -331,6 +332,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
def draw_ohlc(
|
||||
self,
|
||||
name: str,
|
||||
data: np.ndarray,
|
||||
# XXX: pretty sure this is dumb and we don't need an Enum
|
||||
style: pg.GraphicsObject = BarItems,
|
||||
|
@ -345,7 +347,25 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
graphics.draw_from_data(data)
|
||||
self.addItem(graphics)
|
||||
|
||||
self._graphics['ohlc_main'] = graphics
|
||||
self._graphics[name] = graphics
|
||||
|
||||
# XXX: How to stack labels vertically?
|
||||
label = pg.LabelItem(
|
||||
justify='left',
|
||||
size='5pt',
|
||||
)
|
||||
self.scene().addItem(label)
|
||||
|
||||
def update(index: int) -> None:
|
||||
label.setText(
|
||||
"{name} O:{} H:{} L:{} C:{} V:{}".format(
|
||||
*self._array[index].item()[2:],
|
||||
name=name,
|
||||
)
|
||||
)
|
||||
|
||||
self._labels[name] = (label, update)
|
||||
self._update_contents_label(index=-1)
|
||||
|
||||
# set xrange limits
|
||||
xlast = data[-1]['index']
|
||||
|
@ -357,8 +377,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
def draw_curve(
|
||||
self,
|
||||
name: str,
|
||||
data: np.ndarray,
|
||||
name: Optional[str] = 'line_main',
|
||||
) -> pg.PlotDataItem:
|
||||
# draw the indicator as a plain curve
|
||||
curve = pg.PlotDataItem(
|
||||
|
@ -371,10 +391,24 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
# register overlay curve with name
|
||||
if not self._graphics and name is None:
|
||||
name = 'line_main'
|
||||
name = 'a_line_bby'
|
||||
|
||||
self._graphics[name] = curve
|
||||
|
||||
# XXX: How to stack labels vertically?
|
||||
label = pg.LabelItem(
|
||||
justify='left',
|
||||
size='5pt',
|
||||
)
|
||||
self.scene().addItem(label)
|
||||
|
||||
def update(index: int) -> None:
|
||||
data = self._array[index]
|
||||
label.setText(f"{name}: {index} {data}")
|
||||
|
||||
self._labels[name] = (label, update)
|
||||
self._update_contents_label(index=-1)
|
||||
|
||||
# set a "startup view"
|
||||
xlast = len(data) - 1
|
||||
|
||||
|
@ -531,29 +565,28 @@ async def add_new_bars(delay_s, linked_charts):
|
|||
)
|
||||
|
||||
# read value at "open" of bar
|
||||
last_quote = ohlc[-1]
|
||||
# last_quote = ohlc[-1]
|
||||
# XXX: If the last bar has not changed print a flat line and
|
||||
# move to the next. This is a "animation" choice that we may not
|
||||
# keep.
|
||||
# if last_quote == ohlc[-1]:
|
||||
# log.debug("Printing flat line for {sym}")
|
||||
|
||||
# update chart graphics and resize view
|
||||
price_chart.update_from_array(price_chart.name, ohlc)
|
||||
price_chart._set_yrange()
|
||||
|
||||
for name, chart in linked_charts.subplots.items():
|
||||
chart.update_from_array(chart.name, chart._array)
|
||||
chart._set_yrange()
|
||||
|
||||
# We **don't** update the bar right now
|
||||
# since the next quote that arrives should in the
|
||||
# tick streaming task
|
||||
await sleep()
|
||||
|
||||
# XXX: If the last bar has not changed print a flat line and
|
||||
# move to the next. This is a "animation" choice that we may not
|
||||
# keep.
|
||||
if last_quote == ohlc[-1]:
|
||||
log.debug("Printing flat line for {sym}")
|
||||
price_chart.update_from_array('ohlc_main', ohlc)
|
||||
|
||||
# resize view
|
||||
price_chart._set_yrange()
|
||||
|
||||
|
||||
for name, chart in linked_charts.subplots.items():
|
||||
chart.update_from_array('line_main', chart._array)
|
||||
|
||||
# resize view
|
||||
chart._set_yrange()
|
||||
# TODO: should we update a graphics again time here?
|
||||
# Think about race conditions with data update task.
|
||||
|
||||
|
||||
async def _async_main(
|
||||
|
@ -639,9 +672,10 @@ async def _async_main(
|
|||
last,
|
||||
)
|
||||
chart.update_from_array(
|
||||
'ohlc_main',
|
||||
chart.name,
|
||||
chart._array,
|
||||
)
|
||||
chart._set_yrange()
|
||||
|
||||
|
||||
async def chart_from_fsp(
|
||||
|
@ -676,7 +710,7 @@ async def chart_from_fsp(
|
|||
stream = await portal.result()
|
||||
|
||||
# receive processed historical data-array as first message
|
||||
history = (await stream.__anext__())
|
||||
history: np.ndarray = (await stream.__anext__())
|
||||
|
||||
# TODO: enforce type checking here
|
||||
newbars = np.array(history)
|
||||
|
@ -686,10 +720,19 @@ async def chart_from_fsp(
|
|||
array=newbars,
|
||||
)
|
||||
|
||||
# update sub-plot graphics
|
||||
# check for data length mis-allignment and fill missing values
|
||||
diff = len(chart._array) - len(linked_charts.chart._array)
|
||||
if diff < 0:
|
||||
data = chart._array
|
||||
chart._array = np.append(
|
||||
data,
|
||||
np.full(abs(diff), data[-1], dtype=data.dtype)
|
||||
)
|
||||
|
||||
# update chart graphics
|
||||
async for value in stream:
|
||||
chart._array[-1] = value
|
||||
chart.update_from_array('line_main', chart._array)
|
||||
chart.update_from_array(chart.name, chart._array)
|
||||
chart._set_yrange()
|
||||
|
||||
|
||||
|
@ -703,7 +746,7 @@ def _main(
|
|||
# Qt entry point
|
||||
run_qtractor(
|
||||
func=_async_main,
|
||||
args=(sym, brokername),
|
||||
main_widget=ChartSpace,
|
||||
tractor_kwargs=tractor_kwargs,
|
||||
args=(sym, brokername),
|
||||
main_widget=ChartSpace,
|
||||
tractor_kwargs=tractor_kwargs,
|
||||
)
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
Chart graphics for displaying a slew of different data types.
|
||||
"""
|
||||
from typing import List
|
||||
from enum import Enum
|
||||
from itertools import chain
|
||||
|
||||
import numpy as np
|
||||
|
@ -23,10 +22,14 @@ _mouse_rate_limit = 30
|
|||
|
||||
class CrossHair(pg.GraphicsObject):
|
||||
|
||||
def __init__(self, parent, digits: int = 0):
|
||||
def __init__(
|
||||
self,
|
||||
linkedsplitcharts: 'LinkedSplitCharts',
|
||||
digits: int = 0
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.pen = pg.mkPen('#a9a9a9') # gray?
|
||||
self.parent = parent
|
||||
self.lsc = linkedsplitcharts
|
||||
self.graphics = {}
|
||||
self.plots = []
|
||||
self.active_plot = None
|
||||
|
@ -110,18 +113,27 @@ class CrossHair(pg.GraphicsObject):
|
|||
# mouse was not on active plot
|
||||
return
|
||||
|
||||
self.graphics[self.active_plot]['hl'].setY(
|
||||
mouse_point.y()
|
||||
)
|
||||
x, y = mouse_point.x(), mouse_point.y()
|
||||
|
||||
plot = self.active_plot
|
||||
|
||||
self.graphics[plot]['hl'].setY(y)
|
||||
|
||||
self.graphics[self.active_plot]['yl'].update_label(
|
||||
evt_post=pos, point_view=mouse_point
|
||||
)
|
||||
# move the vertical line to the current x coordinate in all charts
|
||||
for opts in self.graphics.values():
|
||||
opts['vl'].setX(mouse_point.x())
|
||||
for plot, opts in self.graphics.items():
|
||||
# move the vertical line to the current x
|
||||
opts['vl'].setX(x)
|
||||
|
||||
# update the chart's "contents" label
|
||||
plot._update_contents_label(int(x))
|
||||
|
||||
# update the label on the bottom of the crosshair
|
||||
self.xaxis_label.update_label(evt_post=pos, point_view=mouse_point)
|
||||
self.xaxis_label.update_label(
|
||||
evt_post=pos,
|
||||
point_view=mouse_point
|
||||
)
|
||||
|
||||
def boundingRect(self):
|
||||
try:
|
||||
|
@ -149,8 +161,8 @@ def _mk_lines_array(data: List, size: int) -> np.ndarray:
|
|||
|
||||
def bars_from_ohlc(
|
||||
data: np.ndarray,
|
||||
w: float,
|
||||
start: int = 0,
|
||||
w: float = 0.43,
|
||||
) -> np.ndarray:
|
||||
"""Generate an array of lines objects from input ohlc data.
|
||||
"""
|
||||
|
@ -160,9 +172,15 @@ def bars_from_ohlc(
|
|||
open, high, low, close, index = q[
|
||||
['open', 'high', 'low', 'close', 'index']]
|
||||
|
||||
# place the x-coord start as "middle" of the drawing range such
|
||||
# that the open arm line-graphic is at the left-most-side of
|
||||
# the indexe's range according to the view mapping.
|
||||
index_start = index + w
|
||||
|
||||
# high - low line
|
||||
if low != high:
|
||||
hl = QLineF(index, low, index, high)
|
||||
# hl = QLineF(index, low, index, high)
|
||||
hl = QLineF(index_start, low, index_start, high)
|
||||
else:
|
||||
# XXX: if we don't do it renders a weird rectangle?
|
||||
# see below too for handling this later...
|
||||
|
@ -170,9 +188,9 @@ def bars_from_ohlc(
|
|||
hl._flat = True
|
||||
|
||||
# open line
|
||||
o = QLineF(index - w, open, index, open)
|
||||
o = QLineF(index_start - w, open, index_start, open)
|
||||
# close line
|
||||
c = QLineF(index + w, close, index, close)
|
||||
c = QLineF(index_start + w, close, index_start, close)
|
||||
|
||||
# indexing here is as per the below comments
|
||||
# lines[3*i:3*i+3] = (hl, o, c)
|
||||
|
@ -207,7 +225,7 @@ class BarItems(pg.GraphicsObject):
|
|||
sigPlotChanged = QtCore.Signal(object)
|
||||
|
||||
# 0.5 is no overlap between arms, 1.0 is full overlap
|
||||
w: float = 0.4
|
||||
w: float = 0.43
|
||||
bull_pen = pg.mkPen('#808080')
|
||||
|
||||
# XXX: tina mode, see below
|
||||
|
@ -234,7 +252,7 @@ class BarItems(pg.GraphicsObject):
|
|||
):
|
||||
"""Draw OHLC datum graphics from a ``np.recarray``.
|
||||
"""
|
||||
lines = bars_from_ohlc(data, start=start)
|
||||
lines = bars_from_ohlc(data, self.w, start=start)
|
||||
|
||||
# save graphics for later reference and keep track
|
||||
# of current internal "last index"
|
||||
|
@ -280,7 +298,7 @@ class BarItems(pg.GraphicsObject):
|
|||
if extra > 0:
|
||||
# generate new graphics to match provided array
|
||||
new = array[index:index + extra]
|
||||
lines = bars_from_ohlc(new)
|
||||
lines = bars_from_ohlc(new, self.w)
|
||||
bars_added = len(lines)
|
||||
self.lines[index:index + bars_added] = lines
|
||||
self.index += bars_added
|
||||
|
@ -311,7 +329,7 @@ class BarItems(pg.GraphicsObject):
|
|||
# if the bar was flat it likely does not have
|
||||
# the index set correctly due to a rendering bug
|
||||
# see above
|
||||
body.setLine(i, low, i, high)
|
||||
body.setLine(i + self.w, low, i + self.w, high)
|
||||
body._flat = False
|
||||
else:
|
||||
body.setLine(body.x1(), low, body.x2(), high)
|
||||
|
|
Loading…
Reference in New Issue