Skip to content

Commit

Permalink
- added CandleStick live plot support
Browse files Browse the repository at this point in the history
- changed update_leading_text, x, y leading line text must be specified now. This was necessary change to display Candlestick plot data.
- typing update
  • Loading branch information
domarm-comat committed Aug 13, 2022
1 parent c6d9cd8 commit 7adbea7
Show file tree
Hide file tree
Showing 15 changed files with 265 additions and 28 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,11 @@ To run built-in examples, use python3 -m parameter like:

# Available plot types #

Pglive supports four plot types: `LiveLinePlot`, `LiveScatterPlot`, `LiveHBarPlot` (horizontal bar plot)
and `LiveVBarPlot` (vertical bar plot).
Pglive supports four plot types: `LiveLinePlot`, `LiveScatterPlot`, `LiveHBarPlot` (horizontal bar plot),
`LiveVBarPlot` (vertical bar plot) and `LiveCandleStickPlot`.

![All plot types](https://i.postimg.cc/637CsKRC/pglive-allplots.gif)
![CandleStick plot](https://i.postimg.cc/0QcmMMb0/plot-candlestick.gif)

From *v0.2.0* you can use any Line, Scatter or Bar pyqgtraph with `DataConnector` directly.
No need to use specific `LivePlot` class.
Expand Down
18 changes: 18 additions & 0 deletions pglive/examples_pyqt5/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import random
import signal
import sys
from math import sin
Expand Down Expand Up @@ -30,6 +31,23 @@ def sin_wave_generator(*data_connectors, flip=False):
sleep(0.01)


def candle_generator(*data_connectors, flip=False):
"""Sine wave generator"""
x = 0
while running:
x += 1
for data_connector in data_connectors:
a,b = sin(x * 0.025), sin(x * 0.020)
s = min(a,b) - random.randint(0, 1000) * 1e-3
e = max(a, b) + random.randint(0, 1000) * 1e-3
candle = (a, b, s, e)
if flip:
data_connector.cb_append_data_point(x, candle)
else:
data_connector.cb_append_data_point(candle, x)
sleep(0.01)


def colors():
"""Primitive color cycler"""
while True:
Expand Down
23 changes: 23 additions & 0 deletions pglive/examples_pyqt5/candlestick_plot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import signal
from threading import Thread

import pglive.examples_pyqt5 as examples
from pglive.sources.data_connector import DataConnector
from pglive.sources.live_candleStickPlot import LiveCandleStickPlot
from pglive.sources.live_plot_widget import LivePlotWidget

"""
In this example Scatter plot is displayed.
"""
win = LivePlotWidget(title="Candlestick Plot @ 10Hz")
plot = LiveCandleStickPlot()
win.addItem(plot)

data_connector = DataConnector(plot, max_points=50, update_rate=10)

win.show()

Thread(target=examples.candle_generator, args=(data_connector,)).start()
signal.signal(signal.SIGINT, lambda sig, frame: examples.stop())
examples.app.exec()
examples.stop()
17 changes: 17 additions & 0 deletions pglive/examples_pyqt6/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import random
import signal
import sys
from math import sin
Expand Down Expand Up @@ -30,6 +31,22 @@ def sin_wave_generator(*data_connectors, flip=False):
sleep(0.01)


def candle_generator(*data_connectors, flip=False):
"""Sine wave generator"""
x = 0
while running:
x += 1
for data_connector in data_connectors:
a,b = sin(x * 0.025), sin(x * 0.020)
s = min(a,b) - random.randint(0, 1000) * 1e-3
e = max(a, b) + random.randint(0, 1000) * 1e-3
candle = (a, b, s, e)
if flip:
data_connector.cb_append_data_point(x, candle)
else:
data_connector.cb_append_data_point(candle, x)
sleep(0.01)

def colors():
"""Primitive color cycler"""
while True:
Expand Down
23 changes: 23 additions & 0 deletions pglive/examples_pyqt6/candlestick_plot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import signal
from threading import Thread

import pglive.examples_pyqt6 as examples
from pglive.sources.data_connector import DataConnector
from pglive.sources.live_candleStickPlot import LiveCandleStickPlot
from pglive.sources.live_plot_widget import LivePlotWidget

"""
In this example Scatter plot is displayed.
"""
win = LivePlotWidget(title="Candlestick Plot @ 10Hz")
plot = LiveCandleStickPlot()
win.addItem(plot)

data_connector = DataConnector(plot, max_points=50, update_rate=10)

win.show()

Thread(target=examples.candle_generator, args=(data_connector,)).start()
signal.signal(signal.SIGINT, lambda sig, frame: examples.stop())
examples.app.exec()
examples.stop()
18 changes: 18 additions & 0 deletions pglive/examples_pyside2/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import random
import signal
import sys
from math import sin
Expand Down Expand Up @@ -30,6 +31,23 @@ def sin_wave_generator(*data_connectors, flip=False):
sleep(0.01)


def candle_generator(*data_connectors, flip=False):
"""Sine wave generator"""
x = 0
while running:
x += 1
for data_connector in data_connectors:
a, b = sin(x * 0.025), sin(x * 0.020)
s = min(a, b) - random.randint(0, 1000) * 1e-3
e = max(a, b) + random.randint(0, 1000) * 1e-3
candle = (a, b, s, e)
if flip:
data_connector.cb_append_data_point(x, candle)
else:
data_connector.cb_append_data_point(candle, x)
sleep(0.01)


def colors():
"""Primitive color cycler"""
while True:
Expand Down
23 changes: 23 additions & 0 deletions pglive/examples_pyside2/candlestick_plot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import signal
from threading import Thread

import pglive.examples_pyside2 as examples
from pglive.sources.data_connector import DataConnector
from pglive.sources.live_candleStickPlot import LiveCandleStickPlot
from pglive.sources.live_plot_widget import LivePlotWidget

"""
In this example Scatter plot is displayed.
"""
win = LivePlotWidget(title="Candlestick Plot @ 10Hz")
plot = LiveCandleStickPlot()
win.addItem(plot)

data_connector = DataConnector(plot, max_points=50, update_rate=10)

win.show()

Thread(target=examples.candle_generator, args=(data_connector,)).start()
signal.signal(signal.SIGINT, lambda sig, frame: examples.stop())
examples.app.exec_()
examples.stop()
18 changes: 18 additions & 0 deletions pglive/examples_pyside6/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import random
import signal
import sys
from math import sin
Expand Down Expand Up @@ -30,6 +31,23 @@ def sin_wave_generator(*data_connectors, flip=False):
sleep(0.01)


def candle_generator(*data_connectors, flip=False):
"""Sine wave generator"""
x = 0
while running:
x += 1
for data_connector in data_connectors:
a, b = sin(x * 0.025), sin(x * 0.020)
s = min(a, b) - random.randint(0, 1000) * 1e-3
e = max(a, b) + random.randint(0, 1000) * 1e-3
candle = (a, b, s, e)
if flip:
data_connector.cb_append_data_point(x, candle)
else:
data_connector.cb_append_data_point(candle, x)
sleep(0.01)


def colors():
"""Primitive color cycler"""
while True:
Expand Down
23 changes: 23 additions & 0 deletions pglive/examples_pyside6/candlestick_plot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import signal
from threading import Thread

import pglive.examples_pyside6 as examples
from pglive.sources.data_connector import DataConnector
from pglive.sources.live_candleStickPlot import LiveCandleStickPlot
from pglive.sources.live_plot_widget import LivePlotWidget

"""
In this example Scatter plot is displayed.
"""
win = LivePlotWidget(title="Candlestick Plot @ 10Hz")
plot = LiveCandleStickPlot()
win.addItem(plot)

data_connector = DataConnector(plot, max_points=50, update_rate=10)

win.show()

Thread(target=examples.candle_generator, args=(data_connector,)).start()
signal.signal(signal.SIGINT, lambda sig, frame: examples.stop())
examples.app.exec()
examples.stop()
7 changes: 4 additions & 3 deletions pglive/sources/live_axis.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
from typing import Any

import pyqtgraph as pg
from pyqtgraph import debug as debug, mkPen, getConfigOption
Expand All @@ -19,7 +20,7 @@ class LiveAxis(pg.AxisItem):
"""Implements live axis"""

def __init__(self, orientation, pen=None, textPen=None, axisPen=None, linkView=None, parent=None, maxTickLength=-5,
showValues=True, text='', units='', unitPrefix='', **kwargs):
showValues=True, text='', units='', unitPrefix='', **kwargs: Any) -> None:
super().__init__(orientation, pen=pen, textPen=textPen, linkView=linkView, parent=parent,
maxTickLength=maxTickLength, showValues=showValues, text=text, units=units,
unitPrefix=unitPrefix, **kwargs)
Expand All @@ -42,7 +43,7 @@ def axisPen(self) -> QPen:
return mkPen(getConfigOption('foreground'))
return mkPen(self._axisPen)

def setAxisPen(self, *args, **kwargs) -> None:
def setAxisPen(self, *args: Any, **kwargs: Any) -> None:
"""
Set axis pen used for drawing axis line.
If no arguments are given, the default foreground color will be used.
Expand All @@ -66,7 +67,7 @@ def tickStrings(self, values: list, scale: float, spacing: float) -> list:
# No specific format
return super().tickStrings(values, scale, spacing)

def drawPicture(self, p, axisSpec, tickSpecs, textSpecs):
def drawPicture(self, p, axisSpec, tickSpecs, textSpecs) -> None:
profiler = debug.Profiler()

p.setRenderHint(p.RenderHint.Antialiasing, False)
Expand Down
69 changes: 69 additions & 0 deletions pglive/sources/live_candleStickPlot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from typing import List, Tuple

import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui

from pglive.sources.live_mixins import MixinLivePlot, MixinLeadingLine


class LiveCandleStickPlot(pg.GraphicsObject, MixinLivePlot, MixinLeadingLine):
"""Live candlestick plot, plotting data [[open, close, min, max], ...]"""
sigPlotChanged = QtCore.Signal(object)

def __init__(self, outline_color: str = "w", high_color: str = 'g', low_color: str = 'r') -> None:
"""Choose colors of candle"""
pg.GraphicsObject.__init__(self)
self.x_data = []
self.y_data = []
self.outline_pen = pg.mkPen(outline_color)
self.high_brush = pg.mkBrush(high_color)
self.low_brush = pg.mkBrush(low_color)
self.picture = QtGui.QPicture()

def paint(self, p, *args) -> None:
p.drawPicture(0, 0, self.picture)

def boundingRect(self) -> QtCore.QRect:
return QtCore.QRectF(self.picture.boundingRect())

def setData(self, x_data: List[float], y_data: List[Tuple[float]]) -> None:
"""y_data must be in format [[open, close, min, max], ...]"""
self.x_data = x_data
self.y_data = y_data
if len(x_data) != len(y_data):
raise Exception("Len of x_data must be the same as y_data")

self.picture = QtGui.QPicture()
p = QtGui.QPainter(self.picture)
p.setPen(self.outline_pen)
try:
w = (x_data[1] - x_data[0]) / 3.
except IndexError:
w = 1 / 3

for index, (open, close, min, max) in enumerate(y_data):
t = x_data[index]
p.drawLine(QtCore.QPointF(t, min), QtCore.QPointF(t, max))
if open > close:
p.setBrush(self.low_brush)
else:
p.setBrush(self.high_brush)
p.drawRect(QtCore.QRectF(t - w, open, w * 2, close - open))
p.end()

self.prepareGeometryChange()
self.informViewBoundsChanged()
self.bounds = [None, None]
self.sigPlotChanged.emit(self)

def update_leading_line(self) -> None:
"""Leading line will display all four fields"""
last_x_point = self.x_data[-1]
last_y_point = self.y_data[-1][0]
if self._vl_kwargs is not None:
self._vl_kwargs["line"].setPos(last_x_point)
if self._hl_kwargs is not None:
self._hl_kwargs["line"].setPos(last_y_point)

y_text = str([round(x, 4) for x in self.y_data[-1]])
self.update_leading_text(last_x_point, last_y_point, str(last_x_point), y_text)
14 changes: 7 additions & 7 deletions pglive/sources/live_mixins.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Union
from typing import List, Union, Any

import pyqtgraph as pg

Expand Down Expand Up @@ -45,7 +45,7 @@ class MixinLeadingLine:
_vl_kwargs = None

def set_leading_line(self, orientation: Union[LeadingLine.HORIZONTAL, LeadingLine.VERTICAL] = LeadingLine.VERTICAL,
pen: QPen = None, text_axis: str = LeadingLine.AXIS_X) -> dict:
pen: QPen = None, text_axis: str = LeadingLine.AXIS_X, **kwargs) -> dict:
text_axis = text_axis.lower()
assert text_axis in (LeadingLine.AXIS_X, LeadingLine.AXIS_Y)

Expand All @@ -58,14 +58,14 @@ def set_leading_line(self, orientation: Union[LeadingLine.HORIZONTAL, LeadingLin
_v_leading_text = pg.TextItem(color="black", angle=-90, fill=pen.color())
_v_leading_line.setZValue(999)
_v_leading_text.setZValue(999)
self._vl_kwargs = {"line": _v_leading_line, "text": _v_leading_text, "pen": pen, "text_axis": text_axis}
self._vl_kwargs = {"line": _v_leading_line, "text": _v_leading_text, "pen": pen, "text_axis": text_axis, **kwargs}
return self._vl_kwargs
elif orientation == LeadingLine.HORIZONTAL:
_h_leading_line = pg.InfiniteLine(angle=0, movable=False, pen=pen)
_h_leading_text = pg.TextItem(color="black", fill=pen.color())
_h_leading_text.setZValue(999)
_h_leading_text.setZValue(999)
self._hl_kwargs = {"line": _h_leading_line, "text": _h_leading_text, "pen": pen, "text_axis": text_axis}
self._hl_kwargs = {"line": _h_leading_line, "text": _h_leading_text, "pen": pen, "text_axis": text_axis, **kwargs}
return self._hl_kwargs

@pyqtSlot()
Expand All @@ -81,21 +81,21 @@ def y_format(self, value: Union[int, float]) -> str:
return str(round(value, 4))

@pyqtSlot()
def update_leading_text(self, x: float, y: float) -> None:
def update_leading_text(self, x: float, y: float, x_text: str, y_text: str) -> None:
"""Update position and text of Vertical and Horizontal leading text"""
vb = self.getViewBox()
width, height = vb.width(), vb.height()

if self._vl_kwargs is not None:
text_axis = self.x_format(x) if self._vl_kwargs["text_axis"] == LeadingLine.AXIS_X else self.y_format(y)
text_axis = x_text if self._vl_kwargs["text_axis"] == LeadingLine.AXIS_X else y_text
self._vl_kwargs["text"].setText(text_axis)
pixel_pos = vb.mapViewToScene(QPointF(x, y))
y_pos = 0 + self._vl_kwargs["text"].boundingRect().height() + 10
new_pos = vb.mapSceneToView(QPointF(pixel_pos.x(), y_pos))
self._vl_kwargs["text"].setPos(new_pos.x(), new_pos.y())

if self._hl_kwargs is not None:
text_axis = self.x_format(x) if self._hl_kwargs["text_axis"] == LeadingLine.AXIS_X else self.y_format(y)
text_axis = x_text if self._hl_kwargs["text_axis"] == LeadingLine.AXIS_X else y_text
self._hl_kwargs["text"].setText(text_axis)
pixel_pos = vb.mapViewToScene(QPointF(x, y))
x_pos = width - self._hl_kwargs["text"].boundingRect().width() + 21
Expand Down
Loading

0 comments on commit 7adbea7

Please sign in to comment.