Merge pull request #255 from pikers/multichart_ux_improvements

Multichart ux improvements
py3.10_support
goodboy 2022-01-25 07:56:04 -05:00 committed by GitHub
commit ff8c33cf7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 216 additions and 190 deletions

View File

@ -18,6 +18,7 @@
High level chart-widget apis. High level chart-widget apis.
''' '''
from __future__ import annotations
from typing import Optional from typing import Optional
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
@ -68,7 +69,7 @@ log = get_logger(__name__)
class GodWidget(QWidget): class GodWidget(QWidget):
''' '''
"Our lord and savior, the holy child of window-shua, there is no "Our lord and savior, the holy child of window-shua, there is no
widget above thee." - 6||6 widget above thee." - 6|6
The highest level composed widget which contains layouts for The highest level composed widget which contains layouts for
organizing charts as well as other sub-widgets used to control or organizing charts as well as other sub-widgets used to control or
@ -104,8 +105,8 @@ class GodWidget(QWidget):
# self.init_strategy_ui() # self.init_strategy_ui()
# self.vbox.addLayout(self.hbox) # self.vbox.addLayout(self.hbox)
self._chart_cache = {} self._chart_cache: dict[str, LinkedSplits] = {}
self.linkedsplits: 'LinkedSplits' = None self.linkedsplits: Optional[LinkedSplits] = None
# assigned in the startup func `_async_main()` # assigned in the startup func `_async_main()`
self._root_n: trio.Nursery = None self._root_n: trio.Nursery = None
@ -135,7 +136,7 @@ class GodWidget(QWidget):
def set_chart_symbol( def set_chart_symbol(
self, self,
symbol_key: str, # of form <fqsn>.<providername> symbol_key: str, # of form <fqsn>.<providername>
linkedsplits: 'LinkedSplits', # type: ignore linkedsplits: LinkedSplits, # type: ignore
) -> None: ) -> None:
# re-sort org cache symbol list in LIFO order # re-sort org cache symbol list in LIFO order
@ -146,20 +147,20 @@ class GodWidget(QWidget):
def get_chart_symbol( def get_chart_symbol(
self, self,
symbol_key: str, symbol_key: str,
) -> 'LinkedSplits': # type: ignore
) -> LinkedSplits: # type: ignore
return self._chart_cache.get(symbol_key) return self._chart_cache.get(symbol_key)
async def load_symbol( async def load_symbol(
self, self,
providername: str, providername: str,
symbol_key: str, symbol_key: str,
loglevel: str, loglevel: str,
reset: bool = False, reset: bool = False,
) -> trio.Event: ) -> trio.Event:
'''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.
@ -178,6 +179,7 @@ class GodWidget(QWidget):
# XXX: this is CRITICAL especially with pixel buffer caching # XXX: this is CRITICAL especially with pixel buffer caching
self.linkedsplits.hide() self.linkedsplits.hide()
self.linkedsplits.unfocus()
# XXX: pretty sure we don't need this # XXX: pretty sure we don't need this
# remove any existing plots? # remove any existing plots?
@ -202,6 +204,11 @@ class GodWidget(QWidget):
) )
self.set_chart_symbol(fqsn, linkedsplits) self.set_chart_symbol(fqsn, linkedsplits)
self.vbox.addWidget(linkedsplits)
linkedsplits.show()
linkedsplits.focus()
await trio.sleep(0)
else: else:
# symbol is already loaded and ems ready # symbol is already loaded and ems ready
@ -215,21 +222,17 @@ class GodWidget(QWidget):
# also switch it over to the new chart's interal-layout # also switch it over to the new chart's interal-layout
# self.linkedsplits.chart.qframe.hbox.removeWidget(self.pp_pane) # self.linkedsplits.chart.qframe.hbox.removeWidget(self.pp_pane)
chart = linkedsplits.chart chart = linkedsplits.chart
await chart.resume_all_feeds()
# chart is already in memory so just focus it # chart is already in memory so just focus it
if self.linkedsplits:
self.linkedsplits.unfocus()
self.vbox.addWidget(linkedsplits)
linkedsplits.show() linkedsplits.show()
linkedsplits.focus() linkedsplits.focus()
await trio.sleep(0)
# resume feeds *after* rendering chart view asap
chart.resume_all_feeds()
self.linkedsplits = linkedsplits self.linkedsplits = linkedsplits
symbol = linkedsplits.symbol symbol = linkedsplits.symbol
if symbol is not None: if symbol is not None:
self.window.setWindowTitle( self.window.setWindowTitle(
f'{symbol.key}@{symbol.brokers} ' f'{symbol.key}@{symbol.brokers} '
@ -239,7 +242,8 @@ class GodWidget(QWidget):
return order_mode_started return order_mode_started
def focus(self) -> None: def focus(self) -> None:
'''Focus the top level widget which in turn focusses the chart '''
Focus the top level widget which in turn focusses the chart
ala "view mode". ala "view mode".
''' '''
@ -247,9 +251,19 @@ class GodWidget(QWidget):
self.clearFocus() self.clearFocus()
self.linkedsplits.chart.setFocus() self.linkedsplits.chart.setFocus()
def resizeEvent(self, event: QtCore.QEvent) -> None:
'''
Top level god widget resize handler.
Where we do UX magic to make things not suck B)
'''
log.debug('god widget resize')
class ChartnPane(QFrame): class ChartnPane(QFrame):
'''One-off ``QFrame`` composite which pairs a chart '''
One-off ``QFrame`` composite which pairs a chart
+ sidepane (often a ``FieldsForm`` + other widgets if + sidepane (often a ``FieldsForm`` + other widgets if
provided) forming a, sort of, "chart row" with a side panel provided) forming a, sort of, "chart row" with a side panel
for configuration and display of off-chart data. for configuration and display of off-chart data.
@ -280,8 +294,6 @@ class ChartnPane(QFrame):
hbox.setContentsMargins(0, 0, 0, 0) hbox.setContentsMargins(0, 0, 0, 0)
hbox.setSpacing(3) hbox.setSpacing(3)
# self.setMaximumWidth()
class LinkedSplits(QWidget): class LinkedSplits(QWidget):
''' '''
@ -349,7 +361,7 @@ class LinkedSplits(QWidget):
if not prop: if not prop:
# proportion allocated to consumer subcharts # proportion allocated to consumer subcharts
if ln < 2: if ln < 2:
prop = 1/(.666 * 6) prop = 1/3
elif ln >= 2: elif ln >= 2:
prop = 3/8 prop = 3/8
@ -379,16 +391,21 @@ class LinkedSplits(QWidget):
style: str = 'bar', style: str = 'bar',
) -> 'ChartPlotWidget': ) -> 'ChartPlotWidget':
"""Start up and show main (price) chart and all linked subcharts. '''Start up and show main (price) chart and all linked subcharts.
The data input struct array must include OHLC fields. The data input struct array must include OHLC fields.
"""
'''
# add crosshairs # add crosshairs
self.cursor = Cursor( self.cursor = Cursor(
linkedsplits=self, linkedsplits=self,
digits=symbol.tick_size_digits, digits=symbol.tick_size_digits,
) )
# NOTE: atm the first (and only) OHLC price chart for the symbol
# is given a special reference but in the future there shouldn't
# be no distinction since we will have multiple symbols per
# view as part of "aggregate feeds".
self.chart = self.add_plot( self.chart = self.add_plot(
name=symbol.key, name=symbol.key,
@ -430,9 +447,7 @@ class LinkedSplits(QWidget):
**cpw_kwargs, **cpw_kwargs,
) -> 'ChartPlotWidget': ) -> 'ChartPlotWidget':
'''Add (sub)plots to chart widget by name. '''Add (sub)plots to chart widget by key.
If ``name`` == ``"main"`` the chart will be the the primary view.
''' '''
if self.chart is None and not _is_main: if self.chart is None and not _is_main:
@ -457,7 +472,10 @@ class LinkedSplits(QWidget):
self.xaxis.hide() self.xaxis.hide()
self.xaxis = xaxis self.xaxis = xaxis
qframe = ChartnPane(sidepane=sidepane, parent=self.splitter) qframe = ChartnPane(
sidepane=sidepane,
parent=self.splitter,
)
cpw = ChartPlotWidget( cpw = ChartPlotWidget(
# this name will be used to register the primary # this name will be used to register the primary
@ -551,17 +569,23 @@ class LinkedSplits(QWidget):
else: else:
assert style == 'bar', 'main chart must be OHLC' assert style == 'bar', 'main chart must be OHLC'
self.resize_sidepanes()
return cpw return cpw
def resize_sidepanes( def resize_sidepanes(
self, self,
) -> None: ) -> None:
'''Size all sidepanes based on the OHLC "main" plot. '''
Size all sidepanes based on the OHLC "main" plot and its
sidepane width.
''' '''
main_chart = self.chart
if main_chart:
sp_w = main_chart.sidepane.width()
for name, cpw in self.subplots.items(): for name, cpw in self.subplots.items():
cpw.sidepane.setMinimumWidth(self.chart.sidepane.width()) cpw.sidepane.setMinimumWidth(sp_w)
cpw.sidepane.setMaximumWidth(self.chart.sidepane.width()) cpw.sidepane.setMaximumWidth(sp_w)
class ChartPlotWidget(pg.PlotWidget): class ChartPlotWidget(pg.PlotWidget):
@ -600,12 +624,18 @@ class ChartPlotWidget(pg.PlotWidget):
view_color: str = 'papas_special', view_color: str = 'papas_special',
pen_color: str = 'bracket', pen_color: str = 'bracket',
# TODO: load from config
use_open_gl: bool = False,
static_yrange: Optional[tuple[float, float]] = None, static_yrange: Optional[tuple[float, float]] = None,
**kwargs, **kwargs,
): ):
"""Configure chart display settings. '''
""" Configure initial display settings and connect view callback
handlers.
'''
self.view_color = view_color self.view_color = view_color
self.pen_color = pen_color self.pen_color = pen_color
@ -614,9 +644,9 @@ class ChartPlotWidget(pg.PlotWidget):
# parent=None, # parent=None,
# plotItem=None, # plotItem=None,
# antialias=True, # antialias=True,
# useOpenGL=True,
**kwargs **kwargs
) )
self.useOpenGL(use_open_gl)
self.name = name self.name = name
self.data_key = data_key self.data_key = data_key
self.linked = linkedsplits self.linked = linkedsplits
@ -665,13 +695,13 @@ class ChartPlotWidget(pg.PlotWidget):
# for when the splitter(s) are resized # for when the splitter(s) are resized
self._vb.sigResized.connect(self._set_yrange) self._vb.sigResized.connect(self._set_yrange)
async def resume_all_feeds(self): def resume_all_feeds(self):
for feed in self._feeds.values(): for feed in self._feeds.values():
await feed.resume() self.linked.godwidget._root_n.start_soon(feed.resume)
async def pause_all_feeds(self): def pause_all_feeds(self):
for feed in self._feeds.values(): for feed in self._feeds.values():
await feed.pause() self.linked.godwidget._root_n.start_soon(feed.pause)
@property @property
def view(self) -> ChartView: def view(self) -> ChartView:
@ -910,46 +940,50 @@ class ChartPlotWidget(pg.PlotWidget):
def update_ohlc_from_array( def update_ohlc_from_array(
self, self,
name: str,
graphics_name: str,
array: np.ndarray, array: np.ndarray,
**kwargs, **kwargs,
) -> pg.GraphicsObject:
"""Update the named internal graphics from ``array``.
""" ) -> pg.GraphicsObject:
'''Update the named internal graphics from ``array``.
'''
self._arrays['ohlc'] = array self._arrays['ohlc'] = array
graphics = self._graphics[name] graphics = self._graphics[graphics_name]
graphics.update_from_array(array, **kwargs) graphics.update_from_array(array, **kwargs)
return graphics return graphics
def update_curve_from_array( def update_curve_from_array(
self, self,
name: str, graphics_name: str,
array: np.ndarray, array: np.ndarray,
array_key: Optional[str] = None, array_key: Optional[str] = None,
**kwargs, **kwargs,
) -> pg.GraphicsObject: ) -> pg.GraphicsObject:
"""Update the named internal graphics from ``array``. '''Update the named internal graphics from ``array``.
""" '''
assert len(array)
data_key = array_key or graphics_name
data_key = array_key or name if graphics_name not in self._overlays:
if name not in self._overlays:
self._arrays['ohlc'] = array self._arrays['ohlc'] = array
else: else:
self._arrays[data_key] = array self._arrays[data_key] = array
curve = self._graphics[name] curve = self._graphics[graphics_name]
if len(array): # NOTE: back when we weren't implementing the curve graphics
# TODO: we should instead implement a diff based # ourselves you'd have updates using this method:
# "only update with new items" on the pg.PlotCurveItem # curve.setData(y=array[graphics_name], x=array['index'], **kwargs)
# one place to dig around this might be the `QBackingStore`
# https://doc.qt.io/qt-5/qbackingstore.html # NOTE: graphics **must** implement a diff based update
# curve.setData(y=array[name], x=array['index'], **kwargs) # operation where an internal ``FastUpdateCurve._xrange`` is
# used to determine if the underlying path needs to be
# pre/ap-pended.
curve.update_from_array( curve.update_from_array(
x=array['index'], x=array['index'],
y=array[data_key], y=array[data_key],
@ -964,7 +998,11 @@ class ChartPlotWidget(pg.PlotWidget):
yrange: Optional[tuple[float, float]] = None, yrange: Optional[tuple[float, float]] = None,
range_margin: float = 0.06, range_margin: float = 0.06,
bars_range: Optional[tuple[int, int, int, int]] = None bars_range: Optional[tuple[int, int, int, int]] = None,
# flag to prevent triggering sibling charts from the same linked
# set from recursion errors.
autoscale_linked_plots: bool = True,
) -> None: ) -> None:
'''Set the viewable y-range based on embedded data. '''Set the viewable y-range based on embedded data.
@ -991,50 +1029,33 @@ class ChartPlotWidget(pg.PlotWidget):
l, lbar, rbar, r = bars_range or self.bars_range() l, lbar, rbar, r = bars_range or self.bars_range()
# TODO: we need a loop for auto-scaled subplots to all if autoscale_linked_plots:
# be triggered by one another # avoid recursion by sibling plots
if self.name != 'volume': linked = self.linked
vlm_chart = self.linked.subplots.get('volume') plots = list(linked.subplots.copy().values())
if vlm_chart: main = linked.chart
vlm_chart._set_yrange(bars_range=(l, lbar, rbar, r)) if main:
# curve = vlm_chart._graphics['volume'] plots.append(main)
# if rbar - lbar < 1500:
# # print('small range')
# curve._fill = True
# else:
# curve._fill = False
# figure out x-range in view such that user can scroll "off" for chart in plots:
# the data set up to the point where ``_min_points_to_show`` if chart and not chart._static_yrange:
# are left. chart._set_yrange(
# view_len = r - l bars_range=(l, lbar, rbar, r),
autoscale_linked_plots=False,
)
# TODO: logic to check if end of bars in view # TODO: logic to check if end of bars in view
# extra = view_len - _min_points_to_show # extra = view_len - _min_points_to_show
# begin = self._arrays['ohlc'][0]['index'] - extra # begin = self._arrays['ohlc'][0]['index'] - extra
# # end = len(self._arrays['ohlc']) - 1 + extra # # end = len(self._arrays['ohlc']) - 1 + extra
# end = self._arrays['ohlc'][-1]['index'] - 1 + extra # end = self._arrays['ohlc'][-1]['index'] - 1 + extra
# XXX: test code for only rendering lines for the bars in view.
# This turns out to be very very poor perf when scaling out to
# many bars (think > 1k) on screen.
# name = self.name
# bars = self._graphics[self.name]
# bars.draw_lines(
# istart=max(lbar, l), iend=min(rbar, r), just_history=True)
# bars_len = rbar - lbar # bars_len = rbar - lbar
# log.debug( # log.debug(
# f"\nl: {l}, lbar: {lbar}, rbar: {rbar}, r: {r}\n" # f"\nl: {l}, lbar: {lbar}, rbar: {rbar}, r: {r}\n"
# f"view_len: {view_len}, bars_len: {bars_len}\n" # f"view_len: {view_len}, bars_len: {bars_len}\n"
# f"begin: {begin}, end: {end}, extra: {extra}" # f"begin: {begin}, end: {end}, extra: {extra}"
# ) # )
# self._set_xlimits(begin, end)
# TODO: this should be some kind of numpy view api
# bars = self._arrays['ohlc'][lbar:rbar]
a = self._arrays['ohlc'] a = self._arrays['ohlc']
ifirst = a[0]['index'] ifirst = a[0]['index']

View File

@ -36,7 +36,7 @@ import trio
from .. import brokers from .. import brokers
from .._cacheables import maybe_open_context from .._cacheables import maybe_open_context
from ..trionics import async_enter_all from tractor.trionics import gather_contexts
from ..data.feed import open_feed, Feed from ..data.feed import open_feed, Feed
from ._chart import ( from ._chart import (
ChartPlotWidget, ChartPlotWidget,
@ -61,7 +61,10 @@ log = get_logger(__name__)
_quote_throttle_rate: int = 58 # Hz _quote_throttle_rate: int = 58 # Hz
def try_read(array: np.ndarray) -> Optional[np.ndarray]: def try_read(
array: np.ndarray
) -> Optional[np.ndarray]:
''' '''
Try to read the last row from a shared mem array or ``None`` Try to read the last row from a shared mem array or ``None``
if the array read returns a zero-length array result. if the array read returns a zero-length array result.
@ -85,7 +88,6 @@ def try_read(array: np.ndarray) -> Optional[np.ndarray]:
# something we need anyway, maybe there should be some kind of # something we need anyway, maybe there should be some kind of
# signal that a prepend is taking place and this consumer can # signal that a prepend is taking place and this consumer can
# respond (eg. redrawing graphics) accordingly. # respond (eg. redrawing graphics) accordingly.
log.warning(f'Read-race on shm array: {graphics_name}@{shm.token}')
# the array read was emtpy # the array read was emtpy
return None return None
@ -101,8 +103,10 @@ def update_fsp_chart(
array = shm.array array = shm.array
last_row = try_read(array) last_row = try_read(array)
# guard against unreadable case # guard against unreadable case
if not last_row: if not last_row:
log.warning(f'Read-race on shm array: {graphics_name}@{shm.token}')
return return
# update graphics # update graphics
@ -175,7 +179,6 @@ def chart_maxmin(
async def update_chart_from_quotes( async def update_chart_from_quotes(
linked: LinkedSplits, linked: LinkedSplits,
stream: tractor.MsgStream, stream: tractor.MsgStream,
ohlcv: np.ndarray, ohlcv: np.ndarray,
@ -247,24 +250,23 @@ async def update_chart_from_quotes(
chart.show() chart.show()
last_quote = time.time() last_quote = time.time()
# NOTE: all code below this loop is expected to be synchronous
# and thus draw instructions are not picked up jntil the next
# wait / iteration.
async for quotes in stream: async for quotes in stream:
now = time.time() now = time.time()
quote_period = now - last_quote quote_period = time.time() - last_quote
quote_rate = round(1/quote_period, 1) if quote_period else float('inf') quote_rate = round(
1/quote_period, 1) if quote_period > 0 else float('inf')
if ( if (
quote_period <= 1/_quote_throttle_rate quote_period <= 1/_quote_throttle_rate
and quote_rate > _quote_throttle_rate + 2 and quote_rate > _quote_throttle_rate * 1.5
): ):
log.warning(f'High quote rate {symbol.key}: {quote_rate}') log.warning(f'High quote rate {symbol.key}: {quote_rate}')
last_quote = now last_quote = now
# chart isn't active/shown so skip render cycle and pause feed(s) # chart isn't active/shown so skip render cycle and pause feed(s)
if chart.linked.isHidden(): if chart.linked.isHidden():
await chart.pause_all_feeds() chart.pause_all_feeds()
continue continue
for sym, quote in quotes.items(): for sym, quote in quotes.items():
@ -454,7 +456,8 @@ def maybe_mk_fsp_shm(
readonly: bool = True, readonly: bool = True,
) -> (ShmArray, bool): ) -> (ShmArray, bool):
'''Allocate a single row shm array for an symbol-fsp pair if none '''
Allocate a single row shm array for an symbol-fsp pair if none
exists, otherwise load the shm already existing for that token. exists, otherwise load the shm already existing for that token.
''' '''
@ -481,7 +484,6 @@ def maybe_mk_fsp_shm(
@acm @acm
async def open_fsp_sidepane( async def open_fsp_sidepane(
linked: LinkedSplits, linked: LinkedSplits,
conf: dict[str, dict[str, str]], conf: dict[str, dict[str, str]],
@ -570,6 +572,7 @@ async def open_fsp_cluster(
async def maybe_open_fsp_cluster( async def maybe_open_fsp_cluster(
workers: int = 2, workers: int = 2,
**kwargs, **kwargs,
) -> AsyncGenerator[int, dict[str, tractor.Portal]]: ) -> AsyncGenerator[int, dict[str, tractor.Portal]]:
kwargs.update( kwargs.update(
@ -589,7 +592,6 @@ async def maybe_open_fsp_cluster(
async def start_fsp_displays( async def start_fsp_displays(
cluster_map: dict[str, tractor.Portal], cluster_map: dict[str, tractor.Portal],
linkedsplits: LinkedSplits, linkedsplits: LinkedSplits,
fsps: dict[str, str], fsps: dict[str, str],
@ -599,11 +601,9 @@ async def start_fsp_displays(
group_status_key: str, group_status_key: str,
loglevel: str, loglevel: str,
# this con
display_in_own_task: bool = False,
) -> None: ) -> None:
'''Create sub-actors (under flat tree) '''
Create sub-actors (under flat tree)
for each entry in config and attach to local graphics update tasks. for each entry in config and attach to local graphics update tasks.
Pass target entrypoint and historical data. Pass target entrypoint and historical data.
@ -623,7 +623,7 @@ async def start_fsp_displays(
for (display_name, conf), (name, portal) in zip( for (display_name, conf), (name, portal) in zip(
fsps.items(), fsps.items(),
# rr to cluster for now.. # round robin to cluster for now..
cycle(cluster_map.items()), cycle(cluster_map.items()),
): ):
func_name = conf['func_name'] func_name = conf['func_name']
@ -668,9 +668,7 @@ async def start_fsp_displays(
async def update_chart_from_fsp( async def update_chart_from_fsp(
portal: tractor.Portal, portal: tractor.Portal,
linkedsplits: LinkedSplits, linkedsplits: LinkedSplits,
brokermod: ModuleType, brokermod: ModuleType,
sym: str, sym: str,
@ -687,7 +685,8 @@ async def update_chart_from_fsp(
profiler: pg.debug.Profiler, profiler: pg.debug.Profiler,
) -> None: ) -> None:
'''FSP stream chart update loop. '''
FSP stream chart update loop.
This is called once for each entry in the fsp This is called once for each entry in the fsp
config map. config map.
@ -792,9 +791,7 @@ async def update_chart_from_fsp(
level_line(chart, 80, orient_v='top') level_line(chart, 80, orient_v='top')
chart._set_yrange() chart._set_yrange()
done() # status updates
done()
chart.linked.resize_sidepanes()
profiler(f'fsp:{func_name} starting update loop') profiler(f'fsp:{func_name} starting update loop')
profiler.finish() profiler.finish()
@ -912,7 +909,6 @@ def has_vlm(ohlcv: ShmArray) -> bool:
@acm @acm
async def maybe_open_vlm_display( async def maybe_open_vlm_display(
linked: LinkedSplits, linked: LinkedSplits,
ohlcv: ShmArray, ohlcv: ShmArray,
@ -926,16 +922,14 @@ async def maybe_open_vlm_display(
shm, opened = maybe_mk_fsp_shm( shm, opened = maybe_mk_fsp_shm(
linked.symbol.key, linked.symbol.key,
'$_vlm', 'vlm',
readonly=True, readonly=True,
) )
async with open_fsp_sidepane( async with open_fsp_sidepane(
linked, { linked, {
'vlm': { 'vlm': {
'params': { 'params': {
'price_func': { 'price_func': {
'default_value': 'chl3', 'default_value': 'chl3',
# tell target ``Edit`` widget to not allow # tell target ``Edit`` widget to not allow
@ -963,9 +957,6 @@ async def maybe_open_vlm_display(
# we do this internally ourselves since # we do this internally ourselves since
# the curve item internals are pretty convoluted. # the curve item internals are pretty convoluted.
style='step', style='step',
# original pyqtgraph flag for reference
# stepMode=True,
) )
# XXX: ONLY for sub-chart fsps, overlays have their # XXX: ONLY for sub-chart fsps, overlays have their
@ -992,25 +983,14 @@ async def maybe_open_vlm_display(
# size view to data once at outset # size view to data once at outset
chart._set_yrange() chart._set_yrange()
# size pain to parent chart
# TODO: this appears to nearly fix a bug where the vlm sidepane
# could be sized correctly nearly immediately (since the
# order pane is already sized), right now it doesn't seem to
# fully align until the VWAP fsp-actor comes up...
await trio.sleep(0)
chart.linked.resize_sidepanes()
await trio.sleep(0)
yield chart yield chart
async def display_symbol_data( async def display_symbol_data(
godwidget: GodWidget, godwidget: GodWidget,
provider: str, provider: str,
sym: str, sym: str,
loglevel: str, loglevel: str,
order_mode_started: trio.Event, order_mode_started: trio.Event,
) -> None: ) -> None:
@ -1037,8 +1017,7 @@ async def display_symbol_data(
# group_key=loading_sym_key, # group_key=loading_sym_key,
# ) # )
async with async_enter_all( async with open_feed(
open_feed(
provider, provider,
[sym], [sym],
loglevel=loglevel, loglevel=loglevel,
@ -1046,11 +1025,8 @@ async def display_symbol_data(
# limit to at least display's FPS # limit to at least display's FPS
# avoiding needless Qt-in-guest-mode context switches # avoiding needless Qt-in-guest-mode context switches
tick_throttle=_quote_throttle_rate, tick_throttle=_quote_throttle_rate,
),
maybe_open_fsp_cluster(),
) as (feed, cluster_map):
) as feed:
ohlcv: ShmArray = feed.shm ohlcv: ShmArray = feed.shm
bars = ohlcv.array bars = ohlcv.array
symbol = feed.symbols[sym] symbol = feed.symbols[sym]
@ -1102,38 +1078,38 @@ async def display_symbol_data(
# TODO: eventually we'll support some kind of n-compose syntax # TODO: eventually we'll support some kind of n-compose syntax
fsp_conf = { fsp_conf = {
'dolla_vlm': { # 'dolla_vlm': {
'func_name': 'dolla_vlm', # 'func_name': 'dolla_vlm',
'zero_on_step': True, # 'zero_on_step': True,
'params': { # 'params': {
'price_func': { # 'price_func': {
'default_value': 'chl3', # 'default_value': 'chl3',
# tell target ``Edit`` widget to not allow # # tell target ``Edit`` widget to not allow
# edits for now. # # edits for now.
'widget_kwargs': {'readonly': True}, # 'widget_kwargs': {'readonly': True},
}, # },
}, # },
'chart_kwargs': {'style': 'step'} # 'chart_kwargs': {'style': 'step'}
}, # },
'rsi': { # 'rsi': {
'func_name': 'rsi', # literal python func ref lookup name # 'func_name': 'rsi', # literal python func ref lookup name
# map of parameters to place on the fsp sidepane widget # # map of parameters to place on the fsp sidepane widget
# which should map to dynamic inputs available to the # # which should map to dynamic inputs available to the
# fsp function at runtime. # # fsp function at runtime.
'params': { # 'params': {
'period': { # 'period': {
'default_value': 14, # 'default_value': 14,
'widget_kwargs': {'readonly': True}, # 'widget_kwargs': {'readonly': True},
}, # },
}, # },
# ``ChartPlotWidget`` options passthrough # # ``ChartPlotWidget`` options passthrough
'chart_kwargs': { # 'chart_kwargs': {
'static_yrange': (0, 100), # 'static_yrange': (0, 100),
}, # },
}, # },
} }
if has_vlm(ohlcv): # and provider != 'binance': if has_vlm(ohlcv): # and provider != 'binance':
@ -1158,10 +1134,15 @@ async def display_symbol_data(
await trio.sleep(0) await trio.sleep(0)
vlm_chart = None vlm_chart = None
async with (
trio.open_nursery() as ln, async with gather_contexts(
maybe_open_vlm_display(linkedsplits, ohlcv) as vlm_chart, (
): trio.open_nursery(),
maybe_open_vlm_display(linkedsplits, ohlcv),
maybe_open_fsp_cluster(),
)
) as (ln, vlm_chart, cluster_map):
# load initial fsp chain (otherwise known as "indicators") # load initial fsp chain (otherwise known as "indicators")
ln.start_soon( ln.start_soon(
start_fsp_displays, start_fsp_displays,
@ -1175,7 +1156,7 @@ async def display_symbol_data(
loglevel, loglevel,
) )
# start graphics update loop(s)after receiving first live quote # start graphics update loop after receiving first live quote
ln.start_soon( ln.start_soon(
update_chart_from_quotes, update_chart_from_quotes,
linkedsplits, linkedsplits,
@ -1201,4 +1182,10 @@ async def display_symbol_data(
order_mode_started order_mode_started
) )
): ):
# let Qt run to render all widgets and make sure the
# sidepanes line up vertically.
await trio.sleep(0)
linkedsplits.resize_sidepanes()
# let the app run.
await trio.sleep_forever() await trio.sleep_forever()

View File

@ -732,7 +732,7 @@ def mk_order_pane_layout(
) -> FieldsForm: ) -> FieldsForm:
font_size: int = _font.px_size - 1 font_size: int = _font.px_size - 2
# TODO: maybe just allocate the whole fields form here # TODO: maybe just allocate the whole fields form here
# and expect an async ctx entry? # and expect an async ctx entry?

View File

@ -341,7 +341,14 @@ class ChartView(ViewBox):
**kwargs, **kwargs,
): ):
super().__init__(parent=parent, **kwargs) super().__init__(
parent=parent,
# TODO: look into the default view padding
# support that might replace somem of our
# ``ChartPlotWidget._set_yrange()`
# defaultPadding=0.,
**kwargs
)
# disable vertical scrolling # disable vertical scrolling
self.setMouseEnabled(x=True, y=False) self.setMouseEnabled(x=True, y=False)
@ -533,7 +540,6 @@ class ChartView(ViewBox):
# self.updateScaleBox(ev.buttonDownPos(), ev.pos()) # self.updateScaleBox(ev.buttonDownPos(), ev.pos())
else: else:
# default bevavior: click to pan view # default bevavior: click to pan view
tr = self.childGroup.transform() tr = self.childGroup.transform()
tr = fn.invertQTransform(tr) tr = fn.invertQTransform(tr)
tr = tr.map(dif*mask) - tr.map(Point(0, 0)) tr = tr.map(dif*mask) - tr.map(Point(0, 0))

View File

@ -110,7 +110,7 @@ class DpiAwareFont:
mx_dpi = max(pdpi, ldpi) mx_dpi = max(pdpi, ldpi)
mn_dpi = min(pdpi, ldpi) mn_dpi = min(pdpi, ldpi)
scale = round(ldpi/pdpi) scale = round(ldpi/pdpi, ndigits=2)
if mx_dpi <= 97: # for low dpi use larger font sizes if mx_dpi <= 97: # for low dpi use larger font sizes
inches = _font_sizes['lo'][self._font_size] inches = _font_sizes['lo'][self._font_size]
@ -121,17 +121,29 @@ class DpiAwareFont:
dpi = mn_dpi dpi = mn_dpi
# dpi is likely somewhat scaled down so use slightly larger font size # dpi is likely somewhat scaled down so use slightly larger font size
if scale > 1 and self._font_size: if scale >= 1.1 and self._font_size:
# TODO: this denominator should probably be determined from
# no idea why
if 1.2 <= scale:
mult = 1.0375
if scale >= 1.5:
mult = 1.375
# TODO: this multiplier should probably be determined from
# relative aspect ratios or something? # relative aspect ratios or something?
inches = inches * (1 / scale) * (1 + 6/16) inches *= mult
dpi = mx_dpi
# TODO: we might want to fiddle with incrementing font size by
# +1 for the edge cases above. it seems doing it via scaling is
# always going to hit that error in range mapping from inches:
# float to px size: int.
self._font_inches = inches self._font_inches = inches
font_size = math.floor(inches * dpi) font_size = math.floor(inches * dpi)
log.debug(
f"\nscreen:{screen.name()} with pDPI: {pdpi}, lDPI: {ldpi}" log.info(
f"screen:{screen.name()}]\n"
f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n"
f"\nOur best guess font size is {font_size}\n" f"\nOur best guess font size is {font_size}\n"
) )
# apply the size # apply the size