Add volume plot as default
Toss in support for a "step mode" curve (unfinished atm) and use it to plot from the `volume` field of the ohlcv shm array (if available). changes to make it happen, - dynamically generate the fsp sidepane form from an input config `dict` |_ dynamically generate the underlying `pydantic` model |_ - add a "volume checker" helper func that inspects the shm array - toss in sidepane resize calls to avoid race where the ohlcv array is plotted too slowly compared to the volume and the chart somehow doesn't show.. - drop duplicate rsi2 cruft (previously used to test plots of the shm data)win_fixes
parent
4436ed2c18
commit
fa9eebab35
|
@ -326,8 +326,8 @@ class LinkedSplits(QWidget):
|
||||||
# self.xaxis.hide()
|
# self.xaxis.hide()
|
||||||
|
|
||||||
self.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
|
self.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
|
||||||
self.splitter.setMidLineWidth(1)
|
self.splitter.setMidLineWidth(0)
|
||||||
self.splitter.setHandleWidth(0)
|
self.splitter.setHandleWidth(2)
|
||||||
|
|
||||||
self.layout = QtWidgets.QVBoxLayout(self)
|
self.layout = QtWidgets.QVBoxLayout(self)
|
||||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
@ -341,8 +341,7 @@ 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.
|
||||||
|
@ -495,8 +494,9 @@ class LinkedSplits(QWidget):
|
||||||
cpw.plotItem.vb.linkedsplits = self
|
cpw.plotItem.vb.linkedsplits = self
|
||||||
cpw.setFrameStyle(
|
cpw.setFrameStyle(
|
||||||
QtWidgets.QFrame.StyledPanel
|
QtWidgets.QFrame.StyledPanel
|
||||||
# | QtWidgets.QFrame.Plain)
|
# | QtWidgets.QFrame.Plain
|
||||||
)
|
)
|
||||||
|
|
||||||
cpw.hideButtons()
|
cpw.hideButtons()
|
||||||
|
|
||||||
# XXX: gives us outline on backside of y-axis
|
# XXX: gives us outline on backside of y-axis
|
||||||
|
@ -515,7 +515,20 @@ class LinkedSplits(QWidget):
|
||||||
cpw.draw_ohlc(name, array, array_key=array_key)
|
cpw.draw_ohlc(name, array, array_key=array_key)
|
||||||
|
|
||||||
elif style == 'line':
|
elif style == 'line':
|
||||||
cpw.draw_curve(name, array, array_key=array_key)
|
cpw.draw_curve(
|
||||||
|
name,
|
||||||
|
array,
|
||||||
|
array_key=array_key,
|
||||||
|
color='default_lightest',
|
||||||
|
)
|
||||||
|
|
||||||
|
elif style == 'step':
|
||||||
|
cpw.draw_curve(
|
||||||
|
name,
|
||||||
|
array,
|
||||||
|
array_key=array_key,
|
||||||
|
step_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Chart style {style} is currently unsupported")
|
raise ValueError(f"Chart style {style} is currently unsupported")
|
||||||
|
@ -523,14 +536,7 @@ class LinkedSplits(QWidget):
|
||||||
if not _is_main:
|
if not _is_main:
|
||||||
# track by name
|
# track by name
|
||||||
self.subplots[name] = cpw
|
self.subplots[name] = cpw
|
||||||
|
|
||||||
# if sidepane:
|
|
||||||
# # TODO: use a "panes" collection to manage this?
|
|
||||||
# qframe.setMaximumWidth(self.chart.sidepane.width())
|
|
||||||
# qframe.setMinimumWidth(self.chart.sidepane.width())
|
|
||||||
|
|
||||||
self.splitter.addWidget(qframe)
|
self.splitter.addWidget(qframe)
|
||||||
|
|
||||||
# scale split regions
|
# scale split regions
|
||||||
self.set_split_sizes()
|
self.set_split_sizes()
|
||||||
|
|
||||||
|
@ -600,7 +606,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# parent=None,
|
# parent=None,
|
||||||
# plotItem=None,
|
# plotItem=None,
|
||||||
# antialias=True,
|
# antialias=True,
|
||||||
useOpenGL=True,
|
# useOpenGL=True,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
self.name = name
|
self.name = name
|
||||||
|
@ -784,7 +790,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
array_key: Optional[str] = None,
|
array_key: Optional[str] = None,
|
||||||
overlay: bool = False,
|
overlay: bool = False,
|
||||||
color: str = 'default_light',
|
color: Optional[str] = None,
|
||||||
add_label: bool = True,
|
add_label: bool = True,
|
||||||
|
|
||||||
**pdi_kwargs,
|
**pdi_kwargs,
|
||||||
|
@ -794,6 +800,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
the input array ``data``.
|
the input array ``data``.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
color = color or self.pen_color or 'default_light'
|
||||||
|
|
||||||
_pdi_defaults = {
|
_pdi_defaults = {
|
||||||
'pen': pg.mkPen(hcolor(color)),
|
'pen': pg.mkPen(hcolor(color)),
|
||||||
}
|
}
|
||||||
|
@ -944,13 +952,13 @@ 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,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set the viewable y-range based on embedded data.
|
'''Set the viewable y-range based on embedded data.
|
||||||
|
|
||||||
This adds auto-scaling like zoom on the scroll wheel such
|
This adds auto-scaling like zoom on the scroll wheel such
|
||||||
that data always fits nicely inside the current view of the
|
that data always fits nicely inside the current view of the
|
||||||
data set.
|
data set.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
set_range = True
|
set_range = True
|
||||||
|
|
||||||
if self._static_yrange == 'axis':
|
if self._static_yrange == 'axis':
|
||||||
|
@ -1003,15 +1011,17 @@ 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
|
||||||
|
|
||||||
if self.data_key != self.linked.symbol.key:
|
if self.data_key != self.linked.symbol.key:
|
||||||
bars = a[self.data_key]
|
bars = bars[self.data_key]
|
||||||
ylow = np.nanmin(bars)
|
ylow = np.nanmin(bars)
|
||||||
yhigh = np.nanmax((bars))
|
yhigh = np.nanmax(bars)
|
||||||
|
# print(f'{(ylow, yhigh)}')
|
||||||
else:
|
else:
|
||||||
# just the std ohlc bars
|
# just the std ohlc bars
|
||||||
ylow = np.nanmin(bars['low'])
|
ylow = np.nanmin(bars['low'])
|
||||||
|
@ -1072,7 +1082,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# TODO: this should go onto some sort of
|
# TODO: this should go onto some sort of
|
||||||
# data-view strimg thinger..right?
|
# data-view strimg thinger..right?
|
||||||
ohlc = self._shm.array
|
ohlc = self._shm.array
|
||||||
# ohlc = chart._shm.array
|
|
||||||
|
|
||||||
# XXX: not sure why the time is so off here
|
# XXX: not sure why the time is so off here
|
||||||
# looks like we're gonna have to do some fixing..
|
# looks like we're gonna have to do some fixing..
|
||||||
|
|
|
@ -20,11 +20,10 @@ Real-time display tasks for charting / graphics.
|
||||||
'''
|
'''
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import BaseModel
|
from pydantic import create_model
|
||||||
import tractor
|
import tractor
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
|
@ -345,45 +344,51 @@ async def fan_out_spawn_fsp_daemons(
|
||||||
# blocks here until all fsp actors complete
|
# blocks here until all fsp actors complete
|
||||||
|
|
||||||
|
|
||||||
class FspConfig(BaseModel):
|
|
||||||
class Config:
|
|
||||||
validate_assignment = True
|
|
||||||
|
|
||||||
name: str
|
|
||||||
period: int
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def open_sidepane(
|
async def open_sidepane(
|
||||||
|
|
||||||
linked: LinkedSplits,
|
linked: LinkedSplits,
|
||||||
display_name: str,
|
conf: dict[str, dict[str, str]],
|
||||||
|
|
||||||
) -> FspConfig:
|
) -> FieldsForm:
|
||||||
|
|
||||||
|
schema = {}
|
||||||
|
|
||||||
|
assert len(conf) == 1 # for now
|
||||||
|
|
||||||
|
# add (single) selection widget
|
||||||
|
for display_name, config in conf.items():
|
||||||
|
schema[display_name] = {
|
||||||
|
'label': '**fsp**:',
|
||||||
|
'type': 'select',
|
||||||
|
'default_value': [display_name],
|
||||||
|
}
|
||||||
|
|
||||||
|
# add parameters for selection "options"
|
||||||
|
defaults = config.get('params', {})
|
||||||
|
for name, default in defaults.items():
|
||||||
|
|
||||||
|
# add to ORM schema
|
||||||
|
schema.update({
|
||||||
|
name: {
|
||||||
|
'label': f'**{name}**:',
|
||||||
|
'type': 'edit',
|
||||||
|
'default_value': default,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
sidepane: FieldsForm = mk_form(
|
sidepane: FieldsForm = mk_form(
|
||||||
parent=linked.godwidget,
|
parent=linked.godwidget,
|
||||||
fields_schema={
|
fields_schema=schema,
|
||||||
'name': {
|
)
|
||||||
'label': '**fsp**:',
|
|
||||||
'type': 'select',
|
|
||||||
'default_value': [
|
|
||||||
f'{display_name}'
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
# TODO: generate this from input map
|
# https://pydantic-docs.helpmanual.io/usage/models/#dynamic-model-creation
|
||||||
'period': {
|
FspConfig = create_model(
|
||||||
'label': '**period**:',
|
'FspConfig',
|
||||||
'type': 'edit',
|
|
||||||
'default_value': 14,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
sidepane.model = FspConfig(
|
|
||||||
name=display_name,
|
name=display_name,
|
||||||
period=14,
|
**defaults,
|
||||||
)
|
)
|
||||||
|
sidepane.model = FspConfig()
|
||||||
|
|
||||||
# just a logger for now until we get fsp configs up and running.
|
# just a logger for now until we get fsp configs up and running.
|
||||||
async def settings_change(key: str, value: str) -> bool:
|
async def settings_change(key: str, value: str) -> bool:
|
||||||
|
@ -410,7 +415,7 @@ async def run_fsp(
|
||||||
src_shm: ShmArray,
|
src_shm: ShmArray,
|
||||||
fsp_func_name: str,
|
fsp_func_name: str,
|
||||||
display_name: str,
|
display_name: str,
|
||||||
conf: dict[str, Any],
|
conf: dict[str, dict],
|
||||||
group_status_key: str,
|
group_status_key: str,
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
|
|
||||||
|
@ -444,7 +449,7 @@ async def run_fsp(
|
||||||
ctx.open_stream() as stream,
|
ctx.open_stream() as stream,
|
||||||
open_sidepane(
|
open_sidepane(
|
||||||
linkedsplits,
|
linkedsplits,
|
||||||
display_name,
|
{display_name: conf},
|
||||||
) as sidepane,
|
) as sidepane,
|
||||||
):
|
):
|
||||||
|
|
||||||
|
@ -453,9 +458,10 @@ async def run_fsp(
|
||||||
if conf.get('overlay'):
|
if conf.get('overlay'):
|
||||||
chart = linkedsplits.chart
|
chart = linkedsplits.chart
|
||||||
chart.draw_curve(
|
chart.draw_curve(
|
||||||
name='vwap',
|
name=display_name,
|
||||||
data=shm.array,
|
data=shm.array,
|
||||||
overlay=True,
|
overlay=True,
|
||||||
|
color='default_light',
|
||||||
)
|
)
|
||||||
last_val_sticky = None
|
last_val_sticky = None
|
||||||
|
|
||||||
|
@ -658,22 +664,23 @@ async def maybe_open_vlm_display(
|
||||||
|
|
||||||
) -> ChartPlotWidget:
|
) -> 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):
|
if not has_vlm(ohlcv):
|
||||||
log.warning(f"{linked.symbol.key} does not seem to have volume info")
|
log.warning(f"{linked.symbol.key} does not seem to have volume info")
|
||||||
else:
|
else:
|
||||||
async with open_sidepane(linked, 'volume') as sidepane:
|
async with open_sidepane(
|
||||||
|
linked, {
|
||||||
|
'volume': {
|
||||||
|
'params': {
|
||||||
|
'price_func': 'ohl3'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) as sidepane:
|
||||||
|
|
||||||
# built-in $vlm
|
# built-in $vlm
|
||||||
shm = ohlcv
|
shm = ohlcv
|
||||||
chart = linked.add_plot(
|
chart = linked.add_plot(
|
||||||
name='vlm',
|
name='volume',
|
||||||
array=shm.array,
|
array=shm.array,
|
||||||
|
|
||||||
array_key='volume',
|
array_key='volume',
|
||||||
|
@ -681,10 +688,10 @@ async def maybe_open_vlm_display(
|
||||||
|
|
||||||
# curve by default
|
# curve by default
|
||||||
ohlc=False,
|
ohlc=False,
|
||||||
|
style='step',
|
||||||
|
|
||||||
# vertical bars
|
# vertical bars, we do this internally ourselves
|
||||||
# stepMode=True,
|
# stepMode=True,
|
||||||
# static_yrange=(0, 100),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# XXX: ONLY for sub-chart fsps, overlays have their
|
# XXX: ONLY for sub-chart fsps, overlays have their
|
||||||
|
@ -703,9 +710,23 @@ async def maybe_open_vlm_display(
|
||||||
|
|
||||||
last_val_sticky.update_from_data(-1, value)
|
last_val_sticky.update_from_data(-1, value)
|
||||||
|
|
||||||
|
chart.update_curve_from_array(
|
||||||
|
'volume',
|
||||||
|
shm.array,
|
||||||
|
)
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
|
|
||||||
|
@ -805,20 +826,11 @@ async def display_symbol_data(
|
||||||
fsp_conf = {
|
fsp_conf = {
|
||||||
'rsi': {
|
'rsi': {
|
||||||
'fsp_func_name': 'rsi',
|
'fsp_func_name': 'rsi',
|
||||||
'period': 14,
|
'params': {'period': 14},
|
||||||
'chart_kwargs': {
|
'chart_kwargs': {
|
||||||
'static_yrange': (0, 100),
|
'static_yrange': (0, 100),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
# # test for duplicate fsps on same chart
|
|
||||||
# 'rsi2': {
|
|
||||||
# 'fsp_func_name': 'rsi',
|
|
||||||
# 'period': 14,
|
|
||||||
# 'chart_kwargs': {
|
|
||||||
# 'static_yrange': (0, 100),
|
|
||||||
# },
|
|
||||||
# },
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_vlm(ohlcv):
|
if has_vlm(ohlcv):
|
||||||
|
@ -831,8 +843,14 @@ async def display_symbol_data(
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
async with (
|
# NOTE: we must immediately tell Qt to show the OHLC chart
|
||||||
|
# to avoid a race where the subplots get added/shown to
|
||||||
|
# the linked set *before* the main price chart!
|
||||||
|
linkedsplits.show()
|
||||||
|
linkedsplits.focus()
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
|
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")
|
||||||
|
@ -864,10 +882,7 @@ async def display_symbol_data(
|
||||||
)
|
)
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
# XXX: this slipped in during a commits refacotr,
|
maybe_open_vlm_display(linkedsplits, ohlcv),
|
||||||
# it's actually landing proper in #231
|
|
||||||
# maybe_open_vlm_display(linkedsplits, ohlcv),
|
|
||||||
|
|
||||||
open_order_mode(
|
open_order_mode(
|
||||||
feed,
|
feed,
|
||||||
chart,
|
chart,
|
||||||
|
|
Loading…
Reference in New Issue