Merge pull request #249 from pikers/basic_vlm_display

Basic vlm display
misc_ib_updates
goodboy 2022-01-23 15:20:43 -05:00 committed by GitHub
commit 242d02b1cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 265 additions and 161 deletions

View File

@ -25,6 +25,9 @@ from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QFrame, QFrame,
QWidget, QWidget,
QHBoxLayout,
QVBoxLayout,
QSplitter,
# QSizePolicy, # QSizePolicy,
) )
import numpy as np import numpy as np
@ -53,6 +56,7 @@ from ._style import (
) )
from ..data.feed import Feed from ..data.feed import Feed
from ..data._source import Symbol from ..data._source import Symbol
from ..data._sharedmem import ShmArray
from ..log import get_logger from ..log import get_logger
from ._interaction import ChartView from ._interaction import ChartView
from ._forms import FieldsForm from ._forms import FieldsForm
@ -64,11 +68,11 @@ log = get_logger(__name__)
class GodWidget(QWidget): class GodWidget(QWidget):
''' '''
"Our lord and savior, the holy child of window-shua, there is no "Our lord and savior, the holy child of window-shua, there is no
widget above thee." - 6|6 widget above thee." - 6||6
The highest level composed widget which contains layouts for The highest level composed widget which contains layouts for
organizing lower level charts as well as other widgets used to organizing charts as well as other sub-widgets used to control or
control or modify them. modify them.
''' '''
def __init__( def __init__(
@ -80,19 +84,19 @@ class GodWidget(QWidget):
super().__init__(parent) super().__init__(parent)
self.hbox = QtWidgets.QHBoxLayout(self) self.hbox = QHBoxLayout(self)
self.hbox.setContentsMargins(0, 0, 0, 0) self.hbox.setContentsMargins(0, 0, 0, 0)
self.hbox.setSpacing(6) self.hbox.setSpacing(6)
self.hbox.setAlignment(Qt.AlignTop) self.hbox.setAlignment(Qt.AlignTop)
self.vbox = QtWidgets.QVBoxLayout() self.vbox = QVBoxLayout()
self.vbox.setContentsMargins(0, 0, 0, 0) self.vbox.setContentsMargins(0, 0, 0, 0)
self.vbox.setSpacing(2) self.vbox.setSpacing(2)
self.vbox.setAlignment(Qt.AlignTop) self.vbox.setAlignment(Qt.AlignTop)
self.hbox.addLayout(self.vbox) self.hbox.addLayout(self.vbox)
# self.toolbar_layout = QtWidgets.QHBoxLayout() # self.toolbar_layout = QHBoxLayout()
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0) # self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
# self.vbox.addLayout(self.toolbar_layout) # self.vbox.addLayout(self.toolbar_layout)
@ -106,25 +110,8 @@ class GodWidget(QWidget):
# assigned in the startup func `_async_main()` # assigned in the startup func `_async_main()`
self._root_n: trio.Nursery = None self._root_n: trio.Nursery = None
def set_chart_symbol(
self,
symbol_key: str, # of form <fqsn>.<providername>
linkedsplits: 'LinkedSplits', # type: ignore
) -> None:
# re-sort org cache symbol list in LIFO order
cache = self._chart_cache
cache.pop(symbol_key, None)
cache[symbol_key] = linkedsplits
def get_chart_symbol(
self,
symbol_key: str,
) -> 'LinkedSplits': # type: ignore
return self._chart_cache.get(symbol_key)
# def init_timeframes_ui(self): # def init_timeframes_ui(self):
# self.tf_layout = QtWidgets.QHBoxLayout() # self.tf_layout = QHBoxLayout()
# self.tf_layout.setSpacing(0) # self.tf_layout.setSpacing(0)
# self.tf_layout.setContentsMargins(0, 12, 0, 0) # self.tf_layout.setContentsMargins(0, 12, 0, 0)
# time_frames = ('1M', '5M', '15M', '30M', '1H', '1D', '1W', 'MN') # time_frames = ('1M', '5M', '15M', '30M', '1H', '1D', '1W', 'MN')
@ -145,6 +132,23 @@ class GodWidget(QWidget):
# self.strategy_box = StrategyBoxWidget(self) # self.strategy_box = StrategyBoxWidget(self)
# self.toolbar_layout.addWidget(self.strategy_box) # self.toolbar_layout.addWidget(self.strategy_box)
def set_chart_symbol(
self,
symbol_key: str, # of form <fqsn>.<providername>
linkedsplits: 'LinkedSplits', # type: ignore
) -> None:
# re-sort org cache symbol list in LIFO order
cache = self._chart_cache
cache.pop(symbol_key, None)
cache[symbol_key] = linkedsplits
def get_chart_symbol(
self,
symbol_key: str,
) -> 'LinkedSplits': # type: ignore
return self._chart_cache.get(symbol_key)
async def load_symbol( async def load_symbol(
self, self,
@ -255,7 +259,7 @@ class ChartnPane(QFrame):
''' '''
sidepane: FieldsForm sidepane: FieldsForm
hbox: QtWidgets.QHBoxLayout hbox: QHBoxLayout
chart: Optional['ChartPlotWidget'] = None chart: Optional['ChartPlotWidget'] = None
def __init__( def __init__(
@ -271,7 +275,7 @@ class ChartnPane(QFrame):
self.sidepane = sidepane self.sidepane = sidepane
self.chart = None self.chart = None
hbox = self.hbox = QtWidgets.QHBoxLayout(self) hbox = self.hbox = QHBoxLayout(self)
hbox.setAlignment(Qt.AlignTop | Qt.AlignLeft) hbox.setAlignment(Qt.AlignTop | Qt.AlignLeft)
hbox.setContentsMargins(0, 0, 0, 0) hbox.setContentsMargins(0, 0, 0, 0)
hbox.setSpacing(3) hbox.setSpacing(3)
@ -281,21 +285,14 @@ class ChartnPane(QFrame):
class LinkedSplits(QWidget): class LinkedSplits(QWidget):
''' '''
Widget that holds a central chart plus derived Composite that holds a central chart plus a set of (derived)
subcharts computed from the original data set apart subcharts (usually computed from the original data) arranged in
by splitters for resizing. a splitter for resizing.
A single internal references to the data is maintained A single internal references to the data is maintained
for each chart and can be updated externally. for each chart and can be updated externally.
''' '''
long_pen = pg.mkPen('#006000')
long_brush = pg.mkBrush('#00ff00')
short_pen = pg.mkPen('#600000')
short_brush = pg.mkBrush('#ff0000')
zoomIsDisabled = QtCore.pyqtSignal(bool)
def __init__( def __init__(
self, self,
@ -325,11 +322,11 @@ class LinkedSplits(QWidget):
# self.xaxis_ind.setStyle(showValues=False) # self.xaxis_ind.setStyle(showValues=False)
# self.xaxis.hide() # self.xaxis.hide()
self.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical) self.splitter = 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 = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(self.splitter) self.layout.addWidget(self.splitter)
@ -341,20 +338,28 @@ class LinkedSplits(QWidget):
def set_split_sizes( def set_split_sizes(
self, self,
# prop: float = 0.375, # proportion allocated to consumer subcharts prop: Optional[float] = None,
prop: float = 5/8,
) -> None: ) -> None:
'''Set the proportion of space allocated for linked subcharts. '''Set the proportion of space allocated for linked subcharts.
''' '''
ln = len(self.subplots)
if not prop:
# proportion allocated to consumer subcharts
if ln < 2:
prop = 1/(.666 * 6)
elif ln >= 2:
prop = 3/8
major = 1 - prop major = 1 - prop
min_h_ind = int((self.height() * prop) / len(self.subplots)) min_h_ind = int((self.height() * prop) / ln)
sizes = [int(self.height() * major)] sizes = [int(self.height() * major)]
sizes.extend([min_h_ind] * len(self.subplots)) sizes.extend([min_h_ind] * ln)
self.splitter.setSizes(sizes) # , int(self.height()*0.2) self.splitter.setSizes(sizes)
def focus(self) -> None: def focus(self) -> None:
if self.chart is not None: if self.chart is not None:
@ -495,8 +500,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 +521,22 @@ 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_light',
)
elif style == 'step':
cpw.draw_curve(
name,
array,
array_key=array_key,
step_mode=True,
color='davies',
fill_color='davies',
)
else: else:
raise ValueError(f"Chart style {style} is currently unsupported") raise ValueError(f"Chart style {style} is currently unsupported")
@ -523,14 +544,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 +614,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
@ -619,7 +633,8 @@ class ChartPlotWidget(pg.PlotWidget):
'ohlc': array, 'ohlc': array,
} }
self._graphics = {} # registry of underlying graphics self._graphics = {} # registry of underlying graphics
self._overlays = set() # registry of overlay curve names # registry of overlay curve names
self._overlays: dict[str, ShmArray] = {}
self._feeds: dict[Symbol, Feed] = {} self._feeds: dict[Symbol, Feed] = {}
@ -732,6 +747,7 @@ class ChartPlotWidget(pg.PlotWidget):
self._vb.setXRange( self._vb.setXRange(
min=l + 1, min=l + 1,
max=r + 1, max=r + 1,
# TODO: holy shit, wtf dude... why tf would this not be 0 by # TODO: holy shit, wtf dude... why tf would this not be 0 by
# default... speechless. # default... speechless.
padding=0, padding=0,
@ -772,7 +788,7 @@ class ChartPlotWidget(pg.PlotWidget):
update_func=ContentsLabel.update_from_ohlc, update_func=ContentsLabel.update_from_ohlc,
) )
self._add_sticky(name) self._add_sticky(name, bg_color='davies')
return graphics return graphics
@ -784,7 +800,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,15 +810,18 @@ class ChartPlotWidget(pg.PlotWidget):
the input array ``data``. the input array ``data``.
""" """
_pdi_defaults = { color = color or self.pen_color or 'default_light'
'pen': pg.mkPen(hcolor(color)), pdi_kwargs.update({
} 'color': color
pdi_kwargs.update(_pdi_defaults) })
data_key = array_key or name data_key = array_key or name
# pg internals for reference.
# curve = pg.PlotDataItem( # curve = pg.PlotDataItem(
# curve = pg.PlotCurveItem( # curve = pg.PlotCurveItem(
# yah, we wrote our own B)
curve = FastAppendCurve( curve = FastAppendCurve(
y=data[data_key], y=data[data_key],
x=data['index'], x=data['index'],
@ -840,14 +859,14 @@ class ChartPlotWidget(pg.PlotWidget):
if overlay: if overlay:
anchor_at = ('bottom', 'left') anchor_at = ('bottom', 'left')
self._overlays.add(name) self._overlays[name] = None
else: else:
anchor_at = ('top', 'left') anchor_at = ('top', 'left')
# TODO: something instead of stickies for overlays # TODO: something instead of stickies for overlays
# (we need something that avoids clutter on x-axis). # (we need something that avoids clutter on x-axis).
self._add_sticky(name, bg_color='default_light') self._add_sticky(name, bg_color=color)
if self.linked.cursor: if self.linked.cursor:
self.linked.cursor.add_curve_cursor(self, curve) self.linked.cursor.add_curve_cursor(self, curve)
@ -861,6 +880,7 @@ class ChartPlotWidget(pg.PlotWidget):
return curve return curve
# TODO: make this a ctx mngr
def _add_sticky( def _add_sticky(
self, self,
@ -941,16 +961,19 @@ class ChartPlotWidget(pg.PlotWidget):
def _set_yrange( def _set_yrange(
self, self,
*, *,
yrange: Optional[tuple[float, float]] = None, yrange: Optional[tuple[float, float]] = None,
range_margin: float = 0.06, range_margin: float = 0.06,
bars_range: Optional[tuple[int, int, int, int]] = None
) -> 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':
@ -966,7 +989,20 @@ class ChartPlotWidget(pg.PlotWidget):
# Determine max, min y values in viewable x-range from data. # Determine max, min y values in viewable x-range from data.
# Make sure min bars/datums on screen is adhered. # Make sure min bars/datums on screen is adhered.
l, lbar, rbar, r = self.bars_range() l, lbar, rbar, r = bars_range or self.bars_range()
# TODO: we need a loop for auto-scaled subplots to all
# be triggered by one another
if self.name != 'volume':
vlm_chart = self.linked.subplots.get('volume')
if vlm_chart:
vlm_chart._set_yrange(bars_range=(l, lbar, rbar, r))
# curve = vlm_chart._graphics['volume']
# if rbar - lbar < 1500:
# # print('small range')
# curve._fill = True
# else:
# curve._fill = False
# figure out x-range in view such that user can scroll "off" # figure out x-range in view such that user can scroll "off"
# the data set up to the point where ``_min_points_to_show`` # the data set up to the point where ``_min_points_to_show``
@ -1003,15 +1039,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 +1110,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

@ -19,12 +19,13 @@ Real-time display tasks for charting / graphics.
''' '''
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
# from pprint import pformat
import time import time
from typing import Any
from types import ModuleType from types import ModuleType
from typing import Optional
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
@ -56,12 +57,14 @@ _clear_throttle_rate: int = 58 # Hz
_book_throttle_rate: int = 16 # Hz _book_throttle_rate: int = 16 # Hz
async def chart_from_quotes( async def update_chart_from_quotes(
chart: ChartPlotWidget, chart: ChartPlotWidget,
stream: tractor.MsgStream, stream: tractor.MsgStream,
ohlcv: np.ndarray, ohlcv: np.ndarray,
wap_in_history: bool = False, wap_in_history: bool = False,
vlm_chart: Optional[ChartPlotWidget] = None,
) -> None: ) -> None:
'''The 'main' (price) chart real-time update loop. '''The 'main' (price) chart real-time update loop.
@ -76,7 +79,8 @@ async def chart_from_quotes(
# - handle odd lot orders # - handle odd lot orders
# - update last open price correctly instead # - update last open price correctly instead
# of copying it from last bar's close # of copying it from last bar's close
# - 5 sec bar lookback-autocorrection like tws does? # - 1-5 sec bar lookback-autocorrection like tws does?
# (would require a background history checker task)
# update last price sticky # update last price sticky
last_price_sticky = chart._ysticks[chart.name] last_price_sticky = chart._ysticks[chart.name]
@ -84,6 +88,9 @@ async def chart_from_quotes(
*ohlcv.array[-1][['index', 'close']] *ohlcv.array[-1][['index', 'close']]
) )
if vlm_chart:
vlm_sticky = vlm_chart._ysticks['volume']
def maxmin(): def maxmin():
# TODO: implement this # TODO: implement this
# https://arxiv.org/abs/cs/0610046 # https://arxiv.org/abs/cs/0610046
@ -94,7 +101,7 @@ async def chart_from_quotes(
last_bars_range = chart.bars_range() last_bars_range = chart.bars_range()
l, lbar, rbar, r = last_bars_range l, lbar, rbar, r = last_bars_range
in_view = array[lbar - ifirst:rbar - ifirst] in_view = array[lbar - ifirst:rbar - ifirst + 1]
assert in_view.size assert in_view.size
@ -105,11 +112,20 @@ async def chart_from_quotes(
# sym = chart.name # sym = chart.name
# mx, mn = np.nanmax(in_view[sym]), np.nanmin(in_view[sym]) # mx, mn = np.nanmax(in_view[sym]), np.nanmin(in_view[sym])
return last_bars_range, mx, max(mn, 0) mx_vlm_in_view = 0
if vlm_chart:
mx_vlm_in_view = np.max(in_view['volume'])
return last_bars_range, mx, max(mn, 0), mx_vlm_in_view
chart.default_view() chart.default_view()
last_bars_range, last_mx, last_mn = maxmin() (
last_bars_range,
last_mx,
last_mn,
last_mx_vlm,
) = maxmin()
last, volume = ohlcv.array[-1][['close', 'volume']] last, volume = ohlcv.array[-1][['close', 'volume']]
@ -129,10 +145,10 @@ async def chart_from_quotes(
# - if trade volume jumps above / below prior L1 price # - if trade volume jumps above / below prior L1 price
# levels this might be dark volume we need to # levels this might be dark volume we need to
# present differently? # present differently -> likely dark vlm
tick_size = chart.linked.symbol.tick_size tick_size = chart.linked.symbol.tick_size
tick_margin = 2 * tick_size tick_margin = 3 * tick_size
last_ask = last_bid = last_clear = time.time() last_ask = last_bid = last_clear = time.time()
chart.show() chart.show()
@ -148,9 +164,40 @@ async def chart_from_quotes(
now = time.time() now = time.time()
# brange, mx_in_view, mn_in_view = maxmin()
(
brange,
mx_in_view,
mn_in_view,
mx_vlm_in_view,
) = maxmin()
l, lbar, rbar, r = brange
mx = mx_in_view + tick_margin
mn = mn_in_view - tick_margin
# NOTE: vlm may be written by the ``brokerd`` backend
# event though a tick sample is not emitted.
# TODO: show dark trades differently
# https://github.com/pikers/piker/issues/116
array = ohlcv.array
if vlm_chart:
# print(f"volume: {end['volume']}")
vlm_chart.update_curve_from_array('volume', array)
vlm_sticky.update_from_data(*array[-1][['index', 'volume']])
if (
mx_vlm_in_view != last_mx_vlm or
mx_vlm_in_view > last_mx_vlm
):
# print(f'mx vlm: {last_mx_vlm} -> {mx_vlm_in_view}')
vlm_chart._set_yrange(yrange=(0, mx_vlm_in_view * 1.375))
last_mx_vlm = mx_vlm_in_view
for tick in quote.get('ticks', ()): for tick in quote.get('ticks', ()):
# print(f"CHART: {quote['symbol']}: {tick}") # log.info(
# f"quotes: {pformat(quote['symbol'])}: {pformat(tick)}")
ticktype = tick.get('type') ticktype = tick.get('type')
price = tick.get('price') price = tick.get('price')
size = tick.get('size') size = tick.get('size')
@ -172,16 +219,13 @@ async def chart_from_quotes(
# set time of last graphics update # set time of last graphics update
last_clear = now last_clear = now
array = ohlcv.array
# update price sticky(s) # update price sticky(s)
end = array[-1] end = array[-1]
last_price_sticky.update_from_data( last_price_sticky.update_from_data(
*end[['index', 'close']] *end[['index', 'close']]
) )
# plot bars # update ohlc sampled price bars
# update price bar
chart.update_ohlc_from_array( chart.update_ohlc_from_array(
chart.name, chart.name,
array, array,
@ -214,11 +258,6 @@ async def chart_from_quotes(
# compute max and min trade values to display in view # compute max and min trade values to display in view
# TODO: we need a streaming minmax algorithm here, see # TODO: we need a streaming minmax algorithm here, see
# def above. # def above.
brange, mx_in_view, mn_in_view = maxmin()
l, lbar, rbar, r = brange
mx = mx_in_view + tick_margin
mn = mn_in_view - tick_margin
# XXX: prettty sure this is correct? # XXX: prettty sure this is correct?
# if ticktype in ('trade', 'last'): # if ticktype in ('trade', 'last'):
@ -242,16 +281,14 @@ async def chart_from_quotes(
elif ticktype in ('bid', 'bsize'): elif ticktype in ('bid', 'bsize'):
l1.bid_label.update_fields({'level': price, 'size': size}) l1.bid_label.update_fields({'level': price, 'size': size})
# update min price in view to keep bid on screen # in view y-range checking for auto-scale
mn = min(price - tick_margin, mn) # update the max/min price in view to keep bid/ask on screen
# update max price in view to keep ask on screen
mx = max(price + tick_margin, mx) mx = max(price + tick_margin, mx)
mn = min(price - tick_margin, mn)
if (mx > last_mx) or ( if (mx > last_mx) or (
mn < last_mn mn < last_mn
): ):
# print(f'new y range: {(mn, mx)}') # print(f'new y range: {(mn, mx)}')
chart._set_yrange( chart._set_yrange(
yrange=(mn, mx), yrange=(mn, mx),
# TODO: we should probably scale # TODO: we should probably scale
@ -345,45 +382,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 +453,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 +487,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 +496,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 +702,25 @@ 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")
yield
return
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',
@ -682,9 +729,13 @@ async def maybe_open_vlm_display(
# curve by default # curve by default
ohlc=False, ohlc=False,
# vertical bars # Draw vertical bars from zero.
# we do this internally ourselves since
# the curve item internals are pretty convoluted.
style='step',
# original pyqtgraph flag for reference
# 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 +754,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
@ -803,22 +868,13 @@ async def display_symbol_data(
# TODO: eventually we'll support some kind of n-compose syntax # TODO: eventually we'll support some kind of n-compose syntax
fsp_conf = { fsp_conf = {
'rsi': { # 'rsi': {
'fsp_func_name': 'rsi',
'period': 14,
'chart_kwargs': {
'static_yrange': (0, 100),
},
},
# # test for duplicate fsps on same chart
# 'rsi2': {
# '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),
# }, # },
# }, # },
} }
if has_vlm(ohlcv): if has_vlm(ohlcv):
@ -831,9 +887,16 @@ 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,
maybe_open_vlm_display(linkedsplits, ohlcv) as vlm_chart,
): ):
# load initial fsp chain (otherwise known as "indicators") # load initial fsp chain (otherwise known as "indicators")
ln.start_soon( ln.start_soon(
@ -849,11 +912,12 @@ async def display_symbol_data(
# start graphics update loop(s)after receiving first live quote # start graphics update loop(s)after receiving first live quote
ln.start_soon( ln.start_soon(
chart_from_quotes, update_chart_from_quotes,
chart, chart,
feed.stream, feed.stream,
ohlcv, ohlcv,
wap_in_history, wap_in_history,
vlm_chart,
) )
ln.start_soon( ln.start_soon(
@ -864,10 +928,6 @@ async def display_symbol_data(
) )
async with ( async with (
# XXX: this slipped in during a commits refacotr,
# it's actually landing proper in #231
# maybe_open_vlm_display(linkedsplits, ohlcv),
open_order_mode( open_order_mode(
feed, feed,
chart, chart,

View File

@ -205,19 +205,26 @@ def hcolor(name: str) -> str:
'svags': '#0a0e14', 'svags': '#0a0e14',
# fifty shades # fifty shades
'original': '#a9a9a9',
'gray': '#808080', # like the kick 'gray': '#808080', # like the kick
'grayer': '#4c4c4c', 'grayer': '#4c4c4c',
'grayest': '#3f3f3f', 'grayest': '#3f3f3f',
'i3': '#494D4F',
'jet': '#343434',
'cadet': '#91A3B0', 'cadet': '#91A3B0',
'marengo': '#91A3B0', 'marengo': '#91A3B0',
'charcoal': '#36454F',
'gunmetal': '#91A3B0', 'gunmetal': '#91A3B0',
'battleship': '#848482', 'battleship': '#848482',
'davies': '#555555',
# bluish
'charcoal': '#36454F',
# default bars
'bracket': '#666666', # like the logo 'bracket': '#666666', # like the logo
'original': '#a9a9a9',
# work well for filled polygons which want a 'bracket' feel
# going light to dark
'davies': '#555555',
'i3': '#494D4F',
'jet': '#343434',
# from ``qdarkstyle`` palette # from ``qdarkstyle`` palette
'default_darkest': DarkPalette.COLOR_BACKGROUND_1, 'default_darkest': DarkPalette.COLOR_BACKGROUND_1,