commit
e52ecbe589
|
@ -52,7 +52,6 @@ from ._l1 import L1Labels
|
||||||
from ._graphics._ohlc import BarItems
|
from ._graphics._ohlc import BarItems
|
||||||
from ._graphics._curve import FastAppendCurve
|
from ._graphics._curve import FastAppendCurve
|
||||||
from ._style import (
|
from ._style import (
|
||||||
_font,
|
|
||||||
hcolor,
|
hcolor,
|
||||||
CHART_MARGINS,
|
CHART_MARGINS,
|
||||||
_xaxis_at,
|
_xaxis_at,
|
||||||
|
@ -68,7 +67,7 @@ from ..data import maybe_open_shm_array
|
||||||
from .. import brokers
|
from .. import brokers
|
||||||
from .. import data
|
from .. import data
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from ._exec import run_qtractor, current_screen
|
from ._exec import run_qtractor
|
||||||
from ._interaction import ChartView
|
from ._interaction import ChartView
|
||||||
from .order_mode import start_order_mode
|
from .order_mode import start_order_mode
|
||||||
from .. import fsp
|
from .. import fsp
|
||||||
|
@ -79,10 +78,11 @@ log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ChartSpace(QtGui.QWidget):
|
class ChartSpace(QtGui.QWidget):
|
||||||
"""High level widget which contains layouts for organizing
|
'''Highest level composed widget which contains layouts for
|
||||||
lower level charts as well as other widgets used to control
|
organizing lower level charts as well as other widgets used to
|
||||||
or modify them.
|
control or modify them.
|
||||||
"""
|
|
||||||
|
'''
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
|
@ -431,6 +431,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
_l1_labels: L1Labels = None
|
_l1_labels: L1Labels = None
|
||||||
|
|
||||||
|
mode_name: str = 'mode: view'
|
||||||
|
|
||||||
# TODO: can take a ``background`` color setting - maybe there's
|
# TODO: can take a ``background`` color setting - maybe there's
|
||||||
# a better one?
|
# a better one?
|
||||||
|
|
||||||
|
@ -1000,7 +1002,7 @@ async def test_bed(
|
||||||
|
|
||||||
|
|
||||||
_clear_throttle_rate: int = 60 # Hz
|
_clear_throttle_rate: int = 60 # Hz
|
||||||
_book_throttle_rate: int = 20 # Hz
|
_book_throttle_rate: int = 16 # Hz
|
||||||
|
|
||||||
|
|
||||||
async def chart_from_quotes(
|
async def chart_from_quotes(
|
||||||
|
@ -1078,7 +1080,14 @@ async def chart_from_quotes(
|
||||||
tick_margin = 2 * tick_size
|
tick_margin = 2 * tick_size
|
||||||
|
|
||||||
last_ask = last_bid = last_clear = time.time()
|
last_ask = last_bid = last_clear = time.time()
|
||||||
|
chart.show()
|
||||||
|
|
||||||
async for quotes in stream:
|
async for quotes in stream:
|
||||||
|
|
||||||
|
# chart isn't actively shown so just skip render cycle
|
||||||
|
if chart._lc.isHidden():
|
||||||
|
continue
|
||||||
|
|
||||||
for sym, quote in quotes.items():
|
for sym, quote in quotes.items():
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
@ -1291,6 +1300,9 @@ 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(
|
||||||
|
f'loading FSP: {display_name}..')
|
||||||
|
|
||||||
async with portal.open_stream_from(
|
async with portal.open_stream_from(
|
||||||
|
|
||||||
# subactor entrypoint
|
# subactor entrypoint
|
||||||
|
@ -1384,13 +1396,20 @@ async def run_fsp(
|
||||||
|
|
||||||
last = time.time()
|
last = time.time()
|
||||||
|
|
||||||
|
done()
|
||||||
|
|
||||||
# update chart graphics
|
# update chart graphics
|
||||||
async for value in stream:
|
async for value in stream:
|
||||||
|
|
||||||
|
# chart isn't actively shown so just skip render cycle
|
||||||
|
if chart._lc.isHidden():
|
||||||
|
continue
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
period = now - last
|
period = now - last
|
||||||
|
|
||||||
# if period <= 1/30:
|
# if period <= 1/30:
|
||||||
if period <= 1/_clear_throttle_rate - 0.001:
|
if period <= 1/_clear_throttle_rate:
|
||||||
# faster then display refresh rate
|
# faster then display refresh rate
|
||||||
# print(f'quote too fast: {1/period}')
|
# print(f'quote too fast: {1/period}')
|
||||||
continue
|
continue
|
||||||
|
@ -1479,7 +1498,7 @@ async def check_for_new_bars(feed, ohlcv, linked_charts):
|
||||||
|
|
||||||
async def chart_symbol(
|
async def chart_symbol(
|
||||||
chart_app: ChartSpace,
|
chart_app: ChartSpace,
|
||||||
brokername: str,
|
provider: str,
|
||||||
sym: str,
|
sym: str,
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -1489,11 +1508,14 @@ async def chart_symbol(
|
||||||
can be viewed and switched between extremely fast.
|
can be viewed and switched between extremely fast.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
sbar = chart_app.window.status_bar
|
||||||
|
loading_sym_done = sbar.open_status(f'loading {sym}.{provider}..')
|
||||||
|
|
||||||
# historical data fetch
|
# historical data fetch
|
||||||
brokermod = brokers.get_brokermod(brokername)
|
brokermod = brokers.get_brokermod(provider)
|
||||||
|
|
||||||
async with data.open_feed(
|
async with data.open_feed(
|
||||||
brokername,
|
provider,
|
||||||
[sym],
|
[sym],
|
||||||
loglevel=loglevel,
|
loglevel=loglevel,
|
||||||
) as feed:
|
) as feed:
|
||||||
|
@ -1526,6 +1548,8 @@ async def chart_symbol(
|
||||||
add_label=False,
|
add_label=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
loading_sym_done()
|
||||||
|
|
||||||
# size view to data once at outset
|
# size view to data once at outset
|
||||||
chart._set_yrange()
|
chart._set_yrange()
|
||||||
|
|
||||||
|
@ -1604,7 +1628,7 @@ async def chart_symbol(
|
||||||
# linked_charts,
|
# linked_charts,
|
||||||
# )
|
# )
|
||||||
|
|
||||||
await start_order_mode(chart, symbol, brokername)
|
await start_order_mode(chart, symbol, provider)
|
||||||
|
|
||||||
|
|
||||||
async def load_providers(
|
async def load_providers(
|
||||||
|
@ -1621,7 +1645,7 @@ async def load_providers(
|
||||||
# search engines.
|
# search engines.
|
||||||
for broker in brokernames:
|
for broker in brokernames:
|
||||||
|
|
||||||
log.info(f'Loading brokerd for {broker}')
|
log.info(f'loading brokerd for {broker}..')
|
||||||
# spin up broker daemons for each provider
|
# spin up broker daemons for each provider
|
||||||
portal = await stack.enter_async_context(
|
portal = await stack.enter_async_context(
|
||||||
maybe_spawn_brokerd(
|
maybe_spawn_brokerd(
|
||||||
|
@ -1645,7 +1669,7 @@ 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()``
|
||||||
widgets: Dict[str, Any],
|
main_widget: ChartSpace,
|
||||||
|
|
||||||
sym: str,
|
sym: str,
|
||||||
brokernames: str,
|
brokernames: str,
|
||||||
|
@ -1659,10 +1683,10 @@ async def _async_main(
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
chart_app = widgets['main']
|
chart_app = main_widget
|
||||||
|
|
||||||
# attempt to configure DPI aware font size
|
# attempt to configure DPI aware font size
|
||||||
screen = current_screen()
|
screen = chart_app.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
|
||||||
|
@ -1672,8 +1696,11 @@ 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')
|
||||||
|
|
||||||
# configure global DPI aware font size
|
# TODO: do styling / themeing setup
|
||||||
_font.configure_to_dpi(screen)
|
# _style.style_ze_sheets(chart_app)
|
||||||
|
|
||||||
|
sbar = chart_app.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:
|
||||||
|
|
||||||
|
@ -1702,8 +1729,6 @@ async def _async_main(
|
||||||
# this internally starts a ``chart_symbol()`` task above
|
# this internally starts a ``chart_symbol()`` task above
|
||||||
chart_app.load_symbol(provider, symbol, loglevel)
|
chart_app.load_symbol(provider, symbol, loglevel)
|
||||||
|
|
||||||
root_n.start_soon(load_providers, brokernames, 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(
|
||||||
|
|
||||||
|
@ -1712,8 +1737,14 @@ async def _async_main(
|
||||||
_search.search_simple_dict,
|
_search.search_simple_dict,
|
||||||
source=chart_app._chart_cache,
|
source=chart_app._chart_cache,
|
||||||
),
|
),
|
||||||
|
# cache is super fast so debounce on super short period
|
||||||
|
pause_period=0.01,
|
||||||
|
|
||||||
):
|
):
|
||||||
|
# load other providers into search **after**
|
||||||
|
# the chart's select cache
|
||||||
|
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 open_key_stream(
|
||||||
search.bar,
|
search.bar,
|
||||||
|
@ -1727,6 +1758,7 @@ async def _async_main(
|
||||||
key_stream,
|
key_stream,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
starting_done()
|
||||||
await trio.sleep_forever()
|
await trio.sleep_forever()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,10 +21,7 @@ Run ``trio`` in guest mode on top of the Qt event loop.
|
||||||
All global Qt runtime settings are mostly defined here.
|
All global Qt runtime settings are mostly defined here.
|
||||||
"""
|
"""
|
||||||
from typing import Tuple, Callable, Dict, Any
|
from typing import Tuple, Callable, Dict, Any
|
||||||
import os
|
|
||||||
import platform
|
import platform
|
||||||
import signal
|
|
||||||
import time
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
# Qt specific
|
# Qt specific
|
||||||
|
@ -32,20 +29,22 @@ import PyQt5 # noqa
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from pyqtgraph import QtGui
|
from pyqtgraph import QtGui
|
||||||
from PyQt5 import QtCore
|
from PyQt5 import QtCore
|
||||||
|
# from PyQt5.QtGui import QLabel, QStatusBar
|
||||||
from PyQt5.QtCore import (
|
from PyQt5.QtCore import (
|
||||||
pyqtRemoveInputHook,
|
pyqtRemoveInputHook,
|
||||||
Qt,
|
Qt,
|
||||||
QCoreApplication,
|
QCoreApplication,
|
||||||
)
|
)
|
||||||
import qdarkstyle
|
import qdarkstyle
|
||||||
|
from qdarkstyle import DarkPalette
|
||||||
# import qdarkgraystyle
|
# import qdarkgraystyle
|
||||||
import trio
|
import trio
|
||||||
import tractor
|
|
||||||
from outcome import Error
|
from outcome import Error
|
||||||
|
|
||||||
from .._daemon import maybe_open_pikerd, _tractor_kwargs
|
from .._daemon import maybe_open_pikerd, _tractor_kwargs
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from ._pg_overrides import _do_overrides
|
from ._pg_overrides import _do_overrides
|
||||||
|
from . import _style
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -59,37 +58,6 @@ pg.enableExperimental = True
|
||||||
_do_overrides()
|
_do_overrides()
|
||||||
|
|
||||||
|
|
||||||
# singleton app per actor
|
|
||||||
_qt_app: QtGui.QApplication = None
|
|
||||||
_qt_win: QtGui.QMainWindow = None
|
|
||||||
|
|
||||||
|
|
||||||
def current_screen() -> QtGui.QScreen:
|
|
||||||
"""Get a frickin screen (if we can, gawd).
|
|
||||||
|
|
||||||
"""
|
|
||||||
global _qt_win, _qt_app
|
|
||||||
|
|
||||||
start = time.time()
|
|
||||||
|
|
||||||
tries = 3
|
|
||||||
for _ in range(3):
|
|
||||||
screen = _qt_app.screenAt(_qt_win.pos())
|
|
||||||
print(f'trying to get screen....')
|
|
||||||
if screen is None:
|
|
||||||
time.sleep(0.5)
|
|
||||||
continue
|
|
||||||
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
if screen is None:
|
|
||||||
# try for the first one we can find
|
|
||||||
screen = _qt_app.screens()[0]
|
|
||||||
|
|
||||||
assert screen, "Wow Qt is dumb as shit and has no screen..."
|
|
||||||
return screen
|
|
||||||
|
|
||||||
|
|
||||||
# XXX: pretty sure none of this shit works on linux as per:
|
# XXX: pretty sure none of this shit works on linux as per:
|
||||||
# https://bugreports.qt.io/browse/QTBUG-53022
|
# https://bugreports.qt.io/browse/QTBUG-53022
|
||||||
# it seems to work on windows.. no idea wtf is up.
|
# it seems to work on windows.. no idea wtf is up.
|
||||||
|
@ -104,33 +72,12 @@ if platform.system() == "Windows":
|
||||||
QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QtGui.QMainWindow):
|
|
||||||
|
|
||||||
size = (800, 500)
|
|
||||||
title = 'piker chart (ur symbol is loading bby)'
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setMinimumSize(*self.size)
|
|
||||||
self.setWindowTitle(self.title)
|
|
||||||
|
|
||||||
def closeEvent(
|
|
||||||
self,
|
|
||||||
event: QtGui.QCloseEvent,
|
|
||||||
) -> None:
|
|
||||||
"""Cancel the root actor asap.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# raising KBI seems to get intercepted by by Qt so just use the system.
|
|
||||||
os.kill(os.getpid(), signal.SIGINT)
|
|
||||||
|
|
||||||
|
|
||||||
def run_qtractor(
|
def run_qtractor(
|
||||||
func: Callable,
|
func: Callable,
|
||||||
args: Tuple,
|
args: Tuple,
|
||||||
main_widget: QtGui.QWidget,
|
main_widget: QtGui.QWidget,
|
||||||
tractor_kwargs: Dict[str, Any] = {},
|
tractor_kwargs: Dict[str, Any] = {},
|
||||||
window_type: QtGui.QMainWindow = MainWindow,
|
window_type: QtGui.QMainWindow = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# avoids annoying message when entering debugger from qt loop
|
# avoids annoying message when entering debugger from qt loop
|
||||||
pyqtRemoveInputHook()
|
pyqtRemoveInputHook()
|
||||||
|
@ -148,10 +95,6 @@ def run_qtractor(
|
||||||
# XXX: lmfao, this is how you disable text edit cursor blinking..smh
|
# XXX: lmfao, this is how you disable text edit cursor blinking..smh
|
||||||
app.setCursorFlashTime(0)
|
app.setCursorFlashTime(0)
|
||||||
|
|
||||||
# set global app singleton
|
|
||||||
global _qt_app
|
|
||||||
_qt_app = app
|
|
||||||
|
|
||||||
# This code is from Nathaniel, and I quote:
|
# This code is from Nathaniel, and I quote:
|
||||||
# "This is substantially faster than using a signal... for some
|
# "This is substantially faster than using a signal... for some
|
||||||
# reason Qt signal dispatch is really slow (and relies on events
|
# reason Qt signal dispatch is really slow (and relies on events
|
||||||
|
@ -188,19 +131,33 @@ def run_qtractor(
|
||||||
app.quit()
|
app.quit()
|
||||||
|
|
||||||
# load dark theme
|
# load dark theme
|
||||||
app.setStyleSheet(qdarkstyle.load_stylesheet(qt_api='pyqt5'))
|
stylesheet = qdarkstyle.load_stylesheet(
|
||||||
|
qt_api='pyqt5',
|
||||||
|
palette=DarkPalette,
|
||||||
|
)
|
||||||
|
app.setStyleSheet(stylesheet)
|
||||||
|
|
||||||
# make window and exec
|
# make window and exec
|
||||||
|
from . import _window
|
||||||
|
|
||||||
|
if window_type is None:
|
||||||
|
window_type = _window.MainWindow
|
||||||
|
|
||||||
window = window_type()
|
window = window_type()
|
||||||
|
|
||||||
|
# set global app's main window singleton
|
||||||
|
_window._qt_win = window
|
||||||
|
|
||||||
|
# configure global DPI aware font sizes now that a screen
|
||||||
|
# should be active from which we can read a DPI.
|
||||||
|
_style._config_fonts_to_screen()
|
||||||
|
|
||||||
|
# hook into app focus change events
|
||||||
|
app.focusChanged.connect(window.on_focus_change)
|
||||||
|
|
||||||
instance = main_widget()
|
instance = main_widget()
|
||||||
instance.window = window
|
instance.window = window
|
||||||
|
|
||||||
widgets = {
|
|
||||||
'window': window,
|
|
||||||
'main': instance,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# override tractor's defaults
|
# override tractor's defaults
|
||||||
tractor_kwargs.update(_tractor_kwargs)
|
tractor_kwargs.update(_tractor_kwargs)
|
||||||
|
|
||||||
|
@ -210,7 +167,7 @@ def run_qtractor(
|
||||||
async with maybe_open_pikerd(
|
async with maybe_open_pikerd(
|
||||||
**tractor_kwargs,
|
**tractor_kwargs,
|
||||||
):
|
):
|
||||||
await func(*((widgets,) + args))
|
await func(*((instance,) + args))
|
||||||
|
|
||||||
# guest mode entry
|
# guest mode entry
|
||||||
trio.lowlevel.start_guest_run(
|
trio.lowlevel.start_guest_run(
|
||||||
|
@ -223,11 +180,6 @@ def run_qtractor(
|
||||||
window.main_widget = main_widget
|
window.main_widget = main_widget
|
||||||
window.setCentralWidget(instance)
|
window.setCentralWidget(instance)
|
||||||
|
|
||||||
# store global ref
|
|
||||||
# set global app singleton
|
|
||||||
global _qt_win
|
|
||||||
_qt_win = window
|
|
||||||
|
|
||||||
# actually render to screen
|
# actually render to screen
|
||||||
window.show()
|
window.show()
|
||||||
app.exec_()
|
app.exec_()
|
||||||
|
|
|
@ -31,6 +31,7 @@ from .._style import (
|
||||||
_xaxis_at,
|
_xaxis_at,
|
||||||
hcolor,
|
hcolor,
|
||||||
_font,
|
_font,
|
||||||
|
_font_small,
|
||||||
)
|
)
|
||||||
from .._axes import YAxisLabel, XAxisLabel
|
from .._axes import YAxisLabel, XAxisLabel
|
||||||
from ...log import get_logger
|
from ...log import get_logger
|
||||||
|
@ -109,7 +110,7 @@ class LineDot(pg.CurvePoint):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# TODO: change this into our own ``Label``
|
# TODO: change this into our own ``_label.Label``
|
||||||
class ContentsLabel(pg.LabelItem):
|
class ContentsLabel(pg.LabelItem):
|
||||||
"""Label anchored to a ``ViewBox`` typically for displaying
|
"""Label anchored to a ``ViewBox`` typically for displaying
|
||||||
datum-wise points from the "viewed" contents.
|
datum-wise points from the "viewed" contents.
|
||||||
|
@ -138,22 +139,14 @@ class ContentsLabel(pg.LabelItem):
|
||||||
justify_text: str = 'left',
|
justify_text: str = 'left',
|
||||||
font_size: Optional[int] = None,
|
font_size: Optional[int] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
font_size = font_size or _font.font.pixelSize()
|
|
||||||
|
font_size = font_size or _font_small.px_size
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
justify=justify_text,
|
justify=justify_text,
|
||||||
size=f'{str(font_size)}px'
|
size=f'{str(font_size)}px'
|
||||||
)
|
)
|
||||||
|
|
||||||
if _font._physical_dpi >= 97:
|
|
||||||
# ad-hoc scale it based on boundingRect
|
|
||||||
# TODO: need proper fix for this?
|
|
||||||
typical_br = _font._qfm.boundingRect('Qyp')
|
|
||||||
anchor_font_size = math.ceil(typical_br.height() * 1.25)
|
|
||||||
|
|
||||||
else:
|
|
||||||
anchor_font_size = font_size
|
|
||||||
|
|
||||||
# anchor to viewbox
|
# anchor to viewbox
|
||||||
self.setParentItem(chart._vb)
|
self.setParentItem(chart._vb)
|
||||||
chart.scene().addItem(self)
|
chart.scene().addItem(self)
|
||||||
|
@ -165,7 +158,7 @@ class ContentsLabel(pg.LabelItem):
|
||||||
|
|
||||||
ydim = margins[1]
|
ydim = margins[1]
|
||||||
if inspect.isfunction(margins[1]):
|
if inspect.isfunction(margins[1]):
|
||||||
margins = margins[0], ydim(anchor_font_size)
|
margins = margins[0], ydim(font_size)
|
||||||
|
|
||||||
self.anchor(itemPos=index, parentPos=index, offset=margins)
|
self.anchor(itemPos=index, parentPos=index, offset=margins)
|
||||||
|
|
||||||
|
|
|
@ -467,6 +467,9 @@ class ChartView(ViewBox):
|
||||||
- zoom on right-click-n-drag to cursor position
|
- zoom on right-click-n-drag to cursor position
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
mode_name: str = 'mode: view'
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
parent: pg.PlotItem = None,
|
parent: pg.PlotItem = None,
|
||||||
|
|
|
@ -96,6 +96,8 @@ class SimpleDelegate(QStyledItemDelegate):
|
||||||
|
|
||||||
class CompleterView(QTreeView):
|
class CompleterView(QTreeView):
|
||||||
|
|
||||||
|
mode_name: str = 'mode: search-nav'
|
||||||
|
|
||||||
# XXX: relevant docs links:
|
# XXX: relevant docs links:
|
||||||
# - simple widget version of this:
|
# - simple widget version of this:
|
||||||
# https://doc.qt.io/qt-5/qtreewidget.html#details
|
# https://doc.qt.io/qt-5/qtreewidget.html#details
|
||||||
|
@ -153,13 +155,14 @@ class CompleterView(QTreeView):
|
||||||
self._font_size: int = 0 # pixels
|
self._font_size: int = 0 # pixels
|
||||||
|
|
||||||
def on_pressed(self, idx: QModelIndex) -> None:
|
def on_pressed(self, idx: QModelIndex) -> None:
|
||||||
|
'''Mouse pressed on view handler.
|
||||||
|
|
||||||
|
'''
|
||||||
search = self.parent()
|
search = self.parent()
|
||||||
search.chart_current_item(clear_to_cache=False)
|
search.chart_current_item(clear_to_cache=False)
|
||||||
search.focus()
|
search.focus()
|
||||||
|
|
||||||
def set_font_size(self, size: int = 18):
|
def set_font_size(self, size: int = 18):
|
||||||
# dpi_px_size = _font.px_size
|
|
||||||
# print(size)
|
# print(size)
|
||||||
if size < 0:
|
if size < 0:
|
||||||
size = 16
|
size = 16
|
||||||
|
@ -424,6 +427,8 @@ class CompleterView(QTreeView):
|
||||||
|
|
||||||
class SearchBar(QtWidgets.QLineEdit):
|
class SearchBar(QtWidgets.QLineEdit):
|
||||||
|
|
||||||
|
mode_name: str = 'mode: search'
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
||||||
self,
|
self,
|
||||||
|
@ -487,6 +492,8 @@ class SearchWidget(QtGui.QWidget):
|
||||||
Includes helper methods for item management in the sub-widgets.
|
Includes helper methods for item management in the sub-widgets.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
mode_name: str = 'mode: search'
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
chart_space: 'ChartSpace', # type: ignore # noqa
|
chart_space: 'ChartSpace', # type: ignore # noqa
|
||||||
|
@ -499,7 +506,7 @@ class SearchWidget(QtGui.QWidget):
|
||||||
# size it as we specify
|
# size it as we specify
|
||||||
self.setSizePolicy(
|
self.setSizePolicy(
|
||||||
QtWidgets.QSizePolicy.Fixed,
|
QtWidgets.QSizePolicy.Fixed,
|
||||||
QtWidgets.QSizePolicy.Expanding,
|
QtWidgets.QSizePolicy.Fixed,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.chart_app = chart_space
|
self.chart_app = chart_space
|
||||||
|
@ -618,13 +625,15 @@ class SearchWidget(QtGui.QWidget):
|
||||||
# making?)
|
# making?)
|
||||||
fqsn = '.'.join([symbol, provider]).lower()
|
fqsn = '.'.join([symbol, provider]).lower()
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
if clear_to_cache:
|
if clear_to_cache:
|
||||||
|
|
||||||
self.bar.clear()
|
self.bar.clear()
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
self.view.set_section_entries(
|
self.view.set_section_entries(
|
||||||
'cache',
|
'cache',
|
||||||
values=list(reversed(chart._chart_cache)),
|
values=list(reversed(chart._chart_cache)),
|
||||||
|
@ -692,8 +701,10 @@ async def fill_results(
|
||||||
recv_chan: trio.abc.ReceiveChannel,
|
recv_chan: trio.abc.ReceiveChannel,
|
||||||
|
|
||||||
# kb debouncing pauses (bracket defaults)
|
# kb debouncing pauses (bracket defaults)
|
||||||
min_pause_time: float = 0.1,
|
min_pause_time: float = 0.01, # absolute min typing throttle
|
||||||
max_pause_time: float = 6/16,
|
|
||||||
|
# max pause required before slow relay
|
||||||
|
max_pause_time: float = 6/16 + 0.001,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Task to search through providers and fill in possible
|
"""Task to search through providers and fill in possible
|
||||||
|
@ -742,11 +753,6 @@ async def fill_results(
|
||||||
_search_active = trio.Event()
|
_search_active = trio.Event()
|
||||||
break
|
break
|
||||||
|
|
||||||
if repeats > 2 and period >= max_pause_time:
|
|
||||||
_search_active = trio.Event()
|
|
||||||
repeats = 0
|
|
||||||
break
|
|
||||||
|
|
||||||
if text == last_text:
|
if text == last_text:
|
||||||
repeats += 1
|
repeats += 1
|
||||||
|
|
||||||
|
@ -754,9 +760,8 @@ async def fill_results(
|
||||||
# print('search currently disabled')
|
# print('search currently disabled')
|
||||||
break
|
break
|
||||||
|
|
||||||
log.debug(f'Search req for {text}')
|
|
||||||
|
|
||||||
already_has_results = has_results[text]
|
already_has_results = has_results[text]
|
||||||
|
log.debug(f'Search req for {text}')
|
||||||
|
|
||||||
# issue multi-provider fan-out search request and place
|
# issue multi-provider fan-out search request and place
|
||||||
# "searching.." statuses on outstanding results providers
|
# "searching.." statuses on outstanding results providers
|
||||||
|
@ -765,16 +770,22 @@ async def fill_results(
|
||||||
for provider, (search, pause) in (
|
for provider, (search, pause) in (
|
||||||
_searcher_cache.copy().items()
|
_searcher_cache.copy().items()
|
||||||
):
|
):
|
||||||
|
# XXX: only conduct search on this backend if it's
|
||||||
if provider != 'cache':
|
# registered for the corresponding pause period AND
|
||||||
view.clear_section(
|
# it hasn't already been searched with the current
|
||||||
provider, status_field='-> searchin..')
|
# input pattern (in which case just look up the old
|
||||||
|
# results).
|
||||||
# only conduct search on this backend if it's
|
|
||||||
# registered for the corresponding pause period.
|
|
||||||
if (period >= pause) and (
|
if (period >= pause) and (
|
||||||
provider not in already_has_results
|
provider not in already_has_results
|
||||||
):
|
):
|
||||||
|
|
||||||
|
# TODO: it may make more sense TO NOT search the
|
||||||
|
# cache in a bg task since we know it's fully
|
||||||
|
# cpu-bound.
|
||||||
|
if provider != 'cache':
|
||||||
|
view.clear_section(
|
||||||
|
provider, status_field='-> searchin..')
|
||||||
|
|
||||||
await n.start(
|
await n.start(
|
||||||
pack_matches,
|
pack_matches,
|
||||||
view,
|
view,
|
||||||
|
@ -786,6 +797,14 @@ async def fill_results(
|
||||||
)
|
)
|
||||||
else: # already has results for this input text
|
else: # already has results for this input text
|
||||||
results = matches[(provider, text)]
|
results = matches[(provider, text)]
|
||||||
|
|
||||||
|
# TODO really for the cache we need an
|
||||||
|
# invalidation signal so that we only re-search
|
||||||
|
# the cache once it's been mutated by the chart
|
||||||
|
# switcher.. right now we're just always
|
||||||
|
# re-searching it's ``dict`` since it's easier
|
||||||
|
# but it also causes it to be slower then cached
|
||||||
|
# results from other providers on occasion.
|
||||||
if results and provider != 'cache':
|
if results and provider != 'cache':
|
||||||
view.set_section_entries(
|
view.set_section_entries(
|
||||||
section=provider,
|
section=provider,
|
||||||
|
@ -794,6 +813,11 @@ async def fill_results(
|
||||||
else:
|
else:
|
||||||
view.clear_section(provider)
|
view.clear_section(provider)
|
||||||
|
|
||||||
|
if repeats > 2 and period > max_pause_time:
|
||||||
|
_search_active = trio.Event()
|
||||||
|
repeats = 0
|
||||||
|
break
|
||||||
|
|
||||||
bar.show()
|
bar.show()
|
||||||
|
|
||||||
|
|
||||||
|
@ -952,7 +976,7 @@ async def register_symbol_search(
|
||||||
|
|
||||||
global _searcher_cache
|
global _searcher_cache
|
||||||
|
|
||||||
pause_period = pause_period or 0.125
|
pause_period = pause_period or 0.1
|
||||||
|
|
||||||
# deliver search func to consumer
|
# deliver search func to consumer
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -22,18 +22,19 @@ import math
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from PyQt5 import QtCore, QtGui
|
from PyQt5 import QtCore, QtGui
|
||||||
from qdarkstyle.palette import DarkPalette
|
from qdarkstyle import DarkPalette
|
||||||
|
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from ._exec import current_screen
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
_magic_inches = 0.0666 * (1 + 6/16)
|
||||||
|
|
||||||
# chart-wide fonts specified in inches
|
# chart-wide fonts specified in inches
|
||||||
_font_sizes: Dict[str, Dict[str, float]] = {
|
_font_sizes: Dict[str, Dict[str, float]] = {
|
||||||
'hi': {
|
'hi': {
|
||||||
'default': 0.0616,
|
'default': _magic_inches,
|
||||||
'small': 0.055,
|
'small': 0.9 * _magic_inches,
|
||||||
},
|
},
|
||||||
'lo': {
|
'lo': {
|
||||||
'default': 6.5 / 64,
|
'default': 6.5 / 64,
|
||||||
|
@ -43,6 +44,7 @@ _font_sizes: Dict[str, Dict[str, float]] = {
|
||||||
|
|
||||||
|
|
||||||
class DpiAwareFont:
|
class DpiAwareFont:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
# TODO: move to config
|
# TODO: move to config
|
||||||
|
@ -52,10 +54,10 @@ class DpiAwareFont:
|
||||||
) -> None:
|
) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self._qfont = QtGui.QFont(name)
|
self._qfont = QtGui.QFont(name)
|
||||||
# self._iwl = size_in_inches or _default_font_inches_we_like
|
|
||||||
self._font_size: str = font_size
|
self._font_size: str = font_size
|
||||||
self._qfm = QtGui.QFontMetrics(self._qfont)
|
self._qfm = QtGui.QFontMetrics(self._qfont)
|
||||||
self._physical_dpi = None
|
self._physical_dpi = None
|
||||||
|
self._font_inches: float = None
|
||||||
self._screen = None
|
self._screen = None
|
||||||
|
|
||||||
def _set_qfont_px_size(self, px_size: int) -> None:
|
def _set_qfont_px_size(self, px_size: int) -> None:
|
||||||
|
@ -64,13 +66,15 @@ class DpiAwareFont:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def screen(self) -> QtGui.QScreen:
|
def screen(self) -> QtGui.QScreen:
|
||||||
|
from ._window import main_window
|
||||||
|
|
||||||
if self._screen is not None:
|
if self._screen is not None:
|
||||||
try:
|
try:
|
||||||
self._screen.refreshRate()
|
self._screen.refreshRate()
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
self._screen = current_screen()
|
self._screen = main_window().current_screen()
|
||||||
else:
|
else:
|
||||||
self._screen = current_screen()
|
self._screen = main_window().current_screen()
|
||||||
|
|
||||||
return self._screen
|
return self._screen
|
||||||
|
|
||||||
|
@ -95,22 +99,34 @@ class DpiAwareFont:
|
||||||
# take the max since scaling can make things ugly in some cases
|
# take the max since scaling can make things ugly in some cases
|
||||||
pdpi = screen.physicalDotsPerInch()
|
pdpi = screen.physicalDotsPerInch()
|
||||||
ldpi = screen.logicalDotsPerInch()
|
ldpi = screen.logicalDotsPerInch()
|
||||||
dpi = max(pdpi, ldpi)
|
mx_dpi = max(pdpi, ldpi)
|
||||||
|
mn_dpi = min(pdpi, ldpi)
|
||||||
|
scale = round(ldpi/pdpi)
|
||||||
|
|
||||||
# for low dpi scale everything down
|
if mx_dpi <= 97: # for low dpi use larger font sizes
|
||||||
if dpi <= 97:
|
|
||||||
inches = _font_sizes['lo'][self._font_size]
|
inches = _font_sizes['lo'][self._font_size]
|
||||||
else:
|
|
||||||
|
else: # hidpi use smaller font sizes
|
||||||
inches = _font_sizes['hi'][self._font_size]
|
inches = _font_sizes['hi'][self._font_size]
|
||||||
|
|
||||||
|
dpi = mn_dpi
|
||||||
|
|
||||||
|
# dpi is likely somewhat scaled down so use slightly larger font size
|
||||||
|
if scale > 1 and self._font_size:
|
||||||
|
# TODO: this denominator should probably be determined from
|
||||||
|
# relative aspect rations or something?
|
||||||
|
inches = inches * (1 / scale) * (1 + 6/16)
|
||||||
|
dpi = mx_dpi
|
||||||
|
|
||||||
|
self._font_inches = inches
|
||||||
|
|
||||||
font_size = math.floor(inches * dpi)
|
font_size = math.floor(inches * dpi)
|
||||||
log.info(
|
log.info(
|
||||||
f"\nscreen:{screen.name()} with DPI: {dpi}"
|
f"\nscreen:{screen.name()} with pDPI: {pdpi}, lDPI: {ldpi}"
|
||||||
f"\nbest font size is {font_size}\n"
|
f"\nOur best guess font size is {font_size}\n"
|
||||||
)
|
)
|
||||||
|
# apply the size
|
||||||
self._set_qfont_px_size(font_size)
|
self._set_qfont_px_size(font_size)
|
||||||
self._physical_dpi = dpi
|
|
||||||
|
|
||||||
def boundingRect(self, value: str) -> QtCore.QRectF:
|
def boundingRect(self, value: str) -> QtCore.QRectF:
|
||||||
|
|
||||||
|
@ -130,12 +146,20 @@ class DpiAwareFont:
|
||||||
|
|
||||||
# use inches size to be cross-resolution compatible?
|
# use inches size to be cross-resolution compatible?
|
||||||
_font = DpiAwareFont()
|
_font = DpiAwareFont()
|
||||||
|
_font_small = DpiAwareFont(font_size='small')
|
||||||
|
|
||||||
|
|
||||||
|
def _config_fonts_to_screen() -> None:
|
||||||
|
'configure global DPI aware font sizes'
|
||||||
|
|
||||||
|
global _font, _font_small
|
||||||
|
_font.configure_to_dpi()
|
||||||
|
_font_small.configure_to_dpi()
|
||||||
|
|
||||||
|
|
||||||
# TODO: re-compute font size when main widget switches screens?
|
# TODO: re-compute font size when main widget switches screens?
|
||||||
# https://forum.qt.io/topic/54136/how-do-i-get-the-qscreen-my-widget-is-on-qapplication-desktop-screen-returns-a-qwidget-and-qobject_cast-qscreen-returns-null/3
|
# https://forum.qt.io/topic/54136/how-do-i-get-the-qscreen-my-widget-is-on-qapplication-desktop-screen-returns-a-qwidget-and-qobject_cast-qscreen-returns-null/3
|
||||||
|
|
||||||
# _i3_rgba = QtGui.QColor.fromRgbF(*[0.14]*3 + [1])
|
|
||||||
|
|
||||||
# splitter widget config
|
# splitter widget config
|
||||||
_xaxis_at = 'bottom'
|
_xaxis_at = 'bottom'
|
||||||
|
|
||||||
|
@ -175,6 +199,7 @@ def hcolor(name: str) -> str:
|
||||||
'gray': '#808080', # like the kick
|
'gray': '#808080', # like the kick
|
||||||
'grayer': '#4c4c4c',
|
'grayer': '#4c4c4c',
|
||||||
'grayest': '#3f3f3f',
|
'grayest': '#3f3f3f',
|
||||||
|
'i3': '#494D4F',
|
||||||
'jet': '#343434',
|
'jet': '#343434',
|
||||||
'cadet': '#91A3B0',
|
'cadet': '#91A3B0',
|
||||||
'marengo': '#91A3B0',
|
'marengo': '#91A3B0',
|
||||||
|
@ -185,9 +210,13 @@ def hcolor(name: str) -> str:
|
||||||
'bracket': '#666666', # like the logo
|
'bracket': '#666666', # like the logo
|
||||||
'original': '#a9a9a9',
|
'original': '#a9a9a9',
|
||||||
|
|
||||||
# palette
|
# from ``qdarkstyle`` palette
|
||||||
'default': DarkPalette.COLOR_BACKGROUND_NORMAL,
|
'default_darkest': DarkPalette.COLOR_BACKGROUND_1,
|
||||||
'default_light': DarkPalette.COLOR_BACKGROUND_LIGHT,
|
'default_dark': DarkPalette.COLOR_BACKGROUND_2,
|
||||||
|
'default': DarkPalette.COLOR_BACKGROUND_3,
|
||||||
|
'default_light': DarkPalette.COLOR_BACKGROUND_4,
|
||||||
|
'default_lightest': DarkPalette.COLOR_BACKGROUND_5,
|
||||||
|
'default_spotlight': DarkPalette.COLOR_BACKGROUND_6,
|
||||||
|
|
||||||
'white': '#ffffff', # for tinas and sunbathers
|
'white': '#ffffff', # for tinas and sunbathers
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,180 @@
|
||||||
|
# piker: trading gear for hackers
|
||||||
|
# Copyright (C) Tyler Goodlet (in stewardship for piker0)
|
||||||
|
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Qt main window singletons and stuff.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import time
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from pyqtgraph import QtGui
|
||||||
|
from PyQt5 import QtCore
|
||||||
|
from PyQt5.QtGui import QLabel, QStatusBar
|
||||||
|
|
||||||
|
from ..log import get_logger
|
||||||
|
from ._style import _font_small, hcolor
|
||||||
|
|
||||||
|
|
||||||
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiStatus:
|
||||||
|
|
||||||
|
bar: QStatusBar
|
||||||
|
statuses: list[str]
|
||||||
|
|
||||||
|
def __init__(self, bar, statuses) -> None:
|
||||||
|
self.bar = bar
|
||||||
|
self.statuses = statuses
|
||||||
|
|
||||||
|
def open_status(
|
||||||
|
self,
|
||||||
|
msg: str,
|
||||||
|
) -> Callable[..., None]:
|
||||||
|
'''Add a status to the status bar and return a close callback which
|
||||||
|
when called will remove the status ``msg``.
|
||||||
|
|
||||||
|
'''
|
||||||
|
self.statuses.append(msg)
|
||||||
|
|
||||||
|
def remove_msg() -> None:
|
||||||
|
self.statuses.remove(msg)
|
||||||
|
self.render()
|
||||||
|
|
||||||
|
self.render()
|
||||||
|
return remove_msg
|
||||||
|
|
||||||
|
def render(self) -> None:
|
||||||
|
if self.statuses:
|
||||||
|
self.bar.showMessage(f'{" ".join(self.statuses)}')
|
||||||
|
else:
|
||||||
|
self.bar.clearMessage()
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QtGui.QMainWindow):
|
||||||
|
|
||||||
|
size = (800, 500)
|
||||||
|
title = 'piker chart (ur symbol is loading bby)'
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setMinimumSize(*self.size)
|
||||||
|
self.setWindowTitle(self.title)
|
||||||
|
|
||||||
|
self._status_bar: QStatusBar = None
|
||||||
|
self._status_label: QLabel = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mode_label(self) -> QtGui.QLabel:
|
||||||
|
|
||||||
|
# init mode label
|
||||||
|
if not self._status_label:
|
||||||
|
|
||||||
|
self._status_label = label = QtGui.QLabel()
|
||||||
|
label.setStyleSheet(
|
||||||
|
f"QLabel {{ color : {hcolor('gunmetal')}; }}"
|
||||||
|
)
|
||||||
|
label.setTextFormat(3) # markdown
|
||||||
|
label.setFont(_font_small.font)
|
||||||
|
label.setMargin(2)
|
||||||
|
label.setAlignment(
|
||||||
|
QtCore.Qt.AlignVCenter
|
||||||
|
| QtCore.Qt.AlignRight
|
||||||
|
)
|
||||||
|
self.statusBar().addPermanentWidget(label)
|
||||||
|
label.show()
|
||||||
|
|
||||||
|
return self._status_label
|
||||||
|
|
||||||
|
def closeEvent(
|
||||||
|
self,
|
||||||
|
event: QtGui.QCloseEvent,
|
||||||
|
) -> None:
|
||||||
|
"""Cancel the root actor asap.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# raising KBI seems to get intercepted by by Qt so just use the system.
|
||||||
|
os.kill(os.getpid(), signal.SIGINT)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status_bar(self) -> QStatusBar:
|
||||||
|
|
||||||
|
# style and cached the status bar on first access
|
||||||
|
if not self._status_bar:
|
||||||
|
|
||||||
|
sb = self.statusBar()
|
||||||
|
sb.setStyleSheet((
|
||||||
|
f"color : {hcolor('gunmetal')};"
|
||||||
|
f"background : {hcolor('default_dark')};"
|
||||||
|
f"font-size : {_font_small.px_size}px;"
|
||||||
|
"padding : 0px;"
|
||||||
|
# "min-height : 19px;"
|
||||||
|
# "qproperty-alignment: AlignVCenter;"
|
||||||
|
))
|
||||||
|
self.setStatusBar(sb)
|
||||||
|
self._status_bar = MultiStatus(sb, [])
|
||||||
|
|
||||||
|
return self._status_bar
|
||||||
|
|
||||||
|
def on_focus_change(
|
||||||
|
self,
|
||||||
|
old: QtGui.QWidget,
|
||||||
|
new: QtGui.QWidget,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
log.debug(f'widget focus changed from {old} -> {new}')
|
||||||
|
|
||||||
|
if new is not None:
|
||||||
|
# cursor left window?
|
||||||
|
name = getattr(new, 'mode_name', '')
|
||||||
|
self.mode_label.setText(name)
|
||||||
|
|
||||||
|
def current_screen(self) -> QtGui.QScreen:
|
||||||
|
"""Get a frickin screen (if we can, gawd).
|
||||||
|
|
||||||
|
"""
|
||||||
|
app = QtGui.QApplication.instance()
|
||||||
|
|
||||||
|
for _ in range(3):
|
||||||
|
screen = app.screenAt(self.pos())
|
||||||
|
print('trying to access QScreen...')
|
||||||
|
if screen is None:
|
||||||
|
time.sleep(0.5)
|
||||||
|
continue
|
||||||
|
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if screen is None:
|
||||||
|
# try for the first one we can find
|
||||||
|
screen = app.screens()[0]
|
||||||
|
|
||||||
|
assert screen, "Wow Qt is dumb as shit and has no screen..."
|
||||||
|
return screen
|
||||||
|
|
||||||
|
|
||||||
|
# singleton app per actor
|
||||||
|
_qt_win: QtGui.QMainWindow = None
|
||||||
|
|
||||||
|
|
||||||
|
def main_window() -> MainWindow:
|
||||||
|
'Return the actor-global Qt window.'
|
||||||
|
|
||||||
|
global _qt_win
|
||||||
|
assert _qt_win
|
||||||
|
return _qt_win
|
|
@ -309,7 +309,15 @@ async def start_order_mode(
|
||||||
chart: 'ChartPlotWidget', # noqa
|
chart: 'ChartPlotWidget', # noqa
|
||||||
symbol: Symbol,
|
symbol: Symbol,
|
||||||
brokername: str,
|
brokername: str,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
'''Activate chart-trader order mode loop:
|
||||||
|
- connect to emsd
|
||||||
|
- load existing positions
|
||||||
|
- begin order handling loop
|
||||||
|
|
||||||
|
'''
|
||||||
|
done = chart.window().status_bar.open_status('Starting order mode...')
|
||||||
|
|
||||||
# spawn EMS actor-service
|
# spawn EMS actor-service
|
||||||
async with (
|
async with (
|
||||||
|
@ -335,6 +343,7 @@ async def start_order_mode(
|
||||||
return ohlc['index'][-1]
|
return ohlc['index'][-1]
|
||||||
|
|
||||||
# Begin order-response streaming
|
# Begin order-response streaming
|
||||||
|
done()
|
||||||
|
|
||||||
# this is where we receive **back** messages
|
# this is where we receive **back** messages
|
||||||
# about executions **from** the EMS actor
|
# about executions **from** the EMS actor
|
||||||
|
|
11
setup.py
11
setup.py
|
@ -70,17 +70,12 @@ setup(
|
||||||
# UI
|
# UI
|
||||||
'PyQt5',
|
'PyQt5',
|
||||||
'pyqtgraph',
|
'pyqtgraph',
|
||||||
'qdarkstyle==2.8.1',
|
'qdarkstyle >= 3.0.2',
|
||||||
#'kivy', see requirement.txt; using a custom branch atm
|
|
||||||
|
|
||||||
# tsdbs
|
|
||||||
'pymarketstore',
|
|
||||||
|
|
||||||
#'kivy', see requirement.txt; using a custom branch atm
|
|
||||||
|
|
||||||
# fuzzy search
|
# fuzzy search
|
||||||
'fuzzywuzzy[speedup]',
|
'fuzzywuzzy[speedup]',
|
||||||
|
|
||||||
|
# tsdbs
|
||||||
|
'pymarketstore',
|
||||||
],
|
],
|
||||||
tests_require=['pytest'],
|
tests_require=['pytest'],
|
||||||
python_requires=">=3.9", # literally for ``datetime.datetime.fromisoformat``...
|
python_requires=">=3.9", # literally for ``datetime.datetime.fromisoformat``...
|
||||||
|
|
Loading…
Reference in New Issue