Port charting to new shm primary indexing

to_qpainterpath_and_beyond
Tyler Goodlet 2020-12-14 12:21:39 -05:00
parent 9930430721
commit c8537d59a8
1 changed files with 194 additions and 112 deletions

View File

@ -17,7 +17,7 @@
""" """
High level Qt chart widgets. High level Qt chart widgets.
""" """
from typing import Tuple, Dict, Any, Optional from typing import Tuple, Dict, Any, Optional, Callable
from functools import partial from functools import partial
from PyQt5 import QtCore, QtGui from PyQt5 import QtCore, QtGui
@ -105,6 +105,7 @@ class ChartSpace(QtGui.QWidget):
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')
btn_prefix = 'TF' btn_prefix = 'TF'
for tf in time_frames: for tf in time_frames:
btn_name = ''.join([btn_prefix, tf]) btn_name = ''.join([btn_prefix, tf])
btn = QtGui.QPushButton(tf) btn = QtGui.QPushButton(tf)
@ -112,6 +113,7 @@ class ChartSpace(QtGui.QWidget):
btn.setEnabled(False) btn.setEnabled(False)
setattr(self, btn_name, btn) setattr(self, btn_name, btn)
self.tf_layout.addWidget(btn) self.tf_layout.addWidget(btn)
self.toolbar_layout.addLayout(self.tf_layout) self.toolbar_layout.addLayout(self.tf_layout)
# XXX: strat loader/saver that we don't need yet. # XXX: strat loader/saver that we don't need yet.
@ -126,6 +128,8 @@ class ChartSpace(QtGui.QWidget):
ohlc: bool = True, ohlc: bool = True,
) -> None: ) -> None:
"""Load a new contract into the charting app. """Load a new contract into the charting app.
Expects a ``numpy`` structured array containing all the ohlcv fields.
""" """
# XXX: let's see if this causes mem problems # XXX: let's see if this causes mem problems
self.window.setWindowTitle(f'piker chart {symbol}') self.window.setWindowTitle(f'piker chart {symbol}')
@ -148,7 +152,8 @@ class ChartSpace(QtGui.QWidget):
if not self.v_layout.isEmpty(): if not self.v_layout.isEmpty():
self.v_layout.removeWidget(linkedcharts) self.v_layout.removeWidget(linkedcharts)
main_chart = linkedcharts.plot_main(s, data, ohlc=ohlc) main_chart = linkedcharts.plot_ohlc_main(s, data)
self.v_layout.addWidget(linkedcharts) self.v_layout.addWidget(linkedcharts)
return linkedcharts, main_chart return linkedcharts, main_chart
@ -176,7 +181,6 @@ class LinkedSplitCharts(QtGui.QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.signals_visible: bool = False self.signals_visible: bool = False
self._array: np.ndarray = None # main data source
self._ch: CrossHair = None # crosshair graphics self._ch: CrossHair = None # crosshair graphics
self.chart: ChartPlotWidget = None # main (ohlc) chart self.chart: ChartPlotWidget = None # main (ohlc) chart
self.subplots: Dict[Tuple[str, ...], ChartPlotWidget] = {} self.subplots: Dict[Tuple[str, ...], ChartPlotWidget] = {}
@ -212,20 +216,18 @@ class LinkedSplitCharts(QtGui.QWidget):
sizes.extend([min_h_ind] * len(self.subplots)) sizes.extend([min_h_ind] * len(self.subplots))
self.splitter.setSizes(sizes) # , int(self.height()*0.2) self.splitter.setSizes(sizes) # , int(self.height()*0.2)
def plot_main( def plot_ohlc_main(
self, self,
symbol: Symbol, symbol: Symbol,
array: np.ndarray, array: np.ndarray,
ohlc: bool = True, style: str = 'bar',
) -> 'ChartPlotWidget': ) -> 'ChartPlotWidget':
"""Start up and show main (price) chart and all linked subcharts. """Start up and show main (price) chart and all linked subcharts.
The data input struct array must include OHLC fields.
""" """
self.digits = symbol.digits() self.digits = symbol.digits()
# TODO: this should eventually be a view onto shared mem or some
# higher level type / API
self._array = array
# add crosshairs # add crosshairs
self._ch = CrossHair( self._ch = CrossHair(
linkedsplitcharts=self, linkedsplitcharts=self,
@ -235,11 +237,13 @@ class LinkedSplitCharts(QtGui.QWidget):
name=symbol.key, name=symbol.key,
array=array, array=array,
xaxis=self.xaxis, xaxis=self.xaxis,
ohlc=ohlc, style=style,
_is_main=True, _is_main=True,
) )
# add crosshair graphic # add crosshair graphic
self.chart.addItem(self._ch) self.chart.addItem(self._ch)
# axis placement
if _xaxis_at == 'bottom': if _xaxis_at == 'bottom':
self.chart.hideAxis('bottom') self.chart.hideAxis('bottom')
@ -253,7 +257,7 @@ class LinkedSplitCharts(QtGui.QWidget):
name: str, name: str,
array: np.ndarray, array: np.ndarray,
xaxis: DynamicDateAxis = None, xaxis: DynamicDateAxis = None,
ohlc: bool = False, style: str = 'line',
_is_main: bool = False, _is_main: bool = False,
**cpw_kwargs, **cpw_kwargs,
) -> 'ChartPlotWidget': ) -> 'ChartPlotWidget':
@ -263,7 +267,7 @@ class LinkedSplitCharts(QtGui.QWidget):
""" """
if self.chart is None and not _is_main: if self.chart is None and not _is_main:
raise RuntimeError( raise RuntimeError(
"A main plot must be created first with `.plot_main()`") "A main plot must be created first with `.plot_ohlc_main()`")
# source of our custom interactions # source of our custom interactions
cv = ChartView() cv = ChartView()
@ -277,6 +281,11 @@ class LinkedSplitCharts(QtGui.QWidget):
) )
cpw = ChartPlotWidget( cpw = ChartPlotWidget(
# this name will be used to register the primary
# graphics curve managed by the subchart
name=name,
array=array, array=array,
parent=self.splitter, parent=self.splitter,
axisItems={ axisItems={
@ -287,11 +296,12 @@ class LinkedSplitCharts(QtGui.QWidget):
cursor=self._ch, cursor=self._ch,
**cpw_kwargs, **cpw_kwargs,
) )
# give viewbox a reference to primary chart
# allowing for kb controls and interactions
# (see our custom view in `._interactions.py`)
cv.chart = cpw cv.chart = cpw
# this name will be used to register the primary
# graphics curve managed by the subchart
cpw.name = name
cpw.plotItem.vb.linked_charts = self cpw.plotItem.vb.linked_charts = self
cpw.setFrameStyle(QtGui.QFrame.StyledPanel) # | QtGui.QFrame.Plain) cpw.setFrameStyle(QtGui.QFrame.StyledPanel) # | QtGui.QFrame.Plain)
cpw.hideButtons() cpw.hideButtons()
@ -305,11 +315,15 @@ class LinkedSplitCharts(QtGui.QWidget):
self._ch.add_plot(cpw) self._ch.add_plot(cpw)
# draw curve graphics # draw curve graphics
if ohlc: if style == 'bar':
cpw.draw_ohlc(name, array) cpw.draw_ohlc(name, array)
else:
elif style == 'line':
cpw.draw_curve(name, array) cpw.draw_curve(name, array)
else:
raise ValueError(f"Chart style {style} is currently unsupported")
if not _is_main: if not _is_main:
# track by name # track by name
self.subplots[name] = cpw self.subplots[name] = cpw
@ -319,6 +333,8 @@ class LinkedSplitCharts(QtGui.QWidget):
# XXX: we need this right? # XXX: we need this right?
# self.splitter.addWidget(cpw) # self.splitter.addWidget(cpw)
else:
assert style == 'bar', 'main chart must be OHLC'
return cpw return cpw
@ -344,6 +360,7 @@ class ChartPlotWidget(pg.PlotWidget):
def __init__( def __init__(
self, self,
# the data view we generate graphics from # the data view we generate graphics from
name: str,
array: np.ndarray, array: np.ndarray,
static_yrange: Optional[Tuple[float, float]] = None, static_yrange: Optional[Tuple[float, float]] = None,
cursor: Optional[CrossHair] = None, cursor: Optional[CrossHair] = None,
@ -356,17 +373,26 @@ class ChartPlotWidget(pg.PlotWidget):
# parent=None, # parent=None,
# plotItem=None, # plotItem=None,
# antialias=True, # antialias=True,
useOpenGL=True,
**kwargs **kwargs
) )
self.name = name
# self.setViewportMargins(0, 0, 0, 0) # self.setViewportMargins(0, 0, 0, 0)
self._array = array # readonly view of data self._ohlc = array # readonly view of ohlc data
self.default_view()
self._arrays = {} # readonly view of overlays self._arrays = {} # readonly view of overlays
self._graphics = {} # registry of underlying graphics self._graphics = {} # registry of underlying graphics
self._overlays = {} # registry of overlay curves self._overlays = set() # registry of overlay curve names
self._labels = {} # registry of underlying graphics self._labels = {} # registry of underlying graphics
self._ysticks = {} # registry of underlying graphics self._ysticks = {} # registry of underlying graphics
self._vb = self.plotItem.vb self._vb = self.plotItem.vb
self._static_yrange = static_yrange # for "known y-range style" self._static_yrange = static_yrange # for "known y-range style"
self._view_mode: str = 'follow' self._view_mode: str = 'follow'
self._cursor = cursor # placehold for mouse self._cursor = cursor # placehold for mouse
@ -377,6 +403,7 @@ class ChartPlotWidget(pg.PlotWidget):
# show background grid # show background grid
self.showGrid(x=True, y=True, alpha=0.5) self.showGrid(x=True, y=True, alpha=0.5)
# TODO: stick in config
# use cross-hair for cursor? # use cross-hair for cursor?
# self.setCursor(QtCore.Qt.CrossCursor) # self.setCursor(QtCore.Qt.CrossCursor)
@ -391,22 +418,25 @@ class ChartPlotWidget(pg.PlotWidget):
self._vb.sigResized.connect(self._set_yrange) self._vb.sigResized.connect(self._set_yrange)
def last_bar_in_view(self) -> bool: def last_bar_in_view(self) -> bool:
self._array[-1]['index'] self._ohlc[-1]['index']
def update_contents_labels( def update_contents_labels(
self, self,
index: int, index: int,
# array_name: str, # array_name: str,
) -> None: ) -> None:
if index >= 0 and index < len(self._array): if index >= 0 and index < self._ohlc[-1]['index']:
for name, (label, update) in self._labels.items(): for name, (label, update) in self._labels.items():
if name is self.name: if name is self.name:
array = self._array array = self._ohlc
else: else:
array = self._arrays[name] array = self._arrays[name]
try:
update(index, array) update(index, array)
except IndexError:
log.exception(f"Failed to update label: {name}")
def _set_xlimits( def _set_xlimits(
self, self,
@ -430,8 +460,11 @@ 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()
lbar = max(l, 0) a = self._ohlc
rbar = min(r, len(self._array)) lbar = max(l, a[0]['index'])
rbar = min(r, a[-1]['index'])
# lbar = max(l, 0)
# rbar = min(r, len(self._ohlc))
return l, lbar, rbar, r return l, lbar, rbar, r
def default_view( def default_view(
@ -441,7 +474,8 @@ class ChartPlotWidget(pg.PlotWidget):
"""Set the view box to the "default" startup view of the scene. """Set the view box to the "default" startup view of the scene.
""" """
xlast = self._array[index]['index'] xlast = self._ohlc[index]['index']
print(xlast)
begin = xlast - _bars_to_left_in_follow_mode begin = xlast - _bars_to_left_in_follow_mode
end = xlast + _bars_from_right_in_follow_mode end = xlast + _bars_from_right_in_follow_mode
@ -462,7 +496,7 @@ class ChartPlotWidget(pg.PlotWidget):
self._vb.setXRange( self._vb.setXRange(
min=l + 1, min=l + 1,
max=r + 1, max=r + 1,
# 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,
) )
@ -477,6 +511,7 @@ class ChartPlotWidget(pg.PlotWidget):
"""Draw OHLC datums to chart. """Draw OHLC datums to chart.
""" """
graphics = style(self.plotItem) graphics = style(self.plotItem)
# adds all bar/candle graphics objects for each data point in # adds all bar/candle graphics objects for each data point in
# the np array buffer to be drawn on next render cycle # the np array buffer to be drawn on next render cycle
self.addItem(graphics) self.addItem(graphics)
@ -486,10 +521,12 @@ class ChartPlotWidget(pg.PlotWidget):
self._graphics[name] = graphics self._graphics[name] = graphics
label = ContentsLabel(chart=self, anchor_at=('top', 'left')) self.add_contents_label(
self._labels[name] = (label, partial(label.update_from_ohlc, name)) name,
label.show() anchor_at=('top', 'left'),
self.update_contents_labels(len(data) - 1) #, name) update_func=ContentsLabel.update_from_ohlc,
)
self.update_contents_labels(len(data) - 1)
self._add_sticky(name) self._add_sticky(name)
@ -500,49 +537,74 @@ class ChartPlotWidget(pg.PlotWidget):
name: str, name: str,
data: np.ndarray, data: np.ndarray,
overlay: bool = False, overlay: bool = False,
color: str = 'default_light',
add_label: bool = True,
**pdi_kwargs, **pdi_kwargs,
) -> pg.PlotDataItem: ) -> pg.PlotDataItem:
# draw the indicator as a plain curve """Draw a "curve" (line plot graphics) for the provided data in
the input array ``data``.
"""
_pdi_defaults = { _pdi_defaults = {
'pen': pg.mkPen(hcolor('default_light')), 'pen': pg.mkPen(hcolor(color)),
} }
pdi_kwargs.update(_pdi_defaults) pdi_kwargs.update(_pdi_defaults)
curve = pg.PlotDataItem( curve = pg.PlotDataItem(
data[name], y=data[name],
x=data['index'],
# antialias=True, # antialias=True,
name=name, name=name,
# TODO: see how this handles with custom ohlcv bars graphics # TODO: see how this handles with custom ohlcv bars graphics
# and/or if we can implement something similar for OHLC graphics
clipToView=True, clipToView=True,
**pdi_kwargs, **pdi_kwargs,
) )
self.addItem(curve) self.addItem(curve)
# register overlay curve with name # register curve graphics and backing array for name
self._graphics[name] = curve self._graphics[name] = curve
if overlay:
anchor_at = ('bottom', 'right')
self._overlays[name] = curve
self._arrays[name] = data self._arrays[name] = data
if overlay:
anchor_at = ('bottom', 'left')
self._overlays.add(name)
else: else:
anchor_at = ('top', 'right') 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='default_light')
label = ContentsLabel(chart=self, anchor_at=anchor_at) if add_label:
self._labels[name] = (label, partial(label.update_from_value, name)) self.add_contents_label(name, anchor_at=anchor_at)
label.show() self.update_contents_labels(len(data) - 1)
self.update_contents_labels(len(data) - 1) #, name)
if self._cursor: if self._cursor:
self._cursor.add_curve_cursor(self, curve) self._cursor.add_curve_cursor(self, curve)
return curve return curve
def add_contents_label(
self,
name: str,
anchor_at: Tuple[str, str] = ('top', 'left'),
update_func: Callable = ContentsLabel.update_from_value,
) -> ContentsLabel:
label = ContentsLabel(chart=self, anchor_at=anchor_at)
self._labels[name] = (
# calls class method on instance
label,
partial(update_func, label, name)
)
label.show()
return label
def _add_sticky( def _add_sticky(
self, self,
name: str, name: str,
@ -569,7 +631,7 @@ class ChartPlotWidget(pg.PlotWidget):
"""Update the named internal graphics from ``array``. """Update the named internal graphics from ``array``.
""" """
self._array = array self._ohlc = array
graphics = self._graphics[name] graphics = self._graphics[name]
graphics.update_from_array(array, **kwargs) graphics.update_from_array(array, **kwargs)
return graphics return graphics
@ -584,14 +646,18 @@ class ChartPlotWidget(pg.PlotWidget):
""" """
if name not in self._overlays: if name not in self._overlays:
self._array = array self._ohlc = array
else: else:
self._arrays[name] = array self._arrays[name] = array
curve = self._graphics[name] curve = self._graphics[name]
# TODO: we should instead implement a diff based # TODO: we should instead implement a diff based
# "only update with new items" on the pg.PlotDataItem # "only update with new items" on the pg.PlotCurveItem
curve.setData(array[name], **kwargs) # one place to dig around this might be the `QBackingStore`
# https://doc.qt.io/qt-5/qbackingstore.html
curve.setData(y=array[name], x=array['index'], **kwargs)
return curve return curve
def _set_yrange( def _set_yrange(
@ -625,8 +691,9 @@ class ChartPlotWidget(pg.PlotWidget):
# TODO: logic to check if end of bars in view # TODO: logic to check if end of bars in view
extra = view_len - _min_points_to_show extra = view_len - _min_points_to_show
begin = 0 - extra begin = self._ohlc[0]['index'] - extra
end = len(self._array) - 1 + extra # end = len(self._ohlc) - 1 + extra
end = self._ohlc[-1]['index'] - 1 + extra
# XXX: test code for only rendering lines for the bars in view. # XXX: test code for only rendering lines for the bars in view.
# This turns out to be very very poor perf when scaling out to # This turns out to be very very poor perf when scaling out to
@ -642,10 +709,15 @@ class ChartPlotWidget(pg.PlotWidget):
# f"view_len: {view_len}, bars_len: {bars_len}\n" # f"view_len: {view_len}, bars_len: {bars_len}\n"
# f"begin: {begin}, end: {end}, extra: {extra}" # f"begin: {begin}, end: {end}, extra: {extra}"
# ) # )
self._set_xlimits(begin, end) # self._set_xlimits(begin, end)
# TODO: this should be some kind of numpy view api # TODO: this should be some kind of numpy view api
bars = self._array[lbar:rbar] # bars = self._ohlc[lbar:rbar]
a = self._ohlc
ifirst = a[0]['index']
bars = a[lbar - ifirst:rbar - ifirst]
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}")
@ -731,10 +803,6 @@ async def _async_main(
# chart_app.init_search() # chart_app.init_search()
# XXX: bug zone if you try to ctl-c after this we get hangs again?
# wtf...
# await tractor.breakpoint()
# historical data fetch # historical data fetch
brokermod = brokers.get_brokermod(brokername) brokermod = brokers.get_brokermod(brokername)
@ -747,30 +815,28 @@ async def _async_main(
ohlcv = feed.shm ohlcv = feed.shm
bars = ohlcv.array bars = ohlcv.array
# TODO: when we start messing with line charts
# c = np.zeros(len(bars), dtype=[
# (sym, bars.dtype.fields['close'][0]),
# ('index', 'i4'),
# ])
# c[sym] = bars['close']
# c['index'] = bars['index']
# linked_charts, chart = chart_app.load_symbol(sym, c, ohlc=False)
# load in symbol's ohlc data # load in symbol's ohlc data
# await tractor.breakpoint()
linked_charts, chart = chart_app.load_symbol(sym, bars) linked_charts, chart = chart_app.load_symbol(sym, bars)
# plot historical vwap if available # plot historical vwap if available
vwap_in_history = False wap_in_history = False
if 'vwap' in bars.dtype.fields:
vwap_in_history = True if brokermod._show_wap_in_history:
if 'bar_wap' in bars.dtype.fields:
wap_in_history = True
chart.draw_curve( chart.draw_curve(
name='vwap', name='bar_wap',
data=bars, data=bars,
overlay=True, add_label=False,
) )
chart._set_yrange() chart._set_yrange()
# TODO: a data view api that makes this less shit
chart._shm = ohlcv
# eventually we'll support some kind of n-compose syntax # eventually we'll support some kind of n-compose syntax
fsp_conf = { fsp_conf = {
'vwap': { 'vwap': {
@ -799,19 +865,13 @@ async def _async_main(
loglevel, loglevel,
) )
# update last price sticky
last_price_sticky = chart._ysticks[chart.name]
last_price_sticky.update_from_data(
*ohlcv.array[-1][['index', 'close']]
)
# start graphics update loop(s)after receiving first live quote # start graphics update loop(s)after receiving first live quote
n.start_soon( n.start_soon(
chart_from_quotes, chart_from_quotes,
chart, chart,
feed.stream, feed.stream,
ohlcv, ohlcv,
vwap_in_history, wap_in_history,
) )
# wait for a first quote before we start any update tasks # wait for a first quote before we start any update tasks
@ -834,7 +894,7 @@ async def chart_from_quotes(
chart: ChartPlotWidget, chart: ChartPlotWidget,
stream, stream,
ohlcv: np.ndarray, ohlcv: np.ndarray,
vwap_in_history: bool = False, wap_in_history: bool = False,
) -> None: ) -> None:
"""The 'main' (price) chart real-time update loop. """The 'main' (price) chart real-time update loop.
@ -847,29 +907,40 @@ async def chart_from_quotes(
# - 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? # - 5 sec bar lookback-autocorrection like tws does?
# update last price sticky
last_price_sticky = chart._ysticks[chart.name] last_price_sticky = chart._ysticks[chart.name]
last_price_sticky.update_from_data(
*ohlcv.array[-1][['index', 'close']]
)
def maxmin(): def maxmin():
# TODO: implement this # TODO: implement this
# https://arxiv.org/abs/cs/0610046 # https://arxiv.org/abs/cs/0610046
# https://github.com/lemire/pythonmaxmin # https://github.com/lemire/pythonmaxmin
array = chart._array array = chart._ohlc
ifirst = array[0]['index']
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:rbar] in_view = array[lbar - ifirst:rbar - ifirst]
assert in_view.size
mx, mn = np.nanmax(in_view['high']), np.nanmin(in_view['low']) mx, mn = np.nanmax(in_view['high']), np.nanmin(in_view['low'])
# TODO: when we start using line charts # TODO: when we start using line charts, probably want to make
# this an overloaded call on our `DataView
# 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, mn return last_bars_range, mx, mn
last_bars_range, last_mx, last_mn = maxmin()
chart.default_view() chart.default_view()
last_bars_range, last_mx, last_mn = maxmin()
last, volume = ohlcv.array[-1][['close', 'volume']] last, volume = ohlcv.array[-1][['close', 'volume']]
l1 = L1Labels( l1 = L1Labels(
@ -889,7 +960,6 @@ async def chart_from_quotes(
async for quotes in stream: async for quotes in stream:
for sym, quote in quotes.items(): for sym, quote in quotes.items():
# print(f'CHART: {quote}')
for tick in quote.get('ticks', ()): for tick in quote.get('ticks', ()):
@ -898,7 +968,14 @@ async def chart_from_quotes(
price = tick.get('price') price = tick.get('price')
size = tick.get('size') size = tick.get('size')
if ticktype in ('trade', 'utrade'): # compute max and min trade values to display in view
# TODO: we need a streaming minmax algorithm here, see
# def above.
brange, mx_in_view, mn_in_view = maxmin()
l, lbar, rbar, r = brange
if ticktype in ('trade', 'utrade', 'last'):
array = ohlcv.array array = ohlcv.array
# update price sticky(s) # update price sticky(s)
@ -907,25 +984,16 @@ async def chart_from_quotes(
*last[['index', 'close']] *last[['index', 'close']]
) )
# plot bars
# update price bar # update price bar
chart.update_ohlc_from_array( chart.update_ohlc_from_array(
chart.name, chart.name,
array, array,
) )
# chart.update_curve_from_array( if wap_in_history:
# chart.name, # update vwap overlay line
# TODO: when we start using line charts chart.update_curve_from_array('bar_wap', ohlcv.array)
# np.array(array['close'], dtype=[(chart.name, 'f8')])
# if vwap_in_history:
# # update vwap overlay line
# chart.update_curve_from_array('vwap', ohlcv.array)
# compute max and min trade values to display in view
# TODO: we need a streaming minmax algorithm here, see
# def above.
brange, mx_in_view, mn_in_view = maxmin()
# XXX: prettty sure this is correct? # XXX: prettty sure this is correct?
# if ticktype in ('trade', 'last'): # if ticktype in ('trade', 'last'):
@ -1021,12 +1089,14 @@ async def spawn_fsps(
# spawn closure, can probably define elsewhere # spawn closure, can probably define elsewhere
async def spawn_fsp_daemon( async def spawn_fsp_daemon(
fsp_name, fsp_name: str,
conf, display_name: str,
conf: dict,
): ):
"""Start an fsp subactor async. """Start an fsp subactor async.
""" """
print(f'FSP NAME: {fsp_name}')
portal = await n.run_in_actor( portal = await n.run_in_actor(
# name as title of sub-chart # name as title of sub-chart
@ -1057,6 +1127,7 @@ async def spawn_fsps(
ln.start_soon( ln.start_soon(
spawn_fsp_daemon, spawn_fsp_daemon,
fsp_func_name, fsp_func_name,
display_name,
conf, conf,
) )
@ -1081,6 +1152,8 @@ async def update_signals(
) -> None: ) -> None:
"""FSP stream chart update loop. """FSP stream chart update loop.
This is called once for each entry in the fsp
config map.
""" """
shm = conf['shm'] shm = conf['shm']
@ -1094,6 +1167,7 @@ async def update_signals(
last_val_sticky = None last_val_sticky = None
else: else:
chart = linked_charts.add_plot( chart = linked_charts.add_plot(
name=fsp_func_name, name=fsp_func_name,
array=shm.array, array=shm.array,
@ -1112,6 +1186,7 @@ async def update_signals(
# fsp_func_name # fsp_func_name
) )
# read last value
array = shm.array array = shm.array
value = array[fsp_func_name][-1] value = array[fsp_func_name][-1]
@ -1119,7 +1194,8 @@ async def update_signals(
last_val_sticky.update_from_data(-1, value) last_val_sticky.update_from_data(-1, value)
chart.update_curve_from_array(fsp_func_name, array) chart.update_curve_from_array(fsp_func_name, array)
chart.default_view()
chart._shm = shm
# 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.
@ -1132,23 +1208,28 @@ async def update_signals(
# graphics.curve.setFillLevel(50) # graphics.curve.setFillLevel(50)
# add moveable over-[sold/bought] lines # add moveable over-[sold/bought] lines
level_line(chart, 30) # and labels only for the 70/30 lines
level_line(chart, 70, orient_v='top') level_line(chart, 20, show_label=False)
level_line(chart, 30, orient_v='top')
level_line(chart, 70, orient_v='bottom')
level_line(chart, 80, orient_v='top', show_label=False)
chart._shm = shm
chart._set_yrange() chart._set_yrange()
stream = conf['stream'] stream = conf['stream']
# update chart graphics # update chart graphics
async for value in stream: async for value in stream:
# p = pg.debug.Profiler(disabled=False, delayed=False)
# read last
array = shm.array array = shm.array
value = array[-1][fsp_func_name] value = array[-1][fsp_func_name]
if last_val_sticky: if last_val_sticky:
last_val_sticky.update_from_data(-1, value) last_val_sticky.update_from_data(-1, value)
# update graphics
chart.update_curve_from_array(fsp_func_name, array) chart.update_curve_from_array(fsp_func_name, array)
# p('rendered rsi datum')
async def check_for_new_bars(feed, ohlcv, linked_charts): async def check_for_new_bars(feed, ohlcv, linked_charts):
@ -1199,7 +1280,7 @@ async def check_for_new_bars(feed, ohlcv, linked_charts):
# resize view # resize view
# price_chart._set_yrange() # price_chart._set_yrange()
for name, curve in price_chart._overlays.items(): for name in price_chart._overlays:
price_chart.update_curve_from_array( price_chart.update_curve_from_array(
name, name,
@ -1207,15 +1288,16 @@ async def check_for_new_bars(feed, ohlcv, linked_charts):
) )
# # TODO: standard api for signal lookups per plot # # TODO: standard api for signal lookups per plot
# if name in price_chart._array.dtype.fields: # if name in price_chart._ohlc.dtype.fields:
# # should have already been incremented above # # should have already been incremented above
# price_chart.update_curve_from_array(name, price_chart._array) # price_chart.update_curve_from_array(name, price_chart._ohlc)
for name, chart in linked_charts.subplots.items(): for name, chart in linked_charts.subplots.items():
chart.update_curve_from_array(chart.name, chart._shm.array) chart.update_curve_from_array(chart.name, chart._shm.array)
# chart._set_yrange() # chart._set_yrange()
# shift the view if in follow mode
price_chart.increment_view() price_chart.increment_view()