Allow for dynamically added plots
Add `ChartPlotWidget.add_plot()` to add sub charts for indicators which can be updated independently. Clean up rt bar update code and drop some legacy ohlc loading cruft.bar_select
parent
b4f1ec7960
commit
bbe02570b3
|
@ -2,22 +2,25 @@
|
||||||
High level Qt chart widgets.
|
High level Qt chart widgets.
|
||||||
"""
|
"""
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple
|
||||||
|
import time
|
||||||
|
|
||||||
import trio
|
from PyQt5 import QtCore, QtGui
|
||||||
|
from pyqtgraph import functions as fn
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from pyqtgraph import functions as fn
|
import tractor
|
||||||
from PyQt5 import QtCore, QtGui
|
import trio
|
||||||
|
|
||||||
from ._axes import (
|
from ._axes import (
|
||||||
DynamicDateAxis,
|
DynamicDateAxis,
|
||||||
PriceAxis,
|
PriceAxis,
|
||||||
)
|
)
|
||||||
from ._graphics import CrossHairItem, ChartType
|
from ._graphics import CrossHair, ChartType
|
||||||
from ._style import _xaxis_at
|
from ._style import _xaxis_at
|
||||||
from ._source import Symbol
|
from ._source import Symbol
|
||||||
from .. import brokers
|
from .. import brokers
|
||||||
from .. log import get_logger
|
from .. import data
|
||||||
|
from ..log import get_logger
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
@ -74,6 +77,7 @@ class ChartSpace(QtGui.QWidget):
|
||||||
"""Load a new contract into the charting app.
|
"""Load a new contract into the charting app.
|
||||||
"""
|
"""
|
||||||
# XXX: let's see if this causes mem problems
|
# XXX: let's see if this causes mem problems
|
||||||
|
self.window.setWindowTitle(f'piker chart {symbol}')
|
||||||
self.chart = self._plot_cache.setdefault(symbol, LinkedSplitCharts())
|
self.chart = self._plot_cache.setdefault(symbol, LinkedSplitCharts())
|
||||||
s = Symbol(key=symbol)
|
s = Symbol(key=symbol)
|
||||||
|
|
||||||
|
@ -107,22 +111,20 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.signals_visible = False
|
self.signals_visible: bool = False
|
||||||
|
self._array: np.ndarray = None # main data source
|
||||||
# main data source
|
self._ch: CrossHair = None # crosshair graphics
|
||||||
self._array: np.ndarray = None
|
self.chart: ChartPlotWidget = None # main (ohlc) chart
|
||||||
|
self.subplots: List[ChartPlotWidget] = []
|
||||||
self._ch = None # crosshair graphics
|
|
||||||
self._index = 0
|
|
||||||
|
|
||||||
self.chart = None # main (ohlc) chart
|
|
||||||
self.indicators = []
|
|
||||||
|
|
||||||
self.xaxis = DynamicDateAxis(
|
self.xaxis = DynamicDateAxis(
|
||||||
orientation='bottom', linked_charts=self)
|
orientation='bottom',
|
||||||
|
linked_charts=self
|
||||||
|
)
|
||||||
self.xaxis_ind = DynamicDateAxis(
|
self.xaxis_ind = DynamicDateAxis(
|
||||||
orientation='bottom', linked_charts=self)
|
orientation='bottom',
|
||||||
|
linked_charts=self
|
||||||
|
)
|
||||||
|
|
||||||
if _xaxis_at == 'bottom':
|
if _xaxis_at == 'bottom':
|
||||||
self.xaxis.setStyle(showValues=False)
|
self.xaxis.setStyle(showValues=False)
|
||||||
|
@ -134,20 +136,18 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
|
|
||||||
self.layout = QtGui.QVBoxLayout(self)
|
self.layout = QtGui.QVBoxLayout(self)
|
||||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
self.layout.addWidget(self.splitter)
|
self.layout.addWidget(self.splitter)
|
||||||
|
|
||||||
def set_split_sizes(
|
def set_split_sizes(
|
||||||
self,
|
self,
|
||||||
prop: float = 0.25
|
prop: float = 0.25 # proportion allocated to consumer subcharts
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set the proportion of space allocated for linked subcharts.
|
"""Set the proportion of space allocated for linked subcharts.
|
||||||
"""
|
"""
|
||||||
major = 1 - prop
|
major = 1 - prop
|
||||||
# 20% allocated to consumer subcharts
|
min_h_ind = int(self.height() * prop / len(self.subplots))
|
||||||
min_h_ind = int(self.height() * prop / len(self.indicators))
|
|
||||||
sizes = [int(self.height() * major)]
|
sizes = [int(self.height() * major)]
|
||||||
sizes.extend([min_h_ind] * len(self.indicators))
|
sizes.extend([min_h_ind] * len(self.subplots))
|
||||||
self.splitter.setSizes(sizes) # , int(self.height()*0.2)
|
self.splitter.setSizes(sizes) # , int(self.height()*0.2)
|
||||||
|
|
||||||
def plot(
|
def plot(
|
||||||
|
@ -155,82 +155,94 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
symbol: Symbol,
|
symbol: Symbol,
|
||||||
array: np.ndarray,
|
array: np.ndarray,
|
||||||
ohlc: bool = True,
|
ohlc: bool = True,
|
||||||
):
|
) -> None:
|
||||||
"""Start up and show main (price) chart and all linked subcharts.
|
"""Start up and show main (price) chart and all linked subcharts.
|
||||||
"""
|
"""
|
||||||
self.digits = symbol.digits()
|
self.digits = symbol.digits()
|
||||||
|
|
||||||
# XXX: this may eventually be a view onto shared mem
|
# XXX: this will eventually be a view onto shared mem
|
||||||
# or some higher level type / API
|
# or some higher level type / API
|
||||||
self._array = array
|
self._array = array
|
||||||
|
|
||||||
cv = ChartView()
|
# add crosshairs
|
||||||
self.chart = ChartPlotWidget(
|
self._ch = CrossHair(
|
||||||
linked_charts=self,
|
parent=self, #.chart,
|
||||||
parent=self.splitter,
|
# subplots=[plot for plot, d in self.subplots],
|
||||||
axisItems={'bottom': self.xaxis, 'right': PriceAxis()},
|
digits=self.digits
|
||||||
viewBox=cv,
|
|
||||||
# enableMenu=False,
|
|
||||||
)
|
)
|
||||||
# TODO: ``pyqtgraph`` doesn't pass through a parent to the
|
self.chart = self.add_plot(
|
||||||
# ``PlotItem`` by default; maybe we should PR this in?
|
name='main',
|
||||||
cv.linked_charts = self
|
array=array, #['close'],
|
||||||
self.chart.plotItem.vb.linked_charts = self
|
xaxis=self.xaxis,
|
||||||
|
ohlc=True,
|
||||||
self.chart.getPlotItem().setContentsMargins(*CHART_MARGINS)
|
)
|
||||||
|
self.chart.addItem(self._ch)
|
||||||
self.chart.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
self.chart.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
||||||
|
|
||||||
if ohlc:
|
|
||||||
self.chart.draw_ohlc(array)
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(
|
|
||||||
"Only OHLC linked charts are supported currently"
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: this is where we would load an indicator chain
|
# TODO: this is where we would load an indicator chain
|
||||||
# XXX: note, if this isn't index aligned with
|
# XXX: note, if this isn't index aligned with
|
||||||
# the source data the chart will go haywire.
|
# the source data the chart will go haywire.
|
||||||
inds = [('open', lambda a: a['close'])]
|
inds = [('open', lambda a: a['close'])]
|
||||||
|
|
||||||
for name, func in inds:
|
for name, func in inds:
|
||||||
|
|
||||||
|
# compute historical subchart values from input array
|
||||||
|
data = func(array)
|
||||||
|
|
||||||
|
# create sub-plot
|
||||||
|
ind_chart = self.add_plot(name=name, array=data)
|
||||||
|
|
||||||
|
self.subplots.append((ind_chart, func))
|
||||||
|
|
||||||
|
# scale split regions
|
||||||
|
self.set_split_sizes()
|
||||||
|
|
||||||
|
def add_plot(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
array: np.ndarray,
|
||||||
|
xaxis: DynamicDateAxis = None,
|
||||||
|
ohlc: bool = False,
|
||||||
|
) -> 'ChartPlotWidget':
|
||||||
|
"""Add (sub)plots to chart widget by name.
|
||||||
|
|
||||||
|
If ``name`` == ``"main"`` the chart will be the the primary view.
|
||||||
|
"""
|
||||||
cv = ChartView()
|
cv = ChartView()
|
||||||
ind_chart = ChartPlotWidget(
|
# use "indicator axis" by default
|
||||||
|
xaxis = self.xaxis_ind if xaxis is None else xaxis
|
||||||
|
cpw = ChartPlotWidget(
|
||||||
linked_charts=self,
|
linked_charts=self,
|
||||||
parent=self.splitter,
|
parent=self.splitter,
|
||||||
axisItems={'bottom': self.xaxis_ind, 'right': PriceAxis()},
|
axisItems={'bottom': xaxis, 'right': PriceAxis()},
|
||||||
# axisItems={'top': self.xaxis_ind, 'right': PriceAxis()},
|
# axisItems={'top': self.xaxis_ind, 'right': PriceAxis()},
|
||||||
viewBox=cv,
|
viewBox=cv,
|
||||||
)
|
)
|
||||||
# this name will be used to register the primary
|
# this name will be used to register the primary
|
||||||
# graphics curve managed by the subchart
|
# graphics curve managed by the subchart
|
||||||
ind_chart.name = name
|
cpw.name = name
|
||||||
cv.linked_charts = self
|
cv.linked_charts = self
|
||||||
self.chart.plotItem.vb.linked_charts = self
|
cpw.plotItem.vb.linked_charts = self
|
||||||
|
|
||||||
ind_chart.setFrameStyle(
|
cpw.setFrameStyle(
|
||||||
QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain
|
QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain
|
||||||
)
|
)
|
||||||
ind_chart.getPlotItem().setContentsMargins(*CHART_MARGINS)
|
cpw.getPlotItem().setContentsMargins(*CHART_MARGINS)
|
||||||
# self.splitter.addWidget(ind_chart)
|
# self.splitter.addWidget(cpw)
|
||||||
|
|
||||||
# compute historical subchart values from input array
|
|
||||||
data = func(array)
|
|
||||||
self.indicators.append((ind_chart, func))
|
|
||||||
|
|
||||||
# link chart x-axis to main quotes chart
|
# link chart x-axis to main quotes chart
|
||||||
ind_chart.setXLink(self.chart)
|
cpw.setXLink(self.chart)
|
||||||
|
|
||||||
# draw curve graphics
|
# draw curve graphics
|
||||||
ind_chart.draw_curve(data, name)
|
if ohlc:
|
||||||
|
cpw.draw_ohlc(array)
|
||||||
|
else:
|
||||||
|
cpw.draw_curve(array, name)
|
||||||
|
|
||||||
self.set_split_sizes()
|
# add to cross-hair's known plots
|
||||||
|
self._ch.add_plot(cpw)
|
||||||
|
|
||||||
ch = self._ch = CrossHairItem(
|
return cpw
|
||||||
self.chart,
|
|
||||||
[_ind for _ind, d in self.indicators],
|
|
||||||
self.digits
|
|
||||||
)
|
|
||||||
self.chart.addItem(ch)
|
|
||||||
|
|
||||||
def update_from_quote(
|
def update_from_quote(
|
||||||
self,
|
self,
|
||||||
|
@ -243,14 +255,10 @@ 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.get('last') or quote['close']
|
||||||
index, time, open, high, low, close, volume = 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?)
|
||||||
# self._array[-1]['close'] = last
|
|
||||||
# self._array[-1]['high'] = max(h, last)
|
|
||||||
# self._array[-1]['low'] = min(l, last)
|
|
||||||
|
|
||||||
# overwrite from quote
|
# overwrite from quote
|
||||||
self._array[-1] = (
|
self._array[-1] = (
|
||||||
index,
|
index,
|
||||||
|
@ -268,19 +276,25 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
array: np.ndarray,
|
array: np.ndarray,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> None:
|
) -> None:
|
||||||
# update the ohlc sequence graphics chart
|
"""Update all linked chart graphics with a new input array.
|
||||||
chart = self.chart
|
|
||||||
|
|
||||||
|
Return the modified graphics objects in a list.
|
||||||
|
"""
|
||||||
|
# update the ohlc sequence graphics chart
|
||||||
# we send a reference to the whole updated array
|
# we send a reference to the whole updated array
|
||||||
chart.update_from_array(array, **kwargs)
|
self.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:
|
graphics = []
|
||||||
|
for chart, func in self.subplots:
|
||||||
# process array in entirely every update
|
# process array in entirely every update
|
||||||
# TODO: change this for streaming
|
# TODO: change this for streaming
|
||||||
data = func(array)
|
data = func(array)
|
||||||
chart.update_from_array(data, name=chart.name, **kwargs)
|
graphic = chart.update_from_array(data, name=chart.name, **kwargs)
|
||||||
|
graphics.append(graphic)
|
||||||
|
|
||||||
|
return graphics
|
||||||
|
|
||||||
|
|
||||||
_min_points_to_show = 3
|
_min_points_to_show = 3
|
||||||
|
@ -316,9 +330,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.parent = linked_charts
|
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
|
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -327,8 +338,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# label.setText("Yo yoyo")
|
# label.setText("Yo yoyo")
|
||||||
# label.setText("<span style='font-size: 12pt'>x=")
|
# label.setText("<span style='font-size: 12pt'>x=")
|
||||||
|
|
||||||
# to be filled in when graphics are rendered
|
# to be filled in when graphics are rendered by name
|
||||||
# by name
|
|
||||||
self._graphics = {}
|
self._graphics = {}
|
||||||
|
|
||||||
# show only right side axes
|
# show only right side axes
|
||||||
|
@ -353,9 +363,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
xlast: int
|
xlast: int
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set view limits (what's shown in the main chart "pane")
|
"""Set view limits (what's shown in the main chart "pane")
|
||||||
based on max / min x / y coords.
|
based on max/min x/y coords.
|
||||||
"""
|
"""
|
||||||
# set panning limits
|
|
||||||
self.setLimits(
|
self.setLimits(
|
||||||
xMin=xfirst,
|
xMin=xfirst,
|
||||||
xMax=xlast,
|
xMax=xlast,
|
||||||
|
@ -393,7 +402,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
# set xrange limits
|
# set xrange limits
|
||||||
xlast = data[-1]['index']
|
xlast = data[-1]['index']
|
||||||
|
|
||||||
# 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)
|
||||||
|
|
||||||
|
@ -416,7 +424,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
# set a "startup view"
|
# set a "startup view"
|
||||||
xlast = len(data) - 1
|
xlast = len(data) - 1
|
||||||
# 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)
|
||||||
|
@ -433,7 +440,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
name: str = 'main',
|
name: str = 'main',
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> pg.GraphicsObject:
|
) -> pg.GraphicsObject:
|
||||||
# self._xlast = len(array) - 1
|
|
||||||
graphics = self._graphics[name]
|
graphics = self._graphics[name]
|
||||||
graphics.update_from_array(array, **kwargs)
|
graphics.update_from_array(array, **kwargs)
|
||||||
|
|
||||||
|
@ -491,7 +497,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
yhigh = bars.max()
|
yhigh = bars.max()
|
||||||
std = np.std(bars)
|
std = np.std(bars)
|
||||||
|
|
||||||
# view margins
|
# view margins: stay within 10% of the "true range"
|
||||||
diff = yhigh - ylow
|
diff = yhigh - ylow
|
||||||
ylow = ylow - (diff * 0.1)
|
ylow = ylow - (diff * 0.1)
|
||||||
yhigh = yhigh + (diff * 0.1)
|
yhigh = yhigh + (diff * 0.1)
|
||||||
|
@ -580,12 +586,15 @@ class ChartView(pg.ViewBox):
|
||||||
self.sigRangeChangedManually.emit(mask)
|
self.sigRangeChangedManually.emit(mask)
|
||||||
|
|
||||||
|
|
||||||
def main(symbol):
|
def _main(
|
||||||
|
sym: str,
|
||||||
|
brokername: str,
|
||||||
|
**qtractor_kwargs,
|
||||||
|
) -> None:
|
||||||
"""Entry point to spawn a chart app.
|
"""Entry point to spawn a chart app.
|
||||||
"""
|
"""
|
||||||
|
from ._exec import run_qtractor
|
||||||
from ._exec import run_qtrio
|
from ._source import ohlc_dtype
|
||||||
# uses pandas_datareader
|
|
||||||
|
|
||||||
async def _main(widgets):
|
async def _main(widgets):
|
||||||
"""Main Qt-trio routine invoked by the Qt loop with
|
"""Main Qt-trio routine invoked by the Qt loop with
|
||||||
|
@ -593,119 +602,112 @@ def main(symbol):
|
||||||
"""
|
"""
|
||||||
chart_app = widgets['main']
|
chart_app = widgets['main']
|
||||||
|
|
||||||
# data-feed setup
|
# historical data fetch
|
||||||
sym = symbol or 'ES.GLOBEX'
|
brokermod = brokers.get_brokermod(brokername)
|
||||||
brokermod = brokers.get_brokermod('ib')
|
|
||||||
async with brokermod.get_client() as client:
|
async with brokermod.get_client() as client:
|
||||||
# figure out the exact symbol
|
# figure out the exact symbol
|
||||||
bars = await client.bars(symbol=sym)
|
bars = await client.bars(symbol=sym)
|
||||||
|
|
||||||
# ``from_buffer` return read-only
|
# remember, msgpack-numpy's ``from_buffer` returns read-only array
|
||||||
bars = np.array(bars)
|
bars = np.array(bars[list(ohlc_dtype.names)])
|
||||||
linked_charts = chart_app.load_symbol('ES', bars)
|
linked_charts = chart_app.load_symbol(sym, bars)
|
||||||
|
|
||||||
async def add_new_bars(delay_s=5.):
|
# determine ohlc delay between bars
|
||||||
import time
|
times = bars['time']
|
||||||
|
delay = times[-1] - times[-2]
|
||||||
|
|
||||||
|
async def add_new_bars(delay_s):
|
||||||
|
"""Task which inserts new bars into the ohlc every ``delay_s`` seconds.
|
||||||
|
"""
|
||||||
|
# adjust delay to compensate for trio processing time
|
||||||
|
ad = delay_s - 0.002
|
||||||
|
|
||||||
ohlc = linked_charts._array
|
ohlc = linked_charts._array
|
||||||
|
|
||||||
last_5s = ohlc[-1]['time']
|
async def sleep():
|
||||||
delay = max((last_5s + 4.99) - time.time(), 0)
|
"""Sleep until next time frames worth has passed from last bar.
|
||||||
|
"""
|
||||||
|
last_ts = ohlc[-1]['time']
|
||||||
|
delay = max((last_ts + ad) - time.time(), 0)
|
||||||
await trio.sleep(delay)
|
await trio.sleep(delay)
|
||||||
|
|
||||||
while True:
|
# sleep for duration of current bar
|
||||||
print('new bar')
|
await sleep()
|
||||||
|
|
||||||
|
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
|
||||||
# done in one place and "graphics update routines"
|
# done in one place and "graphics update routines"
|
||||||
# should not be doing any length checking and array diffing.
|
# should not be doing any length checking and array diffing.
|
||||||
# - don't keep appending, but instead increase the
|
# - don't keep appending, but instead increase the
|
||||||
# underlying array's size less frequently:
|
# underlying array's size less frequently
|
||||||
# - handle odd lot orders
|
# - handle odd lot orders
|
||||||
# - update last open price correctly instead
|
# - update last open price correctly instead
|
||||||
# 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?
|
||||||
index, t, open, high, low, close, volume = ohlc[-1]
|
(index, t, close) = ohlc[-1][['index', 'time', 'close']]
|
||||||
new = np.append(
|
new = np.append(
|
||||||
ohlc,
|
ohlc,
|
||||||
np.array(
|
np.array(
|
||||||
[(index + 1, t + 5, close, close, close, close, 0)],
|
[(index + 1, t + delay, close, close,
|
||||||
|
close, close, 0)],
|
||||||
dtype=ohlc.dtype
|
dtype=ohlc.dtype
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
ohlc = linked_charts._array = new
|
ohlc = linked_charts._array = new
|
||||||
linked_charts.update_from_array(new)
|
last_quote = ohlc[-1]
|
||||||
|
|
||||||
# sleep until next 5s from last bar
|
# we **don't** update the bar right now
|
||||||
last_5s = ohlc[-1]['time']
|
# since the next quote that arrives should
|
||||||
delay = max((last_5s + 4.99) - time.time(), 0)
|
await sleep()
|
||||||
await trio.sleep(4.9999)
|
|
||||||
|
# if the last bar has not changed print a flat line and
|
||||||
|
# move to the next
|
||||||
|
if last_quote == ohlc[-1]:
|
||||||
|
log.debug("Printing flat line for {sym}")
|
||||||
|
linked_charts.update_from_array(ohlc)
|
||||||
|
|
||||||
|
async def stream_to_chart(func):
|
||||||
|
|
||||||
|
async with tractor.open_nursery() as n:
|
||||||
|
portal = await n.run_in_actor(
|
||||||
|
f'fsp_{func.__name__}',
|
||||||
|
func,
|
||||||
|
brokername=brokermod.name,
|
||||||
|
sym=sym,
|
||||||
|
loglevel='info',
|
||||||
|
)
|
||||||
|
stream = await portal.result()
|
||||||
|
|
||||||
|
# retreive named layout and style instructions
|
||||||
|
layout = await stream.__anext__()
|
||||||
|
|
||||||
|
async for quote in stream:
|
||||||
|
ticks = quote.get('ticks')
|
||||||
|
if ticks:
|
||||||
|
for tick in ticks:
|
||||||
|
print(tick)
|
||||||
|
|
||||||
async with trio.open_nursery() as n:
|
async with trio.open_nursery() as n:
|
||||||
n.start_soon(add_new_bars)
|
from piker import fsp
|
||||||
|
|
||||||
async with brokermod.maybe_spawn_brokerd() as portal:
|
async with data.open_feed(brokername, [sym]) as stream:
|
||||||
stream = await portal.run(
|
# start graphics tasks
|
||||||
'piker.brokers.ib',
|
n.start_soon(add_new_bars, delay)
|
||||||
'trio_stream_ticker',
|
n.start_soon(stream_to_chart, fsp.broker_latency)
|
||||||
sym=sym,
|
|
||||||
)
|
async for quote in stream:
|
||||||
# TODO: timeframe logic
|
# XXX: why are we getting both of these again?
|
||||||
async for tick in stream:
|
ticks = quote.get('ticks')
|
||||||
# breakpoint()
|
if ticks:
|
||||||
|
for tick in ticks:
|
||||||
if tick['tickType'] in (48, 77):
|
if tick['tickType'] in (48, 77):
|
||||||
linked_charts.update_from_quote(
|
linked_charts.update_from_quote(
|
||||||
{'last': tick['price']}
|
{'last': tick['price']}
|
||||||
)
|
)
|
||||||
|
# else:
|
||||||
# from .quantdom.loaders import get_quotes
|
# linked_charts.update_from_quote(
|
||||||
# from datetime import datetime
|
# {'last': quote['close']}
|
||||||
# from ._source import from_df
|
|
||||||
# quotes = get_quotes(
|
|
||||||
# symbol=symbol,
|
|
||||||
# date_from=datetime(1900, 1, 1),
|
|
||||||
# date_to=datetime(2030, 12, 31),
|
|
||||||
# )
|
|
||||||
# quotes = from_df(quotes)
|
|
||||||
|
|
||||||
# feed = DataFeed(portal, brokermod)
|
|
||||||
# quote_gen, quotes = await feed.open_stream(
|
|
||||||
# symbols,
|
|
||||||
# 'stock',
|
|
||||||
# rate=rate,
|
|
||||||
# test=test,
|
|
||||||
# )
|
# )
|
||||||
|
|
||||||
# first_quotes, _ = feed.format_quotes(quotes)
|
run_qtractor(_main, (), ChartSpace, **qtractor_kwargs)
|
||||||
|
|
||||||
# if first_quotes[0].get('last') is None:
|
|
||||||
# log.error("Broker API is down temporarily")
|
|
||||||
# return
|
|
||||||
|
|
||||||
# make some fake update data
|
|
||||||
# import itertools
|
|
||||||
# nums = itertools.cycle([315., 320., 325., 310., 3])
|
|
||||||
|
|
||||||
# def gen_nums():
|
|
||||||
# while True:
|
|
||||||
# yield quotes[-1].close + 2
|
|
||||||
# yield quotes[-1].close - 2
|
|
||||||
|
|
||||||
# nums = gen_nums()
|
|
||||||
|
|
||||||
# # await trio.sleep(10)
|
|
||||||
# import time
|
|
||||||
# while True:
|
|
||||||
# new = next(nums)
|
|
||||||
# quotes[-1].close = new
|
|
||||||
# # this updates the linked_charts internal array
|
|
||||||
# # and then passes that array to all subcharts to
|
|
||||||
# # render downstream graphics
|
|
||||||
# start = time.time()
|
|
||||||
# linked_charts.update_from_quote({'last': new})
|
|
||||||
# print(f"Render latency {time.time() - start}")
|
|
||||||
# # 20 Hz seems to be good enough
|
|
||||||
# await trio.sleep(0.05)
|
|
||||||
|
|
||||||
run_qtrio(_main, (), ChartSpace)
|
|
||||||
|
|
|
@ -115,10 +115,10 @@ def optschain(config, symbol, date, tl, rate, test):
|
||||||
def chart(config, symbol, date, tl, rate, test):
|
def chart(config, symbol, date, tl, rate, test):
|
||||||
"""Start an option chain UI
|
"""Start an option chain UI
|
||||||
"""
|
"""
|
||||||
from ._chart import main
|
from ._chart import _main
|
||||||
|
|
||||||
# global opts
|
# global opts
|
||||||
loglevel = config['loglevel']
|
loglevel = config['loglevel']
|
||||||
brokername = config['broker']
|
brokername = config['broker']
|
||||||
|
|
||||||
main(sym=symbol, brokername=brokername, loglevel=loglevel)
|
_main(sym=symbol, brokername=brokername, loglevel=loglevel)
|
||||||
|
|
Loading…
Reference in New Issue