Add a "lines editor" api/component

basic_alerts
Tyler Goodlet 2021-01-05 13:37:03 -05:00
parent 268f207a6c
commit 8d66a17daf
3 changed files with 161 additions and 96 deletions

View File

@ -1,5 +1,5 @@
# piker: trading gear for hackers # piker: trading gear for hackers
# Copyright (C) 2018-present Tyler Goodlet (in stewardship for piker0) # Copyright (C) Tyler Goodlet (in stewardship for piker0)
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -47,8 +47,8 @@ _local_book = {}
@dataclass @dataclass
class OrderBook: class OrderBoi:
"""Send (client?) side order book tracking. """'Buy' (client ?) side order book ctl and tracking.
Mostly for keeping local state to match the EMS and use Mostly for keeping local state to match the EMS and use
events to trigger graphics updates. events to trigger graphics updates.
@ -73,14 +73,14 @@ class OrderBook:
... ...
_orders: OrderBook = None _orders: OrderBoi = None
def get_orders() -> OrderBook: def get_orders() -> OrderBoi:
global _orders global _orders
if _orders is None: if _orders is None:
_orders = OrderBook _orders = OrderBoi
return _orders return _orders
@ -392,6 +392,8 @@ async def spawn_router_stream_alerts(
oid = alert['oid'] oid = alert['oid']
print(f'_lines: {_lines}') print(f'_lines: {_lines}')
print(f'deleting line with oid: {oid}') print(f'deleting line with oid: {oid}')
chart._vb._lines_editor
_lines.pop(oid).delete() _lines.pop(oid).delete()
# TODO: this in another task? # TODO: this in another task?

View File

@ -18,6 +18,7 @@
Lines for orders, alerts, L2. Lines for orders, alerts, L2.
""" """
from dataclasses import dataclass
from typing import Tuple from typing import Tuple
import pyqtgraph as pg import pyqtgraph as pg
@ -87,10 +88,11 @@ class LevelLabel(YSticky):
self.level = level self.level = level
def set_label_str(self, level: float): def set_label_str(self, level: float):
# this is read inside ``.paint()``
# self.label_str = '{size} x {level:.{digits}f}'.format( # self.label_str = '{size} x {level:.{digits}f}'.format(
self.label_str = '{level:.{digits}f}'.format(
# size=self._size, # size=self._size,
# this is read inside ``.paint()``
self.label_str = '{level:.{digits}f}'.format(
digits=self.digits, digits=self.digits,
level=level level=level
).replace(',', ' ') ).replace(',', ' ')
@ -200,14 +202,16 @@ class LevelLine(pg.InfiniteLine):
chart: 'ChartPlotWidget', # type: ignore # noqa chart: 'ChartPlotWidget', # type: ignore # noqa
label: LevelLabel, label: LevelLabel,
highlight_color: str = 'default_light', highlight_color: str = 'default_light',
hl_on_hover: bool = True,
**kwargs, **kwargs,
) -> None: ) -> None:
self.label = label
super().__init__(**kwargs) super().__init__(**kwargs)
self.label = label
self.sigPositionChanged.connect(self.set_level) self.sigPositionChanged.connect(self.set_level)
self._chart = chart self._chart = chart
self._hoh = hl_on_hover
# use slightly thicker highlight # use slightly thicker highlight
pen = pg.mkPen(hcolor(highlight_color)) pen = pg.mkPen(hcolor(highlight_color))
@ -231,7 +235,8 @@ class LevelLine(pg.InfiniteLine):
"""Mouse hover callback. """Mouse hover callback.
""" """
if self.mouseHovering == hover: # XXX: currently we'll just return if _hoh is False
if self.mouseHovering == hover or not self._hoh:
return return
self.mouseHovering = hover self.mouseHovering = hover
@ -315,6 +320,10 @@ def level_line(
show_label: bool = True, show_label: bool = True,
# whether or not the line placed in view should highlight
# when moused over (aka "hovered")
hl_on_hover: bool = True,
**linelabelkwargs **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.
@ -346,6 +355,7 @@ def level_line(
highlight_color=color + '_light', highlight_color=color + '_light',
movable=True, movable=True,
angle=0, angle=0,
hl_on_hover=hl_on_hover,
) )
line.setValue(level) line.setValue(level)
line.setPen(pg.mkPen(hcolor(color))) line.setPen(pg.mkPen(hcolor(color)))

