Support arbitrary fields (with update) in labels
This turned into a larger endeavour then intended but now we're using our own label system on level lines to be able to display things nicely **pinned wherever we want in the UI**. Keep the old ``LevelLabel`` for now for the L1 graphics but we'll likely replace this as well since i'm pretty sure the new label type (which wraps `QGraphicsTextItem`) is more performant anyway.basic_orders
parent
cbf259f3f3
commit
f51e503e47
|
@ -18,74 +18,93 @@
|
||||||
Lines for orders, alerts, L2.
|
Lines for orders, alerts, L2.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from typing import Tuple, Dict, Any, Optional
|
from typing import Tuple, Optional, List
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from PyQt5 import QtCore, QtGui
|
from PyQt5 import QtCore, QtGui
|
||||||
from PyQt5.QtCore import QPointF
|
from PyQt5.QtCore import QPointF
|
||||||
|
|
||||||
|
from .._label import Label, vbr_left, right_axis
|
||||||
from .._style import (
|
from .._style import (
|
||||||
hcolor,
|
hcolor,
|
||||||
_down_2_font_inches_we_like,
|
_down_2_font_inches_we_like,
|
||||||
# _font,
|
|
||||||
# DpiAwareFont
|
|
||||||
)
|
)
|
||||||
from .._axes import YAxisLabel
|
from .._axes import YAxisLabel
|
||||||
|
|
||||||
|
|
||||||
class LevelLabel(YAxisLabel):
|
class LevelLabel(YAxisLabel):
|
||||||
"""Y-axis oriented label that sticks to where it's placed despite
|
"""Y-axis (vertically) oriented, horizontal label that sticks to
|
||||||
chart resizing and supports displaying multiple fields.
|
where it's placed despite chart resizing and supports displaying
|
||||||
|
multiple fields.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
_w_margin = 4
|
_x_margin = 0
|
||||||
_h_margin = 3
|
_y_margin = 0
|
||||||
|
|
||||||
# adjustment "further away from" parent axis
|
# adjustment "further away from" anchor point
|
||||||
_x_offset = 0
|
_x_offset = 9
|
||||||
|
_y_offset = 0
|
||||||
|
|
||||||
# fields to be displayed
|
# fields to be displayed in the label string
|
||||||
# class fields:
|
_fields = {
|
||||||
level: float = 0.0
|
'level': 0,
|
||||||
digits: int = 2
|
'level_digits': 2,
|
||||||
size: float = 2.0
|
}
|
||||||
size_digits: int = int(2.0)
|
# default label template is just a y-level with so much precision
|
||||||
|
_fmt_str = '{level:,.{level_digits}f}'
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
chart,
|
chart,
|
||||||
*args,
|
parent,
|
||||||
|
|
||||||
color: str = 'bracket',
|
color: str = 'bracket',
|
||||||
|
|
||||||
orient_v: str = 'bottom',
|
orient_v: str = 'bottom',
|
||||||
orient_h: str = 'left',
|
orient_h: str = 'left',
|
||||||
**kwargs
|
|
||||||
|
opacity: float = 0,
|
||||||
|
|
||||||
|
# makes order line labels offset from their parent axis
|
||||||
|
# such that they don't collide with the L1/L2 lines/prices
|
||||||
|
# that are displayed on the axis
|
||||||
|
adjust_to_l1: bool = False,
|
||||||
|
|
||||||
|
**axis_label_kwargs,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
chart,
|
chart,
|
||||||
*args,
|
parent=parent,
|
||||||
use_arrow=False,
|
use_arrow=False,
|
||||||
**kwargs
|
opacity=opacity,
|
||||||
|
**axis_label_kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: this is kinda cludgy
|
# TODO: this is kinda cludgy
|
||||||
self._hcolor = None
|
self._hcolor: pg.Pen = None
|
||||||
self.color = color
|
self.color: str = color
|
||||||
|
|
||||||
# orientation around axis options
|
# orientation around axis options
|
||||||
self._orient_v = orient_v
|
self._orient_v = orient_v
|
||||||
self._orient_h = orient_h
|
self._orient_h = orient_h
|
||||||
|
|
||||||
|
self._adjust_to_l1 = adjust_to_l1
|
||||||
|
|
||||||
self._v_shift = {
|
self._v_shift = {
|
||||||
'top': 1.,
|
'top': -1.,
|
||||||
'bottom': 0,
|
'bottom': 0.,
|
||||||
'middle': 1 / 2.
|
'middle': 1 / 2.
|
||||||
}[orient_v]
|
}[orient_v]
|
||||||
|
|
||||||
self._h_shift = {
|
self._h_shift = {
|
||||||
'left': -1., 'right': 0
|
'left': -1.,
|
||||||
|
'right': 0.
|
||||||
}[orient_h]
|
}[orient_h]
|
||||||
|
|
||||||
self._fmt_fields: Dict[str, Dict[str, Any]] = {}
|
self.fields = self._fields.copy()
|
||||||
self._use_extra_fields: bool = False
|
# ensure default format fields are in correct
|
||||||
|
self.set_fmt_str(self._fmt_str, self.fields)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def color(self):
|
def color(self):
|
||||||
|
@ -96,42 +115,65 @@ class LevelLabel(YAxisLabel):
|
||||||
self._hcolor = color
|
self._hcolor = color
|
||||||
self._pen = self.pen = pg.mkPen(hcolor(color))
|
self._pen = self.pen = pg.mkPen(hcolor(color))
|
||||||
|
|
||||||
|
def update_on_resize(self, vr, r):
|
||||||
|
"""Tiis is a ``.sigRangeChanged()`` handler.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.update_fields(self.fields)
|
||||||
|
|
||||||
|
def update_fields(
|
||||||
|
self,
|
||||||
|
fields: dict = None,
|
||||||
|
) -> None:
|
||||||
|
"""Update the label's text contents **and** position from
|
||||||
|
a view box coordinate datum.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.fields.update(fields)
|
||||||
|
level = self.fields['level']
|
||||||
|
|
||||||
|
# map "level" to local coords
|
||||||
|
abs_xy = self._chart.mapFromView(QPointF(0, level))
|
||||||
|
|
||||||
|
self.update_label(
|
||||||
|
abs_xy,
|
||||||
|
self.fields,
|
||||||
|
)
|
||||||
|
|
||||||
def update_label(
|
def update_label(
|
||||||
self,
|
self,
|
||||||
abs_pos: QPointF, # scene coords
|
abs_pos: QPointF, # scene coords
|
||||||
level: float, # data for text
|
fields: dict,
|
||||||
offset: int = 1 # if have margins, k?
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
# write contents, type specific
|
# write contents, type specific
|
||||||
h, w = self.set_label_str(level)
|
h, w = self.set_label_str(fields)
|
||||||
|
|
||||||
|
if self._adjust_to_l1:
|
||||||
|
self._x_offset = _max_l1_line_len
|
||||||
|
|
||||||
# this triggers ``.paint()`` implicitly or no?
|
|
||||||
self.setPos(QPointF(
|
self.setPos(QPointF(
|
||||||
self._h_shift * w - self._x_offset,
|
self._h_shift * (w + self._x_offset),
|
||||||
abs_pos.y() - (self._v_shift * h) - offset
|
abs_pos.y() + self._v_shift * h
|
||||||
))
|
))
|
||||||
# trigger .paint()
|
|
||||||
self.update()
|
|
||||||
|
|
||||||
self.level = level
|
def set_fmt_str(
|
||||||
|
self,
|
||||||
|
fmt_str: str,
|
||||||
|
fields: dict,
|
||||||
|
) -> (str, str):
|
||||||
|
# test that new fmt str can be rendered
|
||||||
|
self._fmt_str = fmt_str
|
||||||
|
self.set_label_str(fields)
|
||||||
|
self.fields.update(fields)
|
||||||
|
return fmt_str, self.label_str
|
||||||
|
|
||||||
def set_label_str(self, level: float):
|
def set_label_str(
|
||||||
|
self,
|
||||||
|
fields: dict,
|
||||||
|
):
|
||||||
# use space as e3 delim
|
# use space as e3 delim
|
||||||
label_str = (f'{level:,.{self.digits}f} ').replace(',', ' ')
|
self.label_str = self._fmt_str.format(**fields).replace(',', ' ')
|
||||||
|
|
||||||
# XXX: not huge on this approach but we need a more formal
|
|
||||||
# way to define "label fields" that i don't have the brain space
|
|
||||||
# for atm.. it's at least a **lot** better then the wacky
|
|
||||||
# internals of InfLinelabel or wtv.
|
|
||||||
|
|
||||||
# mutate label to contain any extra defined format fields
|
|
||||||
if self._use_extra_fields:
|
|
||||||
for fmt_str, fields in self._fmt_fields.items():
|
|
||||||
label_str = fmt_str.format(
|
|
||||||
**{f: getattr(self, f) for f in fields}) + label_str
|
|
||||||
|
|
||||||
self.label_str = label_str
|
|
||||||
|
|
||||||
br = self.boundingRect()
|
br = self.boundingRect()
|
||||||
h, w = br.height(), br.width()
|
h, w = br.height(), br.width()
|
||||||
|
@ -147,6 +189,8 @@ class LevelLabel(YAxisLabel):
|
||||||
) -> None:
|
) -> None:
|
||||||
p.setPen(self._pen)
|
p.setPen(self._pen)
|
||||||
|
|
||||||
|
rect = self.rect
|
||||||
|
|
||||||
if self._orient_v == 'bottom':
|
if self._orient_v == 'bottom':
|
||||||
lp, rp = rect.topLeft(), rect.topRight()
|
lp, rp = rect.topLeft(), rect.topRight()
|
||||||
# p.drawLine(rect.topLeft(), rect.topRight())
|
# p.drawLine(rect.topLeft(), rect.topRight())
|
||||||
|
@ -164,13 +208,6 @@ class LevelLabel(YAxisLabel):
|
||||||
self._pen = self.pen
|
self._pen = self.pen
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
# def view_size(self):
|
|
||||||
# """Widgth and height of this label in view box coordinates.
|
|
||||||
|
|
||||||
# """
|
|
||||||
# return self.height()
|
|
||||||
# self._chart.mapFromView(QPointF(index, value)),
|
|
||||||
|
|
||||||
|
|
||||||
# global for now but probably should be
|
# global for now but probably should be
|
||||||
# attached to chart instance?
|
# attached to chart instance?
|
||||||
|
@ -179,20 +216,19 @@ _max_l1_line_len: float = 0
|
||||||
|
|
||||||
class L1Label(LevelLabel):
|
class L1Label(LevelLabel):
|
||||||
|
|
||||||
size: float = 0
|
|
||||||
size_digits: int = 3
|
|
||||||
|
|
||||||
text_flags = (
|
text_flags = (
|
||||||
QtCore.Qt.TextDontClip
|
QtCore.Qt.TextDontClip
|
||||||
| QtCore.Qt.AlignLeft
|
| QtCore.Qt.AlignLeft
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_label_str(self, level: float) -> None:
|
def set_label_str(
|
||||||
"""Reimplement the label string write to include the level's order-queue's
|
self,
|
||||||
size in the text, eg. 100 x 323.3.
|
fields: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Make sure the max L1 line module var is kept up to date.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
h, w = super().set_label_str(level)
|
h, w = super().set_label_str(fields)
|
||||||
|
|
||||||
# Set a global "max L1 label length" so we can look it up
|
# Set a global "max L1 label length" so we can look it up
|
||||||
# on order lines and adjust their labels not to overlap with it.
|
# on order lines and adjust their labels not to overlap with it.
|
||||||
|
@ -206,8 +242,6 @@ class L1Labels:
|
||||||
"""Level 1 bid ask labels for dynamic update on price-axis.
|
"""Level 1 bid ask labels for dynamic update on price-axis.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
max_value: float = '100.0 x 100 000.00'
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
chart: 'ChartPlotWidget', # noqa
|
chart: 'ChartPlotWidget', # noqa
|
||||||
|
@ -218,39 +252,41 @@ class L1Labels:
|
||||||
|
|
||||||
self.chart = chart
|
self.chart = chart
|
||||||
|
|
||||||
self.bid_label = L1Label(
|
raxis = chart.getAxis('right')
|
||||||
chart=chart,
|
kwargs = {
|
||||||
parent=chart.getAxis('right'),
|
'chart': chart,
|
||||||
opacity=1,
|
'parent': raxis,
|
||||||
font_size_inches=font_size_inches,
|
|
||||||
bg_color='papas_special',
|
'opacity': 1,
|
||||||
fg_color='bracket',
|
'font_size_inches': font_size_inches,
|
||||||
|
'fg_color': chart.pen_color,
|
||||||
|
'bg_color': chart.view_color,
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt_str = (
|
||||||
|
'{size:.{size_digits}f} x '
|
||||||
|
'{level:,.{level_digits}f}'
|
||||||
|
)
|
||||||
|
fields = {
|
||||||
|
'level': 0,
|
||||||
|
'level_digits': digits,
|
||||||
|
'size': 0,
|
||||||
|
'size_digits': size_digits,
|
||||||
|
}
|
||||||
|
|
||||||
|
bid = self.bid_label = L1Label(
|
||||||
orient_v='bottom',
|
orient_v='bottom',
|
||||||
|
**kwargs,
|
||||||
)
|
)
|
||||||
self.bid_label.size_digits = size_digits
|
bid.set_fmt_str(fmt_str=fmt_str, fields=fields)
|
||||||
self.bid_label.digits = digits
|
bid.show()
|
||||||
# self.bid_label._size_br_from_str(self.max_value)
|
|
||||||
|
|
||||||
self.ask_label = L1Label(
|
ask = self.ask_label = L1Label(
|
||||||
chart=chart,
|
|
||||||
parent=chart.getAxis('right'),
|
|
||||||
opacity=1,
|
|
||||||
font_size_inches=font_size_inches,
|
|
||||||
bg_color='papas_special',
|
|
||||||
fg_color='bracket',
|
|
||||||
orient_v='top',
|
orient_v='top',
|
||||||
|
**kwargs,
|
||||||
)
|
)
|
||||||
self.ask_label.size_digits = size_digits
|
ask.set_fmt_str(fmt_str=fmt_str, fields=fields)
|
||||||
self.ask_label.digits = digits
|
ask.show()
|
||||||
# self.ask_label._size_br_from_str(self.max_value)
|
|
||||||
|
|
||||||
self.bid_label._use_extra_fields = True
|
|
||||||
self.ask_label._use_extra_fields = True
|
|
||||||
|
|
||||||
self.bid_label._fmt_fields['{size:.{size_digits}f} x '] = {
|
|
||||||
'size', 'size_digits'}
|
|
||||||
self.ask_label._fmt_fields['{size:.{size_digits}f} x '] = {
|
|
||||||
'size', 'size_digits'}
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: probably worth investigating if we can
|
# TODO: probably worth investigating if we can
|
||||||
|
@ -264,32 +300,46 @@ class LevelLine(pg.InfiniteLine):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
chart: 'ChartPlotWidget', # type: ignore # noqa
|
chart: 'ChartPlotWidget', # type: ignore # noqa
|
||||||
label: LevelLabel,
|
|
||||||
color: str = 'default',
|
color: str = 'default',
|
||||||
highlight_color: str = 'default_light',
|
highlight_color: str = 'default_light',
|
||||||
|
|
||||||
hl_on_hover: bool = True,
|
hl_on_hover: bool = True,
|
||||||
dotted: bool = False,
|
dotted: bool = False,
|
||||||
adjust_to_l1: bool = False,
|
always_show_labels: bool = False,
|
||||||
always_show_label: bool = False,
|
|
||||||
**kwargs,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
super().__init__(
|
||||||
self.label = label
|
movable=True,
|
||||||
|
angle=0,
|
||||||
self.sigPositionChanged.connect(self.set_level)
|
label=None, # don't use the shitty ``InfLineLabel``
|
||||||
|
)
|
||||||
|
|
||||||
self._chart = chart
|
self._chart = chart
|
||||||
self._hoh = hl_on_hover
|
self._hoh = hl_on_hover
|
||||||
self._dotted = dotted
|
self._dotted = dotted
|
||||||
|
self._hcolor: str = None
|
||||||
|
|
||||||
self._hcolor = None
|
# the float y-value in the view coords
|
||||||
|
self.level: float = 0
|
||||||
|
|
||||||
|
# list of labels anchored at one of the 2 line endpoints
|
||||||
|
# inside the viewbox
|
||||||
|
self._labels: List[(int, Label)] = []
|
||||||
|
|
||||||
|
# whenever this line is moved trigger label updates
|
||||||
|
self.sigPositionChanged.connect(self.on_pos_change)
|
||||||
|
|
||||||
|
# sets color to value triggering pen creation
|
||||||
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?
|
||||||
self._track_cursor: bool = False
|
self._track_cursor: bool = False
|
||||||
self._adjust_to_l1 = adjust_to_l1
|
self._always_show_labels = always_show_labels
|
||||||
self._always_show_label = always_show_label
|
|
||||||
|
# # indexed by int
|
||||||
|
# self._endpoints = (None, None)
|
||||||
|
|
||||||
# testing markers
|
# testing markers
|
||||||
# self.addMarker('<|', 0.1, 3)
|
# self.addMarker('<|', 0.1, 3)
|
||||||
|
@ -301,6 +351,9 @@ class LevelLine(pg.InfiniteLine):
|
||||||
# self.addMarker('v', 0.7, 3)
|
# self.addMarker('v', 0.7, 3)
|
||||||
# self.addMarker('o', 0.8, 3)
|
# self.addMarker('o', 0.8, 3)
|
||||||
|
|
||||||
|
def txt_offsets(self) -> Tuple[int, int]:
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def color(self):
|
def color(self):
|
||||||
return self._hcolor
|
return self._hcolor
|
||||||
|
@ -323,15 +376,84 @@ class LevelLine(pg.InfiniteLine):
|
||||||
hoverpen.setWidth(2)
|
hoverpen.setWidth(2)
|
||||||
self.hoverPen = hoverpen
|
self.hoverPen = hoverpen
|
||||||
|
|
||||||
def set_level(self) -> None:
|
def add_label(
|
||||||
|
self,
|
||||||
|
|
||||||
label = self.label
|
# by default we only display the line's level value
|
||||||
|
# in the label
|
||||||
|
fmt_str: str = (
|
||||||
|
'{level:,.{level_digits}f}'
|
||||||
|
),
|
||||||
|
side: str = 'right',
|
||||||
|
|
||||||
# TODO: a better way to accomplish this...
|
font_size_inches: float = _down_2_font_inches_we_like,
|
||||||
if self._adjust_to_l1:
|
color: str = None,
|
||||||
label._x_offset = _max_l1_line_len
|
bg_color: str = None,
|
||||||
|
|
||||||
label.update_from_data(0, self.value())
|
**label_kwargs,
|
||||||
|
) -> LevelLabel:
|
||||||
|
"""Add a ``LevelLabel`` anchored at one of the line endpoints in view.
|
||||||
|
|
||||||
|
"""
|
||||||
|
vb = self.getViewBox()
|
||||||
|
|
||||||
|
label = Label(
|
||||||
|
view=vb,
|
||||||
|
fmt_str=fmt_str,
|
||||||
|
color=self.color,
|
||||||
|
)
|
||||||
|
|
||||||
|
if side == 'right':
|
||||||
|
label.set_x_anchor_func(right_axis(self._chart, label))
|
||||||
|
elif side == 'left':
|
||||||
|
label.set_x_anchor_func(vbr_left(label))
|
||||||
|
|
||||||
|
self._labels.append((side, label))
|
||||||
|
|
||||||
|
return label
|
||||||
|
|
||||||
|
def on_pos_change(
|
||||||
|
self,
|
||||||
|
line: 'LevelLine', # noqa
|
||||||
|
) -> None:
|
||||||
|
"""Position changed handler.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.update_labels({'level': self.value()})
|
||||||
|
|
||||||
|
def update_labels(
|
||||||
|
self,
|
||||||
|
fields_data: dict,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
for at, label in self._labels:
|
||||||
|
label.color = self.color
|
||||||
|
|
||||||
|
label.fields.update(fields_data)
|
||||||
|
|
||||||
|
level = fields_data.get('level')
|
||||||
|
if level:
|
||||||
|
label.set_view_y(level)
|
||||||
|
|
||||||
|
label.render()
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def hide_labels(self) -> None:
|
||||||
|
for at, label in self._labels:
|
||||||
|
label.hide()
|
||||||
|
|
||||||
|
def show_labels(self) -> None:
|
||||||
|
for at, label in self._labels:
|
||||||
|
label.show()
|
||||||
|
|
||||||
|
def set_level(
|
||||||
|
self,
|
||||||
|
level: float,
|
||||||
|
) -> None:
|
||||||
|
self.setPos(level)
|
||||||
|
self.level = self.value()
|
||||||
|
self.update()
|
||||||
|
|
||||||
def on_tracked_source(
|
def on_tracked_source(
|
||||||
self,
|
self,
|
||||||
|
@ -342,8 +464,7 @@ class LevelLine(pg.InfiniteLine):
|
||||||
# line is set to track the cursor: for every movement
|
# line is set to track the cursor: for every movement
|
||||||
# this callback is invoked to reposition the line
|
# this callback is invoked to reposition the line
|
||||||
self.movable = True
|
self.movable = True
|
||||||
self.setPos(y) # implictly calls ``.set_level()``
|
self.set_level(y) # implictly calls reposition handler
|
||||||
self.update()
|
|
||||||
|
|
||||||
def setMouseHover(self, hover: bool) -> None:
|
def setMouseHover(self, hover: bool) -> None:
|
||||||
"""Mouse hover callback.
|
"""Mouse hover callback.
|
||||||
|
@ -361,12 +482,18 @@ class LevelLine(pg.InfiniteLine):
|
||||||
# highlight if so configured
|
# highlight if so configured
|
||||||
if self._hoh:
|
if self._hoh:
|
||||||
self.currentPen = self.hoverPen
|
self.currentPen = self.hoverPen
|
||||||
self.label.highlight(self.hoverPen)
|
|
||||||
|
# for at, label in self._labels:
|
||||||
|
# label.highlight(self.hoverPen)
|
||||||
|
|
||||||
# add us to cursor state
|
# add us to cursor state
|
||||||
chart._cursor.add_hovered(self)
|
cur = chart._cursor
|
||||||
|
cur.add_hovered(self)
|
||||||
|
cur.graphics[chart]['yl'].hide()
|
||||||
|
|
||||||
|
for at, label in self._labels:
|
||||||
|
label.show()
|
||||||
|
|
||||||
self.label.show()
|
|
||||||
# TODO: hide y-crosshair?
|
# TODO: hide y-crosshair?
|
||||||
# chart._cursor.graphics[chart]['hl'].hide()
|
# chart._cursor.graphics[chart]['hl'].hide()
|
||||||
|
|
||||||
|
@ -374,12 +501,15 @@ class LevelLine(pg.InfiniteLine):
|
||||||
# self.setCursor(QtCore.Qt.DragMoveCursor)
|
# self.setCursor(QtCore.Qt.DragMoveCursor)
|
||||||
else:
|
else:
|
||||||
self.currentPen = self.pen
|
self.currentPen = self.pen
|
||||||
self.label.unhighlight()
|
|
||||||
|
|
||||||
chart._cursor._hovered.remove(self)
|
cur = chart._cursor
|
||||||
|
cur._hovered.remove(self)
|
||||||
|
cur.graphics[chart]['yl'].show()
|
||||||
|
|
||||||
if not self._always_show_label:
|
if not self._always_show_labels:
|
||||||
self.label.hide()
|
for at, label in self._labels:
|
||||||
|
label.hide()
|
||||||
|
# label.unhighlight()
|
||||||
|
|
||||||
# highlight any attached label
|
# highlight any attached label
|
||||||
|
|
||||||
|
@ -387,12 +517,16 @@ class LevelLine(pg.InfiniteLine):
|
||||||
|
|
||||||
def mouseDragEvent(self, ev):
|
def mouseDragEvent(self, ev):
|
||||||
chart = self._chart
|
chart = self._chart
|
||||||
|
|
||||||
# hide y-crosshair
|
# hide y-crosshair
|
||||||
chart._cursor.graphics[chart]['hl'].hide()
|
chart._cursor.graphics[chart]['hl'].hide()
|
||||||
|
|
||||||
# highlight
|
# highlight
|
||||||
self.currentPen = self.hoverPen
|
self.currentPen = self.hoverPen
|
||||||
self.label.highlight(self.hoverPen)
|
# self.label.highlight(self.hoverPen)
|
||||||
|
for at, label in self._labels:
|
||||||
|
# label.highlight(self.hoverPen)
|
||||||
|
label.show()
|
||||||
|
|
||||||
# normal tracking behavior
|
# normal tracking behavior
|
||||||
super().mouseDragEvent(ev)
|
super().mouseDragEvent(ev)
|
||||||
|
@ -400,15 +534,8 @@ class LevelLine(pg.InfiniteLine):
|
||||||
# This is the final position in the drag
|
# This is the final position in the drag
|
||||||
if ev.isFinish():
|
if ev.isFinish():
|
||||||
# show y-crosshair again
|
# show y-crosshair again
|
||||||
chart = self._chart
|
|
||||||
chart._cursor.graphics[chart]['hl'].show()
|
chart._cursor.graphics[chart]['hl'].show()
|
||||||
|
|
||||||
def mouseDoubleClickEvent(
|
|
||||||
self,
|
|
||||||
ev: QtGui.QMouseEvent,
|
|
||||||
) -> None:
|
|
||||||
print(f'double click {ev}')
|
|
||||||
|
|
||||||
def delete(self) -> None:
|
def delete(self) -> None:
|
||||||
"""Remove this line from containing chart/view/scene.
|
"""Remove this line from containing chart/view/scene.
|
||||||
|
|
||||||
|
@ -416,29 +543,26 @@ class LevelLine(pg.InfiniteLine):
|
||||||
scene = self.scene()
|
scene = self.scene()
|
||||||
if scene:
|
if scene:
|
||||||
# self.label.parent.scene().removeItem(self.label)
|
# self.label.parent.scene().removeItem(self.label)
|
||||||
scene.removeItem(self.label)
|
for at, label in self._labels:
|
||||||
|
label.delete()
|
||||||
|
|
||||||
|
self._labels.clear()
|
||||||
|
|
||||||
self._chart.plotItem.removeItem(self)
|
self._chart.plotItem.removeItem(self)
|
||||||
|
|
||||||
def getEndpoints(self):
|
def mouseDoubleClickEvent(
|
||||||
"""Get line endpoints at view edges.
|
self,
|
||||||
|
ev: QtGui.QMouseEvent,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
Stolen from InfLineLabel.
|
# TODO: enter labels edit mode
|
||||||
|
print(f'double click {ev}')
|
||||||
"""
|
|
||||||
# calculate points where line intersects view box
|
|
||||||
# (in line coordinates)
|
|
||||||
lr = self.boundingRect()
|
|
||||||
pt1 = pg.Point(lr.left(), 0)
|
|
||||||
pt2 = pg.Point(lr.right(), 0)
|
|
||||||
|
|
||||||
return pt1, pt2
|
|
||||||
|
|
||||||
|
|
||||||
def level_line(
|
def level_line(
|
||||||
chart: 'ChartPlogWidget', # noqa
|
chart: 'ChartPlogWidget', # noqa
|
||||||
level: float,
|
level: float,
|
||||||
digits: int = 1,
|
|
||||||
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.
|
||||||
|
@ -451,91 +575,119 @@ def level_line(
|
||||||
# line style
|
# line style
|
||||||
dotted: bool = False,
|
dotted: bool = False,
|
||||||
|
|
||||||
adjust_to_l1: bool = False,
|
# label fields and options
|
||||||
|
digits: int = 1,
|
||||||
|
|
||||||
always_show_label: bool = False,
|
always_show_labels: bool = False,
|
||||||
|
|
||||||
|
add_label: bool = True,
|
||||||
|
|
||||||
|
orient_v: str = 'bottom',
|
||||||
|
|
||||||
**linelabelkwargs
|
|
||||||
) -> LevelLine:
|
) -> LevelLine:
|
||||||
"""Convenience routine to add a styled horizontal line to a plot.
|
"""Convenience routine to add a styled horizontal line to a plot.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
label = LevelLabel(
|
|
||||||
chart=chart,
|
|
||||||
parent=chart.getAxis('right'),
|
|
||||||
# TODO: pass this from symbol data
|
|
||||||
digits=digits,
|
|
||||||
opacity=0.616,
|
|
||||||
font_size_inches=font_size_inches,
|
|
||||||
color=color,
|
|
||||||
|
|
||||||
# TODO: make this take the view's bg pen
|
|
||||||
bg_color='papas_special',
|
|
||||||
fg_color=color,
|
|
||||||
**linelabelkwargs
|
|
||||||
)
|
|
||||||
label.update_from_data(0, level)
|
|
||||||
|
|
||||||
# by default, the label must be shown by client code
|
|
||||||
label.hide()
|
|
||||||
|
|
||||||
# TODO: can we somehow figure out a max value from the parent axis?
|
|
||||||
label._size_br_from_str(label.label_str)
|
|
||||||
|
|
||||||
line = LevelLine(
|
line = LevelLine(
|
||||||
chart,
|
chart,
|
||||||
label,
|
|
||||||
|
|
||||||
color=color,
|
color=color,
|
||||||
# lookup "highlight" equivalent
|
# lookup "highlight" equivalent
|
||||||
highlight_color=color + '_light',
|
highlight_color=color + '_light',
|
||||||
|
|
||||||
movable=True,
|
|
||||||
angle=0,
|
|
||||||
|
|
||||||
dotted=dotted,
|
dotted=dotted,
|
||||||
|
|
||||||
# UX related options
|
# UX related options
|
||||||
|
|
||||||
hl_on_hover=hl_on_hover,
|
hl_on_hover=hl_on_hover,
|
||||||
|
|
||||||
# makes order line labels offset from their parent axis
|
|
||||||
# such that they don't collide with the L1/L2 lines/prices
|
|
||||||
# that are displayed on the axis
|
|
||||||
adjust_to_l1=adjust_to_l1,
|
|
||||||
|
|
||||||
# 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_label=always_show_label,
|
always_show_labels=always_show_labels,
|
||||||
)
|
)
|
||||||
|
|
||||||
# activate/draw label
|
|
||||||
line.setValue(level) # it's just .setPos() right?
|
|
||||||
line.set_level()
|
|
||||||
|
|
||||||
chart.plotItem.addItem(line)
|
chart.plotItem.addItem(line)
|
||||||
|
|
||||||
|
if add_label:
|
||||||
|
|
||||||
|
label = line.add_label(
|
||||||
|
side='right',
|
||||||
|
opacity=1,
|
||||||
|
)
|
||||||
|
label.orient_v = orient_v
|
||||||
|
|
||||||
|
line.update_labels({'level': level, 'level_digits': 2})
|
||||||
|
label.render()
|
||||||
|
|
||||||
|
line.hide_labels()
|
||||||
|
|
||||||
|
# activate/draw label
|
||||||
|
line.set_level(level)
|
||||||
|
|
||||||
return line
|
return line
|
||||||
|
|
||||||
|
|
||||||
def order_line(
|
def order_line(
|
||||||
*args,
|
chart,
|
||||||
size: Optional[int] = None,
|
level: float,
|
||||||
|
level_digits: float,
|
||||||
|
|
||||||
|
size: Optional[int] = 1,
|
||||||
size_digits: int = 0,
|
size_digits: int = 0,
|
||||||
**kwargs,
|
|
||||||
|
submit_price: float = None,
|
||||||
|
|
||||||
|
order_status: str = 'dark',
|
||||||
|
order_type: str = 'limit',
|
||||||
|
|
||||||
|
opacity=0.616,
|
||||||
|
|
||||||
|
orient_v: str = 'bottom',
|
||||||
|
|
||||||
|
**line_kwargs,
|
||||||
) -> LevelLine:
|
) -> LevelLine:
|
||||||
"""Convenience routine to add a line graphic representing an order execution
|
"""Convenience routine to add a line graphic representing an order
|
||||||
submitted to the EMS via the chart's "order mode".
|
execution submitted to the EMS via the chart's "order mode".
|
||||||
|
|
||||||
"""
|
"""
|
||||||
line = level_line(*args, adjust_to_l1=True, **kwargs)
|
line = level_line(
|
||||||
line.label._fmt_fields['{size:.{size_digits}f} x '] = {
|
chart,
|
||||||
'size', 'size_digits'}
|
level,
|
||||||
|
add_label=False,
|
||||||
|
**line_kwargs
|
||||||
|
)
|
||||||
|
|
||||||
if size is not None:
|
llabel = line.add_label(
|
||||||
|
side='left',
|
||||||
|
fmt_str='{order_status}-{order_type}:{submit_price}',
|
||||||
|
)
|
||||||
|
llabel.fields = {
|
||||||
|
'order_status': order_status,
|
||||||
|
'order_type': order_type,
|
||||||
|
'submit_price': submit_price,
|
||||||
|
}
|
||||||
|
llabel.orient_v = orient_v
|
||||||
|
llabel.render()
|
||||||
|
llabel.show()
|
||||||
|
|
||||||
line.label._use_extra_fields = True
|
rlabel = line.add_label(
|
||||||
line.label.size = size
|
side='right',
|
||||||
line.label.size_digits = size_digits
|
fmt_str=(
|
||||||
|
'{size:.{size_digits}f} x '
|
||||||
|
'{level:,.{level_digits}f}'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rlabel.fields = {
|
||||||
|
'size': size,
|
||||||
|
'size_digits': size_digits,
|
||||||
|
'level': level,
|
||||||
|
'level_digits': level_digits,
|
||||||
|
}
|
||||||
|
|
||||||
|
rlabel.orient_v = orient_v
|
||||||
|
rlabel.render()
|
||||||
|
rlabel.show()
|
||||||
|
|
||||||
|
# sanity check
|
||||||
|
line.update_labels({'level': level})
|
||||||
|
|
||||||
return line
|
return line
|
||||||
|
|
Loading…
Reference in New Issue