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
parent
e91ba55d68
commit
eb5d64ceef
|
@ -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,20 +503,29 @@ 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)
|
||||||
|
|
||||||
|
# yrange
|
||||||
|
if self._static_yrange is not None:
|
||||||
|
yrange = self._static_yrange
|
||||||
|
|
||||||
|
if yrange is not None:
|
||||||
|
ylow, yhigh = yrange
|
||||||
|
self._static_yrange = yrange
|
||||||
|
else:
|
||||||
|
|
||||||
# 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._array[lbar:rbar]
|
||||||
if not len(bars):
|
if not len(bars):
|
||||||
|
@ -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
|
||||||
|
try:
|
||||||
|
# on startup labels might not yet be rendered
|
||||||
top, bottom = (vb.mapToView(tl).y(), vb.mapToView(br).y())
|
top, bottom = (vb.mapToView(tl).y(), vb.mapToView(br).y())
|
||||||
label_h = top - bottom
|
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(
|
||||||
|
|
Loading…
Reference in New Issue