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.
bar_select
Tyler Goodlet 2020-06-17 11:45:43 -04:00
parent 36ac26cdcf
commit 9d6dffe5ec
2 changed files with 129 additions and 69 deletions

View File

@ -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)

View File

@ -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("<span style='font-size: 12pt'>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)