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,6 +411,7 @@ 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" | ||||||
|  | @ -403,35 +421,36 @@ class ChartPlotWidget(pg.PlotWidget): | ||||||
|         # 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,7 +554,7 @@ 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 | ||||||
| 
 | 
 | ||||||
|  | @ -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