Add a "lines editor" api/component
parent
268f207a6c
commit
8d66a17daf
|
@ -1,5 +1,5 @@
|
|||
# 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
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
|
@ -47,8 +47,8 @@ _local_book = {}
|
|||
|
||||
|
||||
@dataclass
|
||||
class OrderBook:
|
||||
"""Send (client?) side order book tracking.
|
||||
class OrderBoi:
|
||||
"""'Buy' (client ?) side order book ctl and tracking.
|
||||
|
||||
Mostly for keeping local state to match the EMS and use
|
||||
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
|
||||
|
||||
if _orders is None:
|
||||
_orders = OrderBook
|
||||
_orders = OrderBoi
|
||||
|
||||
return _orders
|
||||
|
||||
|
@ -392,6 +392,8 @@ async def spawn_router_stream_alerts(
|
|||
oid = alert['oid']
|
||||
print(f'_lines: {_lines}')
|
||||
print(f'deleting line with oid: {oid}')
|
||||
|
||||
chart._vb._lines_editor
|
||||
_lines.pop(oid).delete()
|
||||
|
||||
# TODO: this in another task?
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
Lines for orders, alerts, L2.
|
||||
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple
|
||||
|
||||
import pyqtgraph as pg
|
||||
|
@ -87,10 +88,11 @@ class LevelLabel(YSticky):
|
|||
self.level = level
|
||||
|
||||
def set_label_str(self, level: float):
|
||||
# this is read inside ``.paint()``
|
||||
# self.label_str = '{size} x {level:.{digits}f}'.format(
|
||||
self.label_str = '{level:.{digits}f}'.format(
|
||||
# size=self._size,
|
||||
|
||||
# this is read inside ``.paint()``
|
||||
self.label_str = '{level:.{digits}f}'.format(
|
||||
digits=self.digits,
|
||||
level=level
|
||||
).replace(',', ' ')
|
||||
|
@ -200,14 +202,16 @@ class LevelLine(pg.InfiniteLine):
|
|||
chart: 'ChartPlotWidget', # type: ignore # noqa
|
||||
label: LevelLabel,
|
||||
highlight_color: str = 'default_light',
|
||||
hl_on_hover: bool = True,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.label = label
|
||||
|
||||
super().__init__(**kwargs)
|
||||
self.label = label
|
||||
|
||||
self.sigPositionChanged.connect(self.set_level)
|
||||
|
||||
self._chart = chart
|
||||
self._hoh = hl_on_hover
|
||||
|
||||
# use slightly thicker highlight
|
||||
pen = pg.mkPen(hcolor(highlight_color))
|
||||
|
@ -231,7 +235,8 @@ class LevelLine(pg.InfiniteLine):
|
|||
"""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
|
||||
|
||||
self.mouseHovering = hover
|
||||
|
@ -315,6 +320,10 @@ def level_line(
|
|||
|
||||
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
|
||||
) -> LevelLine:
|
||||
"""Convenience routine to add a styled horizontal line to a plot.
|
||||
|
@ -346,6 +355,7 @@ def level_line(
|
|||
highlight_color=color + '_light',
|
||||
movable=True,
|
||||
angle=0,
|
||||
hl_on_hover=hl_on_hover,
|
||||
)
|
||||
line.setValue(level)
|
||||
line.setPen(pg.mkPen(hcolor(color)))
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# 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
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
|
@ -17,7 +17,7 @@
|
|||
"""
|
||||
UX interaction customs.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
|
@ -28,7 +28,7 @@ import numpy as np
|
|||
|
||||
from ..log import get_logger
|
||||
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
|
||||
|
||||
|
||||
|
@ -199,19 +199,133 @@ class SelectRect(QtGui.QGraphicsRectItem):
|
|||
|
||||
|
||||
@dataclass
|
||||
class LinesEditor:
|
||||
class LineEditor:
|
||||
view: 'ChartView'
|
||||
chart: 'ChartPlotWidget'
|
||||
active_line: 'LevelLine'
|
||||
_lines: field(default_factory=dict)
|
||||
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):
|
||||
|
@ -236,8 +350,9 @@ class ChartView(ViewBox):
|
|||
self.addItem(self.select_box, ignoreBounds=True)
|
||||
self._chart: 'ChartPlotWidget' = None # noqa
|
||||
|
||||
self._lines_editor = LineEditor(view=self, _lines=_lines)
|
||||
self._key_buffer = []
|
||||
self._active_staged_line: 'LevelLine' = None # noqa
|
||||
self._active_staged_line: LevelLine = None # noqa
|
||||
|
||||
@property
|
||||
def chart(self) -> 'ChartPlotWidget': # type: ignore # noqa
|
||||
|
@ -247,6 +362,7 @@ class ChartView(ViewBox):
|
|||
def chart(self, chart: 'ChartPlotWidget') -> None: # type: ignore # noqa
|
||||
self._chart = chart
|
||||
self.select_box.chart = chart
|
||||
self._lines_editor.chart = chart
|
||||
|
||||
def wheelEvent(self, ev, axis=None):
|
||||
"""Override "center-point" location for scrolling.
|
||||
|
@ -407,7 +523,7 @@ class ChartView(ViewBox):
|
|||
|
||||
"""
|
||||
button = ev.button()
|
||||
pos = ev.pos()
|
||||
# pos = ev.pos()
|
||||
|
||||
if button == QtCore.Qt.RightButton and self.menuEnabled():
|
||||
ev.accept()
|
||||
|
@ -417,33 +533,8 @@ class ChartView(ViewBox):
|
|||
|
||||
ev.accept()
|
||||
|
||||
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.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}')
|
||||
# commit the "staged" line under the cursor
|
||||
self._lines_editor.commit_line()
|
||||
|
||||
def keyReleaseEvent(self, ev):
|
||||
"""
|
||||
|
@ -465,23 +556,8 @@ class ChartView(ViewBox):
|
|||
self.setMouseMode(ViewBox.PanMode)
|
||||
|
||||
if text == 'a':
|
||||
|
||||
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 line:
|
||||
line.delete()
|
||||
|
||||
self._active_staged_line = None
|
||||
|
||||
# show the crosshair y line
|
||||
hl = cursor.graphics[chart]['hl']
|
||||
hl.show()
|
||||
# draw "staged" line under cursor position
|
||||
self._lines_editor.unstage_line()
|
||||
|
||||
def keyPressEvent(self, ev):
|
||||
"""
|
||||
|
@ -526,35 +602,12 @@ class ChartView(ViewBox):
|
|||
self.chart.default_view()
|
||||
|
||||
elif text == 'a':
|
||||
|
||||
chart = self.chart._cursor.active_plot
|
||||
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)
|
||||
# add a line at the current cursor
|
||||
self._lines_editor.stage_line()
|
||||
|
||||
elif text == 'd':
|
||||
# Delete any hoverable under the cursor
|
||||
cursor = self.chart._cursor
|
||||
chart = cursor.active_plot
|
||||
|
||||
for item in cursor._hovered:
|
||||
# hovered items must also offer
|
||||
# a ``.delete()`` method
|
||||
item.delete()
|
||||
# delete any lines under the cursor
|
||||
self._lines_editor.remove_line()
|
||||
|
||||
# Leaving this for light reference purposes
|
||||
|
||||
|
|
Loading…
Reference in New Issue