Render plots from provided input sequence(s)
Previously graphics were loaded and rendered implicitly during the import and creation of certain objects. Remove all this and instead expect client code to pass in the OHLC sequence to plot. Speed up the bars graphics rendering by simplifying to a single iteration of the input array; gives about a 2x speedup.bar_select
parent
f77a39ceb7
commit
45906c2729
|
@ -118,20 +118,24 @@ def chart(config, symbol, date, tl, rate, test):
|
|||
"""Start an option chain UI
|
||||
"""
|
||||
from .qt._exec import run_qtrio
|
||||
from .qt._chart import QuotesTabWidget
|
||||
from .qt.quantdom.base import Symbol
|
||||
from .qt._chart import Chart
|
||||
|
||||
# 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']
|
||||
s = Symbol(ticker=symbol, mode=Symbol.SHARES)
|
||||
get_quotes(
|
||||
symbol=s.ticker,
|
||||
quotes = get_quotes(
|
||||
symbol=symbol,
|
||||
date_from=datetime(1900, 1, 1),
|
||||
date_to=datetime(2030, 12, 31),
|
||||
)
|
||||
# spawn chart
|
||||
qtw.update_chart(s)
|
||||
qtw.load_symbol(symbol, quotes)
|
||||
await trio.sleep_forever()
|
||||
|
||||
run_qtrio(plot_symbol, (), QuotesTabWidget)
|
||||
run_qtrio(plot_symbol, (), Chart)
|
||||
|
|
|
@ -72,7 +72,14 @@ class AxisLabel(pg.GraphicsObject):
|
|||
bg_color = pg.mkColor('#808080')
|
||||
fg_color = pg.mkColor('#000000')
|
||||
|
||||
def __init__(self, parent=None, digits=0, color=None, opacity=1, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
digits=0,
|
||||
color=None,
|
||||
opacity=1,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.parent = parent
|
||||
self.opacity = opacity
|
||||
|
|
|
@ -10,12 +10,13 @@ from ._axes import (
|
|||
FromTimeFieldDateAxis,
|
||||
PriceAxis,
|
||||
)
|
||||
from ._graphics import CrossHairItem, CandlestickItem, BarItem
|
||||
from ._graphics import CrossHairItem, ChartType
|
||||
from ._style import _xaxis_at
|
||||
from ._source import Symbol
|
||||
|
||||
from .quantdom.charts import CenteredTextItem
|
||||
from .quantdom.base import Quotes
|
||||
from .quantdom.const import ChartType
|
||||
# from .quantdom.const import ChartType
|
||||
from .quantdom.portfolio import Order, Portfolio
|
||||
|
||||
|
||||
|
@ -23,20 +24,21 @@ from .quantdom.portfolio import Order, Portfolio
|
|||
CHART_MARGINS = (0, 0, 10, 3)
|
||||
|
||||
|
||||
class QuotesTabWidget(QtGui.QWidget):
|
||||
class Chart(QtGui.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.layout = QtGui.QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.v_layout = QtGui.QVBoxLayout(self)
|
||||
self.v_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.toolbar_layout = QtGui.QHBoxLayout()
|
||||
self.toolbar_layout.setContentsMargins(10, 10, 15, 0)
|
||||
self.chart_layout = QtGui.QHBoxLayout()
|
||||
self.h_layout = QtGui.QHBoxLayout()
|
||||
|
||||
# self.init_timeframes_ui()
|
||||
# self.init_strategy_ui()
|
||||
|
||||
self.layout.addLayout(self.toolbar_layout)
|
||||
self.layout.addLayout(self.chart_layout)
|
||||
self.v_layout.addLayout(self.toolbar_layout)
|
||||
self.v_layout.addLayout(self.h_layout)
|
||||
self._plot_cache = {}
|
||||
|
||||
def init_timeframes_ui(self):
|
||||
self.tf_layout = QtGui.QHBoxLayout()
|
||||
|
@ -58,21 +60,32 @@ class QuotesTabWidget(QtGui.QWidget):
|
|||
# self.strategy_box = StrategyBoxWidget(self)
|
||||
# self.toolbar_layout.addWidget(self.strategy_box)
|
||||
|
||||
# TODO: this needs to be changed to ``load_symbol()``
|
||||
# which will not only load historical data but also a real-time
|
||||
# stream and schedule the redraw events on new quotes
|
||||
def update_chart(self, symbol):
|
||||
if not self.chart_layout.isEmpty():
|
||||
self.chart_layout.removeWidget(self.chart)
|
||||
self.chart = SplitterChart()
|
||||
self.chart.plot(symbol)
|
||||
self.chart_layout.addWidget(self.chart)
|
||||
def load_symbol(
|
||||
self,
|
||||
symbol: str,
|
||||
data: np.ndarray,
|
||||
) -> None:
|
||||
"""Load a new contract into the charting app.
|
||||
"""
|
||||
# XXX: let's see if this causes mem problems
|
||||
self.chart = self._plot_cache.setdefault(symbol, SplitterPlots())
|
||||
s = Symbol(key=symbol)
|
||||
|
||||
def add_signals(self):
|
||||
self.chart.add_signals()
|
||||
# remove any existing plots
|
||||
if not self.h_layout.isEmpty():
|
||||
self.h_layout.removeWidget(self.chart)
|
||||
|
||||
self.chart.plot(s, data)
|
||||
self.h_layout.addWidget(self.chart)
|
||||
|
||||
# TODO: add signalling painter system
|
||||
# def add_signals(self):
|
||||
# self.chart.add_signals()
|
||||
|
||||
|
||||
class SplitterChart(QtGui.QWidget):
|
||||
class SplitterPlots(QtGui.QWidget):
|
||||
"""Widget that holds a price chart plus indicators separated by splitters.
|
||||
"""
|
||||
|
||||
long_pen = pg.mkPen('#006000')
|
||||
long_brush = pg.mkBrush('#00ff00')
|
||||
|
@ -131,16 +144,21 @@ class SplitterChart(QtGui.QWidget):
|
|||
sizes.extend([min_h_ind] * len(self.indicators))
|
||||
self.splitter.setSizes(sizes) # , int(self.height()*0.2)
|
||||
|
||||
def plot(self, symbol):
|
||||
def plot(
|
||||
self,
|
||||
symbol: Symbol,
|
||||
data: np.ndarray,
|
||||
):
|
||||
"""Start up and show price chart and all registered indicators.
|
||||
"""
|
||||
self.digits = symbol.digits
|
||||
self.digits = symbol.digits()
|
||||
|
||||
cv = ChartView()
|
||||
self.chart = ChartPlotWidget(
|
||||
split_charts=self,
|
||||
parent=self.splitter,
|
||||
axisItems={'bottom': self.xaxis, 'right': PriceAxis()},
|
||||
viewBox=ChartView,
|
||||
viewBox=cv,
|
||||
# enableMenu=False,
|
||||
)
|
||||
# TODO: ``pyqtgraph`` doesn't pass through a parent to the
|
||||
|
@ -150,27 +168,28 @@ class SplitterChart(QtGui.QWidget):
|
|||
self.chart.getPlotItem().setContentsMargins(*CHART_MARGINS)
|
||||
self.chart.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
||||
|
||||
self.chart.draw_ohlc(data)
|
||||
|
||||
# TODO: this is where we would load an indicator chain
|
||||
inds = [Quotes.open]
|
||||
|
||||
for d in inds:
|
||||
ind = ChartPlotWidget(
|
||||
cv = ChartView()
|
||||
ind_chart = ChartPlotWidget(
|
||||
split_charts=self,
|
||||
parent=self.splitter,
|
||||
axisItems={'bottom': self.xaxis_ind, 'right': PriceAxis()},
|
||||
# axisItems={'top': self.xaxis_ind, 'right': PriceAxis()},
|
||||
viewBox=ChartView,
|
||||
viewBox=cv,
|
||||
)
|
||||
self.chart.plotItem.vb.splitter_widget = self
|
||||
|
||||
ind.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
||||
ind.getPlotItem().setContentsMargins(*CHART_MARGINS)
|
||||
# self.splitter.addWidget(ind)
|
||||
self.indicators.append((ind, d))
|
||||
|
||||
self.chart.draw_ohlc()
|
||||
|
||||
for ind_chart, d in self.indicators:
|
||||
ind_chart.setFrameStyle(
|
||||
QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain
|
||||
)
|
||||
ind_chart.getPlotItem().setContentsMargins(*CHART_MARGINS)
|
||||
# self.splitter.addWidget(ind_chart)
|
||||
self.indicators.append((ind_chart, d))
|
||||
|
||||
# link chart x-axis to main quotes chart
|
||||
ind_chart.setXLink(self.chart)
|
||||
|
@ -245,8 +264,6 @@ _min_points_to_show = 20
|
|||
_min_bars_in_view = 10
|
||||
|
||||
|
||||
# TODO: This is a sub-class of ``GracphicView`` which can
|
||||
# take a ``background`` color setting.
|
||||
class ChartPlotWidget(pg.PlotWidget):
|
||||
"""``GraphicsView`` subtype containing a single ``PlotItem``.
|
||||
|
||||
|
@ -260,6 +277,9 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
sig_mouse_leave = QtCore.Signal(object)
|
||||
sig_mouse_enter = QtCore.Signal(object)
|
||||
|
||||
# TODO: can take a ``background`` color setting - maybe there's
|
||||
# a better one?
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
split_charts,
|
||||
|
@ -319,15 +339,18 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
def draw_ohlc(
|
||||
self,
|
||||
data: np.ndarray,
|
||||
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
|
||||
self.addItem(_get_chart_points(style))
|
||||
graphics.draw_from_data(data)
|
||||
self.addItem(graphics)
|
||||
|
||||
def draw_curve(
|
||||
self,
|
||||
|
@ -422,9 +445,9 @@ class ChartView(pg.ViewBox):
|
|||
else:
|
||||
mask = self.state['mouseEnabled'][:]
|
||||
|
||||
# don't zoom more then the min points setting
|
||||
lbar, rbar = self.splitter_widget.chart.bars_range()
|
||||
if ev.delta() >= 0 and rbar - lbar <= _min_points_to_show:
|
||||
# don't zoom more then the min points setting
|
||||
return
|
||||
|
||||
# actual scaling factor
|
||||
|
@ -447,14 +470,3 @@ class ChartView(pg.ViewBox):
|
|||
self.scaleBy(s, center)
|
||||
ev.accept()
|
||||
self.sigRangeChangedManually.emit(mask)
|
||||
|
||||
|
||||
# this function is borderline ridiculous.
|
||||
# The creation of these chart types mutates all the input data
|
||||
# inside each type's constructor (mind blown)
|
||||
def _get_chart_points(style):
|
||||
if style == ChartType.CANDLESTICK:
|
||||
return CandlestickItem()
|
||||
elif style == ChartType.BAR:
|
||||
return BarItem()
|
||||
return pg.PlotDataItem(Quotes.close, pen='b')
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
"""
|
||||
Chart graphics for displaying a slew of different data types.
|
||||
"""
|
||||
from enum import Enum
|
||||
from contextlib import contextmanager
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from PyQt5 import QtCore, QtGui
|
||||
from PyQt5.QtCore import QLineF
|
||||
|
||||
from .quantdom.utils import timeit
|
||||
from .quantdom.base import Quotes
|
||||
|
||||
from ._style import _xaxis_at
|
||||
from ._style import _xaxis_at, _tina_mode
|
||||
from ._axes import YAxisLabel, XAxisLabel
|
||||
|
||||
|
||||
|
@ -163,11 +167,10 @@ class CrossHairItem(pg.GraphicsObject):
|
|||
return self.parent.boundingRect()
|
||||
|
||||
|
||||
class BarItem(pg.GraphicsObject):
|
||||
# XXX: From the customGraphicsItem.py example:
|
||||
# The only required methods are paint() and boundingRect()
|
||||
|
||||
w = 0.5
|
||||
class BarItems(pg.GraphicsObject):
|
||||
"""Price range bars graphics rendered from a OHLC sequence.
|
||||
"""
|
||||
w: float = 0.5
|
||||
|
||||
bull_brush = bear_brush = pg.mkPen('#808080')
|
||||
# bull_brush = pg.mkPen('#00cc00')
|
||||
|
@ -175,44 +178,72 @@ class BarItem(pg.GraphicsObject):
|
|||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.generatePicture()
|
||||
self.picture = QtGui.QPicture()
|
||||
self.lines = None
|
||||
# self.generatePicture()
|
||||
|
||||
# TODO: this is the routine to be retriggered for redraw
|
||||
@timeit
|
||||
def generatePicture(self):
|
||||
@contextmanager
|
||||
def painter(self):
|
||||
# pre-computing a QPicture object allows paint() to run much
|
||||
# more quickly, rather than re-drawing the shapes every time.
|
||||
self.picture = QtGui.QPicture()
|
||||
p = QtGui.QPainter(self.picture)
|
||||
self._generate(p)
|
||||
yield p
|
||||
p.end()
|
||||
|
||||
def _generate(self, p):
|
||||
@timeit
|
||||
def draw_from_data(self, data):
|
||||
# XXX: overloaded method to allow drawing other candle types
|
||||
|
||||
high_to_low = np.array(
|
||||
[QtCore.QLineF(q.id, q.low, q.id, q.high) for q in Quotes]
|
||||
)
|
||||
open_stick = np.array(
|
||||
[QtCore.QLineF(q.id - self.w, q.open, q.id, q.open)
|
||||
for q in Quotes]
|
||||
)
|
||||
close_stick = np.array(
|
||||
[
|
||||
QtCore.QLineF(q.id + self.w, q.close, q.id, q.close)
|
||||
for q in Quotes
|
||||
]
|
||||
)
|
||||
lines = np.concatenate([high_to_low, open_stick, close_stick])
|
||||
long_bars = np.resize(Quotes.close > Quotes.open, len(lines))
|
||||
short_bars = np.resize(Quotes.close < Quotes.open, len(lines))
|
||||
high_to_low = np.empty_like(data, dtype=object)
|
||||
open_sticks = np.empty_like(data, dtype=object)
|
||||
close_sticks = np.empty_like(data, dtype=object)
|
||||
with self.painter() as p:
|
||||
import time
|
||||
start = time.time()
|
||||
for i, q in enumerate(data):
|
||||
high_to_low[i] = QLineF(q['id'], q['low'], q['id'], q['high'])
|
||||
open_sticks[i] = QLineF(
|
||||
q['id'] - self.w, q['open'], q['id'], q['open'])
|
||||
close_sticks[i] = QtCore.QLineF(
|
||||
q['id'] + self.w, q['close'], q['id'], q['close'])
|
||||
|
||||
p.setPen(self.bull_brush)
|
||||
p.drawLines(*lines[long_bars])
|
||||
# high_to_low = np.array(
|
||||
# [QtCore.QLineF(q.id, q.low, q.id, q.high) for q in Quotes]
|
||||
# )
|
||||
# open_sticks = np.array(
|
||||
# [QtCore.QLineF(q.id - self.w, q.open, q.id, q.open)
|
||||
# for q in Quotes]
|
||||
# )
|
||||
# close_sticks = np.array(
|
||||
# [
|
||||
# QtCore.QLineF(q.id + self.w, q.close, q.id, q.close)
|
||||
# for q in Quotes
|
||||
# ]
|
||||
# )
|
||||
print(f"took {time.time() - start}")
|
||||
self.lines = lines = np.concatenate([high_to_low, open_sticks, close_sticks])
|
||||
|
||||
p.setPen(self.bear_brush)
|
||||
p.drawLines(*lines[short_bars])
|
||||
if _tina_mode:
|
||||
long_bars = np.resize(Quotes.close > Quotes.open, len(lines))
|
||||
short_bars = np.resize(Quotes.close < Quotes.open, len(lines))
|
||||
ups = lines[long_bars]
|
||||
downs = lines[short_bars]
|
||||
|
||||
# draw "up" bars
|
||||
p.setPen(self.bull_brush)
|
||||
p.drawLines(*ups)
|
||||
|
||||
# draw "down" bars
|
||||
p.setPen(self.bear_brush)
|
||||
p.drawLines(*downs)
|
||||
|
||||
else: # piker mode
|
||||
p.setPen(self.bull_brush)
|
||||
p.drawLines(*lines)
|
||||
|
||||
# XXX: From the customGraphicsItem.py example:
|
||||
# The only required methods are paint() and boundingRect()
|
||||
def paint(self, p, *args):
|
||||
p.drawPicture(0, 0, self.picture)
|
||||
|
||||
|
@ -224,7 +255,7 @@ class BarItem(pg.GraphicsObject):
|
|||
return QtCore.QRectF(self.picture.boundingRect())
|
||||
|
||||
|
||||
class CandlestickItem(BarItem):
|
||||
class CandlestickItems(BarItems):
|
||||
|
||||
w2 = 0.7
|
||||
line_pen = pg.mkPen('#000000')
|
||||
|
@ -250,3 +281,11 @@ class CandlestickItem(BarItem):
|
|||
|
||||
p.setBrush(self.bear_brush)
|
||||
p.drawRects(*rects[Quotes.close < Quotes.open])
|
||||
|
||||
|
||||
class ChartType(Enum):
|
||||
"""Bar type to graphics class map.
|
||||
"""
|
||||
BAR = BarItems
|
||||
CANDLESTICK = CandlestickItems
|
||||
LINE = pg.PlotDataItem
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
"""
|
||||
Numpy data source machinery.
|
||||
"""
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
OHLC_dtype = np.dtype(
|
||||
[
|
||||
('id', int),
|
||||
('time', float),
|
||||
('open', float),
|
||||
('high', float),
|
||||
('low', float),
|
||||
('close', float),
|
||||
('volume', int),
|
||||
]
|
||||
)
|
||||
|
||||
# tf = {
|
||||
# 1: TimeFrame.M1,
|
||||
# 5: TimeFrame.M5,
|
||||
# 15: TimeFrame.M15,
|
||||
# 30: TimeFrame.M30,
|
||||
# 60: TimeFrame.H1,
|
||||
# 240: TimeFrame.H4,
|
||||
# 1440: TimeFrame.D1,
|
||||
# }
|
||||
|
||||
|
||||
@dataclass
|
||||
class Symbol:
|
||||
"""I guess this is some kinda container thing for dealing with
|
||||
all the different meta-data formats from brokers?
|
||||
"""
|
||||
key: str = ''
|
||||
min_tick: float = 0.01
|
||||
contract: str = ''
|
||||
|
||||
def digits(self) -> int:
|
||||
"""Return the trailing number of digits specified by the
|
||||
min tick size for the instrument.
|
||||
"""
|
||||
return int(math.log(self.min_tick, 0.1))
|
||||
|
||||
|
||||
def from_df(
|
||||
df: pd.DataFrame,
|
||||
source=None,
|
||||
default_tf=None
|
||||
):
|
||||
"""Cast OHLC ``pandas.DataFrame`` to ``numpy.structarray``.
|
||||
"""
|
||||
# shape = (len(df),)
|
||||
# array.resize(shape, refcheck=False)
|
||||
array = np.array([], dtype=OHLC_dtype)
|
||||
|
||||
df.reset_index(inplace=True)
|
||||
df.insert(0, 'id', df.index)
|
||||
array['time'] = np.array([d.timestamp().time for d in df.Date])
|
||||
|
||||
# try to rename from some camel case
|
||||
df = df.rename(
|
||||
columns={
|
||||
# 'Date': 'time',
|
||||
'Open': 'open',
|
||||
'High': 'high',
|
||||
'Low': 'low',
|
||||
'Close': 'close',
|
||||
'Volume': 'volume',
|
||||
}
|
||||
)
|
||||
for name in array.dtype.names:
|
||||
array[name] = df[name]
|
||||
|
||||
array[:] = df[:]
|
||||
|
||||
_nan_to_closest_num(array)
|
||||
# self._set_time_frame(default_tf)
|
||||
|
||||
return array
|
||||
|
||||
|
||||
def _nan_to_closest_num(array: np.ndarray):
|
||||
"""Return interpolated values instead of NaN.
|
||||
"""
|
||||
|
||||
for col in ['open', 'high', 'low', 'close']:
|
||||
mask = np.isnan(array[col])
|
||||
if not mask.size:
|
||||
continue
|
||||
array[col][mask] = np.interp(
|
||||
np.flatnonzero(mask), np.flatnonzero(~mask), array[col][~mask]
|
||||
)
|
|
@ -4,10 +4,6 @@ Qt styling.
|
|||
from PyQt5 import QtGui
|
||||
|
||||
|
||||
# TODO: add "tina mode" to make everything look "conventional"
|
||||
# white background (for tinas like our pal xb)
|
||||
# pg.setConfigOption('background', 'w')
|
||||
|
||||
|
||||
# chart-wide font
|
||||
_font = QtGui.QFont("Hack", 4)
|
||||
|
@ -16,3 +12,17 @@ _i3_rgba = QtGui.QColor.fromRgbF(*[0.14]*3 + [1])
|
|||
|
||||
# splitter widget config
|
||||
_xaxis_at = 'bottom'
|
||||
|
||||
|
||||
_tina_mode = False
|
||||
|
||||
|
||||
def enable_tina_mode() -> None:
|
||||
"""Enable "tina mode" to make everything look "conventional"
|
||||
like your pet hedgehog always wanted.
|
||||
"""
|
||||
|
||||
_tina_mode = True
|
||||
|
||||
# white background (for tinas like our pal xb)
|
||||
pg.setConfigOption('background', 'w')
|
||||
|
|
Loading…
Reference in New Issue