WIP resize sidepanes to master plot
parent
4d06502bc8
commit
7d00244e8b
|
@ -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"
|
||||||
|
|
|
@ -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,22 +276,21 @@ 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,
|
||||||
# spawns local task that consume and chart data streams from
|
trio.open_nursery() as ln,
|
||||||
# sub-procs
|
):
|
||||||
async with trio.open_nursery() as ln:
|
|
||||||
|
|
||||||
# Currently we spawn an actor per fsp chain but
|
# Currently we spawn an actor per fsp chain but
|
||||||
# likely we'll want to pool them eventually to
|
# likely we'll want to pool them eventually to
|
||||||
|
@ -339,45 +339,30 @@ async def spawn_fsps(
|
||||||
display_name,
|
display_name,
|
||||||
conf,
|
conf,
|
||||||
group_status_key,
|
group_status_key,
|
||||||
|
loglevel,
|
||||||
)
|
)
|
||||||
|
|
||||||
# blocks here until all fsp actors complete
|
# blocks here until all fsp actors complete
|
||||||
|
|
||||||
|
|
||||||
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,
|
|
||||||
|
|
||||||
) -> 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,
|
|
||||||
)
|
|
||||||
|
|
||||||
# make sidepane config widget
|
|
||||||
class FspConfig(BaseModel):
|
class FspConfig(BaseModel):
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
validate_assignment = True
|
validate_assignment = True
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
period: int
|
period: int
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def open_sidepane(
|
||||||
|
|
||||||
|
linked: LinkedSplits,
|
||||||
|
display_name: str,
|
||||||
|
|
||||||
|
) -> FspConfig:
|
||||||
|
|
||||||
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,
|
||||||
|
|
Loading…
Reference in New Issue