Cleanup yrange auto-update callback

This was a mess before with a weird loop using the parent split charts
to update all "indicators". Instead just have each plot do its own
yrange updates since the signals are being handled just fine per plot.
Handle both the OHLC and plane line chart cases with a hacky `try:,
except IndexError:` for now.

Oh, and move the main entry point for the chart app to the relevant
module. I added some WIP bar update code for the moment.
its_happening
Tyler Goodlet 2020-06-17 11:45:43 -04:00
parent 970a528264
commit 56f65bcd40
2 changed files with 129 additions and 69 deletions

View File

@ -1,11 +1,9 @@
""" """
Console interface to UI components. Console interface to UI components.
""" """
from datetime import datetime
from functools import partial from functools import partial
import os import os
import click import click
import trio
import tractor import tractor
from ..cli import cli from ..cli import cli
@ -116,25 +114,6 @@ def optschain(config, symbol, date, tl, rate, test):
def chart(config, symbol, date, tl, rate, test): def chart(config, symbol, date, tl, rate, test):
"""Start an option chain UI """Start an option chain UI
""" """
from .qt._exec import run_qtrio from .qt._chart import main
from .qt._chart import Chart
# uses pandas_datareader main(symbol)
from .qt.quantdom.loaders import get_quotes
async def plot_symbol(widgets):
"""Main Qt-trio routine invoked by the Qt loop with
the widgets ``dict``.
"""
qtw = widgets['main']
quotes = get_quotes(
symbol=symbol,
date_from=datetime(1900, 1, 1),
date_to=datetime(2030, 12, 31),
)
# spawn chart
qtw.load_symbol(symbol, quotes)
await trio.sleep_forever()
run_qtrio(plot_symbol, (), Chart)

View File

