Add "follow mode"
Makes the chart act like tws where each new time step increment the chart shifts to the right so that the last bar stays in place. This gets things looking like a proper auto-trading UX. Added a couple methods to ``ChartPlotWidget`` to make this work: - ``.default_view()`` to set the preferred view based on user settings - ``.increment_view()`` to shift the view one time frame right Also, split up the `.update_from_array()` method to be curve/ohlc specific allowing for passing in a struct array with a named field containing curve data more straightforwardly. This also simplifies the contest label update functions.bar_select
parent
1902507703
commit
88583d999a
|
@ -253,7 +253,7 @@ class YSticky(YAxisLabel):
|
||||||
else:
|
else:
|
||||||
# non-ohlc case
|
# non-ohlc case
|
||||||
index = len(a) - 1
|
index = len(a) - 1
|
||||||
last = a[-1]
|
last = a[chart.name][-1]
|
||||||
self.update_from_data(
|
self.update_from_data(
|
||||||
index,
|
index,
|
||||||
last,
|
last,
|
||||||
|
|
|
@ -16,8 +16,10 @@ from ._axes import (
|
||||||
from ._graphics import CrossHair, BarItems, h_line
|
from ._graphics import CrossHair, BarItems, h_line
|
||||||
from ._axes import YSticky
|
from ._axes import YSticky
|
||||||
from ._style import (
|
from ._style import (
|
||||||
_xaxis_at, _min_points_to_show, hcolor,
|
hcolor,
|
||||||
CHART_MARGINS,
|
CHART_MARGINS,
|
||||||
|
_xaxis_at,
|
||||||
|
_min_points_to_show,
|
||||||
_bars_from_right_in_follow_mode,
|
_bars_from_right_in_follow_mode,
|
||||||
_bars_to_left_in_follow_mode,
|
_bars_to_left_in_follow_mode,
|
||||||
# _font,
|
# _font,
|
||||||
|
@ -155,7 +157,7 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
|
|
||||||
def set_split_sizes(
|
def set_split_sizes(
|
||||||
self,
|
self,
|
||||||
prop: float = 0.25 # proportion allocated to consumer subcharts
|
prop: float = 0.28 # proportion allocated to consumer subcharts
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set the proportion of space allocated for linked subcharts.
|
"""Set the proportion of space allocated for linked subcharts.
|
||||||
"""
|
"""
|
||||||
|
@ -306,6 +308,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
self._ysticks = {} # registry of underlying graphics
|
self._ysticks = {} # registry of underlying graphics
|
||||||
self._vb = self.plotItem.vb
|
self._vb = self.plotItem.vb
|
||||||
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'
|
||||||
|
|
||||||
# show only right side axes
|
# show only right side axes
|
||||||
self.hideAxis('left')
|
self.hideAxis('left')
|
||||||
|
@ -314,9 +317,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# show background grid
|
# show background grid
|
||||||
self.showGrid(x=True, y=True, alpha=0.4)
|
self.showGrid(x=True, y=True, alpha=0.4)
|
||||||
|
|
||||||
# don't need right?
|
|
||||||
# self._vb.setXRange(0, 0)
|
|
||||||
|
|
||||||
# use cross-hair for cursor?
|
# use cross-hair for cursor?
|
||||||
# self.setCursor(QtCore.Qt.CrossCursor)
|
# self.setCursor(QtCore.Qt.CrossCursor)
|
||||||
|
|
||||||
|
@ -330,6 +330,9 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# for when the splitter(s) are resized
|
# for when the splitter(s) are resized
|
||||||
self._vb.sigResized.connect(self._set_yrange)
|
self._vb.sigResized.connect(self._set_yrange)
|
||||||
|
|
||||||
|
def last_bar_in_view(self) -> bool:
|
||||||
|
self._array[-1]['index']
|
||||||
|
|
||||||
def _update_contents_label(self, index: int) -> None:
|
def _update_contents_label(self, index: int) -> None:
|
||||||
if index >= 0 and index < len(self._array):
|
if index >= 0 and index < len(self._array):
|
||||||
for name, (label, update) in self._labels.items():
|
for name, (label, update) in self._labels.items():
|
||||||
|
@ -356,6 +359,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
def bars_range(self) -> Tuple[int, int, int, int]:
|
def bars_range(self) -> Tuple[int, int, int, int]:
|
||||||
"""Return a range tuple for the bars present in view.
|
"""Return a range tuple for the bars present in view.
|
||||||
"""
|
"""
|
||||||
|
# vr = self.viewRect()
|
||||||
|
# l, r = int(vr.left()), int(vr.right())
|
||||||
l, r = self.view_range()
|
l, r = self.view_range()
|
||||||
lbar = max(l, 0)
|
lbar = max(l, 0)
|
||||||
rbar = min(r, len(self._array))
|
rbar = min(r, len(self._array))
|
||||||
|
@ -405,19 +410,43 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
self._update_contents_label(len(data) - 1)
|
self._update_contents_label(len(data) - 1)
|
||||||
label.show()
|
label.show()
|
||||||
|
|
||||||
# set xrange limits
|
|
||||||
xlast = data[-1]['index']
|
|
||||||
|
|
||||||
# show last 50 points on startup
|
|
||||||
self.plotItem.vb.setXRange(
|
|
||||||
xlast - _bars_to_left_in_follow_mode,
|
|
||||||
xlast + _bars_from_right_in_follow_mode
|
|
||||||
)
|
|
||||||
|
|
||||||
self._add_sticky(name)
|
self._add_sticky(name)
|
||||||
|
|
||||||
return graphics
|
return graphics
|
||||||
|
|
||||||
|
def default_view(
|
||||||
|
self,
|
||||||
|
index: int = -1,
|
||||||
|
) -> None:
|
||||||
|
"""Set the view box to the "default" startup view of the scene.
|
||||||
|
|
||||||
|
"""
|
||||||
|
xlast = self._array[index]['index']
|
||||||
|
begin = xlast - _bars_to_left_in_follow_mode
|
||||||
|
end = xlast + _bars_from_right_in_follow_mode
|
||||||
|
|
||||||
|
self.plotItem.vb.setXRange(
|
||||||
|
min=begin,
|
||||||
|
max=end,
|
||||||
|
padding=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
def increment_view(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
"""Increment the data view one step to the right thus "following"
|
||||||
|
the current time slot/step/bar.
|
||||||
|
|
||||||
|
"""
|
||||||
|
l, r = self.view_range()
|
||||||
|
self._vb.setXRange(
|
||||||
|
min=l + 1,
|
||||||
|
max=r + 1,
|
||||||
|
# holy shit, wtf dude... why tf would this not be 0 by
|
||||||
|
# default... speechless.
|
||||||
|
padding=0,
|
||||||
|
)
|
||||||
|
|
||||||
def draw_curve(
|
def draw_curve(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
|
@ -432,7 +461,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
pdi_kwargs.update(_pdi_defaults)
|
pdi_kwargs.update(_pdi_defaults)
|
||||||
|
|
||||||
curve = pg.PlotDataItem(
|
curve = pg.PlotDataItem(
|
||||||
data,
|
data[name],
|
||||||
# antialias=True,
|
# antialias=True,
|
||||||
name=name,
|
name=name,
|
||||||
# TODO: see how this handles with custom ohlcv bars graphics
|
# TODO: see how this handles with custom ohlcv bars graphics
|
||||||
|
@ -459,39 +488,19 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
label.anchor(itemPos=(1, 1), parentPos=(1, 1), offset=(0, 3))
|
label.anchor(itemPos=(1, 1), parentPos=(1, 1), offset=(0, 3))
|
||||||
self._overlays[name] = curve
|
self._overlays[name] = curve
|
||||||
|
|
||||||
def update(index: int) -> None:
|
|
||||||
data = self._array[index][name]
|
|
||||||
label.setText(f"{name}: {data:.2f}")
|
|
||||||
else:
|
else:
|
||||||
label.anchor(itemPos=(0, 0), parentPos=(0, 0), offset=(0, -4))
|
label.anchor(itemPos=(0, 0), parentPos=(0, 0), offset=(0, -4))
|
||||||
|
|
||||||
def update(index: int) -> None:
|
def update(index: int) -> None:
|
||||||
data = self._array[index]
|
data = self._array[index][name]
|
||||||
label.setText(f"{name}: {data:.2f}")
|
label.setText(f"{name}: {data:.2f}")
|
||||||
|
|
||||||
# def update(index: int) -> None:
|
|
||||||
# data = self._array[index]
|
|
||||||
# label.setText(f"{name} -> {data:.2f}")
|
|
||||||
|
|
||||||
label.show()
|
label.show()
|
||||||
self.scene().addItem(label)
|
self.scene().addItem(label)
|
||||||
|
|
||||||
self._labels[name] = (label, update)
|
self._labels[name] = (label, update)
|
||||||
self._update_contents_label(len(data) - 1)
|
self._update_contents_label(len(data) - 1)
|
||||||
|
|
||||||
# set a "startup view"
|
|
||||||
xlast = len(data) - 1
|
|
||||||
|
|
||||||
# configure "follow mode" view on startup
|
|
||||||
self.plotItem.vb.setXRange(
|
|
||||||
xlast - _bars_to_left_in_follow_mode,
|
|
||||||
xlast + _bars_from_right_in_follow_mode
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: we should instead implement a diff based
|
|
||||||
# "only update with new items" on the pg.PlotDataItem
|
|
||||||
curve.update_from_array = curve.setData
|
|
||||||
|
|
||||||
self._add_sticky(name)
|
self._add_sticky(name)
|
||||||
|
|
||||||
return curve
|
return curve
|
||||||
|
@ -511,20 +520,40 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
)
|
)
|
||||||
return last
|
return last
|
||||||
|
|
||||||
def update_from_array(
|
def update_ohlc_from_array(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
array: np.ndarray,
|
array: np.ndarray,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> pg.GraphicsObject:
|
) -> pg.GraphicsObject:
|
||||||
|
"""Update the named internal graphics from ``array``.
|
||||||
|
|
||||||
|
"""
|
||||||
if name not in self._overlays:
|
if name not in self._overlays:
|
||||||
self._array = array
|
self._array = array
|
||||||
|
|
||||||
graphics = self._graphics[name]
|
graphics = self._graphics[name]
|
||||||
graphics.update_from_array(array, **kwargs)
|
graphics.update_from_array(array, **kwargs)
|
||||||
|
|
||||||
return graphics
|
return graphics
|
||||||
|
|
||||||
|
def update_curve_from_array(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
array: np.ndarray,
|
||||||
|
**kwargs,
|
||||||
|
) -> pg.GraphicsObject:
|
||||||
|
"""Update the named internal graphics from ``array``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if name not in self._overlays:
|
||||||
|
self._array = array
|
||||||
|
|
||||||
|
curve = self._graphics[name]
|
||||||
|
# TODO: we should instead implement a diff based
|
||||||
|
# "only update with new items" on the pg.PlotDataItem
|
||||||
|
curve.setData(array[name], **kwargs)
|
||||||
|
return curve
|
||||||
|
|
||||||
def _set_yrange(
|
def _set_yrange(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
@ -535,12 +564,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
This adds auto-scaling like zoom on the scroll wheel such
|
This adds auto-scaling like zoom on the scroll wheel such
|
||||||
that data always fits nicely inside the current view of the
|
that data always fits nicely inside the current view of the
|
||||||
data set.
|
data set.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# yrange
|
|
||||||
# if self._static_yrange is not None:
|
|
||||||
# yrange = self._static_yrange
|
|
||||||
|
|
||||||
if self._static_yrange is not None:
|
if self._static_yrange is not None:
|
||||||
ylow, yhigh = self._static_yrange
|
ylow, yhigh = self._static_yrange
|
||||||
|
|
||||||
|
@ -552,6 +577,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# the data set up to the point where ``_min_points_to_show``
|
# the data set up to the point where ``_min_points_to_show``
|
||||||
# are left.
|
# are left.
|
||||||
view_len = r - l
|
view_len = r - l
|
||||||
|
|
||||||
# TODO: logic to check if end of bars in view
|
# TODO: logic to check if end of bars in view
|
||||||
extra = view_len - _min_points_to_show
|
extra = view_len - _min_points_to_show
|
||||||
begin = 0 - extra
|
begin = 0 - extra
|
||||||
|
@ -673,7 +699,7 @@ async def _async_main(
|
||||||
vwap_in_history = True
|
vwap_in_history = True
|
||||||
chart.draw_curve(
|
chart.draw_curve(
|
||||||
name='vwap',
|
name='vwap',
|
||||||
data=bars['vwap'],
|
data=bars,
|
||||||
overlay=True,
|
overlay=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -751,6 +777,8 @@ async def chart_from_quotes(
|
||||||
|
|
||||||
last_bars_range, last_mx, last_mn = maxmin()
|
last_bars_range, last_mx, last_mn = maxmin()
|
||||||
|
|
||||||
|
chart.default_view()
|
||||||
|
|
||||||
async for quotes in stream:
|
async for quotes in stream:
|
||||||
for sym, quote in quotes.items():
|
for sym, quote in quotes.items():
|
||||||
for tick in iterticks(quote, type='trade'):
|
for tick in iterticks(quote, type='trade'):
|
||||||
|
@ -761,7 +789,7 @@ async def chart_from_quotes(
|
||||||
last_price_sticky.update_from_data(*last[['index', 'close']])
|
last_price_sticky.update_from_data(*last[['index', 'close']])
|
||||||
|
|
||||||
# update price bar
|
# update price bar
|
||||||
chart.update_from_array(
|
chart.update_ohlc_from_array(
|
||||||
chart.name,
|
chart.name,
|
||||||
array,
|
array,
|
||||||
)
|
)
|
||||||
|
@ -778,7 +806,7 @@ async def chart_from_quotes(
|
||||||
|
|
||||||
if vwap_in_history:
|
if vwap_in_history:
|
||||||
# update vwap overlay line
|
# update vwap overlay line
|
||||||
chart.update_from_array('vwap', ohlcv.array['vwap'])
|
chart.update_curve_from_array('vwap', ohlcv.array)
|
||||||
|
|
||||||
# TODO:
|
# TODO:
|
||||||
# - eventually we'll want to update bid/ask labels and
|
# - eventually we'll want to update bid/ask labels and
|
||||||
|
@ -847,7 +875,10 @@ async def chart_from_fsp(
|
||||||
|
|
||||||
chart = linked_charts.add_plot(
|
chart = linked_charts.add_plot(
|
||||||
name=fsp_func_name,
|
name=fsp_func_name,
|
||||||
array=shm.array[fsp_func_name],
|
array=shm.array,
|
||||||
|
|
||||||
|
# curve by default
|
||||||
|
ohlc=False,
|
||||||
|
|
||||||
# settings passed down to ``ChartPlotWidget``
|
# settings passed down to ``ChartPlotWidget``
|
||||||
static_yrange=(0, 100),
|
static_yrange=(0, 100),
|
||||||
|
@ -862,7 +893,8 @@ async def chart_from_fsp(
|
||||||
last_val_sticky = chart._ysticks[chart.name]
|
last_val_sticky = chart._ysticks[chart.name]
|
||||||
last_val_sticky.update_from_data(-1, value)
|
last_val_sticky.update_from_data(-1, value)
|
||||||
|
|
||||||
chart.update_from_array(chart.name, array[fsp_func_name])
|
chart.update_curve_from_array(fsp_func_name, array)
|
||||||
|
chart.default_view()
|
||||||
|
|
||||||
# TODO: figure out if we can roll our own `FillToThreshold` to
|
# TODO: figure out if we can roll our own `FillToThreshold` to
|
||||||
# get brush filled polygons for OS/OB conditions.
|
# get brush filled polygons for OS/OB conditions.
|
||||||
|
@ -887,7 +919,7 @@ async def chart_from_fsp(
|
||||||
array = shm.array
|
array = shm.array
|
||||||
value = array[-1][fsp_func_name]
|
value = array[-1][fsp_func_name]
|
||||||
last_val_sticky.update_from_data(-1, value)
|
last_val_sticky.update_from_data(-1, value)
|
||||||
chart.update_from_array(chart.name, array[fsp_func_name])
|
chart.update_curve_from_array(fsp_func_name, array)
|
||||||
# p('rendered rsi datum')
|
# p('rendered rsi datum')
|
||||||
|
|
||||||
|
|
||||||
|
@ -901,11 +933,12 @@ async def check_for_new_bars(feed, ohlcv, linked_charts):
|
||||||
# aware of the instrument's tradable hours?
|
# aware of the instrument's tradable hours?
|
||||||
|
|
||||||
price_chart = linked_charts.chart
|
price_chart = linked_charts.chart
|
||||||
|
price_chart.default_view()
|
||||||
|
|
||||||
async for index in await feed.index_stream():
|
async for index in await feed.index_stream():
|
||||||
|
|
||||||
# update chart historical bars graphics
|
# update chart historical bars graphics
|
||||||
price_chart.update_from_array(
|
price_chart.update_ohlc_from_array(
|
||||||
price_chart.name,
|
price_chart.name,
|
||||||
ohlcv.array,
|
ohlcv.array,
|
||||||
# When appending a new bar, in the time between the insert
|
# When appending a new bar, in the time between the insert
|
||||||
|
@ -922,15 +955,17 @@ async def check_for_new_bars(feed, ohlcv, linked_charts):
|
||||||
# TODO: standard api for signal lookups per plot
|
# TODO: standard api for signal lookups per plot
|
||||||
if name in price_chart._array.dtype.fields:
|
if name in price_chart._array.dtype.fields:
|
||||||
# should have already been incremented above
|
# should have already been incremented above
|
||||||
price_chart.update_from_array(
|
price_chart.update_curve_from_array(
|
||||||
name,
|
name,
|
||||||
price_chart._array[name],
|
price_chart._array,
|
||||||
)
|
)
|
||||||
|
|
||||||
for name, chart in linked_charts.subplots.items():
|
for name, chart in linked_charts.subplots.items():
|
||||||
chart.update_from_array(chart.name, chart._shm.array[chart.name])
|
chart.update_curve_from_array(chart.name, chart._shm.array)
|
||||||
# chart._set_yrange()
|
# chart._set_yrange()
|
||||||
|
|
||||||
|
price_chart.increment_view()
|
||||||
|
|
||||||
|
|
||||||
def _main(
|
def _main(
|
||||||
sym: str,
|
sym: str,
|
||||||
|
|
|
@ -404,7 +404,7 @@ class BarItems(pg.GraphicsObject):
|
||||||
# @timeit
|
# @timeit
|
||||||
def paint(self, p, opt, widget):
|
def paint(self, p, opt, widget):
|
||||||
|
|
||||||
profiler = pg.debug.Profiler(disabled=False, delayed=False)
|
# profiler = pg.debug.Profiler(disabled=False, delayed=False)
|
||||||
|
|
||||||
# TODO: use to avoid drawing artefacts?
|
# TODO: use to avoid drawing artefacts?
|
||||||
# self.prepareGeometryChange()
|
# self.prepareGeometryChange()
|
||||||
|
@ -425,7 +425,7 @@ class BarItems(pg.GraphicsObject):
|
||||||
# self._pmi.setPixmap(self.picture)
|
# self._pmi.setPixmap(self.picture)
|
||||||
# print(self.scene())
|
# print(self.scene())
|
||||||
|
|
||||||
profiler('bars redraw:')
|
# profiler('bars redraw:')
|
||||||
|
|
||||||
def boundingRect(self):
|
def boundingRect(self):
|
||||||
# TODO: can we do rect caching to make this faster?
|
# TODO: can we do rect caching to make this faster?
|
||||||
|
@ -482,7 +482,9 @@ class BarItems(pg.GraphicsObject):
|
||||||
|
|
||||||
|
|
||||||
def h_line(level: float) -> pg.InfiniteLine:
|
def h_line(level: float) -> pg.InfiniteLine:
|
||||||
|
"""Convenience routine to add a styled horizontal line to a plot.
|
||||||
|
|
||||||
|
"""
|
||||||
line = pg.InfiniteLine(
|
line = pg.InfiniteLine(
|
||||||
movable=True,
|
movable=True,
|
||||||
angle=0,
|
angle=0,
|
||||||
|
|
Loading…
Reference in New Issue