WIP resize sidepanes to master plot

fsp_feeds
Tyler Goodlet 2021-09-16 19:28:26 -04:00
parent 4d06502bc8
commit 7d00244e8b
2 changed files with 210 additions and 119 deletions

View File

@ -276,6 +276,8 @@ 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):
''' '''
@ -339,7 +341,8 @@ class LinkedSplits(QWidget):
def set_split_sizes( def set_split_sizes(
self, self,
prop: float = 0.375 # proportion allocated to consumer subcharts # prop: float = 0.375, # proportion allocated to consumer subcharts
prop: float = 5/8,
) -> None: ) -> None:
'''Set the proportion of space allocated for linked subcharts. '''Set the proportion of space allocated for linked subcharts.
@ -450,7 +453,6 @@ class LinkedSplits(QWidget):
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
@ -522,10 +524,10 @@ class LinkedSplits(QWidget):
# track by name # track by name
self.subplots[name] = cpw self.subplots[name] = cpw
if sidepane: # if sidepane:
# TODO: use a "panes" collection to manage this? # # TODO: use a "panes" collection to manage this?
sidepane.setMinimumWidth(self.chart.sidepane.width()) # qframe.setMaximumWidth(self.chart.sidepane.width())
sidepane.setMaximumWidth(self.chart.sidepane.width()) # qframe.setMinimumWidth(self.chart.sidepane.width())
self.splitter.addWidget(qframe) self.splitter.addWidget(qframe)
@ -537,6 +539,16 @@ class LinkedSplits(QWidget):
return cpw return cpw
def resize_sidepanes(
self,
) -> None:
'''Size all sidepanes based on the OHLC "main" plot.
'''
for name, cpw in self.subplots.items():
cpw.sidepane.setMinimumWidth(self.chart.sidepane.width())
cpw.sidepane.setMaximumWidth(self.chart.sidepane.width())
class ChartPlotWidget(pg.PlotWidget): class ChartPlotWidget(pg.PlotWidget):
''' '''
@ -681,9 +693,9 @@ class ChartPlotWidget(pg.PlotWidget):
"""Return a range tuple for the bars present in view. """Return a range tuple for the bars present in view.
""" """
l, r = self.view_range() l, r = self.view_range()
a = self._arrays['ohlc'] array = self._arrays['ohlc']
lbar = max(l, a[0]['index']) lbar = max(l, array[0]['index'])
rbar = min(r, a[-1]['index']) rbar = min(r, array[-1]['index'])
return l, lbar, rbar, r return l, lbar, rbar, r
def default_view( def default_view(
@ -991,22 +1003,19 @@ class ChartPlotWidget(pg.PlotWidget):
a = self._arrays['ohlc'] a = self._arrays['ohlc']
ifirst = a[0]['index'] ifirst = a[0]['index']
bars = a[lbar - ifirst:rbar - ifirst + 1] bars = a[lbar - ifirst:rbar - ifirst + 1]
if not len(bars): if not len(bars):
# likely no data loaded yet or extreme scrolling? # likely no data loaded yet or extreme scrolling?
log.error(f"WTF bars_range = {lbar}:{rbar}") log.error(f"WTF bars_range = {lbar}:{rbar}")
return return
# TODO: should probably just have some kinda attr mark if self.data_key != self.linked.symbol.key:
# that determines this behavior based on array type bars = a[self.data_key]
try: ylow = np.nanmin(bars)
yhigh = np.nanmax((bars))
else:
# just the std ohlc bars
ylow = np.nanmin(bars['low']) ylow = np.nanmin(bars['low'])
yhigh = np.nanmax(bars['high']) yhigh = np.nanmax(bars['high'])
except (IndexError, ValueError):
# likely non-ohlc array?
bars = bars[self.name]
ylow = np.nanmin(bars)
yhigh = np.nanmax(bars)
if set_range: if set_range:
# view margins: stay within a % of the "true range" # view margins: stay within a % of the "true range"

View File

@ -18,6 +18,7 @@
Real-time display tasks for charting / graphics. Real-time display tasks for charting / graphics.
''' '''
from contextlib import asynccontextmanager
import time import time
from typing import Any from typing import Any
from types import ModuleType from types import ModuleType
@ -264,7 +265,7 @@ async def chart_from_quotes(
last_mx, last_mn = mx, mn last_mx, last_mn = mx, mn
async def spawn_fsps( async def fan_out_spawn_fsp_daemons(
linkedsplits: LinkedSplits, linkedsplits: LinkedSplits,
fsps: dict[str, str], fsps: dict[str, str],
@ -275,109 +276,93 @@ async def spawn_fsps(
loglevel: str, loglevel: str,
) -> None: ) -> None:
"""Start financial signal processing in subactor. '''Create financial signal processing sub-actors (under flat tree)
for each entry in config and attach to local graphics update tasks.
Pass target entrypoint and historical data. Pass target entrypoint and historical data.
""" '''
linkedsplits.focus() linkedsplits.focus()
uid = tractor.current_actor().uid uid = tractor.current_actor().uid
# spawns sub-processes which execute cpu bound FSP code # spawns sub-processes which execute cpu bound FSP code
async with tractor.open_nursery(loglevel=loglevel) as n: async with (
tractor.open_nursery() as n,
trio.open_nursery() as ln,
):
# spawns local task that consume and chart data streams from # Currently we spawn an actor per fsp chain but
# sub-procs # likely we'll want to pool them eventually to
async with trio.open_nursery() as ln: # scale horizonatlly once cores are used up.
for display_name, conf in fsps.items():
# Currently we spawn an actor per fsp chain but fsp_func_name = conf['fsp_func_name']
# likely we'll want to pool them eventually to
# scale horizonatlly once cores are used up.
for display_name, conf in fsps.items():
fsp_func_name = conf['fsp_func_name'] # TODO: load function here and introspect
# return stream type(s)
# TODO: load function here and introspect # TODO: should `index` be a required internal field?
# return stream type(s) fsp_dtype = np.dtype([('index', int), (fsp_func_name, float)])
# TODO: should `index` be a required internal field? key = f'{sym}.fsp.{display_name}.{".".join(uid)}'
fsp_dtype = np.dtype([('index', int), (fsp_func_name, float)])
key = f'{sym}.fsp.{display_name}.{".".join(uid)}' # this is all sync currently
shm, opened = maybe_open_shm_array(
key,
# TODO: create entry for each time frame
dtype=fsp_dtype,
readonly=True,
)
# this is all sync currently # XXX: fsp may have been opened by a duplicate chart.
shm, opened = maybe_open_shm_array( # Error for now until we figure out how to wrap fsps as
key, # "feeds". assert opened, f"A chart for {key} likely
# TODO: create entry for each time frame # already exists?"
dtype=fsp_dtype,
readonly=True,
)
# XXX: fsp may have been opened by a duplicate chart. conf['shm'] = shm
# Error for now until we figure out how to wrap fsps as
# "feeds". assert opened, f"A chart for {key} likely
# already exists?"
conf['shm'] = shm portal = await n.start_actor(
enable_modules=['piker.fsp'],
name='fsp.' + display_name,
)
portal = await n.start_actor( # init async
enable_modules=['piker.fsp'], ln.start_soon(
name='fsp.' + display_name, run_fsp,
) portal,
linkedsplits,
brokermod,
sym,
src_shm,
fsp_func_name,
display_name,
conf,
group_status_key,
loglevel,
)
# init async # blocks here until all fsp actors complete
ln.start_soon(
run_fsp,
portal,
linkedsplits,
brokermod,
sym,
src_shm,
fsp_func_name,
display_name,
conf,
group_status_key,
)
# blocks here until all fsp actors complete
async def run_fsp( class FspConfig(BaseModel):
class Config:
validate_assignment = True
portal: tractor._portal.Portal, name: str
linkedsplits: LinkedSplits, period: int
brokermod: ModuleType,
sym: str,
src_shm: ShmArray, @asynccontextmanager
fsp_func_name: str, async def open_sidepane(
linked: LinkedSplits,
display_name: str, display_name: str,
conf: dict[str, Any],
group_status_key: str,
) -> None: ) -> FspConfig:
"""FSP stream chart update loop.
This is called once for each entry in the fsp
config map.
"""
done = linkedsplits.window().status_bar.open_status(
f'loading fsp, {display_name}..',
group_key=group_status_key,
)
# make sidepane config widget
class FspConfig(BaseModel):
class Config:
validate_assignment = True
name: str
period: int
sidepane: FieldsForm = mk_form( sidepane: FieldsForm = mk_form(
parent=linkedsplits.godwidget, parent=linked.godwidget,
fields_schema={ fields_schema={
'name': { 'name': {
'label': '**fsp**:', 'label': '**fsp**:',
@ -386,6 +371,8 @@ async def run_fsp(
f'{display_name}' f'{display_name}'
], ],
}, },
# TODO: generate this from input map
'period': { 'period': {
'label': '**period**:', 'label': '**period**:',
'type': 'edit', 'type': 'edit',
@ -403,10 +390,46 @@ async def run_fsp(
print(f'{key}: {value}') print(f'{key}: {value}')
return True return True
# TODO:
async with (
open_form_input_handling(
sidepane,
focus_next=linked.godwidget,
on_value_change=settings_change,
)
):
yield sidepane
async def run_fsp(
portal: tractor._portal.Portal,
linkedsplits: LinkedSplits,
brokermod: ModuleType,
sym: str,
src_shm: ShmArray,
fsp_func_name: str,
display_name: str,
conf: dict[str, Any],
group_status_key: str,
loglevel: str,
) -> None:
'''FSP stream chart update loop.
This is called once for each entry in the fsp
config map.
'''
done = linkedsplits.window().status_bar.open_status(
f'loading fsp, {display_name}..',
group_key=group_status_key,
)
async with ( async with (
portal.open_stream_from( portal.open_stream_from(
# subactor entrypoint # chaining entrypoint
fsp.cascade, fsp.cascade,
# name as title of sub-chart # name as title of sub-chart
@ -415,15 +438,14 @@ async def run_fsp(
dst_shm_token=conf['shm'].token, dst_shm_token=conf['shm'].token,
symbol=sym, symbol=sym,
fsp_func_name=fsp_func_name, fsp_func_name=fsp_func_name,
loglevel=loglevel,
) as stream, ) as stream,
# TODO: open_sidepane(
open_form_input_handling( linkedsplits,
sidepane, display_name,
focus_next=linkedsplits.godwidget, ) as sidepane,
on_value_change=settings_change,
),
): ):
# receive last index for processed historical # receive last index for processed historical
@ -472,7 +494,7 @@ async def run_fsp(
# read from last calculated value # read from last calculated value
array = shm.array array = shm.array
# XXX: fsp func names are unique meaning we don't have # XXX: fsp func names must be unique meaning we don't have
# duplicates of the underlying data even if multiple # duplicates of the underlying data even if multiple
# sub-charts reference it under different 'named charts'. # sub-charts reference it under different 'named charts'.
value = array[fsp_func_name][-1] value = array[fsp_func_name][-1]
@ -489,6 +511,8 @@ async def run_fsp(
array_key=fsp_func_name array_key=fsp_func_name
) )
chart.linked.resize_sidepanes()
# TODO: figure out if we can roll our own `FillToThreshold` to # TODO: figure out if we can roll our own `FillToThreshold` to
# get brush filled polygons for OS/OB conditions. # get brush filled polygons for OS/OB conditions.
# ``pg.FillBetweenItems`` seems to be one technique using # ``pg.FillBetweenItems`` seems to be one technique using
@ -622,6 +646,73 @@ async def check_for_new_bars(feed, ohlcv, linkedsplits):
price_chart.increment_view() price_chart.increment_view()
def has_vlm(ohlcv: ShmArray) -> bool:
# make sure that the instrument supports volume history
# (sometimes this is not the case for some commodities and
# derivatives)
volm = ohlcv.array['volume']
return not bool(np.all(np.isin(volm, -1)) or np.all(np.isnan(volm)))
@asynccontextmanager
async def maybe_open_vlm_display(
linked: LinkedSplits,
ohlcv: ShmArray,
) -> ChartPlotWidget:
# make sure that the instrument supports volume history
# (sometimes this is not the case for some commodities and
# derivatives)
# volm = ohlcv.array['volume']
# if (
# np.all(np.isin(volm, -1)) or
# np.all(np.isnan(volm))
# ):
if not has_vlm(ohlcv):
log.warning(f"{linked.symbol.key} does not seem to have volume info")
else:
async with open_sidepane(linked, 'volume') as sidepane:
# built-in $vlm
shm = ohlcv
chart = linked.add_plot(
name='vlm',
array=shm.array,
array_key='volume',
sidepane=sidepane,
# curve by default
ohlc=False,
# vertical bars
# stepMode=True,
# static_yrange=(0, 100),
)
# XXX: ONLY for sub-chart fsps, overlays have their
# data looked up from the chart's internal array set.
# TODO: we must get a data view api going STAT!!
chart._shm = shm
# should **not** be the same sub-chart widget
assert chart.name != linked.chart.name
# sticky only on sub-charts atm
last_val_sticky = chart._ysticks[chart.name]
# read from last calculated value
value = shm.array['volume'][-1]
last_val_sticky.update_from_data(-1, value)
# size view to data once at outset
chart._set_yrange()
yield chart
async def display_symbol_data( async def display_symbol_data(
godwidget: GodWidget, godwidget: GodWidget,
@ -686,6 +777,7 @@ async def display_symbol_data(
# add as next-to-y-axis singleton pane # add as next-to-y-axis singleton pane
godwidget.pp_pane = pp_pane godwidget.pp_pane = pp_pane
# create main OHLC chart
chart = linkedsplits.plot_ohlc_main( chart = linkedsplits.plot_ohlc_main(
symbol, symbol,
bars, bars,
@ -722,7 +814,7 @@ async def display_symbol_data(
'static_yrange': (0, 100), 'static_yrange': (0, 100),
}, },
}, },
# test for duplicate fsps on same chart # # test for duplicate fsps on same chart
# 'rsi2': { # 'rsi2': {
# 'fsp_func_name': 'rsi', # 'fsp_func_name': 'rsi',
# 'period': 14, # 'period': 14,
@ -733,18 +825,8 @@ async def display_symbol_data(
} }
# make sure that the instrument supports volume history if has_vlm(ohlcv):
# (sometimes this is not the case for some commodities and # add VWAP to fsp config for downstream loading
# derivatives)
volm = ohlcv.array['volume']
if (
np.all(np.isin(volm, -1)) or
np.all(np.isnan(volm))
):
log.warning(
f"{sym} does not seem to have volume info,"
" dropping volume signals")
else:
fsp_conf.update({ fsp_conf.update({
'vwap': { 'vwap': {
'fsp_func_name': 'vwap', 'fsp_func_name': 'vwap',
@ -756,11 +838,10 @@ async def display_symbol_data(
async with ( async with (
trio.open_nursery() as ln, trio.open_nursery() as ln,
): ):
# load initial fsp chain (otherwise known as "indicators") # load initial fsp chain (otherwise known as "indicators")
ln.start_soon( ln.start_soon(
spawn_fsps, fan_out_spawn_fsp_daemons,
linkedsplits, linkedsplits,
fsp_conf, fsp_conf,
sym, sym,
@ -787,6 +868,7 @@ async def display_symbol_data(
) )
async with ( async with (
maybe_open_vlm_display(linkedsplits, ohlcv),
open_order_mode( open_order_mode(
feed, feed,