From 9d6dffe5ecdd5cc79fb3a72e03887c77e7a99ee9 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 17 Jun 2020 11:45:43 -0400 Subject: [PATCH] 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. --- piker/ui/cli.py | 25 +----- piker/ui/qt/_chart.py | 173 +++++++++++++++++++++++++++++++----------- 2 files changed, 129 insertions(+), 69 deletions(-) diff --git a/piker/ui/cli.py b/piker/ui/cli.py index 54a77eee..59d7bfb5 100644 --- a/piker/ui/cli.py +++ b/piker/ui/cli.py @@ -1,11 +1,9 @@ """ Console interface to UI components. """ -from datetime import datetime from functools import partial import os import click -import trio import tractor from ..cli import cli @@ -117,25 +115,6 @@ def optschain(config, symbol, date, tl, rate, test): def chart(config, symbol, date, tl, rate, test): """Start an option chain UI """ - from .qt._exec import run_qtrio - from .qt._chart import Chart + from .qt._chart import main - # uses pandas_datareader - 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) + main(symbol) diff --git a/piker/ui/qt/_chart.py b/piker/ui/qt/_chart.py index cdf6b988..28425605 100644 --- a/piker/ui/qt/_chart.py +++ b/piker/ui/qt/_chart.py @@ -1,6 +1,7 @@ """ High level Qt chart widgets. """ +import trio import numpy as np import pyqtgraph as pg from pyqtgraph import functions as fn @@ -12,7 +13,7 @@ from ._axes import ( ) from ._graphics import CrossHairItem, ChartType from ._style import _xaxis_at -from ._source import Symbol +from ._source import Symbol, ohlc_zeros from .quantdom.charts import CenteredTextItem from .quantdom.base import Quotes @@ -77,6 +78,7 @@ class Chart(QtGui.QWidget): self.chart.plot(s, data) self.h_layout.addWidget(self.chart) + return self.chart # TODO: add signalling painter system # def add_signals(self): @@ -163,6 +165,7 @@ class SplitterPlots(QtGui.QWidget): ) # TODO: ``pyqtgraph`` doesn't pass through a parent to the # ``PlotItem`` by default; maybe we should PR this in? + cv.splitter_widget = self self.chart.plotItem.vb.splitter_widget = self self.chart.getPlotItem().setContentsMargins(*CHART_MARGINS) @@ -182,6 +185,7 @@ class SplitterPlots(QtGui.QWidget): # axisItems={'top': self.xaxis_ind, 'right': PriceAxis()}, viewBox=cv, ) + cv.splitter_widget = self self.chart.plotItem.vb.splitter_widget = self ind_chart.setFrameStyle( @@ -260,7 +264,7 @@ class SplitterPlots(QtGui.QWidget): self.signals_visible = True -_min_points_to_show = 20 +_min_points_to_show = 15 _min_bars_in_view = 10 @@ -291,7 +295,6 @@ class ChartPlotWidget(pg.PlotWidget): ): """Configure chart display settings. """ - super().__init__(**kwargs) # label = pg.LabelItem(justify='left') # self.addItem(label) @@ -299,6 +302,12 @@ class ChartPlotWidget(pg.PlotWidget): # label.setText("x=") 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 self.hideAxis('left') self.showAxis('right') @@ -306,51 +315,75 @@ class ChartPlotWidget(pg.PlotWidget): # show background grid self.showGrid(x=True, y=True, alpha=0.4) + self.plotItem.vb.setXRange(0, 0) + # use cross-hair for cursor 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 # based on y-range contents 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() def bars_range(self): """Return a range tuple for the bars present in view. """ - 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 def draw_ohlc( self, data: np.ndarray, + # XXX: pretty sure this is dumb and we don't need an Enum style: ChartType = ChartType.BAR, ) -> None: """Draw OHLC datums to chart. """ # remember it's an enum type.. graphics = style.value() - # adds all bar/candle graphics objects for each - # data point in the np array buffer to - # be drawn on next render cycle + + # adds all bar/candle graphics objects for each data point in + # the np array buffer to be drawn on next render cycle graphics.draw_from_data(data) + self._graphics['ohlc'] = 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( self, @@ -360,6 +393,12 @@ class ChartPlotWidget(pg.PlotWidget): curve = pg.PlotDataItem(data, antialias=True) 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): """Callback for each y-range update. @@ -374,34 +413,40 @@ class ChartPlotWidget(pg.PlotWidget): # self.setAutoVisible(x=False, y=True) # self.enableAutoRange(x=False, y=True) - chart = self - chart_parent = self.parent - lbar, rbar = self.bars_range() - # vr = chart.viewRect() - # lbar, rbar = int(vr.left()), int(vr.right()) - if chart_parent.signals_visible: - chart_parent._show_text_signals(lbar, rbar) + # if chart_parent.signals_visible: + # chart_parent._show_text_signals(lbar, rbar) - bars = Quotes[lbar:rbar] - ylow = bars.low.min() * 0.98 - yhigh = bars.high.max() * 1.02 + bars = self._array[lbar:rbar] + if not len(bars): + # likely no data loaded yet + return - std = np.std(bars.close) - chart.setLimits(yMin=ylow, yMax=yhigh, minYRange=std) + # TODO: should probably just have some kinda attr mark + # 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) - 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 # pg.PlotWidget.enterEvent(self, ev) self.sig_mouse_enter.emit(self) @@ -429,6 +474,7 @@ class ChartView(pg.ViewBox): super().__init__(parent=parent, **kwargs) # disable vertical scrolling self.setMouseEnabled(x=True, y=False) + self.splitter_widget = None def wheelEvent(self, ev, axis=None): """Override "center-point" location for scrolling. @@ -447,11 +493,12 @@ class ChartView(pg.ViewBox): # don't zoom more then the min points setting lbar, rbar = self.splitter_widget.chart.bars_range() + # breakpoint() if ev.delta() >= 0 and rbar - lbar <= _min_points_to_show: return # 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] # center = pg.Point( @@ -470,3 +517,37 @@ class ChartView(pg.ViewBox): self.scaleBy(s, center) ev.accept() 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)