First draft: symbol switching via QLineEdit widget

symbol_search
Tyler Goodlet 2021-04-12 18:04:26 -04:00
parent 27aed85404
commit 0627f7dcee
2 changed files with 189 additions and 66 deletions

View File

@ -24,6 +24,8 @@ from types import ModuleType
from functools import partial
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import Qt
from PyQt5 import QtWidgets
import numpy as np
import pyqtgraph as pg
import tractor
@ -48,6 +50,7 @@ from ._graphics._ohlc import BarItems
from ._graphics._curve import FastAppendCurve
from ._style import (
_font,
DpiAwareFont,
hcolor,
CHART_MARGINS,
_xaxis_at,
@ -70,6 +73,99 @@ from .. import fsp
log = get_logger(__name__)
class FontSizedQLineEdit(QtWidgets.QLineEdit):
def __init__(
self,
parent_chart: 'ChartSpace',
font: DpiAwareFont = _font,
) -> None:
super().__init__(parent_chart)
self.dpi_font = font
self.chart_app = parent_chart
# size it as we specify
self.setSizePolicy(
QtWidgets.QSizePolicy.Fixed,
QtWidgets.QSizePolicy.Fixed,
)
self.setFont(font.font)
# witty bit of margin
self.setTextMargins(2, 2, 2, 2)
def sizeHint(self) -> QtCore.QSize:
psh = super().sizeHint()
psh.setHeight(self.dpi_font.px_size + 2)
return psh
def unfocus(self) -> None:
self.hide()
self.clearFocus()
def keyPressEvent(self, ev: QtCore.QEvent) -> None:
# by default we don't markt it as consumed?
# ev.ignore()
super().keyPressEvent(ev)
ev.accept()
# text = ev.text()
key = ev.key()
mods = ev.modifiers()
ctrl = False
if mods == QtCore.Qt.ControlModifier:
ctrl = True
if ctrl:
if key == QtCore.Qt.Key_C:
self.unfocus()
self.chart_app.linkedcharts.focus()
# TODO:
elif key == QtCore.Qt.Key_K:
print('move up')
elif key == QtCore.Qt.Key_J:
print('move down')
elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
print(f'Requesting symbol: {self.text()}')
symbol = self.text()
app = self.chart_app
self.chart_app.load_symbol(
app.linkedcharts.symbol.brokers[0],
symbol,
'info',
)
# self.hide()
self.unfocus()
# if self._executing():
# # ignore all key presses while executing, except for Ctrl-C
# if event.modifiers() == Qt.ControlModifier and key == Qt.Key_C:
# self._handle_ctrl_c()
# return True
# handler = self._key_event_handlers.get(key)
# intercepted = handler and handler(event)
# Assumes that Control+Key is a movement command, i.e. should not be
# handled as text insertion. However, on win10 AltGr is reported as
# Alt+Control which is why we handle this case like regular
# # keypresses, see #53:
# if not event.modifiers() & Qt.ControlModifier or \
# event.modifiers() & Qt.AltModifier:
# self._keep_cursor_in_buffer()
# if not intercepted and event.text():
# intercepted = True
# self.insert_input_text(event.text())
# return False
class ChartSpace(QtGui.QWidget):
"""High level widget which contains layouts for organizing
lower level charts as well as other widgets used to control
@ -80,7 +176,7 @@ class ChartSpace(QtGui.QWidget):
self.v_layout = QtGui.QVBoxLayout(self)
self.v_layout.setContentsMargins(0, 0, 0, 0)
self.v_layout.setSpacing(0)
self.v_layout.setSpacing(1)
self.toolbar_layout = QtGui.QHBoxLayout()
self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
@ -93,21 +189,18 @@ class ChartSpace(QtGui.QWidget):
self.v_layout.addLayout(self.toolbar_layout)
self.v_layout.addLayout(self.h_layout)
self._chart_cache = {}
self.linkedcharts: 'LinkedSplitCharts' = None
self.symbol_label: Optional[QtGui.QLabel] = None
def init_search(self):
self.symbol_label = label = QtGui.QLabel()
label.setTextFormat(3) # markdown
label.setFont(_font.font)
label.setMargin(0)
# title = f'sym: {self.symbol}'
# label.setText(title)
self._root_n: Optional[trio.Nursery] = None
label.setAlignment(
QtCore.Qt.AlignVCenter
| QtCore.Qt.AlignLeft
)
self.v_layout.addWidget(label)
def open_search(self):
# search = self.search = QtWidgets.QLineEdit()
self.search = FontSizedQLineEdit(self)
self.search.unfocus()
self.v_layout.addWidget(self.search)
# search.installEventFilter(self)
def init_timeframes_ui(self):
self.tf_layout = QtGui.QHBoxLayout()
@ -130,38 +223,59 @@ class ChartSpace(QtGui.QWidget):
# def init_strategy_ui(self):
# self.strategy_box = StrategyBoxWidget(self)
# self.toolbar_layout.addWidget(self.strategy_box)
def load_symbol(
self,
brokername: str,
symbol_key: str,
data: np.ndarray,
loglevel: str,
ohlc: bool = True,
reset: bool = False,
) -> None:
"""Load a new contract into the charting app.
Expects a ``numpy`` structured array containing all the ohlcv fields.
"""
# TODO: symbol search
# # of course this doesn't work :eyeroll:
# h = _font.boundingRect('Ag').height()
# print(f'HEIGHT {h}')
# self.symbol_label.setFixedHeight(h + 4)
# self.v_layout.update()
# self.symbol_label.setText(f'/`{symbol}`')
linkedcharts = self._chart_cache.setdefault(
symbol_key,
LinkedSplitCharts(self)
)
self.linkedcharts = linkedcharts
"""
linkedcharts = self._chart_cache.get(symbol_key)
# switching to a new viewable chart
if not self.v_layout.isEmpty():
# and not (
# self.linkedcharts is linkedcharts
# ):
# XXX: this is CRITICAL especially with pixel buffer caching
self.linkedcharts.hide()
# remove any existing plots
if not self.v_layout.isEmpty():
self.v_layout.removeWidget(linkedcharts)
self.v_layout.removeWidget(self.linkedcharts)
if linkedcharts is None or reset:
# we must load a fresh linked charts set
linkedcharts = LinkedSplitCharts(self)
self._root_n.start_soon(
chart_symbol,
self,
brokername,
symbol_key,
loglevel,
)
self.v_layout.addWidget(linkedcharts)
return linkedcharts
# if linkedcharts.chart:
# breakpoint()
# else:
# chart is already in memory so just focus it
if self.linkedcharts:
self.linkedcharts.unfocus()
# self.v_layout.addWidget(linkedcharts)
self.linkedcharts = linkedcharts
linkedcharts.focus()
# return linkedcharts
# TODO: add signalling painter system
# def add_signals(self):
@ -184,8 +298,10 @@ class LinkedSplitCharts(QtGui.QWidget):
zoomIsDisabled = QtCore.pyqtSignal(bool)
def __init__(
self,
chart_space: ChartSpace,
) -> None:
super().__init__()
self.signals_visible: bool = False
@ -194,6 +310,8 @@ class LinkedSplitCharts(QtGui.QWidget):
self.subplots: Dict[Tuple[str, ...], ChartPlotWidget] = {}
self.chart_space = chart_space
self.chart_space = chart_space
self.xaxis = DynamicDateAxis(
orientation='bottom',
linked_charts=self
@ -232,6 +350,14 @@ class LinkedSplitCharts(QtGui.QWidget):
sizes.extend([min_h_ind] * len(self.subplots))
self.splitter.setSizes(sizes) # , int(self.height()*0.2)
def focus(self) -> None:
if self.chart is not None:
self.chart.setFocus()
def unfocus(self) -> None:
if self.chart is not None:
self.chart.clearFocus()
def plot_ohlc_main(
self,
symbol: Symbol,
@ -1159,7 +1285,7 @@ async def spawn_fsps(
# XXX: fsp may have been opened by a duplicate chart. Error for
# now until we figure out how to wrap fsps as "feeds".
assert opened, f"A chart for {key} likely already exists?"
# assert opened, f"A chart for {key} likely already exists?"
conf['shm'] = shm
@ -1253,6 +1379,8 @@ async def run_fsp(
# fsp_func_name
)
chart._lc.focus()
# read last value
array = shm.array
value = array[fsp_func_name][-1]
@ -1261,7 +1389,6 @@ async def run_fsp(
last_val_sticky.update_from_data(-1, value)
chart.update_curve_from_array(fsp_func_name, array)
chart._shm = shm
# TODO: figure out if we can roll our own `FillToThreshold` to
@ -1387,14 +1514,7 @@ async def chart_symbol(
brokername: str,
sym: str,
loglevel: str,
task_status: TaskStatus[Symbol] = trio.TASK_STATUS_IGNORED,
) -> None:
"""Spawn a real-time chart widget for this symbol and app session.
These widgets can remain up but hidden so that multiple symbols
can be viewed and switched between extremely fast.
"""
# historical data fetch
brokermod = brokers.get_brokermod(brokername)
@ -1404,24 +1524,21 @@ async def chart_symbol(
loglevel=loglevel,
) as feed:
ohlcv: ShmArray = feed.shm
ohlcv = feed.shm
bars = ohlcv.array
symbol = feed.symbols[sym]
task_status.started(symbol)
# load in symbol's ohlc data
chart_app.window.setWindowTitle(
f'{symbol.key}@{symbol.brokers} '
f'tick:{symbol.tick_size}'
)
# await tractor.breakpoint()
linked_charts = chart_app.linkedcharts
linked_charts._symbol = symbol
chart = linked_charts.plot_ohlc_main(symbol, bars)
chart.setFocus()
linked_charts.focus()
# plot historical vwap if available
wap_in_history = False
@ -1494,6 +1611,9 @@ async def chart_symbol(
wap_in_history,
)
# await tractor.breakpoint()
# chart_app.linkedcharts.focus()
# wait for a first quote before we start any update tasks
quote = await feed.receive()
@ -1514,7 +1634,7 @@ async def chart_symbol(
# chart,
# linked_charts,
# )
chart_app.linkedcharts.focus()
await start_order_mode(chart, symbol, brokername)
@ -1522,7 +1642,7 @@ async def _async_main(
# implicit required argument provided by ``qtractor_run()``
widgets: Dict[str, Any],
symbol_key: str,
sym: str,
brokername: str,
loglevel: str,
@ -1550,29 +1670,24 @@ async def _async_main(
# configure global DPI aware font size
_font.configure_to_dpi(screen)
# try:
async with trio.open_nursery() as root_n:
# set root nursery for spawning other charts/feeds
# that run cached in the bg
chart_app._root_n = root_n
chart_app.load_symbol(brokername, symbol_key, loglevel)
# TODO: trigger on ctlr-k
chart_app.open_search()
symbol = await root_n.start(
chart_symbol,
chart_app,
brokername,
symbol_key,
loglevel,
)
chart_app.window.setWindowTitle(
f'{symbol.key}@{symbol.brokers} '
f'tick:{symbol.tick_size}'
)
# this internally starts a ``chart_symbol()`` task above
chart_app.load_symbol(brokername, sym, loglevel)
await trio.sleep_forever()
# finally:
# root_n.cancel()
def _main(
sym: str,

View File

@ -486,6 +486,8 @@ class ChartView(ViewBox):
self._key_buffer = []
self._key_active: bool = False
self.setFocusPolicy(QtCore.Qt.StrongFocus)
@property
def chart(self) -> 'ChartPlotWidget': # type: ignore # noqa
return self._chart
@ -757,6 +759,12 @@ class ChartView(ViewBox):
if mods == QtCore.Qt.AltModifier:
pass
# ctlr-k
if key == QtCore.Qt.Key_K and ctrl:
search = self._chart._lc.chart_space.search
search.show()
search.setFocus()
# esc
if key == QtCore.Qt.Key_Escape or (ctrl and key == QtCore.Qt.Key_C):
# ctrl-c as cancel