View File

@ -1,5 +1,5 @@
# piker: trading gear for hackers # piker: trading gear for hackers
# Copyright (C) 2018-present Tyler Goodlet (in stewardship of piker0) # Copyright (C) Tyler Goodlet (in stewardship for piker0)
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -17,7 +17,7 @@
""" """
UX interaction customs. UX interaction customs.
""" """
from dataclasses import dataclass from dataclasses import dataclass, field
from typing import Optional from typing import Optional
import uuid import uuid
@ -28,7 +28,7 @@ import numpy as np
from ..log import get_logger from ..log import get_logger
from ._style import _min_points_to_show, hcolor, _font from ._style import _min_points_to_show, hcolor, _font
from ._graphics._lines import level_line from ._graphics._lines import level_line, LevelLine
from .._ems import _lines from .._ems import _lines
@ -199,19 +199,133 @@ class SelectRect(QtGui.QGraphicsRectItem):
@dataclass @dataclass
class LinesEditor: class LineEditor:
view: 'ChartView' view: 'ChartView'
chart: 'ChartPlotWidget' _lines: field(default_factory=dict)
active_line: 'LevelLine' chart: 'ChartPlotWidget' = None # type: ignore # noqa
_active_staged_line: LevelLine = None
_stage_line: LevelLine = None
def stage_line(self) -> 'LevelLine': def stage_line(self, color: str = 'alert_yellow') -> LevelLine:
... """Stage a line at the current chart's cursor position
and return it.
def commit_line(self) -> 'LevelLine': """
... chart = self.chart._cursor.active_plot
chart.setCursor(QtCore.Qt.PointingHandCursor)
cursor = chart._cursor
y = chart._cursor._datum_xy[1]
def remove_line(self, line) -> None: line = self._stage_line
... if not line:
# add a "staged" cursor-tracking line to view
# and cash it in a a var
line = level_line(
chart,
level=y,
color=color,
# don't highlight the "staging" line
hl_on_hover=False,
)
self._stage_line = line
else:
# use the existing staged line instead
# of allocating more mem / objects repeatedly
line.setValue(y)
line.show()
line.label.show()
self._active_staged_line = line
# hide crosshair y-line
cursor.graphics[chart]['hl'].hide()
# add line to cursor trackers
cursor._trackers.add(line)
return line
def unstage_line(self) -> LevelLine:
"""Inverse of ``.stage_line()``.
"""
chart = self.chart._cursor.active_plot
chart.setCursor(QtCore.Qt.ArrowCursor)
cursor = chart._cursor
# delete "staged" cursor tracking line from view
line = self._active_staged_line
cursor._trackers.remove(line)
if self._stage_line:
self._stage_line.hide()
self._stage_line.label.hide()
# if line:
# line.delete()
self._active_staged_line = None
# show the crosshair y line
hl = cursor.graphics[chart]['hl']
hl.show()
def commit_line(self) -> LevelLine:
line = self._active_staged_line
if line:
chart = self.chart._cursor.active_plot
y = chart._cursor._datum_xy[1]
# XXX: should make this an explicit attr
# it's assigned inside ``.add_plot()``
lc = self.view.linked_charts
oid = str(uuid.uuid4())
lc._to_router.send_nowait({
'chart': lc,
'type': 'alert',
'price': y,
'oid': oid,
# 'symbol': lc.chart.name,
# 'brokers': lc.symbol.brokers,
# 'price': y,
})
line = level_line(
chart,
level=y,
color='alert_yellow',
)
# register for later
_lines[oid] = line
log.debug(f'clicked y: {y}')
def remove_line(
self,
line: LevelLine = None,
uuid: str = None,
) -> None:
"""Remove a line by refernce or uuid.
If no lines or ids are provided remove all lines under the
cursor position.
"""
# Delete any hoverable under the cursor
cursor = self.chart._cursor
if line:
line.delete()
else:
for item in cursor._hovered:
# hovered items must also offer
# a ``.delete()`` method
item.delete()
class ChartView(ViewBox): class ChartView(ViewBox):
@ -236,8 +350,9 @@ class ChartView(ViewBox):
self.addItem(self.select_box, ignoreBounds=True) self.addItem(self.select_box, ignoreBounds=True)
self._chart: 'ChartPlotWidget' = None # noqa self._chart: 'ChartPlotWidget' = None # noqa
self._lines_editor = LineEditor(view=self, _lines=_lines)
self._key_buffer = [] self._key_buffer = []
self._active_staged_line: 'LevelLine' = None # noqa self._active_staged_line: LevelLine = None # noqa
@property @property
def chart(self) -> 'ChartPlotWidget': # type: ignore # noqa def chart(self) -> 'ChartPlotWidget': # type: ignore # noqa
@ -247,6 +362,7 @@ class ChartView(ViewBox):
def chart(self, chart: 'ChartPlotWidget') -> None: # type: ignore # noqa def chart(self, chart: 'ChartPlotWidget') -> None: # type: ignore # noqa
self._chart = chart self._chart = chart
self.select_box.chart = chart self.select_box.chart = chart
self._lines_editor.chart = chart
def wheelEvent(self, ev, axis=None): def wheelEvent(self, ev, axis=None):
"""Override "center-point" location for scrolling. """Override "center-point" location for scrolling.
@ -407,7 +523,7 @@ class ChartView(ViewBox):
""" """
button = ev.button() button = ev.button()
pos = ev.pos() # pos = ev.pos()
if button == QtCore.Qt.RightButton and self.menuEnabled(): if button == QtCore.Qt.RightButton and self.menuEnabled():
ev.accept() ev.accept()
@ -417,33 +533,8 @@ class ChartView(ViewBox):
ev.accept() ev.accept()
line = self._active_staged_line # commit the "staged" line under the cursor
if line: self._lines_editor.commit_line()
chart = self.chart._cursor.active_plot
y = chart._cursor._datum_xy[1]
# XXX: should make this an explicit attr
# it's assigned inside ``.add_plot()``
lc = self.linked_charts
oid = str(uuid.uuid4())
lc._to_router.send_nowait({
'chart': lc,
'type': 'alert',
'price': y,
'oid': oid,
# 'symbol': lc.chart.name,
# 'brokers': lc.symbol.brokers,
# 'price': y,
})
line = level_line(
chart,
level=y,
color='alert_yellow',
)
_lines[oid] = line
log.info(f'clicked {pos}')
def keyReleaseEvent(self, ev): def keyReleaseEvent(self, ev):
""" """
@ -465,23 +556,8 @@ class ChartView(ViewBox):
self.setMouseMode(ViewBox.PanMode) self.setMouseMode(ViewBox.PanMode)
if text == 'a': if text == 'a':
# draw "staged" line under cursor position
chart = self.chart._cursor.active_plot self._lines_editor.unstage_line()
chart.setCursor(QtCore.Qt.ArrowCursor)
cursor = chart._cursor
# delete "staged" cursor tracking line from view
line = self._active_staged_line
cursor._trackers.remove(line)
if line:
line.delete()
self._active_staged_line = None
# show the crosshair y line
hl = cursor.graphics[chart]['hl']
hl.show()
def keyPressEvent(self, ev): def keyPressEvent(self, ev):
""" """
@ -526,35 +602,12 @@ class ChartView(ViewBox):
self.chart.default_view() self.chart.default_view()
elif text == 'a': elif text == 'a':
# add a line at the current cursor
chart = self.chart._cursor.active_plot self._lines_editor.stage_line()
chart.setCursor(QtCore.Qt.PointingHandCursor)
cursor = chart._cursor
# add a "staged" cursor-tracking alert line
line = level_line(
chart,
level=chart._cursor._datum_xy[1],
color='alert_yellow',
)
self._active_staged_line = line
# hide crosshair y-line
cursor.graphics[chart]['hl'].hide()
# add line to cursor trackers
cursor._trackers.add(line)
elif text == 'd': elif text == 'd':
# Delete any hoverable under the cursor # delete any lines under the cursor
cursor = self.chart._cursor self._lines_editor.remove_line()
chart = cursor.active_plot
for item in cursor._hovered:
# hovered items must also offer
# a ``.delete()`` method
item.delete()
# Leaving this for light reference purposes # Leaving this for light reference purposes