Convert view box to async input handling
Instead of callbacks for key presses/releases convert our `ChartView`'s kb input handling to async code using our event relaying-over-mem-chan system. This is a first step toward a more async driven modal control UX. Changed a bunch of "chart" component naming as part of this as well, namely: `ChartSpace` -> `GodWidget` and `LinkedSplitCharts` -> `LinkedSplits`. Engage the view boxe's async handler code as part of new symbol data loading in `display_symbol_data()`. More re-orging to come!asyncify_input_modes
parent
85621af8af
commit
75804a441c
|
@ -26,6 +26,7 @@ from functools import partial
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtGui
|
from PyQt5 import QtCore, QtGui
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtCore import QEvent
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
import tractor
|
import tractor
|
||||||
|
@ -60,7 +61,7 @@ from ._style import (
|
||||||
_bars_to_left_in_follow_mode,
|
_bars_to_left_in_follow_mode,
|
||||||
)
|
)
|
||||||
from . import _search
|
from . import _search
|
||||||
from ._event import open_key_stream
|
from . import _event
|
||||||
from ..data._source import Symbol
|
from ..data._source import Symbol
|
||||||
from ..data._sharedmem import ShmArray
|
from ..data._sharedmem import ShmArray
|
||||||
from ..data import maybe_open_shm_array
|
from ..data import maybe_open_shm_array
|
||||||
|
@ -77,13 +78,23 @@ from ..data import feed
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ChartSpace(QtGui.QWidget):
|
class GodWidget(QtGui.QWidget):
|
||||||
'''Highest level composed widget which contains layouts for
|
'''
|
||||||
|
"Our lord and savior, the holy child of window-shua, there is no
|
||||||
|
widget above thee." - 6|6
|
||||||
|
|
||||||
|
The highest level composed widget which contains layouts for
|
||||||
organizing lower level charts as well as other widgets used to
|
organizing lower level charts as well as other widgets used to
|
||||||
control or modify them.
|
control or modify them.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
def __init__(self, parent=None):
|
def __init__(
|
||||||
|
|
||||||
|
self,
|
||||||
|
parent=None,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self.hbox = QtGui.QHBoxLayout(self)
|
self.hbox = QtGui.QHBoxLayout(self)
|
||||||
|
@ -96,51 +107,54 @@ class ChartSpace(QtGui.QWidget):
|
||||||
|
|
||||||
self.hbox.addLayout(self.vbox)
|
self.hbox.addLayout(self.vbox)
|
||||||
|
|
||||||
self.toolbar_layout = QtGui.QHBoxLayout()
|
# self.toolbar_layout = QtGui.QHBoxLayout()
|
||||||
self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
|
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
# self.vbox.addLayout(self.toolbar_layout)
|
||||||
|
|
||||||
# self.init_timeframes_ui()
|
# self.init_timeframes_ui()
|
||||||
# self.init_strategy_ui()
|
# self.init_strategy_ui()
|
||||||
self.vbox.addLayout(self.toolbar_layout)
|
|
||||||
# self.vbox.addLayout(self.hbox)
|
# self.vbox.addLayout(self.hbox)
|
||||||
|
|
||||||
self._chart_cache = {}
|
self._chart_cache = {}
|
||||||
self.linkedcharts: 'LinkedSplitCharts' = None
|
self.linkedsplits: 'LinkedSplits' = None
|
||||||
self._root_n: Optional[trio.Nursery] = None
|
|
||||||
|
# assigned in the startup func `_async_main()`
|
||||||
|
self._root_n: trio.Nursery = None
|
||||||
|
self._task_stack: AsyncExitStack = None
|
||||||
|
|
||||||
def set_chart_symbol(
|
def set_chart_symbol(
|
||||||
self,
|
self,
|
||||||
symbol_key: str, # of form <fqsn>.<providername>
|
symbol_key: str, # of form <fqsn>.<providername>
|
||||||
linked_charts: 'LinkedSplitCharts', # type: ignore
|
linkedsplits: 'LinkedSplits', # type: ignore
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
# re-sort org cache symbol list in LIFO order
|
# re-sort org cache symbol list in LIFO order
|
||||||
cache = self._chart_cache
|
cache = self._chart_cache
|
||||||
cache.pop(symbol_key, None)
|
cache.pop(symbol_key, None)
|
||||||
cache[symbol_key] = linked_charts
|
cache[symbol_key] = linkedsplits
|
||||||
|
|
||||||
def get_chart_symbol(
|
def get_chart_symbol(
|
||||||
self,
|
self,
|
||||||
symbol_key: str,
|
symbol_key: str,
|
||||||
) -> 'LinkedSplitCharts': # type: ignore
|
) -> 'LinkedSplits': # type: ignore
|
||||||
return self._chart_cache.get(symbol_key)
|
return self._chart_cache.get(symbol_key)
|
||||||
|
|
||||||
def init_timeframes_ui(self):
|
# def init_timeframes_ui(self):
|
||||||
self.tf_layout = QtGui.QHBoxLayout()
|
# self.tf_layout = QtGui.QHBoxLayout()
|
||||||
self.tf_layout.setSpacing(0)
|
# self.tf_layout.setSpacing(0)
|
||||||
self.tf_layout.setContentsMargins(0, 12, 0, 0)
|
# self.tf_layout.setContentsMargins(0, 12, 0, 0)
|
||||||
time_frames = ('1M', '5M', '15M', '30M', '1H', '1D', '1W', 'MN')
|
# time_frames = ('1M', '5M', '15M', '30M', '1H', '1D', '1W', 'MN')
|
||||||
btn_prefix = 'TF'
|
# btn_prefix = 'TF'
|
||||||
|
|
||||||
for tf in time_frames:
|
# for tf in time_frames:
|
||||||
btn_name = ''.join([btn_prefix, tf])
|
# btn_name = ''.join([btn_prefix, tf])
|
||||||
btn = QtGui.QPushButton(tf)
|
# btn = QtGui.QPushButton(tf)
|
||||||
# TODO:
|
# # TODO:
|
||||||
btn.setEnabled(False)
|
# btn.setEnabled(False)
|
||||||
setattr(self, btn_name, btn)
|
# setattr(self, btn_name, btn)
|
||||||
self.tf_layout.addWidget(btn)
|
# self.tf_layout.addWidget(btn)
|
||||||
|
|
||||||
self.toolbar_layout.addLayout(self.tf_layout)
|
# self.toolbar_layout.addLayout(self.tf_layout)
|
||||||
|
|
||||||
# XXX: strat loader/saver that we don't need yet.
|
# XXX: strat loader/saver that we don't need yet.
|
||||||
# def init_strategy_ui(self):
|
# def init_strategy_ui(self):
|
||||||
|
@ -166,46 +180,46 @@ class ChartSpace(QtGui.QWidget):
|
||||||
# fully qualified symbol name (SNS i guess is what we're making?)
|
# fully qualified symbol name (SNS i guess is what we're making?)
|
||||||
fqsn = '.'.join([symbol_key, providername])
|
fqsn = '.'.join([symbol_key, providername])
|
||||||
|
|
||||||
linkedcharts = self.get_chart_symbol(fqsn)
|
linkedsplits = self.get_chart_symbol(fqsn)
|
||||||
|
|
||||||
if not self.vbox.isEmpty():
|
if not self.vbox.isEmpty():
|
||||||
# XXX: this is CRITICAL especially with pixel buffer caching
|
# XXX: this is CRITICAL especially with pixel buffer caching
|
||||||
self.linkedcharts.hide()
|
self.linkedsplits.hide()
|
||||||
|
|
||||||
# XXX: pretty sure we don't need this
|
# XXX: pretty sure we don't need this
|
||||||
# remove any existing plots?
|
# remove any existing plots?
|
||||||
# XXX: ahh we might want to support cache unloading..
|
# XXX: ahh we might want to support cache unloading..
|
||||||
self.vbox.removeWidget(self.linkedcharts)
|
self.vbox.removeWidget(self.linkedsplits)
|
||||||
|
|
||||||
# switching to a new viewable chart
|
# switching to a new viewable chart
|
||||||
if linkedcharts is None or reset:
|
if linkedsplits is None or reset:
|
||||||
|
|
||||||
# we must load a fresh linked charts set
|
# we must load a fresh linked charts set
|
||||||
linkedcharts = LinkedSplitCharts(self)
|
linkedsplits = LinkedSplits(self)
|
||||||
|
|
||||||
# spawn new task to start up and update new sub-chart instances
|
# spawn new task to start up and update new sub-chart instances
|
||||||
self._root_n.start_soon(
|
self._root_n.start_soon(
|
||||||
chart_symbol,
|
display_symbol_data,
|
||||||
self,
|
self,
|
||||||
providername,
|
providername,
|
||||||
symbol_key,
|
symbol_key,
|
||||||
loglevel,
|
loglevel,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.set_chart_symbol(fqsn, linkedcharts)
|
self.set_chart_symbol(fqsn, linkedsplits)
|
||||||
|
|
||||||
self.vbox.addWidget(linkedcharts)
|
self.vbox.addWidget(linkedsplits)
|
||||||
|
|
||||||
# chart is already in memory so just focus it
|
# chart is already in memory so just focus it
|
||||||
if self.linkedcharts:
|
if self.linkedsplits:
|
||||||
self.linkedcharts.unfocus()
|
self.linkedsplits.unfocus()
|
||||||
|
|
||||||
# self.vbox.addWidget(linkedcharts)
|
# self.vbox.addWidget(linkedsplits)
|
||||||
linkedcharts.show()
|
linkedsplits.show()
|
||||||
linkedcharts.focus()
|
linkedsplits.focus()
|
||||||
self.linkedcharts = linkedcharts
|
self.linkedsplits = linkedsplits
|
||||||
|
|
||||||
symbol = linkedcharts.symbol
|
symbol = linkedsplits.symbol
|
||||||
|
|
||||||
if symbol is not None:
|
if symbol is not None:
|
||||||
self.window.setWindowTitle(
|
self.window.setWindowTitle(
|
||||||
|
@ -214,14 +228,16 @@ class ChartSpace(QtGui.QWidget):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LinkedSplitCharts(QtGui.QWidget):
|
class LinkedSplits(QtGui.QWidget):
|
||||||
"""Widget that holds a central chart plus derived
|
'''
|
||||||
|
Widget that holds a central chart plus derived
|
||||||
subcharts computed from the original data set apart
|
subcharts computed from the original data set apart
|
||||||
by splitters for resizing.
|
by splitters for resizing.
|
||||||
|
|
||||||
A single internal references to the data is maintained
|
A single internal references to the data is maintained
|
||||||
for each chart and can be updated externally.
|
for each chart and can be updated externally.
|
||||||
"""
|
|
||||||
|
'''
|
||||||
long_pen = pg.mkPen('#006000')
|
long_pen = pg.mkPen('#006000')
|
||||||
long_brush = pg.mkBrush('#00ff00')
|
long_brush = pg.mkBrush('#00ff00')
|
||||||
short_pen = pg.mkPen('#600000')
|
short_pen = pg.mkPen('#600000')
|
||||||
|
@ -232,21 +248,24 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
||||||
self,
|
self,
|
||||||
chart_space: ChartSpace,
|
godwidget: GodWidget,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.signals_visible: bool = False
|
|
||||||
|
# self.signals_visible: bool = False
|
||||||
self._cursor: Cursor = None # crosshair graphics
|
self._cursor: Cursor = None # crosshair graphics
|
||||||
|
|
||||||
|
self.godwidget = godwidget
|
||||||
self.chart: ChartPlotWidget = None # main (ohlc) chart
|
self.chart: ChartPlotWidget = None # main (ohlc) chart
|
||||||
self.subplots: Dict[Tuple[str, ...], ChartPlotWidget] = {}
|
self.subplots: Dict[Tuple[str, ...], ChartPlotWidget] = {}
|
||||||
self.chart_space = chart_space
|
|
||||||
|
|
||||||
self.chart_space = chart_space
|
self.godwidget = godwidget
|
||||||
|
|
||||||
self.xaxis = DynamicDateAxis(
|
self.xaxis = DynamicDateAxis(
|
||||||
orientation='bottom',
|
orientation='bottom',
|
||||||
linked_charts=self
|
linkedsplits=self
|
||||||
)
|
)
|
||||||
# if _xaxis_at == 'bottom':
|
# if _xaxis_at == 'bottom':
|
||||||
# self.xaxis.setStyle(showValues=False)
|
# self.xaxis.setStyle(showValues=False)
|
||||||
|
@ -302,7 +321,7 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
"""
|
"""
|
||||||
# add crosshairs
|
# add crosshairs
|
||||||
self._cursor = Cursor(
|
self._cursor = Cursor(
|
||||||
linkedsplitcharts=self,
|
linkedsplits=self,
|
||||||
digits=symbol.digits(),
|
digits=symbol.digits(),
|
||||||
)
|
)
|
||||||
self.chart = self.add_plot(
|
self.chart = self.add_plot(
|
||||||
|
@ -342,14 +361,14 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
"A main plot must be created first with `.plot_ohlc_main()`")
|
"A main plot must be created first with `.plot_ohlc_main()`")
|
||||||
|
|
||||||
# source of our custom interactions
|
# source of our custom interactions
|
||||||
cv = ChartView()
|
cv = ChartView(name)
|
||||||
cv.linked_charts = self
|
cv.linkedsplits = self
|
||||||
|
|
||||||
# use "indicator axis" by default
|
# use "indicator axis" by default
|
||||||
if xaxis is None:
|
if xaxis is None:
|
||||||
xaxis = DynamicDateAxis(
|
xaxis = DynamicDateAxis(
|
||||||
orientation='bottom',
|
orientation='bottom',
|
||||||
linked_charts=self
|
linkedsplits=self
|
||||||
)
|
)
|
||||||
|
|
||||||
cpw = ChartPlotWidget(
|
cpw = ChartPlotWidget(
|
||||||
|
@ -360,11 +379,11 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
|
|
||||||
array=array,
|
array=array,
|
||||||
parent=self.splitter,
|
parent=self.splitter,
|
||||||
linked_charts=self,
|
linkedsplits=self,
|
||||||
axisItems={
|
axisItems={
|
||||||
'bottom': xaxis,
|
'bottom': xaxis,
|
||||||
'right': PriceAxis(linked_charts=self, orientation='right'),
|
'right': PriceAxis(linkedsplits=self, orientation='right'),
|
||||||
'left': PriceAxis(linked_charts=self, orientation='left'),
|
'left': PriceAxis(linkedsplits=self, orientation='left'),
|
||||||
},
|
},
|
||||||
viewBox=cv,
|
viewBox=cv,
|
||||||
cursor=self._cursor,
|
cursor=self._cursor,
|
||||||
|
@ -377,7 +396,7 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
# (see our custom view mode in `._interactions.py`)
|
# (see our custom view mode in `._interactions.py`)
|
||||||
cv.chart = cpw
|
cv.chart = cpw
|
||||||
|
|
||||||
cpw.plotItem.vb.linked_charts = self
|
cpw.plotItem.vb.linkedsplits = self
|
||||||
cpw.setFrameStyle(QtGui.QFrame.StyledPanel) # | QtGui.QFrame.Plain)
|
cpw.setFrameStyle(QtGui.QFrame.StyledPanel) # | QtGui.QFrame.Plain)
|
||||||
cpw.hideButtons()
|
cpw.hideButtons()
|
||||||
# XXX: gives us outline on backside of y-axis
|
# XXX: gives us outline on backside of y-axis
|
||||||
|
@ -415,7 +434,8 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
|
|
||||||
|
|
||||||
class ChartPlotWidget(pg.PlotWidget):
|
class ChartPlotWidget(pg.PlotWidget):
|
||||||
"""``GraphicsView`` subtype containing a single ``PlotItem``.
|
'''
|
||||||
|
``GraphicsView`` subtype containing a single ``PlotItem``.
|
||||||
|
|
||||||
- The added methods allow for plotting OHLC sequences from
|
- The added methods allow for plotting OHLC sequences from
|
||||||
``np.ndarray``s with appropriate field names.
|
``np.ndarray``s with appropriate field names.
|
||||||
|
@ -425,7 +445,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
(Could be replaced with a ``pg.GraphicsLayoutWidget`` if we
|
(Could be replaced with a ``pg.GraphicsLayoutWidget`` if we
|
||||||
eventually want multiple plots managed together?)
|
eventually want multiple plots managed together?)
|
||||||
"""
|
|
||||||
|
'''
|
||||||
sig_mouse_leave = QtCore.Signal(object)
|
sig_mouse_leave = QtCore.Signal(object)
|
||||||
sig_mouse_enter = QtCore.Signal(object)
|
sig_mouse_enter = QtCore.Signal(object)
|
||||||
|
|
||||||
|
@ -441,7 +462,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# the data view we generate graphics from
|
# the data view we generate graphics from
|
||||||
name: str,
|
name: str,
|
||||||
array: np.ndarray,
|
array: np.ndarray,
|
||||||
linked_charts: LinkedSplitCharts,
|
linkedsplits: LinkedSplits,
|
||||||
|
|
||||||
view_color: str = 'papas_special',
|
view_color: str = 'papas_special',
|
||||||
pen_color: str = 'bracket',
|
pen_color: str = 'bracket',
|
||||||
|
@ -465,7 +486,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
self.name = name
|
self.name = name
|
||||||
self._lc = linked_charts
|
self._lc = linkedsplits
|
||||||
|
|
||||||
# scene-local placeholder for book graphics
|
# scene-local placeholder for book graphics
|
||||||
# sizing to avoid overlap with data contents
|
# sizing to avoid overlap with data contents
|
||||||
|
@ -1006,10 +1027,12 @@ _book_throttle_rate: int = 16 # Hz
|
||||||
|
|
||||||
|
|
||||||
async def chart_from_quotes(
|
async def chart_from_quotes(
|
||||||
|
|
||||||
chart: ChartPlotWidget,
|
chart: ChartPlotWidget,
|
||||||
stream,
|
stream: tractor.MsgStream,
|
||||||
ohlcv: np.ndarray,
|
ohlcv: np.ndarray,
|
||||||
wap_in_history: bool = False,
|
wap_in_history: bool = False,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""The 'main' (price) chart real-time update loop.
|
"""The 'main' (price) chart real-time update loop.
|
||||||
|
|
||||||
|
@ -1211,12 +1234,14 @@ async def chart_from_quotes(
|
||||||
|
|
||||||
|
|
||||||
async def spawn_fsps(
|
async def spawn_fsps(
|
||||||
linked_charts: LinkedSplitCharts,
|
|
||||||
|
linkedsplits: LinkedSplits,
|
||||||
fsps: Dict[str, str],
|
fsps: Dict[str, str],
|
||||||
sym,
|
sym,
|
||||||
src_shm,
|
src_shm,
|
||||||
brokermod,
|
brokermod,
|
||||||
loglevel,
|
loglevel,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Start financial signal processing in subactor.
|
"""Start financial signal processing in subactor.
|
||||||
|
|
||||||
|
@ -1224,7 +1249,7 @@ async def spawn_fsps(
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
linked_charts.focus()
|
linkedsplits.focus()
|
||||||
|
|
||||||
# spawns sub-processes which execute cpu bound FSP code
|
# spawns sub-processes which execute cpu bound FSP code
|
||||||
async with tractor.open_nursery(loglevel=loglevel) as n:
|
async with tractor.open_nursery(loglevel=loglevel) as n:
|
||||||
|
@ -1271,7 +1296,7 @@ async def spawn_fsps(
|
||||||
ln.start_soon(
|
ln.start_soon(
|
||||||
run_fsp,
|
run_fsp,
|
||||||
portal,
|
portal,
|
||||||
linked_charts,
|
linkedsplits,
|
||||||
brokermod,
|
brokermod,
|
||||||
sym,
|
sym,
|
||||||
src_shm,
|
src_shm,
|
||||||
|
@ -1286,7 +1311,7 @@ async def spawn_fsps(
|
||||||
async def run_fsp(
|
async def run_fsp(
|
||||||
|
|
||||||
portal: tractor._portal.Portal,
|
portal: tractor._portal.Portal,
|
||||||
linked_charts: LinkedSplitCharts,
|
linkedsplits: LinkedSplits,
|
||||||
brokermod: ModuleType,
|
brokermod: ModuleType,
|
||||||
sym: str,
|
sym: str,
|
||||||
src_shm: ShmArray,
|
src_shm: ShmArray,
|
||||||
|
@ -1300,7 +1325,7 @@ async def run_fsp(
|
||||||
This is called once for each entry in the fsp
|
This is called once for each entry in the fsp
|
||||||
config map.
|
config map.
|
||||||
"""
|
"""
|
||||||
done = linked_charts.window().status_bar.open_status(
|
done = linkedsplits.window().status_bar.open_status(
|
||||||
f'loading FSP: {display_name}..')
|
f'loading FSP: {display_name}..')
|
||||||
|
|
||||||
async with portal.open_stream_from(
|
async with portal.open_stream_from(
|
||||||
|
@ -1324,7 +1349,7 @@ async def run_fsp(
|
||||||
shm = conf['shm']
|
shm = conf['shm']
|
||||||
|
|
||||||
if conf.get('overlay'):
|
if conf.get('overlay'):
|
||||||
chart = linked_charts.chart
|
chart = linkedsplits.chart
|
||||||
chart.draw_curve(
|
chart.draw_curve(
|
||||||
name='vwap',
|
name='vwap',
|
||||||
data=shm.array,
|
data=shm.array,
|
||||||
|
@ -1334,7 +1359,7 @@ async def run_fsp(
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
chart = linked_charts.add_plot(
|
chart = linkedsplits.add_plot(
|
||||||
name=fsp_func_name,
|
name=fsp_func_name,
|
||||||
array=shm.array,
|
array=shm.array,
|
||||||
|
|
||||||
|
@ -1358,7 +1383,7 @@ async def run_fsp(
|
||||||
chart._shm = shm
|
chart._shm = shm
|
||||||
|
|
||||||
# should **not** be the same sub-chart widget
|
# should **not** be the same sub-chart widget
|
||||||
assert chart.name != linked_charts.chart.name
|
assert chart.name != linkedsplits.chart.name
|
||||||
|
|
||||||
# sticky only on sub-charts atm
|
# sticky only on sub-charts atm
|
||||||
last_val_sticky = chart._ysticks[chart.name]
|
last_val_sticky = chart._ysticks[chart.name]
|
||||||
|
@ -1441,7 +1466,7 @@ async def run_fsp(
|
||||||
last = now
|
last = now
|
||||||
|
|
||||||
|
|
||||||
async def check_for_new_bars(feed, ohlcv, linked_charts):
|
async def check_for_new_bars(feed, ohlcv, linkedsplits):
|
||||||
"""Task which updates from new bars in the shared ohlcv buffer every
|
"""Task which updates from new bars in the shared ohlcv buffer every
|
||||||
``delay_s`` seconds.
|
``delay_s`` seconds.
|
||||||
|
|
||||||
|
@ -1451,7 +1476,7 @@ async def check_for_new_bars(feed, ohlcv, linked_charts):
|
||||||
# Likely the best way to solve this is to make this task
|
# Likely the best way to solve this is to make this task
|
||||||
# aware of the instrument's tradable hours?
|
# aware of the instrument's tradable hours?
|
||||||
|
|
||||||
price_chart = linked_charts.chart
|
price_chart = linkedsplits.chart
|
||||||
price_chart.default_view()
|
price_chart.default_view()
|
||||||
|
|
||||||
async with feed.index_stream() as stream:
|
async with feed.index_stream() as stream:
|
||||||
|
@ -1489,55 +1514,63 @@ async def check_for_new_bars(feed, ohlcv, linked_charts):
|
||||||
price_chart._arrays[name]
|
price_chart._arrays[name]
|
||||||
)
|
)
|
||||||
|
|
||||||
for name, chart in linked_charts.subplots.items():
|
for name, chart in linkedsplits.subplots.items():
|
||||||
chart.update_curve_from_array(chart.name, chart._shm.array)
|
chart.update_curve_from_array(chart.name, chart._shm.array)
|
||||||
|
|
||||||
# shift the view if in follow mode
|
# shift the view if in follow mode
|
||||||
price_chart.increment_view()
|
price_chart.increment_view()
|
||||||
|
|
||||||
|
|
||||||
async def chart_symbol(
|
async def display_symbol_data(
|
||||||
chart_app: ChartSpace,
|
|
||||||
|
godwidget: GodWidget,
|
||||||
provider: str,
|
provider: str,
|
||||||
sym: str,
|
sym: str,
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Spawn a real-time chart widget for this symbol and app session.
|
'''Spawn a real-time displayed and updated chart for provider symbol.
|
||||||
|
|
||||||
These widgets can remain up but hidden so that multiple symbols
|
Spawned ``LinkedSplits`` chart widgets can remain up but hidden so
|
||||||
can be viewed and switched between extremely fast.
|
that multiple symbols can be viewed and switched between extremely
|
||||||
|
fast from a cached watch-list.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
sbar = chart_app.window.status_bar
|
sbar = godwidget.window.status_bar
|
||||||
loading_sym_done = sbar.open_status(f'loading {sym}.{provider}..')
|
loading_sym_done = sbar.open_status(f'loading {sym}.{provider}..')
|
||||||
|
|
||||||
# historical data fetch
|
# historical data fetch
|
||||||
brokermod = brokers.get_brokermod(provider)
|
brokermod = brokers.get_brokermod(provider)
|
||||||
|
|
||||||
async with data.open_feed(
|
async with(
|
||||||
|
|
||||||
provider,
|
data.open_feed(
|
||||||
[sym],
|
provider,
|
||||||
loglevel=loglevel,
|
[sym],
|
||||||
|
loglevel=loglevel,
|
||||||
|
|
||||||
# 60 FPS to limit context switches
|
# 60 FPS to limit context switches
|
||||||
tick_throttle=_clear_throttle_rate,
|
tick_throttle=_clear_throttle_rate,
|
||||||
|
|
||||||
) as feed:
|
) as feed,
|
||||||
|
|
||||||
|
trio.open_nursery() as n,
|
||||||
|
):
|
||||||
|
|
||||||
ohlcv: ShmArray = feed.shm
|
ohlcv: ShmArray = feed.shm
|
||||||
bars = ohlcv.array
|
bars = ohlcv.array
|
||||||
symbol = feed.symbols[sym]
|
symbol = feed.symbols[sym]
|
||||||
|
|
||||||
# load in symbol's ohlc data
|
# load in symbol's ohlc data
|
||||||
chart_app.window.setWindowTitle(
|
godwidget.window.setWindowTitle(
|
||||||
f'{symbol.key}@{symbol.brokers} '
|
f'{symbol.key}@{symbol.brokers} '
|
||||||
f'tick:{symbol.tick_size}'
|
f'tick:{symbol.tick_size}'
|
||||||
)
|
)
|
||||||
|
|
||||||
linked_charts = chart_app.linkedcharts
|
linkedsplits = godwidget.linkedsplits
|
||||||
linked_charts._symbol = symbol
|
linkedsplits._symbol = symbol
|
||||||
chart = linked_charts.plot_ohlc_main(symbol, bars)
|
|
||||||
|
chart = linkedsplits.plot_ohlc_main(symbol, bars)
|
||||||
chart.setFocus()
|
chart.setFocus()
|
||||||
|
|
||||||
# plot historical vwap if available
|
# plot historical vwap if available
|
||||||
|
@ -1591,54 +1624,59 @@ async def chart_symbol(
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
async with trio.open_nursery() as n:
|
|
||||||
|
|
||||||
# load initial fsp chain (otherwise known as "indicators")
|
# load initial fsp chain (otherwise known as "indicators")
|
||||||
n.start_soon(
|
n.start_soon(
|
||||||
spawn_fsps,
|
spawn_fsps,
|
||||||
linked_charts,
|
linkedsplits,
|
||||||
fsp_conf,
|
fsp_conf,
|
||||||
sym,
|
sym,
|
||||||
ohlcv,
|
ohlcv,
|
||||||
brokermod,
|
brokermod,
|
||||||
loglevel,
|
loglevel,
|
||||||
)
|
)
|
||||||
|
|
||||||
# start graphics update loop(s)after receiving first live quote
|
# start graphics update loop(s)after receiving first live quote
|
||||||
n.start_soon(
|
n.start_soon(
|
||||||
chart_from_quotes,
|
chart_from_quotes,
|
||||||
chart,
|
chart,
|
||||||
feed.stream,
|
feed.stream,
|
||||||
ohlcv,
|
ohlcv,
|
||||||
wap_in_history,
|
wap_in_history,
|
||||||
)
|
)
|
||||||
|
|
||||||
# wait for a first quote before we start any update tasks
|
# wait for a first quote before we start any update tasks
|
||||||
# quote = await feed.receive()
|
# quote = await feed.receive()
|
||||||
# log.info(f'Received first quote {quote}')
|
# log.info(f'Received first quote {quote}')
|
||||||
|
|
||||||
n.start_soon(
|
n.start_soon(
|
||||||
check_for_new_bars,
|
check_for_new_bars,
|
||||||
feed,
|
feed,
|
||||||
# delay,
|
# delay,
|
||||||
ohlcv,
|
ohlcv,
|
||||||
linked_charts
|
linkedsplits
|
||||||
)
|
)
|
||||||
|
|
||||||
# interactive testing
|
# interactive testing
|
||||||
# n.start_soon(
|
# n.start_soon(
|
||||||
# test_bed,
|
# test_bed,
|
||||||
# ohlcv,
|
# ohlcv,
|
||||||
# chart,
|
# chart,
|
||||||
# linked_charts,
|
# linkedsplits,
|
||||||
# )
|
# )
|
||||||
|
|
||||||
|
# start async input handling for chart's view
|
||||||
|
# await godwidget._task_stack.enter_async_context(
|
||||||
|
async with chart._vb.open_async_input_handler():
|
||||||
|
|
||||||
await start_order_mode(chart, symbol, provider)
|
await start_order_mode(chart, symbol, provider)
|
||||||
|
|
||||||
|
|
||||||
async def load_providers(
|
async def load_providers(
|
||||||
|
|
||||||
brokernames: list[str],
|
brokernames: list[str],
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
# TODO: seems like our incentive for brokerd caching lelel
|
# TODO: seems like our incentive for brokerd caching lelel
|
||||||
|
@ -1673,8 +1711,9 @@ async def load_providers(
|
||||||
|
|
||||||
|
|
||||||
async def _async_main(
|
async def _async_main(
|
||||||
|
|
||||||
# implicit required argument provided by ``qtractor_run()``
|
# implicit required argument provided by ``qtractor_run()``
|
||||||
main_widget: ChartSpace,
|
main_widget: GodWidget,
|
||||||
|
|
||||||
sym: str,
|
sym: str,
|
||||||
brokernames: str,
|
brokernames: str,
|
||||||
|
@ -1688,13 +1727,14 @@ async def _async_main(
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
chart_app = main_widget
|
godwidget = main_widget
|
||||||
|
|
||||||
# attempt to configure DPI aware font size
|
# attempt to configure DPI aware font size
|
||||||
screen = chart_app.window.current_screen()
|
screen = godwidget.window.current_screen()
|
||||||
|
|
||||||
# configure graphics update throttling based on display refresh rate
|
# configure graphics update throttling based on display refresh rate
|
||||||
global _clear_throttle_rate
|
global _clear_throttle_rate
|
||||||
|
|
||||||
_clear_throttle_rate = min(
|
_clear_throttle_rate = min(
|
||||||
round(screen.refreshRate()),
|
round(screen.refreshRate()),
|
||||||
_clear_throttle_rate,
|
_clear_throttle_rate,
|
||||||
|
@ -1702,37 +1742,39 @@ async def _async_main(
|
||||||
log.info(f'Set graphics update rate to {_clear_throttle_rate} Hz')
|
log.info(f'Set graphics update rate to {_clear_throttle_rate} Hz')
|
||||||
|
|
||||||
# TODO: do styling / themeing setup
|
# TODO: do styling / themeing setup
|
||||||
# _style.style_ze_sheets(chart_app)
|
# _style.style_ze_sheets(godwidget)
|
||||||
|
|
||||||
sbar = chart_app.window.status_bar
|
sbar = godwidget.window.status_bar
|
||||||
starting_done = sbar.open_status('starting ze chartz...')
|
starting_done = sbar.open_status('starting ze chartz...')
|
||||||
|
|
||||||
async with trio.open_nursery() as root_n:
|
async with (
|
||||||
|
trio.open_nursery() as root_n,
|
||||||
|
AsyncExitStack() as chart_task_stack,
|
||||||
|
):
|
||||||
|
|
||||||
# set root nursery for spawning other charts/feeds
|
# set root nursery and task stack for spawning other charts/feeds
|
||||||
# that run cached in the bg
|
# that run cached in the bg
|
||||||
chart_app._root_n = root_n
|
godwidget._root_n = root_n
|
||||||
|
godwidget._task_stack = chart_task_stack
|
||||||
|
|
||||||
# setup search widget
|
# setup search widget and focus main chart view at startup
|
||||||
search = _search.SearchWidget(chart_space=chart_app)
|
search = _search.SearchWidget(godwidget=godwidget)
|
||||||
|
|
||||||
# the main chart's view is given focus at startup
|
|
||||||
search.bar.unfocus()
|
search.bar.unfocus()
|
||||||
|
|
||||||
# add search singleton to global chart-space widget
|
# add search singleton to global chart-space widget
|
||||||
chart_app.hbox.addWidget(
|
godwidget.hbox.addWidget(
|
||||||
search,
|
search,
|
||||||
|
|
||||||
# alights to top and uses minmial space based on
|
# alights to top and uses minmial space based on
|
||||||
# search bar size hint (i think?)
|
# search bar size hint (i think?)
|
||||||
alignment=Qt.AlignTop
|
alignment=Qt.AlignTop
|
||||||
)
|
)
|
||||||
chart_app.search = search
|
godwidget.search = search
|
||||||
|
|
||||||
symbol, _, provider = sym.rpartition('.')
|
symbol, _, provider = sym.rpartition('.')
|
||||||
|
|
||||||
# this internally starts a ``chart_symbol()`` task above
|
# this internally starts a ``display_symbol_data()`` task above
|
||||||
chart_app.load_symbol(provider, symbol, loglevel)
|
godwidget.load_symbol(provider, symbol, loglevel)
|
||||||
|
|
||||||
# spin up a search engine for the local cached symbol set
|
# spin up a search engine for the local cached symbol set
|
||||||
async with _search.register_symbol_search(
|
async with _search.register_symbol_search(
|
||||||
|
@ -1740,7 +1782,7 @@ async def _async_main(
|
||||||
provider_name='cache',
|
provider_name='cache',
|
||||||
search_routine=partial(
|
search_routine=partial(
|
||||||
_search.search_simple_dict,
|
_search.search_simple_dict,
|
||||||
source=chart_app._chart_cache,
|
source=godwidget._chart_cache,
|
||||||
),
|
),
|
||||||
# cache is super fast so debounce on super short period
|
# cache is super fast so debounce on super short period
|
||||||
pause_period=0.01,
|
pause_period=0.01,
|
||||||
|
@ -1751,18 +1793,17 @@ async def _async_main(
|
||||||
root_n.start_soon(load_providers, brokernames, loglevel)
|
root_n.start_soon(load_providers, brokernames, loglevel)
|
||||||
|
|
||||||
# start handling search bar kb inputs
|
# start handling search bar kb inputs
|
||||||
async with open_key_stream(
|
async with (
|
||||||
search.bar,
|
|
||||||
) as key_stream:
|
|
||||||
|
|
||||||
# start kb handling task for searcher
|
_event.open_handler(
|
||||||
root_n.start_soon(
|
search.bar,
|
||||||
_search.handle_keyboard_input,
|
event_types={QEvent.KeyPress},
|
||||||
# chart_app,
|
async_handler=_search.handle_keyboard_input,
|
||||||
search,
|
# let key repeats pass through for search
|
||||||
key_stream,
|
filter_auto_repeats=False,
|
||||||
)
|
)
|
||||||
|
):
|
||||||
|
# remove startup status text
|
||||||
starting_done()
|
starting_done()
|
||||||
await trio.sleep_forever()
|
await trio.sleep_forever()
|
||||||
|
|
||||||
|
@ -1780,6 +1821,6 @@ def _main(
|
||||||
run_qtractor(
|
run_qtractor(
|
||||||
func=_async_main,
|
func=_async_main,
|
||||||
args=(sym, brokernames, piker_loglevel),
|
args=(sym, brokernames, piker_loglevel),
|
||||||
main_widget=ChartSpace,
|
main_widget=GodWidget,
|
||||||
tractor_kwargs=tractor_kwargs,
|
tractor_kwargs=tractor_kwargs,
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,14 +18,17 @@
|
||||||
Chart view box primitives
|
Chart view box primitives
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Optional, Dict
|
from typing import Optional, Dict
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from PyQt5.QtCore import QPointF
|
from PyQt5.QtCore import QPointF, Qt
|
||||||
|
from PyQt5.QtCore import QEvent
|
||||||
from pyqtgraph import ViewBox, Point, QtCore, QtGui
|
from pyqtgraph import ViewBox, Point, QtCore, QtGui
|
||||||
from pyqtgraph import functions as fn
|
from pyqtgraph import functions as fn
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import trio
|
||||||
|
|
||||||
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
|
||||||
|
@ -457,8 +460,28 @@ class ArrowEditor:
|
||||||
self.chart.plotItem.removeItem(arrow)
|
self.chart.plotItem.removeItem(arrow)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_viewmode_inputs(
|
||||||
|
|
||||||
|
view: 'ChartView',
|
||||||
|
recv_chan: trio.abc.ReceiveChannel,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
async for event, etype, key, mods, text in recv_chan:
|
||||||
|
log.debug(f'key: {key}, mods: {mods}, text: {text}')
|
||||||
|
|
||||||
|
if etype in {QEvent.KeyPress}:
|
||||||
|
|
||||||
|
await view.on_key_press(text, key, mods)
|
||||||
|
|
||||||
|
elif etype in {QEvent.KeyRelease}:
|
||||||
|
|
||||||
|
await view.on_key_release(text, key, mods)
|
||||||
|
|
||||||
|
|
||||||
class ChartView(ViewBox):
|
class ChartView(ViewBox):
|
||||||
"""Price chart view box with interaction behaviors you'd expect from
|
'''
|
||||||
|
Price chart view box with interaction behaviors you'd expect from
|
||||||
any interactive platform:
|
any interactive platform:
|
||||||
|
|
||||||
- zoom on mouse scroll that auto fits y-axis
|
- zoom on mouse scroll that auto fits y-axis
|
||||||
|
@ -466,23 +489,30 @@ class ChartView(ViewBox):
|
||||||
- zoom on x to most recent in view datum
|
- zoom on x to most recent in view datum
|
||||||
- zoom on right-click-n-drag to cursor position
|
- zoom on right-click-n-drag to cursor position
|
||||||
|
|
||||||
"""
|
'''
|
||||||
|
|
||||||
mode_name: str = 'mode: view'
|
mode_name: str = 'mode: view'
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
||||||
self,
|
self,
|
||||||
|
name: str,
|
||||||
parent: pg.PlotItem = None,
|
parent: pg.PlotItem = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
):
|
):
|
||||||
super().__init__(parent=parent, **kwargs)
|
super().__init__(parent=parent, **kwargs)
|
||||||
|
|
||||||
# disable vertical scrolling
|
# disable vertical scrolling
|
||||||
self.setMouseEnabled(x=True, y=False)
|
self.setMouseEnabled(x=True, y=False)
|
||||||
self.linked_charts = None
|
|
||||||
self.select_box = SelectRect(self)
|
self.linkedsplits = None
|
||||||
self.addItem(self.select_box, ignoreBounds=True)
|
|
||||||
self._chart: 'ChartPlotWidget' = None # noqa
|
self._chart: 'ChartPlotWidget' = None # noqa
|
||||||
|
|
||||||
|
# add our selection box annotator
|
||||||
|
self.select_box = SelectRect(self)
|
||||||
|
self.addItem(self.select_box, ignoreBounds=True)
|
||||||
|
|
||||||
|
self.name = name
|
||||||
self.mode = None
|
self.mode = None
|
||||||
|
|
||||||
# kb ctrls processing
|
# kb ctrls processing
|
||||||
|
@ -491,6 +521,19 @@ class ChartView(ViewBox):
|
||||||
|
|
||||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def open_async_input_handler(
|
||||||
|
self,
|
||||||
|
) -> 'ChartView':
|
||||||
|
from . import _event
|
||||||
|
|
||||||
|
async with _event.open_handler(
|
||||||
|
self,
|
||||||
|
event_types={QEvent.KeyPress, QEvent.KeyRelease},
|
||||||
|
async_handler=handle_viewmode_inputs,
|
||||||
|
):
|
||||||
|
yield self
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def chart(self) -> 'ChartPlotWidget': # type: ignore # noqa
|
def chart(self) -> 'ChartPlotWidget': # type: ignore # noqa
|
||||||
return self._chart
|
return self._chart
|
||||||
|
@ -501,21 +544,21 @@ class ChartView(ViewBox):
|
||||||
self.select_box.chart = chart
|
self.select_box.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.
|
||||||
|
|
||||||
This is an override of the ``ViewBox`` method simply changing
|
This is an override of the ``ViewBox`` method simply changing
|
||||||
the center of the zoom to be the y-axis.
|
the center of the zoom to be the y-axis.
|
||||||
|
|
||||||
TODO: PR a method into ``pyqtgraph`` to make this configurable
|
TODO: PR a method into ``pyqtgraph`` to make this configurable
|
||||||
"""
|
|
||||||
|
|
||||||
|
'''
|
||||||
if axis in (0, 1):
|
if axis in (0, 1):
|
||||||
mask = [False, False]
|
mask = [False, False]
|
||||||
mask[axis] = self.state['mouseEnabled'][axis]
|
mask[axis] = self.state['mouseEnabled'][axis]
|
||||||
else:
|
else:
|
||||||
mask = self.state['mouseEnabled'][:]
|
mask = self.state['mouseEnabled'][:]
|
||||||
|
|
||||||
chart = self.linked_charts.chart
|
chart = self.linkedsplits.chart
|
||||||
|
|
||||||
# don't zoom more then the min points setting
|
# don't zoom more then the min points setting
|
||||||
l, lbar, rbar, r = chart.bars_range()
|
l, lbar, rbar, r = chart.bars_range()
|
||||||
|
@ -573,7 +616,6 @@ class ChartView(ViewBox):
|
||||||
end_of_l1,
|
end_of_l1,
|
||||||
key=lambda p: p.x()
|
key=lambda p: p.x()
|
||||||
)
|
)
|
||||||
# breakpoint()
|
|
||||||
# focal = pg.Point(last_bar.x() + end_of_l1)
|
# focal = pg.Point(last_bar.x() + end_of_l1)
|
||||||
|
|
||||||
self._resetTarget()
|
self._resetTarget()
|
||||||
|
@ -697,22 +739,26 @@ class ChartView(ViewBox):
|
||||||
ev.accept()
|
ev.accept()
|
||||||
self.mode.submit_exec()
|
self.mode.submit_exec()
|
||||||
|
|
||||||
def keyReleaseEvent(self, ev: QtCore.QEvent):
|
# def keyReleaseEvent(self, ev: QtCore.QEvent):
|
||||||
|
def keyReleaseEvent(self, event: QtCore.QEvent) -> None:
|
||||||
|
'''This routine is rerouted to an async handler.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_key_release(
|
||||||
|
|
||||||
|
self,
|
||||||
|
|
||||||
|
text: str,
|
||||||
|
key: int, # 3-digit
|
||||||
|
mods: int, # 7-digit
|
||||||
|
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Key release to normally to trigger release of input mode
|
Key release to normally to trigger release of input mode
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# TODO: is there a global setting for this?
|
if key == Qt.Key_Shift:
|
||||||
if ev.isAutoRepeat():
|
|
||||||
ev.ignore()
|
|
||||||
return
|
|
||||||
|
|
||||||
ev.accept()
|
|
||||||
# text = ev.text()
|
|
||||||
key = ev.key()
|
|
||||||
mods = ev.modifiers()
|
|
||||||
|
|
||||||
if key == QtCore.Qt.Key_Shift:
|
|
||||||
# if self.state['mouseMode'] == ViewBox.RectMode:
|
# if self.state['mouseMode'] == ViewBox.RectMode:
|
||||||
self.setMouseMode(ViewBox.PanMode)
|
self.setMouseMode(ViewBox.PanMode)
|
||||||
|
|
||||||
|
@ -722,39 +768,37 @@ class ChartView(ViewBox):
|
||||||
|
|
||||||
# if self.state['mouseMode'] == ViewBox.RectMode:
|
# if self.state['mouseMode'] == ViewBox.RectMode:
|
||||||
# if key == QtCore.Qt.Key_Space:
|
# if key == QtCore.Qt.Key_Space:
|
||||||
if mods == QtCore.Qt.ControlModifier or key == QtCore.Qt.Key_Control:
|
if mods == Qt.ControlModifier or key == QtCore.Qt.Key_Control:
|
||||||
self.mode._exec_mode = 'dark'
|
self.mode._exec_mode = 'dark'
|
||||||
|
|
||||||
if key in {QtCore.Qt.Key_A, QtCore.Qt.Key_F, QtCore.Qt.Key_D}:
|
if key in {Qt.Key_A, Qt.Key_F, Qt.Key_D}:
|
||||||
# remove "staged" level line under cursor position
|
# remove "staged" level line under cursor position
|
||||||
self.mode.lines.unstage_line()
|
self.mode.lines.unstage_line()
|
||||||
|
|
||||||
self._key_active = False
|
self._key_active = False
|
||||||
|
|
||||||
def keyPressEvent(self, ev: QtCore.QEvent) -> None:
|
def keyPressEvent(self, event: QtCore.QEvent) -> None:
|
||||||
"""
|
'''This routine is rerouted to an async handler.
|
||||||
This routine should capture key presses in the current view box.
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
"""
|
async def on_key_press(
|
||||||
# TODO: is there a global setting for this?
|
|
||||||
if ev.isAutoRepeat():
|
|
||||||
ev.ignore()
|
|
||||||
return
|
|
||||||
|
|
||||||
ev.accept()
|
self,
|
||||||
text = ev.text()
|
|
||||||
key = ev.key()
|
|
||||||
mods = ev.modifiers()
|
|
||||||
|
|
||||||
print(f'text: {text}, key: {key}')
|
text: str,
|
||||||
|
key: int, # 3-digit
|
||||||
|
mods: int, # 7-digit
|
||||||
|
|
||||||
if mods == QtCore.Qt.ShiftModifier:
|
) -> None:
|
||||||
|
|
||||||
|
if mods == Qt.ShiftModifier:
|
||||||
if self.state['mouseMode'] == ViewBox.PanMode:
|
if self.state['mouseMode'] == ViewBox.PanMode:
|
||||||
self.setMouseMode(ViewBox.RectMode)
|
self.setMouseMode(ViewBox.RectMode)
|
||||||
|
|
||||||
# ctrl
|
# ctrl
|
||||||
ctrl = False
|
ctrl = False
|
||||||
if mods == QtCore.Qt.ControlModifier:
|
if mods == Qt.ControlModifier:
|
||||||
ctrl = True
|
ctrl = True
|
||||||
self.mode._exec_mode = 'live'
|
self.mode._exec_mode = 'live'
|
||||||
|
|
||||||
|
@ -767,20 +811,20 @@ class ChartView(ViewBox):
|
||||||
|
|
||||||
# ctlr-<space>/<l> for "lookup", "search" -> open search tree
|
# ctlr-<space>/<l> for "lookup", "search" -> open search tree
|
||||||
if ctrl and key in {
|
if ctrl and key in {
|
||||||
QtCore.Qt.Key_L,
|
Qt.Key_L,
|
||||||
QtCore.Qt.Key_Space,
|
Qt.Key_Space,
|
||||||
}:
|
}:
|
||||||
search = self._chart._lc.chart_space.search
|
search = self._chart._lc.godwidget.search
|
||||||
search.focus()
|
search.focus()
|
||||||
|
|
||||||
# esc
|
# esc
|
||||||
if key == QtCore.Qt.Key_Escape or (ctrl and key == QtCore.Qt.Key_C):
|
if key == Qt.Key_Escape or (ctrl and key == Qt.Key_C):
|
||||||
# ctrl-c as cancel
|
# ctrl-c as cancel
|
||||||
# https://forum.qt.io/topic/532/how-to-catch-ctrl-c-on-a-widget/9
|
# https://forum.qt.io/topic/532/how-to-catch-ctrl-c-on-a-widget/9
|
||||||
self.select_box.clear()
|
self.select_box.clear()
|
||||||
|
|
||||||
# cancel order or clear graphics
|
# cancel order or clear graphics
|
||||||
if key == QtCore.Qt.Key_C or key == QtCore.Qt.Key_Delete:
|
if key == Qt.Key_C or key == Qt.Key_Delete:
|
||||||
# delete any lines under the cursor
|
# delete any lines under the cursor
|
||||||
mode = self.mode
|
mode = self.mode
|
||||||
for line in mode.lines.lines_under_cursor():
|
for line in mode.lines.lines_under_cursor():
|
||||||
|
@ -789,18 +833,18 @@ class ChartView(ViewBox):
|
||||||
self._key_buffer.append(text)
|
self._key_buffer.append(text)
|
||||||
|
|
||||||
# View modes
|
# View modes
|
||||||
if key == QtCore.Qt.Key_R:
|
if key == Qt.Key_R:
|
||||||
self.chart.default_view()
|
self.chart.default_view()
|
||||||
|
|
||||||
# Order modes: stage orders at the current cursor level
|
# Order modes: stage orders at the current cursor level
|
||||||
|
|
||||||
elif key == QtCore.Qt.Key_D: # for "damp eet"
|
elif key == Qt.Key_D: # for "damp eet"
|
||||||
self.mode.set_exec('sell')
|
self.mode.set_exec('sell')
|
||||||
|
|
||||||
elif key == QtCore.Qt.Key_F: # for "fillz eet"
|
elif key == Qt.Key_F: # for "fillz eet"
|
||||||
self.mode.set_exec('buy')
|
self.mode.set_exec('buy')
|
||||||
|
|
||||||
elif key == QtCore.Qt.Key_A:
|
elif key == Qt.Key_A:
|
||||||
self.mode.set_exec('alert')
|
self.mode.set_exec('alert')
|
||||||
|
|
||||||
# XXX: Leaving this for light reference purposes, there
|
# XXX: Leaving this for light reference purposes, there
|
||||||
|
@ -808,7 +852,7 @@ class ChartView(ViewBox):
|
||||||
|
|
||||||
# Key presses are used only when mouse mode is RectMode
|
# Key presses are used only when mouse mode is RectMode
|
||||||
# The following events are implemented:
|
# The following events are implemented:
|
||||||
# ctrl-A : zooms out to the default "full" view of the plot
|
# ctrl-A : zooms out to the default "full" self of the plot
|
||||||
# ctrl-+ : moves forward in the zooming stack (if it exists)
|
# ctrl-+ : moves forward in the zooming stack (if it exists)
|
||||||
# ctrl-- : moves backward in the zooming stack (if it exists)
|
# ctrl-- : moves backward in the zooming stack (if it exists)
|
||||||
|
|
||||||
|
@ -819,5 +863,5 @@ class ChartView(ViewBox):
|
||||||
# self.scaleHistory(len(self.axHistory))
|
# self.scaleHistory(len(self.axHistory))
|
||||||
else:
|
else:
|
||||||
# maybe propagate to parent widget
|
# maybe propagate to parent widget
|
||||||
ev.ignore()
|
# event.ignore()
|
||||||
self._key_active = False
|
self._key_active = False
|
||||||
|
|
|
@ -447,7 +447,7 @@ class SearchBar(QtWidgets.QLineEdit):
|
||||||
|
|
||||||
self.view: CompleterView = view
|
self.view: CompleterView = view
|
||||||
self.dpi_font = font
|
self.dpi_font = font
|
||||||
self.chart_app = parent_chart
|
self.godwidget = parent_chart
|
||||||
|
|
||||||
# size it as we specify
|
# size it as we specify
|
||||||
# https://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum
|
# https://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum
|
||||||
|
@ -496,12 +496,12 @@ class SearchWidget(QtGui.QWidget):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
chart_space: 'ChartSpace', # type: ignore # noqa
|
godwidget: 'GodWidget', # type: ignore # noqa
|
||||||
columns: List[str] = ['src', 'symbol'],
|
columns: List[str] = ['src', 'symbol'],
|
||||||
parent=None,
|
parent=None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent or chart_space)
|
super().__init__(parent or godwidget)
|
||||||
|
|
||||||
# size it as we specify
|
# size it as we specify
|
||||||
self.setSizePolicy(
|
self.setSizePolicy(
|
||||||
|
@ -509,7 +509,7 @@ class SearchWidget(QtGui.QWidget):
|
||||||
QtWidgets.QSizePolicy.Fixed,
|
QtWidgets.QSizePolicy.Fixed,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.chart_app = chart_space
|
self.godwidget = godwidget
|
||||||
|
|
||||||
self.vbox = QtGui.QVBoxLayout(self)
|
self.vbox = QtGui.QVBoxLayout(self)
|
||||||
self.vbox.setContentsMargins(0, 0, 0, 0)
|
self.vbox.setContentsMargins(0, 0, 0, 0)
|
||||||
|
@ -540,7 +540,7 @@ class SearchWidget(QtGui.QWidget):
|
||||||
)
|
)
|
||||||
self.bar = SearchBar(
|
self.bar = SearchBar(
|
||||||
parent=self,
|
parent=self,
|
||||||
parent_chart=chart_space,
|
parent_chart=godwidget,
|
||||||
view=self.view,
|
view=self.view,
|
||||||
)
|
)
|
||||||
self.bar_hbox.addWidget(self.bar)
|
self.bar_hbox.addWidget(self.bar)
|
||||||
|
@ -557,7 +557,7 @@ class SearchWidget(QtGui.QWidget):
|
||||||
# fill cache list if nothing existing
|
# fill cache list if nothing existing
|
||||||
self.view.set_section_entries(
|
self.view.set_section_entries(
|
||||||
'cache',
|
'cache',
|
||||||
list(reversed(self.chart_app._chart_cache)),
|
list(reversed(self.godwidget._chart_cache)),
|
||||||
clear_all=True,
|
clear_all=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -611,7 +611,7 @@ class SearchWidget(QtGui.QWidget):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
provider, symbol = value
|
provider, symbol = value
|
||||||
chart = self.chart_app
|
chart = self.godwidget
|
||||||
|
|
||||||
log.info(f'Requesting symbol: {symbol}.{provider}')
|
log.info(f'Requesting symbol: {symbol}.{provider}')
|
||||||
|
|
||||||
|
@ -632,7 +632,7 @@ class SearchWidget(QtGui.QWidget):
|
||||||
# Re-order the symbol cache on the chart to display in
|
# Re-order the symbol cache on the chart to display in
|
||||||
# LIFO order. this is normally only done internally by
|
# LIFO order. this is normally only done internally by
|
||||||
# the chart on new symbols being loaded into memory
|
# the chart on new symbols being loaded into memory
|
||||||
chart.set_chart_symbol(fqsn, chart.linkedcharts)
|
chart.set_chart_symbol(fqsn, chart.linkedsplits)
|
||||||
|
|
||||||
self.view.set_section_entries(
|
self.view.set_section_entries(
|
||||||
'cache',
|
'cache',
|
||||||
|
@ -650,6 +650,7 @@ _search_enabled: bool = False
|
||||||
|
|
||||||
|
|
||||||
async def pack_matches(
|
async def pack_matches(
|
||||||
|
|
||||||
view: CompleterView,
|
view: CompleterView,
|
||||||
has_results: dict[str, set[str]],
|
has_results: dict[str, set[str]],
|
||||||
matches: dict[(str, str), [str]],
|
matches: dict[(str, str), [str]],
|
||||||
|
@ -823,7 +824,7 @@ async def fill_results(
|
||||||
|
|
||||||
async def handle_keyboard_input(
|
async def handle_keyboard_input(
|
||||||
|
|
||||||
search: SearchWidget,
|
searchbar: SearchBar,
|
||||||
recv_chan: trio.abc.ReceiveChannel,
|
recv_chan: trio.abc.ReceiveChannel,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -831,8 +832,9 @@ async def handle_keyboard_input(
|
||||||
global _search_active, _search_enabled
|
global _search_active, _search_enabled
|
||||||
|
|
||||||
# startup
|
# startup
|
||||||
chart = search.chart_app
|
bar = searchbar
|
||||||
bar = search.bar
|
search = searchbar.parent()
|
||||||
|
chart = search.godwidget
|
||||||
view = bar.view
|
view = bar.view
|
||||||
view.set_font_size(bar.dpi_font.px_size)
|
view.set_font_size(bar.dpi_font.px_size)
|
||||||
|
|
||||||
|
@ -851,7 +853,7 @@ async def handle_keyboard_input(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async for event, key, mods, txt in recv_chan:
|
async for event, etype, key, mods, txt in recv_chan:
|
||||||
|
|
||||||
log.debug(f'key: {key}, mods: {mods}, txt: {txt}')
|
log.debug(f'key: {key}, mods: {mods}, txt: {txt}')
|
||||||
|
|
||||||
|
@ -889,7 +891,7 @@ async def handle_keyboard_input(
|
||||||
|
|
||||||
# kill the search and focus back on main chart
|
# kill the search and focus back on main chart
|
||||||
if chart:
|
if chart:
|
||||||
chart.linkedcharts.focus()
|
chart.linkedsplits.focus()
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue