Factor common chart configuration
parent
6bac250db5
commit
856c6f2fd8
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Real-time quotes charting components
|
Real-time quotes charting components
|
||||||
"""
|
"""
|
||||||
from typing import Callable
|
from typing import Callable, List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
@ -279,7 +279,9 @@ class ScrollFromRightView(pg.ViewBox):
|
||||||
# )
|
# )
|
||||||
furthest_right_coord = self.boundingRect().topRight()
|
furthest_right_coord = self.boundingRect().topRight()
|
||||||
center = pg.Point(
|
center = pg.Point(
|
||||||
fn.invertQTransform(self.childGroup.transform()).map(furthest_right_coord)
|
fn.invertQTransform(
|
||||||
|
self.childGroup.transform()
|
||||||
|
).map(furthest_right_coord)
|
||||||
)
|
)
|
||||||
|
|
||||||
self._resetTarget()
|
self._resetTarget()
|
||||||
|
@ -288,13 +290,15 @@ class ScrollFromRightView(pg.ViewBox):
|
||||||
self.sigRangeChangedManually.emit(mask)
|
self.sigRangeChangedManually.emit(mask)
|
||||||
|
|
||||||
|
|
||||||
# TODO: convert this to a ``ViewBox`` type giving us
|
# TODO: This is a sub-class of ``GracphicView`` which can
|
||||||
# control over mouse scrolling and a context menu
|
|
||||||
# This is a sub-class of ``GracphicView`` which can
|
|
||||||
# take a ``background`` color setting.
|
# take a ``background`` color setting.
|
||||||
class CustomPlotWidget(pg.PlotWidget):
|
class ChartPlotWidget(pg.PlotWidget):
|
||||||
"""``GraphicsView`` subtype containing a single ``PlotItem``.
|
"""``GraphicsView`` subtype containing a single ``PlotItem``.
|
||||||
|
|
||||||
|
Overrides a ``pyqtgraph.PlotWidget`` (a ``GraphicsView`` containing
|
||||||
|
a single ``PlotItem``) to intercept and and re-emit mouse enter/exit
|
||||||
|
events.
|
||||||
|
|
||||||
(Could be replaced with a ``pg.GraphicsLayoutWidget`` if we
|
(Could be replaced with a ``pg.GraphicsLayoutWidget`` if we
|
||||||
eventually want multiple plots managed together).
|
eventually want multiple plots managed together).
|
||||||
"""
|
"""
|
||||||
|
@ -360,6 +364,7 @@ class CrossHairItem(pg.GraphicsObject):
|
||||||
slot=lambda: self.mouseAction('Leave', False),
|
slot=lambda: self.mouseAction('Leave', False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# determine where to place x-axis label
|
||||||
if _xaxis_at == 'bottom':
|
if _xaxis_at == 'bottom':
|
||||||
# place below is last indicator subplot
|
# place below is last indicator subplot
|
||||||
self.xaxis_label = XAxisLabel(
|
self.xaxis_label = XAxisLabel(
|
||||||
|
@ -407,7 +412,8 @@ class CrossHairItem(pg.GraphicsObject):
|
||||||
else:
|
else:
|
||||||
self.yaxis_label.show()
|
self.yaxis_label.show()
|
||||||
self.hline.show()
|
self.hline.show()
|
||||||
else: # Leave
|
# Leave
|
||||||
|
else:
|
||||||
# hide horiz line and y-label
|
# hide horiz line and y-label
|
||||||
if ind:
|
if ind:
|
||||||
self.indicators[ind]['hl'].hide()
|
self.indicators[ind]['hl'].hide()
|
||||||
|
@ -424,7 +430,7 @@ class CrossHairItem(pg.GraphicsObject):
|
||||||
|
|
||||||
pos = evt[0]
|
pos = evt[0]
|
||||||
|
|
||||||
# if the mouse is within the parent ``CustomPlotWidget``
|
# if the mouse is within the parent ``ChartPlotWidget``
|
||||||
if self.parent.sceneBoundingRect().contains(pos):
|
if self.parent.sceneBoundingRect().contains(pos):
|
||||||
# mouse_point = self.vb.mapSceneToView(pos)
|
# mouse_point = self.vb.mapSceneToView(pos)
|
||||||
mouse_point = self.parent.mapToView(pos)
|
mouse_point = self.parent.mapToView(pos)
|
||||||
|
@ -551,22 +557,16 @@ class CandlestickItem(BarItem):
|
||||||
p.drawRects(*rects[Quotes.close < Quotes.open])
|
p.drawRects(*rects[Quotes.close < Quotes.open])
|
||||||
|
|
||||||
|
|
||||||
def _configure_quotes_chart(
|
def _configure_chart(
|
||||||
chart: CustomPlotWidget,
|
chart: ChartPlotWidget,
|
||||||
style: ChartType,
|
|
||||||
update_yrange_limits: Callable,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update and format a chart with quotes data.
|
"""Configure a chart with common settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# show only right side axes
|
||||||
chart.hideAxis('left')
|
chart.hideAxis('left')
|
||||||
chart.showAxis('right')
|
chart.showAxis('right')
|
||||||
|
|
||||||
# adds all bar/candle graphics objects for each
|
|
||||||
# data point in the np array buffer to
|
|
||||||
# be drawn on next render cycle
|
|
||||||
chart.addItem(_get_chart_points(style))
|
|
||||||
|
|
||||||
# set panning limits
|
# set panning limits
|
||||||
chart.setLimits(
|
chart.setLimits(
|
||||||
xMin=Quotes[0].id,
|
xMin=Quotes[0].id,
|
||||||
|
@ -575,16 +575,53 @@ def _configure_quotes_chart(
|
||||||
yMin=Quotes.low.min() * 0.98,
|
yMin=Quotes.low.min() * 0.98,
|
||||||
yMax=Quotes.high.max() * 1.02,
|
yMax=Quotes.high.max() * 1.02,
|
||||||
)
|
)
|
||||||
|
# show background grid
|
||||||
chart.showGrid(x=True, y=True, alpha=0.4)
|
chart.showGrid(x=True, y=True, alpha=0.4)
|
||||||
|
|
||||||
|
# use cross-hair for cursor
|
||||||
chart.setCursor(QtCore.Qt.CrossCursor)
|
chart.setCursor(QtCore.Qt.CrossCursor)
|
||||||
|
|
||||||
|
|
||||||
|
def _configure_quotes_chart(
|
||||||
|
chart: ChartPlotWidget,
|
||||||
|
style: ChartType,
|
||||||
|
update_yrange_limits: Callable,
|
||||||
|
) -> None:
|
||||||
|
"""Update and format a chart with quotes data.
|
||||||
|
"""
|
||||||
|
_configure_chart(chart)
|
||||||
|
|
||||||
|
# adds all bar/candle graphics objects for each
|
||||||
|
# data point in the np array buffer to
|
||||||
|
# be drawn on next render cycle
|
||||||
|
chart.addItem(_get_chart_points(style))
|
||||||
|
|
||||||
# assign callback for rescaling y-axis automatically
|
# assign callback for rescaling y-axis automatically
|
||||||
# based on y-range contents
|
# based on y-range contents
|
||||||
|
|
||||||
# TODO: this can likely be ported to built-in: .enableAutoRange()
|
# TODO: this can likely be ported to built-in: .enableAutoRange()
|
||||||
# but needs testing
|
# but needs testing
|
||||||
chart.sigXRangeChanged.connect(update_yrange_limits)
|
chart.sigXRangeChanged.connect(update_yrange_limits)
|
||||||
|
|
||||||
|
|
||||||
|
def _configure_ind_charts(
|
||||||
|
indicators: List[Tuple[ChartPlotWidget, np.ndarray]],
|
||||||
|
xlink_to_chart: ChartPlotWidget,
|
||||||
|
) -> None:
|
||||||
|
for ind_chart, d in indicators:
|
||||||
|
# default config
|
||||||
|
_configure_chart(ind_chart)
|
||||||
|
|
||||||
|
curve = pg.PlotDataItem(d, pen='b', antialias=True)
|
||||||
|
ind_chart.addItem(curve)
|
||||||
|
|
||||||
|
# XXX: never do this lol
|
||||||
|
# ind.setAspectLocked(1)
|
||||||
|
|
||||||
|
# link chart x-axis to main quotes chart
|
||||||
|
ind_chart.setXLink(xlink_to_chart)
|
||||||
|
|
||||||
|
|
||||||
class QuotesChart(QtGui.QWidget):
|
class QuotesChart(QtGui.QWidget):
|
||||||
|
|
||||||
long_pen = pg.mkPen('#006000')
|
long_pen = pg.mkPen('#006000')
|
||||||
|
@ -639,25 +676,6 @@ class QuotesChart(QtGui.QWidget):
|
||||||
del self.signals_group_text
|
del self.signals_group_text
|
||||||
self.signals_visible = False
|
self.signals_visible = False
|
||||||
|
|
||||||
def _update_ind_charts(self):
|
|
||||||
for ind, d in self.indicators:
|
|
||||||
curve = pg.PlotDataItem(d, pen='b', antialias=True)
|
|
||||||
ind.addItem(curve)
|
|
||||||
ind.hideAxis('left')
|
|
||||||
ind.showAxis('right')
|
|
||||||
# XXX: never do this lol
|
|
||||||
# ind.setAspectLocked(1)
|
|
||||||
ind.setXLink(self.chart)
|
|
||||||
ind.setLimits(
|
|
||||||
xMin=Quotes[0].id,
|
|
||||||
xMax=Quotes[-1].id,
|
|
||||||
minXRange=60,
|
|
||||||
yMin=Quotes.open.min() * 0.98,
|
|
||||||
yMax=Quotes.open.max() * 1.02,
|
|
||||||
)
|
|
||||||
ind.showGrid(x=True, y=True, alpha=0.4)
|
|
||||||
ind.setCursor(QtCore.Qt.CrossCursor)
|
|
||||||
|
|
||||||
def _update_sizes(self):
|
def _update_sizes(self):
|
||||||
min_h_ind = int(self.height() * 0.3 / len(self.indicators))
|
min_h_ind = int(self.height() * 0.3 / len(self.indicators))
|
||||||
sizes = [int(self.height() * 0.7)]
|
sizes = [int(self.height() * 0.7)]
|
||||||
|
@ -691,7 +709,7 @@ class QuotesChart(QtGui.QWidget):
|
||||||
|
|
||||||
def plot(self, symbol):
|
def plot(self, symbol):
|
||||||
self.digits = symbol.digits
|
self.digits = symbol.digits
|
||||||
self.chart = CustomPlotWidget(
|
self.chart = ChartPlotWidget(
|
||||||
parent=self.splitter,
|
parent=self.splitter,
|
||||||
axisItems={'bottom': self.xaxis, 'right': PriceAxis()},
|
axisItems={'bottom': self.xaxis, 'right': PriceAxis()},
|
||||||
viewBox=ScrollFromRightView,
|
viewBox=ScrollFromRightView,
|
||||||
|
@ -704,7 +722,7 @@ class QuotesChart(QtGui.QWidget):
|
||||||
inds = [Quotes.open]
|
inds = [Quotes.open]
|
||||||
|
|
||||||
for d in inds:
|
for d in inds:
|
||||||
ind = CustomPlotWidget(
|
ind = ChartPlotWidget(
|
||||||
parent=self.splitter,
|
parent=self.splitter,
|
||||||
axisItems={'bottom': self.xaxis_ind, 'right': PriceAxis()},
|
axisItems={'bottom': self.xaxis_ind, 'right': PriceAxis()},
|
||||||
# axisItems={'top': self.xaxis_ind, 'right': PriceAxis()},
|
# axisItems={'top': self.xaxis_ind, 'right': PriceAxis()},
|
||||||
|
@ -720,7 +738,10 @@ class QuotesChart(QtGui.QWidget):
|
||||||
self.style,
|
self.style,
|
||||||
self._update_yrange_limits
|
self._update_yrange_limits
|
||||||
)
|
)
|
||||||
self._update_ind_charts()
|
_configure_ind_charts(
|
||||||
|
self.indicators,
|
||||||
|
xlink_to_chart=self.chart
|
||||||
|
)
|
||||||
self._update_sizes()
|
self._update_sizes()
|
||||||
|
|
||||||
ch = CrossHairItem(
|
ch = CrossHairItem(
|
||||||
|
@ -783,7 +804,7 @@ class QuotesChart(QtGui.QWidget):
|
||||||
self.signals_visible = True
|
self.signals_visible = True
|
||||||
|
|
||||||
|
|
||||||
# this function is borderline rediculous.
|
# this function is borderline ridiculous.
|
||||||
# The creation of these chart types mutates all the input data
|
# The creation of these chart types mutates all the input data
|
||||||
# inside each type's constructor (mind blown)
|
# inside each type's constructor (mind blown)
|
||||||
def _get_chart_points(style):
|
def _get_chart_points(style):
|
||||||
|
|
Loading…
Reference in New Issue