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
Tyler Goodlet 2021-09-18 15:14:09 -04:00
parent 4436ed2c18
commit fa9eebab35
2 changed files with 104 additions and 80 deletions

View File

@ -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..

View File

@ -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,