2020-12-29 13:43:25 +00:00
|
|
|
# piker: trading gear for hackers
|
2021-02-08 12:04:08 +00:00
|
|
|
# Copyright (C) Tyler Goodlet (in stewardship for piker0)
|
2020-12-29 13:43:25 +00:00
|
|
|
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
"""
|
|
|
|
Lines for orders, alerts, L2.
|
|
|
|
|
|
|
|
"""
|
2021-07-16 18:50:59 +00:00
|
|
|
from functools import partial
|
2021-03-31 18:25:51 +00:00
|
|
|
from math import floor
|
2021-02-11 18:59:50 +00:00
|
|
|
from typing import Tuple, Optional, List
|
2020-12-29 13:43:25 +00:00
|
|
|
|
|
|
|
import pyqtgraph as pg
|
2021-03-12 02:35:09 +00:00
|
|
|
from pyqtgraph import Point, functions as fn
|
|
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
2021-03-08 03:16:46 +00:00
|
|
|
from PyQt5.QtCore import QPointF
|
2021-07-16 20:03:32 +00:00
|
|
|
from PyQt5.QtGui import QGraphicsPathItem
|
2020-12-29 13:43:25 +00:00
|
|
|
|
2021-07-12 13:18:11 +00:00
|
|
|
from ._annotate import mk_marker, qgo_draw_markers
|
2021-07-16 18:50:59 +00:00
|
|
|
from ._anchors import (
|
|
|
|
marker_right_points,
|
|
|
|
vbr_left,
|
|
|
|
right_axis,
|
|
|
|
update_pp_nav,
|
2021-07-16 20:03:32 +00:00
|
|
|
path_topright,
|
2021-07-16 18:50:59 +00:00
|
|
|
)
|
|
|
|
from ._label import Label
|
2021-07-12 13:18:11 +00:00
|
|
|
from ._style import hcolor, _font
|
2021-01-26 16:27:50 +00:00
|
|
|
|
2020-12-29 13:43:25 +00:00
|
|
|
|
2021-01-14 17:59:00 +00:00
|
|
|
# TODO: probably worth investigating if we can
|
|
|
|
# make .boundingRect() faster:
|
|
|
|
# https://stackoverflow.com/questions/26156486/determine-bounding-rect-of-line-in-qt
|
2020-12-29 13:43:25 +00:00
|
|
|
class LevelLine(pg.InfiniteLine):
|
2021-01-03 15:43:53 +00:00
|
|
|
|
2020-12-29 13:43:25 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
2021-01-03 15:43:53 +00:00
|
|
|
chart: 'ChartPlotWidget', # type: ignore # noqa
|
2021-02-11 18:59:50 +00:00
|
|
|
|
2021-03-18 17:28:28 +00:00
|
|
|
# style
|
2021-01-12 02:22:58 +00:00
|
|
|
color: str = 'default',
|
2021-01-03 15:43:53 +00:00
|
|
|
highlight_color: str = 'default_light',
|
2021-01-23 03:57:42 +00:00
|
|
|
dotted: bool = False,
|
2021-07-16 16:31:05 +00:00
|
|
|
# marker_size: int = 20,
|
2021-03-18 17:28:28 +00:00
|
|
|
|
|
|
|
# UX look and feel opts
|
2021-02-11 18:59:50 +00:00
|
|
|
always_show_labels: bool = False,
|
2021-03-18 17:28:28 +00:00
|
|
|
hl_on_hover: bool = True,
|
2021-03-12 02:35:09 +00:00
|
|
|
hide_xhair_on_hover: bool = True,
|
2021-03-18 17:28:28 +00:00
|
|
|
only_show_markers_on_hover: bool = True,
|
|
|
|
use_marker_margin: bool = False,
|
|
|
|
|
2021-03-12 02:35:09 +00:00
|
|
|
movable: bool = True,
|
2021-01-05 18:37:03 +00:00
|
|
|
|
2021-02-11 18:59:50 +00:00
|
|
|
) -> None:
|
2021-01-03 15:43:53 +00:00
|
|
|
|
2021-07-16 13:26:06 +00:00
|
|
|
# TODO: at this point it's probably not worth the inheritance
|
|
|
|
# any more since we've reimplemented ``.pain()`` among other
|
|
|
|
# things..
|
2021-02-11 18:59:50 +00:00
|
|
|
super().__init__(
|
2021-03-12 02:35:09 +00:00
|
|
|
movable=movable,
|
2021-02-11 18:59:50 +00:00
|
|
|
angle=0,
|
2021-03-12 02:35:09 +00:00
|
|
|
|
|
|
|
# don't use the shitty ``InfLineLabel``
|
|
|
|
label=None,
|
2021-02-11 18:59:50 +00:00
|
|
|
)
|
2021-01-23 03:57:42 +00:00
|
|
|
|
2021-01-03 15:43:53 +00:00
|
|
|
self._chart = chart
|
2021-01-05 18:37:03 +00:00
|
|
|
self._hoh = hl_on_hover
|
2021-01-23 03:57:42 +00:00
|
|
|
self._dotted = dotted
|
2021-03-12 02:35:09 +00:00
|
|
|
self._hide_xhair_on_hover = hide_xhair_on_hover
|
2021-03-07 20:50:50 +00:00
|
|
|
|
2021-03-13 22:31:15 +00:00
|
|
|
self._marker = None
|
2021-07-16 16:31:05 +00:00
|
|
|
# self._default_mkr_size = marker_size
|
2021-03-18 17:28:28 +00:00
|
|
|
self._moh = only_show_markers_on_hover
|
|
|
|
self.show_markers: bool = True # presuming the line is hovered at init
|
|
|
|
|
|
|
|
# should line go all the way to far end or leave a "margin"
|
|
|
|
# space for other graphics (eg. L1 book)
|
|
|
|
self.use_marker_margin: bool = use_marker_margin
|
2021-03-13 22:31:15 +00:00
|
|
|
|
2021-03-07 20:50:50 +00:00
|
|
|
if dotted:
|
|
|
|
self._style = QtCore.Qt.DashLine
|
|
|
|
else:
|
|
|
|
self._style = QtCore.Qt.SolidLine
|
|
|
|
|
2021-02-11 18:59:50 +00:00
|
|
|
self._hcolor: str = 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
|
2021-07-16 13:26:06 +00:00
|
|
|
self._labels: List[Label] = []
|
2021-03-13 22:31:15 +00:00
|
|
|
self._markers: List[(int, Label)] = []
|
2021-01-03 15:43:53 +00:00
|
|
|
|
2021-02-11 18:59:50 +00:00
|
|
|
# whenever this line is moved trigger label updates
|
|
|
|
self.sigPositionChanged.connect(self.on_pos_change)
|
|
|
|
|
|
|
|
# sets color to value triggering pen creation
|
2021-03-12 02:35:09 +00:00
|
|
|
self._hl_color = highlight_color
|
2021-01-12 02:22:58 +00:00
|
|
|
self.color = color
|
|
|
|
|
2021-01-23 03:57:42 +00:00
|
|
|
# TODO: for when we want to move groups of lines?
|
2021-01-03 15:43:53 +00:00
|
|
|
self._track_cursor: bool = False
|
2021-02-11 18:59:50 +00:00
|
|
|
self._always_show_labels = always_show_labels
|
|
|
|
|
2021-03-07 20:50:50 +00:00
|
|
|
self._on_drag_start = lambda l: None
|
|
|
|
self._on_drag_end = lambda l: None
|
|
|
|
|
2021-03-08 03:16:46 +00:00
|
|
|
self._y_incr_mult = 1 / chart._lc._symbol.tick_size
|
2021-03-18 17:28:28 +00:00
|
|
|
self._last_scene_y: float = 0
|
2021-03-08 03:16:46 +00:00
|
|
|
|
2021-03-16 23:36:05 +00:00
|
|
|
self._right_end_sc: float = 0
|
|
|
|
|
2021-02-11 18:59:50 +00:00
|
|
|
def txt_offsets(self) -> Tuple[int, int]:
|
|
|
|
return 0, 0
|
|
|
|
|
2021-01-12 02:22:58 +00:00
|
|
|
@property
|
|
|
|
def color(self):
|
|
|
|
return self._hcolor
|
|
|
|
|
|
|
|
@color.setter
|
|
|
|
def color(self, color: str) -> None:
|
2021-01-23 03:57:42 +00:00
|
|
|
# set pens to new color
|
2021-01-12 02:22:58 +00:00
|
|
|
self._hcolor = color
|
2021-01-23 03:57:42 +00:00
|
|
|
pen = pg.mkPen(hcolor(color))
|
2021-03-12 02:35:09 +00:00
|
|
|
hoverpen = pg.mkPen(hcolor(self._hl_color))
|
2021-01-23 03:57:42 +00:00
|
|
|
|
2021-03-07 20:50:50 +00:00
|
|
|
pen.setStyle(self._style)
|
|
|
|
hoverpen.setStyle(self._style)
|
2021-01-12 02:22:58 +00:00
|
|
|
|
2021-01-23 03:57:42 +00:00
|
|
|
# set regular pen
|
|
|
|
self.setPen(pen)
|
|
|
|
|
|
|
|
# use slightly thicker highlight for hover pen
|
|
|
|
hoverpen.setWidth(2)
|
|
|
|
self.hoverPen = hoverpen
|
2021-01-20 01:47:55 +00:00
|
|
|
|
2021-02-11 18:59:50 +00:00
|
|
|
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,
|
2021-07-16 13:26:06 +00:00
|
|
|
|
2021-02-11 18:59:50 +00:00
|
|
|
) -> None:
|
|
|
|
|
2021-07-16 13:26:06 +00:00
|
|
|
for label in self._labels:
|
|
|
|
|
2021-02-11 18:59:50 +00:00
|
|
|
label.color = self.color
|
2021-03-07 20:50:50 +00:00
|
|
|
# print(f'color is {self.color}')
|
2021-02-11 18:59:50 +00:00
|
|
|
|
|
|
|
label.fields.update(fields_data)
|
|
|
|
|
|
|
|
level = fields_data.get('level')
|
|
|
|
if level:
|
2021-07-16 13:26:06 +00:00
|
|
|
label.set_view_pos(y=level)
|
2021-02-11 18:59:50 +00:00
|
|
|
|
|
|
|
label.render()
|
|
|
|
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def hide_labels(self) -> None:
|
2021-07-16 13:26:06 +00:00
|
|
|
for label in self._labels:
|
2021-02-11 18:59:50 +00:00
|
|
|
label.hide()
|
|
|
|
|
|
|
|
def show_labels(self) -> None:
|
2021-07-16 13:26:06 +00:00
|
|
|
for label in self._labels:
|
2021-02-11 18:59:50 +00:00
|
|
|
label.show()
|
|
|
|
|
|
|
|
def set_level(
|
|
|
|
self,
|
|
|
|
level: float,
|
|
|
|
) -> None:
|
2021-03-07 20:50:50 +00:00
|
|
|
last = self.value()
|
|
|
|
|
|
|
|
# if the position hasn't changed then ``.update_labels()``
|
|
|
|
# will not be called by a non-triggered `.on_pos_change()`,
|
|
|
|
# so we need to call it manually to avoid mismatching
|
|
|
|
# label-to-line color when the line is updated but not
|
|
|
|
# "moved".
|
|
|
|
if level == last:
|
|
|
|
self.update_labels({'level': level})
|
|
|
|
|
2021-02-11 18:59:50 +00:00
|
|
|
self.setPos(level)
|
|
|
|
self.level = self.value()
|
|
|
|
self.update()
|
2020-12-29 13:43:25 +00:00
|
|
|
|
2021-01-03 15:43:53 +00:00
|
|
|
def on_tracked_source(
|
|
|
|
self,
|
|
|
|
x: int,
|
|
|
|
y: float
|
|
|
|
) -> None:
|
2021-01-26 16:27:50 +00:00
|
|
|
# XXX: this is called by our ``Cursor`` type once this
|
|
|
|
# line is set to track the cursor: for every movement
|
|
|
|
# this callback is invoked to reposition the line
|
2021-01-03 15:43:53 +00:00
|
|
|
self.movable = True
|
2021-02-11 18:59:50 +00:00
|
|
|
self.set_level(y) # implictly calls reposition handler
|
2021-01-03 15:43:53 +00:00
|
|
|
|
|
|
|
def mouseDragEvent(self, ev):
|
2021-03-07 20:50:50 +00:00
|
|
|
"""Override the ``InfiniteLine`` handler since we need more
|
|
|
|
detailed control and start end signalling.
|
|
|
|
|
|
|
|
"""
|
2021-06-22 11:17:49 +00:00
|
|
|
cursor = self._chart.linked.cursor
|
2021-02-11 18:59:50 +00:00
|
|
|
|
2021-01-03 15:43:53 +00:00
|
|
|
# hide y-crosshair
|
2021-06-22 11:17:49 +00:00
|
|
|
cursor.hide_xhair()
|
2021-01-03 15:43:53 +00:00
|
|
|
|
|
|
|
# highlight
|
|
|
|
self.currentPen = self.hoverPen
|
2021-03-12 02:35:09 +00:00
|
|
|
self.show_labels()
|
2021-01-03 15:43:53 +00:00
|
|
|
|
2021-03-07 20:50:50 +00:00
|
|
|
# XXX: normal tracking behavior pulled out from parent type
|
|
|
|
if self.movable and ev.button() == QtCore.Qt.LeftButton:
|
2021-03-08 03:16:46 +00:00
|
|
|
ev.accept()
|
2021-03-07 20:50:50 +00:00
|
|
|
|
|
|
|
if ev.isStart():
|
|
|
|
self.moving = True
|
2021-03-08 03:16:46 +00:00
|
|
|
down_pos = ev.buttonDownPos()
|
|
|
|
self.cursorOffset = self.pos() - self.mapToParent(down_pos)
|
2021-03-07 20:50:50 +00:00
|
|
|
self.startPosition = self.pos()
|
|
|
|
|
2021-03-08 03:16:46 +00:00
|
|
|
self._on_drag_start(self)
|
2021-03-07 20:50:50 +00:00
|
|
|
|
|
|
|
if not self.moving:
|
|
|
|
return
|
|
|
|
|
2021-03-08 03:16:46 +00:00
|
|
|
pos = self.cursorOffset + self.mapToParent(ev.pos())
|
|
|
|
|
|
|
|
# TODO: we should probably figure out a std api
|
|
|
|
# for this kind of thing given we already have
|
|
|
|
# it on the cursor system...
|
|
|
|
|
|
|
|
# round to nearest symbol tick
|
|
|
|
m = self._y_incr_mult
|
2021-03-12 02:35:09 +00:00
|
|
|
self.setPos(
|
|
|
|
QPointF(
|
2021-03-13 22:31:15 +00:00
|
|
|
self.pos().x(), # don't allow shifting horizontally
|
2021-03-12 02:35:09 +00:00
|
|
|
round(pos.y() * m) / m
|
|
|
|
)
|
|
|
|
)
|
2021-03-08 03:16:46 +00:00
|
|
|
|
2021-03-07 20:50:50 +00:00
|
|
|
self.sigDragged.emit(self)
|
2021-03-08 03:16:46 +00:00
|
|
|
|
2021-03-07 20:50:50 +00:00
|
|
|
if ev.isFinish():
|
|
|
|
self.moving = False
|
|
|
|
self.sigPositionChangeFinished.emit(self)
|
|
|
|
self._on_drag_end(self)
|
2021-01-03 15:43:53 +00:00
|
|
|
|
|
|
|
# This is the final position in the drag
|
|
|
|
if ev.isFinish():
|
|
|
|
# show y-crosshair again
|
2021-06-22 11:17:49 +00:00
|
|
|
cursor.show_xhair()
|
2021-01-03 15:43:53 +00:00
|
|
|
|
|
|
|
def delete(self) -> None:
|
|
|
|
"""Remove this line from containing chart/view/scene.
|
|
|
|
|
|
|
|
"""
|
|
|
|
scene = self.scene()
|
|
|
|
if scene:
|
2021-07-16 13:26:06 +00:00
|
|
|
for label in self._labels:
|
2021-02-11 18:59:50 +00:00
|
|
|
label.delete()
|
2021-01-03 15:43:53 +00:00
|
|
|
|
2021-07-16 13:26:06 +00:00
|
|
|
# gc managed labels?
|
2021-02-11 18:59:50 +00:00
|
|
|
self._labels.clear()
|
2021-01-26 16:27:50 +00:00
|
|
|
|
2021-03-13 22:31:15 +00:00
|
|
|
if self._marker:
|
|
|
|
self.scene().removeItem(self._marker)
|
|
|
|
|
2021-03-07 20:50:50 +00:00
|
|
|
# remove from chart/cursor states
|
|
|
|
chart = self._chart
|
2021-06-22 11:17:49 +00:00
|
|
|
cur = chart.linked.cursor
|
2021-03-07 20:50:50 +00:00
|
|
|
|
|
|
|
if self in cur._hovered:
|
|
|
|
cur._hovered.remove(self)
|
|
|
|
|
|
|
|
chart.plotItem.removeItem(self)
|
2021-01-26 16:27:50 +00:00
|
|
|
|
2021-02-11 18:59:50 +00:00
|
|
|
def mouseDoubleClickEvent(
|
|
|
|
self,
|
|
|
|
ev: QtGui.QMouseEvent,
|
|
|
|
) -> None:
|
2021-01-26 16:27:50 +00:00
|
|
|
|
2021-02-11 18:59:50 +00:00
|
|
|
# TODO: enter labels edit mode
|
|
|
|
print(f'double click {ev}')
|
2021-01-26 16:27:50 +00:00
|
|
|
|
2021-03-12 02:35:09 +00:00
|
|
|
def right_point(
|
|
|
|
self,
|
|
|
|
) -> float:
|
|
|
|
|
|
|
|
chart = self._chart
|
|
|
|
l1_len = chart._max_l1_line_len
|
|
|
|
ryaxis = chart.getAxis('right')
|
2021-03-16 23:36:05 +00:00
|
|
|
up_to_l1_sc = ryaxis.pos().x() - l1_len
|
2021-03-12 02:35:09 +00:00
|
|
|
|
2021-03-16 23:36:05 +00:00
|
|
|
return up_to_l1_sc
|
2021-03-12 02:35:09 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2021-03-18 17:28:28 +00:00
|
|
|
# these are in viewbox coords
|
2021-03-12 02:35:09 +00:00
|
|
|
vb_left, vb_right = self._endPoints
|
2021-03-16 23:36:05 +00:00
|
|
|
vb = self.getViewBox()
|
2021-03-13 22:31:15 +00:00
|
|
|
|
2021-07-16 16:31:05 +00:00
|
|
|
line_end, marker_right, r_axis_x = marker_right_points(self._chart)
|
2021-03-16 23:36:05 +00:00
|
|
|
|
2021-03-18 17:28:28 +00:00
|
|
|
if self.show_markers and self.markers:
|
2021-03-16 23:36:05 +00:00
|
|
|
|
2021-03-18 17:28:28 +00:00
|
|
|
p.setPen(self.pen)
|
2021-06-23 14:06:05 +00:00
|
|
|
qgo_draw_markers(
|
2021-03-16 23:36:05 +00:00
|
|
|
self.markers,
|
2021-03-18 17:28:28 +00:00
|
|
|
self.pen.color(),
|
2021-03-12 02:35:09 +00:00
|
|
|
p,
|
|
|
|
vb_left,
|
|
|
|
vb_right,
|
2021-03-16 23:36:05 +00:00
|
|
|
marker_right,
|
2021-03-12 02:35:09 +00:00
|
|
|
)
|
2021-03-16 23:36:05 +00:00
|
|
|
# marker_size = self.markers[0][2]
|
|
|
|
self._maxMarkerSize = max([m[2] / 2. for m in self.markers])
|
|
|
|
|
2021-03-18 17:28:28 +00:00
|
|
|
# this seems slower when moving around
|
|
|
|
# order lines.. not sure wtf is up with that.
|
|
|
|
# for now we're just using it on the position line.
|
2021-03-16 23:36:05 +00:00
|
|
|
elif self._marker:
|
2021-03-18 17:28:28 +00:00
|
|
|
self._marker.setPos(
|
|
|
|
QPointF(marker_right, self.scene_y())
|
|
|
|
)
|
2021-07-16 15:40:56 +00:00
|
|
|
# TODO: make this label update part of a scene-aware-marker
|
|
|
|
# composed annotation
|
2021-07-16 13:26:06 +00:00
|
|
|
self._marker.label.update()
|
2021-03-18 17:28:28 +00:00
|
|
|
|
|
|
|
elif not self.use_marker_margin:
|
|
|
|
# basically means **don't** shorten the line with normally
|
|
|
|
# reserved space for a direction marker but, leave small
|
|
|
|
# blank gap for style
|
2021-03-16 23:36:05 +00:00
|
|
|
line_end = r_axis_x - 10
|
|
|
|
|
|
|
|
line_end_view = vb.mapToView(Point(line_end, 0)).x()
|
|
|
|
|
2021-03-18 17:28:28 +00:00
|
|
|
# self.currentPen.setJoinStyle(QtCore.Qt.MiterJoin)
|
|
|
|
p.setPen(self.currentPen)
|
2021-03-16 23:36:05 +00:00
|
|
|
p.drawLine(
|
|
|
|
Point(vb_left, 0),
|
|
|
|
Point(line_end_view, 0)
|
|
|
|
)
|
|
|
|
self._right_end_sc = line_end
|
|
|
|
|
2021-03-18 17:28:28 +00:00
|
|
|
def hide(self) -> None:
|
|
|
|
super().hide()
|
|
|
|
if self._marker:
|
|
|
|
self._marker.hide()
|
|
|
|
|
2021-03-16 23:36:05 +00:00
|
|
|
def scene_right_xy(self) -> QPointF:
|
|
|
|
return self.getViewBox().mapFromView(
|
|
|
|
QPointF(0, self.value())
|
|
|
|
)
|
2021-03-12 02:35:09 +00:00
|
|
|
|
2021-03-13 22:31:15 +00:00
|
|
|
def scene_y(self) -> float:
|
|
|
|
return self.getViewBox().mapFromView(Point(0, self.value())).y()
|
|
|
|
|
|
|
|
def add_marker(
|
|
|
|
self,
|
2021-07-21 19:50:09 +00:00
|
|
|
path: QtWidgets.QGraphicsPathItem,
|
2021-07-16 13:26:06 +00:00
|
|
|
|
|
|
|
) -> QtWidgets.QGraphicsPathItem:
|
2021-03-13 22:31:15 +00:00
|
|
|
|
2021-06-23 14:06:05 +00:00
|
|
|
# add path to scene
|
|
|
|
self.getViewBox().scene().addItem(path)
|
2021-03-13 22:31:15 +00:00
|
|
|
|
|
|
|
self._marker = path
|
|
|
|
|
2021-03-16 23:36:05 +00:00
|
|
|
rsc = self.right_point()
|
2021-03-13 22:31:15 +00:00
|
|
|
|
|
|
|
self._marker.setPen(self.currentPen)
|
|
|
|
self._marker.setBrush(fn.mkBrush(self.currentPen.color()))
|
|
|
|
# y_in_sc = chart._vb.mapFromView(Point(0, self.value())).y()
|
|
|
|
path.setPos(QPointF(rsc, self.scene_y()))
|
|
|
|
|
2021-07-16 13:26:06 +00:00
|
|
|
return path
|
2021-03-16 23:36:05 +00:00
|
|
|
# self.update()
|
|
|
|
|
2021-03-12 02:35:09 +00:00
|
|
|
def hoverEvent(self, ev):
|
2021-03-16 23:36:05 +00:00
|
|
|
"""Mouse hover callback.
|
2021-03-12 02:35:09 +00:00
|
|
|
|
|
|
|
"""
|
2021-06-22 11:17:49 +00:00
|
|
|
cur = self._chart.linked.cursor
|
2021-03-16 23:36:05 +00:00
|
|
|
|
|
|
|
# hovered
|
2021-03-12 02:35:09 +00:00
|
|
|
if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton):
|
2021-03-16 23:36:05 +00:00
|
|
|
|
|
|
|
# if already hovered we don't need to run again
|
|
|
|
if self.mouseHovering is True:
|
|
|
|
return
|
|
|
|
|
2021-03-18 17:28:28 +00:00
|
|
|
if self._moh:
|
|
|
|
self.show_markers = True
|
|
|
|
|
2021-07-16 13:26:06 +00:00
|
|
|
if self._marker:
|
|
|
|
self._marker.show()
|
|
|
|
|
2021-03-16 23:36:05 +00:00
|
|
|
# highlight if so configured
|
|
|
|
if self._hoh:
|
|
|
|
|
|
|
|
self.currentPen = self.hoverPen
|
|
|
|
|
|
|
|
if self not in cur._trackers:
|
|
|
|
# only disable cursor y-label updates
|
|
|
|
# if we're highlighting a line
|
|
|
|
cur._y_label_update = False
|
|
|
|
|
|
|
|
# add us to cursor state
|
|
|
|
cur.add_hovered(self)
|
|
|
|
|
|
|
|
if self._hide_xhair_on_hover:
|
|
|
|
cur.hide_xhair(
|
|
|
|
# set y-label to current value
|
|
|
|
y_label_level=self.value(),
|
2021-03-18 17:28:28 +00:00
|
|
|
just_vertical=True,
|
2021-03-16 23:36:05 +00:00
|
|
|
|
|
|
|
# fg_color=self._hcolor,
|
|
|
|
# bg_color=self._hcolor,
|
|
|
|
)
|
|
|
|
|
|
|
|
# if we want highlighting of labels
|
|
|
|
# it should be delegated into this method
|
|
|
|
self.show_labels()
|
|
|
|
|
|
|
|
self.mouseHovering = True
|
|
|
|
|
2021-03-18 17:28:28 +00:00
|
|
|
# un-hovered
|
|
|
|
else:
|
2021-03-16 23:36:05 +00:00
|
|
|
if self.mouseHovering is False:
|
|
|
|
return
|
|
|
|
|
|
|
|
cur._y_label_update = True
|
|
|
|
|
|
|
|
self.currentPen = self.pen
|
|
|
|
|
|
|
|
cur._hovered.remove(self)
|
|
|
|
|
2021-03-18 17:28:28 +00:00
|
|
|
if self._moh:
|
|
|
|
self.show_markers = False
|
|
|
|
|
2021-07-16 13:26:06 +00:00
|
|
|
if self._marker:
|
|
|
|
self._marker.hide()
|
|
|
|
|
2021-03-16 23:36:05 +00:00
|
|
|
if self not in cur._trackers:
|
|
|
|
cur.show_xhair(y_label_level=self.value())
|
|
|
|
|
|
|
|
if not self._always_show_labels:
|
2021-07-16 13:26:06 +00:00
|
|
|
for label in self._labels:
|
2021-03-16 23:36:05 +00:00
|
|
|
label.hide()
|
|
|
|
label.txt.update()
|
|
|
|
# label.unhighlight()
|
|
|
|
|
|
|
|
self.mouseHovering = False
|
|
|
|
|
|
|
|
self.update()
|
2021-03-12 02:35:09 +00:00
|
|
|
|
2020-12-29 13:43:25 +00:00
|
|
|
|
|
|
|
def level_line(
|
2021-07-21 19:50:09 +00:00
|
|
|
chart: 'ChartPlotWidget', # noqa
|
2020-12-29 13:43:25 +00:00
|
|
|
level: float,
|
2021-01-05 18:37:03 +00:00
|
|
|
|
2021-01-23 03:57:42 +00:00
|
|
|
# line style
|
|
|
|
dotted: bool = False,
|
2021-07-16 13:26:06 +00:00
|
|
|
color: str = 'default',
|
|
|
|
|
|
|
|
# ux
|
|
|
|
hl_on_hover: bool = True,
|
2021-01-23 03:57:42 +00:00
|
|
|
|
2021-02-11 18:59:50 +00:00
|
|
|
# label fields and options
|
|
|
|
digits: int = 1,
|
|
|
|
always_show_labels: bool = False,
|
|
|
|
add_label: bool = True,
|
|
|
|
orient_v: str = 'bottom',
|
2021-03-12 02:35:09 +00:00
|
|
|
**kwargs,
|
|
|
|
|
2020-12-29 13:43:25 +00:00
|
|
|
) -> LevelLine:
|
|
|
|
"""Convenience routine to add a styled horizontal line to a plot.
|
|
|
|
|
|
|
|
"""
|
2021-03-12 02:35:09 +00:00
|
|
|
hl_color = color + '_light' if hl_on_hover else color
|
2020-12-29 13:43:25 +00:00
|
|
|
|
|
|
|
line = LevelLine(
|
2021-01-03 15:43:53 +00:00
|
|
|
chart,
|
2021-01-12 02:22:58 +00:00
|
|
|
color=color,
|
2021-03-12 02:35:09 +00:00
|
|
|
|
2021-01-03 15:43:53 +00:00
|
|
|
# lookup "highlight" equivalent
|
2021-03-12 02:35:09 +00:00
|
|
|
highlight_color=hl_color,
|
2021-01-27 03:16:18 +00:00
|
|
|
|
2021-01-23 03:57:42 +00:00
|
|
|
dotted=dotted,
|
2021-01-27 03:16:18 +00:00
|
|
|
|
|
|
|
# UX related options
|
|
|
|
hl_on_hover=hl_on_hover,
|
|
|
|
|
|
|
|
# when set to True the label is always shown instead of just on
|
|
|
|
# highlight (which is a privacy thing for orders)
|
2021-02-11 18:59:50 +00:00
|
|
|
always_show_labels=always_show_labels,
|
2021-03-12 02:35:09 +00:00
|
|
|
|
|
|
|
**kwargs,
|
2020-12-29 13:43:25 +00:00
|
|
|
)
|
2021-01-03 15:43:53 +00:00
|
|
|
|
2020-12-29 13:43:25 +00:00
|
|
|
chart.plotItem.addItem(line)
|
|
|
|
|
2021-02-11 18:59:50 +00:00
|
|
|
if add_label:
|
|
|
|
|
2021-07-16 13:26:06 +00:00
|
|
|
label = Label(
|
|
|
|
|
|
|
|
view=line.getViewBox(),
|
|
|
|
|
|
|
|
# by default we only display the line's level value
|
|
|
|
# in the label
|
|
|
|
fmt_str=('{level:,.{level_digits}f}'),
|
|
|
|
color=color,
|
2021-02-11 18:59:50 +00:00
|
|
|
)
|
|
|
|
|
2021-07-16 13:26:06 +00:00
|
|
|
# anchor to right side (of view ) label
|
|
|
|
label.set_x_anchor_func(
|
|
|
|
right_axis(
|
|
|
|
chart,
|
|
|
|
label,
|
|
|
|
side='left', # side of axis
|
|
|
|
offset=0,
|
|
|
|
avoid_book=False,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
# add to label set which will be updated on level changes
|
|
|
|
line._labels.append(label)
|
|
|
|
|
|
|
|
label.orient_v = orient_v
|
2021-02-11 18:59:50 +00:00
|
|
|
line.update_labels({'level': level, 'level_digits': 2})
|
|
|
|
label.render()
|
|
|
|
|
|
|
|
line.hide_labels()
|
|
|
|
|
|
|
|
# activate/draw label
|
|
|
|
line.set_level(level)
|
|
|
|
|
2021-01-26 16:27:50 +00:00
|
|
|
return line
|
|
|
|
|
|
|
|
|
|
|
|
def order_line(
|
2021-07-16 13:26:06 +00:00
|
|
|
|
2021-02-11 18:59:50 +00:00
|
|
|
chart,
|
|
|
|
level: float,
|
|
|
|
level_digits: float,
|
2021-03-13 22:31:15 +00:00
|
|
|
action: str, # buy or sell
|
2021-02-11 18:59:50 +00:00
|
|
|
|
|
|
|
size: Optional[int] = 1,
|
2021-01-26 16:27:50 +00:00
|
|
|
size_digits: int = 0,
|
2021-03-13 22:31:15 +00:00
|
|
|
show_markers: bool = False,
|
2021-02-11 18:59:50 +00:00
|
|
|
submit_price: float = None,
|
2021-03-13 22:31:15 +00:00
|
|
|
exec_type: str = 'dark',
|
2021-02-11 18:59:50 +00:00
|
|
|
order_type: str = 'limit',
|
|
|
|
orient_v: str = 'bottom',
|
|
|
|
|
|
|
|
**line_kwargs,
|
2021-03-13 22:31:15 +00:00
|
|
|
|
2021-01-26 16:27:50 +00:00
|
|
|
) -> LevelLine:
|
2021-02-11 18:59:50 +00:00
|
|
|
"""Convenience routine to add a line graphic representing an order
|
|
|
|
execution submitted to the EMS via the chart's "order mode".
|
2021-01-26 16:27:50 +00:00
|
|
|
|
|
|
|
"""
|
2021-02-11 18:59:50 +00:00
|
|
|
line = level_line(
|
|
|
|
chart,
|
|
|
|
level,
|
|
|
|
add_label=False,
|
2021-03-18 17:28:28 +00:00
|
|
|
use_marker_margin=True,
|
|
|
|
# only_show_markers_on_hover=True,
|
2021-02-11 18:59:50 +00:00
|
|
|
**line_kwargs
|
|
|
|
)
|
2021-01-26 16:27:50 +00:00
|
|
|
|
2021-03-13 22:31:15 +00:00
|
|
|
if show_markers:
|
2021-03-31 18:25:51 +00:00
|
|
|
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
|
|
|
|
|
2021-03-13 22:31:15 +00:00
|
|
|
# add arrow marker on end of line nearest y-axis
|
|
|
|
marker_style, marker_size = {
|
2021-03-31 18:25:51 +00:00
|
|
|
'buy': ('|<', arrow_size),
|
|
|
|
'sell': ('>|', arrow_size),
|
|
|
|
'alert': ('v', alert_size),
|
2021-03-13 22:31:15 +00:00
|
|
|
}[action]
|
|
|
|
|
2021-03-16 23:36:05 +00:00
|
|
|
# this fixes it the artifact issue! .. of course, bouding rect stuff
|
|
|
|
line._maxMarkerSize = marker_size
|
|
|
|
|
|
|
|
# use ``QPathGraphicsItem``s to draw markers in scene coords
|
|
|
|
# instead of the old way that was doing the same but by
|
|
|
|
# resetting the graphics item transform intermittently
|
2021-06-22 11:17:49 +00:00
|
|
|
|
|
|
|
# XXX: this is our new approach but seems slower?
|
2021-07-16 13:26:06 +00:00
|
|
|
# path = line.add_marker(mk_marker(marker_style, marker_size))
|
|
|
|
# assert line._marker == path
|
2021-06-22 11:17:49 +00:00
|
|
|
|
2021-03-16 23:36:05 +00:00
|
|
|
assert not line.markers
|
2021-03-13 22:31:15 +00:00
|
|
|
|
2021-03-16 23:36:05 +00:00
|
|
|
# the old way which is still somehow faster?
|
|
|
|
path = mk_marker(
|
2021-03-13 22:31:15 +00:00
|
|
|
marker_style,
|
|
|
|
# the "position" here is now ignored since we modified
|
|
|
|
# internals to pin markers to the right end of the line
|
2021-03-16 23:36:05 +00:00
|
|
|
marker_size,
|
|
|
|
use_qgpath=False,
|
2021-03-13 22:31:15 +00:00
|
|
|
)
|
2021-06-22 11:17:49 +00:00
|
|
|
# manually append for later ``InfiniteLine.paint()`` drawing
|
|
|
|
# XXX: this was manually tested as faster then using the
|
|
|
|
# QGraphicsItem around a painter path.. probably needs further
|
|
|
|
# testing to figure out why tf that's true.
|
2021-03-16 23:36:05 +00:00
|
|
|
line.markers.append((path, 0, marker_size))
|
2021-02-11 18:59:50 +00:00
|
|
|
|
2021-03-13 22:31:15 +00:00
|
|
|
orient_v = 'top' if action == 'sell' else 'bottom'
|
2021-02-11 18:59:50 +00:00
|
|
|
|
2021-03-13 22:31:15 +00:00
|
|
|
if action == 'alert':
|
|
|
|
|
2021-07-16 13:26:06 +00:00
|
|
|
llabel = Label(
|
|
|
|
|
|
|
|
view=line.getViewBox(),
|
|
|
|
color=line.color,
|
|
|
|
|
|
|
|
# completely different labelling for alerts
|
|
|
|
fmt_str='alert => {level}',
|
2021-03-13 22:31:15 +00:00
|
|
|
)
|
2021-07-16 13:26:06 +00:00
|
|
|
|
|
|
|
# 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))
|
|
|
|
|
2021-03-13 22:31:15 +00:00
|
|
|
llabel.fields = {
|
|
|
|
'level': level,
|
|
|
|
'level_digits': level_digits,
|
|
|
|
}
|
|
|
|
llabel.orient_v = orient_v
|
|
|
|
llabel.render()
|
|
|
|
llabel.show()
|
|
|
|
|
|
|
|
else:
|
2021-07-16 13:26:06 +00:00
|
|
|
|
|
|
|
rlabel = Label(
|
|
|
|
|
|
|
|
view=line.getViewBox(),
|
|
|
|
|
|
|
|
# display the order pos size
|
|
|
|
fmt_str=('{size:.{size_digits}f} '),
|
|
|
|
color=line.color,
|
2021-03-13 22:31:15 +00:00
|
|
|
)
|
2021-07-16 13:26:06 +00:00
|
|
|
|
|
|
|
# set anchor callback
|
|
|
|
# right side label by default
|
|
|
|
rlabel.set_x_anchor_func(
|
|
|
|
right_axis(
|
|
|
|
chart,
|
|
|
|
rlabel,
|
|
|
|
side='left', # side of axis
|
|
|
|
offset=4*marker_size,
|
|
|
|
avoid_book=True,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
line._labels.append(rlabel)
|
|
|
|
|
2021-03-13 22:31:15 +00:00
|
|
|
rlabel.fields = {
|
|
|
|
'size': size,
|
|
|
|
'size_digits': size_digits,
|
|
|
|
}
|
|
|
|
|
|
|
|
rlabel.orient_v = orient_v
|
|
|
|
rlabel.render()
|
|
|
|
rlabel.show()
|
|
|
|
|
2021-02-11 18:59:50 +00:00
|
|
|
# sanity check
|
|
|
|
line.update_labels({'level': level})
|
2021-01-26 16:27:50 +00:00
|
|
|
|
2020-12-29 13:43:25 +00:00
|
|
|
return line
|
2021-03-12 02:35:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
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".
|
|
|
|
|
|
|
|
"""
|
2021-07-16 13:26:06 +00:00
|
|
|
hcolor = 'default_light'
|
|
|
|
|
2021-03-12 02:35:09 +00:00
|
|
|
line = level_line(
|
|
|
|
chart,
|
|
|
|
level,
|
2021-07-16 13:26:06 +00:00
|
|
|
color=hcolor,
|
2021-03-12 02:35:09 +00:00
|
|
|
add_label=False,
|
|
|
|
hl_on_hover=False,
|
|
|
|
movable=False,
|
|
|
|
hide_xhair_on_hover=False,
|
2021-03-18 17:28:28 +00:00
|
|
|
use_marker_margin=True,
|
2021-07-16 13:26:06 +00:00
|
|
|
only_show_markers_on_hover=False,
|
|
|
|
always_show_labels=True,
|
2021-03-12 02:35:09 +00:00
|
|
|
)
|
2021-03-18 17:28:28 +00:00
|
|
|
# hide position marker when out of view (for now)
|
|
|
|
vb = line.getViewBox()
|
|
|
|
|
2021-07-16 13:26:06 +00:00
|
|
|
# arrow marker
|
|
|
|
# scale marker size with dpi-aware font size
|
|
|
|
font_size = _font.font.pixelSize()
|
|
|
|
|
|
|
|
# scale marker size with dpi-aware font size
|
|
|
|
arrow_size = floor(1.375 * font_size)
|
|
|
|
|
|
|
|
if size > 0:
|
|
|
|
style = '|<'
|
|
|
|
elif size < 0:
|
|
|
|
style = '>|'
|
|
|
|
|
|
|
|
arrow_path = mk_marker(style, size=arrow_size)
|
|
|
|
|
|
|
|
path_br = arrow_path.mapToScene(
|
|
|
|
arrow_path.path()
|
|
|
|
).boundingRect()
|
|
|
|
|
|
|
|
# monkey-cache height for sizing on pp nav-hub
|
|
|
|
arrow_path._height = path_br.height()
|
|
|
|
arrow_path._width = path_br.width()
|
|
|
|
|
|
|
|
marker_label = Label(
|
|
|
|
view=vb,
|
|
|
|
fmt_str='pp',
|
|
|
|
color=hcolor,
|
|
|
|
update_on_range_change=False,
|
|
|
|
)
|
|
|
|
arrow_path.label = marker_label
|
|
|
|
|
2021-07-16 20:03:32 +00:00
|
|
|
marker_label.scene_anchor = partial(
|
|
|
|
path_topright,
|
|
|
|
gpath=arrow_path,
|
|
|
|
label=marker_label,
|
|
|
|
)
|
2021-07-16 13:26:06 +00:00
|
|
|
|
|
|
|
line._labels.append(marker_label)
|
|
|
|
|
|
|
|
marker_label.render()
|
|
|
|
marker_label.show()
|
|
|
|
|
|
|
|
# XXX: uses new marker drawing approach
|
|
|
|
line.add_marker(arrow_path)
|
|
|
|
line.set_level(level)
|
|
|
|
|
|
|
|
# sanity check
|
|
|
|
line.update_labels({'level': level})
|
|
|
|
|
2021-07-16 18:50:59 +00:00
|
|
|
vb.sigRangeChanged.connect(partial(update_pp_nav, chartview=vb, line=line))
|
2021-03-12 02:35:09 +00:00
|
|
|
|
|
|
|
return line
|