Add WIP real-time 5s bar charting

its_happening
Tyler Goodlet 2020-07-08 15:42:05 -04:00
parent ee4b3a327c
commit 9dc3bdf273
1 changed files with 115 additions and 46 deletions

View File

@ -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)