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
|
"""Start an option chain UI
|
||||||
"""
|
"""
|
||||||
from .qt._exec import run_qtrio
|
from .qt._exec import run_qtrio
|
||||||
from .qt._chart import QuotesTabWidget
|
from .qt._chart import Chart
|
||||||
from .qt.quantdom.base import Symbol
|
|
||||||
|
# uses pandas_datareader
|
||||||
from .qt.quantdom.loaders import get_quotes
|
from .qt.quantdom.loaders import get_quotes
|
||||||
|
|
||||||
async def plot_symbol(widgets):
|
async def plot_symbol(widgets):
|
||||||
|
"""Main Qt-trio routine invoked by the Qt loop with
|
||||||
|
the widgets ``dict``.
|
||||||
|
"""
|
||||||
|
|
||||||
qtw = widgets['main']
|
qtw = widgets['main']
|
||||||
s = Symbol(ticker=symbol, mode=Symbol.SHARES)
|
quotes = get_quotes(
|
||||||
get_quotes(
|
symbol=symbol,
|
||||||
symbol=s.ticker,
|
|
||||||
date_from=datetime(1900, 1, 1),
|
date_from=datetime(1900, 1, 1),
|
||||||
date_to=datetime(2030, 12, 31),
|
date_to=datetime(2030, 12, 31),
|
||||||
)
|
)
|
||||||
# spawn chart
|
# spawn chart
|
||||||
qtw.update_chart(s)
|
qtw.load_symbol(symbol, quotes)
|
||||||
await trio.sleep_forever()
|
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')
|
bg_color = pg.mkColor('#808080')
|
||||||
fg_color = pg.mkColor('#000000')
|
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)
|
super().__init__(parent)
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.opacity = opacity
|
self.opacity = opacity
|
||||||
|
|
|
@ -10,12 +10,13 @@ from ._axes import (
|
||||||
FromTimeFieldDateAxis,
|
FromTimeFieldDateAxis,
|
||||||
PriceAxis,
|
PriceAxis,
|
||||||
)
|
)
|
||||||
from ._graphics import CrossHairItem, CandlestickItem, BarItem
|
from ._graphics import CrossHairItem, ChartType
|
||||||
from ._style import _xaxis_at
|
from ._style import _xaxis_at
|
||||||
|
from ._source import Symbol
|
||||||
|
|
||||||
from .quantdom.charts import CenteredTextItem
|
from .quantdom.charts import CenteredTextItem
|
||||||
from .quantdom.base import Quotes
|
from .quantdom.base import Quotes
|
||||||
from .quantdom.const import ChartType
|
# from .quantdom.const import ChartType
|
||||||
from .quantdom.portfolio import Order, Portfolio
|
from .quantdom.portfolio import Order, Portfolio
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,20 +24,21 @@ from .quantdom.portfolio import Order, Portfolio
|
||||||
CHART_MARGINS = (0, 0, 10, 3)
|
CHART_MARGINS = (0, 0, 10, 3)
|
||||||
|
|
||||||
|
|
||||||
class QuotesTabWidget(QtGui.QWidget):
|
class Chart(QtGui.QWidget):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.layout = QtGui.QVBoxLayout(self)
|
self.v_layout = QtGui.QVBoxLayout(self)
|
||||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
self.v_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
self.toolbar_layout = QtGui.QHBoxLayout()
|
self.toolbar_layout = QtGui.QHBoxLayout()
|
||||||
self.toolbar_layout.setContentsMargins(10, 10, 15, 0)
|
self.toolbar_layout.setContentsMargins(10, 10, 15, 0)
|
||||||
self.chart_layout = QtGui.QHBoxLayout()
|
self.h_layout = QtGui.QHBoxLayout()
|
||||||
|
|
||||||
# self.init_timeframes_ui()
|
# self.init_timeframes_ui()
|
||||||
# self.init_strategy_ui()
|
# self.init_strategy_ui()
|
||||||
|
|
||||||
self.layout.addLayout(self.toolbar_layout)
|
self.v_layout.addLayout(self.toolbar_layout)
|
||||||
self.layout.addLayout(self.chart_layout)
|
self.v_layout.addLayout(self.h_layout)
|
||||||
|
self._plot_cache = {}
|
||||||
|
|
||||||
def init_timeframes_ui(self):
|
def init_timeframes_ui(self):
|
||||||
self.tf_layout = QtGui.QHBoxLayout()
|
self.tf_layout = QtGui.QHBoxLayout()
|
||||||
|
@ -58,21 +60,32 @@ class QuotesTabWidget(QtGui.QWidget):
|
||||||
# self.strategy_box = StrategyBoxWidget(self)
|
# self.strategy_box = StrategyBoxWidget(self)
|
||||||
# self.toolbar_layout.addWidget(self.strategy_box)
|
# self.toolbar_layout.addWidget(self.strategy_box)
|
||||||
|
|
||||||
# TODO: this needs to be changed to ``load_symbol()``
|
def load_symbol(
|
||||||
# which will not only load historical data but also a real-time
|
self,
|
||||||
# stream and schedule the redraw events on new quotes
|
symbol: str,
|
||||||
def update_chart(self, symbol):
|
data: np.ndarray,
|
||||||
if not self.chart_layout.isEmpty():
|
) -> None:
|
||||||
self.chart_layout.removeWidget(self.chart)
|
"""Load a new contract into the charting app.
|
||||||
self.chart = SplitterChart()
|
"""
|
||||||
self.chart.plot(symbol)
|
# XXX: let's see if this causes mem problems
|
||||||
self.chart_layout.addWidget(self.chart)
|
self.chart = self._plot_cache.setdefault(symbol, SplitterPlots())
|
||||||
|
s = Symbol(key=symbol)
|
||||||
|
|
||||||
def add_signals(self):
|
# remove any existing plots
|
||||||
self.chart.add_signals()
|
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_pen = pg.mkPen('#006000')
|
||||||
long_brush = pg.mkBrush('#00ff00')
|
long_brush = pg.mkBrush('#00ff00')
|
||||||
|
@ -131,16 +144,21 @@ class SplitterChart(QtGui.QWidget):
|
||||||
sizes.extend([min_h_ind] * len(self.indicators))
|
sizes.extend([min_h_ind] * len(self.indicators))
|
||||||
self.splitter.setSizes(sizes) # , int(self.height()*0.2)
|
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.
|
"""Start up and show price chart and all registered indicators.
|
||||||
"""
|
"""
|
||||||
self.digits = symbol.digits
|
self.digits = symbol.digits()
|
||||||
|
|
||||||
|
cv = ChartView()
|
||||||
self.chart = ChartPlotWidget(
|
self.chart = ChartPlotWidget(
|
||||||
split_charts=self,
|
split_charts=self,
|
||||||
parent=self.splitter,
|
parent=self.splitter,
|
||||||
axisItems={'bottom': self.xaxis, 'right': PriceAxis()},
|
axisItems={'bottom': self.xaxis, 'right': PriceAxis()},
|
||||||
viewBox=ChartView,
|
viewBox=cv,
|
||||||
# enableMenu=False,
|
# enableMenu=False,
|
||||||
)
|
)
|
||||||
# TODO: ``pyqtgraph`` doesn't pass through a parent to the
|
# 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.getPlotItem().setContentsMargins(*CHART_MARGINS)
|
||||||
self.chart.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
self.chart.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
||||||
|
|
||||||
|
self.chart.draw_ohlc(data)
|
||||||
|
|
||||||
# TODO: this is where we would load an indicator chain
|
# TODO: this is where we would load an indicator chain
|
||||||
inds = [Quotes.open]
|
inds = [Quotes.open]
|
||||||
|
|
||||||
for d in inds:
|
for d in inds:
|
||||||
ind = ChartPlotWidget(
|
cv = ChartView()
|
||||||
|
ind_chart = ChartPlotWidget(
|
||||||
split_charts=self,
|
split_charts=self,
|
||||||
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()},
|
||||||
viewBox=ChartView,
|
viewBox=cv,
|
||||||
)
|
)
|
||||||
self.chart.plotItem.vb.splitter_widget = self
|
self.chart.plotItem.vb.splitter_widget = self
|
||||||
|
|
||||||
ind.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
ind_chart.setFrameStyle(
|
||||||
ind.getPlotItem().setContentsMargins(*CHART_MARGINS)
|
QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain
|
||||||
# self.splitter.addWidget(ind)
|
)
|
||||||
self.indicators.append((ind, d))
|
ind_chart.getPlotItem().setContentsMargins(*CHART_MARGINS)
|
||||||
|
# self.splitter.addWidget(ind_chart)
|
||||||
self.chart.draw_ohlc()
|
self.indicators.append((ind_chart, d))
|
||||||
|
|
||||||
for ind_chart, d in self.indicators:
|
|
||||||
|
|
||||||
# link chart x-axis to main quotes chart
|
# link chart x-axis to main quotes chart
|
||||||
ind_chart.setXLink(self.chart)
|
ind_chart.setXLink(self.chart)
|
||||||
|
@ -245,8 +264,6 @@ _min_points_to_show = 20
|
||||||
_min_bars_in_view = 10
|
_min_bars_in_view = 10
|
||||||
|
|
||||||
|
|
||||||
# TODO: This is a sub-class of ``GracphicView`` which can
|
|
||||||
# take a ``background`` color setting.
|
|
||||||
class ChartPlotWidget(pg.PlotWidget):
|
class ChartPlotWidget(pg.PlotWidget):
|
||||||
"""``GraphicsView`` subtype containing a single ``PlotItem``.
|
"""``GraphicsView`` subtype containing a single ``PlotItem``.
|
||||||
|
|
||||||
|
@ -260,6 +277,9 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
sig_mouse_leave = QtCore.Signal(object)
|
sig_mouse_leave = QtCore.Signal(object)
|
||||||
sig_mouse_enter = QtCore.Signal(object)
|
sig_mouse_enter = QtCore.Signal(object)
|
||||||
|
|
||||||
|
# TODO: can take a ``background`` color setting - maybe there's
|
||||||
|
# a better one?
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
split_charts,
|
split_charts,
|
||||||
|
@ -319,15 +339,18 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
def draw_ohlc(
|
def draw_ohlc(
|
||||||
self,
|
self,
|
||||||
|
data: np.ndarray,
|
||||||
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..
|
||||||
|
graphics = style.value()
|
||||||
# adds all bar/candle graphics objects for each
|
# adds all bar/candle graphics objects for each
|
||||||
# data point in the np array buffer to
|
# data point in the np array buffer to
|
||||||
# be drawn on next render cycle
|
# be drawn on next render cycle
|
||||||
self.addItem(_get_chart_points(style))
|
graphics.draw_from_data(data)
|
||||||
|
self.addItem(graphics)
|
||||||
|
|
||||||
def draw_curve(
|
def draw_curve(
|
||||||
self,
|
self,
|
||||||
|
@ -422,9 +445,9 @@ class ChartView(pg.ViewBox):
|
||||||
else:
|
else:
|
||||||
mask = self.state['mouseEnabled'][:]
|
mask = self.state['mouseEnabled'][:]
|
||||||
|
|
||||||
|
# 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()
|
||||||
if ev.delta() >= 0 and rbar - lbar <= _min_points_to_show:
|
if ev.delta() >= 0 and rbar - lbar <= _min_points_to_show:
|
||||||
# don't zoom more then the min points setting
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# actual scaling factor
|
# actual scaling factor
|
||||||
|
@ -447,14 +470,3 @@ class ChartView(pg.ViewBox):
|
||||||
self.scaleBy(s, center)
|
self.scaleBy(s, center)
|
||||||
ev.accept()
|
ev.accept()
|
||||||
self.sigRangeChangedManually.emit(mask)
|
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.
|
Chart graphics for displaying a slew of different data types.
|
||||||
"""
|
"""
|
||||||
|
from enum import Enum
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from PyQt5 import QtCore, QtGui
|
from PyQt5 import QtCore, QtGui
|
||||||
|
from PyQt5.QtCore import QLineF
|
||||||
|
|
||||||
from .quantdom.utils import timeit
|
from .quantdom.utils import timeit
|
||||||
from .quantdom.base import Quotes
|
from .quantdom.base import Quotes
|
||||||
|
|
||||||
from ._style import _xaxis_at
|
from ._style import _xaxis_at, _tina_mode
|
||||||
from ._axes import YAxisLabel, XAxisLabel
|
from ._axes import YAxisLabel, XAxisLabel
|
||||||
|
|
||||||
|
|
||||||
|
@ -163,11 +167,10 @@ class CrossHairItem(pg.GraphicsObject):
|
||||||
return self.parent.boundingRect()
|
return self.parent.boundingRect()
|
||||||
|
|
||||||
|
|
||||||
class BarItem(pg.GraphicsObject):
|
class BarItems(pg.GraphicsObject):
|
||||||
# XXX: From the customGraphicsItem.py example:
|
"""Price range bars graphics rendered from a OHLC sequence.
|
||||||
# The only required methods are paint() and boundingRect()
|
"""
|
||||||
|
w: float = 0.5
|
||||||
w = 0.5
|
|
||||||
|
|
||||||
bull_brush = bear_brush = pg.mkPen('#808080')
|
bull_brush = bear_brush = pg.mkPen('#808080')
|
||||||
# bull_brush = pg.mkPen('#00cc00')
|
# bull_brush = pg.mkPen('#00cc00')
|
||||||
|
@ -175,44 +178,72 @@ class BarItem(pg.GraphicsObject):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.generatePicture()
|
self.picture = QtGui.QPicture()
|
||||||
|
self.lines = None
|
||||||
|
# self.generatePicture()
|
||||||
|
|
||||||
# TODO: this is the routine to be retriggered for redraw
|
# TODO: this is the routine to be retriggered for redraw
|
||||||
@timeit
|
@contextmanager
|
||||||
def generatePicture(self):
|
def painter(self):
|
||||||
# pre-computing a QPicture object allows paint() to run much
|
# pre-computing a QPicture object allows paint() to run much
|
||||||
# more quickly, rather than re-drawing the shapes every time.
|
# more quickly, rather than re-drawing the shapes every time.
|
||||||
self.picture = QtGui.QPicture()
|
|
||||||
p = QtGui.QPainter(self.picture)
|
p = QtGui.QPainter(self.picture)
|
||||||
self._generate(p)
|
yield p
|
||||||
p.end()
|
p.end()
|
||||||
|
|
||||||
def _generate(self, p):
|
@timeit
|
||||||
|
def draw_from_data(self, data):
|
||||||
# XXX: overloaded method to allow drawing other candle types
|
# XXX: overloaded method to allow drawing other candle types
|
||||||
|
|
||||||
high_to_low = np.array(
|
high_to_low = np.empty_like(data, dtype=object)
|
||||||
[QtCore.QLineF(q.id, q.low, q.id, q.high) for q in Quotes]
|
open_sticks = np.empty_like(data, dtype=object)
|
||||||
)
|
close_sticks = np.empty_like(data, dtype=object)
|
||||||
open_stick = np.array(
|
with self.painter() as p:
|
||||||
[QtCore.QLineF(q.id - self.w, q.open, q.id, q.open)
|
import time
|
||||||
for q in Quotes]
|
start = time.time()
|
||||||
)
|
for i, q in enumerate(data):
|
||||||
close_stick = np.array(
|
high_to_low[i] = QLineF(q['id'], q['low'], q['id'], q['high'])
|
||||||
[
|
open_sticks[i] = QLineF(
|
||||||
QtCore.QLineF(q.id + self.w, q.close, q.id, q.close)
|
q['id'] - self.w, q['open'], q['id'], q['open'])
|
||||||
for q in Quotes
|
close_sticks[i] = QtCore.QLineF(
|
||||||
]
|
q['id'] + self.w, q['close'], q['id'], q['close'])
|
||||||
)
|
|
||||||
lines = np.concatenate([high_to_low, open_stick, close_stick])
|
# 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])
|
||||||
|
|
||||||
|
if _tina_mode:
|
||||||
long_bars = np.resize(Quotes.close > Quotes.open, len(lines))
|
long_bars = np.resize(Quotes.close > Quotes.open, len(lines))
|
||||||
short_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.setPen(self.bull_brush)
|
||||||
p.drawLines(*lines[long_bars])
|
p.drawLines(*ups)
|
||||||
|
|
||||||
|
# draw "down" bars
|
||||||
p.setPen(self.bear_brush)
|
p.setPen(self.bear_brush)
|
||||||
p.drawLines(*lines[short_bars])
|
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):
|
def paint(self, p, *args):
|
||||||
p.drawPicture(0, 0, self.picture)
|
p.drawPicture(0, 0, self.picture)
|
||||||
|
|
||||||
|
@ -224,7 +255,7 @@ class BarItem(pg.GraphicsObject):
|
||||||
return QtCore.QRectF(self.picture.boundingRect())
|
return QtCore.QRectF(self.picture.boundingRect())
|
||||||
|
|
||||||
|
|
||||||
class CandlestickItem(BarItem):
|
class CandlestickItems(BarItems):
|
||||||
|
|
||||||
w2 = 0.7
|
w2 = 0.7
|
||||||
line_pen = pg.mkPen('#000000')
|
line_pen = pg.mkPen('#000000')
|
||||||
|
@ -250,3 +281,11 @@ class CandlestickItem(BarItem):
|
||||||
|
|
||||||
p.setBrush(self.bear_brush)
|
p.setBrush(self.bear_brush)
|
||||||
p.drawRects(*rects[Quotes.close < Quotes.open])
|
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
|
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
|
# chart-wide font
|
||||||
_font = QtGui.QFont("Hack", 4)
|
_font = QtGui.QFont("Hack", 4)
|
||||||
|
@ -16,3 +12,17 @@ _i3_rgba = QtGui.QColor.fromRgbF(*[0.14]*3 + [1])
|
||||||
|
|
||||||
# splitter widget config
|
# splitter widget config
|
||||||
_xaxis_at = 'bottom'
|
_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