Add a "lines editor" api/component
parent
268f207a6c
commit
8d66a17daf
|
@ -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?
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue