Factor components into more suitably named modules
							parent
							
								
									44984272be
								
							
						
					
					
						commit
						ef214226a2
					
				| 
						 | 
				
			
			@ -0,0 +1,171 @@
 | 
			
		|||
"""
 | 
			
		||||
Chart axes graphics and behavior.
 | 
			
		||||
"""
 | 
			
		||||
import pyqtgraph as pg
 | 
			
		||||
from PyQt5 import QtCore, QtGui
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from .quantdom.base import Quotes
 | 
			
		||||
from .quantdom.utils import fromtimestamp
 | 
			
		||||
from ._style import _font
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PriceAxis(pg.AxisItem):
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        super().__init__(orientation='right')
 | 
			
		||||
        self.setStyle(**{
 | 
			
		||||
            'textFillLimits': [(0, 0.8)],
 | 
			
		||||
            # 'tickTextWidth': 5,
 | 
			
		||||
            # 'tickTextHeight': 5,
 | 
			
		||||
            # 'autoExpandTextSpace': True,
 | 
			
		||||
            # 'maxTickLength': -20,
 | 
			
		||||
        })
 | 
			
		||||
        self.setLabel(**{'font-size': '10pt'})
 | 
			
		||||
        self.setTickFont(_font)
 | 
			
		||||
 | 
			
		||||
    # XXX: drop for now since it just eats up h space
 | 
			
		||||
 | 
			
		||||
    # def tickStrings(self, vals, scale, spacing):
 | 
			
		||||
    #     digts = max(0, np.ceil(-np.log10(spacing * scale)))
 | 
			
		||||
    #     return [
 | 
			
		||||
    #         ('{:<8,.%df}' % digts).format(v).replace(',', ' ') for v in vals
 | 
			
		||||
    #     ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FromTimeFieldDateAxis(pg.AxisItem):
 | 
			
		||||
    tick_tpl = {'D1': '%Y-%b-%d'}
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.setTickFont(_font)
 | 
			
		||||
        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):
 | 
			
		||||
        # if len(values) > 1 or not values:
 | 
			
		||||
        #     values = Quotes.time
 | 
			
		||||
 | 
			
		||||
        # strings = super().tickStrings(values, scale, spacing)
 | 
			
		||||
        s_period = 'D1'
 | 
			
		||||
        strings = []
 | 
			
		||||
        for ibar in values:
 | 
			
		||||
            if ibar > self.quotes_count:
 | 
			
		||||
                return strings
 | 
			
		||||
            dt_tick = fromtimestamp(Quotes[int(ibar)].time)
 | 
			
		||||
            strings.append(
 | 
			
		||||
                dt_tick.strftime(self.tick_tpl[s_period])
 | 
			
		||||
            )
 | 
			
		||||
        return strings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AxisLabel(pg.GraphicsObject):
 | 
			
		||||
 | 
			
		||||
    # bg_color = pg.mkColor('#a9a9a9')
 | 
			
		||||
    bg_color = pg.mkColor('#808080')
 | 
			
		||||
    fg_color = pg.mkColor('#000000')
 | 
			
		||||
 | 
			
		||||
    def __init__(self, parent=None, digits=0, color=None, opacity=1, **kwargs):
 | 
			
		||||
        super().__init__(parent)
 | 
			
		||||
        self.parent = parent
 | 
			
		||||
        self.opacity = opacity
 | 
			
		||||
        self.label_str = ''
 | 
			
		||||
        self.digits = digits
 | 
			
		||||
        # self.quotes_count = len(Quotes) - 1
 | 
			
		||||
 | 
			
		||||
        if isinstance(color, QtGui.QPen):
 | 
			
		||||
            self.bg_color = color.color()
 | 
			
		||||
            self.fg_color = pg.mkColor('#ffffff')
 | 
			
		||||
        elif isinstance(color, list):
 | 
			
		||||
            self.bg_color = {'>0': color[0].color(), '<0': color[1].color()}
 | 
			
		||||
            self.fg_color = pg.mkColor('#ffffff')
 | 
			
		||||
 | 
			
		||||
        self.setFlag(self.ItemIgnoresTransformations)
 | 
			
		||||
 | 
			
		||||
    def tick_to_string(self, tick_pos):
 | 
			
		||||
        raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
    def boundingRect(self):  # noqa
 | 
			
		||||
        raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
    def update_label(self, evt_post, point_view):
 | 
			
		||||
        raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
    def update_label_test(self, ypos=0, ydata=0):
 | 
			
		||||
        self.label_str = self.tick_to_string(ydata)
 | 
			
		||||
        height = self.boundingRect().height()
 | 
			
		||||
        offset = 0  # if have margins
 | 
			
		||||
        new_pos = QtCore.QPointF(0, ypos - height / 2 - offset)
 | 
			
		||||
        self.setPos(new_pos)
 | 
			
		||||
 | 
			
		||||
    def paint(self, p, option, widget):
 | 
			
		||||
        p.setRenderHint(p.TextAntialiasing, True)
 | 
			
		||||
        p.setPen(self.fg_color)
 | 
			
		||||
        if self.label_str:
 | 
			
		||||
            if not isinstance(self.bg_color, dict):
 | 
			
		||||
                bg_color = self.bg_color
 | 
			
		||||
            else:
 | 
			
		||||
                if int(self.label_str.replace(' ', '')) > 0:
 | 
			
		||||
                    bg_color = self.bg_color['>0']
 | 
			
		||||
                else:
 | 
			
		||||
                    bg_color = self.bg_color['<0']
 | 
			
		||||
            p.setOpacity(self.opacity)
 | 
			
		||||
            p.fillRect(option.rect, bg_color)
 | 
			
		||||
            p.setOpacity(1)
 | 
			
		||||
            p.setFont(_font)
 | 
			
		||||
 | 
			
		||||
        p.drawText(option.rect, self.text_flags, self.label_str)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class XAxisLabel(AxisLabel):
 | 
			
		||||
 | 
			
		||||
    text_flags = (
 | 
			
		||||
        QtCore.Qt.TextDontClip | QtCore.Qt.AlignCenter | QtCore.Qt.AlignTop
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def tick_to_string(self, tick_pos):
 | 
			
		||||
        # TODO: change to actual period
 | 
			
		||||
        tpl = self.parent.tick_tpl['D1']
 | 
			
		||||
        if tick_pos > len(Quotes):
 | 
			
		||||
            return 'Unknown Time'
 | 
			
		||||
        return fromtimestamp(Quotes[round(tick_pos)].time).strftime(tpl)
 | 
			
		||||
 | 
			
		||||
    def boundingRect(self):  # noqa
 | 
			
		||||
        return QtCore.QRectF(0, 0, 145, 50)
 | 
			
		||||
 | 
			
		||||
    def update_label(self, evt_post, point_view):
 | 
			
		||||
        ibar = point_view.x()
 | 
			
		||||
        # if ibar > self.quotes_count:
 | 
			
		||||
        #     return
 | 
			
		||||
        self.label_str = self.tick_to_string(ibar)
 | 
			
		||||
        width = self.boundingRect().width()
 | 
			
		||||
        offset = 0  # if have margins
 | 
			
		||||
        new_pos = QtCore.QPointF(evt_post.x() - width / 2 - offset, 0)
 | 
			
		||||
        self.setPos(new_pos)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class YAxisLabel(AxisLabel):
 | 
			
		||||
 | 
			
		||||
    text_flags = (
 | 
			
		||||
        QtCore.Qt.TextDontClip | QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def tick_to_string(self, tick_pos):
 | 
			
		||||
        return ('{: ,.%df}' % self.digits).format(tick_pos).replace(',', ' ')
 | 
			
		||||
 | 
			
		||||
    def boundingRect(self):  # noqa
 | 
			
		||||
        return QtCore.QRectF(0, 0, 80, 40)
 | 
			
		||||
 | 
			
		||||
    def update_label(self, evt_post, point_view):
 | 
			
		||||
        self.label_str = self.tick_to_string(point_view.y())
 | 
			
		||||
        height = self.boundingRect().height()
 | 
			
		||||
        offset = 0  # if have margins
 | 
			
		||||
        new_pos = QtCore.QPointF(0, evt_post.y() - height / 2 - offset)
 | 
			
		||||
        self.setPos(new_pos)
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +1,29 @@
 | 
			
		|||
"""
 | 
			
		||||
High level Qt chart wrapping widgets.
 | 
			
		||||
High level Qt chart widgets.
 | 
			
		||||
"""
 | 
			
		||||
from PyQt5 import QtGui
 | 
			
		||||
import numpy as np
 | 
			
		||||
import pyqtgraph as pg
 | 
			
		||||
from pyqtgraph import functions as fn
 | 
			
		||||
from PyQt5 import QtCore, QtGui
 | 
			
		||||
 | 
			
		||||
from .quantdom.charts import SplitterChart
 | 
			
		||||
from ._axes import (
 | 
			
		||||
    FromTimeFieldDateAxis,
 | 
			
		||||
    PriceAxis,
 | 
			
		||||
)
 | 
			
		||||
from ._graphics import CrossHairItem, CandlestickItem, BarItem
 | 
			
		||||
from ._style import _xaxis_at
 | 
			
		||||
 | 
			
		||||
from .quantdom.charts import CenteredTextItem
 | 
			
		||||
from .quantdom.base import Quotes
 | 
			
		||||
from .quantdom.const import ChartType
 | 
			
		||||
from .quantdom.portfolio import Order, Portfolio
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# white background (for tinas like our pal xb)
 | 
			
		||||
# pg.setConfigOption('background', 'w')
 | 
			
		||||
 | 
			
		||||
# margins
 | 
			
		||||
CHART_MARGINS = (0, 0, 10, 3)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class QuotesTabWidget(QtGui.QWidget):
 | 
			
		||||
| 
						 | 
				
			
			@ -53,3 +73,383 @@ class QuotesTabWidget(QtGui.QWidget):
 | 
			
		|||
 | 
			
		||||
    def add_signals(self):
 | 
			
		||||
        self.chart.add_signals()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SplitterChart(QtGui.QWidget):
 | 
			
		||||
 | 
			
		||||
    long_pen = pg.mkPen('#006000')
 | 
			
		||||
    long_brush = pg.mkBrush('#00ff00')
 | 
			
		||||
    short_pen = pg.mkPen('#600000')
 | 
			
		||||
    short_brush = pg.mkBrush('#ff0000')
 | 
			
		||||
 | 
			
		||||
    zoomIsDisabled = QtCore.pyqtSignal(bool)
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        self.signals_visible = False
 | 
			
		||||
        self.indicators = []
 | 
			
		||||
 | 
			
		||||
        self.xaxis = FromTimeFieldDateAxis(orientation='bottom')
 | 
			
		||||
        # self.xaxis = pg.DateAxisItem()
 | 
			
		||||
 | 
			
		||||
        self.xaxis_ind = FromTimeFieldDateAxis(orientation='bottom')
 | 
			
		||||
 | 
			
		||||
        if _xaxis_at == 'bottom':
 | 
			
		||||
            self.xaxis.setStyle(showValues=False)
 | 
			
		||||
        else:
 | 
			
		||||
            self.xaxis_ind.setStyle(showValues=False)
 | 
			
		||||
 | 
			
		||||
        self.splitter = QtGui.QSplitter(QtCore.Qt.Vertical)
 | 
			
		||||
        self.splitter.setHandleWidth(5)
 | 
			
		||||
 | 
			
		||||
        self.layout = QtGui.QVBoxLayout(self)
 | 
			
		||||
        self.layout.setContentsMargins(0, 0, 0, 0)
 | 
			
		||||
 | 
			
		||||
        self.layout.addWidget(self.splitter)
 | 
			
		||||
 | 
			
		||||
    def _show_text_signals(self, lbar, rbar):
 | 
			
		||||
        signals = [
 | 
			
		||||
            sig
 | 
			
		||||
            for sig in self.signals_text_items[lbar:rbar]
 | 
			
		||||
            if isinstance(sig, CenteredTextItem)
 | 
			
		||||
        ]
 | 
			
		||||
        if len(signals) <= 50:
 | 
			
		||||
            for sig in signals:
 | 
			
		||||
                sig.show()
 | 
			
		||||
        else:
 | 
			
		||||
            for sig in signals:
 | 
			
		||||
                sig.hide()
 | 
			
		||||
 | 
			
		||||
    def _remove_signals(self):
 | 
			
		||||
        self.chart.removeItem(self.signals_group_arrow)
 | 
			
		||||
        self.chart.removeItem(self.signals_group_text)
 | 
			
		||||
        del self.signals_text_items
 | 
			
		||||
        del self.signals_group_arrow
 | 
			
		||||
        del self.signals_group_text
 | 
			
		||||
        self.signals_visible = False
 | 
			
		||||
 | 
			
		||||
    def _update_sizes(self):
 | 
			
		||||
        min_h_ind = int(self.height() * 0.2 / len(self.indicators))
 | 
			
		||||
        sizes = [int(self.height() * 0.8)]
 | 
			
		||||
        sizes.extend([min_h_ind] * len(self.indicators))
 | 
			
		||||
        self.splitter.setSizes(sizes)  # , int(self.height()*0.2)
 | 
			
		||||
 | 
			
		||||
    def plot(self, symbol):
 | 
			
		||||
        self.digits = symbol.digits
 | 
			
		||||
        self.chart = ChartPlotWidget(
 | 
			
		||||
            split_charts=self,
 | 
			
		||||
            parent=self.splitter,
 | 
			
		||||
            axisItems={'bottom': self.xaxis, 'right': PriceAxis()},
 | 
			
		||||
            viewBox=ChartView,
 | 
			
		||||
            # enableMenu=False,
 | 
			
		||||
        )
 | 
			
		||||
        # TODO: ``pyqtgraph`` doesn't pass through a parent to the
 | 
			
		||||
        # ``PlotItem`` by default; maybe we should PR this in?
 | 
			
		||||
        self.chart.plotItem.parent = self
 | 
			
		||||
 | 
			
		||||
        self.chart.getPlotItem().setContentsMargins(*CHART_MARGINS)
 | 
			
		||||
        self.chart.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
 | 
			
		||||
 | 
			
		||||
        # TODO: this is where we would load an indicator chain
 | 
			
		||||
        inds = [Quotes.open]
 | 
			
		||||
 | 
			
		||||
        for d in inds:
 | 
			
		||||
            ind = ChartPlotWidget(
 | 
			
		||||
                split_charts=self,
 | 
			
		||||
                parent=self.splitter,
 | 
			
		||||
                axisItems={'bottom': self.xaxis_ind, 'right': PriceAxis()},
 | 
			
		||||
                # axisItems={'top': self.xaxis_ind, 'right': PriceAxis()},
 | 
			
		||||
                viewBox=ChartView,
 | 
			
		||||
            )
 | 
			
		||||
            ind.plotItem.parent = 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:
 | 
			
		||||
 | 
			
		||||
            # link chart x-axis to main quotes chart
 | 
			
		||||
            ind_chart.setXLink(self.chart)
 | 
			
		||||
 | 
			
		||||
            # XXX: never do this lol
 | 
			
		||||
            # ind.setAspectLocked(1)
 | 
			
		||||
            ind_chart.draw_curve(d)
 | 
			
		||||
 | 
			
		||||
        self._update_sizes()
 | 
			
		||||
 | 
			
		||||
        ch = CrossHairItem(
 | 
			
		||||
            self.chart, [_ind for _ind, d in self.indicators], self.digits
 | 
			
		||||
        )
 | 
			
		||||
        self.chart.addItem(ch)
 | 
			
		||||
 | 
			
		||||
    def add_signals(self):
 | 
			
		||||
        self.signals_group_text = QtGui.QGraphicsItemGroup()
 | 
			
		||||
        self.signals_group_arrow = QtGui.QGraphicsItemGroup()
 | 
			
		||||
        self.signals_text_items = np.empty(len(Quotes), dtype=object)
 | 
			
		||||
 | 
			
		||||
        for p in Portfolio.positions:
 | 
			
		||||
            x, price = p.id_bar_open, p.open_price
 | 
			
		||||
            if p.type == Order.BUY:
 | 
			
		||||
                y = Quotes[x].low * 0.99
 | 
			
		||||
                pg.ArrowItem(
 | 
			
		||||
                    parent=self.signals_group_arrow,
 | 
			
		||||
                    pos=(x, y),
 | 
			
		||||
                    pen=self.long_pen,
 | 
			
		||||
                    brush=self.long_brush,
 | 
			
		||||
                    angle=90,
 | 
			
		||||
                    headLen=12,
 | 
			
		||||
                    tipAngle=50,
 | 
			
		||||
                )
 | 
			
		||||
                text_sig = CenteredTextItem(
 | 
			
		||||
                    parent=self.signals_group_text,
 | 
			
		||||
                    pos=(x, y),
 | 
			
		||||
                    pen=self.long_pen,
 | 
			
		||||
                    brush=self.long_brush,
 | 
			
		||||
                    text=('Buy at {:.%df}' % self.digits).format(price),
 | 
			
		||||
                    valign=QtCore.Qt.AlignBottom,
 | 
			
		||||
                )
 | 
			
		||||
                text_sig.hide()
 | 
			
		||||
            else:
 | 
			
		||||
                y = Quotes[x].high * 1.01
 | 
			
		||||
                pg.ArrowItem(
 | 
			
		||||
                    parent=self.signals_group_arrow,
 | 
			
		||||
                    pos=(x, y),
 | 
			
		||||
                    pen=self.short_pen,
 | 
			
		||||
                    brush=self.short_brush,
 | 
			
		||||
                    angle=-90,
 | 
			
		||||
                    headLen=12,
 | 
			
		||||
                    tipAngle=50,
 | 
			
		||||
                )
 | 
			
		||||
                text_sig = CenteredTextItem(
 | 
			
		||||
                    parent=self.signals_group_text,
 | 
			
		||||
                    pos=(x, y),
 | 
			
		||||
                    pen=self.short_pen,
 | 
			
		||||
                    brush=self.short_brush,
 | 
			
		||||
                    text=('Sell at {:.%df}' % self.digits).format(price),
 | 
			
		||||
                    valign=QtCore.Qt.AlignTop,
 | 
			
		||||
                )
 | 
			
		||||
                text_sig.hide()
 | 
			
		||||
 | 
			
		||||
            self.signals_text_items[x] = text_sig
 | 
			
		||||
 | 
			
		||||
        self.chart.addItem(self.signals_group_arrow)
 | 
			
		||||
        self.chart.addItem(self.signals_group_text)
 | 
			
		||||
        self.signals_visible = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# 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``.
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
    eventually want multiple plots managed together).
 | 
			
		||||
    """
 | 
			
		||||
    sig_mouse_leave = QtCore.Signal(object)
 | 
			
		||||
    sig_mouse_enter = QtCore.Signal(object)
 | 
			
		||||
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
        split_charts,
 | 
			
		||||
        **kwargs,
 | 
			
		||||
        # parent=None,
 | 
			
		||||
        # background='default',
 | 
			
		||||
        # plotItem=None,
 | 
			
		||||
        # **kargs
 | 
			
		||||
    ):
 | 
			
		||||
        """Configure chart display settings.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        super().__init__(**kwargs)
 | 
			
		||||
        # label = pg.LabelItem(justify='left')
 | 
			
		||||
        # self.addItem(label)
 | 
			
		||||
        # label.setText("Yo yoyo")
 | 
			
		||||
        # label.setText("<span style='font-size: 12pt'>x=")
 | 
			
		||||
        self.parent = split_charts
 | 
			
		||||
 | 
			
		||||
        # show only right side axes
 | 
			
		||||
        self.hideAxis('left')
 | 
			
		||||
        self.showAxis('right')
 | 
			
		||||
 | 
			
		||||
        # show background grid
 | 
			
		||||
        self.showGrid(x=True, y=True, alpha=0.4)
 | 
			
		||||
 | 
			
		||||
        # use cross-hair for cursor
 | 
			
		||||
        self.setCursor(QtCore.Qt.CrossCursor)
 | 
			
		||||
 | 
			
		||||
        # set panning limits
 | 
			
		||||
        min_points_to_show = 20
 | 
			
		||||
        min_bars_in_view = 10
 | 
			
		||||
        max_lookahead = min_points_to_show - min_bars_in_view
 | 
			
		||||
        last = Quotes[-1].id
 | 
			
		||||
        self.setLimits(
 | 
			
		||||
            xMin=Quotes[0].id,
 | 
			
		||||
            xMax=last + max_lookahead,
 | 
			
		||||
            minXRange=min_points_to_show,
 | 
			
		||||
            # maxYRange=highest-lowest,
 | 
			
		||||
            yMin=Quotes.low.min() * 0.98,
 | 
			
		||||
            yMax=Quotes.high.max() * 1.02,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # show last 50 points on startup
 | 
			
		||||
        self.plotItem.vb.setXRange(last - 50, last + max_lookahead)
 | 
			
		||||
 | 
			
		||||
        # assign callback for rescaling y-axis automatically
 | 
			
		||||
        # based on y-range contents
 | 
			
		||||
        self.sigXRangeChanged.connect(self._update_yrange_limits)
 | 
			
		||||
        self._update_yrange_limits()
 | 
			
		||||
 | 
			
		||||
    def bars_range(self):
 | 
			
		||||
        """Return a range tuple for the bars present in view.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        vr = self.viewRect()
 | 
			
		||||
        lbar, rbar = int(vr.left()), int(min(vr.right(), len(Quotes) - 1))
 | 
			
		||||
        return lbar, rbar
 | 
			
		||||
 | 
			
		||||
    def draw_ohlc(
 | 
			
		||||
        self,
 | 
			
		||||
        style: ChartType = ChartType.BAR,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        """Draw OHLC datums to chart.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        # 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))
 | 
			
		||||
 | 
			
		||||
    def draw_curve(
 | 
			
		||||
        self,
 | 
			
		||||
        data: np.ndarray,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        # draw the indicator as a plain curve
 | 
			
		||||
        curve = pg.PlotDataItem(data, antialias=True)
 | 
			
		||||
        self.addItem(curve)
 | 
			
		||||
 | 
			
		||||
    def _update_yrange_limits(self):
 | 
			
		||||
        """Callback for each y-range update.
 | 
			
		||||
 | 
			
		||||
        This adds auto-scaling like zoom on the scroll wheel such
 | 
			
		||||
        that data always fits nicely inside the current view of the
 | 
			
		||||
        data set.
 | 
			
		||||
        """
 | 
			
		||||
        # TODO: this can likely be ported in part to the built-ins:
 | 
			
		||||
        # self.setYRange(Quotes.low.min() * .98, Quotes.high.max() * 1.02)
 | 
			
		||||
        # self.setMouseEnabled(x=True, y=False)
 | 
			
		||||
        # self.setXRange(Quotes[0].id, Quotes[-1].id)
 | 
			
		||||
        # self.setAutoVisible(x=False, y=True)
 | 
			
		||||
        # self.enableAutoRange(x=False, y=True)
 | 
			
		||||
 | 
			
		||||
        chart = self
 | 
			
		||||
        chart_parent = self.parent
 | 
			
		||||
 | 
			
		||||
        lbar, rbar = self.bars_range()
 | 
			
		||||
        # vr = chart.viewRect()
 | 
			
		||||
        # lbar, rbar = int(vr.left()), int(vr.right())
 | 
			
		||||
 | 
			
		||||
        if chart_parent.signals_visible:
 | 
			
		||||
            chart_parent._show_text_signals(lbar, rbar)
 | 
			
		||||
 | 
			
		||||
        bars = Quotes[lbar:rbar]
 | 
			
		||||
        ylow = bars.low.min() * 0.98
 | 
			
		||||
        yhigh = bars.high.max() * 1.02
 | 
			
		||||
 | 
			
		||||
        std = np.std(bars.close)
 | 
			
		||||
        chart.setLimits(yMin=ylow, yMax=yhigh, minYRange=std)
 | 
			
		||||
        chart.setYRange(ylow, yhigh)
 | 
			
		||||
 | 
			
		||||
        for i, d in chart_parent.indicators:
 | 
			
		||||
            # ydata = i.plotItem.items[0].getData()[1]
 | 
			
		||||
            ydata = d[lbar:rbar]
 | 
			
		||||
            ylow = ydata.min() * 0.98
 | 
			
		||||
            yhigh = ydata.max() * 1.02
 | 
			
		||||
            std = np.std(ydata)
 | 
			
		||||
            i.setLimits(yMin=ylow, yMax=yhigh, minYRange=std)
 | 
			
		||||
            i.setYRange(ylow, yhigh)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def enterEvent(self, ev):  # noqa
 | 
			
		||||
        # pg.PlotWidget.enterEvent(self, ev)
 | 
			
		||||
        self.sig_mouse_enter.emit(self)
 | 
			
		||||
 | 
			
		||||
    def leaveEvent(self, ev):  # noqa
 | 
			
		||||
        # pg.PlotWidget.leaveEvent(self, ev)
 | 
			
		||||
        self.sig_mouse_leave.emit(self)
 | 
			
		||||
        self.scene().leaveEvent(ev)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChartView(pg.ViewBox):
 | 
			
		||||
    """Price chart view box with interaction behaviors you'd expect from
 | 
			
		||||
    an interactive platform:
 | 
			
		||||
 | 
			
		||||
    - zoom on mouse scroll that auto fits y-axis
 | 
			
		||||
    - no vertical scrolling
 | 
			
		||||
    - zoom to a "fixed point" on the y-axis
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
        parent=None,
 | 
			
		||||
        **kwargs,
 | 
			
		||||
        # invertY=False,
 | 
			
		||||
    ):
 | 
			
		||||
        super().__init__(parent=parent, **kwargs)
 | 
			
		||||
        self.chart = parent
 | 
			
		||||
 | 
			
		||||
        # disable vertical scrolling
 | 
			
		||||
        self.setMouseEnabled(x=True, y=False)
 | 
			
		||||
 | 
			
		||||
    def wheelEvent(self, ev, axis=None):
 | 
			
		||||
        """Override "center-point" location for scrolling.
 | 
			
		||||
 | 
			
		||||
        This is an override of the ``ViewBox`` method simply changing
 | 
			
		||||
        the center of the zoom to be the y-axis.
 | 
			
		||||
 | 
			
		||||
        TODO: PR a method into ``pyqtgraph`` to make this configurable
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        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]
 | 
			
		||||
 | 
			
		||||
        # center = pg.Point(
 | 
			
		||||
        #     fn.invertQTransform(self.childGroup.transform()).map(ev.pos())
 | 
			
		||||
        # )
 | 
			
		||||
 | 
			
		||||
        # XXX: scroll "around" the right most element in the view
 | 
			
		||||
        furthest_right_coord = self.boundingRect().topRight()
 | 
			
		||||
        center = pg.Point(
 | 
			
		||||
           fn.invertQTransform(
 | 
			
		||||
               self.childGroup.transform()
 | 
			
		||||
            ).map(furthest_right_coord)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self._resetTarget()
 | 
			
		||||
        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')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,252 @@
 | 
			
		|||
"""
 | 
			
		||||
Chart graphics for displaying a slew of different data types.
 | 
			
		||||
"""
 | 
			
		||||
import numpy as np
 | 
			
		||||
import pyqtgraph as pg
 | 
			
		||||
from PyQt5 import QtCore, QtGui
 | 
			
		||||
 | 
			
		||||
from .quantdom.utils import timeit
 | 
			
		||||
from .quantdom.base import Quotes
 | 
			
		||||
 | 
			
		||||
from ._style import _xaxis_at
 | 
			
		||||
from ._axes import YAxisLabel, XAxisLabel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_mouse_rate_limit = 60
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CrossHairItem(pg.GraphicsObject):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, parent, indicators=None, digits=0):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        # self.pen = pg.mkPen('#000000')
 | 
			
		||||
        self.pen = pg.mkPen('#a9a9a9')
 | 
			
		||||
        self.parent = parent
 | 
			
		||||
        self.indicators = {}
 | 
			
		||||
        self.activeIndicator = None
 | 
			
		||||
        self.xaxis = self.parent.getAxis('bottom')
 | 
			
		||||
        self.yaxis = self.parent.getAxis('right')
 | 
			
		||||
 | 
			
		||||
        self.vline = self.parent.addLine(x=0, pen=self.pen, movable=False)
 | 
			
		||||
        self.hline = self.parent.addLine(y=0, pen=self.pen, movable=False)
 | 
			
		||||
 | 
			
		||||
        self.proxy_moved = pg.SignalProxy(
 | 
			
		||||
            self.parent.scene().sigMouseMoved,
 | 
			
		||||
            rateLimit=_mouse_rate_limit,
 | 
			
		||||
            slot=self.mouseMoved,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.yaxis_label = YAxisLabel(
 | 
			
		||||
            parent=self.yaxis, digits=digits, opacity=1
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        indicators = indicators or []
 | 
			
		||||
 | 
			
		||||
        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]
 | 
			
		||||
 | 
			
		||||
            self.proxy_enter = pg.SignalProxy(
 | 
			
		||||
                self.parent.sig_mouse_enter,
 | 
			
		||||
                rateLimit=_mouse_rate_limit,
 | 
			
		||||
                slot=lambda: self.mouseAction('Enter', False),
 | 
			
		||||
            )
 | 
			
		||||
            self.proxy_leave = pg.SignalProxy(
 | 
			
		||||
                self.parent.sig_mouse_leave,
 | 
			
		||||
                rateLimit=_mouse_rate_limit,
 | 
			
		||||
                slot=lambda: self.mouseAction('Leave', False),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # determine where to place x-axis label
 | 
			
		||||
        if _xaxis_at == 'bottom':
 | 
			
		||||
            # place below is last indicator subplot
 | 
			
		||||
            self.xaxis_label = XAxisLabel(
 | 
			
		||||
                parent=last_ind.getAxis('bottom'), opacity=1
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            # keep x-axis right below main chart
 | 
			
		||||
            self.xaxis_label = XAxisLabel(parent=self.xaxis, opacity=1)
 | 
			
		||||
 | 
			
		||||
        for i in indicators:
 | 
			
		||||
            # add vertial and horizonal lines and a y-axis label
 | 
			
		||||
            vl = i.addLine(x=0, pen=self.pen, movable=False)
 | 
			
		||||
            hl = i.addLine(y=0, pen=self.pen, movable=False)
 | 
			
		||||
            yl = YAxisLabel(parent=i.getAxis('right'), opacity=1)
 | 
			
		||||
 | 
			
		||||
            px_moved = pg.SignalProxy(
 | 
			
		||||
                i.scene().sigMouseMoved,
 | 
			
		||||
                rateLimit=_mouse_rate_limit,
 | 
			
		||||
                slot=self.mouseMoved
 | 
			
		||||
            )
 | 
			
		||||
            px_enter = pg.SignalProxy(
 | 
			
		||||
                i.sig_mouse_enter,
 | 
			
		||||
                rateLimit=_mouse_rate_limit,
 | 
			
		||||
                slot=lambda: self.mouseAction('Enter', i),
 | 
			
		||||
            )
 | 
			
		||||
            px_leave = pg.SignalProxy(
 | 
			
		||||
                i.sig_mouse_leave,
 | 
			
		||||
                rateLimit=_mouse_rate_limit,
 | 
			
		||||
                slot=lambda: self.mouseAction('Leave', i),
 | 
			
		||||
            )
 | 
			
		||||
            self.indicators[i] = {
 | 
			
		||||
                'vl': vl,
 | 
			
		||||
                'hl': hl,
 | 
			
		||||
                'yl': yl,
 | 
			
		||||
                'px': (px_moved, px_enter, px_leave),
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
    def mouseAction(self, action, ind=False):  # noqa
 | 
			
		||||
        if action == 'Enter':
 | 
			
		||||
            # show horiz line and y-label
 | 
			
		||||
            if ind:
 | 
			
		||||
                self.indicators[ind]['hl'].show()
 | 
			
		||||
                self.indicators[ind]['yl'].show()
 | 
			
		||||
                self.activeIndicator = ind
 | 
			
		||||
            else:
 | 
			
		||||
                self.yaxis_label.show()
 | 
			
		||||
                self.hline.show()
 | 
			
		||||
        # Leave
 | 
			
		||||
        else:
 | 
			
		||||
            # hide horiz line and y-label
 | 
			
		||||
            if ind:
 | 
			
		||||
                self.indicators[ind]['hl'].hide()
 | 
			
		||||
                self.indicators[ind]['yl'].hide()
 | 
			
		||||
                self.activeIndicator = None
 | 
			
		||||
            else:
 | 
			
		||||
                self.yaxis_label.hide()
 | 
			
		||||
                self.hline.hide()
 | 
			
		||||
 | 
			
		||||
    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]
 | 
			
		||||
 | 
			
		||||
        # if the mouse is within the parent ``ChartPlotWidget``
 | 
			
		||||
        if self.parent.sceneBoundingRect().contains(pos):
 | 
			
		||||
            # mouse_point = self.vb.mapSceneToView(pos)
 | 
			
		||||
            mouse_point = self.parent.mapToView(pos)
 | 
			
		||||
 | 
			
		||||
            # move the vertial line to the current x coordinate
 | 
			
		||||
            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)
 | 
			
		||||
 | 
			
		||||
            # update the vertical line in any indicators subplots
 | 
			
		||||
            for opts in self.indicators.values():
 | 
			
		||||
                opts['vl'].setX(mouse_point.x())
 | 
			
		||||
 | 
			
		||||
            if self.activeIndicator:
 | 
			
		||||
                # vertial position of the mouse is inside an indicator
 | 
			
		||||
                mouse_point_ind = self.activeIndicator.mapToView(pos)
 | 
			
		||||
                self.indicators[self.activeIndicator]['hl'].setY(
 | 
			
		||||
                    mouse_point_ind.y()
 | 
			
		||||
                )
 | 
			
		||||
                self.indicators[self.activeIndicator]['yl'].update_label(
 | 
			
		||||
                    evt_post=pos, point_view=mouse_point_ind
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                # vertial position of the mouse is inside the main chart
 | 
			
		||||
                self.hline.setY(mouse_point.y())
 | 
			
		||||
                self.yaxis_label.update_label(
 | 
			
		||||
                    evt_post=pos, point_view=mouse_point
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    def paint(self, p, *args):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def boundingRect(self):
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
    bull_brush = bear_brush = pg.mkPen('#808080')
 | 
			
		||||
    # bull_brush = pg.mkPen('#00cc00')
 | 
			
		||||
    # bear_brush = pg.mkPen('#fa0000')
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        self.generatePicture()
 | 
			
		||||
 | 
			
		||||
    # TODO: this is the routine to be retriggered for redraw
 | 
			
		||||
    @timeit
 | 
			
		||||
    def generatePicture(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)
 | 
			
		||||
        p.end()
 | 
			
		||||
 | 
			
		||||
    def _generate(self, p):
 | 
			
		||||
        # 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))
 | 
			
		||||
 | 
			
		||||
        p.setPen(self.bull_brush)
 | 
			
		||||
        p.drawLines(*lines[long_bars])
 | 
			
		||||
 | 
			
		||||
        p.setPen(self.bear_brush)
 | 
			
		||||
        p.drawLines(*lines[short_bars])
 | 
			
		||||
 | 
			
		||||
    def paint(self, p, *args):
 | 
			
		||||
        p.drawPicture(0, 0, self.picture)
 | 
			
		||||
 | 
			
		||||
    def boundingRect(self):
 | 
			
		||||
        # boundingRect _must_ indicate the entire area that will be
 | 
			
		||||
        # drawn on or else we will get artifacts and possibly crashing.
 | 
			
		||||
        # (in this case, QPicture does all the work of computing the
 | 
			
		||||
        # bouning rect for us)
 | 
			
		||||
        return QtCore.QRectF(self.picture.boundingRect())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CandlestickItem(BarItem):
 | 
			
		||||
 | 
			
		||||
    w2 = 0.7
 | 
			
		||||
    line_pen = pg.mkPen('#000000')
 | 
			
		||||
    bull_brush = pg.mkBrush('#00ff00')
 | 
			
		||||
    bear_brush = pg.mkBrush('#ff0000')
 | 
			
		||||
 | 
			
		||||
    def _generate(self, p):
 | 
			
		||||
        rects = np.array(
 | 
			
		||||
            [
 | 
			
		||||
                QtCore.QRectF(q.id - self.w, q.open, self.w2, q.close - q.open)
 | 
			
		||||
                for q in Quotes
 | 
			
		||||
            ]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        p.setPen(self.line_pen)
 | 
			
		||||
        p.drawLines(
 | 
			
		||||
            [QtCore.QLineF(q.id, q.low, q.id, q.high)
 | 
			
		||||
             for q in Quotes]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        p.setBrush(self.bull_brush)
 | 
			
		||||
        p.drawRects(*rects[Quotes.close > Quotes.open])
 | 
			
		||||
 | 
			
		||||
        p.setBrush(self.bear_brush)
 | 
			
		||||
        p.drawRects(*rects[Quotes.close < Quotes.open])
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
"""
 | 
			
		||||
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)
 | 
			
		||||
_i3_rgba = QtGui.QColor.fromRgbF(*[0.14]*3 + [1])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# splitter widget config
 | 
			
		||||
_xaxis_at = 'bottom'
 | 
			
		||||
| 
						 | 
				
			
			@ -1,31 +1,9 @@
 | 
			
		|||
"""
 | 
			
		||||
Real-time quotes charting components
 | 
			
		||||
"""
 | 
			
		||||
from typing import List, Tuple
 | 
			
		||||
 | 
			
		||||
import numpy as np
 | 
			
		||||
import pyqtgraph as pg
 | 
			
		||||
from pyqtgraph import functions as fn
 | 
			
		||||
from PyQt5 import QtCore, QtGui
 | 
			
		||||
 | 
			
		||||
from .base import Quotes
 | 
			
		||||
from .const import ChartType
 | 
			
		||||
from .portfolio import Order, Portfolio
 | 
			
		||||
from .utils import fromtimestamp, timeit
 | 
			
		||||
 | 
			
		||||
__all__ = ('SplitterChart')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# white background (for tinas like our pal xb)
 | 
			
		||||
# pg.setConfigOption('background', 'w')
 | 
			
		||||
 | 
			
		||||
# 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):
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -62,62 +40,6 @@ class SampleLegendItem(pg.graphicsItems.LegendItem.ItemSample):
 | 
			
		|||
            p.drawRect(0, 10, 18, 0.5)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PriceAxis(pg.AxisItem):
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        super().__init__(orientation='right')
 | 
			
		||||
        self.setStyle(**{
 | 
			
		||||
            'textFillLimits': [(0, 0.8)],
 | 
			
		||||
            # 'tickTextWidth': 5,
 | 
			
		||||
            # 'tickTextHeight': 5,
 | 
			
		||||
            # 'autoExpandTextSpace': True,
 | 
			
		||||
            # 'maxTickLength': -20,
 | 
			
		||||
        })
 | 
			
		||||
        self.setLabel(**{'font-size': '10pt'})
 | 
			
		||||
        self.setTickFont(_font)
 | 
			
		||||
 | 
			
		||||
    # XXX: drop for now since it just eats up h space
 | 
			
		||||
 | 
			
		||||
    # def tickStrings(self, vals, scale, spacing):
 | 
			
		||||
    #     digts = max(0, np.ceil(-np.log10(spacing * scale)))
 | 
			
		||||
    #     return [
 | 
			
		||||
    #         ('{:<8,.%df}' % digts).format(v).replace(',', ' ') for v in vals
 | 
			
		||||
    #     ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FromTimeFieldDateAxis(pg.AxisItem):
 | 
			
		||||
    tick_tpl = {'D1': '%Y-%b-%d'}
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.setTickFont(_font)
 | 
			
		||||
        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):
 | 
			
		||||
        # if len(values) > 1 or not values:
 | 
			
		||||
        #     values = Quotes.time
 | 
			
		||||
 | 
			
		||||
        # strings = super().tickStrings(values, scale, spacing)
 | 
			
		||||
        s_period = 'D1'
 | 
			
		||||
        strings = []
 | 
			
		||||
        for ibar in values:
 | 
			
		||||
            if ibar > self.quotes_count:
 | 
			
		||||
                return strings
 | 
			
		||||
            dt_tick = fromtimestamp(Quotes[int(ibar)].time)
 | 
			
		||||
            strings.append(
 | 
			
		||||
                dt_tick.strftime(self.tick_tpl[s_period])
 | 
			
		||||
            )
 | 
			
		||||
        return strings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CenteredTextItem(QtGui.QGraphicsTextItem):
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
| 
						 | 
				
			
			@ -155,729 +77,3 @@ class CenteredTextItem(QtGui.QGraphicsTextItem):
 | 
			
		|||
            p.fillRect(option.rect, self.brush)
 | 
			
		||||
            p.setOpacity(1)
 | 
			
		||||
        p.drawText(option.rect, self.text_flags, self.toPlainText())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AxisLabel(pg.GraphicsObject):
 | 
			
		||||
 | 
			
		||||
    # bg_color = pg.mkColor('#a9a9a9')
 | 
			
		||||
    bg_color = pg.mkColor('#808080')
 | 
			
		||||
    fg_color = pg.mkColor('#000000')
 | 
			
		||||
 | 
			
		||||
    def __init__(self, parent=None, digits=0, color=None, opacity=1, **kwargs):
 | 
			
		||||
        super().__init__(parent)
 | 
			
		||||
        self.parent = parent
 | 
			
		||||
        self.opacity = opacity
 | 
			
		||||
        self.label_str = ''
 | 
			
		||||
        self.digits = digits
 | 
			
		||||
        # self.quotes_count = len(Quotes) - 1
 | 
			
		||||
 | 
			
		||||
        if isinstance(color, QtGui.QPen):
 | 
			
		||||
            self.bg_color = color.color()
 | 
			
		||||
            self.fg_color = pg.mkColor('#ffffff')
 | 
			
		||||
        elif isinstance(color, list):
 | 
			
		||||
            self.bg_color = {'>0': color[0].color(), '<0': color[1].color()}
 | 
			
		||||
            self.fg_color = pg.mkColor('#ffffff')
 | 
			
		||||
 | 
			
		||||
        self.setFlag(self.ItemIgnoresTransformations)
 | 
			
		||||
 | 
			
		||||
    def tick_to_string(self, tick_pos):
 | 
			
		||||
        raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
    def boundingRect(self):  # noqa
 | 
			
		||||
        raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
    def update_label(self, evt_post, point_view):
 | 
			
		||||
        raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
    def update_label_test(self, ypos=0, ydata=0):
 | 
			
		||||
        self.label_str = self.tick_to_string(ydata)
 | 
			
		||||
        height = self.boundingRect().height()
 | 
			
		||||
        offset = 0  # if have margins
 | 
			
		||||
        new_pos = QtCore.QPointF(0, ypos - height / 2 - offset)
 | 
			
		||||
        self.setPos(new_pos)
 | 
			
		||||
 | 
			
		||||
    def paint(self, p, option, widget):
 | 
			
		||||
        p.setRenderHint(p.TextAntialiasing, True)
 | 
			
		||||
        p.setPen(self.fg_color)
 | 
			
		||||
        if self.label_str:
 | 
			
		||||
            if not isinstance(self.bg_color, dict):
 | 
			
		||||
                bg_color = self.bg_color
 | 
			
		||||
            else:
 | 
			
		||||
                if int(self.label_str.replace(' ', '')) > 0:
 | 
			
		||||
                    bg_color = self.bg_color['>0']
 | 
			
		||||
                else:
 | 
			
		||||
                    bg_color = self.bg_color['<0']
 | 
			
		||||
            p.setOpacity(self.opacity)
 | 
			
		||||
            p.fillRect(option.rect, bg_color)
 | 
			
		||||
            p.setOpacity(1)
 | 
			
		||||
            p.setFont(_font)
 | 
			
		||||
 | 
			
		||||
        p.drawText(option.rect, self.text_flags, self.label_str)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class XAxisLabel(AxisLabel):
 | 
			
		||||
 | 
			
		||||
    text_flags = (
 | 
			
		||||
        QtCore.Qt.TextDontClip | QtCore.Qt.AlignCenter | QtCore.Qt.AlignTop
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def tick_to_string(self, tick_pos):
 | 
			
		||||
        # TODO: change to actual period
 | 
			
		||||
        tpl = self.parent.tick_tpl['D1']
 | 
			
		||||
        if tick_pos > len(Quotes):
 | 
			
		||||
            return 'Unknown Time'
 | 
			
		||||
        return fromtimestamp(Quotes[round(tick_pos)].time).strftime(tpl)
 | 
			
		||||
 | 
			
		||||
    def boundingRect(self):  # noqa
 | 
			
		||||
        return QtCore.QRectF(0, 0, 145, 50)
 | 
			
		||||
 | 
			
		||||
    def update_label(self, evt_post, point_view):
 | 
			
		||||
        ibar = point_view.x()
 | 
			
		||||
        # if ibar > self.quotes_count:
 | 
			
		||||
        #     return
 | 
			
		||||
        self.label_str = self.tick_to_string(ibar)
 | 
			
		||||
        width = self.boundingRect().width()
 | 
			
		||||
        offset = 0  # if have margins
 | 
			
		||||
        new_pos = QtCore.QPointF(evt_post.x() - width / 2 - offset, 0)
 | 
			
		||||
        self.setPos(new_pos)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class YAxisLabel(AxisLabel):
 | 
			
		||||
 | 
			
		||||
    text_flags = (
 | 
			
		||||
        QtCore.Qt.TextDontClip | QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def tick_to_string(self, tick_pos):
 | 
			
		||||
        return ('{: ,.%df}' % self.digits).format(tick_pos).replace(',', ' ')
 | 
			
		||||
 | 
			
		||||
    def boundingRect(self):  # noqa
 | 
			
		||||
        return QtCore.QRectF(0, 0, 80, 40)
 | 
			
		||||
 | 
			
		||||
    def update_label(self, evt_post, point_view):
 | 
			
		||||
        self.label_str = self.tick_to_string(point_view.y())
 | 
			
		||||
        height = self.boundingRect().height()
 | 
			
		||||
        offset = 0  # if have margins
 | 
			
		||||
        new_pos = QtCore.QPointF(0, evt_post.y() - height / 2 - offset)
 | 
			
		||||
        self.setPos(new_pos)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChartView(pg.ViewBox):
 | 
			
		||||
    """Price chart view box with interaction behaviors you'd expect from
 | 
			
		||||
    an interactive platform:
 | 
			
		||||
 | 
			
		||||
    - zoom on mouse scroll that auto fits y-axis
 | 
			
		||||
    - no vertical scrolling
 | 
			
		||||
    - zoom to a "fixed point" on the y-axis
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
        parent=None,
 | 
			
		||||
        **kwargs,
 | 
			
		||||
        # invertY=False,
 | 
			
		||||
    ):
 | 
			
		||||
        super().__init__(parent=parent, **kwargs)
 | 
			
		||||
        self.chart = parent
 | 
			
		||||
 | 
			
		||||
        # disable vertical scrolling
 | 
			
		||||
        self.setMouseEnabled(x=True, y=False)
 | 
			
		||||
 | 
			
		||||
    def wheelEvent(self, ev, axis=None):
 | 
			
		||||
        """Override "center-point" location for scrolling.
 | 
			
		||||
 | 
			
		||||
        This is an override of the ``ViewBox`` method simply changing
 | 
			
		||||
        the center of the zoom to be the y-axis.
 | 
			
		||||
 | 
			
		||||
        TODO: PR a method into ``pyqtgraph`` to make this configurable
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        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]
 | 
			
		||||
 | 
			
		||||
        # center = pg.Point(
 | 
			
		||||
        #     fn.invertQTransform(self.childGroup.transform()).map(ev.pos())
 | 
			
		||||
        # )
 | 
			
		||||
 | 
			
		||||
        # XXX: scroll "around" the right most element in the view
 | 
			
		||||
        furthest_right_coord = self.boundingRect().topRight()
 | 
			
		||||
        center = pg.Point(
 | 
			
		||||
           fn.invertQTransform(
 | 
			
		||||
               self.childGroup.transform()
 | 
			
		||||
            ).map(furthest_right_coord)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self._resetTarget()
 | 
			
		||||
        self.scaleBy(s, center)
 | 
			
		||||
        ev.accept()
 | 
			
		||||
        self.sigRangeChangedManually.emit(mask)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# 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``.
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
    eventually want multiple plots managed together).
 | 
			
		||||
    """
 | 
			
		||||
    sig_mouse_leave = QtCore.Signal(object)
 | 
			
		||||
    sig_mouse_enter = QtCore.Signal(object)
 | 
			
		||||
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
        split_charts,
 | 
			
		||||
        **kwargs,
 | 
			
		||||
        # parent=None,
 | 
			
		||||
        # background='default',
 | 
			
		||||
        # plotItem=None,
 | 
			
		||||
        # **kargs
 | 
			
		||||
    ):
 | 
			
		||||
        """Configure chart display settings.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        super().__init__(**kwargs)
 | 
			
		||||
        # label = pg.LabelItem(justify='left')
 | 
			
		||||
        # self.addItem(label)
 | 
			
		||||
        # label.setText("Yo yoyo")
 | 
			
		||||
        # label.setText("<span style='font-size: 12pt'>x=")
 | 
			
		||||
        self.parent = split_charts
 | 
			
		||||
 | 
			
		||||
        # show only right side axes
 | 
			
		||||
        self.hideAxis('left')
 | 
			
		||||
        self.showAxis('right')
 | 
			
		||||
 | 
			
		||||
        # show background grid
 | 
			
		||||
        self.showGrid(x=True, y=True, alpha=0.4)
 | 
			
		||||
 | 
			
		||||
        # use cross-hair for cursor
 | 
			
		||||
        self.setCursor(QtCore.Qt.CrossCursor)
 | 
			
		||||
 | 
			
		||||
        # set panning limits
 | 
			
		||||
        min_points_to_show = 20
 | 
			
		||||
        min_bars_in_view = 10
 | 
			
		||||
        max_lookahead = min_points_to_show - min_bars_in_view
 | 
			
		||||
        last = Quotes[-1].id
 | 
			
		||||
        self.setLimits(
 | 
			
		||||
            xMin=Quotes[0].id,
 | 
			
		||||
            xMax=last + max_lookahead,
 | 
			
		||||
            minXRange=min_points_to_show,
 | 
			
		||||
            # maxYRange=highest-lowest,
 | 
			
		||||
            yMin=Quotes.low.min() * 0.98,
 | 
			
		||||
            yMax=Quotes.high.max() * 1.02,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # show last 50 points on startup
 | 
			
		||||
        self.plotItem.vb.setXRange(last - 50, last + max_lookahead)
 | 
			
		||||
 | 
			
		||||
        # assign callback for rescaling y-axis automatically
 | 
			
		||||
        # based on y-range contents
 | 
			
		||||
        self.sigXRangeChanged.connect(self._update_yrange_limits)
 | 
			
		||||
        self._update_yrange_limits()
 | 
			
		||||
 | 
			
		||||
    def bars_range(self):
 | 
			
		||||
        """Return a range tuple for the bars present in view.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        vr = self.viewRect()
 | 
			
		||||
        lbar, rbar = int(vr.left()), int(min(vr.right(), len(Quotes) - 1))
 | 
			
		||||
        return lbar, rbar
 | 
			
		||||
 | 
			
		||||
    def draw_ohlc(
 | 
			
		||||
        self,
 | 
			
		||||
        style: ChartType = ChartType.BAR,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        """Draw OHLC datums to chart.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        # 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))
 | 
			
		||||
 | 
			
		||||
    def draw_curve(
 | 
			
		||||
        self,
 | 
			
		||||
        data: np.ndarray,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        # draw the indicator as a plain curve
 | 
			
		||||
        curve = pg.PlotDataItem(data, antialias=True)
 | 
			
		||||
        ind_chart.addItem(curve)
 | 
			
		||||
 | 
			
		||||
    def _update_yrange_limits(self):
 | 
			
		||||
        """Callback for each y-range update.
 | 
			
		||||
 | 
			
		||||
        This adds auto-scaling like zoom on the scroll wheel such
 | 
			
		||||
        that data always fits nicely inside the current view of the
 | 
			
		||||
        data set.
 | 
			
		||||
        """
 | 
			
		||||
        # TODO: this can likely be ported in part to the built-ins:
 | 
			
		||||
        # self.setYRange(Quotes.low.min() * .98, Quotes.high.max() * 1.02)
 | 
			
		||||
        # self.setMouseEnabled(x=True, y=False)
 | 
			
		||||
        # self.setXRange(Quotes[0].id, Quotes[-1].id)
 | 
			
		||||
        # self.setAutoVisible(x=False, y=True)
 | 
			
		||||
        # self.enableAutoRange(x=False, y=True)
 | 
			
		||||
 | 
			
		||||
        chart = self
 | 
			
		||||
        chart_parent = self.parent
 | 
			
		||||
 | 
			
		||||
        lbar, rbar = self.bars_range()
 | 
			
		||||
        # vr = chart.viewRect()
 | 
			
		||||
        # lbar, rbar = int(vr.left()), int(vr.right())
 | 
			
		||||
 | 
			
		||||
        if chart_parent.signals_visible:
 | 
			
		||||
            chart_parent._show_text_signals(lbar, rbar)
 | 
			
		||||
 | 
			
		||||
        bars = Quotes[lbar:rbar]
 | 
			
		||||
        ylow = bars.low.min() * 0.98
 | 
			
		||||
        yhigh = bars.high.max() * 1.02
 | 
			
		||||
 | 
			
		||||
        std = np.std(bars.close)
 | 
			
		||||
        chart.setLimits(yMin=ylow, yMax=yhigh, minYRange=std)
 | 
			
		||||
        chart.setYRange(ylow, yhigh)
 | 
			
		||||
 | 
			
		||||
        for i, d in chart_parent.indicators:
 | 
			
		||||
            # ydata = i.plotItem.items[0].getData()[1]
 | 
			
		||||
            ydata = d[lbar:rbar]
 | 
			
		||||
            ylow = ydata.min() * 0.98
 | 
			
		||||
            yhigh = ydata.max() * 1.02
 | 
			
		||||
            std = np.std(ydata)
 | 
			
		||||
            i.setLimits(yMin=ylow, yMax=yhigh, minYRange=std)
 | 
			
		||||
            i.setYRange(ylow, yhigh)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def enterEvent(self, ev):  # noqa
 | 
			
		||||
        # pg.PlotWidget.enterEvent(self, ev)
 | 
			
		||||
        self.sig_mouse_enter.emit(self)
 | 
			
		||||
 | 
			
		||||
    def leaveEvent(self, ev):  # noqa
 | 
			
		||||
        # pg.PlotWidget.leaveEvent(self, ev)
 | 
			
		||||
        self.sig_mouse_leave.emit(self)
 | 
			
		||||
        self.scene().leaveEvent(ev)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_mouse_rate_limit = 60
 | 
			
		||||
_xaxis_at = 'bottom'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CrossHairItem(pg.GraphicsObject):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, parent, indicators=None, digits=0):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        # self.pen = pg.mkPen('#000000')
 | 
			
		||||
        self.pen = pg.mkPen('#a9a9a9')
 | 
			
		||||
        self.parent = parent
 | 
			
		||||
        self.indicators = {}
 | 
			
		||||
        self.activeIndicator = None
 | 
			
		||||
        self.xaxis = self.parent.getAxis('bottom')
 | 
			
		||||
        self.yaxis = self.parent.getAxis('right')
 | 
			
		||||
 | 
			
		||||
        self.vline = self.parent.addLine(x=0, pen=self.pen, movable=False)
 | 
			
		||||
        self.hline = self.parent.addLine(y=0, pen=self.pen, movable=False)
 | 
			
		||||
 | 
			
		||||
        self.proxy_moved = pg.SignalProxy(
 | 
			
		||||
            self.parent.scene().sigMouseMoved,
 | 
			
		||||
            rateLimit=_mouse_rate_limit,
 | 
			
		||||
            slot=self.mouseMoved,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.yaxis_label = YAxisLabel(
 | 
			
		||||
            parent=self.yaxis, digits=digits, opacity=1
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        indicators = indicators or []
 | 
			
		||||
 | 
			
		||||
        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]
 | 
			
		||||
 | 
			
		||||
            self.proxy_enter = pg.SignalProxy(
 | 
			
		||||
                self.parent.sig_mouse_enter,
 | 
			
		||||
                rateLimit=_mouse_rate_limit,
 | 
			
		||||
                slot=lambda: self.mouseAction('Enter', False),
 | 
			
		||||
            )
 | 
			
		||||
            self.proxy_leave = pg.SignalProxy(
 | 
			
		||||
                self.parent.sig_mouse_leave,
 | 
			
		||||
                rateLimit=_mouse_rate_limit,
 | 
			
		||||
                slot=lambda: self.mouseAction('Leave', False),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # determine where to place x-axis label
 | 
			
		||||
        if _xaxis_at == 'bottom':
 | 
			
		||||
            # place below is last indicator subplot
 | 
			
		||||
            self.xaxis_label = XAxisLabel(
 | 
			
		||||
                parent=last_ind.getAxis('bottom'), opacity=1
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            # keep x-axis right below main chart
 | 
			
		||||
            self.xaxis_label = XAxisLabel(parent=self.xaxis, opacity=1)
 | 
			
		||||
 | 
			
		||||
        for i in indicators:
 | 
			
		||||
            # add vertial and horizonal lines and a y-axis label
 | 
			
		||||
            vl = i.addLine(x=0, pen=self.pen, movable=False)
 | 
			
		||||
            hl = i.addLine(y=0, pen=self.pen, movable=False)
 | 
			
		||||
            yl = YAxisLabel(parent=i.getAxis('right'), opacity=1)
 | 
			
		||||
 | 
			
		||||
            px_moved = pg.SignalProxy(
 | 
			
		||||
                i.scene().sigMouseMoved,
 | 
			
		||||
                rateLimit=_mouse_rate_limit,
 | 
			
		||||
                slot=self.mouseMoved
 | 
			
		||||
            )
 | 
			
		||||
            px_enter = pg.SignalProxy(
 | 
			
		||||
                i.sig_mouse_enter,
 | 
			
		||||
                rateLimit=_mouse_rate_limit,
 | 
			
		||||
                slot=lambda: self.mouseAction('Enter', i),
 | 
			
		||||
            )
 | 
			
		||||
            px_leave = pg.SignalProxy(
 | 
			
		||||
                i.sig_mouse_leave,
 | 
			
		||||
                rateLimit=_mouse_rate_limit,
 | 
			
		||||
                slot=lambda: self.mouseAction('Leave', i),
 | 
			
		||||
            )
 | 
			
		||||
            self.indicators[i] = {
 | 
			
		||||
                'vl': vl,
 | 
			
		||||
                'hl': hl,
 | 
			
		||||
                'yl': yl,
 | 
			
		||||
                'px': (px_moved, px_enter, px_leave),
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
    def mouseAction(self, action, ind=False):  # noqa
 | 
			
		||||
        if action == 'Enter':
 | 
			
		||||
            # show horiz line and y-label
 | 
			
		||||
            if ind:
 | 
			
		||||
                self.indicators[ind]['hl'].show()
 | 
			
		||||
                self.indicators[ind]['yl'].show()
 | 
			
		||||
                self.activeIndicator = ind
 | 
			
		||||
            else:
 | 
			
		||||
                self.yaxis_label.show()
 | 
			
		||||
                self.hline.show()
 | 
			
		||||
        # Leave
 | 
			
		||||
        else:
 | 
			
		||||
            # hide horiz line and y-label
 | 
			
		||||
            if ind:
 | 
			
		||||
                self.indicators[ind]['hl'].hide()
 | 
			
		||||
                self.indicators[ind]['yl'].hide()
 | 
			
		||||
                self.activeIndicator = None
 | 
			
		||||
            else:
 | 
			
		||||
                self.yaxis_label.hide()
 | 
			
		||||
                self.hline.hide()
 | 
			
		||||
 | 
			
		||||
    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]
 | 
			
		||||
 | 
			
		||||
        # if the mouse is within the parent ``ChartPlotWidget``
 | 
			
		||||
        if self.parent.sceneBoundingRect().contains(pos):
 | 
			
		||||
            # mouse_point = self.vb.mapSceneToView(pos)
 | 
			
		||||
            mouse_point = self.parent.mapToView(pos)
 | 
			
		||||
 | 
			
		||||
            # move the vertial line to the current x coordinate
 | 
			
		||||
            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)
 | 
			
		||||
 | 
			
		||||
            # update the vertical line in any indicators subplots
 | 
			
		||||
            for opts in self.indicators.values():
 | 
			
		||||
                opts['vl'].setX(mouse_point.x())
 | 
			
		||||
 | 
			
		||||
            if self.activeIndicator:
 | 
			
		||||
                # vertial position of the mouse is inside an indicator
 | 
			
		||||
                mouse_point_ind = self.activeIndicator.mapToView(pos)
 | 
			
		||||
                self.indicators[self.activeIndicator]['hl'].setY(
 | 
			
		||||
                    mouse_point_ind.y()
 | 
			
		||||
                )
 | 
			
		||||
                self.indicators[self.activeIndicator]['yl'].update_label(
 | 
			
		||||
                    evt_post=pos, point_view=mouse_point_ind
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                # vertial position of the mouse is inside the main chart
 | 
			
		||||
                self.hline.setY(mouse_point.y())
 | 
			
		||||
                self.yaxis_label.update_label(
 | 
			
		||||
                    evt_post=pos, point_view=mouse_point
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    def paint(self, p, *args):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def boundingRect(self):
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
    bull_brush = bear_brush = pg.mkPen('#808080')
 | 
			
		||||
    # bull_brush = pg.mkPen('#00cc00')
 | 
			
		||||
    # bear_brush = pg.mkPen('#fa0000')
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        self.generatePicture()
 | 
			
		||||
 | 
			
		||||
    # TODO: this is the routine to be retriggered for redraw
 | 
			
		||||
    @timeit
 | 
			
		||||
    def generatePicture(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)
 | 
			
		||||
        p.end()
 | 
			
		||||
 | 
			
		||||
    def _generate(self, p):
 | 
			
		||||
        # 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))
 | 
			
		||||
 | 
			
		||||
        p.setPen(self.bull_brush)
 | 
			
		||||
        p.drawLines(*lines[long_bars])
 | 
			
		||||
 | 
			
		||||
        p.setPen(self.bear_brush)
 | 
			
		||||
        p.drawLines(*lines[short_bars])
 | 
			
		||||
 | 
			
		||||
    def paint(self, p, *args):
 | 
			
		||||
        p.drawPicture(0, 0, self.picture)
 | 
			
		||||
 | 
			
		||||
    def boundingRect(self):
 | 
			
		||||
        # boundingRect _must_ indicate the entire area that will be
 | 
			
		||||
        # drawn on or else we will get artifacts and possibly crashing.
 | 
			
		||||
        # (in this case, QPicture does all the work of computing the
 | 
			
		||||
        # bouning rect for us)
 | 
			
		||||
        return QtCore.QRectF(self.picture.boundingRect())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CandlestickItem(BarItem):
 | 
			
		||||
 | 
			
		||||
    w2 = 0.7
 | 
			
		||||
    line_pen = pg.mkPen('#000000')
 | 
			
		||||
    bull_brush = pg.mkBrush('#00ff00')
 | 
			
		||||
    bear_brush = pg.mkBrush('#ff0000')
 | 
			
		||||
 | 
			
		||||
    def _generate(self, p):
 | 
			
		||||
        rects = np.array(
 | 
			
		||||
            [
 | 
			
		||||
                QtCore.QRectF(q.id - self.w, q.open, self.w2, q.close - q.open)
 | 
			
		||||
                for q in Quotes
 | 
			
		||||
            ]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        p.setPen(self.line_pen)
 | 
			
		||||
        p.drawLines(
 | 
			
		||||
            [QtCore.QLineF(q.id, q.low, q.id, q.high)
 | 
			
		||||
             for q in Quotes]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        p.setBrush(self.bull_brush)
 | 
			
		||||
        p.drawRects(*rects[Quotes.close > Quotes.open])
 | 
			
		||||
 | 
			
		||||
        p.setBrush(self.bear_brush)
 | 
			
		||||
        p.drawRects(*rects[Quotes.close < Quotes.open])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SplitterChart(QtGui.QWidget):
 | 
			
		||||
 | 
			
		||||
    long_pen = pg.mkPen('#006000')
 | 
			
		||||
    long_brush = pg.mkBrush('#00ff00')
 | 
			
		||||
    short_pen = pg.mkPen('#600000')
 | 
			
		||||
    short_brush = pg.mkBrush('#ff0000')
 | 
			
		||||
 | 
			
		||||
    zoomIsDisabled = QtCore.pyqtSignal(bool)
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        self.signals_visible = False
 | 
			
		||||
        self.indicators = []
 | 
			
		||||
 | 
			
		||||
        self.xaxis = FromTimeFieldDateAxis(orientation='bottom')
 | 
			
		||||
        # self.xaxis = pg.DateAxisItem()
 | 
			
		||||
 | 
			
		||||
        self.xaxis_ind = FromTimeFieldDateAxis(orientation='bottom')
 | 
			
		||||
 | 
			
		||||
        if _xaxis_at == 'bottom':
 | 
			
		||||
            self.xaxis.setStyle(showValues=False)
 | 
			
		||||
        else:
 | 
			
		||||
            self.xaxis_ind.setStyle(showValues=False)
 | 
			
		||||
 | 
			
		||||
        self.splitter = QtGui.QSplitter(QtCore.Qt.Vertical)
 | 
			
		||||
        self.splitter.setHandleWidth(5)
 | 
			
		||||
 | 
			
		||||
        self.layout = QtGui.QVBoxLayout(self)
 | 
			
		||||
        self.layout.setContentsMargins(0, 0, 0, 0)
 | 
			
		||||
 | 
			
		||||
        self.layout.addWidget(self.splitter)
 | 
			
		||||
 | 
			
		||||
    def _show_text_signals(self, lbar, rbar):
 | 
			
		||||
        signals = [
 | 
			
		||||
            sig
 | 
			
		||||
            for sig in self.signals_text_items[lbar:rbar]
 | 
			
		||||
            if isinstance(sig, CenteredTextItem)
 | 
			
		||||
        ]
 | 
			
		||||
        if len(signals) <= 50:
 | 
			
		||||
            for sig in signals:
 | 
			
		||||
                sig.show()
 | 
			
		||||
        else:
 | 
			
		||||
            for sig in signals:
 | 
			
		||||
                sig.hide()
 | 
			
		||||
 | 
			
		||||
    def _remove_signals(self):
 | 
			
		||||
        self.chart.removeItem(self.signals_group_arrow)
 | 
			
		||||
        self.chart.removeItem(self.signals_group_text)
 | 
			
		||||
        del self.signals_text_items
 | 
			
		||||
        del self.signals_group_arrow
 | 
			
		||||
        del self.signals_group_text
 | 
			
		||||
        self.signals_visible = False
 | 
			
		||||
 | 
			
		||||
    def _update_sizes(self):
 | 
			
		||||
        min_h_ind = int(self.height() * 0.2 / len(self.indicators))
 | 
			
		||||
        sizes = [int(self.height() * 0.8)]
 | 
			
		||||
        sizes.extend([min_h_ind] * len(self.indicators))
 | 
			
		||||
        self.splitter.setSizes(sizes)  # , int(self.height()*0.2)
 | 
			
		||||
 | 
			
		||||
    def plot(self, symbol):
 | 
			
		||||
        self.digits = symbol.digits
 | 
			
		||||
        self.chart = ChartPlotWidget(
 | 
			
		||||
            split_charts=self,
 | 
			
		||||
            parent=self.splitter,
 | 
			
		||||
            axisItems={'bottom': self.xaxis, 'right': PriceAxis()},
 | 
			
		||||
            viewBox=ChartView,
 | 
			
		||||
            # enableMenu=False,
 | 
			
		||||
        )
 | 
			
		||||
        # TODO: ``pyqtgraph`` doesn't pass through a parent to the
 | 
			
		||||
        # ``PlotItem`` by default; maybe we should PR this in?
 | 
			
		||||
        self.chart.plotItem.parent = self
 | 
			
		||||
 | 
			
		||||
        self.chart.getPlotItem().setContentsMargins(*CHART_MARGINS)
 | 
			
		||||
        self.chart.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
 | 
			
		||||
 | 
			
		||||
        # TODO: this is where we would load an indicator chain
 | 
			
		||||
        inds = [Quotes.open]
 | 
			
		||||
 | 
			
		||||
        for d in inds:
 | 
			
		||||
            ind = ChartPlotWidget(
 | 
			
		||||
                split_charts=self,
 | 
			
		||||
                parent=self.splitter,
 | 
			
		||||
                axisItems={'bottom': self.xaxis_ind, 'right': PriceAxis()},
 | 
			
		||||
                # axisItems={'top': self.xaxis_ind, 'right': PriceAxis()},
 | 
			
		||||
                viewBox=ChartView,
 | 
			
		||||
            )
 | 
			
		||||
            ind.plotItem.parent = 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:
 | 
			
		||||
 | 
			
		||||
            # link chart x-axis to main quotes chart
 | 
			
		||||
            ind_chart.setXLink(self.chart)
 | 
			
		||||
 | 
			
		||||
            # XXX: never do this lol
 | 
			
		||||
            # ind.setAspectLocked(1)
 | 
			
		||||
            ind_chart.draw_curve(d)
 | 
			
		||||
 | 
			
		||||
        self._update_sizes()
 | 
			
		||||
 | 
			
		||||
        ch = CrossHairItem(
 | 
			
		||||
            self.chart, [_ind for _ind, d in self.indicators], self.digits
 | 
			
		||||
        )
 | 
			
		||||
        self.chart.addItem(ch)
 | 
			
		||||
 | 
			
		||||
    def add_signals(self):
 | 
			
		||||
        self.signals_group_text = QtGui.QGraphicsItemGroup()
 | 
			
		||||
        self.signals_group_arrow = QtGui.QGraphicsItemGroup()
 | 
			
		||||
        self.signals_text_items = np.empty(len(Quotes), dtype=object)
 | 
			
		||||
 | 
			
		||||
        for p in Portfolio.positions:
 | 
			
		||||
            x, price = p.id_bar_open, p.open_price
 | 
			
		||||
            if p.type == Order.BUY:
 | 
			
		||||
                y = Quotes[x].low * 0.99
 | 
			
		||||
                pg.ArrowItem(
 | 
			
		||||
                    parent=self.signals_group_arrow,
 | 
			
		||||
                    pos=(x, y),
 | 
			
		||||
                    pen=self.long_pen,
 | 
			
		||||
                    brush=self.long_brush,
 | 
			
		||||
                    angle=90,
 | 
			
		||||
                    headLen=12,
 | 
			
		||||
                    tipAngle=50,
 | 
			
		||||
                )
 | 
			
		||||
                text_sig = CenteredTextItem(
 | 
			
		||||
                    parent=self.signals_group_text,
 | 
			
		||||
                    pos=(x, y),
 | 
			
		||||
                    pen=self.long_pen,
 | 
			
		||||
                    brush=self.long_brush,
 | 
			
		||||
                    text=('Buy at {:.%df}' % self.digits).format(price),
 | 
			
		||||
                    valign=QtCore.Qt.AlignBottom,
 | 
			
		||||
                )
 | 
			
		||||
                text_sig.hide()
 | 
			
		||||
            else:
 | 
			
		||||
                y = Quotes[x].high * 1.01
 | 
			
		||||
                pg.ArrowItem(
 | 
			
		||||
                    parent=self.signals_group_arrow,
 | 
			
		||||
                    pos=(x, y),
 | 
			
		||||
                    pen=self.short_pen,
 | 
			
		||||
                    brush=self.short_brush,
 | 
			
		||||
                    angle=-90,
 | 
			
		||||
                    headLen=12,
 | 
			
		||||
                    tipAngle=50,
 | 
			
		||||
                )
 | 
			
		||||
                text_sig = CenteredTextItem(
 | 
			
		||||
                    parent=self.signals_group_text,
 | 
			
		||||
                    pos=(x, y),
 | 
			
		||||
                    pen=self.short_pen,
 | 
			
		||||
                    brush=self.short_brush,
 | 
			
		||||
                    text=('Sell at {:.%df}' % self.digits).format(price),
 | 
			
		||||
                    valign=QtCore.Qt.AlignTop,
 | 
			
		||||
                )
 | 
			
		||||
                text_sig.hide()
 | 
			
		||||
 | 
			
		||||
            self.signals_text_items[x] = text_sig
 | 
			
		||||
 | 
			
		||||
        self.chart.addItem(self.signals_group_arrow)
 | 
			
		||||
        self.chart.addItem(self.signals_group_text)
 | 
			
		||||
        self.signals_visible = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# 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')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue