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