@ -1,6 +1,7 @@
""" """
High level Qt chart widgets. High level Qt chart widgets.
""" """
import trio
import numpy as np import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph import functions as fn from pyqtgraph import functions as fn
@ -12,7 +13,7 @@ from ._axes import (
) )
from ._graphics import CrossHairItem, ChartType from ._graphics import CrossHairItem, ChartType
from ._style import _xaxis_at from ._style import _xaxis_at
from ._source import Symbol from ._source import Symbol, ohlc_zeros
from .quantdom.charts import CenteredTextItem from .quantdom.charts import CenteredTextItem
from .quantdom.base import Quotes from .quantdom.base import Quotes
@ -77,6 +78,7 @@ class Chart(QtGui.QWidget):
self.chart.plot(s, data) self.chart.plot(s, data)
self.h_layout.addWidget(self.chart) self.h_layout.addWidget(self.chart)
return self.chart
# TODO: add signalling painter system # TODO: add signalling painter system
# def add_signals(self): # def add_signals(self):
@ -163,6 +165,7 @@ class SplitterPlots(QtGui.QWidget):
) )
# TODO: ``pyqtgraph`` doesn't pass through a parent to the # TODO: ``pyqtgraph`` doesn't pass through a parent to the
# ``PlotItem`` by default; maybe we should PR this in? # ``PlotItem`` by default; maybe we should PR this in?
cv.splitter_widget = self
self.chart.plotItem.vb.splitter_widget = self self.chart.plotItem.vb.splitter_widget = self
self.chart.getPlotItem().setContentsMargins(*CHART_MARGINS) self.chart.getPlotItem().setContentsMargins(*CHART_MARGINS)
@ -182,6 +185,7 @@ class SplitterPlots(QtGui.QWidget):
# axisItems={'top': self.xaxis_ind, 'right': PriceAxis()}, # axisItems={'top': self.xaxis_ind, 'right': PriceAxis()},
viewBox=cv, viewBox=cv,
) )
cv.splitter_widget = self
self.chart.plotItem.vb.splitter_widget = self self.chart.plotItem.vb.splitter_widget = self
ind_chart.setFrameStyle( ind_chart.setFrameStyle(
@ -260,7 +264,7 @@ class SplitterPlots(QtGui.QWidget):
self.signals_visible = True self.signals_visible = True
_min_points_to_show = 20 _min_points_to_show = 15
_min_bars_in_view = 10 _min_bars_in_view = 10
@ -291,7 +295,6 @@ class ChartPlotWidget(pg.PlotWidget):
): ):
"""Configure chart display settings. """Configure chart display settings.
""" """
super().__init__(**kwargs) super().__init__(**kwargs)
# label = pg.LabelItem(justify='left') # label = pg.LabelItem(justify='left')
# self.addItem(label) # self.addItem(label)
@ -299,6 +302,12 @@ class ChartPlotWidget(pg.PlotWidget):
# label.setText("<span style='font-size: 12pt'>x=") # label.setText("<span style='font-size: 12pt'>x=")
self.parent = split_charts self.parent = split_charts
# placeholder for source of data
self._array = ohlc_zeros(1)
# to be filled in when data is loaded
self._graphics = {}
# show only right side axes # show only right side axes
self.hideAxis('left') self.hideAxis('left')
self.showAxis('right') self.showAxis('right')
@ -306,51 +315,75 @@ 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)
self.plotItem.vb.setXRange(0, 0)
# use cross-hair for cursor # use cross-hair for cursor
self.setCursor(QtCore.Qt.CrossCursor) self.setCursor(QtCore.Qt.CrossCursor)
# set panning limits
max_lookahead = _min_points_to_show - _min_bars_in_view
last = Quotes[-1].id
self.setLimits(
xMin=Quotes[0].id,
xMax=last + max_lookahead,
minXRange=_min_points_to_show,
# maxYRange=highest-lowest,
yMin=Quotes.low.min() * 0.98,
yMax=Quotes.high.max() * 1.02,
)
# show last 50 points on startup
self.plotItem.vb.setXRange(last - 50, last + max_lookahead)
# assign callback for rescaling y-axis automatically # assign callback for rescaling y-axis automatically
# based on y-range contents # based on y-range contents
self.sigXRangeChanged.connect(self._update_yrange_limits) self.sigXRangeChanged.connect(self._update_yrange_limits)
def set_view_limits(self, xfirst, xlast, ymin, ymax):
# max_lookahead = _min_points_to_show - _min_bars_in_view
# set panning limits
# last = data[-1]['id']
self.setLimits(
# xMin=data[0]['id'],
xMin=xfirst,
# xMax=last + _min_points_to_show - 3,
xMax=xlast + _min_points_to_show - 3,
minXRange=_min_points_to_show,
# maxYRange=highest-lowest,
# yMin=data['low'].min() * 0.98,
# yMax=data['high'].max() * 1.02,
yMin=ymin * 0.98,
yMax=ymax * 1.02,
)
# show last 50 points on startup
# self.plotItem.vb.setXRange(last - 50, last + 50)
self.plotItem.vb.setXRange(xlast - 50, xlast + 50)
# fit y
self._update_yrange_limits() self._update_yrange_limits()
def bars_range(self): def bars_range(self):
"""Return a range tuple for the bars present in view. """Return a range tuple for the bars present in view.
""" """
vr = self.viewRect() vr = self.viewRect()
lbar, rbar = int(vr.left()), int(min(vr.right(), len(Quotes) - 1)) lbar = int(vr.left())
rbar = int(min(vr.right(), len(self._array) - 1))
return lbar, rbar return lbar, rbar
def draw_ohlc( def draw_ohlc(
self, self,
data: np.ndarray, data: np.ndarray,
# XXX: pretty sure this is dumb and we don't need an Enum
style: ChartType = ChartType.BAR, style: ChartType = ChartType.BAR,
) -> None: ) -> None:
"""Draw OHLC datums to chart. """Draw OHLC datums to chart.
""" """
# remember it's an enum type.. # remember it's an enum type..
graphics = style.value() graphics = style.value()
# adds all bar/candle graphics objects for each
# data point in the np array buffer to # adds all bar/candle graphics objects for each data point in
# be drawn on next render cycle # the np array buffer to be drawn on next render cycle
graphics.draw_from_data(data) graphics.draw_from_data(data)
self._graphics['ohlc'] = graphics
self.addItem(graphics) self.addItem(graphics)
self._array = data
# update view limits
self.set_view_limits(
data[0]['id'],
data[-1]['id'],
data['low'].min(),
data['high'].max()
)
return graphics
def draw_curve( def draw_curve(
self, self,
@ -360,6 +393,12 @@ class ChartPlotWidget(pg.PlotWidget):
curve = pg.PlotDataItem(data, antialias=True) curve = pg.PlotDataItem(data, antialias=True)
self.addItem(curve) self.addItem(curve)
# update view limits
self.set_view_limits(0, len(data)-1, data.min(), data.max())
self._array = data
return curve
def _update_yrange_limits(self): def _update_yrange_limits(self):
"""Callback for each y-range update. """Callback for each y-range update.
@ -374,34 +413,40 @@ class ChartPlotWidget(pg.PlotWidget):
# self.setAutoVisible(x=False, y=True) # self.setAutoVisible(x=False, y=True)
# self.enableAutoRange(x=False, y=True) # self.enableAutoRange(x=False, y=True)
chart = self
chart_parent = self.parent
lbar, rbar = self.bars_range() lbar, rbar = self.bars_range()
# vr = chart.viewRect()
# lbar, rbar = int(vr.left()), int(vr.right())
if chart_parent.signals_visible: # if chart_parent.signals_visible:
chart_parent._show_text_signals(lbar, rbar) # chart_parent._show_text_signals(lbar, rbar)
bars = Quotes[lbar:rbar] bars = self._array[lbar:rbar]
ylow = bars.low.min() * 0.98 if not len(bars):
yhigh = bars.high.max() * 1.02 # likely no data loaded yet
return
std = np.std(bars.close) # TODO: should probably just have some kinda attr mark
chart.setLimits(yMin=ylow, yMax=yhigh, minYRange=std) # that determines this behavior based on array type
try:
ylow = bars['low'].min()
yhigh = bars['high'].max()
std = np.std(bars['close'])
except IndexError:
# must be non-ohlc array?
ylow = bars.min()
yhigh = bars.max()
std = np.std(bars)
# view margins
ylow *= 0.98
yhigh *= 1.02
chart = self
chart.setLimits(
yMin=ylow,
yMax=yhigh,
minYRange=std
)
chart.setYRange(ylow, yhigh) chart.setYRange(ylow, yhigh)
for i, d in chart_parent.indicators:
# ydata = i.plotItem.items[0].getData()[1]
ydata = d[lbar:rbar]
ylow = ydata.min() * 0.98
yhigh = ydata.max() * 1.02
std = np.std(ydata)
i.setLimits(yMin=ylow, yMax=yhigh, minYRange=std)
i.setYRange(ylow, yhigh)
def enterEvent(self, ev): # noqa def enterEvent(self, ev): # noqa
# pg.PlotWidget.enterEvent(self, ev) # pg.PlotWidget.enterEvent(self, ev)
self.sig_mouse_enter.emit(self) self.sig_mouse_enter.emit(self)
@ -429,6 +474,7 @@ class ChartView(pg.ViewBox):
super().__init__(parent=parent, **kwargs) super().__init__(parent=parent, **kwargs)
# disable vertical scrolling # disable vertical scrolling
self.setMouseEnabled(x=True, y=False) self.setMouseEnabled(x=True, y=False)
self.splitter_widget = None
def wheelEvent(self, ev, axis=None): def wheelEvent(self, ev, axis=None):
"""Override "center-point" location for scrolling. """Override "center-point" location for scrolling.
@ -447,11 +493,12 @@ class ChartView(pg.ViewBox):
# don't zoom more then the min points setting # don't zoom more then the min points setting
lbar, rbar = self.splitter_widget.chart.bars_range() lbar, rbar = self.splitter_widget.chart.bars_range()
# breakpoint()
if ev.delta() >= 0 and rbar - lbar <= _min_points_to_show: if ev.delta() >= 0 and rbar - lbar <= _min_points_to_show:
return return
# actual scaling factor # actual scaling factor
s = 1.02 ** (ev.delta() * self.state['wheelScaleFactor']) s = 1.02 ** (ev.delta() * -1/10) # self.state['wheelScaleFactor'])
s = [(None if m is False else s) for m in mask] s = [(None if m is False else s) for m in mask]
# center = pg.Point( # center = pg.Point(
@ -470,3 +517,37 @@ class ChartView(pg.ViewBox):
self.scaleBy(s, center) self.scaleBy(s, center)
ev.accept() ev.accept()
self.sigRangeChangedManually.emit(mask) self.sigRangeChangedManually.emit(mask)
def main(symbol):
"""Entry point to spawn a chart app.
"""
from datetime import datetime
from ._exec import run_qtrio
# uses pandas_datareader
from .quantdom.loaders import get_quotes
async def _main(widgets):
"""Main Qt-trio routine invoked by the Qt loop with
the widgets ``dict``.
"""
chart_app = widgets['main']
quotes = get_quotes(
symbol=symbol,
date_from=datetime(1900, 1, 1),
date_to=datetime(2030, 12, 31),
)
# spawn chart
splitter_chart = chart_app.load_symbol(symbol, quotes)
import itertools
nums = itertools.cycle([315., 320., 325.])
while True:
await trio.sleep(0.05)
splitter_chart.chart._graphics['ohlc'].update_last_bar(
{'last': next(nums)})
# splitter_chart.chart.plotItem.sigPlotChanged.emit(self)
# breakpoint()
run_qtrio(_main, (), Chart)