Add WIP real-time 5s bar charting
parent
ee4b3a327c
commit
9dc3bdf273
|
@ -244,28 +244,46 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
# TODO: eventually we'll want to update bid/ask labels and other
|
# TODO: eventually we'll want to update bid/ask labels and other
|
||||||
# data as subscribed by underlying UI consumers.
|
# data as subscribed by underlying UI consumers.
|
||||||
last = quote['last']
|
last = quote['last']
|
||||||
current = self._array[-1]
|
index, time, open, high, low, close, volume = self._array[-1]
|
||||||
|
|
||||||
# update ohlc (I guess we're enforcing this for now?)
|
# update ohlc (I guess we're enforcing this for now?)
|
||||||
current['close'] = last
|
# self._array[-1]['close'] = last
|
||||||
current['high'] = max(current['high'], last)
|
# self._array[-1]['high'] = max(h, last)
|
||||||
current['low'] = min(current['low'], last)
|
# self._array[-1]['low'] = min(l, last)
|
||||||
|
|
||||||
|
# overwrite from quote
|
||||||
|
self._array[-1] = (
|
||||||
|
index,
|
||||||
|
time,
|
||||||
|
open,
|
||||||
|
max(high, last),
|
||||||
|
min(low, last),
|
||||||
|
last,
|
||||||
|
volume,
|
||||||
|
)
|
||||||
|
self.update_from_array(self._array)
|
||||||
|
|
||||||
|
def update_from_array(
|
||||||
|
self,
|
||||||
|
array: np.ndarray,
|
||||||
|
**kwargs,
|
||||||
|
) -> None:
|
||||||
# update the ohlc sequence graphics chart
|
# update the ohlc sequence graphics chart
|
||||||
chart = self.chart
|
chart = self.chart
|
||||||
|
|
||||||
# we send a reference to the whole updated array
|
# we send a reference to the whole updated array
|
||||||
chart.update_from_array(self._array)
|
chart.update_from_array(array, **kwargs)
|
||||||
|
|
||||||
# TODO: the "data" here should really be a function
|
# TODO: the "data" here should really be a function
|
||||||
# and it should be managed and computed outside of this UI
|
# and it should be managed and computed outside of this UI
|
||||||
for chart, func in self.indicators:
|
for chart, func in self.indicators:
|
||||||
# process array in entirely every update
|
# process array in entirely every update
|
||||||
# TODO: change this for streaming
|
# TODO: change this for streaming
|
||||||
data = func(self._array)
|
data = func(array)
|
||||||
chart.update_from_array(data, chart.name)
|
chart.update_from_array(data, name=chart.name, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
_min_points_to_show = 20
|
_min_points_to_show = 3
|
||||||
|
|
||||||
|
|
||||||
class ChartPlotWidget(pg.PlotWidget):
|
class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
@ -300,7 +318,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
self.parent = linked_charts
|
self.parent = linked_charts
|
||||||
# this is the index of that last input array entry and is
|
# this is the index of that last input array entry and is
|
||||||
# updated and used to figure out how many bars are in view
|
# updated and used to figure out how many bars are in view
|
||||||
self._xlast = 0
|
# self._xlast = 0
|
||||||
|
|
||||||
# XXX: label setting doesn't seem to work?
|
# XXX: label setting doesn't seem to work?
|
||||||
# likely custom graphics need special handling
|
# likely custom graphics need special handling
|
||||||
|
@ -353,7 +371,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
"""
|
"""
|
||||||
l, r = self.view_range()
|
l, r = self.view_range()
|
||||||
lbar = max(l, 0)
|
lbar = max(l, 0)
|
||||||
rbar = min(r, len(self.parent._array) - 1)
|
rbar = min(r, len(self.parent._array))
|
||||||
return l, lbar, rbar, r
|
return l, lbar, rbar, r
|
||||||
|
|
||||||
def draw_ohlc(
|
def draw_ohlc(
|
||||||
|
@ -374,8 +392,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
self.addItem(graphics)
|
self.addItem(graphics)
|
||||||
|
|
||||||
# set xrange limits
|
# set xrange limits
|
||||||
self._xlast = xlast = data[-1]['index']
|
xlast = data[-1]['index']
|
||||||
# self._set_xlimits(data[0]['index'] - 100, xlast)
|
|
||||||
|
|
||||||
# show last 50 points on startup
|
# show last 50 points on startup
|
||||||
self.plotItem.vb.setXRange(xlast - 50, xlast + 50)
|
self.plotItem.vb.setXRange(xlast - 50, xlast + 50)
|
||||||
|
@ -394,44 +411,46 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# register overlay curve with name
|
# register overlay curve with name
|
||||||
if not self._graphics and name is None:
|
if not self._graphics and name is None:
|
||||||
name = 'main'
|
name = 'main'
|
||||||
|
|
||||||
self._graphics[name] = curve
|
self._graphics[name] = curve
|
||||||
|
|
||||||
# set a "startup view"
|
# set a "startup view"
|
||||||
xlast = len(data)-1
|
xlast = len(data) - 1
|
||||||
# self._set_xlimits(0, xlast)
|
# self._set_xlimits(0, xlast)
|
||||||
|
|
||||||
# show last 50 points on startup
|
# show last 50 points on startup
|
||||||
self.plotItem.vb.setXRange(xlast - 50, xlast + 50)
|
self.plotItem.vb.setXRange(xlast - 50, xlast + 50)
|
||||||
|
|
||||||
|
# TODO: we should instead implement a diff based
|
||||||
|
# "only update with new items" on the pg.PlotDataItem
|
||||||
|
curve.update_from_array = curve.setData
|
||||||
|
|
||||||
return curve
|
return curve
|
||||||
|
|
||||||
def update_from_array(
|
def update_from_array(
|
||||||
self,
|
self,
|
||||||
array: np.ndarray,
|
array: np.ndarray,
|
||||||
name: str = 'main',
|
name: str = 'main',
|
||||||
) -> None:
|
**kwargs,
|
||||||
self._xlast = len(array) - 1
|
) -> pg.GraphicsObject:
|
||||||
|
# self._xlast = len(array) - 1
|
||||||
graphics = self._graphics[name]
|
graphics = self._graphics[name]
|
||||||
graphics.setData(array)
|
graphics.update_from_array(array, **kwargs)
|
||||||
|
|
||||||
# update view
|
# update view
|
||||||
self._set_yrange()
|
self._set_yrange()
|
||||||
|
|
||||||
|
return graphics
|
||||||
|
|
||||||
def _set_yrange(
|
def _set_yrange(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Callback for each y-range update.
|
"""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.
|
||||||
"""
|
"""
|
||||||
# TODO: this can likely be ported in part to the built-ins:
|
|
||||||
# self.setYRange(Quotes.low.min() * .98, Quotes.high.max() * 1.02)
|
|
||||||
# self.setMouseEnabled(x=True, y=False)
|
|
||||||
# self.setXRange(Quotes[0].id, Quotes[-1].id)
|
|
||||||
# self.setAutoVisible(x=False, y=True)
|
|
||||||
# self.enableAutoRange(x=False, y=True)
|
|
||||||
|
|
||||||
l, lbar, rbar, r = self.bars_range()
|
l, lbar, rbar, r = self.bars_range()
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -474,8 +493,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
# view margins
|
# view margins
|
||||||
diff = yhigh - ylow
|
diff = yhigh - ylow
|
||||||
ylow = ylow - (diff * 0.08)
|
ylow = ylow - (diff * 0.1)
|
||||||
yhigh = yhigh + (diff * 0.08)
|
yhigh = yhigh + (diff * 0.1)
|
||||||
|
|
||||||
chart = self
|
chart = self
|
||||||
chart.setLimits(
|
chart.setLimits(
|
||||||
|
@ -535,12 +554,12 @@ class ChartView(pg.ViewBox):
|
||||||
if ev.delta() > 0 and vl <= _min_points_to_show:
|
if ev.delta() > 0 and vl <= _min_points_to_show:
|
||||||
log.trace("Max zoom bruh...")
|
log.trace("Max zoom bruh...")
|
||||||
return
|
return
|
||||||
if ev.delta() < 0 and vl >= len(self.linked_charts._array) - 1:
|
if ev.delta() < 0 and vl >= len(self.linked_charts._array):
|
||||||
log.trace("Min zoom bruh...")
|
log.trace("Min zoom bruh...")
|
||||||
return
|
return
|
||||||
|
|
||||||
# actual scaling factor
|
# actual scaling factor
|
||||||
s = 1.015 ** (ev.delta() * -1/20) # self.state['wheelScaleFactor'])
|
s = 1.015 ** (ev.delta() * -1 / 20) # self.state['wheelScaleFactor'])
|
||||||
s = [(None if m is False else s) for m in mask]
|
s = [(None if m is False else s) for m in mask]
|
||||||
|
|
||||||
# center = pg.Point(
|
# center = pg.Point(
|
||||||
|
@ -572,9 +591,74 @@ def main(symbol):
|
||||||
"""Main Qt-trio routine invoked by the Qt loop with
|
"""Main Qt-trio routine invoked by the Qt loop with
|
||||||
the widgets ``dict``.
|
the widgets ``dict``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
chart_app = widgets['main']
|
chart_app = widgets['main']
|
||||||
|
|
||||||
|
# data-feed setup
|
||||||
|
sym = symbol or 'ES.GLOBEX'
|
||||||
|
brokermod = brokers.get_brokermod('ib')
|
||||||
|
async with brokermod.get_client() as client:
|
||||||
|
# figure out the exact symbol
|
||||||
|
bars = await client.bars(symbol=sym)
|
||||||
|
|
||||||
|
# ``from_buffer` return read-only
|
||||||
|
bars = np.array(bars)
|
||||||
|
linked_charts = chart_app.load_symbol('ES', bars)
|
||||||
|
|
||||||
|
async def add_new_bars(delay_s=5.):
|
||||||
|
import time
|
||||||
|
|
||||||
|
ohlc = linked_charts._array
|
||||||
|
|
||||||
|
last_5s = ohlc[-1]['time']
|
||||||
|
delay = max((last_5s + 4.99) - time.time(), 0)
|
||||||
|
await trio.sleep(delay)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print('new bar')
|
||||||
|
|
||||||
|
# TODO: bunch of stuff:
|
||||||
|
# - I'm starting to think all this logic should be
|
||||||
|
# done in one place and "graphics update routines"
|
||||||
|
# should not be doing any length checking and array diffing.
|
||||||
|
# - don't keep appending, but instead increase the
|
||||||
|
# underlying array's size less frequently:
|
||||||
|
# - handle odd lot orders
|
||||||
|
# - update last open price correctly instead
|
||||||
|
# of copying it from last bar's close
|
||||||
|
# - 5 sec bar lookback-autocorrection like tws does
|
||||||
|
index, t, open, high, low, close, volume = ohlc[-1]
|
||||||
|
new = np.append(
|
||||||
|
ohlc,
|
||||||
|
np.array(
|
||||||
|
[(index + 1, t + 5, close, close, close, close, 0)],
|
||||||
|
dtype=ohlc.dtype
|
||||||
|
),
|
||||||
|
)
|
||||||
|
ohlc = linked_charts._array = new
|
||||||
|
linked_charts.update_from_array(new)
|
||||||
|
|
||||||
|
# sleep until next 5s from last bar
|
||||||
|
last_5s = ohlc[-1]['time']
|
||||||
|
delay = max((last_5s + 4.99) - time.time(), 0)
|
||||||
|
await trio.sleep(4.9999)
|
||||||
|
|
||||||
|
async with trio.open_nursery() as n:
|
||||||
|
n.start_soon(add_new_bars)
|
||||||
|
|
||||||
|
async with brokermod.maybe_spawn_brokerd() as portal:
|
||||||
|
stream = await portal.run(
|
||||||
|
'piker.brokers.ib',
|
||||||
|
'trio_stream_ticker',
|
||||||
|
sym=sym,
|
||||||
|
)
|
||||||
|
# TODO: timeframe logic
|
||||||
|
async for tick in stream:
|
||||||
|
# breakpoint()
|
||||||
|
if tick['tickType'] in (48, 77):
|
||||||
|
linked_charts.update_from_quote(
|
||||||
|
{'last': tick['price']}
|
||||||
|
)
|
||||||
|
|
||||||
# from .quantdom.loaders import get_quotes
|
# from .quantdom.loaders import get_quotes
|
||||||
# from datetime import datetime
|
# from datetime import datetime
|
||||||
# from ._source import from_df
|
# from ._source import from_df
|
||||||
|
@ -585,15 +669,6 @@ def main(symbol):
|
||||||
# )
|
# )
|
||||||
# quotes = from_df(quotes)
|
# quotes = from_df(quotes)
|
||||||
|
|
||||||
# data-feed spawning
|
|
||||||
brokermod = brokers.get_brokermod('ib')
|
|
||||||
async with brokermod.get_client() as client:
|
|
||||||
# figure out the exact symbol
|
|
||||||
bars = await client.bars(symbol='ES')
|
|
||||||
|
|
||||||
# wow, just wow.. non-contiguous eh?
|
|
||||||
bars = np.array(bars)
|
|
||||||
|
|
||||||
# feed = DataFeed(portal, brokermod)
|
# feed = DataFeed(portal, brokermod)
|
||||||
# quote_gen, quotes = await feed.open_stream(
|
# quote_gen, quotes = await feed.open_stream(
|
||||||
# symbols,
|
# symbols,
|
||||||
|
@ -608,10 +683,6 @@ def main(symbol):
|
||||||
# log.error("Broker API is down temporarily")
|
# log.error("Broker API is down temporarily")
|
||||||
# return
|
# return
|
||||||
|
|
||||||
# spawn chart
|
|
||||||
linked_charts = chart_app.load_symbol(symbol, bars)
|
|
||||||
await trio.sleep_forever()
|
|
||||||
|
|
||||||
# make some fake update data
|
# make some fake update data
|
||||||
# import itertools
|
# import itertools
|
||||||
# nums = itertools.cycle([315., 320., 325., 310., 3])
|
# nums = itertools.cycle([315., 320., 325., 310., 3])
|
||||||
|
@ -637,6 +708,4 @@ def main(symbol):
|
||||||
# # 20 Hz seems to be good enough
|
# # 20 Hz seems to be good enough
|
||||||
# await trio.sleep(0.05)
|
# await trio.sleep(0.05)
|
||||||
|
|
||||||
await trio.sleep_forever()
|
|
||||||
|
|
||||||
run_qtrio(_main, (), ChartSpace)
|
run_qtrio(_main, (), ChartSpace)
|
||||||
|
|
Loading…
Reference in New Issue