Styling, start re-org, commenting
- Move out equity plotting to new module. - Make axis margins and fonts look good on i3. - Adjust axis labels colors to gray. - Start commenting a lot of the code after figuring out what it all does when cross referencing with ``pyqtgraph``. - Add option to move date axis to middle.its_happening
parent
49949ae6d5
commit
9d4a432757
|
@ -32,7 +32,6 @@ def run_qtrio(
|
||||||
args,
|
args,
|
||||||
main_widget,
|
main_widget,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
# avoids annoying message when entering debugger from qt loop
|
# avoids annoying message when entering debugger from qt loop
|
||||||
pyqtRemoveInputHook()
|
pyqtRemoveInputHook()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
"""
|
||||||
|
Strategy and performance charting
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import pyqtgraph as pg
|
||||||
|
from PyQt5 import QtCore, QtGui
|
||||||
|
|
||||||
|
from .base import Quotes
|
||||||
|
from .portfolio import Portfolio
|
||||||
|
from .utils import timeit
|
||||||
|
from .charts import (
|
||||||
|
PriceAxis,
|
||||||
|
CHART_MARGINS,
|
||||||
|
SampleLegendItem,
|
||||||
|
YAxisLabel,
|
||||||
|
CrossHairItem,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EquityChart(QtGui.QWidget):
|
||||||
|
|
||||||
|
eq_pen_pos_color = pg.mkColor('#00cc00')
|
||||||
|
eq_pen_neg_color = pg.mkColor('#cc0000')
|
||||||
|
eq_brush_pos_color = pg.mkColor('#40ee40')
|
||||||
|
eq_brush_neg_color = pg.mkColor('#ee4040')
|
||||||
|
long_pen_color = pg.mkColor('#008000')
|
||||||
|
short_pen_color = pg.mkColor('#800000')
|
||||||
|
buy_and_hold_pen_color = pg.mkColor('#4444ff')
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.xaxis = pg.DateAxisItem()
|
||||||
|
self.xaxis.setStyle(tickTextOffset=7, textFillLimits=[(0, 0.80)])
|
||||||
|
self.yaxis = PriceAxis()
|
||||||
|
|
||||||
|
self.layout = QtGui.QVBoxLayout(self)
|
||||||
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
self.chart = pg.PlotWidget(
|
||||||
|
axisItems={'bottom': self.xaxis, 'right': self.yaxis},
|
||||||
|
enableMenu=False,
|
||||||
|
)
|
||||||
|
self.chart.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
||||||
|
self.chart.getPlotItem().setContentsMargins(*CHART_MARGINS)
|
||||||
|
self.chart.showGrid(x=True, y=True)
|
||||||
|
self.chart.hideAxis('left')
|
||||||
|
self.chart.showAxis('right')
|
||||||
|
|
||||||
|
self.chart.setCursor(QtCore.Qt.BlankCursor)
|
||||||
|
self.chart.sigXRangeChanged.connect(self._update_yrange_limits)
|
||||||
|
|
||||||
|
self.layout.addWidget(self.chart)
|
||||||
|
|
||||||
|
def _add_legend(self):
|
||||||
|
legend = pg.LegendItem((140, 100), offset=(10, 10))
|
||||||
|
legend.setParentItem(self.chart.getPlotItem())
|
||||||
|
|
||||||
|
for arr, item in self.curves:
|
||||||
|
legend.addItem(
|
||||||
|
SampleLegendItem(item),
|
||||||
|
item.opts['name']
|
||||||
|
if not isinstance(item, tuple)
|
||||||
|
else item[0].opts['name'],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _add_ylabels(self):
|
||||||
|
self.ylabels = []
|
||||||
|
for arr, item in self.curves:
|
||||||
|
color = (
|
||||||
|
item.opts['pen']
|
||||||
|
if not isinstance(item, tuple)
|
||||||
|
else [i.opts['pen'] for i in item]
|
||||||
|
)
|
||||||
|
label = YAxisLabel(parent=self.yaxis, color=color)
|
||||||
|
self.ylabels.append(label)
|
||||||
|
|
||||||
|
def _update_ylabels(self, vb, rbar):
|
||||||
|
for i, curve in enumerate(self.curves):
|
||||||
|
arr, item = curve
|
||||||
|
ylast = arr[rbar]
|
||||||
|
ypos = vb.mapFromView(QtCore.QPointF(0, ylast)).y()
|
||||||
|
axlabel = self.ylabels[i]
|
||||||
|
axlabel.update_label_test(ypos=ypos, ydata=ylast)
|
||||||
|
|
||||||
|
def _update_yrange_limits(self, vb=None):
|
||||||
|
if not hasattr(self, 'min_curve'):
|
||||||
|
return
|
||||||
|
vr = self.chart.viewRect()
|
||||||
|
lbar, rbar = int(vr.left()), int(vr.right())
|
||||||
|
ylow = self.min_curve[lbar:rbar].min() * 1.1
|
||||||
|
yhigh = self.max_curve[lbar:rbar].max() * 1.1
|
||||||
|
|
||||||
|
std = np.std(self.max_curve[lbar:rbar]) * 4
|
||||||
|
self.chart.setLimits(yMin=ylow, yMax=yhigh, minYRange=std)
|
||||||
|
self.chart.setYRange(ylow, yhigh)
|
||||||
|
self._update_ylabels(vb, rbar)
|
||||||
|
|
||||||
|
@timeit
|
||||||
|
def plot(self):
|
||||||
|
equity_curve = Portfolio.equity_curve
|
||||||
|
eq_pos = np.zeros_like(equity_curve)
|
||||||
|
eq_neg = np.zeros_like(equity_curve)
|
||||||
|
eq_pos[equity_curve >= 0] = equity_curve[equity_curve >= 0]
|
||||||
|
eq_neg[equity_curve <= 0] = equity_curve[equity_curve <= 0]
|
||||||
|
|
||||||
|
# Equity
|
||||||
|
self.eq_pos_curve = pg.PlotCurveItem(
|
||||||
|
eq_pos,
|
||||||
|
name='Equity',
|
||||||
|
fillLevel=0,
|
||||||
|
antialias=True,
|
||||||
|
pen=self.eq_pen_pos_color,
|
||||||
|
brush=self.eq_brush_pos_color,
|
||||||
|
)
|
||||||
|
self.eq_neg_curve = pg.PlotCurveItem(
|
||||||
|
eq_neg,
|
||||||
|
name='Equity',
|
||||||
|
fillLevel=0,
|
||||||
|
antialias=True,
|
||||||
|
pen=self.eq_pen_neg_color,
|
||||||
|
brush=self.eq_brush_neg_color,
|
||||||
|
)
|
||||||
|
self.chart.addItem(self.eq_pos_curve)
|
||||||
|
self.chart.addItem(self.eq_neg_curve)
|
||||||
|
|
||||||
|
# Only Long
|
||||||
|
self.long_curve = pg.PlotCurveItem(
|
||||||
|
Portfolio.long_curve,
|
||||||
|
name='Only Long',
|
||||||
|
pen=self.long_pen_color,
|
||||||
|
antialias=True,
|
||||||
|
)
|
||||||
|
self.chart.addItem(self.long_curve)
|
||||||
|
|
||||||
|
# Only Short
|
||||||
|
self.short_curve = pg.PlotCurveItem(
|
||||||
|
Portfolio.short_curve,
|
||||||
|
name='Only Short',
|
||||||
|
pen=self.short_pen_color,
|
||||||
|
antialias=True,
|
||||||
|
)
|
||||||
|
self.chart.addItem(self.short_curve)
|
||||||
|
|
||||||
|
# Buy and Hold
|
||||||
|
self.buy_and_hold_curve = pg.PlotCurveItem(
|
||||||
|
Portfolio.buy_and_hold_curve,
|
||||||
|
name='Buy and Hold',
|
||||||
|
pen=self.buy_and_hold_pen_color,
|
||||||
|
antialias=True,
|
||||||
|
)
|
||||||
|
self.chart.addItem(self.buy_and_hold_curve)
|
||||||
|
|
||||||
|
self.curves = [
|
||||||
|
(Portfolio.equity_curve, (self.eq_pos_curve, self.eq_neg_curve)),
|
||||||
|
(Portfolio.long_curve, self.long_curve),
|
||||||
|
(Portfolio.short_curve, self.short_curve),
|
||||||
|
(Portfolio.buy_and_hold_curve, self.buy_and_hold_curve),
|
||||||
|
]
|
||||||
|
|
||||||
|
self._add_legend()
|
||||||
|
self._add_ylabels()
|
||||||
|
|
||||||
|
ch = CrossHairItem(self.chart)
|
||||||
|
self.chart.addItem(ch)
|
||||||
|
|
||||||
|
arrs = (
|
||||||
|
Portfolio.equity_curve,
|
||||||
|
Portfolio.buy_and_hold_curve,
|
||||||
|
Portfolio.long_curve,
|
||||||
|
Portfolio.short_curve,
|
||||||
|
)
|
||||||
|
np_arrs = np.concatenate(arrs)
|
||||||
|
_min = abs(np_arrs.min()) * -1.1
|
||||||
|
_max = np_arrs.max() * 1.1
|
||||||
|
|
||||||
|
self.chart.setLimits(
|
||||||
|
xMin=Quotes[0].id,
|
||||||
|
xMax=Quotes[-1].id,
|
||||||
|
yMin=_min,
|
||||||
|
yMax=_max,
|
||||||
|
minXRange=60,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.min_curve = arrs[0].copy()
|
||||||
|
self.max_curve = arrs[0].copy()
|
||||||
|
for arr in arrs[1:]:
|
||||||
|
self.min_curve = np.minimum(self.min_curve, arr)
|
||||||
|
self.max_curve = np.maximum(self.max_curve, arr)
|
|
@ -10,6 +10,16 @@ from .const import ChartType, TimeFrame
|
||||||
__all__ = ('Indicator', 'Symbol', 'Quotes')
|
__all__ = ('Indicator', 'Symbol', 'Quotes')
|
||||||
|
|
||||||
|
|
||||||
|
# I actually can't think of a worse reason to override an array than
|
||||||
|
# this:
|
||||||
|
# - a method .new() that mutates the data from an input data frame
|
||||||
|
# - mutating the time column wholesale based on a setting
|
||||||
|
# - enforcing certain fields / columns
|
||||||
|
# - zero overriding of any of the array interface for the purposes of
|
||||||
|
# a different underlying implementation.
|
||||||
|
|
||||||
|
# Literally all this can be done in a simple function with way less
|
||||||
|
# confusion for the reader.
|
||||||
class BaseQuotes(np.recarray):
|
class BaseQuotes(np.recarray):
|
||||||
def __new__(cls, shape=None, dtype=None, order='C'):
|
def __new__(cls, shape=None, dtype=None, order='C'):
|
||||||
dt = np.dtype(
|
dt = np.dtype(
|
||||||
|
@ -49,6 +59,8 @@ class BaseQuotes(np.recarray):
|
||||||
minutes = int(np.diff(self.time[-10:]).min() / 60)
|
minutes = int(np.diff(self.time[-10:]).min() / 60)
|
||||||
self.timeframe = tf.get(minutes) or tf[default_tf]
|
self.timeframe = tf.get(minutes) or tf[default_tf]
|
||||||
|
|
||||||
|
# bruh this isn't creating anything it's copying data in
|
||||||
|
# from a data frame...
|
||||||
def new(self, data, source=None, default_tf=None):
|
def new(self, data, source=None, default_tf=None):
|
||||||
shape = (len(data),)
|
shape = (len(data),)
|
||||||
self.resize(shape, refcheck=False)
|
self.resize(shape, refcheck=False)
|
||||||
|
@ -77,7 +89,7 @@ class BaseQuotes(np.recarray):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def convert_dates(self, dates):
|
def convert_dates(self, dates):
|
||||||
return np.array([d.timestamp() for d in dates])
|
return np.array([d.timestamp().time for d in dates])
|
||||||
|
|
||||||
|
|
||||||
class SymbolType(Enum):
|
class SymbolType(Enum):
|
||||||
|
@ -129,4 +141,6 @@ class Indicator:
|
||||||
self.lineStyle.update(kwargs)
|
self.lineStyle.update(kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# This creates a global array that seems to be shared between all
|
||||||
|
# charting UI components
|
||||||
Quotes = BaseQuotes()
|
Quotes = BaseQuotes()
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
"""Chart."""
|
"""
|
||||||
|
Real-time quotes charting components
|
||||||
|
"""
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
from pyqtgraph import functions as fn
|
||||||
from PyQt5 import QtCore, QtGui
|
from PyQt5 import QtCore, QtGui
|
||||||
|
|
||||||
from .base import Quotes
|
from .base import Quotes
|
||||||
|
@ -9,14 +13,22 @@ from .const import ChartType
|
||||||
from .portfolio import Order, Portfolio
|
from .portfolio import Order, Portfolio
|
||||||
from .utils import fromtimestamp, timeit
|
from .utils import fromtimestamp, timeit
|
||||||
|
|
||||||
__all__ = ('QuotesChart', 'EquityChart')
|
__all__ = ('QuotesChart')
|
||||||
|
|
||||||
|
|
||||||
|
# white background for tinas like xb
|
||||||
# pg.setConfigOption('background', 'w')
|
# pg.setConfigOption('background', 'w')
|
||||||
CHART_MARGINS = (0, 0, 20, 5)
|
|
||||||
|
# margins
|
||||||
|
CHART_MARGINS = (0, 0, 10, 3)
|
||||||
|
|
||||||
|
# chart-wide font
|
||||||
|
_font = QtGui.QFont("Hack", 4)
|
||||||
|
_i3_rgba = QtGui.QColor.fromRgbF(*[0.14]*3 + [1])
|
||||||
|
|
||||||
|
|
||||||
class SampleLegendItem(pg.graphicsItems.LegendItem.ItemSample):
|
class SampleLegendItem(pg.graphicsItems.LegendItem.ItemSample):
|
||||||
|
|
||||||
def paint(self, p, *args):
|
def paint(self, p, *args):
|
||||||
p.setRenderHint(p.Antialiasing)
|
p.setRenderHint(p.Antialiasing)
|
||||||
if isinstance(self.item, tuple):
|
if isinstance(self.item, tuple):
|
||||||
|
@ -51,32 +63,58 @@ class SampleLegendItem(pg.graphicsItems.LegendItem.ItemSample):
|
||||||
|
|
||||||
|
|
||||||
class PriceAxis(pg.AxisItem):
|
class PriceAxis(pg.AxisItem):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(orientation='right')
|
super().__init__(orientation='right')
|
||||||
self.style.update({'textFillLimits': [(0, 0.8)]})
|
# self.setStyle(**{
|
||||||
|
# 'textFillLimits': [(0, 0.8)],
|
||||||
|
# # 'tickTextWidth': 5,
|
||||||
|
# # 'tickTextHeight': 5,
|
||||||
|
# # 'autoExpandTextSpace': True,
|
||||||
|
# # 'maxTickLength': -20,
|
||||||
|
# })
|
||||||
|
# self.setLabel(**{'font-size':'10pt'})
|
||||||
|
self.setTickFont(_font)
|
||||||
|
|
||||||
def tickStrings(self, vals, scale, spacing):
|
# XXX: drop for now since it just eats up h space
|
||||||
digts = max(0, np.ceil(-np.log10(spacing * scale)))
|
|
||||||
return [
|
# def tickStrings(self, vals, scale, spacing):
|
||||||
('{:<8,.%df}' % digts).format(v).replace(',', ' ') for v in vals
|
# digts = max(0, np.ceil(-np.log10(spacing * scale)))
|
||||||
]
|
# return [
|
||||||
|
# ('{:<8,.%df}' % digts).format(v).replace(',', ' ') for v in vals
|
||||||
|
# ]
|
||||||
|
|
||||||
|
|
||||||
class DateAxis(pg.AxisItem):
|
class FromTimeFieldDateAxis(pg.AxisItem):
|
||||||
tick_tpl = {'D1': '%d %b\n%Y'}
|
tick_tpl = {'D1': '%Y-%b-%d'}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self.setTickFont(_font)
|
||||||
self.quotes_count = len(Quotes) - 1
|
self.quotes_count = len(Quotes) - 1
|
||||||
|
|
||||||
|
# default styling
|
||||||
|
self.setStyle(
|
||||||
|
tickTextOffset=7,
|
||||||
|
textFillLimits=[(0, 0.90)],
|
||||||
|
# TODO: doesn't seem to work -> bug in pyqtgraph?
|
||||||
|
# tickTextHeight=2,
|
||||||
|
)
|
||||||
|
|
||||||
def tickStrings(self, values, scale, spacing):
|
def tickStrings(self, values, scale, spacing):
|
||||||
|
# if len(values) > 1 or not values:
|
||||||
|
# values = Quotes.time
|
||||||
|
|
||||||
|
# strings = super().tickStrings(values, scale, spacing)
|
||||||
s_period = 'D1'
|
s_period = 'D1'
|
||||||
strings = []
|
strings = []
|
||||||
for ibar in values:
|
for ibar in values:
|
||||||
if ibar > self.quotes_count:
|
if ibar > self.quotes_count:
|
||||||
return strings
|
return strings
|
||||||
dt_tick = fromtimestamp(Quotes[int(ibar)].time)
|
dt_tick = fromtimestamp(Quotes[int(ibar)].time)
|
||||||
strings.append(dt_tick.strftime(self.tick_tpl[s_period]))
|
strings.append(
|
||||||
|
dt_tick.strftime(self.tick_tpl[s_period])
|
||||||
|
)
|
||||||
return strings
|
return strings
|
||||||
|
|
||||||
|
|
||||||
|
@ -121,7 +159,8 @@ class CenteredTextItem(QtGui.QGraphicsTextItem):
|
||||||
|
|
||||||
class AxisLabel(pg.GraphicsObject):
|
class AxisLabel(pg.GraphicsObject):
|
||||||
|
|
||||||
bg_color = pg.mkColor('#dbdbdb')
|
# bg_color = pg.mkColor('#a9a9a9')
|
||||||
|
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):
|
||||||
|
@ -130,13 +169,15 @@ class AxisLabel(pg.GraphicsObject):
|
||||||
self.opacity = opacity
|
self.opacity = opacity
|
||||||
self.label_str = ''
|
self.label_str = ''
|
||||||
self.digits = digits
|
self.digits = digits
|
||||||
self.quotes_count = len(Quotes) - 1
|
# self.quotes_count = len(Quotes) - 1
|
||||||
|
|
||||||
if isinstance(color, QtGui.QPen):
|
if isinstance(color, QtGui.QPen):
|
||||||
self.bg_color = color.color()
|
self.bg_color = color.color()
|
||||||
self.fg_color = pg.mkColor('#ffffff')
|
self.fg_color = pg.mkColor('#ffffff')
|
||||||
elif isinstance(color, list):
|
elif isinstance(color, list):
|
||||||
self.bg_color = {'>0': color[0].color(), '<0': color[1].color()}
|
self.bg_color = {'>0': color[0].color(), '<0': color[1].color()}
|
||||||
self.fg_color = pg.mkColor('#ffffff')
|
self.fg_color = pg.mkColor('#ffffff')
|
||||||
|
|
||||||
self.setFlag(self.ItemIgnoresTransformations)
|
self.setFlag(self.ItemIgnoresTransformations)
|
||||||
|
|
||||||
def tick_to_string(self, tick_pos):
|
def tick_to_string(self, tick_pos):
|
||||||
|
@ -169,6 +210,8 @@ class AxisLabel(pg.GraphicsObject):
|
||||||
p.setOpacity(self.opacity)
|
p.setOpacity(self.opacity)
|
||||||
p.fillRect(option.rect, bg_color)
|
p.fillRect(option.rect, bg_color)
|
||||||
p.setOpacity(1)
|
p.setOpacity(1)
|
||||||
|
p.setFont(_font)
|
||||||
|
|
||||||
p.drawText(option.rect, self.text_flags, self.label_str)
|
p.drawText(option.rect, self.text_flags, self.label_str)
|
||||||
|
|
||||||
|
|
||||||
|
@ -184,12 +227,12 @@ class XAxisLabel(AxisLabel):
|
||||||
return fromtimestamp(Quotes[round(tick_pos)].time).strftime(tpl)
|
return fromtimestamp(Quotes[round(tick_pos)].time).strftime(tpl)
|
||||||
|
|
||||||
def boundingRect(self): # noqa
|
def boundingRect(self): # noqa
|
||||||
return QtCore.QRectF(0, 0, 60, 38)
|
return QtCore.QRectF(0, 0, 145, 50)
|
||||||
|
|
||||||
def update_label(self, evt_post, point_view):
|
def update_label(self, evt_post, point_view):
|
||||||
ibar = point_view.x()
|
ibar = point_view.x()
|
||||||
if ibar > self.quotes_count:
|
# if ibar > self.quotes_count:
|
||||||
return
|
# return
|
||||||
self.label_str = self.tick_to_string(ibar)
|
self.label_str = self.tick_to_string(ibar)
|
||||||
width = self.boundingRect().width()
|
width = self.boundingRect().width()
|
||||||
offset = 0 # if have margins
|
offset = 0 # if have margins
|
||||||
|
@ -207,7 +250,7 @@ class YAxisLabel(AxisLabel):
|
||||||
return ('{: ,.%df}' % self.digits).format(tick_pos).replace(',', ' ')
|
return ('{: ,.%df}' % self.digits).format(tick_pos).replace(',', ' ')
|
||||||
|
|
||||||
def boundingRect(self): # noqa
|
def boundingRect(self): # noqa
|
||||||
return QtCore.QRectF(0, 0, 74, 24)
|
return QtCore.QRectF(0, 0, 80, 40)
|
||||||
|
|
||||||
def update_label(self, evt_post, point_view):
|
def update_label(self, evt_post, point_view):
|
||||||
self.label_str = self.tick_to_string(point_view.y())
|
self.label_str = self.tick_to_string(point_view.y())
|
||||||
|
@ -217,10 +260,28 @@ class YAxisLabel(AxisLabel):
|
||||||
self.setPos(new_pos)
|
self.setPos(new_pos)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: convert this to a ``ViewBox`` type giving us
|
||||||
|
# control over mouse scrolling and a context menu
|
||||||
class CustomPlotWidget(pg.PlotWidget):
|
class CustomPlotWidget(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)
|
||||||
|
|
||||||
|
# def wheelEvent(self, ev, axis=None):
|
||||||
|
# if axis in (0, 1):
|
||||||
|
# mask = [False, False]
|
||||||
|
# mask[axis] = self.state['mouseEnabled'][axis]
|
||||||
|
# else:
|
||||||
|
# mask = self.state['mouseEnabled'][:]
|
||||||
|
|
||||||
|
# # actual scaling factor
|
||||||
|
# s = 1.02 ** (ev.delta() * self.state['wheelScaleFactor'])
|
||||||
|
# s = [(None if m is False else s) for m in mask]
|
||||||
|
|
||||||
|
# self._resetTarget()
|
||||||
|
# self.scaleBy(s, center)
|
||||||
|
# ev.accept()
|
||||||
|
# self.sigRangeChangedManually.emit(mask)
|
||||||
|
|
||||||
def enterEvent(self, ev): # noqa
|
def enterEvent(self, ev): # noqa
|
||||||
self.sig_mouse_enter.emit(self)
|
self.sig_mouse_enter.emit(self)
|
||||||
|
|
||||||
|
@ -229,7 +290,8 @@ class CustomPlotWidget(pg.PlotWidget):
|
||||||
self.scene().leaveEvent(ev)
|
self.scene().leaveEvent(ev)
|
||||||
|
|
||||||
|
|
||||||
_rate_limit = 30
|
_mouse_rate_limit = 60
|
||||||
|
_xaxis_at = 'bottom'
|
||||||
|
|
||||||
|
|
||||||
class CrossHairItem(pg.GraphicsObject):
|
class CrossHairItem(pg.GraphicsObject):
|
||||||
|
@ -249,7 +311,7 @@ class CrossHairItem(pg.GraphicsObject):
|
||||||
|
|
||||||
self.proxy_moved = pg.SignalProxy(
|
self.proxy_moved = pg.SignalProxy(
|
||||||
self.parent.scene().sigMouseMoved,
|
self.parent.scene().sigMouseMoved,
|
||||||
rateLimit=_rate_limit,
|
rateLimit=_mouse_rate_limit,
|
||||||
slot=self.mouseMoved,
|
slot=self.mouseMoved,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -258,39 +320,52 @@ class CrossHairItem(pg.GraphicsObject):
|
||||||
)
|
)
|
||||||
|
|
||||||
indicators = indicators or []
|
indicators = indicators or []
|
||||||
|
|
||||||
if indicators:
|
if indicators:
|
||||||
|
# when there are indicators present in sub-plot rows
|
||||||
|
# take the last one (nearest to the bottom) and place the
|
||||||
|
# crosshair label on it's x-axis.
|
||||||
last_ind = indicators[-1]
|
last_ind = indicators[-1]
|
||||||
self.xaxis_label = XAxisLabel(
|
|
||||||
parent=last_ind.getAxis('bottom'), opacity=1
|
|
||||||
)
|
|
||||||
self.proxy_enter = pg.SignalProxy(
|
self.proxy_enter = pg.SignalProxy(
|
||||||
self.parent.sig_mouse_enter,
|
self.parent.sig_mouse_enter,
|
||||||
rateLimit=_rate_limit,
|
rateLimit=_mouse_rate_limit,
|
||||||
slot=lambda: self.mouseAction('Enter', False),
|
slot=lambda: self.mouseAction('Enter', False),
|
||||||
)
|
)
|
||||||
self.proxy_leave = pg.SignalProxy(
|
self.proxy_leave = pg.SignalProxy(
|
||||||
self.parent.sig_mouse_leave,
|
self.parent.sig_mouse_leave,
|
||||||
rateLimit=_rate_limit,
|
rateLimit=_mouse_rate_limit,
|
||||||
slot=lambda: self.mouseAction('Leave', False),
|
slot=lambda: self.mouseAction('Leave', False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if _xaxis_at == 'bottom':
|
||||||
|
# place below is last indicator subplot
|
||||||
|
self.xaxis_label = XAxisLabel(
|
||||||
|
parent=last_ind.getAxis('bottom'), opacity=1
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
|
# keep x-axis right below main chart
|
||||||
self.xaxis_label = XAxisLabel(parent=self.xaxis, opacity=1)
|
self.xaxis_label = XAxisLabel(parent=self.xaxis, opacity=1)
|
||||||
|
|
||||||
for i in indicators:
|
for i in indicators:
|
||||||
|
# add vertial and horizonal lines and a y-axis label
|
||||||
vl = i.addLine(x=0, pen=self.pen, movable=False)
|
vl = i.addLine(x=0, pen=self.pen, movable=False)
|
||||||
hl = i.addLine(y=0, pen=self.pen, movable=False)
|
hl = i.addLine(y=0, pen=self.pen, movable=False)
|
||||||
yl = YAxisLabel(parent=i.getAxis('right'), opacity=1)
|
yl = YAxisLabel(parent=i.getAxis('right'), opacity=1)
|
||||||
|
|
||||||
px_moved = pg.SignalProxy(
|
px_moved = pg.SignalProxy(
|
||||||
i.scene().sigMouseMoved, rateLimit=_rate_limit, slot=self.mouseMoved
|
i.scene().sigMouseMoved,
|
||||||
|
rateLimit=_mouse_rate_limit,
|
||||||
|
slot=self.mouseMoved
|
||||||
)
|
)
|
||||||
px_enter = pg.SignalProxy(
|
px_enter = pg.SignalProxy(
|
||||||
i.sig_mouse_enter,
|
i.sig_mouse_enter,
|
||||||
rateLimit=_rate_limit,
|
rateLimit=_mouse_rate_limit,
|
||||||
slot=lambda: self.mouseAction('Enter', i),
|
slot=lambda: self.mouseAction('Enter', i),
|
||||||
)
|
)
|
||||||
px_leave = pg.SignalProxy(
|
px_leave = pg.SignalProxy(
|
||||||
i.sig_mouse_leave,
|
i.sig_mouse_leave,
|
||||||
rateLimit=_rate_limit,
|
rateLimit=_mouse_rate_limit,
|
||||||
slot=lambda: self.mouseAction('Leave', i),
|
slot=lambda: self.mouseAction('Leave', i),
|
||||||
)
|
)
|
||||||
self.indicators[i] = {
|
self.indicators[i] = {
|
||||||
|
@ -302,6 +377,7 @@ class CrossHairItem(pg.GraphicsObject):
|
||||||
|
|
||||||
def mouseAction(self, action, ind=False): # noqa
|
def mouseAction(self, action, ind=False): # noqa
|
||||||
if action == 'Enter':
|
if action == 'Enter':
|
||||||
|
# show horiz line and y-label
|
||||||
if ind:
|
if ind:
|
||||||
self.indicators[ind]['hl'].show()
|
self.indicators[ind]['hl'].show()
|
||||||
self.indicators[ind]['yl'].show()
|
self.indicators[ind]['yl'].show()
|
||||||
|
@ -310,6 +386,7 @@ class CrossHairItem(pg.GraphicsObject):
|
||||||
self.yaxis_label.show()
|
self.yaxis_label.show()
|
||||||
self.hline.show()
|
self.hline.show()
|
||||||
else: # Leave
|
else: # Leave
|
||||||
|
# hide horiz line and y-label
|
||||||
if ind:
|
if ind:
|
||||||
self.indicators[ind]['hl'].hide()
|
self.indicators[ind]['hl'].hide()
|
||||||
self.indicators[ind]['yl'].hide()
|
self.indicators[ind]['yl'].hide()
|
||||||
|
@ -319,16 +396,29 @@ class CrossHairItem(pg.GraphicsObject):
|
||||||
self.hline.hide()
|
self.hline.hide()
|
||||||
|
|
||||||
def mouseMoved(self, evt): # noqa
|
def mouseMoved(self, evt): # noqa
|
||||||
|
"""Update horizonal and vertical lines when mouse moves inside
|
||||||
|
either the main chart or any indicator subplot.
|
||||||
|
"""
|
||||||
|
|
||||||
pos = evt[0]
|
pos = evt[0]
|
||||||
|
|
||||||
|
# if the mouse is within the parent ``CustomPlotWidget``
|
||||||
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)
|
||||||
|
|
||||||
|
# move the vertial line to the current x coordinate
|
||||||
self.vline.setX(mouse_point.x())
|
self.vline.setX(mouse_point.x())
|
||||||
|
|
||||||
|
# update the label on the bottom of the crosshair
|
||||||
self.xaxis_label.update_label(evt_post=pos, point_view=mouse_point)
|
self.xaxis_label.update_label(evt_post=pos, point_view=mouse_point)
|
||||||
|
|
||||||
|
# update the vertical line in any indicators subplots
|
||||||
for opts in self.indicators.values():
|
for opts in self.indicators.values():
|
||||||
opts['vl'].setX(mouse_point.x())
|
opts['vl'].setX(mouse_point.x())
|
||||||
|
|
||||||
if self.activeIndicator:
|
if self.activeIndicator:
|
||||||
|
# vertial position of the mouse is inside an indicator
|
||||||
mouse_point_ind = self.activeIndicator.mapToView(pos)
|
mouse_point_ind = self.activeIndicator.mapToView(pos)
|
||||||
self.indicators[self.activeIndicator]['hl'].setY(
|
self.indicators[self.activeIndicator]['hl'].setY(
|
||||||
mouse_point_ind.y()
|
mouse_point_ind.y()
|
||||||
|
@ -337,6 +427,7 @@ class CrossHairItem(pg.GraphicsObject):
|
||||||
evt_post=pos, point_view=mouse_point_ind
|
evt_post=pos, point_view=mouse_point_ind
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
# vertial position of the mouse is inside and main chart
|
||||||
self.hline.setY(mouse_point.y())
|
self.hline.setY(mouse_point.y())
|
||||||
self.yaxis_label.update_label(
|
self.yaxis_label.update_label(
|
||||||
evt_post=pos, point_view=mouse_point
|
evt_post=pos, point_view=mouse_point
|
||||||
|
@ -351,28 +442,31 @@ class CrossHairItem(pg.GraphicsObject):
|
||||||
|
|
||||||
class BarItem(pg.GraphicsObject):
|
class BarItem(pg.GraphicsObject):
|
||||||
|
|
||||||
w = 0.35
|
w = 0.5
|
||||||
bull_brush = pg.mkPen('#00cc00')
|
|
||||||
bear_brush = pg.mkPen('#fa0000')
|
bull_brush = bear_brush = pg.mkPen('#808080')
|
||||||
|
# bull_brush = pg.mkPen('#00cc00')
|
||||||
|
# bear_brush = pg.mkPen('#fa0000')
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.generatePicture()
|
self.generatePicture()
|
||||||
|
|
||||||
def _generate(self, p):
|
def _generate(self, p):
|
||||||
hl = np.array(
|
high_to_low = np.array(
|
||||||
[QtCore.QLineF(q.id, q.low, q.id, q.high) for q in Quotes]
|
[QtCore.QLineF(q.id, q.low, q.id, q.high) for q in Quotes]
|
||||||
)
|
)
|
||||||
op = np.array(
|
open_stick = np.array(
|
||||||
[QtCore.QLineF(q.id - self.w, q.open, q.id, q.open) for q in Quotes]
|
[QtCore.QLineF(q.id - self.w, q.open, q.id, q.open)
|
||||||
|
for q in Quotes]
|
||||||
)
|
)
|
||||||
cl = np.array(
|
close_stick = np.array(
|
||||||
[
|
[
|
||||||
QtCore.QLineF(q.id + self.w, q.close, q.id, q.close)
|
QtCore.QLineF(q.id + self.w, q.close, q.id, q.close)
|
||||||
for q in Quotes
|
for q in Quotes
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
lines = np.concatenate([hl, op, cl])
|
lines = np.concatenate([high_to_low, open_stick, close_stick])
|
||||||
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))
|
||||||
|
|
||||||
|
@ -382,6 +476,7 @@ class BarItem(pg.GraphicsObject):
|
||||||
p.setPen(self.bear_brush)
|
p.setPen(self.bear_brush)
|
||||||
p.drawLines(*lines[short_bars])
|
p.drawLines(*lines[short_bars])
|
||||||
|
|
||||||
|
# TODO: this is the routine to be retriggered for redraw
|
||||||
@timeit
|
@timeit
|
||||||
def generatePicture(self):
|
def generatePicture(self):
|
||||||
self.picture = QtGui.QPicture()
|
self.picture = QtGui.QPicture()
|
||||||
|
@ -412,7 +507,10 @@ class CandlestickItem(BarItem):
|
||||||
)
|
)
|
||||||
|
|
||||||
p.setPen(self.line_pen)
|
p.setPen(self.line_pen)
|
||||||
p.drawLines([QtCore.QLineF(q.id, q.low, q.id, q.high) for q in Quotes])
|
p.drawLines(
|
||||||
|
[QtCore.QLineF(q.id, q.low, q.id, q.high)
|
||||||
|
for q in Quotes]
|
||||||
|
)
|
||||||
|
|
||||||
p.setBrush(self.bull_brush)
|
p.setBrush(self.bull_brush)
|
||||||
p.drawRects(*rects[Quotes.close > Quotes.open])
|
p.drawRects(*rects[Quotes.close > Quotes.open])
|
||||||
|
@ -421,6 +519,34 @@ class CandlestickItem(BarItem):
|
||||||
p.drawRects(*rects[Quotes.close < Quotes.open])
|
p.drawRects(*rects[Quotes.close < Quotes.open])
|
||||||
|
|
||||||
|
|
||||||
|
def _configure_quotes_chart(
|
||||||
|
chart: CustomPlotWidget,
|
||||||
|
style: ChartType,
|
||||||
|
update_yrange_limits: Callable,
|
||||||
|
) -> None:
|
||||||
|
"""Update and format a chart with quotes data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
chart.hideAxis('left')
|
||||||
|
chart.showAxis('right')
|
||||||
|
chart.addItem(_get_chart_points(style))
|
||||||
|
chart.setLimits(
|
||||||
|
xMin=Quotes[0].id,
|
||||||
|
xMax=Quotes[-1].id,
|
||||||
|
minXRange=60,
|
||||||
|
yMin=Quotes.low.min() * 0.98,
|
||||||
|
yMax=Quotes.high.max() * 1.02,
|
||||||
|
)
|
||||||
|
chart.showGrid(x=True, y=True)
|
||||||
|
chart.setCursor(QtCore.Qt.BlankCursor)
|
||||||
|
|
||||||
|
# assign callback for rescaling y-axis automatically
|
||||||
|
# based on y-range contents
|
||||||
|
# TODO: this can likely be ported to built-in: .enableAutoRange()
|
||||||
|
# but needs testing
|
||||||
|
chart.sigXRangeChanged.connect(update_yrange_limits)
|
||||||
|
|
||||||
|
|
||||||
class QuotesChart(QtGui.QWidget):
|
class QuotesChart(QtGui.QWidget):
|
||||||
|
|
||||||
long_pen = pg.mkPen('#006000')
|
long_pen = pg.mkPen('#006000')
|
||||||
|
@ -436,19 +562,21 @@ class QuotesChart(QtGui.QWidget):
|
||||||
self.style = ChartType.BAR
|
self.style = ChartType.BAR
|
||||||
self.indicators = []
|
self.indicators = []
|
||||||
|
|
||||||
self.xaxis = DateAxis(orientation='bottom')
|
self.xaxis = FromTimeFieldDateAxis(orientation='bottom')
|
||||||
self.xaxis.setStyle(
|
# self.xaxis = pg.DateAxisItem()
|
||||||
tickTextOffset=7, textFillLimits=[(0, 0.80)], showValues=False
|
|
||||||
)
|
|
||||||
|
|
||||||
self.xaxis_ind = DateAxis(orientation='bottom')
|
self.xaxis_ind = FromTimeFieldDateAxis(orientation='bottom')
|
||||||
self.xaxis_ind.setStyle(tickTextOffset=7, textFillLimits=[(0, 0.80)])
|
|
||||||
|
if _xaxis_at == 'bottom':
|
||||||
|
self.xaxis.setStyle(showValues=False)
|
||||||
|
else:
|
||||||
|
self.xaxis_ind.setStyle(showValues=False)
|
||||||
|
|
||||||
self.layout = QtGui.QVBoxLayout(self)
|
self.layout = QtGui.QVBoxLayout(self)
|
||||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
self.splitter = QtGui.QSplitter(QtCore.Qt.Vertical)
|
self.splitter = QtGui.QSplitter(QtCore.Qt.Vertical)
|
||||||
self.splitter.setHandleWidth(4)
|
self.splitter.setHandleWidth(5)
|
||||||
|
|
||||||
self.layout.addWidget(self.splitter)
|
self.layout.addWidget(self.splitter)
|
||||||
|
|
||||||
|
@ -473,21 +601,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_quotes_chart(self):
|
|
||||||
self.chart.hideAxis('left')
|
|
||||||
self.chart.showAxis('right')
|
|
||||||
self.chart.addItem(_get_chart_points(self.style))
|
|
||||||
self.chart.setLimits(
|
|
||||||
xMin=Quotes[0].id,
|
|
||||||
xMax=Quotes[-1].id,
|
|
||||||
minXRange=60,
|
|
||||||
yMin=Quotes.low.min() * 0.98,
|
|
||||||
yMax=Quotes.high.max() * 1.02,
|
|
||||||
)
|
|
||||||
self.chart.showGrid(x=True, y=True)
|
|
||||||
self.chart.setCursor(QtCore.Qt.BlankCursor)
|
|
||||||
self.chart.sigXRangeChanged.connect(self._update_yrange_limits)
|
|
||||||
|
|
||||||
def _update_ind_charts(self):
|
def _update_ind_charts(self):
|
||||||
for ind, d in self.indicators:
|
for ind, d in self.indicators:
|
||||||
curve = pg.PlotDataItem(d, pen='b', antialias=True)
|
curve = pg.PlotDataItem(d, pen='b', antialias=True)
|
||||||
|
@ -513,10 +626,14 @@ class QuotesChart(QtGui.QWidget):
|
||||||
self.splitter.setSizes(sizes) # , int(self.height()*0.2)
|
self.splitter.setSizes(sizes) # , int(self.height()*0.2)
|
||||||
|
|
||||||
def _update_yrange_limits(self):
|
def _update_yrange_limits(self):
|
||||||
|
"""Callback for each y-range update.
|
||||||
|
"""
|
||||||
vr = self.chart.viewRect()
|
vr = self.chart.viewRect()
|
||||||
lbar, rbar = int(vr.left()), int(vr.right())
|
lbar, rbar = int(vr.left()), int(vr.right())
|
||||||
|
|
||||||
if self.signals_visible:
|
if self.signals_visible:
|
||||||
self._show_text_signals(lbar, rbar)
|
self._show_text_signals(lbar, rbar)
|
||||||
|
|
||||||
bars = Quotes[lbar:rbar]
|
bars = Quotes[lbar:rbar]
|
||||||
ylow = bars.low.min() * 0.98
|
ylow = bars.low.min() * 0.98
|
||||||
yhigh = bars.high.max() * 1.02
|
yhigh = bars.high.max() * 1.02
|
||||||
|
@ -540,15 +657,17 @@ class QuotesChart(QtGui.QWidget):
|
||||||
axisItems={'bottom': self.xaxis, 'right': PriceAxis()},
|
axisItems={'bottom': self.xaxis, 'right': PriceAxis()},
|
||||||
enableMenu=False,
|
enableMenu=False,
|
||||||
)
|
)
|
||||||
# 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)
|
||||||
|
|
||||||
|
# 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 = CustomPlotWidget(
|
ind = CustomPlotWidget(
|
||||||
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()},
|
||||||
enableMenu=False,
|
enableMenu=False,
|
||||||
)
|
)
|
||||||
ind.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
ind.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
||||||
|
@ -556,7 +675,11 @@ class QuotesChart(QtGui.QWidget):
|
||||||
# self.splitter.addWidget(ind)
|
# self.splitter.addWidget(ind)
|
||||||
self.indicators.append((ind, d))
|
self.indicators.append((ind, d))
|
||||||
|
|
||||||
self._update_quotes_chart()
|
_configure_quotes_chart(
|
||||||
|
self.chart,
|
||||||
|
self.style,
|
||||||
|
self._update_yrange_limits
|
||||||
|
)
|
||||||
self._update_ind_charts()
|
self._update_ind_charts()
|
||||||
self._update_sizes()
|
self._update_sizes()
|
||||||
|
|
||||||
|
@ -620,177 +743,9 @@ class QuotesChart(QtGui.QWidget):
|
||||||
self.signals_visible = True
|
self.signals_visible = True
|
||||||
|
|
||||||
|
|
||||||
class EquityChart(QtGui.QWidget):
|
# this function is borderline rediculous.
|
||||||
|
# The creation of these chart types mutates all the input data
|
||||||
eq_pen_pos_color = pg.mkColor('#00cc00')
|
# inside each type's constructor (mind blown)
|
||||||
eq_pen_neg_color = pg.mkColor('#cc0000')
|
|
||||||
eq_brush_pos_color = pg.mkColor('#40ee40')
|
|
||||||
eq_brush_neg_color = pg.mkColor('#ee4040')
|
|
||||||
long_pen_color = pg.mkColor('#008000')
|
|
||||||
short_pen_color = pg.mkColor('#800000')
|
|
||||||
buy_and_hold_pen_color = pg.mkColor('#4444ff')
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.xaxis = DateAxis(orientation='bottom')
|
|
||||||
self.xaxis.setStyle(tickTextOffset=7, textFillLimits=[(0, 0.80)])
|
|
||||||
self.yaxis = PriceAxis()
|
|
||||||
|
|
||||||
self.layout = QtGui.QVBoxLayout(self)
|
|
||||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
|
|
||||||
self.chart = pg.PlotWidget(
|
|
||||||
axisItems={'bottom': self.xaxis, 'right': self.yaxis},
|
|
||||||
enableMenu=False,
|
|
||||||
)
|
|
||||||
self.chart.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
|
||||||
self.chart.getPlotItem().setContentsMargins(*CHART_MARGINS)
|
|
||||||
self.chart.showGrid(x=True, y=True)
|
|
||||||
self.chart.hideAxis('left')
|
|
||||||
self.chart.showAxis('right')
|
|
||||||
|
|
||||||
self.chart.setCursor(QtCore.Qt.BlankCursor)
|
|
||||||
self.chart.sigXRangeChanged.connect(self._update_yrange_limits)
|
|
||||||
|
|
||||||
self.layout.addWidget(self.chart)
|
|
||||||
|
|
||||||
def _add_legend(self):
|
|
||||||
legend = pg.LegendItem((140, 100), offset=(10, 10))
|
|
||||||
legend.setParentItem(self.chart.getPlotItem())
|
|
||||||
|
|
||||||
for arr, item in self.curves:
|
|
||||||
legend.addItem(
|
|
||||||
SampleLegendItem(item),
|
|
||||||
item.opts['name']
|
|
||||||
if not isinstance(item, tuple)
|
|
||||||
else item[0].opts['name'],
|
|
||||||
)
|
|
||||||
|
|
||||||
def _add_ylabels(self):
|
|
||||||
self.ylabels = []
|
|
||||||
for arr, item in self.curves:
|
|
||||||
color = (
|
|
||||||
item.opts['pen']
|
|
||||||
if not isinstance(item, tuple)
|
|
||||||
else [i.opts['pen'] for i in item]
|
|
||||||
)
|
|
||||||
label = YAxisLabel(parent=self.yaxis, color=color)
|
|
||||||
self.ylabels.append(label)
|
|
||||||
|
|
||||||
def _update_ylabels(self, vb, rbar):
|
|
||||||
for i, curve in enumerate(self.curves):
|
|
||||||
arr, item = curve
|
|
||||||
ylast = arr[rbar]
|
|
||||||
ypos = vb.mapFromView(QtCore.QPointF(0, ylast)).y()
|
|
||||||
axlabel = self.ylabels[i]
|
|
||||||
axlabel.update_label_test(ypos=ypos, ydata=ylast)
|
|
||||||
|
|
||||||
def _update_yrange_limits(self, vb=None):
|
|
||||||
if not hasattr(self, 'min_curve'):
|
|
||||||
return
|
|
||||||
vr = self.chart.viewRect()
|
|
||||||
lbar, rbar = int(vr.left()), int(vr.right())
|
|
||||||
ylow = self.min_curve[lbar:rbar].min() * 1.1
|
|
||||||
yhigh = self.max_curve[lbar:rbar].max() * 1.1
|
|
||||||
|
|
||||||
std = np.std(self.max_curve[lbar:rbar]) * 4
|
|
||||||
self.chart.setLimits(yMin=ylow, yMax=yhigh, minYRange=std)
|
|
||||||
self.chart.setYRange(ylow, yhigh)
|
|
||||||
self._update_ylabels(vb, rbar)
|
|
||||||
|
|
||||||
@timeit
|
|
||||||
def plot(self):
|
|
||||||
equity_curve = Portfolio.equity_curve
|
|
||||||
eq_pos = np.zeros_like(equity_curve)
|
|
||||||
eq_neg = np.zeros_like(equity_curve)
|
|
||||||
eq_pos[equity_curve >= 0] = equity_curve[equity_curve >= 0]
|
|
||||||
eq_neg[equity_curve <= 0] = equity_curve[equity_curve <= 0]
|
|
||||||
|
|
||||||
# Equity
|
|
||||||
self.eq_pos_curve = pg.PlotCurveItem(
|
|
||||||
eq_pos,
|
|
||||||
name='Equity',
|
|
||||||
fillLevel=0,
|
|
||||||
antialias=True,
|
|
||||||
pen=self.eq_pen_pos_color,
|
|
||||||
brush=self.eq_brush_pos_color,
|
|
||||||
)
|
|
||||||
self.eq_neg_curve = pg.PlotCurveItem(
|
|
||||||
eq_neg,
|
|
||||||
name='Equity',
|
|
||||||
fillLevel=0,
|
|
||||||
antialias=True,
|
|
||||||
pen=self.eq_pen_neg_color,
|
|
||||||
brush=self.eq_brush_neg_color,
|
|
||||||
)
|
|
||||||
self.chart.addItem(self.eq_pos_curve)
|
|
||||||
self.chart.addItem(self.eq_neg_curve)
|
|
||||||
|
|
||||||
# Only Long
|
|
||||||
self.long_curve = pg.PlotCurveItem(
|
|
||||||
Portfolio.long_curve,
|
|
||||||
name='Only Long',
|
|
||||||
pen=self.long_pen_color,
|
|
||||||
antialias=True,
|
|
||||||
)
|
|
||||||
self.chart.addItem(self.long_curve)
|
|
||||||
|
|
||||||
# Only Short
|
|
||||||
self.short_curve = pg.PlotCurveItem(
|
|
||||||
Portfolio.short_curve,
|
|
||||||
name='Only Short',
|
|
||||||
pen=self.short_pen_color,
|
|
||||||
antialias=True,
|
|
||||||
)
|
|
||||||
self.chart.addItem(self.short_curve)
|
|
||||||
|
|
||||||
# Buy and Hold
|
|
||||||
self.buy_and_hold_curve = pg.PlotCurveItem(
|
|
||||||
Portfolio.buy_and_hold_curve,
|
|
||||||
name='Buy and Hold',
|
|
||||||
pen=self.buy_and_hold_pen_color,
|
|
||||||
antialias=True,
|
|
||||||
)
|
|
||||||
self.chart.addItem(self.buy_and_hold_curve)
|
|
||||||
|
|
||||||
self.curves = [
|
|
||||||
(Portfolio.equity_curve, (self.eq_pos_curve, self.eq_neg_curve)),
|
|
||||||
(Portfolio.long_curve, self.long_curve),
|
|
||||||
(Portfolio.short_curve, self.short_curve),
|
|
||||||
(Portfolio.buy_and_hold_curve, self.buy_and_hold_curve),
|
|
||||||
]
|
|
||||||
|
|
||||||
self._add_legend()
|
|
||||||
self._add_ylabels()
|
|
||||||
|
|
||||||
ch = CrossHairItem(self.chart)
|
|
||||||
self.chart.addItem(ch)
|
|
||||||
|
|
||||||
arrs = (
|
|
||||||
Portfolio.equity_curve,
|
|
||||||
Portfolio.buy_and_hold_curve,
|
|
||||||
Portfolio.long_curve,
|
|
||||||
Portfolio.short_curve,
|
|
||||||
)
|
|
||||||
np_arrs = np.concatenate(arrs)
|
|
||||||
_min = abs(np_arrs.min()) * -1.1
|
|
||||||
_max = np_arrs.max() * 1.1
|
|
||||||
|
|
||||||
self.chart.setLimits(
|
|
||||||
xMin=Quotes[0].id,
|
|
||||||
xMax=Quotes[-1].id,
|
|
||||||
yMin=_min,
|
|
||||||
yMax=_max,
|
|
||||||
minXRange=60,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.min_curve = arrs[0].copy()
|
|
||||||
self.max_curve = arrs[0].copy()
|
|
||||||
for arr in arrs[1:]:
|
|
||||||
self.min_curve = np.minimum(self.min_curve, arr)
|
|
||||||
self.max_curve = np.maximum(self.max_curve, arr)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_chart_points(style):
|
def _get_chart_points(style):
|
||||||
if style == ChartType.CANDLESTICK:
|
if style == ChartType.CANDLESTICK:
|
||||||
return CandlestickItem()
|
return CandlestickItem()
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
import pickle
|
import pickle
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pandas_datareader.data as web
|
import pandas_datareader.data as web
|
||||||
from pandas_datareader._utils import RemoteDataError
|
from pandas_datareader._utils import RemoteDataError
|
||||||
|
@ -15,7 +17,7 @@ from pandas_datareader.data import (
|
||||||
from pandas_datareader.nasdaq_trader import get_nasdaq_symbols
|
from pandas_datareader.nasdaq_trader import get_nasdaq_symbols
|
||||||
from pandas_datareader.exceptions import ImmediateDeprecationError
|
from pandas_datareader.exceptions import ImmediateDeprecationError
|
||||||
|
|
||||||
from .base import Quotes
|
from .base import Quotes, Symbol
|
||||||
from .utils import get_data_path, timeit
|
from .utils import get_data_path, timeit
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
@ -71,7 +73,15 @@ class QuotesLoader:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@timeit
|
@timeit
|
||||||
def get_quotes(cls, symbol, date_from, date_to):
|
def get_quotes(
|
||||||
|
cls,
|
||||||
|
symbol: Symbol,
|
||||||
|
date_from: datetime.datetime,
|
||||||
|
date_to: datetime.datetime,
|
||||||
|
) -> Quotes:
|
||||||
|
"""Retrieve quotes data from a provider and return a ``numpy.ndarray`` subtype.
|
||||||
|
"""
|
||||||
|
|
||||||
quotes = None
|
quotes = None
|
||||||
fpath = cls._get_file_path(symbol, cls.timeframe, date_from, date_to)
|
fpath = cls._get_file_path(symbol, cls.timeframe, date_from, date_to)
|
||||||
if os.path.exists(fpath):
|
if os.path.exists(fpath):
|
||||||
|
|
|
@ -62,7 +62,9 @@ class BasePortfolio:
|
||||||
for p in self.positions:
|
for p in self.positions:
|
||||||
if p.status == Position.OPEN:
|
if p.status == Position.OPEN:
|
||||||
p.close(
|
p.close(
|
||||||
price=Quotes[-1].open, volume=p.volume, time=Quotes[-1].time
|
price=Quotes[-1].open,
|
||||||
|
volume=p.volume,
|
||||||
|
time=Quotes[-1].time
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_market_position(self):
|
def _get_market_position(self):
|
||||||
|
|
Loading…
Reference in New Issue