Rejig order line creation / config

In an effort to simplify line creation and management from an order
mode here's a slew of changes:
- use our new ``LevelMarker`` for order lines and fully drop usage
 of the original marker implementation stuff from `pg.InfiniteLine`
- add a left side label which shows the instrument's "units" value
  - the most fundamental unit for the "size" of the order
- allow passing in an optional `marker_size: str` so that `action: str`
  doesn't necessarily have to be passed (eg. when copying from an
  existing line)
- change a couple of internal line config options to be public attrs
  which can now be configured dynamically in real-time (since they're
  all `bool` anyway):
  * `hl_on_hover` -> `highlight_on_hover`
  * `_always_show_labels` -> `always_show_labels`
- `LevelLine.set_level()` now only sets the position if it was **not**
  called from the position changed signal (which would be redundant)
fsp_feeds
Tyler Goodlet 2021-08-15 12:17:45 -04:00
parent c37ce664f5
commit 92d6b19777
1 changed files with 115 additions and 108 deletions

View File

@ -18,22 +18,20 @@
Lines for orders, alerts, L2. Lines for orders, alerts, L2.
""" """
from functools import partial
from math import floor from math import floor
from typing import Tuple, Optional, List from typing import Tuple, Optional, List, Callable
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph import Point, functions as fn from pyqtgraph import Point, functions as fn
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QPointF from PyQt5.QtCore import QPointF
from PyQt5.QtGui import QGraphicsPathItem
from ._annotate import mk_marker_path, qgo_draw_markers from ._annotate import qgo_draw_markers, LevelMarker
from ._anchors import ( from ._anchors import (
marker_right_points, marker_right_points,
vbr_left, vbr_left,
right_axis, right_axis,
gpath_pin, # gpath_pin,
) )
from ._label import Label from ._label import Label
from ._style import hcolor, _font from ._style import hcolor, _font
@ -52,15 +50,13 @@ class LevelLine(pg.InfiniteLine):
color: str = 'default', color: str = 'default',
highlight_color: str = 'default_light', highlight_color: str = 'default_light',
dotted: bool = False, dotted: bool = False,
# marker_size: int = 20,
# UX look and feel opts # UX look and feel opts
always_show_labels: bool = False, always_show_labels: bool = False,
hl_on_hover: bool = True, highlight_on_hover: bool = True,
hide_xhair_on_hover: bool = True, hide_xhair_on_hover: bool = True,
only_show_markers_on_hover: bool = True, only_show_markers_on_hover: bool = True,
use_marker_margin: bool = False, use_marker_margin: bool = False,
movable: bool = True, movable: bool = True,
) -> None: ) -> None:
@ -77,7 +73,7 @@ class LevelLine(pg.InfiniteLine):
) )
self._chart = chart self._chart = chart
self._hoh = hl_on_hover self.highlight_on_hover = highlight_on_hover
self._dotted = dotted self._dotted = dotted
self._hide_xhair_on_hover = hide_xhair_on_hover self._hide_xhair_on_hover = hide_xhair_on_hover
@ -117,7 +113,7 @@ class LevelLine(pg.InfiniteLine):
# TODO: for when we want to move groups of lines? # TODO: for when we want to move groups of lines?
self._track_cursor: bool = False self._track_cursor: bool = False
self._always_show_labels = always_show_labels self.always_show_labels = always_show_labels
self._on_drag_start = lambda l: None self._on_drag_start = lambda l: None
self._on_drag_end = lambda l: None self._on_drag_end = lambda l: None
@ -156,7 +152,9 @@ class LevelLine(pg.InfiniteLine):
"""Position changed handler. """Position changed handler.
""" """
self.update_labels({'level': self.value()}) level = self.value()
self.update_labels({'level': level})
self.set_level(level, called_from_on_pos_change=True)
def update_labels( def update_labels(
self, self,
@ -190,19 +188,23 @@ class LevelLine(pg.InfiniteLine):
def set_level( def set_level(
self, self,
level: float, level: float,
called_from_on_pos_change: bool = False,
) -> None: ) -> None:
last = self.value()
# if the position hasn't changed then ``.update_labels()`` if not called_from_on_pos_change:
# will not be called by a non-triggered `.on_pos_change()`, last = self.value()
# so we need to call it manually to avoid mismatching
# label-to-line color when the line is updated but not # if the position hasn't changed then ``.update_labels()``
# "moved". # will not be called by a non-triggered `.on_pos_change()`,
if level == last: # so we need to call it manually to avoid mismatching
self.update_labels({'level': level}) # label-to-line color when the line is updated but not
# "moved".
if level == last:
self.update_labels({'level': level})
self.setPos(level)
self.setPos(level)
self.level = self.value() self.level = self.value()
self.update() self.update()
@ -453,7 +455,7 @@ class LevelLine(pg.InfiniteLine):
self._marker.show() self._marker.show()
# highlight if so configured # highlight if so configured
if self._hoh: if self.highlight_on_hover:
self.currentPen = self.hoverPen self.currentPen = self.hoverPen
@ -502,7 +504,7 @@ class LevelLine(pg.InfiniteLine):
if self not in cur._trackers: if self not in cur._trackers:
cur.show_xhair(y_label_level=self.value()) cur.show_xhair(y_label_level=self.value())
if not self._always_show_labels: if not self.always_show_labels:
for label in self._labels: for label in self._labels:
label.hide() label.hide()
label.txt.update() label.txt.update()
@ -514,6 +516,7 @@ class LevelLine(pg.InfiniteLine):
def level_line( def level_line(
chart: 'ChartPlotWidget', # noqa chart: 'ChartPlotWidget', # noqa
level: float, level: float,
@ -522,7 +525,7 @@ def level_line(
color: str = 'default', color: str = 'default',
# ux # ux
hl_on_hover: bool = True, highlight_on_hover: bool = True,
# label fields and options # label fields and options
always_show_labels: bool = False, always_show_labels: bool = False,
@ -534,7 +537,7 @@ def level_line(
"""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 hl_color = color + '_light' if highlight_on_hover else color
line = LevelLine( line = LevelLine(
chart, chart,
@ -546,7 +549,7 @@ def level_line(
dotted=dotted, dotted=dotted,
# UX related options # UX related options
hl_on_hover=hl_on_hover, highlight_on_hover=highlight_on_hover,
# 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)
@ -599,15 +602,14 @@ def order_line(
chart, chart,
level: float, level: float,
level_digits: float,
action: str, # buy or sell action: str, # buy or sell
marker_style: Optional[str] = None,
level_digits: Optional[float] = 3,
size: Optional[int] = 1, size: Optional[int] = 1,
size_digits: int = 1, size_digits: int = 1,
show_markers: bool = False, show_markers: bool = False,
submit_price: float = None, submit_price: float = None,
exec_type: str = 'dark',
order_type: str = 'limit',
orient_v: str = 'bottom', orient_v: str = 'bottom',
**line_kwargs, **line_kwargs,
@ -626,18 +628,72 @@ def order_line(
**line_kwargs **line_kwargs
) )
font_size = _font.font.pixelSize()
# scale marker size with dpi-aware font size
marker_size = floor(1.375 * font_size)
orient_v = 'top' if action == 'sell' else 'bottom'
if action == 'alert':
label = Label(
view=line.getViewBox(),
color=line.color,
# completely different labelling for alerts
fmt_str='alert => {level}',
)
# for now, we're just duplicating the label contents i guess..
line._labels.append(label)
# anchor to left side of view / line
label.set_x_anchor_func(vbr_left(label))
label.fields = {
'level': level,
'level_digits': level_digits,
}
marker_size = marker_size * 0.666
else:
# pp_label.scene_anchor = partial(
# gpath_pin,
# location_description='right-of-path-centered',
# gpath=marker,
# label=label,
# )
label = Label(
view=line.getViewBox(),
# display the order pos size, which is some multiple
# of the user defined base unit size
fmt_str=('units: {size:.{size_digits}f}'), # old
color=line.color,
)
label.set_x_anchor_func(vbr_left(label))
line._labels.append(label)
label.fields = {
'size': size,
'size_digits': 0,
}
label.orient_v = orient_v
label.render()
label.show()
if show_markers: if show_markers:
font_size = _font.font.pixelSize()
# scale marker size with dpi-aware font size
arrow_size = floor(1.375 * font_size)
alert_size = arrow_size * 0.666
# add arrow marker on end of line nearest y-axis # add arrow marker on end of line nearest y-axis
marker_style, marker_size = { marker_style = marker_style or {
'buy': ('|<', arrow_size), 'buy': '|<',
'sell': ('>|', arrow_size), 'sell': '>|',
'alert': ('v', alert_size), 'alert': 'v',
}[action] }[action]
# this fixes it the artifact issue! .. of course, bounding rect stuff # this fixes it the artifact issue! .. of course, bounding rect stuff
@ -648,18 +704,27 @@ def order_line(
# resetting the graphics item transform intermittently # resetting the graphics item transform intermittently
# the old way which is still somehow faster? # the old way which is still somehow faster?
marker = QGraphicsPathItem( marker = LevelMarker(
mk_marker_path( chart=chart,
marker_style, style=marker_style,
# the "position" here is now ignored since we modified get_level=line.value,
# internals to pin markers to the right end of the line size=marker_size,
# marker_size, keep_in_view=False,
# on_paint=self.update_graphics,
# uncommment for the old transform / .paint() marker method
# use_qgpath=False,
)
) )
marker.scale(marker_size, marker_size)
# marker = QGraphicsPathItem(
# mk_marker_path(
# marker_style,
# # the "position" here is now ignored since we modified
# # internals to pin markers to the right end of the line
# # marker_size,
# # uncommment for the old transform / .paint() marker method
# # use_qgpath=False,
# )
# )
# marker.scale(marker_size, marker_size)
# XXX: this is our new approach but seems slower? # XXX: this is our new approach but seems slower?
marker = line.add_marker(marker) marker = line.add_marker(marker)
@ -677,65 +742,7 @@ def order_line(
# # testing to figure out why tf that's true. # # testing to figure out why tf that's true.
# line.markers.append((marker, 0, marker_size)) # line.markers.append((marker, 0, marker_size))
orient_v = 'top' if action == 'sell' else 'bottom' marker.label = label
if action == 'alert':
llabel = Label(
view=line.getViewBox(),
color=line.color,
# completely different labelling for alerts
fmt_str='alert => {level}',
)
# for now, we're just duplicating the label contents i guess..
line._labels.append(llabel)
# anchor to left side of view / line
llabel.set_x_anchor_func(vbr_left(llabel))
llabel.fields = {
'level': level,
'level_digits': level_digits,
}
llabel.orient_v = orient_v
llabel.render()
llabel.show()
marker.label = llabel
else:
rlabel = Label(
view=line.getViewBox(),
# display the order pos size, which is some multiple
# of the user defined base unit size
fmt_str=('{size:.{size_digits}f}'), # old
color=line.color,
)
marker.label = rlabel
rlabel.scene_anchor = partial(
gpath_pin,
location_description='right-of-path-centered',
gpath=marker,
label=rlabel,
)
line._labels.append(rlabel)
rlabel.fields = {
'size': size,
'size_digits': 0,
}
rlabel.orient_v = orient_v
rlabel.render()
rlabel.show()
# sanity check # sanity check
line.update_labels({'level': level}) line.update_labels({'level': level})