commit
e52ecbe589
|
@ -52,7 +52,6 @@ from ._l1 import L1Labels
|
|||
from ._graphics._ohlc import BarItems
|
||||
from ._graphics._curve import FastAppendCurve
|
||||
from ._style import (
|
||||
_font,
|
||||
hcolor,
|
||||
CHART_MARGINS,
|
||||
_xaxis_at,
|
||||
|
@ -68,7 +67,7 @@ from ..data import maybe_open_shm_array
|
|||
from .. import brokers
|
||||
from .. import data
|
||||
from ..log import get_logger
|
||||
from ._exec import run_qtractor, current_screen
|
||||
from ._exec import run_qtractor
|
||||
from ._interaction import ChartView
|
||||
from .order_mode import start_order_mode
|
||||
from .. import fsp
|
||||
|
@ -79,10 +78,11 @@ log = get_logger(__name__)
|
|||
|
||||
|
||||
class ChartSpace(QtGui.QWidget):
|
||||
"""High level widget which contains layouts for organizing
|
||||
lower level charts as well as other widgets used to control
|
||||
or modify them.
|
||||
"""
|
||||
'''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):
|
||||
super().__init__(parent)
|
||||
|
||||
|
@ -431,6 +431,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
_l1_labels: L1Labels = None
|
||||
|
||||
mode_name: str = 'mode: view'
|
||||
|
||||
# TODO: can take a ``background`` color setting - maybe there's
|
||||
# a better one?
|
||||
|
||||
|
@ -1000,7 +1002,7 @@ async def test_bed(
|
|||
|
||||
|
||||
_clear_throttle_rate: int = 60 # Hz
|
||||
_book_throttle_rate: int = 20 # Hz
|
||||
_book_throttle_rate: int = 16 # Hz
|
||||
|
||||
|
||||
async def chart_from_quotes(
|
||||
|
@ -1078,7 +1080,14 @@ async def chart_from_quotes(
|
|||
tick_margin = 2 * tick_size
|
||||
|
||||
last_ask = last_bid = last_clear = time.time()
|
||||
chart.show()
|
||||
|
||||
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():
|
||||
|
||||
now = time.time()
|
||||
|
@ -1291,6 +1300,9 @@ async def run_fsp(
|
|||
This is called once for each entry in the fsp
|
||||
config map.
|
||||
"""
|
||||
done = linked_charts.window().status_bar.open_status(
|
||||
f'loading FSP: {display_name}..')
|
||||
|
||||
async with portal.open_stream_from(
|
||||
|
||||
# subactor entrypoint
|
||||
|
@ -1384,13 +1396,20 @@ async def run_fsp(
|
|||
|
||||
last = time.time()
|
||||
|
||||
done()
|
||||
|
||||
# update chart graphics
|
||||
async for value in stream:
|
||||
|
||||
# chart isn't actively shown so just skip render cycle
|
||||
if chart._lc.isHidden():
|
||||
continue
|
||||
|
||||
now = time.time()
|
||||
period = now - last
|
||||
|
||||
# if period <= 1/30:
|
||||
if period <= 1/_clear_throttle_rate - 0.001:
|
||||
if period <= 1/_clear_throttle_rate:
|
||||
# faster then display refresh rate
|
||||
# print(f'quote too fast: {1/period}')
|
||||
continue
|
||||
|
@ -1479,7 +1498,7 @@ async def check_for_new_bars(feed, ohlcv, linked_charts):
|
|||
|
||||
async def chart_symbol(
|
||||
chart_app: ChartSpace,
|
||||
brokername: str,
|
||||
provider: str,
|
||||
sym: str,
|
||||
loglevel: str,
|
||||
) -> None:
|
||||
|
@ -1489,11 +1508,14 @@ async def chart_symbol(
|
|||
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
|
||||
brokermod = brokers.get_brokermod(brokername)
|
||||
brokermod = brokers.get_brokermod(provider)
|
||||
|
||||
async with data.open_feed(
|
||||
brokername,
|
||||
provider,
|
||||
[sym],
|
||||
loglevel=loglevel,
|
||||
) as feed:
|
||||
|
@ -1526,6 +1548,8 @@ async def chart_symbol(
|
|||
add_label=False,
|
||||
)
|
||||
|
||||
loading_sym_done()
|
||||
|
||||
# size view to data once at outset
|
||||
chart._set_yrange()
|
||||
|
||||
|
@ -1604,7 +1628,7 @@ async def chart_symbol(
|
|||
# linked_charts,
|
||||
# )
|
||||
|
||||
await start_order_mode(chart, symbol, brokername)
|
||||
await start_order_mode(chart, symbol, provider)
|
||||
|
||||
|
||||
async def load_providers(
|
||||
|
@ -1621,7 +1645,7 @@ async def load_providers(
|
|||
# search engines.
|
||||
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
|
||||
portal = await stack.enter_async_context(
|
||||
maybe_spawn_brokerd(
|
||||
|
@ -1645,7 +1669,7 @@ async def load_providers(
|
|||
|
||||
async def _async_main(
|
||||
# implicit required argument provided by ``qtractor_run()``
|
||||
widgets: Dict[str, Any],
|
||||
main_widget: ChartSpace,
|
||||
|
||||
sym: 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
|
||||
screen = current_screen()
|
||||
screen = chart_app.window.current_screen()
|
||||
|
||||
# configure graphics update throttling based on display refresh 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')
|
||||
|
||||
# configure global DPI aware font size
|
||||
_font.configure_to_dpi(screen)
|
||||
# TODO: do styling / themeing setup
|
||||
# _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:
|
||||
|
||||
|
@ -1702,8 +1729,6 @@ async def _async_main(
|
|||
# this internally starts a ``chart_symbol()`` task above
|
||||
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
|
||||
async with _search.register_symbol_search(
|
||||
|
||||
|
@ -1712,8 +1737,14 @@ async def _async_main(
|
|||
_search.search_simple_dict,
|
||||
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
|
||||
async with open_key_stream(
|
||||
search.bar,
|
||||
|
@ -1727,6 +1758,7 @@ async def _async_main(
|
|||
key_stream,
|
||||
)
|
||||
|
||||
starting_done()
|
||||
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.
|
||||
"""
|
||||
from typing import Tuple, Callable, Dict, Any
|
||||
import os
|
||||
import platform
|
||||
import signal
|
||||
import time
|
||||
import traceback
|
||||
|
||||
# Qt specific
|
||||
|
@ -32,20 +29,22 @@ import PyQt5 # noqa
|
|||
import pyqtgraph as pg
|
||||
from pyqtgraph import QtGui
|
||||
from PyQt5 import QtCore
|
||||
# from PyQt5.QtGui import QLabel, QStatusBar
|
||||
from PyQt5.QtCore import (
|
||||
pyqtRemoveInputHook,
|
||||
Qt,
|
||||
QCoreApplication,
|
||||
)
|
||||
import qdarkstyle
|
||||
from qdarkstyle import DarkPalette
|
||||
# import qdarkgraystyle
|
||||
import trio
|
||||
import tractor
|
||||
from outcome import Error
|
||||
|
||||
from .._daemon import maybe_open_pikerd, _tractor_kwargs
|
||||
from ..log import get_logger
|
||||
from ._pg_overrides import _do_overrides
|
||||
from . import _style
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
@ -59,37 +58,6 @@ pg.enableExperimental = True
|
|||
_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:
|
||||
# https://bugreports.qt.io/browse/QTBUG-53022
|
||||
# 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)
|
||||
|
||||
|
||||
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(
|
||||
func: Callable,
|
||||
args: Tuple,
|
||||
main_widget: QtGui.QWidget,
|
||||
tractor_kwargs: Dict[str, Any] = {},
|
||||
window_type: QtGui.QMainWindow = MainWindow,
|
||||
window_type: QtGui.QMainWindow = None,
|
||||
) -> None:
|
||||
# avoids annoying message when entering debugger from qt loop
|
||||
pyqtRemoveInputHook()
|
||||
|
@ -148,10 +95,6 @@ def run_qtractor(
|
|||
# XXX: lmfao, this is how you disable text edit cursor blinking..smh
|
||||
app.setCursorFlashTime(0)
|
||||
|
||||
# set global app singleton
|
||||
global _qt_app
|
||||
_qt_app = app
|
||||
|
||||
# This code is from Nathaniel, and I quote:
|
||||
# "This is substantially faster than using a signal... for some
|
||||
# reason Qt signal dispatch is really slow (and relies on events
|
||||
|
@ -188,19 +131,33 @@ def run_qtractor(
|
|||
app.quit()
|
||||
|
||||
# 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
|
||||
from . import _window
|
||||
|
||||
if window_type is None:
|
||||
window_type = _window.MainWindow
|
||||
|
||||
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.window = window
|
||||
|
||||
widgets = {
|
||||
'window': window,
|
||||
'main': instance,
|
||||
}
|
||||
|
||||
|
||||
# override tractor's defaults
|
||||
tractor_kwargs.update(_tractor_kwargs)
|
||||
|
||||
|
@ -210,7 +167,7 @@ def run_qtractor(
|
|||
async with maybe_open_pikerd(
|
||||
**tractor_kwargs,
|
||||
):
|
||||
await func(*((widgets,) + args))
|
||||
await func(*((instance,) + args))
|
||||
|
||||
# guest mode entry
|
||||
trio.lowlevel.start_guest_run(
|
||||
|
@ -223,11 +180,6 @@ def run_qtractor(
|
|||
window.main_widget = main_widget
|
||||
window.setCentralWidget(instance)
|
||||
|
||||
# store global ref
|
||||
# set global app singleton
|
||||
global _qt_win
|
||||
_qt_win = window
|
||||
|
||||
# actually render to screen
|
||||
window.show()
|
||||
app.exec_()
|
||||
|
|
|
@ -31,6 +31,7 @@ from .._style import (
|
|||
_xaxis_at,
|
||||
hcolor,
|
||||
_font,
|
||||
_font_small,
|
||||
)
|
||||
from .._axes import YAxisLabel, XAxisLabel
|
||||
from ...log import get_logger
|
||||
|
@ -109,7 +110,7 @@ class LineDot(pg.CurvePoint):
|
|||
return False
|
||||
|
||||
|
||||
# TODO: change this into our own ``Label``
|
||||
# TODO: change this into our own ``_label.Label``
|
||||
class ContentsLabel(pg.LabelItem):
|
||||
"""Label anchored to a ``ViewBox`` typically for displaying
|
||||
datum-wise points from the "viewed" contents.
|
||||
|
@ -138,22 +139,14 @@ class ContentsLabel(pg.LabelItem):
|
|||
justify_text: str = 'left',
|
||||
font_size: Optional[int] = None,
|
||||
) -> None:
|
||||
font_size = font_size or _font.font.pixelSize()
|
||||
|
||||
font_size = font_size or _font_small.px_size
|
||||
|
||||
super().__init__(
|
||||
justify=justify_text,
|
||||
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
|
||||
self.setParentItem(chart._vb)
|
||||
chart.scene().addItem(self)
|
||||
|
@ -165,7 +158,7 @@ class ContentsLabel(pg.LabelItem):
|
|||
|
||||
ydim = 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)
|
||||
|
||||
|
|
|
@ -467,6 +467,9 @@ class ChartView(ViewBox):
|
|||
- zoom on right-click-n-drag to cursor position
|
||||
|
||||
"""
|
||||
|
||||
mode_name: str = 'mode: view'
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: pg.PlotItem = None,
|
||||
|
|
|
@ -96,6 +96,8 @@ class SimpleDelegate(QStyledItemDelegate):
|
|||
|
||||
class CompleterView(QTreeView):
|
||||
|
||||
mode_name: str = 'mode: search-nav'
|
||||
|
||||
# XXX: relevant docs links:
|
||||
# - simple widget version of this:
|
||||
# https://doc.qt.io/qt-5/qtreewidget.html#details
|
||||
|
@ -153,13 +155,14 @@ class CompleterView(QTreeView):
|
|||
self._font_size: int = 0 # pixels
|
||||
|
||||
def on_pressed(self, idx: QModelIndex) -> None:
|
||||
'''Mouse pressed on view handler.
|
||||
|
||||
'''
|
||||
search = self.parent()
|
||||
search.chart_current_item(clear_to_cache=False)
|
||||
search.focus()
|
||||
|
||||
def set_font_size(self, size: int = 18):
|
||||
# dpi_px_size = _font.px_size
|
||||
# print(size)
|
||||
if size < 0:
|
||||
size = 16
|
||||
|
@ -424,6 +427,8 @@ class CompleterView(QTreeView):
|
|||
|
||||
class SearchBar(QtWidgets.QLineEdit):
|
||||
|
||||
mode_name: str = 'mode: search'
|
||||
|
||||
def __init__(
|
||||
|
||||
self,
|
||||
|
@ -487,6 +492,8 @@ class SearchWidget(QtGui.QWidget):
|
|||
Includes helper methods for item management in the sub-widgets.
|
||||
|
||||
'''
|
||||
mode_name: str = 'mode: search'
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
chart_space: 'ChartSpace', # type: ignore # noqa
|
||||
|
@ -499,7 +506,7 @@ class SearchWidget(QtGui.QWidget):
|
|||
# size it as we specify
|
||||
self.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Fixed,
|
||||
QtWidgets.QSizePolicy.Expanding,
|
||||
QtWidgets.QSizePolicy.Fixed,
|
||||
)
|
||||
|
||||
self.chart_app = chart_space
|
||||
|
@ -618,13 +625,15 @@ class SearchWidget(QtGui.QWidget):
|
|||
# making?)
|
||||
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:
|
||||
|
||||
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(
|
||||
'cache',
|
||||
values=list(reversed(chart._chart_cache)),
|
||||
|
@ -692,8 +701,10 @@ async def fill_results(
|
|||
recv_chan: trio.abc.ReceiveChannel,
|
||||
|
||||
# kb debouncing pauses (bracket defaults)
|
||||
min_pause_time: float = 0.1,
|
||||
max_pause_time: float = 6/16,
|
||||
min_pause_time: float = 0.01, # absolute min typing throttle
|
||||
|
||||
# max pause required before slow relay
|
||||
max_pause_time: float = 6/16 + 0.001,
|
||||
|
||||
) -> None:
|
||||
"""Task to search through providers and fill in possible
|
||||
|
@ -742,11 +753,6 @@ async def fill_results(
|
|||
_search_active = trio.Event()
|
||||
break
|
||||
|
||||
if repeats > 2 and period >= max_pause_time:
|
||||
_search_active = trio.Event()
|
||||
repeats = 0
|
||||
break
|
||||
|
||||
if text == last_text:
|
||||
repeats += 1
|
||||
|
||||
|
@ -754,9 +760,8 @@ async def fill_results(
|
|||
# print('search currently disabled')
|
||||
break
|
||||
|
||||
log.debug(f'Search req for {text}')
|
||||
|
||||
already_has_results = has_results[text]
|
||||
log.debug(f'Search req for {text}')
|
||||
|
||||
# issue multi-provider fan-out search request and place
|
||||
# "searching.." statuses on outstanding results providers
|
||||
|
@ -765,16 +770,22 @@ async def fill_results(
|
|||
for provider, (search, pause) in (
|
||||
_searcher_cache.copy().items()
|
||||
):
|
||||
|
||||
if provider != 'cache':
|
||||
view.clear_section(
|
||||
provider, status_field='-> searchin..')
|
||||
|
||||
# only conduct search on this backend if it's
|
||||
# registered for the corresponding pause period.
|
||||
# XXX: only conduct search on this backend if it's
|
||||
# registered for the corresponding pause period AND
|
||||
# it hasn't already been searched with the current
|
||||
# input pattern (in which case just look up the old
|
||||
# results).
|
||||
if (period >= pause) and (
|
||||
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(
|
||||
pack_matches,
|
||||
view,
|
||||
|
@ -786,6 +797,14 @@ async def fill_results(
|
|||
)
|
||||
else: # already has results for this input 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':
|
||||
view.set_section_entries(
|
||||
section=provider,
|
||||
|
@ -794,6 +813,11 @@ async def fill_results(
|
|||
else:
|
||||
view.clear_section(provider)
|
||||
|
||||
if repeats > 2 and period > max_pause_time:
|
||||
_search_active = trio.Event()
|
||||
repeats = 0
|
||||
break
|
||||
|
||||
bar.show()
|
||||
|
||||
|
||||
|
@ -952,7 +976,7 @@ async def register_symbol_search(
|
|||
|
||||
global _searcher_cache
|
||||
|
||||
pause_period = pause_period or 0.125
|
||||
pause_period = pause_period or 0.1
|
||||
|
||||
# deliver search func to consumer
|
||||
try:
|
||||
|
|
|
@ -22,18 +22,19 @@ import math
|
|||
|
||||
import pyqtgraph as pg
|
||||
from PyQt5 import QtCore, QtGui
|
||||
from qdarkstyle.palette import DarkPalette
|
||||
from qdarkstyle import DarkPalette
|
||||
|
||||
from ..log import get_logger
|
||||
from ._exec import current_screen
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
_magic_inches = 0.0666 * (1 + 6/16)
|
||||
|
||||
# chart-wide fonts specified in inches
|
||||
_font_sizes: Dict[str, Dict[str, float]] = {
|
||||
'hi': {
|
||||
'default': 0.0616,
|
||||
'small': 0.055,
|
||||
'default': _magic_inches,
|
||||
'small': 0.9 * _magic_inches,
|
||||
},
|
||||
'lo': {
|
||||
'default': 6.5 / 64,
|
||||
|
@ -43,6 +44,7 @@ _font_sizes: Dict[str, Dict[str, float]] = {
|
|||
|
||||
|
||||
class DpiAwareFont:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
# TODO: move to config
|
||||
|
@ -52,10 +54,10 @@ class DpiAwareFont:
|
|||
) -> None:
|
||||
self.name = name
|
||||
self._qfont = QtGui.QFont(name)
|
||||
# self._iwl = size_in_inches or _default_font_inches_we_like
|
||||
self._font_size: str = font_size
|
||||
self._qfm = QtGui.QFontMetrics(self._qfont)
|
||||
self._physical_dpi = None
|
||||
self._font_inches: float = None
|
||||
self._screen = None
|
||||
|
||||
def _set_qfont_px_size(self, px_size: int) -> None:
|
||||
|
@ -64,13 +66,15 @@ class DpiAwareFont:
|
|||
|
||||
@property
|
||||
def screen(self) -> QtGui.QScreen:
|
||||
from ._window import main_window
|
||||
|
||||
if self._screen is not None:
|
||||
try:
|
||||
self._screen.refreshRate()
|
||||
except RuntimeError:
|
||||
self._screen = current_screen()
|
||||
self._screen = main_window().current_screen()
|
||||
else:
|
||||
self._screen = current_screen()
|
||||
self._screen = main_window().current_screen()
|
||||
|
||||
return self._screen
|
||||
|
||||
|
@ -95,22 +99,34 @@ class DpiAwareFont:
|
|||
# take the max since scaling can make things ugly in some cases
|
||||
pdpi = screen.physicalDotsPerInch()
|
||||
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 dpi <= 97:
|
||||
if mx_dpi <= 97: # for low dpi use larger font sizes
|
||||
inches = _font_sizes['lo'][self._font_size]
|
||||
else:
|
||||
|
||||
else: # hidpi use smaller font sizes
|
||||
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)
|
||||
log.info(
|
||||
f"\nscreen:{screen.name()} with DPI: {dpi}"
|
||||
f"\nbest font size is {font_size}\n"
|
||||
f"\nscreen:{screen.name()} with pDPI: {pdpi}, lDPI: {ldpi}"
|
||||
f"\nOur best guess font size is {font_size}\n"
|
||||
)
|
||||
|
||||
# apply the size
|
||||
self._set_qfont_px_size(font_size)
|
||||
self._physical_dpi = dpi
|
||||
|
||||
def boundingRect(self, value: str) -> QtCore.QRectF:
|
||||
|
||||
|
@ -130,12 +146,20 @@ class DpiAwareFont:
|
|||
|
||||
# use inches size to be cross-resolution compatible?
|
||||
_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?
|
||||
# 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
|
||||
_xaxis_at = 'bottom'
|
||||
|
||||
|
@ -175,6 +199,7 @@ def hcolor(name: str) -> str:
|
|||
'gray': '#808080', # like the kick
|
||||
'grayer': '#4c4c4c',
|
||||
'grayest': '#3f3f3f',
|
||||
'i3': '#494D4F',
|
||||
'jet': '#343434',
|
||||
'cadet': '#91A3B0',
|
||||
'marengo': '#91A3B0',
|
||||
|
@ -185,9 +210,13 @@ def hcolor(name: str) -> str:
|
|||
'bracket': '#666666', # like the logo
|
||||
'original': '#a9a9a9',
|
||||
|
||||
# palette
|
||||
'default': DarkPalette.COLOR_BACKGROUND_NORMAL,
|
||||
'default_light': DarkPalette.COLOR_BACKGROUND_LIGHT,
|
||||
# from ``qdarkstyle`` palette
|
||||
'default_darkest': DarkPalette.COLOR_BACKGROUND_1,
|
||||
'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
|
||||
|
||||
|
|
|
@ -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
|
||||
symbol: Symbol,
|
||||
brokername: str,
|
||||
|
||||
) -> 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
|
||||
async with (
|
||||
|
@ -335,6 +343,7 @@ async def start_order_mode(
|
|||
return ohlc['index'][-1]
|
||||
|
||||
# Begin order-response streaming
|
||||
done()
|
||||
|
||||
# this is where we receive **back** messages
|
||||
# about executions **from** the EMS actor
|
||||
|
|
11
setup.py
11
setup.py
|
@ -70,17 +70,12 @@ setup(
|
|||
# UI
|
||||
'PyQt5',
|
||||
'pyqtgraph',
|
||||
'qdarkstyle==2.8.1',
|
||||
#'kivy', see requirement.txt; using a custom branch atm
|
||||
|
||||
# tsdbs
|
||||
'pymarketstore',
|
||||
|
||||
#'kivy', see requirement.txt; using a custom branch atm
|
||||
|
||||
'qdarkstyle >= 3.0.2',
|
||||
# fuzzy search
|
||||
'fuzzywuzzy[speedup]',
|
||||
|
||||
# tsdbs
|
||||
'pymarketstore',
|
||||
],
|
||||
tests_require=['pytest'],
|
||||
python_requires=">=3.9", # literally for ``datetime.datetime.fromisoformat``...
|
||||
|
|
Loading…
Reference in New Issue