First draft: symbol switching via QLineEdit widget
parent
27aed85404
commit
0627f7dcee
|
@ -24,6 +24,8 @@ from types import ModuleType
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtGui
|
from PyQt5 import QtCore, QtGui
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5 import QtWidgets
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
import tractor
|
import tractor
|
||||||
|
@ -48,6 +50,7 @@ from ._graphics._ohlc import BarItems
|
||||||
from ._graphics._curve import FastAppendCurve
|
from ._graphics._curve import FastAppendCurve
|
||||||
from ._style import (
|
from ._style import (
|
||||||
_font,
|
_font,
|
||||||
|
DpiAwareFont,
|
||||||
hcolor,
|
hcolor,
|
||||||
CHART_MARGINS,
|
CHART_MARGINS,
|
||||||
_xaxis_at,
|
_xaxis_at,
|
||||||
|
@ -70,6 +73,99 @@ from .. import fsp
|
||||||
log = get_logger(__name__)
|
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):
|
class ChartSpace(QtGui.QWidget):
|
||||||
"""High level widget which contains layouts for organizing
|
"""High level widget which contains layouts for organizing
|
||||||
lower level charts as well as other widgets used to control
|
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 = QtGui.QVBoxLayout(self)
|
||||||
self.v_layout.setContentsMargins(0, 0, 0, 0)
|
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 = QtGui.QHBoxLayout()
|
||||||
self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
|
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.toolbar_layout)
|
||||||
self.v_layout.addLayout(self.h_layout)
|
self.v_layout.addLayout(self.h_layout)
|
||||||
self._chart_cache = {}
|
self._chart_cache = {}
|
||||||
|
self.linkedcharts: 'LinkedSplitCharts' = None
|
||||||
self.symbol_label: Optional[QtGui.QLabel] = None
|
self.symbol_label: Optional[QtGui.QLabel] = None
|
||||||
|
|
||||||
def init_search(self):
|
self._root_n: Optional[trio.Nursery] = None
|
||||||
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)
|
|
||||||
|
|
||||||
label.setAlignment(
|
def open_search(self):
|
||||||
QtCore.Qt.AlignVCenter
|
# search = self.search = QtWidgets.QLineEdit()
|
||||||
| QtCore.Qt.AlignLeft
|
self.search = FontSizedQLineEdit(self)
|
||||||
)
|
self.search.unfocus()
|
||||||
self.v_layout.addWidget(label)
|
self.v_layout.addWidget(self.search)
|
||||||
|
|
||||||
|
# search.installEventFilter(self)
|
||||||
|
|
||||||
def init_timeframes_ui(self):
|
def init_timeframes_ui(self):
|
||||||
self.tf_layout = QtGui.QHBoxLayout()
|
self.tf_layout = QtGui.QHBoxLayout()
|
||||||
|
@ -130,38 +223,59 @@ class ChartSpace(QtGui.QWidget):
|
||||||
# def init_strategy_ui(self):
|
# def init_strategy_ui(self):
|
||||||
# self.strategy_box = StrategyBoxWidget(self)
|
# self.strategy_box = StrategyBoxWidget(self)
|
||||||
# self.toolbar_layout.addWidget(self.strategy_box)
|
# self.toolbar_layout.addWidget(self.strategy_box)
|
||||||
|
|
||||||
def load_symbol(
|
def load_symbol(
|
||||||
self,
|
self,
|
||||||
brokername: str,
|
brokername: str,
|
||||||
symbol_key: str,
|
symbol_key: str,
|
||||||
data: np.ndarray,
|
loglevel: str,
|
||||||
ohlc: bool = True,
|
ohlc: bool = True,
|
||||||
|
reset: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Load a new contract into the charting app.
|
"""Load a new contract into the charting app.
|
||||||
|
|
||||||
Expects a ``numpy`` structured array containing all the ohlcv fields.
|
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,
|
linkedcharts = self._chart_cache.get(symbol_key)
|
||||||
LinkedSplitCharts(self)
|
|
||||||
)
|
# switching to a new viewable chart
|
||||||
self.linkedcharts = linkedcharts
|
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
|
# remove any existing plots
|
||||||
if not self.v_layout.isEmpty():
|
self.v_layout.removeWidget(self.linkedcharts)
|
||||||
self.v_layout.removeWidget(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)
|
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
|
# TODO: add signalling painter system
|
||||||
# def add_signals(self):
|
# def add_signals(self):
|
||||||
|
@ -184,8 +298,10 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
zoomIsDisabled = QtCore.pyqtSignal(bool)
|
zoomIsDisabled = QtCore.pyqtSignal(bool)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
||||||
self,
|
self,
|
||||||
chart_space: ChartSpace,
|
chart_space: ChartSpace,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.signals_visible: bool = False
|
self.signals_visible: bool = False
|
||||||
|
@ -194,6 +310,8 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
self.subplots: Dict[Tuple[str, ...], ChartPlotWidget] = {}
|
self.subplots: Dict[Tuple[str, ...], ChartPlotWidget] = {}
|
||||||
self.chart_space = chart_space
|
self.chart_space = chart_space
|
||||||
|
|
||||||
|
self.chart_space = chart_space
|
||||||
|
|
||||||
self.xaxis = DynamicDateAxis(
|
self.xaxis = DynamicDateAxis(
|
||||||
orientation='bottom',
|
orientation='bottom',
|
||||||
linked_charts=self
|
linked_charts=self
|
||||||
|
@ -232,6 +350,14 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
sizes.extend([min_h_ind] * len(self.subplots))
|
sizes.extend([min_h_ind] * len(self.subplots))
|
||||||
self.splitter.setSizes(sizes) # , int(self.height()*0.2)
|
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(
|
def plot_ohlc_main(
|
||||||
self,
|
self,
|
||||||
symbol: Symbol,
|
symbol: Symbol,
|
||||||
|
@ -1159,7 +1285,7 @@ async def spawn_fsps(
|
||||||
|
|
||||||
# XXX: fsp may have been opened by a duplicate chart. Error for
|
# XXX: fsp may have been opened by a duplicate chart. Error for
|
||||||
# now until we figure out how to wrap fsps as "feeds".
|
# 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
|
conf['shm'] = shm
|
||||||
|
|
||||||
|
@ -1253,6 +1379,8 @@ async def run_fsp(
|
||||||
# fsp_func_name
|
# fsp_func_name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
chart._lc.focus()
|
||||||
|
|
||||||
# read last value
|
# read last value
|
||||||
array = shm.array
|
array = shm.array
|
||||||
value = array[fsp_func_name][-1]
|
value = array[fsp_func_name][-1]
|
||||||
|
@ -1261,7 +1389,6 @@ async def run_fsp(
|
||||||
last_val_sticky.update_from_data(-1, value)
|
last_val_sticky.update_from_data(-1, value)
|
||||||
|
|
||||||
chart.update_curve_from_array(fsp_func_name, array)
|
chart.update_curve_from_array(fsp_func_name, array)
|
||||||
|
|
||||||
chart._shm = shm
|
chart._shm = shm
|
||||||
|
|
||||||
# TODO: figure out if we can roll our own `FillToThreshold` to
|
# TODO: figure out if we can roll our own `FillToThreshold` to
|
||||||
|
@ -1387,14 +1514,7 @@ async def chart_symbol(
|
||||||
brokername: str,
|
brokername: str,
|
||||||
sym: str,
|
sym: str,
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
task_status: TaskStatus[Symbol] = trio.TASK_STATUS_IGNORED,
|
|
||||||
) -> None:
|
) -> 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
|
# historical data fetch
|
||||||
brokermod = brokers.get_brokermod(brokername)
|
brokermod = brokers.get_brokermod(brokername)
|
||||||
|
|
||||||
|
@ -1404,24 +1524,21 @@ async def chart_symbol(
|
||||||
loglevel=loglevel,
|
loglevel=loglevel,
|
||||||
) as feed:
|
) as feed:
|
||||||
|
|
||||||
ohlcv: ShmArray = feed.shm
|
ohlcv = feed.shm
|
||||||
bars = ohlcv.array
|
bars = ohlcv.array
|
||||||
symbol = feed.symbols[sym]
|
symbol = feed.symbols[sym]
|
||||||
|
|
||||||
task_status.started(symbol)
|
|
||||||
|
|
||||||
# load in symbol's ohlc data
|
# load in symbol's ohlc data
|
||||||
chart_app.window.setWindowTitle(
|
chart_app.window.setWindowTitle(
|
||||||
f'{symbol.key}@{symbol.brokers} '
|
f'{symbol.key}@{symbol.brokers} '
|
||||||
f'tick:{symbol.tick_size}'
|
f'tick:{symbol.tick_size}'
|
||||||
)
|
)
|
||||||
|
|
||||||
# await tractor.breakpoint()
|
# await tractor.breakpoint()
|
||||||
linked_charts = chart_app.linkedcharts
|
linked_charts = chart_app.linkedcharts
|
||||||
linked_charts._symbol = symbol
|
linked_charts._symbol = symbol
|
||||||
chart = linked_charts.plot_ohlc_main(symbol, bars)
|
chart = linked_charts.plot_ohlc_main(symbol, bars)
|
||||||
|
|
||||||
chart.setFocus()
|
linked_charts.focus()
|
||||||
|
|
||||||
# plot historical vwap if available
|
# plot historical vwap if available
|
||||||
wap_in_history = False
|
wap_in_history = False
|
||||||
|
@ -1494,6 +1611,9 @@ async def chart_symbol(
|
||||||
wap_in_history,
|
wap_in_history,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# await tractor.breakpoint()
|
||||||
|
# chart_app.linkedcharts.focus()
|
||||||
|
|
||||||
# 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()
|
||||||
|
|
||||||
|
@ -1514,7 +1634,7 @@ async def chart_symbol(
|
||||||
# chart,
|
# chart,
|
||||||
# linked_charts,
|
# linked_charts,
|
||||||
# )
|
# )
|
||||||
|
chart_app.linkedcharts.focus()
|
||||||
await start_order_mode(chart, symbol, brokername)
|
await start_order_mode(chart, symbol, brokername)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1522,7 +1642,7 @@ async def _async_main(
|
||||||
# implicit required argument provided by ``qtractor_run()``
|
# implicit required argument provided by ``qtractor_run()``
|
||||||
widgets: Dict[str, Any],
|
widgets: Dict[str, Any],
|
||||||
|
|
||||||
symbol_key: str,
|
sym: str,
|
||||||
brokername: str,
|
brokername: str,
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
|
|
||||||
|
@ -1550,29 +1670,24 @@ async def _async_main(
|
||||||
# configure global DPI aware font size
|
# configure global DPI aware font size
|
||||||
_font.configure_to_dpi(screen)
|
_font.configure_to_dpi(screen)
|
||||||
|
|
||||||
|
# try:
|
||||||
async with trio.open_nursery() as root_n:
|
async with trio.open_nursery() as root_n:
|
||||||
|
|
||||||
# set root nursery for spawning other charts/feeds
|
# set root nursery for spawning other charts/feeds
|
||||||
# that run cached in the bg
|
# that run cached in the bg
|
||||||
chart_app._root_n = root_n
|
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(
|
# this internally starts a ``chart_symbol()`` task above
|
||||||
chart_symbol,
|
chart_app.load_symbol(brokername, sym, loglevel)
|
||||||
chart_app,
|
|
||||||
brokername,
|
|
||||||
symbol_key,
|
|
||||||
loglevel,
|
|
||||||
)
|
|
||||||
|
|
||||||
chart_app.window.setWindowTitle(
|
|
||||||
f'{symbol.key}@{symbol.brokers} '
|
|
||||||
f'tick:{symbol.tick_size}'
|
|
||||||
)
|
|
||||||
|
|
||||||
await trio.sleep_forever()
|
await trio.sleep_forever()
|
||||||
|
|
||||||
|
# finally:
|
||||||
|
# root_n.cancel()
|
||||||
|
|
||||||
|
|
||||||
def _main(
|
def _main(
|
||||||
sym: str,
|
sym: str,
|
||||||
|
|
|
@ -486,6 +486,8 @@ class ChartView(ViewBox):
|
||||||
self._key_buffer = []
|
self._key_buffer = []
|
||||||
self._key_active: bool = False
|
self._key_active: bool = False
|
||||||
|
|
||||||
|
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def chart(self) -> 'ChartPlotWidget': # type: ignore # noqa
|
def chart(self) -> 'ChartPlotWidget': # type: ignore # noqa
|
||||||
return self._chart
|
return self._chart
|
||||||
|
@ -757,6 +759,12 @@ class ChartView(ViewBox):
|
||||||
if mods == QtCore.Qt.AltModifier:
|
if mods == QtCore.Qt.AltModifier:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# ctlr-k
|
||||||
|
if key == QtCore.Qt.Key_K and ctrl:
|
||||||
|
search = self._chart._lc.chart_space.search
|
||||||
|
search.show()
|
||||||
|
search.setFocus()
|
||||||
|
|
||||||
# esc
|
# esc
|
||||||
if key == QtCore.Qt.Key_Escape or (ctrl and key == QtCore.Qt.Key_C):
|
if key == QtCore.Qt.Key_Escape or (ctrl and key == QtCore.Qt.Key_C):
|
||||||
# ctrl-c as cancel
|
# ctrl-c as cancel
|
||||||
|
|
Loading…
Reference in New Issue