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!backup_asyncify_input_modes
parent
4759e79d3d
commit
08a21378b9
|
@ -26,6 +26,7 @@ from functools import partial
|
|||
|
||||
from PyQt5 import QtCore, QtGui
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtCore import QEvent
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
import tractor
|
||||
|
@ -60,7 +61,7 @@ from ._style import (
|
|||
_bars_to_left_in_follow_mode,
|
||||
)
|
||||
from . import _search
|
||||
from ._event import open_key_stream
|
||||
from . import _event
|
||||
from ..data._source import Symbol
|
||||
from ..data._sharedmem import ShmArray
|
||||
from ..data import maybe_open_shm_array
|
||||
|
@ -77,13 +78,23 @@ from ..data import feed
|
|||
log = get_logger(__name__)
|
||||
|
||||
|
||||
class ChartSpace(QtGui.QWidget):
|
||||
'''Highest level composed widget which contains layouts for
|
||||
class GodWidget(QtGui.QWidget):
|
||||
'''
|
||||
"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
|
||||
control or modify them.
|
||||
|
||||
'''
|
||||
def __init__(self, parent=None):
|
||||
def __init__(
|
||||
|
||||
self,
|
||||
parent=None,
|
||||
|
||||
) -> None:
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.hbox = QtGui.QHBoxLayout(self)
|
||||
|
@ -96,51 +107,54 @@ class ChartSpace(QtGui.QWidget):
|
|||
|
||||
self.hbox.addLayout(self.vbox)
|
||||
|
||||
self.toolbar_layout = QtGui.QHBoxLayout()
|
||||
self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
|
||||
# self.toolbar_layout = QtGui.QHBoxLayout()
|
||||
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
|
||||
# self.vbox.addLayout(self.toolbar_layout)
|
||||
|
||||
# self.init_timeframes_ui()
|
||||
# self.init_strategy_ui()
|
||||
self.vbox.addLayout(self.toolbar_layout)
|
||||
# self.vbox.addLayout(self.hbox)
|
||||
|
||||
self._chart_cache = {}
|
||||
self.linkedcharts: 'LinkedSplitCharts' = None
|
||||
self._root_n: Optional[trio.Nursery] = None
|
||||
self.linkedsplits: 'LinkedSplits' = None
|
||||
|
||||
# assigned in the startup func `_async_main()`
|
||||
self._root_n: trio.Nursery = None
|
||||
self._task_stack: AsyncExitStack = None
|
||||
|
||||
def set_chart_symbol(
|
||||
self,
|
||||
symbol_key: str, # of form <fqsn>.<providername>
|
||||
linked_charts: 'LinkedSplitCharts', # type: ignore
|
||||
linkedsplits: 'LinkedSplits', # type: ignore
|
||||
|
||||
) -> None:
|
||||
# re-sort org cache symbol list in LIFO order
|
||||
cache = self._chart_cache
|
||||
cache.pop(symbol_key, None)
|
||||
cache[symbol_key] = linked_charts
|
||||
cache[symbol_key] = linkedsplits
|
||||
|
||||
def get_chart_symbol(
|
||||
self,
|
||||
symbol_key: str,
|
||||
) -> 'LinkedSplitCharts': # type: ignore
|
||||
) -> 'LinkedSplits': # type: ignore
|
||||
return self._chart_cache.get(symbol_key)
|
||||
|
||||
def init_timeframes_ui(self):
|
||||
self.tf_layout = QtGui.QHBoxLayout()
|
||||
self.tf_layout.setSpacing(0)
|
||||
self.tf_layout.setContentsMargins(0, 12, 0, 0)
|
||||
time_frames = ('1M', '5M', '15M', '30M', '1H', '1D', '1W', 'MN')
|
||||
btn_prefix = 'TF'
|
||||
# def init_timeframes_ui(self):
|
||||
# self.tf_layout = QtGui.QHBoxLayout()
|
||||
# self.tf_layout.setSpacing(0)
|
||||
# self.tf_layout.setContentsMargins(0, 12, 0, 0)
|
||||
# time_frames = ('1M', '5M', '15M', '30M', '1H', '1D', '1W', 'MN')
|
||||
# btn_prefix = 'TF'
|
||||
|
||||
for tf in time_frames:
|
||||
btn_name = ''.join([btn_prefix, tf])
|
||||
btn = QtGui.QPushButton(tf)
|
||||
# TODO:
|
||||
btn.setEnabled(False)
|
||||
setattr(self, btn_name, btn)
|
||||
self.tf_layout.addWidget(btn)
|
||||
# for tf in time_frames:
|
||||
# btn_name = ''.join([btn_prefix, tf])
|
||||
# btn = QtGui.QPushButton(tf)
|
||||
# # TODO:
|
||||
# btn.setEnabled(False)
|
||||
# setattr(self, btn_name, 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.
|
||||
# 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?)
|
||||
fqsn = '.'.join([symbol_key, providername])
|
||||
|
||||
linkedcharts = self.get_chart_symbol(fqsn)
|
||||
linkedsplits = self.get_chart_symbol(fqsn)
|
||||
|
||||
if not self.vbox.isEmpty():
|
||||
# XXX: this is CRITICAL especially with pixel buffer caching
|
||||
self.linkedcharts.hide()
|
||||
self.linkedsplits.hide()
|
||||
|
||||
# XXX: pretty sure we don't need this
|
||||
# remove any existing plots?
|
||||
# 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
|
||||
if linkedcharts is None or reset:
|
||||
if linkedsplits is None or reset:
|
||||
|
||||
# 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
|
||||
self._root_n.start_soon(
|
||||
chart_symbol,
|
||||
display_symbol_data,
|
||||
self,
|
||||
providername,
|
||||
symbol_key,
|
||||
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
|
||||
if self.linkedcharts:
|
||||
self.linkedcharts.unfocus()
|
||||
if self.linkedsplits:
|
||||
self.linkedsplits.unfocus()
|
||||
|
||||
# self.vbox.addWidget(linkedcharts)
|
||||
linkedcharts.show()
|
||||
linkedcharts.focus()
|
||||
self.linkedcharts = linkedcharts
|
||||
# self.vbox.addWidget(linkedsplits)
|
||||
linkedsplits.show()
|
||||
linkedsplits.focus()
|
||||
self.linkedsplits = linkedsplits
|
||||
|
||||
symbol = linkedcharts.symbol
|
||||
symbol = linkedsplits.symbol
|
||||
|
||||
if symbol is not None:
|
||||
self.window.setWindowTitle(
|
||||
|
@ -214,14 +228,16 @@ class ChartSpace(QtGui.QWidget):
|
|||
)
|
||||
|
||||
|
||||
class LinkedSplitCharts(QtGui.QWidget):
|
||||
"""Widget that holds a central chart plus derived
|
||||
class LinkedSplits(QtGui.QWidget):
|
||||
'''
|
||||
Widget that holds a central chart plus derived
|
||||
subcharts computed from the original data set apart
|
||||
by splitters for resizing.
|
||||
|
||||
A single internal references to the data is maintained
|
||||
for each chart and can be updated externally.
|
||||
"""
|
||||
|
||||
'''
|
||||
long_pen = pg.mkPen('#006000')
|
||||
long_brush = pg.mkBrush('#00ff00')
|
||||
short_pen = pg.mkPen('#600000')
|
||||
|
@ -232,21 +248,24 @@ class LinkedSplitCharts(QtGui.QWidget):
|
|||
def __init__(
|
||||
|
||||
self,
|
||||
chart_space: ChartSpace,
|
||||
godwidget: GodWidget,
|
||||
|
||||
) -> None:
|
||||
|
||||
super().__init__()
|
||||
self.signals_visible: bool = False
|
||||
|
||||
# self.signals_visible: bool = False
|
||||
self._cursor: Cursor = None # crosshair graphics
|
||||
|
||||
self.godwidget = godwidget
|
||||
self.chart: ChartPlotWidget = None # main (ohlc) chart
|
||||
self.subplots: Dict[Tuple[str, ...], ChartPlotWidget] = {}
|
||||
self.chart_space = chart_space
|
||||
|
||||
self.chart_space = chart_space
|
||||
self.godwidget = godwidget
|
||||
|
||||
self.xaxis = DynamicDateAxis(
|
||||
orientation='bottom',
|
||||
linked_charts=self
|
||||
linkedsplits=self
|
||||
)
|
||||
# if _xaxis_at == 'bottom':
|
||||
# self.xaxis.setStyle(showValues=False)
|
||||
|
@ -302,7 +321,7 @@ class LinkedSplitCharts(QtGui.QWidget):
|
|||
"""
|
||||
# add crosshairs
|
||||
self._cursor = Cursor(
|
||||
linkedsplitcharts=self,
|
||||
linkedsplits=self,
|
||||
digits=symbol.digits(),
|
||||
)
|
||||
self.chart = self.add_plot(
|
||||
|
@ -342,14 +361,14 @@ class LinkedSplitCharts(QtGui.QWidget):
|
|||
"A main plot must be created first with `.plot_ohlc_main()`")
|
||||
|
||||
# source of our custom interactions
|
||||
cv = ChartView()
|
||||
cv.linked_charts = self
|
||||
cv = ChartView(name)
|
||||
cv.linkedsplits = self
|
||||
|
||||
# use "indicator axis" by default
|
||||
if xaxis is None:
|
||||
xaxis = DynamicDateAxis(
|
||||
orientation='bottom',
|
||||
linked_charts=self
|
||||
linkedsplits=self
|
||||
)
|
||||
|
||||
cpw = ChartPlotWidget(
|
||||
|
@ -360,11 +379,11 @@ class LinkedSplitCharts(QtGui.QWidget):
|
|||
|
||||
array=array,
|
||||
parent=self.splitter,
|
||||
linked_charts=self,
|
||||
linkedsplits=self,
|
||||
axisItems={
|
||||
'bottom': xaxis,
|
||||
'right': PriceAxis(linked_charts=self, orientation='right'),
|
||||
'left': PriceAxis(linked_charts=self, orientation='left'),
|
||||
'right': PriceAxis(linkedsplits=self, orientation='right'),
|
||||
'left': PriceAxis(linkedsplits=self, orientation='left'),
|
||||
},
|
||||
viewBox=cv,
|
||||
cursor=self._cursor,
|
||||
|
@ -377,7 +396,7 @@ class LinkedSplitCharts(QtGui.QWidget):
|
|||
# (see our custom view mode in `._interactions.py`)
|
||||
cv.chart = cpw
|
||||
|
||||
cpw.plotItem.vb.linked_charts = self
|
||||
cpw.plotItem.vb.linkedsplits = self
|
||||
cpw.setFrameStyle(QtGui.QFrame.StyledPanel) # | QtGui.QFrame.Plain)
|
||||
cpw.hideButtons()
|
||||
# XXX: gives us outline on backside of y-axis
|
||||
|
@ -415,7 +434,8 @@ class LinkedSplitCharts(QtGui.QWidget):
|
|||
|
||||
|
||||
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
|
||||
``np.ndarray``s with appropriate field names.
|
||||
|
@ -425,7 +445,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
(Could be replaced with a ``pg.GraphicsLayoutWidget`` if we
|
||||
eventually want multiple plots managed together?)
|
||||
"""
|
||||
|
||||
'''
|
||||
sig_mouse_leave = QtCore.Signal(object)
|
||||
sig_mouse_enter = QtCore.Signal(object)
|
||||
|
||||
|
@ -441,7 +462,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# the data view we generate graphics from
|
||||
name: str,
|
||||
array: np.ndarray,
|
||||
linked_charts: LinkedSplitCharts,
|
||||
linkedsplits: LinkedSplits,
|
||||
|
||||
view_color: str = 'papas_special',
|
||||
pen_color: str = 'bracket',
|
||||
|
@ -465,7 +486,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
**kwargs
|
||||
)
|
||||
self.name = name
|
||||
self._lc = linked_charts
|
||||
self._lc = linkedsplits
|
||||
|
||||
# scene-local placeholder for book graphics
|
||||
# sizing to avoid overlap with data contents
|
||||
|
@ -1006,10 +1027,12 @@ _book_throttle_rate: int = 16 # Hz
|
|||
|
||||
|
||||
async def chart_from_quotes(
|
||||
|
||||
chart: ChartPlotWidget,
|
||||
stream,
|
||||
stream: tractor.MsgStream,
|
||||
ohlcv: np.ndarray,
|
||||
wap_in_history: bool = False,
|
||||
|
||||
) -> None:
|
||||
"""The 'main' (price) chart real-time update loop.
|
||||
|
||||
|
@ -1211,12 +1234,14 @@ async def chart_from_quotes(
|
|||
|
||||
|
||||
async def spawn_fsps(
|
||||
linked_charts: LinkedSplitCharts,
|
||||
|
||||
linkedsplits: LinkedSplits,
|
||||
fsps: Dict[str, str],
|
||||
sym,
|
||||
src_shm,
|
||||
brokermod,
|
||||
loglevel,
|
||||
|
||||
) -> None:
|
||||
"""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
|
||||
async with tractor.open_nursery(loglevel=loglevel) as n:
|
||||
|
@ -1271,7 +1296,7 @@ async def spawn_fsps(
|
|||
ln.start_soon(
|
||||
run_fsp,
|
||||
portal,
|
||||
linked_charts,
|
||||
linkedsplits,
|
||||
brokermod,
|
||||
sym,
|
||||
src_shm,
|
||||
|
@ -1286,7 +1311,7 @@ async def spawn_fsps(
|
|||
async def run_fsp(
|
||||
|
||||
portal: tractor._portal.Portal,
|
||||
linked_charts: LinkedSplitCharts,
|
||||
linkedsplits: LinkedSplits,
|
||||
brokermod: ModuleType,
|
||||
sym: str,
|
||||
src_shm: ShmArray,
|
||||
|
@ -1300,7 +1325,7 @@ async def run_fsp(
|
|||
This is called once for each entry in the fsp
|
||||
config map.
|
||||
"""
|
||||
done = linked_charts.window().status_bar.open_status(
|
||||
done = linkedsplits.window().status_bar.open_status(
|
||||
f'loading FSP: {display_name}..')
|
||||
|
||||
async with portal.open_stream_from(
|
||||
|
@ -1324,7 +1349,7 @@ async def run_fsp(
|
|||
shm = conf['shm']
|
||||
|
||||
if conf.get('overlay'):
|
||||
chart = linked_charts.chart
|
||||
chart = linkedsplits.chart
|
||||
chart.draw_curve(
|
||||
name='vwap',
|
||||
data=shm.array,
|
||||
|
@ -1334,7 +1359,7 @@ async def run_fsp(
|
|||
|
||||
else:
|
||||
|
||||
chart = linked_charts.add_plot(
|
||||
chart = linkedsplits.add_plot(
|
||||
name=fsp_func_name,
|
||||
array=shm.array,
|
||||
|
||||
|
@ -1358,7 +1383,7 @@ async def run_fsp(
|
|||
chart._shm = shm
|
||||
|
||||
# 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
|
||||
last_val_sticky = chart._ysticks[chart.name]
|
||||
|
@ -1441,7 +1466,7 @@ async def run_fsp(
|
|||
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
|
||||
``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
|
||||
# aware of the instrument's tradable hours?
|
||||
|
||||
price_chart = linked_charts.chart
|
||||
price_chart = linkedsplits.chart
|
||||
price_chart.default_view()
|
||||
|
||||
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]
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
# shift the view if in follow mode
|
||||
price_chart.increment_view()
|
||||
|
||||
|
||||
async def chart_symbol(
|
||||
chart_app: ChartSpace,
|
||||
async def display_symbol_data(
|
||||
|
||||
godwidget: GodWidget,
|
||||
provider: str,
|
||||
sym: str,
|
||||
loglevel: str,
|
||||
|
||||
) -> 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
|
||||
can be viewed and switched between extremely fast.
|
||||
Spawned ``LinkedSplits`` chart widgets can remain up but hidden so
|
||||
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}..')
|
||||
|
||||
# historical data fetch
|
||||
brokermod = brokers.get_brokermod(provider)
|
||||
|
||||
async with data.open_feed(
|
||||
async with(
|
||||
|
||||
provider,
|
||||
[sym],
|
||||
loglevel=loglevel,
|
||||
data.open_feed(
|
||||
provider,
|
||||
[sym],
|
||||
loglevel=loglevel,
|
||||
|
||||
# 60 FPS to limit context switches
|
||||
tick_throttle=_clear_throttle_rate,
|
||||
# 60 FPS to limit context switches
|
||||
tick_throttle=_clear_throttle_rate,
|
||||
|
||||
) as feed:
|
||||
) as feed,
|
||||
|
||||
trio.open_nursery() as n,
|
||||
):
|
||||
|
||||
ohlcv: ShmArray = feed.shm
|
||||
bars = ohlcv.array
|
||||
symbol = feed.symbols[sym]
|
||||
|
||||
# load in symbol's ohlc data
|
||||
chart_app.window.setWindowTitle(
|
||||
godwidget.window.setWindowTitle(
|
||||
f'{symbol.key}@{symbol.brokers} '
|
||||
f'tick:{symbol.tick_size}'
|
||||
)
|
||||
|
||||
linked_charts = chart_app.linkedcharts
|
||||
linked_charts._symbol = symbol
|
||||
chart = linked_charts.plot_ohlc_main(symbol, bars)
|
||||
linkedsplits = godwidget.linkedsplits
|
||||
linkedsplits._symbol = symbol
|
||||
|
||||
chart = linkedsplits.plot_ohlc_main(symbol, bars)
|
||||
chart.setFocus()
|
||||
|
||||
# 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")
|
||||
n.start_soon(
|
||||
spawn_fsps,
|
||||
linked_charts,
|
||||
fsp_conf,
|
||||
sym,
|
||||
ohlcv,
|
||||
brokermod,
|
||||
loglevel,
|
||||
)
|
||||
# load initial fsp chain (otherwise known as "indicators")
|
||||
n.start_soon(
|
||||
spawn_fsps,
|
||||
linkedsplits,
|
||||
fsp_conf,
|
||||
sym,
|
||||
ohlcv,
|
||||
brokermod,
|
||||
loglevel,
|
||||
)
|
||||
|
||||
# start graphics update loop(s)after receiving first live quote
|
||||
n.start_soon(
|
||||
chart_from_quotes,
|
||||
chart,
|
||||
feed.stream,
|
||||
ohlcv,
|
||||
wap_in_history,
|
||||
)
|
||||
# start graphics update loop(s)after receiving first live quote
|
||||
n.start_soon(
|
||||
chart_from_quotes,
|
||||
chart,
|
||||
feed.stream,
|
||||
ohlcv,
|
||||
wap_in_history,
|
||||
)
|
||||
|
||||
# wait for a first quote before we start any update tasks
|
||||
# quote = await feed.receive()
|
||||
# log.info(f'Received first quote {quote}')
|
||||
# wait for a first quote before we start any update tasks
|
||||
# quote = await feed.receive()
|
||||
# log.info(f'Received first quote {quote}')
|
||||
|
||||
n.start_soon(
|
||||
check_for_new_bars,
|
||||
feed,
|
||||
# delay,
|
||||
ohlcv,
|
||||
linked_charts
|
||||
)
|
||||
n.start_soon(
|
||||
check_for_new_bars,
|
||||
feed,
|
||||
# delay,
|
||||
ohlcv,
|
||||
linkedsplits
|
||||
)
|
||||
|
||||
# interactive testing
|
||||
# n.start_soon(
|
||||
# test_bed,
|
||||
# ohlcv,
|
||||
# chart,
|
||||
# linked_charts,
|
||||
# )
|
||||
# interactive testing
|
||||
# n.start_soon(
|
||||
# test_bed,
|
||||
# ohlcv,
|
||||
# chart,
|
||||
# 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)
|
||||
|
||||
|
||||
async def load_providers(
|
||||
|
||||
brokernames: list[str],
|
||||
loglevel: str,
|
||||
|
||||
) -> None:
|
||||
|
||||
# TODO: seems like our incentive for brokerd caching lelel
|
||||
|
@ -1673,8 +1711,9 @@ async def load_providers(
|
|||
|
||||
|
||||
async def _async_main(
|
||||
|
||||
# implicit required argument provided by ``qtractor_run()``
|
||||
main_widget: ChartSpace,
|
||||
main_widget: GodWidget,
|
||||
|
||||
sym: 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
|
||||
screen = chart_app.window.current_screen()
|
||||
screen = godwidget.window.current_screen()
|
||||
|
||||
# configure graphics update throttling based on display refresh rate
|
||||
global _clear_throttle_rate
|
||||
|
||||
_clear_throttle_rate = min(
|
||||
round(screen.refreshRate()),
|
||||
_clear_throttle_rate,
|
||||
|
@ -1702,37 +1742,39 @@ async def _async_main(
|
|||
log.info(f'Set graphics update rate to {_clear_throttle_rate} Hz')
|
||||
|
||||
# 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...')
|
||||
|
||||
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
|
||||
chart_app._root_n = root_n
|
||||
godwidget._root_n = root_n
|
||||
godwidget._task_stack = chart_task_stack
|
||||
|
||||
# setup search widget
|
||||
search = _search.SearchWidget(chart_space=chart_app)
|
||||
|
||||
# the main chart's view is given focus at startup
|
||||
# setup search widget and focus main chart view at startup
|
||||
search = _search.SearchWidget(godwidget=godwidget)
|
||||
search.bar.unfocus()
|
||||
|
||||
# add search singleton to global chart-space widget
|
||||
chart_app.hbox.addWidget(
|
||||
godwidget.hbox.addWidget(
|
||||
search,
|
||||
|
||||
# alights to top and uses minmial space based on
|
||||
# search bar size hint (i think?)
|
||||
alignment=Qt.AlignTop
|
||||
)
|
||||
chart_app.search = search
|
||||
godwidget.search = search
|
||||
|
||||
symbol, _, provider = sym.rpartition('.')
|
||||
|
||||
# this internally starts a ``chart_symbol()`` task above
|
||||
chart_app.load_symbol(provider, symbol, loglevel)
|
||||
# this internally starts a ``display_symbol_data()`` task above
|
||||
godwidget.load_symbol(provider, symbol, loglevel)
|
||||
|
||||
# spin up a search engine for the local cached symbol set
|
||||
async with _search.register_symbol_search(
|
||||
|
@ -1740,7 +1782,7 @@ async def _async_main(
|
|||
provider_name='cache',
|
||||
search_routine=partial(
|
||||
_search.search_simple_dict,
|
||||
source=chart_app._chart_cache,
|
||||
source=godwidget._chart_cache,
|
||||
),
|
||||
# cache is super fast so debounce on super short period
|
||||
pause_period=0.01,
|
||||
|
@ -1751,18 +1793,17 @@ async def _async_main(
|
|||
root_n.start_soon(load_providers, brokernames, loglevel)
|
||||
|
||||
# start handling search bar kb inputs
|
||||
async with open_key_stream(
|
||||
search.bar,
|
||||
) as key_stream:
|
||||
async with (
|
||||
|
||||
# start kb handling task for searcher
|
||||
root_n.start_soon(
|
||||
_search.handle_keyboard_input,
|
||||
# chart_app,
|
||||
search,
|
||||
key_stream,
|
||||
_event.open_handler(
|
||||
search.bar,
|
||||
event_types={QEvent.KeyPress},
|
||||
async_handler=_search.handle_keyboard_input,
|
||||
# let key repeats pass through for search
|
||||
filter_auto_repeats=False,
|
||||
)
|
||||
|
||||
):
|
||||
# remove startup status text
|
||||
starting_done()
|
||||
await trio.sleep_forever()
|
||||
|
||||
|
@ -1780,6 +1821,6 @@ def _main(
|
|||
run_qtractor(
|
||||
func=_async_main,
|
||||
args=(sym, brokernames, piker_loglevel),
|
||||
main_widget=ChartSpace,
|
||||
main_widget=GodWidget,
|
||||
tractor_kwargs=tractor_kwargs,
|
||||
)
|
||||
|
|
|
@ -18,14 +18,17 @@
|
|||
Chart view box primitives
|
||||
|
||||
"""
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Dict
|
||||
|
||||
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 functions as fn
|
||||
import numpy as np
|
||||
import trio
|
||||
|
||||
from ..log import get_logger
|
||||
from ._style import _min_points_to_show, hcolor, _font
|
||||
|
@ -457,8 +460,28 @@ class ArrowEditor:
|
|||
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):
|
||||
"""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:
|
||||
|
||||
- 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 right-click-n-drag to cursor position
|
||||
|
||||
"""
|
||||
|
||||
'''
|
||||
mode_name: str = 'mode: view'
|
||||
|
||||
def __init__(
|
||||
|
||||
self,
|
||||
name: str,
|
||||
parent: pg.PlotItem = None,
|
||||
**kwargs,
|
||||
|
||||
):
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
|
||||
# disable vertical scrolling
|
||||
self.setMouseEnabled(x=True, y=False)
|
||||
self.linked_charts = None
|
||||
self.select_box = SelectRect(self)
|
||||
self.addItem(self.select_box, ignoreBounds=True)
|
||||
|
||||
self.linkedsplits = None
|
||||
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
|
||||
|
||||
# kb ctrls processing
|
||||
|
@ -491,6 +521,19 @@ class ChartView(ViewBox):
|
|||
|
||||
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
|
||||
def chart(self) -> 'ChartPlotWidget': # type: ignore # noqa
|
||||
return self._chart
|
||||
|
@ -501,21 +544,21 @@ class ChartView(ViewBox):
|
|||
self.select_box.chart = chart
|
||||
|
||||
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
|
||||
the center of the zoom to be the y-axis.
|
||||
|
||||
TODO: PR a method into ``pyqtgraph`` to make this configurable
|
||||
"""
|
||||
|
||||
'''
|
||||
if axis in (0, 1):
|
||||
mask = [False, False]
|
||||
mask[axis] = self.state['mouseEnabled'][axis]
|
||||
else:
|
||||
mask = self.state['mouseEnabled'][:]
|
||||
|
||||
chart = self.linked_charts.chart
|
||||
chart = self.linkedsplits.chart
|
||||
|
||||
# don't zoom more then the min points setting
|
||||
l, lbar, rbar, r = chart.bars_range()
|
||||
|
@ -573,7 +616,6 @@ class ChartView(ViewBox):
|
|||
end_of_l1,
|
||||
key=lambda p: p.x()
|
||||
)
|
||||
# breakpoint()
|
||||
# focal = pg.Point(last_bar.x() + end_of_l1)
|
||||
|
||||
self._resetTarget()
|
||||
|
@ -697,22 +739,26 @@ class ChartView(ViewBox):
|
|||
ev.accept()
|
||||
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
|
||||
|
||||
"""
|
||||
# TODO: is there a global setting for this?
|
||||
if ev.isAutoRepeat():
|
||||
ev.ignore()
|
||||
return
|
||||
|
||||
ev.accept()
|
||||
# text = ev.text()
|
||||
key = ev.key()
|
||||
mods = ev.modifiers()
|
||||
|
||||
if key == QtCore.Qt.Key_Shift:
|
||||
if key == Qt.Key_Shift:
|
||||
# if self.state['mouseMode'] == ViewBox.RectMode:
|
||||
self.setMouseMode(ViewBox.PanMode)
|
||||
|
||||
|
@ -722,39 +768,37 @@ class ChartView(ViewBox):
|
|||
|
||||
# if self.state['mouseMode'] == ViewBox.RectMode:
|
||||
# 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'
|
||||
|
||||
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
|
||||
self.mode.lines.unstage_line()
|
||||
|
||||
self._key_active = False
|
||||
|
||||
def keyPressEvent(self, ev: QtCore.QEvent) -> None:
|
||||
"""
|
||||
This routine should capture key presses in the current view box.
|
||||
def keyPressEvent(self, event: QtCore.QEvent) -> None:
|
||||
'''This routine is rerouted to an async handler.
|
||||
'''
|
||||
pass
|
||||
|
||||
"""
|
||||
# TODO: is there a global setting for this?
|
||||
if ev.isAutoRepeat():
|
||||
ev.ignore()
|
||||
return
|
||||
async def on_key_press(
|
||||
|
||||
ev.accept()
|
||||
text = ev.text()
|
||||
key = ev.key()
|
||||
mods = ev.modifiers()
|
||||
self,
|
||||
|
||||
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:
|
||||
self.setMouseMode(ViewBox.RectMode)
|
||||
|
||||
# ctrl
|
||||
ctrl = False
|
||||
if mods == QtCore.Qt.ControlModifier:
|
||||
if mods == Qt.ControlModifier:
|
||||
ctrl = True
|
||||
self.mode._exec_mode = 'live'
|
||||
|
||||
|
@ -767,20 +811,20 @@ class ChartView(ViewBox):
|
|||
|
||||
# ctlr-<space>/<l> for "lookup", "search" -> open search tree
|
||||
if ctrl and key in {
|
||||
QtCore.Qt.Key_L,
|
||||
QtCore.Qt.Key_Space,
|
||||
Qt.Key_L,
|
||||
Qt.Key_Space,
|
||||
}:
|
||||
search = self._chart._lc.chart_space.search
|
||||
search = self._chart._lc.godwidget.search
|
||||
search.focus()
|
||||
|
||||
# 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
|
||||
# https://forum.qt.io/topic/532/how-to-catch-ctrl-c-on-a-widget/9
|
||||
self.select_box.clear()
|
||||
|
||||
# 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
|
||||
mode = self.mode
|
||||
for line in mode.lines.lines_under_cursor():
|
||||
|
@ -789,18 +833,18 @@ class ChartView(ViewBox):
|
|||
self._key_buffer.append(text)
|
||||
|
||||
# View modes
|
||||
if key == QtCore.Qt.Key_R:
|
||||
if key == Qt.Key_R:
|
||||
self.chart.default_view()
|
||||
|
||||
# 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')
|
||||
|
||||
elif key == QtCore.Qt.Key_F: # for "fillz eet"
|
||||
elif key == Qt.Key_F: # for "fillz eet"
|
||||
self.mode.set_exec('buy')
|
||||
|
||||
elif key == QtCore.Qt.Key_A:
|
||||
elif key == Qt.Key_A:
|
||||
self.mode.set_exec('alert')
|
||||
|
||||
# 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
|
||||
# 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 backward in the zooming stack (if it exists)
|
||||
|
||||
|
@ -819,5 +863,5 @@ class ChartView(ViewBox):
|
|||
# self.scaleHistory(len(self.axHistory))
|
||||
else:
|
||||
# maybe propagate to parent widget
|
||||
ev.ignore()
|
||||
# event.ignore()
|
||||
self._key_active = False
|
||||
|
|
|
@ -447,7 +447,7 @@ class SearchBar(QtWidgets.QLineEdit):
|
|||
|
||||
self.view: CompleterView = view
|
||||
self.dpi_font = font
|
||||
self.chart_app = parent_chart
|
||||
self.godwidget = parent_chart
|
||||
|
||||
# size it as we specify
|
||||
# https://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum
|
||||
|
@ -496,12 +496,12 @@ class SearchWidget(QtGui.QWidget):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
chart_space: 'ChartSpace', # type: ignore # noqa
|
||||
godwidget: 'GodWidget', # type: ignore # noqa
|
||||
columns: List[str] = ['src', 'symbol'],
|
||||
parent=None,
|
||||
|
||||
) -> None:
|
||||
super().__init__(parent or chart_space)
|
||||
super().__init__(parent or godwidget)
|
||||
|
||||
# size it as we specify
|
||||
self.setSizePolicy(
|
||||
|
@ -509,7 +509,7 @@ class SearchWidget(QtGui.QWidget):
|
|||
QtWidgets.QSizePolicy.Fixed,
|
||||
)
|
||||
|
||||
self.chart_app = chart_space
|
||||
self.godwidget = godwidget
|
||||
|
||||
self.vbox = QtGui.QVBoxLayout(self)
|
||||
self.vbox.setContentsMargins(0, 0, 0, 0)
|
||||
|
@ -540,7 +540,7 @@ class SearchWidget(QtGui.QWidget):
|
|||
)
|
||||
self.bar = SearchBar(
|
||||
parent=self,
|
||||
parent_chart=chart_space,
|
||||
parent_chart=godwidget,
|
||||
view=self.view,
|
||||
)
|
||||
self.bar_hbox.addWidget(self.bar)
|
||||
|
@ -557,7 +557,7 @@ class SearchWidget(QtGui.QWidget):
|
|||
# fill cache list if nothing existing
|
||||
self.view.set_section_entries(
|
||||
'cache',
|
||||
list(reversed(self.chart_app._chart_cache)),
|
||||
list(reversed(self.godwidget._chart_cache)),
|
||||
clear_all=True,
|
||||
)
|
||||
|
||||
|
@ -611,7 +611,7 @@ class SearchWidget(QtGui.QWidget):
|
|||
return None
|
||||
|
||||
provider, symbol = value
|
||||
chart = self.chart_app
|
||||
chart = self.godwidget
|
||||
|
||||
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
|
||||
# LIFO order. this is normally only done internally by
|
||||
# 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(
|
||||
'cache',
|
||||
|
@ -650,6 +650,7 @@ _search_enabled: bool = False
|
|||
|
||||
|
||||
async def pack_matches(
|
||||
|
||||
view: CompleterView,
|
||||
has_results: dict[str, set[str]],
|
||||
matches: dict[(str, str), [str]],
|
||||
|
@ -823,7 +824,7 @@ async def fill_results(
|
|||
|
||||
async def handle_keyboard_input(
|
||||
|
||||
search: SearchWidget,
|
||||
searchbar: SearchBar,
|
||||
recv_chan: trio.abc.ReceiveChannel,
|
||||
|
||||
) -> None:
|
||||
|
@ -831,8 +832,9 @@ async def handle_keyboard_input(
|
|||
global _search_active, _search_enabled
|
||||
|
||||
# startup
|
||||
chart = search.chart_app
|
||||
bar = search.bar
|
||||
bar = searchbar
|
||||
search = searchbar.parent()
|
||||
chart = search.godwidget
|
||||
view = bar.view
|
||||
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}')
|
||||
|
||||
|
@ -889,7 +891,7 @@ async def handle_keyboard_input(
|
|||
|
||||
# kill the search and focus back on main chart
|
||||
if chart:
|
||||
chart.linkedcharts.focus()
|
||||
chart.linkedsplits.focus()
|
||||
|
||||
continue
|
||||
|
||||
|
|
Loading…
Reference in New Issue