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.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
self.splitter.setMidLineWidth(1)
self.splitter.setHandleWidth(0)
self.splitter.setMidLineWidth(0)
self.splitter.setHandleWidth(2)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
@ -341,8 +341,7 @@ class LinkedSplits(QWidget):
def set_split_sizes(
self,
# prop: float = 0.375, # proportion allocated to consumer subcharts
prop: float = 5/8,
prop: float = 0.375, # proportion allocated to consumer subcharts
) -> None:
'''Set the proportion of space allocated for linked subcharts.
@ -495,8 +494,9 @@ class LinkedSplits(QWidget):
cpw.plotItem.vb.linkedsplits = self
cpw.setFrameStyle(
QtWidgets.QFrame.StyledPanel
# | QtWidgets.QFrame.Plain)
# | QtWidgets.QFrame.Plain
)
cpw.hideButtons()
# 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)
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:
raise ValueError(f"Chart style {style} is currently unsupported")
@ -523,14 +536,7 @@ class LinkedSplits(QWidget):
if not _is_main:
# track by name
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)
# scale split regions
self.set_split_sizes()
@ -600,7 +606,7 @@ class ChartPlotWidget(pg.PlotWidget):
# parent=None,
# plotItem=None,
# antialias=True,
useOpenGL=True,
# useOpenGL=True,
**kwargs
)
self.name = name
@ -784,7 +790,7 @@ class ChartPlotWidget(pg.PlotWidget):
array_key: Optional[str] = None,
overlay: bool = False,
color: str = 'default_light',
color: Optional[str] = None,
add_label: bool = True,
**pdi_kwargs,
@ -794,6 +800,8 @@ class ChartPlotWidget(pg.PlotWidget):
the input array ``data``.
"""
color = color or self.pen_color or 'default_light'
_pdi_defaults = {
'pen': pg.mkPen(hcolor(color)),
}
@ -944,13 +952,13 @@ class ChartPlotWidget(pg.PlotWidget):
yrange: Optional[tuple[float, float]] = None,
range_margin: float = 0.06,
) -> 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
that data always fits nicely inside the current view of the
data set.
"""
'''
set_range = True
if self._static_yrange == 'axis':
@ -1003,15 +1011,17 @@ class ChartPlotWidget(pg.PlotWidget):
a = self._arrays['ohlc']
ifirst = a[0]['index']
bars = a[lbar - ifirst:rbar - ifirst + 1]
if not len(bars):
# likely no data loaded yet or extreme scrolling?
log.error(f"WTF bars_range = {lbar}:{rbar}")
return
if self.data_key != self.linked.symbol.key:
bars = a[self.data_key]
bars = bars[self.data_key]
ylow = np.nanmin(bars)
yhigh = np.nanmax((bars))
yhigh = np.nanmax(bars)
# print(f'{(ylow, yhigh)}')
else:
# just the std ohlc bars
ylow = np.nanmin(bars['low'])
@ -1072,7 +1082,6 @@ class ChartPlotWidget(pg.PlotWidget):
# TODO: this should go onto some sort of
# data-view strimg thinger..right?
ohlc = self._shm.array
# ohlc = chart._shm.array
# XXX: not sure why the time is so off here
# 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
import time
from typing import Any
from types import ModuleType
import numpy as np
from pydantic import BaseModel
from pydantic import create_model
import tractor
import trio
@ -345,45 +344,51 @@ async def fan_out_spawn_fsp_daemons(
# blocks here until all fsp actors complete
class FspConfig(BaseModel):
class Config:
validate_assignment = True
name: str
period: int
@asynccontextmanager
async def open_sidepane(
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(
parent=linked.godwidget,
fields_schema={
'name': {
'label': '**fsp**:',
'type': 'select',
'default_value': [
f'{display_name}'
],
},
fields_schema=schema,
)
# TODO: generate this from input map
'period': {
'label': '**period**:',
'type': 'edit',
'default_value': 14,
},
},
)
sidepane.model = FspConfig(
# https://pydantic-docs.helpmanual.io/usage/models/#dynamic-model-creation
FspConfig = create_model(
'FspConfig',
name=display_name,
period=14,
**defaults,
)
sidepane.model = FspConfig()
# just a logger for now until we get fsp configs up and running.
async def settings_change(key: str, value: str) -> bool:
@ -410,7 +415,7 @@ async def run_fsp(
src_shm: ShmArray,
fsp_func_name: str,
display_name: str,
conf: dict[str, Any],
conf: dict[str, dict],
group_status_key: str,
loglevel: str,
@ -444,7 +449,7 @@ async def run_fsp(
ctx.open_stream() as stream,
open_sidepane(
linkedsplits,
display_name,
{display_name: conf},
) as sidepane,
):
@ -453,9 +458,10 @@ async def run_fsp(
if conf.get('overlay'):
chart = linkedsplits.chart
chart.draw_curve(
name='vwap',
name=display_name,
data=shm.array,
overlay=True,
color='default_light',
)
last_val_sticky = None
@ -658,22 +664,23 @@ async def maybe_open_vlm_display(
) -> 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:
async with open_sidepane(
linked, {
'volume': {
'params': {
'price_func': 'ohl3'
}
}
},
) as sidepane:
# built-in $vlm
shm = ohlcv
chart = linked.add_plot(
name='vlm',
name='volume',
array=shm.array,
array_key='volume',
@ -681,10 +688,10 @@ async def maybe_open_vlm_display(
# curve by default
ohlc=False,
style='step',
# vertical bars
# vertical bars, we do this internally ourselves
# stepMode=True,
# static_yrange=(0, 100),
)
# 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)
chart.update_curve_from_array(
'volume',
shm.array,
)
# size view to data once at outset
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
@ -805,20 +826,11 @@ async def display_symbol_data(
fsp_conf = {
'rsi': {
'fsp_func_name': 'rsi',
'period': 14,
'params': {'period': 14},
'chart_kwargs': {
'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):
@ -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,
):
# load initial fsp chain (otherwise known as "indicators")
@ -864,10 +882,7 @@ async def display_symbol_data(
)
async with (
# XXX: this slipped in during a commits refacotr,
# it's actually landing proper in #231
# maybe_open_vlm_display(linkedsplits, ohlcv),
maybe_open_vlm_display(linkedsplits, ohlcv),
open_order_mode(
feed,
chart,