Add WIP real-time 5s bar charting
parent
4c5bc19ec7
commit
b9224cd396
|
@ -244,28 +244,46 @@ class LinkedSplitCharts(QtGui.QWidget):
|
|||
# TODO: eventually we'll want to update bid/ask labels and other
|
||||
# data as subscribed by underlying UI consumers.
|
||||
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?)
|
||||
current['close'] = last
|
||||
current['high'] = max(current['high'], last)
|
||||
current['low'] = min(current['low'], last)
|
||||
# self._array[-1]['close'] = last
|
||||
# self._array[-1]['high'] = max(h, 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
|
||||
chart = self.chart
|
||||
|
||||
# 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
|
||||
# and it should be managed and computed outside of this UI
|
||||
for chart, func in self.indicators:
|
||||
# process array in entirely every update
|
||||
# TODO: change this for streaming
|
||||
data = func(self._array)
|
||||
chart.update_from_array(data, chart.name)
|
||||
data = func(array)
|
||||
chart.update_from_array(data, name=chart.name, **kwargs)
|
||||
|
||||
|
||||
_min_points_to_show = 20
|
||||
_min_points_to_show = 3
|
||||
|
||||
|
||||
class ChartPlotWidget(pg.PlotWidget):
|
||||
|
@ -300,7 +318,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
self.parent = linked_charts
|
||||
# this is the index of that last input array entry and is
|
||||
# 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?
|
||||
# likely custom graphics need special handling
|
||||
|
@ -353,7 +371,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
"""
|
||||
l, r = self.view_range()
|
||||
lbar = max(l, 0)
|
||||
rbar = min(r, len(self.parent._array) - 1)
|
||||
rbar = min(r, len(self.parent._array))
|
||||
return l, lbar, rbar, r
|
||||
|
||||
def draw_ohlc(
|
||||
|
@ -374,8 +392,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
self.addItem(graphics)
|
||||
|
||||
# set xrange limits
|
||||
self._xlast = xlast = data[-1]['index']
|
||||
# self._set_xlimits(data[0]['index'] - 100, xlast)
|
||||
xlast = data[-1]['index']
|
||||
|
||||
# show last 50 points on startup
|
||||
self.plotItem.vb.setXRange(xlast - 50, xlast + 50)
|
||||
|
@ -394,44 +411,46 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# register overlay curve with name
|
||||
if not self._graphics and name is None:
|
||||
name = 'main'
|
||||
|
||||
self._graphics[name] = curve
|
||||
|
||||
# set a "startup view"
|
||||
xlast = len(data)-1
|
||||
xlast = len(data) - 1
|
||||
# self._set_xlimits(0, xlast)
|
||||
|
||||
# show last 50 points on startup
|
||||
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
|
||||
|
||||
def update_from_array(
|
||||
self,
|
||||
array: np.ndarray,
|
||||
name: str = 'main',
|
||||
) -> None:
|
||||
self._xlast = len(array) - 1
|
||||
**kwargs,
|
||||
) -> pg.GraphicsObject:
|
||||
# self._xlast = len(array) - 1
|
||||
graphics = self._graphics[name]
|
||||
graphics.setData(array)
|
||||
graphics.update_from_array(array, **kwargs)
|
||||
|
||||
# update view
|
||||
self._set_yrange()
|
||||
|
||||
return graphics
|
||||
|
||||
def _set_yrange(
|
||||
self,
|
||||
) -> 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
|
||||
that data always fits nicely inside the current view of the
|
||||
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()
|
||||
|
||||
# figure out x-range in view such that user can scroll "off" the data
|
||||
|
@ -474,8 +493,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
# view margins
|
||||
diff = yhigh - ylow
|
||||
ylow = ylow - (diff * 0.08)
|
||||
yhigh = yhigh + (diff * 0.08)
|
||||
ylow = ylow - (diff * 0.1)
|
||||
yhigh = yhigh + (diff * 0.1)
|
||||
|
||||
chart = self
|
||||
chart.setLimits(
|
||||
|
@ -535,12 +554,12 @@ class ChartView(pg.ViewBox):
|
|||
if ev.delta() > 0 and vl <= _min_points_to_show:
|
||||
log.trace("Max zoom bruh...")
|
||||
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...")
|
||||
return
|
||||
|
||||
# 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]
|
||||
|
||||
# center = pg.Point(
|
||||
|
@ -550,8 +569,8 @@ class ChartView(pg.ViewBox):
|
|||
# XXX: scroll "around" the right most element in the view
|
||||
furthest_right_coord = self.boundingRect().topRight()
|
||||
center = pg.Point(
|
||||
fn.invertQTransform(
|
||||
self.childGroup.transform()
|
||||
fn.invertQTransform(
|
||||
self.childGroup.transform()
|
||||
).map(furthest_right_coord)
|
||||
)
|
||||
|
||||
|
@ -572,9 +591,74 @@ def main(symbol):
|
|||
"""Main Qt-trio routine invoked by the Qt loop with
|
||||
the widgets ``dict``.
|
||||
"""
|
||||
|
||||
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 datetime import datetime
|
||||
# from ._source import from_df
|
||||
|
@ -585,15 +669,6 @@ def main(symbol):
|
|||
# )
|
||||
# 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)
|
||||
# quote_gen, quotes = await feed.open_stream(
|
||||
# symbols,
|
||||
|
@ -608,10 +683,6 @@ def main(symbol):
|
|||
# log.error("Broker API is down temporarily")
|
||||
# return
|
||||
|
||||
# spawn chart
|
||||
linked_charts = chart_app.load_symbol(symbol, bars)
|
||||
await trio.sleep_forever()
|
||||
|
||||
# make some fake update data
|
||||
# import itertools
|
||||
# nums = itertools.cycle([315., 320., 325., 310., 3])
|
||||
|
@ -637,6 +708,4 @@ def main(symbol):
|
|||
# # 20 Hz seems to be good enough
|
||||
# await trio.sleep(0.05)
|
||||
|
||||
await trio.sleep_forever()
|
||||
|
||||
run_qtrio(_main, (), ChartSpace)
|
||||
|
|
Loading…
Reference in New Issue