Fix a bunch of scrolling / panning logic
Don't allow zooming to less then a min number of data points. Allow panning "outside" the data set (i.e. moving one of the sequence "ends" to the middle of the view. Start adding logging.bar_select
parent
4c753b5ee6
commit
013c0fef15
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
High level Qt chart widgets.
|
High level Qt chart widgets.
|
||||||
"""
|
"""
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
@ -16,8 +16,12 @@ from ._axes import (
|
||||||
from ._graphics import CrossHairItem, ChartType
|
from ._graphics import CrossHairItem, ChartType
|
||||||
from ._style import _xaxis_at
|
from ._style import _xaxis_at
|
||||||
from ._source import Symbol
|
from ._source import Symbol
|
||||||
|
from .. import brokers
|
||||||
|
from .. log import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
log = get_logger(__name__)
|
||||||
|
|
||||||
# margins
|
# margins
|
||||||
CHART_MARGINS = (0, 0, 10, 3)
|
CHART_MARGINS = (0, 0, 10, 3)
|
||||||
|
|
||||||
|
@ -106,7 +110,7 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
self.signals_visible = False
|
self.signals_visible = False
|
||||||
|
|
||||||
# main data source
|
# main data source
|
||||||
self._array = None
|
self._array: np.ndarray = None
|
||||||
|
|
||||||
self._ch = None # crosshair graphics
|
self._ch = None # crosshair graphics
|
||||||
self._index = 0
|
self._index = 0
|
||||||
|
@ -135,7 +139,7 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
|
|
||||||
def set_split_sizes(
|
def set_split_sizes(
|
||||||
self,
|
self,
|
||||||
prop: float = 0.2
|
prop: float = 0.25
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set the proportion of space allocated for linked subcharts.
|
"""Set the proportion of space allocated for linked subcharts.
|
||||||
"""
|
"""
|
||||||
|
@ -186,7 +190,7 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
# 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:
|
||||||
cv = ChartView()
|
cv = ChartView()
|
||||||
|
@ -261,15 +265,14 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
chart.update_from_array(data, chart.name)
|
chart.update_from_array(data, chart.name)
|
||||||
|
|
||||||
|
|
||||||
_min_points_to_show = 15
|
_min_points_to_show = 20
|
||||||
_min_bars_in_view = 10
|
|
||||||
|
|
||||||
|
|
||||||
class ChartPlotWidget(pg.PlotWidget):
|
class ChartPlotWidget(pg.PlotWidget):
|
||||||
"""``GraphicsView`` subtype containing a single ``PlotItem``.
|
"""``GraphicsView`` subtype containing a single ``PlotItem``.
|
||||||
|
|
||||||
- The added methods allow for plotting OHLC sequences from
|
- The added methods allow for plotting OHLC sequences from
|
||||||
``np.recarray``s with appropriate field names.
|
``np.ndarray``s with appropriate field names.
|
||||||
- Overrides a ``pyqtgraph.PlotWidget`` (a ``GraphicsView`` containing
|
- Overrides a ``pyqtgraph.PlotWidget`` (a ``GraphicsView`` containing
|
||||||
a single ``PlotItem``) to intercept and and re-emit mouse enter/exit
|
a single ``PlotItem``) to intercept and and re-emit mouse enter/exit
|
||||||
events.
|
events.
|
||||||
|
@ -334,22 +337,24 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
"""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.
|
||||||
"""
|
"""
|
||||||
# max_lookahead = _min_points_to_show - _min_bars_in_view
|
|
||||||
|
|
||||||
# set panning limits
|
# set panning limits
|
||||||
self.setLimits(
|
self.setLimits(
|
||||||
xMin=xfirst,
|
xMin=xfirst,
|
||||||
xMax=xlast + _min_points_to_show - 3,
|
xMax=xlast,
|
||||||
minXRange=_min_points_to_show,
|
minXRange=_min_points_to_show,
|
||||||
)
|
)
|
||||||
|
|
||||||
def bars_range(self):
|
def view_range(self) -> Tuple[int, int]:
|
||||||
|
vr = self.viewRect()
|
||||||
|
return int(vr.left()), int(vr.right())
|
||||||
|
|
||||||
|
def bars_range(self) -> Tuple[int, int, int, int]:
|
||||||
"""Return a range tuple for the bars present in view.
|
"""Return a range tuple for the bars present in view.
|
||||||
"""
|
"""
|
||||||
vr = self.viewRect()
|
l, r = self.view_range()
|
||||||
lbar = int(vr.left())
|
lbar = max(l, 0)
|
||||||
rbar = int(vr.right())
|
rbar = min(r, len(self.parent._array) - 1)
|
||||||
return lbar, rbar
|
return l, lbar, rbar, r
|
||||||
|
|
||||||
def draw_ohlc(
|
def draw_ohlc(
|
||||||
self,
|
self,
|
||||||
|
@ -370,7 +375,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
# set xrange limits
|
# set xrange limits
|
||||||
self._xlast = xlast = data[-1]['index']
|
self._xlast = xlast = data[-1]['index']
|
||||||
self._set_xlimits(data[0]['index'], xlast)
|
# 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)
|
||||||
|
@ -393,7 +398,7 @@ 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)
|
# 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)
|
||||||
|
@ -427,14 +432,33 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# self.setAutoVisible(x=False, y=True)
|
# self.setAutoVisible(x=False, y=True)
|
||||||
# self.enableAutoRange(x=False, y=True)
|
# self.enableAutoRange(x=False, y=True)
|
||||||
|
|
||||||
# figure out x-range bars on screen
|
l, lbar, rbar, r = self.bars_range()
|
||||||
lbar, rbar = self.bars_range()
|
|
||||||
|
# figure out x-range in view such that user can scroll "off" the data
|
||||||
|
# set up to the point where ``_min_points_to_show`` are left.
|
||||||
|
# if l < lbar or r > rbar:
|
||||||
|
bars_len = rbar - lbar
|
||||||
|
view_len = r - l
|
||||||
|
# TODO: logic to check if end of bars in view
|
||||||
|
extra = view_len - _min_points_to_show
|
||||||
|
begin = 0 - extra
|
||||||
|
end = len(self.parent._array) - 1 + extra
|
||||||
|
|
||||||
|
log.trace(
|
||||||
|
f"\nl: {l}, lbar: {lbar}, rbar: {rbar}, r: {r}\n"
|
||||||
|
f"view_len: {view_len}, bars_len: {bars_len}\n"
|
||||||
|
f"begin: {begin}, end: {end}, extra: {extra}"
|
||||||
|
)
|
||||||
|
self._set_xlimits(begin, end)
|
||||||
|
|
||||||
# TODO: this should be some kind of numpy view api
|
# TODO: this should be some kind of numpy view api
|
||||||
bars = self.parent._array[lbar:rbar]
|
bars = self.parent._array[lbar:rbar]
|
||||||
if not len(bars):
|
if not len(bars):
|
||||||
# likely no data loaded yet
|
# likely no data loaded yet
|
||||||
|
print(f"WTF bars_range = {lbar}:{rbar}")
|
||||||
return
|
return
|
||||||
|
elif lbar < 0:
|
||||||
|
breakpoint()
|
||||||
|
|
||||||
# TODO: should probably just have some kinda attr mark
|
# TODO: should probably just have some kinda attr mark
|
||||||
# that determines this behavior based on array type
|
# that determines this behavior based on array type
|
||||||
|
@ -449,8 +473,9 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
std = np.std(bars)
|
std = np.std(bars)
|
||||||
|
|
||||||
# view margins
|
# view margins
|
||||||
ylow *= 0.98
|
diff = yhigh - ylow
|
||||||
yhigh *= 1.02
|
ylow = ylow - (diff * 0.08)
|
||||||
|
yhigh = yhigh + (diff * 0.08)
|
||||||
|
|
||||||
chart = self
|
chart = self
|
||||||
chart.setLimits(
|
chart.setLimits(
|
||||||
|
@ -504,8 +529,14 @@ class ChartView(pg.ViewBox):
|
||||||
mask = self.state['mouseEnabled'][:]
|
mask = self.state['mouseEnabled'][:]
|
||||||
|
|
||||||
# don't zoom more then the min points setting
|
# don't zoom more then the min points setting
|
||||||
lbar, rbar = self.linked_charts.chart.bars_range()
|
l, lbar, rbar, r = self.linked_charts.chart.bars_range()
|
||||||
if ev.delta() >= 0 and rbar - lbar <= _min_points_to_show:
|
vl = r - l
|
||||||
|
|
||||||
|
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:
|
||||||
|
log.trace("Min zoom bruh...")
|
||||||
return
|
return
|
||||||
|
|
||||||
# actual scaling factor
|
# actual scaling factor
|
||||||
|
@ -533,12 +564,9 @@ class ChartView(pg.ViewBox):
|
||||||
def main(symbol):
|
def main(symbol):
|
||||||
"""Entry point to spawn a chart app.
|
"""Entry point to spawn a chart app.
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from ._exec import run_qtrio
|
from ._exec import run_qtrio
|
||||||
from ._source import from_df
|
|
||||||
# uses pandas_datareader
|
# uses pandas_datareader
|
||||||
from .quantdom.loaders import get_quotes
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -546,35 +574,68 @@ def main(symbol):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
chart_app = widgets['main']
|
chart_app = widgets['main']
|
||||||
quotes = get_quotes(
|
|
||||||
symbol=symbol,
|
# from .quantdom.loaders import get_quotes
|
||||||
date_from=datetime(1900, 1, 1),
|
# from datetime import datetime
|
||||||
date_to=datetime(2030, 12, 31),
|
# from ._source import from_df
|
||||||
)
|
# quotes = get_quotes(
|
||||||
quotes = from_df(quotes)
|
# symbol=symbol,
|
||||||
|
# date_from=datetime(1900, 1, 1),
|
||||||
|
# date_to=datetime(2030, 12, 31),
|
||||||
|
# )
|
||||||
|
# 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,
|
||||||
|
# 'stock',
|
||||||
|
# rate=rate,
|
||||||
|
# test=test,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# first_quotes, _ = feed.format_quotes(quotes)
|
||||||
|
|
||||||
|
# if first_quotes[0].get('last') is None:
|
||||||
|
# log.error("Broker API is down temporarily")
|
||||||
|
# return
|
||||||
|
|
||||||
# spawn chart
|
# spawn chart
|
||||||
linked_charts = chart_app.load_symbol(symbol, quotes)
|
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])
|
||||||
|
|
||||||
def gen_nums():
|
# def gen_nums():
|
||||||
while True:
|
# while True:
|
||||||
yield quotes[-1].close + 1
|
# yield quotes[-1].close + 2
|
||||||
|
# yield quotes[-1].close - 2
|
||||||
|
|
||||||
nums = gen_nums()
|
# nums = gen_nums()
|
||||||
|
|
||||||
await trio.sleep(10)
|
# # await trio.sleep(10)
|
||||||
while True:
|
# import time
|
||||||
new = next(nums)
|
# while True:
|
||||||
quotes[-1].close = new
|
# new = next(nums)
|
||||||
# this updates the linked_charts internal array
|
# quotes[-1].close = new
|
||||||
# and then passes that array to all subcharts to
|
# # this updates the linked_charts internal array
|
||||||
# render downstream graphics
|
# # and then passes that array to all subcharts to
|
||||||
linked_charts.update_from_quote({'last': new})
|
# # render downstream graphics
|
||||||
await trio.sleep(.1)
|
# 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)
|
||||||
|
|
||||||
await trio.sleep_forever()
|
await trio.sleep_forever()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue