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
  set
basic_orders
Tyler Goodlet 2021-03-11 21:35:09 -05:00
parent 7075a968b4
commit 98bfee028a
1 changed files with 247 additions and 31 deletions

View File

@ -21,8 +21,10 @@ Lines for orders, alerts, L2.
from typing import Tuple, Optional, List
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
import numpy as np
from .._label import Label, vbr_left, right_axis
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
# make .boundingRect() faster:
# 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,
dotted: bool = False,
always_show_labels: bool = False,
hide_xhair_on_hover: bool = True,
movable: bool = True,
) -> None:
super().__init__(
movable=True,
movable=movable,
angle=0,
label=None, # don't use the shitty ``InfLineLabel``
# don't use the shitty ``InfLineLabel``
label=None,
)
self._chart = chart
self._hoh = hl_on_hover
self._dotted = dotted
self._hide_xhair_on_hover = hide_xhair_on_hover
if dotted:
self._style = QtCore.Qt.DashLine
@ -83,6 +151,7 @@ class LevelLine(pg.InfiniteLine):
self.sigPositionChanged.connect(self.on_pos_change)
# sets color to value triggering pen creation
self._hl_color = highlight_color
self.color = color
# 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
# 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]:
return 0, 0
@ -116,7 +175,7 @@ class LevelLine(pg.InfiniteLine):
# set pens to new color
self._hcolor = color
pen = pg.mkPen(hcolor(color))
hoverpen = pg.mkPen(hcolor(color + '_light'))
hoverpen = pg.mkPen(hcolor(self._hl_color))
pen.setStyle(self._style)
hoverpen.setStyle(self._style)
@ -240,6 +299,7 @@ class LevelLine(pg.InfiniteLine):
self.mouseHovering = hover
chart = self._chart
cur = chart._cursor
if hover:
# highlight if so configured
@ -250,13 +310,12 @@ class LevelLine(pg.InfiniteLine):
# label.highlight(self.hoverPen)
# add us to cursor state
cur = chart._cursor
cur.add_hovered(self)
cur.graphics[chart]['yl'].hide()
cur.graphics[chart]['hl'].hide()
for at, label in self._labels:
label.show()
if self._hide_xhair_on_hover:
cur.hide_xhair()
self.show_labels()
# TODO: hide y-crosshair?
# chart._cursor.graphics[chart]['hl'].hide()
@ -266,17 +325,18 @@ class LevelLine(pg.InfiniteLine):
else:
self.currentPen = self.pen
cur = chart._cursor
cur._hovered.remove(self)
if self not in cur._trackers:
g = cur.graphics[chart]
g['yl'].show()
g['hl'].show()
cur.show_xhair()
# g = cur.graphics[chart]
# g['yl'].show()
# g['hl'].show()
if not self._always_show_labels:
for at, label in self._labels:
label.hide()
label.txt.update()
# label.unhighlight()
# highlight any attached label
@ -295,9 +355,7 @@ class LevelLine(pg.InfiniteLine):
# highlight
self.currentPen = self.hoverPen
for at, label in self._labels:
# label.highlight(self.hoverPen)
label.show()
self.show_labels()
# XXX: normal tracking behavior pulled out from parent type
if self.movable and ev.button() == QtCore.Qt.LeftButton:
@ -322,7 +380,12 @@ class LevelLine(pg.InfiniteLine):
# round to nearest symbol tick
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)
@ -364,11 +427,112 @@ class LevelLine(pg.InfiniteLine):
# TODO: enter labels edit mode
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(
chart: 'ChartPlogWidget', # noqa
level: float,
color: str = 'default',
# size 4 font on 4k screen scaled down, so small-ish.
@ -390,16 +554,20 @@ def level_line(
orient_v: str = 'bottom',
**kwargs,
) -> LevelLine:
"""Convenience routine to add a styled horizontal line to a plot.
"""
hl_color = color + '_light' if hl_on_hover else color
line = LevelLine(
chart,
color=color,
# lookup "highlight" equivalent
highlight_color=color + '_light',
highlight_color=hl_color,
dotted=dotted,
@ -409,6 +577,8 @@ def level_line(
# when set to True the label is always shown instead of just on
# highlight (which is a privacy thing for orders)
always_show_labels=always_show_labels,
**kwargs,
)
chart.plotItem.addItem(line)
@ -445,8 +615,6 @@ def order_line(
order_status: str = 'dark',
order_type: str = 'limit',
opacity=0.616,
orient_v: str = 'bottom',
**line_kwargs,
@ -497,3 +665,51 @@ def order_line(
line.update_labels({'level': level})
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