Add a position line api
Add a line which shows the current average price position with and arrow marker denoting the direction (long or short). Required some further rewriting of the infinite line from pyqtgraph including: - adjusting marker (arrow) placement to be offset from axis + l1 labels - fixing the hover event to not require the `.movable` attribute to be setbasic_orders
parent
7075a968b4
commit
98bfee028a
|
@ -21,8 +21,10 @@ Lines for orders, alerts, L2.
|
||||||
from typing import Tuple, Optional, List
|
from typing import Tuple, Optional, List
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from PyQt5 import QtCore, QtGui
|
from pyqtgraph import Point, functions as fn
|
||||||
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
from PyQt5.QtCore import QPointF
|
from PyQt5.QtCore import QPointF
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from .._label import Label, vbr_left, right_axis
|
from .._label import Label, vbr_left, right_axis
|
||||||
from .._style import (
|
from .._style import (
|
||||||
|
@ -31,6 +33,67 @@ from .._style import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mk_marker(
|
||||||
|
self,
|
||||||
|
marker,
|
||||||
|
position: float = 0.5,
|
||||||
|
size: float = 10.0
|
||||||
|
) -> QtGui.QPainterPath:
|
||||||
|
"""Add a marker to be displayed on the line.
|
||||||
|
|
||||||
|
============= =========================================================
|
||||||
|
**Arguments**
|
||||||
|
marker String indicating the style of marker to add:
|
||||||
|
``'<|'``, ``'|>'``, ``'>|'``, ``'|<'``, ``'<|>'``,
|
||||||
|
``'>|<'``, ``'^'``, ``'v'``, ``'o'``
|
||||||
|
position Position (0.0-1.0) along the visible extent of the line
|
||||||
|
to place the marker. Default is 0.5.
|
||||||
|
size Size of the marker in pixels. Default is 10.0.
|
||||||
|
============= =========================================================
|
||||||
|
"""
|
||||||
|
path = QtGui.QPainterPath()
|
||||||
|
|
||||||
|
if marker == 'o':
|
||||||
|
path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
|
||||||
|
|
||||||
|
# arrow pointing away-from the top of line
|
||||||
|
if '<|' in marker:
|
||||||
|
p = QtGui.QPolygonF([Point(0.5, 0), Point(0, -0.5), Point(-0.5, 0)])
|
||||||
|
path.addPolygon(p)
|
||||||
|
path.closeSubpath()
|
||||||
|
|
||||||
|
# arrow pointing away-from the bottom of line
|
||||||
|
if '|>' in marker:
|
||||||
|
p = QtGui.QPolygonF([Point(0.5, 0), Point(0, 0.5), Point(-0.5, 0)])
|
||||||
|
path.addPolygon(p)
|
||||||
|
path.closeSubpath()
|
||||||
|
|
||||||
|
# arrow pointing in-to the top of line
|
||||||
|
if '>|' in marker:
|
||||||
|
p = QtGui.QPolygonF([Point(0.5, -0.5), Point(0, 0), Point(-0.5, -0.5)])
|
||||||
|
path.addPolygon(p)
|
||||||
|
path.closeSubpath()
|
||||||
|
|
||||||
|
# arrow pointing in-to the bottom of line
|
||||||
|
if '|<' in marker:
|
||||||
|
p = QtGui.QPolygonF([Point(0.5, 0.5), Point(0, 0), Point(-0.5, 0.5)])
|
||||||
|
path.addPolygon(p)
|
||||||
|
path.closeSubpath()
|
||||||
|
|
||||||
|
if '^' in marker:
|
||||||
|
p = QtGui.QPolygonF([Point(0, -0.5), Point(0.5, 0), Point(0, 0.5)])
|
||||||
|
path.addPolygon(p)
|
||||||
|
path.closeSubpath()
|
||||||
|
|
||||||
|
if 'v' in marker:
|
||||||
|
p = QtGui.QPolygonF([Point(0, -0.5), Point(-0.5, 0), Point(0, 0.5)])
|
||||||
|
path.addPolygon(p)
|
||||||
|
path.closeSubpath()
|
||||||
|
|
||||||
|
self._maxMarkerSize = max([m[2] / 2. for m in self.markers])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: probably worth investigating if we can
|
# TODO: probably worth investigating if we can
|
||||||
# make .boundingRect() faster:
|
# make .boundingRect() faster:
|
||||||
# https://stackoverflow.com/questions/26156486/determine-bounding-rect-of-line-in-qt
|
# https://stackoverflow.com/questions/26156486/determine-bounding-rect-of-line-in-qt
|
||||||
|
@ -52,18 +115,23 @@ class LevelLine(pg.InfiniteLine):
|
||||||
hl_on_hover: bool = True,
|
hl_on_hover: bool = True,
|
||||||
dotted: bool = False,
|
dotted: bool = False,
|
||||||
always_show_labels: bool = False,
|
always_show_labels: bool = False,
|
||||||
|
hide_xhair_on_hover: bool = True,
|
||||||
|
movable: bool = True,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
movable=True,
|
movable=movable,
|
||||||
angle=0,
|
angle=0,
|
||||||
label=None, # don't use the shitty ``InfLineLabel``
|
|
||||||
|
# don't use the shitty ``InfLineLabel``
|
||||||
|
label=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._chart = chart
|
self._chart = chart
|
||||||
self._hoh = hl_on_hover
|
self._hoh = hl_on_hover
|
||||||
self._dotted = dotted
|
self._dotted = dotted
|
||||||
|
self._hide_xhair_on_hover = hide_xhair_on_hover
|
||||||
|
|
||||||
if dotted:
|
if dotted:
|
||||||
self._style = QtCore.Qt.DashLine
|
self._style = QtCore.Qt.DashLine
|
||||||
|
@ -83,6 +151,7 @@ class LevelLine(pg.InfiniteLine):
|
||||||
self.sigPositionChanged.connect(self.on_pos_change)
|
self.sigPositionChanged.connect(self.on_pos_change)
|
||||||
|
|
||||||
# sets color to value triggering pen creation
|
# sets color to value triggering pen creation
|
||||||
|
self._hl_color = highlight_color
|
||||||
self.color = color
|
self.color = color
|
||||||
|
|
||||||
# TODO: for when we want to move groups of lines?
|
# TODO: for when we want to move groups of lines?
|
||||||
|
@ -94,16 +163,6 @@ class LevelLine(pg.InfiniteLine):
|
||||||
|
|
||||||
self._y_incr_mult = 1 / chart._lc._symbol.tick_size
|
self._y_incr_mult = 1 / chart._lc._symbol.tick_size
|
||||||
|
|
||||||
# testing markers
|
|
||||||
# self.addMarker('<|', 0.1, 3)
|
|
||||||
# self.addMarker('<|>', 0.2, 3)
|
|
||||||
# self.addMarker('>|', 0.3, 3)
|
|
||||||
# self.addMarker('>|<', 0.4, 3)
|
|
||||||
# self.addMarker('>|<', 0.5, 3)
|
|
||||||
# self.addMarker('^', 0.6, 3)
|
|
||||||
# self.addMarker('v', 0.7, 3)
|
|
||||||
# self.addMarker('o', 0.8, 3)
|
|
||||||
|
|
||||||
def txt_offsets(self) -> Tuple[int, int]:
|
def txt_offsets(self) -> Tuple[int, int]:
|
||||||
return 0, 0
|
return 0, 0
|
||||||
|
|
||||||
|
@ -116,7 +175,7 @@ class LevelLine(pg.InfiniteLine):
|
||||||
# set pens to new color
|
# set pens to new color
|
||||||
self._hcolor = color
|
self._hcolor = color
|
||||||
pen = pg.mkPen(hcolor(color))
|
pen = pg.mkPen(hcolor(color))
|
||||||
hoverpen = pg.mkPen(hcolor(color + '_light'))
|
hoverpen = pg.mkPen(hcolor(self._hl_color))
|
||||||
|
|
||||||
pen.setStyle(self._style)
|
pen.setStyle(self._style)
|
||||||
hoverpen.setStyle(self._style)
|
hoverpen.setStyle(self._style)
|
||||||
|
@ -240,6 +299,7 @@ class LevelLine(pg.InfiniteLine):
|
||||||
self.mouseHovering = hover
|
self.mouseHovering = hover
|
||||||
|
|
||||||
chart = self._chart
|
chart = self._chart
|
||||||
|
cur = chart._cursor
|
||||||
|
|
||||||
if hover:
|
if hover:
|
||||||
# highlight if so configured
|
# highlight if so configured
|
||||||
|
@ -250,13 +310,12 @@ class LevelLine(pg.InfiniteLine):
|
||||||
# label.highlight(self.hoverPen)
|
# label.highlight(self.hoverPen)
|
||||||
|
|
||||||
# add us to cursor state
|
# add us to cursor state
|
||||||
cur = chart._cursor
|
|
||||||
cur.add_hovered(self)
|
cur.add_hovered(self)
|
||||||
cur.graphics[chart]['yl'].hide()
|
|
||||||
cur.graphics[chart]['hl'].hide()
|
|
||||||
|
|
||||||
for at, label in self._labels:
|
if self._hide_xhair_on_hover:
|
||||||
label.show()
|
cur.hide_xhair()
|
||||||
|
|
||||||
|
self.show_labels()
|
||||||
|
|
||||||
# TODO: hide y-crosshair?
|
# TODO: hide y-crosshair?
|
||||||
# chart._cursor.graphics[chart]['hl'].hide()
|
# chart._cursor.graphics[chart]['hl'].hide()
|
||||||
|
@ -266,17 +325,18 @@ class LevelLine(pg.InfiniteLine):
|
||||||
else:
|
else:
|
||||||
self.currentPen = self.pen
|
self.currentPen = self.pen
|
||||||
|
|
||||||
cur = chart._cursor
|
|
||||||
cur._hovered.remove(self)
|
cur._hovered.remove(self)
|
||||||
|
|
||||||
if self not in cur._trackers:
|
if self not in cur._trackers:
|
||||||
g = cur.graphics[chart]
|
cur.show_xhair()
|
||||||
g['yl'].show()
|
# g = cur.graphics[chart]
|
||||||
g['hl'].show()
|
# g['yl'].show()
|
||||||
|
# g['hl'].show()
|
||||||
|
|
||||||
if not self._always_show_labels:
|
if not self._always_show_labels:
|
||||||
for at, label in self._labels:
|
for at, label in self._labels:
|
||||||
label.hide()
|
label.hide()
|
||||||
|
label.txt.update()
|
||||||
# label.unhighlight()
|
# label.unhighlight()
|
||||||
|
|
||||||
# highlight any attached label
|
# highlight any attached label
|
||||||
|
@ -295,9 +355,7 @@ class LevelLine(pg.InfiniteLine):
|
||||||
|
|
||||||
# highlight
|
# highlight
|
||||||
self.currentPen = self.hoverPen
|
self.currentPen = self.hoverPen
|
||||||
for at, label in self._labels:
|
self.show_labels()
|
||||||
# label.highlight(self.hoverPen)
|
|
||||||
label.show()
|
|
||||||
|
|
||||||
# XXX: normal tracking behavior pulled out from parent type
|
# XXX: normal tracking behavior pulled out from parent type
|
||||||
if self.movable and ev.button() == QtCore.Qt.LeftButton:
|
if self.movable and ev.button() == QtCore.Qt.LeftButton:
|
||||||
|
@ -322,7 +380,12 @@ class LevelLine(pg.InfiniteLine):
|
||||||
|
|
||||||
# round to nearest symbol tick
|
# round to nearest symbol tick
|
||||||
m = self._y_incr_mult
|
m = self._y_incr_mult
|
||||||
self.setPos(QPointF(pos.x(), round(pos.y() * m) / m))
|
self.setPos(
|
||||||
|
QPointF(
|
||||||
|
pos.x(),
|
||||||
|
round(pos.y() * m) / m
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self.sigDragged.emit(self)
|
self.sigDragged.emit(self)
|
||||||
|
|
||||||
|
@ -364,11 +427,112 @@ class LevelLine(pg.InfiniteLine):
|
||||||
# TODO: enter labels edit mode
|
# TODO: enter labels edit mode
|
||||||
print(f'double click {ev}')
|
print(f'double click {ev}')
|
||||||
|
|
||||||
|
def draw_markers(
|
||||||
|
self,
|
||||||
|
p: QtGui.QPainter,
|
||||||
|
left: float,
|
||||||
|
right: float,
|
||||||
|
right_offset: float,
|
||||||
|
) -> None:
|
||||||
|
# paint markers in native coordinate system
|
||||||
|
tr = p.transform()
|
||||||
|
p.resetTransform()
|
||||||
|
|
||||||
|
start = tr.map(Point(left, 0))
|
||||||
|
end = tr.map(Point(right, 0))
|
||||||
|
up = tr.map(Point(left, 1))
|
||||||
|
dif = end - start
|
||||||
|
# length = Point(dif).length()
|
||||||
|
angle = np.arctan2(dif.y(), dif.x()) * 180 / np.pi
|
||||||
|
|
||||||
|
p.translate(start)
|
||||||
|
p.rotate(angle)
|
||||||
|
|
||||||
|
up = up - start
|
||||||
|
det = up.x() * dif.y() - dif.x() * up.y()
|
||||||
|
p.scale(1, 1 if det > 0 else -1)
|
||||||
|
|
||||||
|
p.setBrush(fn.mkBrush(self.currentPen.color()))
|
||||||
|
tr = p.transform()
|
||||||
|
for path, pos, size in self.markers:
|
||||||
|
p.setTransform(tr)
|
||||||
|
# x = length * pos
|
||||||
|
x = right_offset
|
||||||
|
p.translate(x, 0)
|
||||||
|
p.scale(size, size)
|
||||||
|
p.drawPath(path)
|
||||||
|
|
||||||
|
def right_point(
|
||||||
|
self,
|
||||||
|
) -> float:
|
||||||
|
|
||||||
|
chart = self._chart
|
||||||
|
l1_len = chart._max_l1_line_len
|
||||||
|
ryaxis = chart.getAxis('right')
|
||||||
|
|
||||||
|
if self.markers:
|
||||||
|
size = self.markers[0][2]
|
||||||
|
else:
|
||||||
|
size = 0
|
||||||
|
|
||||||
|
r_axis_x = ryaxis.pos().x()
|
||||||
|
right_offset = l1_len + size + 10
|
||||||
|
right_scene_coords = r_axis_x - right_offset
|
||||||
|
|
||||||
|
right_view_coords = chart._vb.mapToView(
|
||||||
|
Point(right_scene_coords, 0)).x()
|
||||||
|
|
||||||
|
return (
|
||||||
|
right_scene_coords,
|
||||||
|
right_view_coords,
|
||||||
|
right_offset,
|
||||||
|
)
|
||||||
|
|
||||||
|
def paint(
|
||||||
|
self,
|
||||||
|
p: QtGui.QPainter,
|
||||||
|
opt: QtWidgets.QStyleOptionGraphicsItem,
|
||||||
|
w: QtWidgets.QWidget
|
||||||
|
) -> None:
|
||||||
|
"""Core paint which we override (yet again)
|
||||||
|
from pg..
|
||||||
|
|
||||||
|
"""
|
||||||
|
p.setRenderHint(p.Antialiasing)
|
||||||
|
|
||||||
|
vb_left, vb_right = self._endPoints
|
||||||
|
pen = self.currentPen
|
||||||
|
pen.setJoinStyle(QtCore.Qt.MiterJoin)
|
||||||
|
p.setPen(pen)
|
||||||
|
|
||||||
|
rsc, rvc, rosc = self.right_point()
|
||||||
|
|
||||||
|
p.drawLine(
|
||||||
|
Point(vb_left, 0),
|
||||||
|
Point(rvc, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.markers:
|
||||||
|
self.draw_markers(
|
||||||
|
p,
|
||||||
|
vb_left,
|
||||||
|
vb_right,
|
||||||
|
rsc
|
||||||
|
)
|
||||||
|
|
||||||
|
def hoverEvent(self, ev):
|
||||||
|
"""Gawd, basically overriding it all at this point...
|
||||||
|
|
||||||
|
"""
|
||||||
|
if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton):
|
||||||
|
self.setMouseHover(True)
|
||||||
|
else:
|
||||||
|
self.setMouseHover(False)
|
||||||
|
|
||||||
|
|
||||||
def level_line(
|
def level_line(
|
||||||
chart: 'ChartPlogWidget', # noqa
|
chart: 'ChartPlogWidget', # noqa
|
||||||
level: float,
|
level: float,
|
||||||
|
|
||||||
color: str = 'default',
|
color: str = 'default',
|
||||||
|
|
||||||
# size 4 font on 4k screen scaled down, so small-ish.
|
# size 4 font on 4k screen scaled down, so small-ish.
|
||||||
|
@ -390,16 +554,20 @@ def level_line(
|
||||||
|
|
||||||
orient_v: str = 'bottom',
|
orient_v: str = 'bottom',
|
||||||
|
|
||||||
|
**kwargs,
|
||||||
|
|
||||||
) -> LevelLine:
|
) -> LevelLine:
|
||||||
"""Convenience routine to add a styled horizontal line to a plot.
|
"""Convenience routine to add a styled horizontal line to a plot.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
hl_color = color + '_light' if hl_on_hover else color
|
||||||
|
|
||||||
line = LevelLine(
|
line = LevelLine(
|
||||||
chart,
|
chart,
|
||||||
color=color,
|
color=color,
|
||||||
|
|
||||||
# lookup "highlight" equivalent
|
# lookup "highlight" equivalent
|
||||||
highlight_color=color + '_light',
|
highlight_color=hl_color,
|
||||||
|
|
||||||
dotted=dotted,
|
dotted=dotted,
|
||||||
|
|
||||||
|
@ -409,6 +577,8 @@ def level_line(
|
||||||
# when set to True the label is always shown instead of just on
|
# when set to True the label is always shown instead of just on
|
||||||
# highlight (which is a privacy thing for orders)
|
# highlight (which is a privacy thing for orders)
|
||||||
always_show_labels=always_show_labels,
|
always_show_labels=always_show_labels,
|
||||||
|
|
||||||
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
chart.plotItem.addItem(line)
|
chart.plotItem.addItem(line)
|
||||||
|
@ -445,8 +615,6 @@ def order_line(
|
||||||
order_status: str = 'dark',
|
order_status: str = 'dark',
|
||||||
order_type: str = 'limit',
|
order_type: str = 'limit',
|
||||||
|
|
||||||
opacity=0.616,
|
|
||||||
|
|
||||||
orient_v: str = 'bottom',
|
orient_v: str = 'bottom',
|
||||||
|
|
||||||
**line_kwargs,
|
**line_kwargs,
|
||||||
|
@ -497,3 +665,51 @@ def order_line(
|
||||||
line.update_labels({'level': level})
|
line.update_labels({'level': level})
|
||||||
|
|
||||||
return line
|
return line
|
||||||
|
|
||||||
|
|
||||||
|
def position_line(
|
||||||
|
chart,
|
||||||
|
size: float,
|
||||||
|
|
||||||
|
level: float,
|
||||||
|
|
||||||
|
orient_v: str = 'bottom',
|
||||||
|
|
||||||
|
) -> LevelLine:
|
||||||
|
"""Convenience routine to add a line graphic representing an order
|
||||||
|
execution submitted to the EMS via the chart's "order mode".
|
||||||
|
|
||||||
|
"""
|
||||||
|
line = level_line(
|
||||||
|
chart,
|
||||||
|
level,
|
||||||
|
color='default_light',
|
||||||
|
add_label=False,
|
||||||
|
hl_on_hover=False,
|
||||||
|
movable=False,
|
||||||
|
always_show_labels=False,
|
||||||
|
hide_xhair_on_hover=False,
|
||||||
|
)
|
||||||
|
if size > 0:
|
||||||
|
line.addMarker('|<', 0.9, 20)
|
||||||
|
|
||||||
|
elif size < 0:
|
||||||
|
line.addMarker('>|', 0.9, 20)
|
||||||
|
|
||||||
|
rlabel = line.add_label(
|
||||||
|
side='left',
|
||||||
|
fmt_str='{direction}: {size}\n${$:.2f}',
|
||||||
|
)
|
||||||
|
rlabel.fields = {
|
||||||
|
'direction': 'long' if size > 0 else 'short',
|
||||||
|
'$': size * level,
|
||||||
|
'size': size,
|
||||||
|
}
|
||||||
|
rlabel.orient_v = orient_v
|
||||||
|
rlabel.render()
|
||||||
|
rlabel.show()
|
||||||
|
|
||||||
|
# sanity check
|
||||||
|
line.update_labels({'level': level})
|
||||||
|
|
||||||
|
return line
|
||||||
|
|
Loading…
Reference in New Issue