Add support for overlay curves and fixed y-range

Allow passing a fixed ylow, yhigh tuple to `._set_yrange()` which avoids
recomputing the range from data if desired (eg. rsi-like bounded
signals). Add support for overlay curves to the OHLC chart and add basic
support to brokers which provide a historical 'vwap`. The data array
increment logic had to be tweaked to copy the vwap from the last bar.

Oh, and hack the subchart curves with two extra prepended datums to make
them align "better" with the ohlc main chart; need to talk to
`pyqtgraph` core about how to do this more correctly.
bar_select
Tyler Goodlet 2020-09-11 13:16:11 -04:00
parent e91ba55d68
commit eb5d64ceef
1 changed files with 111 additions and 41 deletions

View File

@ -292,6 +292,7 @@ class ChartPlotWidget(pg.PlotWidget):
self._ysticks = {} # registry of underlying graphics self._ysticks = {} # registry of underlying graphics
self._yrange = yrange self._yrange = yrange
self._vb = self.plotItem.vb self._vb = self.plotItem.vb
self._static_yrange = None
# show only right side axes # show only right side axes
self.hideAxis('left') self.hideAxis('left')
@ -368,7 +369,7 @@ class ChartPlotWidget(pg.PlotWidget):
# Ogi says: "use ..." # Ogi says: "use ..."
label = pg.LabelItem( label = pg.LabelItem(
justify='left', justify='left',
size='5pt', size='4pt',
) )
self.scene().addItem(label) self.scene().addItem(label)
@ -383,6 +384,7 @@ class ChartPlotWidget(pg.PlotWidget):
self._labels[name] = (label, update) self._labels[name] = (label, update)
self._update_contents_label(index=-1) self._update_contents_label(index=-1)
label.show()
# set xrange limits # set xrange limits
xlast = data[-1]['index'] xlast = data[-1]['index']
@ -398,13 +400,22 @@ class ChartPlotWidget(pg.PlotWidget):
self, self,
name: str, name: str,
data: np.ndarray, data: np.ndarray,
overlay: bool = False,
**pdi_kwargs,
) -> pg.PlotDataItem: ) -> pg.PlotDataItem:
# draw the indicator as a plain curve # draw the indicator as a plain curve
_pdi_defaults = {
'pen': pg.mkPen(hcolor('default_light')),
}
pdi_kwargs.update(_pdi_defaults)
curve = pg.PlotDataItem( curve = pg.PlotDataItem(
data, data,
antialias=True, antialias=True,
name=name,
# TODO: see how this handles with custom ohlcv bars graphics # TODO: see how this handles with custom ohlcv bars graphics
clipToView=True, clipToView=True,
**pdi_kwargs,
) )
self.addItem(curve) self.addItem(curve)
@ -417,8 +428,15 @@ class ChartPlotWidget(pg.PlotWidget):
# XXX: How to stack labels vertically? # XXX: How to stack labels vertically?
label = pg.LabelItem( label = pg.LabelItem(
justify='left', justify='left',
size='5pt', size='4pt',
) )
label.setParentItem(self._vb)
if overlay:
# position bottom left if an overlay
label.anchor(itemPos=(0, 1), parentPos=(0, 1), offset=(0, 25))
label.show()
self.scene().addItem(label) self.scene().addItem(label)
def update(index: int) -> None: def update(index: int) -> None:
@ -471,6 +489,8 @@ class ChartPlotWidget(pg.PlotWidget):
def _set_yrange( def _set_yrange(
self, self,
*,
yrange: Optional[Tuple[float, float]] = None,
) -> None: ) -> None:
"""Set the viewable y-range based on embedded data. """Set the viewable y-range based on embedded data.
@ -483,38 +503,47 @@ class ChartPlotWidget(pg.PlotWidget):
# figure out x-range in view such that user can scroll "off" the data # figure out x-range in view such that user can scroll "off" the data
# set up to the point where ``_min_points_to_show`` are left. # set up to the point where ``_min_points_to_show`` are left.
# if l < lbar or r > rbar: # if l < lbar or r > rbar:
bars_len = rbar - lbar
view_len = r - l view_len = r - l
# 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 = 0 - extra
end = len(self._array) - 1 + extra end = len(self._array) - 1 + extra
log.trace( # bars_len = rbar - lbar
f"\nl: {l}, lbar: {lbar}, rbar: {rbar}, r: {r}\n" # log.trace(
f"view_len: {view_len}, bars_len: {bars_len}\n" # f"\nl: {l}, lbar: {lbar}, rbar: {rbar}, r: {r}\n"
f"begin: {begin}, end: {end}, extra: {extra}" # f"view_len: {view_len}, bars_len: {bars_len}\n"
) # 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 # yrange
bars = self._array[lbar:rbar] if self._static_yrange is not None:
if not len(bars): yrange = self._static_yrange
# likely no data loaded yet
log.error(f"WTF bars_range = {lbar}:{rbar}")
return
# TODO: should probably just have some kinda attr mark if yrange is not None:
# that determines this behavior based on array type ylow, yhigh = yrange
try: self._static_yrange = yrange
ylow = np.nanmin(bars['low']) else:
yhigh = np.nanmax(bars['high'])
# std = np.std(bars['close']) # TODO: this should be some kind of numpy view api
except IndexError: bars = self._array[lbar:rbar]
# must be non-ohlc array? if not len(bars):
ylow = np.nanmin(bars) # likely no data loaded yet
yhigh = np.nanmax(bars) log.error(f"WTF bars_range = {lbar}:{rbar}")
# std = np.std(bars) return
# TODO: should probably just have some kinda attr mark
# that determines this behavior based on array type
try:
ylow = np.nanmin(bars['low'])
yhigh = np.nanmax(bars['high'])
# std = np.std(bars['close'])
except IndexError:
# must be non-ohlc array?
ylow = np.nanmin(bars)
yhigh = np.nanmax(bars)
# std = np.std(bars)
# view margins: stay within 10% of the "true range" # view margins: stay within 10% of the "true range"
diff = yhigh - ylow diff = yhigh - ylow
@ -527,8 +556,12 @@ class ChartPlotWidget(pg.PlotWidget):
rect = label.itemRect() rect = label.itemRect()
tl, br = rect.topLeft(), rect.bottomRight() tl, br = rect.topLeft(), rect.bottomRight()
vb = self.plotItem.vb vb = self.plotItem.vb
top, bottom = (vb.mapToView(tl).y(), vb.mapToView(br).y()) try:
label_h = top - bottom # on startup labels might not yet be rendered
top, bottom = (vb.mapToView(tl).y(), vb.mapToView(br).y())
label_h = top - bottom
except np.linalg.LinAlgError:
label_h = 0
# print(f'label height {self.name}: {label_h}') # print(f'label height {self.name}: {label_h}')
else: else:
label_h = 0 label_h = 0
@ -575,6 +608,17 @@ async def add_new_bars(delay_s, linked_charts):
# sleep for duration of current bar # sleep for duration of current bar
await sleep() await sleep()
def incr_ohlc_array(array: np.ndarray):
(index, t, close) = array[-1][['index', 'time', 'close']]
# this copies non-std fields (eg. vwap) from the last datum
_next = np.array(array[-1], dtype=array.dtype)
_next[
['index', 'time', 'volume', 'open', 'high', 'low', 'close']
] = (index + 1, t + delay_s, 0, close, close, close, close)
new_array = np.append(array, _next,)
return new_array
while True: while True:
# TODO: bunch of stuff: # TODO: bunch of stuff:
# - I'm starting to think all this logic should be # - I'm starting to think all this logic should be
@ -587,17 +631,6 @@ async def add_new_bars(delay_s, linked_charts):
# 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?
def incr_ohlc_array(array: np.ndarray):
(index, t, close) = array[-1][['index', 'time', 'close']]
# this copies non-std fields (eg. vwap) from the last datum
_next = np.array(array[-1], dtype=array.dtype)
_next[
['index', 'time', 'volume', 'open', 'high', 'low', 'close']
] = (index + 1, t + delay_s, 0, close, close, close, close)
new_array = np.append(array, _next,)
return new_array
# add new increment/bar # add new increment/bar
start = time.time() start = time.time()
ohlc = price_chart._array = incr_ohlc_array(ohlc) ohlc = price_chart._array = incr_ohlc_array(ohlc)
@ -624,7 +657,11 @@ async def add_new_bars(delay_s, linked_charts):
price_chart.update_from_array( price_chart.update_from_array(
price_chart.name, price_chart.name,
ohlc, ohlc,
just_history=True # When appending a new bar, in the time between the insert
# here and the Qt render call the underlying price data may
# have already been updated, thus make sure to also update
# the last bar if necessary on this render cycle.
# just_history=True
) )
# resize view # resize view
price_chart._set_yrange() price_chart._set_yrange()
@ -676,6 +713,16 @@ async def _async_main(
# load in symbol's ohlc data # load in symbol's ohlc data
linked_charts, chart = chart_app.load_symbol(sym, bars) linked_charts, chart = chart_app.load_symbol(sym, bars)
# plot historical vwap if available
vwap_in_history = False
if 'vwap' in bars.dtype.fields:
vwap_in_history = True
chart.draw_curve(
name='vwap',
data=bars['vwap'],
overlay=True,
)
# determine ohlc delay between bars # determine ohlc delay between bars
times = bars['time'] times = bars['time']
@ -745,6 +792,16 @@ async def _async_main(
*chart._array[-1][['index', 'close']]) *chart._array[-1][['index', 'close']])
chart._set_yrange() chart._set_yrange()
vwap = quote.get('vwap')
if vwap and vwap_in_history:
chart._array['vwap'][-1] = vwap
print(f"vwap: {quote['vwap']}")
# update vwap overlay line
chart.update_from_array(
'vwap',
chart._array['vwap'],
)
async def chart_from_fsp( async def chart_from_fsp(
linked_charts, linked_charts,
@ -781,6 +838,18 @@ async def chart_from_fsp(
# TODO: enforce type checking here # TODO: enforce type checking here
newbars = np.array(history) newbars = np.array(history)
# XXX: hack to get curves aligned with bars graphics: prepend a copy of
# the first datum..
# TODO: talk to ``pyqtgraph`` core about proper way to solve
newbars = np.append(
np.array(newbars[0], dtype=newbars.dtype),
newbars
)
newbars = np.append(
np.array(newbars[0], dtype=newbars.dtype),
newbars
)
chart = linked_charts.add_plot( chart = linked_charts.add_plot(
name=func_name, name=func_name,
array=newbars, array=newbars,
@ -799,13 +868,14 @@ async def chart_from_fsp(
last_val_sticky = chart._ysticks[chart.name] last_val_sticky = chart._ysticks[chart.name]
last_val_sticky.update_from_data(-1, value) last_val_sticky.update_from_data(-1, value)
chart._set_yrange(yrange=(0, 100))
# update chart graphics # update chart graphics
async for value in stream: async for value in stream:
chart._array[-1] = value chart._array[-1] = value
last_val_sticky.update_from_data(-1, value) last_val_sticky.update_from_data(-1, value)
chart._set_yrange()
chart.update_from_array(chart.name, chart._array) chart.update_from_array(chart.name, chart._array)
chart._set_yrange() # chart._set_yrange()
def _main( def _main(