Compare commits

..

No commits in common. "9aeb8603ac297be2731ec499a1f8cb557e4cd09d" and "ffb405c74b0d1eafe85e03a7f39122288b546ec7" have entirely different histories.

8 changed files with 254 additions and 369 deletions

View File

@ -57,15 +57,13 @@ _ohlc_dtype = [
('close', float), ('close', float),
('volume', float), ('volume', float),
('count', int), ('count', int),
('bar_wap', float), ('vwap', float),
] ]
# UI components allow this to be declared such that additional # UI components allow this to be declared such that additional
# (historical) fields can be exposed. # (historical) fields can be exposed.
ohlc_dtype = np.dtype(_ohlc_dtype) ohlc_dtype = np.dtype(_ohlc_dtype)
_show_wap_in_history = True
class Client: class Client:
@ -343,7 +341,7 @@ async def stream_quotes(
while True: while True:
try: try:
async with trio_websocket.open_websocket_url( async with trio_websocket.open_websocket_url(
'wss://ws.kraken.com/', 'wss://ws.kraken.com',
) as ws: ) as ws:
# XXX: setup subs # XXX: setup subs
@ -435,7 +433,7 @@ async def stream_quotes(
'high', 'high',
'low', 'low',
'close', 'close',
'bar_wap', # in this case vwap of bar 'vwap',
'volume'] 'volume']
][-1] = ( ][-1] = (
o, o,

View File

@ -20,7 +20,6 @@ Financial signal processing for the peeps.
from typing import AsyncIterator, Callable, Tuple from typing import AsyncIterator, Callable, Tuple
import trio import trio
from trio_typing import TaskStatus
import tractor import tractor
import numpy as np import numpy as np
@ -76,7 +75,6 @@ async def increment_signals(
# write new slot to the buffer # write new slot to the buffer
dst_shm.push(last) dst_shm.push(last)
len(dst_shm.array)
@tractor.stream @tractor.stream
@ -101,19 +99,9 @@ async def cascade(
async with data.open_feed(brokername, [symbol]) as feed: async with data.open_feed(brokername, [symbol]) as feed:
assert src.token == feed.shm.token assert src.token == feed.shm.token
async def fsp_compute(
task_status: TaskStatus[None] = trio.TASK_STATUS_IGNORED,
) -> None:
# TODO: load appropriate fsp with input args # TODO: load appropriate fsp with input args
async def filter_by_sym( async def filter_by_sym(sym, stream):
sym: str,
stream,
):
# task cancellation won't kill the channel
with stream.shield():
async for quotes in stream: async for quotes in stream:
for symbol, quotes in quotes.items(): for symbol, quotes in quotes.items():
if symbol == sym: if symbol == sym:
@ -134,9 +122,7 @@ async def cascade(
# - developing some system on top of the shared mem array that # - developing some system on top of the shared mem array that
# is `index` aware such that historical data can be indexed # is `index` aware such that historical data can be indexed
# relative to the true first datum? Not sure if this is sane # relative to the true first datum? Not sure if this is sane
# for incremental compuations. # for derivatives.
dst._first.value = src._first.value
dst._last.value = src._first.value
# Conduct a single iteration of fsp with historical bars input # Conduct a single iteration of fsp with historical bars input
# and get historical output # and get historical output
@ -150,7 +136,6 @@ async def cascade(
) )
history[fsp_func_name] = history_output history[fsp_func_name] = history_output
# check for data length mis-allignment and fill missing values # check for data length mis-allignment and fill missing values
diff = len(src.array) - len(history) diff = len(src.array) - len(history)
if diff >= 0: if diff >= 0:
@ -161,47 +146,13 @@ async def cascade(
# compare with source signal and time align # compare with source signal and time align
index = dst.push(history) index = dst.push(history)
await ctx.send_yield(index) yield index
# setup a respawn handle async with trio.open_nursery() as n:
with trio.CancelScope() as cs: n.start_soon(increment_signals, feed, dst)
task_status.started(cs)
# rt stream
async for processed in out_stream: async for processed in out_stream:
log.debug(f"{fsp_func_name}: {processed}") log.debug(f"{fsp_func_name}: {processed}")
index = src.index index = src.index
dst.array[-1][fsp_func_name] = processed dst.array[-1][fsp_func_name] = processed
# stream latest shm array index entry
await ctx.send_yield(index) await ctx.send_yield(index)
last_len = new_len = len(src.array)
async with trio.open_nursery() as n:
cs = await n.start(fsp_compute)
# Increment the underlying shared memory buffer on every "increment"
# msg received from the underlying data feed.
async for msg in await feed.index_stream():
new_len = len(src.array)
if new_len > last_len + 1:
# respawn the signal compute task if the source
# signal has been updated
cs.cancel()
cs = await n.start(fsp_compute)
# TODO: adopt an incremental update engine/approach
# where possible here eventually!
array = dst.array
last = array[-1:].copy()
# write new slot to the buffer
dst.push(last)
last_len = new_len

View File

@ -151,8 +151,8 @@ def wma(
return np.convolve(signal, weights, 'valid') return np.convolve(signal, weights, 'valid')
# @piker.fsp.signal( # @piker.fsp(
# timeframes=['1s', '5s', '15s', '1m', '5m', '1H'], # aggregates=[60, 60*5, 60*60, '4H', '1D'],
# ) # )
async def _rsi( async def _rsi(
source: 'QuoteStream[Dict[str, Any]]', # noqa source: 'QuoteStream[Dict[str, Any]]', # noqa

View File

@ -116,25 +116,13 @@ class DynamicDateAxis(Axis):
indexes: List[int], indexes: List[int],
) -> List[str]: ) -> List[str]:
# try: bars = self.linked_charts.chart._ohlc
chart = self.linked_charts.chart
bars = chart._ohlc
shm = self.linked_charts.chart._shm
first = shm._first.value
bars_len = len(bars) bars_len = len(bars)
times = bars['time'] times = bars['time']
epochs = times[list( epochs = times[list(
map( map(int, filter(lambda i: i < bars_len, indexes))
int,
filter(
lambda i: i > 0 and i < bars_len,
(i-first for i in indexes)
)
)
)] )]
# TODO: **don't** have this hard coded shift to EST # TODO: **don't** have this hard coded shift to EST
dts = pd.to_datetime(epochs, unit='s') # - 4*pd.offsets.Hour() dts = pd.to_datetime(epochs, unit='s') # - 4*pd.offsets.Hour()

View File

@ -17,7 +17,7 @@
""" """
High level Qt chart widgets. High level Qt chart widgets.
""" """
from typing import Tuple, Dict, Any, Optional, Callable from typing import Tuple, Dict, Any, Optional
from functools import partial from functools import partial
from PyQt5 import QtCore, QtGui from PyQt5 import QtCore, QtGui
@ -105,7 +105,6 @@ class ChartSpace(QtGui.QWidget):
self.tf_layout.setContentsMargins(0, 12, 0, 0) self.tf_layout.setContentsMargins(0, 12, 0, 0)
time_frames = ('1M', '5M', '15M', '30M', '1H', '1D', '1W', 'MN') time_frames = ('1M', '5M', '15M', '30M', '1H', '1D', '1W', 'MN')
btn_prefix = 'TF' btn_prefix = 'TF'
for tf in time_frames: for tf in time_frames:
btn_name = ''.join([btn_prefix, tf]) btn_name = ''.join([btn_prefix, tf])
btn = QtGui.QPushButton(tf) btn = QtGui.QPushButton(tf)
@ -113,7 +112,6 @@ class ChartSpace(QtGui.QWidget):
btn.setEnabled(False) btn.setEnabled(False)
setattr(self, btn_name, btn) setattr(self, btn_name, btn)
self.tf_layout.addWidget(btn) self.tf_layout.addWidget(btn)
self.toolbar_layout.addLayout(self.tf_layout) self.toolbar_layout.addLayout(self.tf_layout)
# XXX: strat loader/saver that we don't need yet. # XXX: strat loader/saver that we don't need yet.
@ -128,8 +126,6 @@ class ChartSpace(QtGui.QWidget):
ohlc: bool = True, ohlc: bool = True,
) -> None: ) -> None:
"""Load a new contract into the charting app. """Load a new contract into the charting app.
Expects a ``numpy`` structured array containing all the ohlcv fields.
""" """
# 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.window.setWindowTitle(f'piker chart {symbol}')
@ -152,8 +148,7 @@ class ChartSpace(QtGui.QWidget):
if not self.v_layout.isEmpty(): if not self.v_layout.isEmpty():
self.v_layout.removeWidget(linkedcharts) self.v_layout.removeWidget(linkedcharts)
main_chart = linkedcharts.plot_ohlc_main(s, data) main_chart = linkedcharts.plot_main(s, data, ohlc=ohlc)
self.v_layout.addWidget(linkedcharts) self.v_layout.addWidget(linkedcharts)
return linkedcharts, main_chart return linkedcharts, main_chart
@ -181,6 +176,7 @@ class LinkedSplitCharts(QtGui.QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.signals_visible: bool = False self.signals_visible: bool = False
self._array: np.ndarray = None # main data source
self._ch: CrossHair = None # crosshair graphics self._ch: CrossHair = None # crosshair graphics
self.chart: ChartPlotWidget = None # main (ohlc) chart self.chart: ChartPlotWidget = None # main (ohlc) chart
self.subplots: Dict[Tuple[str, ...], ChartPlotWidget] = {} self.subplots: Dict[Tuple[str, ...], ChartPlotWidget] = {}
@ -216,18 +212,20 @@ class LinkedSplitCharts(QtGui.QWidget):
sizes.extend([min_h_ind] * len(self.subplots)) 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_ohlc_main( def plot_main(
self, self,
symbol: Symbol, symbol: Symbol,
array: np.ndarray, array: np.ndarray,
style: str = 'bar', ohlc: bool = True,
) -> 'ChartPlotWidget': ) -> 'ChartPlotWidget':
"""Start up and show main (price) chart and all linked subcharts. """Start up and show main (price) chart and all linked subcharts.
The data input struct array must include OHLC fields.
""" """
self.digits = symbol.digits() self.digits = symbol.digits()
# TODO: this should eventually be a view onto shared mem or some
# higher level type / API
self._array = array
# add crosshairs # add crosshairs
self._ch = CrossHair( self._ch = CrossHair(
linkedsplitcharts=self, linkedsplitcharts=self,
@ -237,13 +235,11 @@ class LinkedSplitCharts(QtGui.QWidget):
name=symbol.key, name=symbol.key,
array=array, array=array,
xaxis=self.xaxis, xaxis=self.xaxis,
style=style, ohlc=ohlc,
_is_main=True, _is_main=True,
) )
# add crosshair graphic # add crosshair graphic
self.chart.addItem(self._ch) self.chart.addItem(self._ch)
# axis placement
if _xaxis_at == 'bottom': if _xaxis_at == 'bottom':
self.chart.hideAxis('bottom') self.chart.hideAxis('bottom')
@ -257,7 +253,7 @@ class LinkedSplitCharts(QtGui.QWidget):
name: str, name: str,
array: np.ndarray, array: np.ndarray,
xaxis: DynamicDateAxis = None, xaxis: DynamicDateAxis = None,
style: str = 'line', ohlc: bool = False,
_is_main: bool = False, _is_main: bool = False,
**cpw_kwargs, **cpw_kwargs,
) -> 'ChartPlotWidget': ) -> 'ChartPlotWidget':
@ -267,7 +263,7 @@ class LinkedSplitCharts(QtGui.QWidget):
""" """
if self.chart is None and not _is_main: if self.chart is None and not _is_main:
raise RuntimeError( raise RuntimeError(
"A main plot must be created first with `.plot_ohlc_main()`") "A main plot must be created first with `.plot_main()`")
# source of our custom interactions # source of our custom interactions
cv = ChartView() cv = ChartView()
@ -281,11 +277,6 @@ class LinkedSplitCharts(QtGui.QWidget):
) )
cpw = ChartPlotWidget( cpw = ChartPlotWidget(
# this name will be used to register the primary
# graphics curve managed by the subchart
name=name,
array=array, array=array,
parent=self.splitter, parent=self.splitter,
axisItems={ axisItems={
@ -296,12 +287,11 @@ class LinkedSplitCharts(QtGui.QWidget):
cursor=self._ch, cursor=self._ch,
**cpw_kwargs, **cpw_kwargs,
) )
# give viewbox a reference to primary chart
# allowing for kb controls and interactions
# (see our custom view in `._interactions.py`)
cv.chart = cpw cv.chart = cpw
# this name will be used to register the primary
# graphics curve managed by the subchart
cpw.name = name
cpw.plotItem.vb.linked_charts = self cpw.plotItem.vb.linked_charts = self
cpw.setFrameStyle(QtGui.QFrame.StyledPanel) # | QtGui.QFrame.Plain) cpw.setFrameStyle(QtGui.QFrame.StyledPanel) # | QtGui.QFrame.Plain)
cpw.hideButtons() cpw.hideButtons()
@ -315,14 +305,10 @@ class LinkedSplitCharts(QtGui.QWidget):
self._ch.add_plot(cpw) self._ch.add_plot(cpw)
# draw curve graphics # draw curve graphics
if style == 'bar': if ohlc:
cpw.draw_ohlc(name, array) cpw.draw_ohlc(name, array)
elif style == 'line':
cpw.draw_curve(name, array)
else: else:
raise ValueError(f"Chart style {style} is currently unsupported") cpw.draw_curve(name, array)
if not _is_main: if not _is_main:
# track by name # track by name
@ -333,8 +319,6 @@ class LinkedSplitCharts(QtGui.QWidget):
# XXX: we need this right? # XXX: we need this right?
# self.splitter.addWidget(cpw) # self.splitter.addWidget(cpw)
else:
assert style == 'bar', 'main chart must be OHLC'
return cpw return cpw
@ -360,7 +344,6 @@ class ChartPlotWidget(pg.PlotWidget):
def __init__( def __init__(
self, self,
# the data view we generate graphics from # the data view we generate graphics from
name: str,
array: np.ndarray, array: np.ndarray,
static_yrange: Optional[Tuple[float, float]] = None, static_yrange: Optional[Tuple[float, float]] = None,
cursor: Optional[CrossHair] = None, cursor: Optional[CrossHair] = None,
@ -373,26 +356,17 @@ class ChartPlotWidget(pg.PlotWidget):
# parent=None, # parent=None,
# plotItem=None, # plotItem=None,
# antialias=True, # antialias=True,
useOpenGL=True,
**kwargs **kwargs
) )
self.name = name
# self.setViewportMargins(0, 0, 0, 0) # self.setViewportMargins(0, 0, 0, 0)
self._ohlc = array # readonly view of ohlc data self._array = array # readonly view of data
self.default_view()
self._arrays = {} # readonly view of overlays self._arrays = {} # readonly view of overlays
self._graphics = {} # registry of underlying graphics self._graphics = {} # registry of underlying graphics
self._overlays = set() # registry of overlay curve names self._overlays = {} # registry of overlay curves
self._labels = {} # registry of underlying graphics self._labels = {} # registry of underlying graphics
self._ysticks = {} # registry of underlying graphics self._ysticks = {} # registry of underlying graphics
self._vb = self.plotItem.vb self._vb = self.plotItem.vb
self._static_yrange = static_yrange # for "known y-range style" self._static_yrange = static_yrange # for "known y-range style"
self._view_mode: str = 'follow' self._view_mode: str = 'follow'
self._cursor = cursor # placehold for mouse self._cursor = cursor # placehold for mouse
@ -403,7 +377,6 @@ class ChartPlotWidget(pg.PlotWidget):
# show background grid # show background grid
self.showGrid(x=True, y=True, alpha=0.5) self.showGrid(x=True, y=True, alpha=0.5)
# TODO: stick in config
# use cross-hair for cursor? # use cross-hair for cursor?
# self.setCursor(QtCore.Qt.CrossCursor) # self.setCursor(QtCore.Qt.CrossCursor)
@ -418,25 +391,22 @@ class ChartPlotWidget(pg.PlotWidget):
self._vb.sigResized.connect(self._set_yrange) self._vb.sigResized.connect(self._set_yrange)
def last_bar_in_view(self) -> bool: def last_bar_in_view(self) -> bool:
self._ohlc[-1]['index'] self._array[-1]['index']
def update_contents_labels( def update_contents_labels(
self, self,
index: int, index: int,
# array_name: str, # array_name: str,
) -> None: ) -> None:
if index >= 0 and index < self._ohlc[-1]['index']: if index >= 0 and index < len(self._array):
for name, (label, update) in self._labels.items(): for name, (label, update) in self._labels.items():
if name is self.name : if name is self.name :
array = self._ohlc array = self._array
else: else:
array = self._arrays[name] array = self._arrays[name]
try:
update(index, array) update(index, array)
except IndexError:
log.exception(f"Failed to update label: {name}")
def _set_xlimits( def _set_xlimits(
self, self,
@ -460,11 +430,8 @@ class ChartPlotWidget(pg.PlotWidget):
"""Return a range tuple for the bars present in view. """Return a range tuple for the bars present in view.
""" """
l, r = self.view_range() l, r = self.view_range()
a = self._ohlc lbar = max(l, 0)
lbar = max(l, a[0]['index']) rbar = min(r, len(self._array))
rbar = min(r, a[-1]['index'])
# lbar = max(l, 0)
# rbar = min(r, len(self._ohlc))
return l, lbar, rbar, r return l, lbar, rbar, r
def default_view( def default_view(
@ -474,8 +441,7 @@ class ChartPlotWidget(pg.PlotWidget):
"""Set the view box to the "default" startup view of the scene. """Set the view box to the "default" startup view of the scene.
""" """
xlast = self._ohlc[index]['index'] xlast = self._array[index]['index']
print(xlast)
begin = xlast - _bars_to_left_in_follow_mode begin = xlast - _bars_to_left_in_follow_mode
end = xlast + _bars_from_right_in_follow_mode end = xlast + _bars_from_right_in_follow_mode
@ -496,7 +462,7 @@ class ChartPlotWidget(pg.PlotWidget):
self._vb.setXRange( self._vb.setXRange(
min=l + 1, min=l + 1,
max=r + 1, max=r + 1,
# TODO: holy shit, wtf dude... why tf would this not be 0 by # holy shit, wtf dude... why tf would this not be 0 by
# default... speechless. # default... speechless.
padding=0, padding=0,
) )
@ -511,7 +477,6 @@ class ChartPlotWidget(pg.PlotWidget):
"""Draw OHLC datums to chart. """Draw OHLC datums to chart.
""" """
graphics = style(self.plotItem) graphics = style(self.plotItem)
# adds all bar/candle graphics objects for each data point in # adds all bar/candle graphics objects for each data point in
# the np array buffer to be drawn on next render cycle # the np array buffer to be drawn on next render cycle
self.addItem(graphics) self.addItem(graphics)
@ -521,12 +486,10 @@ class ChartPlotWidget(pg.PlotWidget):
self._graphics[name] = graphics self._graphics[name] = graphics
self.add_contents_label( label = ContentsLabel(chart=self, anchor_at=('top', 'left'))
name, self._labels[name] = (label, partial(label.update_from_ohlc, name))
anchor_at=('top', 'left'), label.show()
update_func=ContentsLabel.update_from_ohlc, self.update_contents_labels(len(data) - 1) #, name)
)
self.update_contents_labels(len(data) - 1)
self._add_sticky(name) self._add_sticky(name)
@ -537,76 +500,49 @@ class ChartPlotWidget(pg.PlotWidget):
name: str, name: str,
data: np.ndarray, data: np.ndarray,
overlay: bool = False, overlay: bool = False,
color: str = 'default_light',
add_label: bool = True,
**pdi_kwargs, **pdi_kwargs,
) -> pg.PlotDataItem: ) -> pg.PlotDataItem:
"""Draw a "curve" (line plot graphics) for the provided data in # draw the indicator as a plain curve
the input array ``data``.
"""
_pdi_defaults = { _pdi_defaults = {
'pen': pg.mkPen(hcolor(color)), 'pen': pg.mkPen(hcolor('default_light')),
} }
pdi_kwargs.update(_pdi_defaults) pdi_kwargs.update(_pdi_defaults)
curve = pg.PlotDataItem( curve = pg.PlotDataItem(
y=data[name], data[name],
x=data['index'],
# antialias=True, # antialias=True,
name=name, name=name,
# TODO: see how this handles with custom ohlcv bars graphics # TODO: see how this handles with custom ohlcv bars graphics
# and/or if we can implement something similar for OHLC graphics clipToView=True,
# clipToView=True,
autoDownsample=True,
downsampleMethod='subsample',
**pdi_kwargs, **pdi_kwargs,
) )
self.addItem(curve) self.addItem(curve)
# register curve graphics and backing array for name # register overlay curve with name
self._graphics[name] = curve self._graphics[name] = curve
self._arrays[name] = data
if overlay: if overlay:
anchor_at = ('bottom', 'left') anchor_at = ('bottom', 'right')
self._overlays.add(name) self._overlays[name] = curve
self._arrays[name] = data
else: else:
anchor_at = ('top', 'left') anchor_at = ('top', 'right')
# TODO: something instead of stickies for overlays # TODO: something instead of stickies for overlays
# (we need something that avoids clutter on x-axis). # (we need something that avoids clutter on x-axis).
self._add_sticky(name, bg_color='default_light') self._add_sticky(name, bg_color='default_light')
if add_label: label = ContentsLabel(chart=self, anchor_at=anchor_at)
self.add_contents_label(name, anchor_at=anchor_at) self._labels[name] = (label, partial(label.update_from_value, name))
self.update_contents_labels(len(data) - 1) label.show()
self.update_contents_labels(len(data) - 1) #, name)
if self._cursor: if self._cursor:
self._cursor.add_curve_cursor(self, curve) self._cursor.add_curve_cursor(self, curve)
return curve return curve
def add_contents_label(
self,
name: str,
anchor_at: Tuple[str, str] = ('top', 'left'),
update_func: Callable = ContentsLabel.update_from_value,
) -> ContentsLabel:
label = ContentsLabel(chart=self, anchor_at=anchor_at)
self._labels[name] = (
# calls class method on instance
label,
partial(update_func, label, name)
)
label.show()
return label
def _add_sticky( def _add_sticky(
self, self,
name: str, name: str,
@ -633,7 +569,7 @@ class ChartPlotWidget(pg.PlotWidget):
"""Update the named internal graphics from ``array``. """Update the named internal graphics from ``array``.
""" """
self._ohlc = array self._array = array
graphics = self._graphics[name] graphics = self._graphics[name]
graphics.update_from_array(array, **kwargs) graphics.update_from_array(array, **kwargs)
return graphics return graphics
@ -648,18 +584,14 @@ class ChartPlotWidget(pg.PlotWidget):
""" """
if name not in self._overlays: if name not in self._overlays:
self._ohlc = array self._array = array
else: else:
self._arrays[name] = array self._arrays[name] = array
curve = self._graphics[name] curve = self._graphics[name]
# TODO: we should instead implement a diff based # TODO: we should instead implement a diff based
# "only update with new items" on the pg.PlotCurveItem # "only update with new items" on the pg.PlotDataItem
# one place to dig around this might be the `QBackingStore` curve.setData(array[name], **kwargs)
# https://doc.qt.io/qt-5/qbackingstore.html
curve.setData(y=array[name], x=array['index'], **kwargs)
return curve return curve
def _set_yrange( def _set_yrange(
@ -693,9 +625,8 @@ class ChartPlotWidget(pg.PlotWidget):
# TODO: logic to check if end of bars in view # TODO: logic to check if end of bars in view
extra = view_len - _min_points_to_show extra = view_len - _min_points_to_show
begin = self._ohlc[0]['index'] - extra begin = 0 - extra
# end = len(self._ohlc) - 1 + extra end = len(self._array) - 1 + extra
end = self._ohlc[-1]['index'] - 1 + extra
# XXX: test code for only rendering lines for the bars in view. # XXX: test code for only rendering lines for the bars in view.
# This turns out to be very very poor perf when scaling out to # This turns out to be very very poor perf when scaling out to
@ -711,15 +642,10 @@ class ChartPlotWidget(pg.PlotWidget):
# f"view_len: {view_len}, bars_len: {bars_len}\n" # f"view_len: {view_len}, bars_len: {bars_len}\n"
# f"begin: {begin}, end: {end}, extra: {extra}" # f"begin: {begin}, end: {end}, extra: {extra}"
# ) # )
# self._set_xlimits(begin, end) 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._ohlc[lbar:rbar] bars = self._array[lbar:rbar]
a = self._ohlc
ifirst = a[0]['index']
bars = a[lbar - ifirst:rbar - ifirst]
if not len(bars): if not len(bars):
# likely no data loaded yet or extreme scrolling? # likely no data loaded yet or extreme scrolling?
log.error(f"WTF bars_range = {lbar}:{rbar}") log.error(f"WTF bars_range = {lbar}:{rbar}")
@ -805,6 +731,10 @@ async def _async_main(
# chart_app.init_search() # chart_app.init_search()
# XXX: bug zone if you try to ctl-c after this we get hangs again?
# wtf...
# await tractor.breakpoint()
# historical data fetch # historical data fetch
brokermod = brokers.get_brokermod(brokername) brokermod = brokers.get_brokermod(brokername)
@ -817,28 +747,30 @@ async def _async_main(
ohlcv = feed.shm ohlcv = feed.shm
bars = ohlcv.array bars = ohlcv.array
# TODO: when we start messing with line charts
# c = np.zeros(len(bars), dtype=[
# (sym, bars.dtype.fields['close'][0]),
# ('index', 'i4'),
# ])
# c[sym] = bars['close']
# c['index'] = bars['index']
# linked_charts, chart = chart_app.load_symbol(sym, c, ohlc=False)
# load in symbol's ohlc data # load in symbol's ohlc data
# await tractor.breakpoint()
linked_charts, chart = chart_app.load_symbol(sym, bars) linked_charts, chart = chart_app.load_symbol(sym, bars)
# plot historical vwap if available # plot historical vwap if available
wap_in_history = False vwap_in_history = False
if 'vwap' in bars.dtype.fields:
if brokermod._show_wap_in_history: vwap_in_history = True
if 'bar_wap' in bars.dtype.fields:
wap_in_history = True
chart.draw_curve( chart.draw_curve(
name='bar_wap', name='vwap',
data=bars, data=bars,
add_label=False, overlay=True,
) )
chart._set_yrange() chart._set_yrange()
# TODO: a data view api that makes this less shit
chart._shm = ohlcv
# eventually we'll support some kind of n-compose syntax # eventually we'll support some kind of n-compose syntax
fsp_conf = { fsp_conf = {
'vwap': { 'vwap': {
@ -867,13 +799,19 @@ async def _async_main(
loglevel, loglevel,
) )
# update last price sticky
last_price_sticky = chart._ysticks[chart.name]
last_price_sticky.update_from_data(
*ohlcv.array[-1][['index', 'close']]
)
# start graphics update loop(s)after receiving first live quote # start graphics update loop(s)after receiving first live quote
n.start_soon( n.start_soon(
chart_from_quotes, chart_from_quotes,
chart, chart,
feed.stream, feed.stream,
ohlcv, ohlcv,
wap_in_history, vwap_in_history,
) )
# wait for a first quote before we start any update tasks # wait for a first quote before we start any update tasks
@ -896,7 +834,7 @@ async def chart_from_quotes(
chart: ChartPlotWidget, chart: ChartPlotWidget,
stream, stream,
ohlcv: np.ndarray, ohlcv: np.ndarray,
wap_in_history: bool = False, vwap_in_history: bool = False,
) -> None: ) -> None:
"""The 'main' (price) chart real-time update loop. """The 'main' (price) chart real-time update loop.
@ -909,40 +847,29 @@ async def chart_from_quotes(
# - 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?
# update last price sticky
last_price_sticky = chart._ysticks[chart.name] last_price_sticky = chart._ysticks[chart.name]
last_price_sticky.update_from_data(
*ohlcv.array[-1][['index', 'close']]
)
def maxmin(): def maxmin():
# TODO: implement this # TODO: implement this
# https://arxiv.org/abs/cs/0610046 # https://arxiv.org/abs/cs/0610046
# https://github.com/lemire/pythonmaxmin # https://github.com/lemire/pythonmaxmin
array = chart._ohlc array = chart._array
ifirst = array[0]['index']
last_bars_range = chart.bars_range() last_bars_range = chart.bars_range()
l, lbar, rbar, r = last_bars_range l, lbar, rbar, r = last_bars_range
in_view = array[lbar - ifirst:rbar - ifirst] in_view = array[lbar:rbar]
assert in_view.size
mx, mn = np.nanmax(in_view['high']), np.nanmin(in_view['low']) mx, mn = np.nanmax(in_view['high']), np.nanmin(in_view['low'])
# TODO: when we start using line charts, probably want to make # TODO: when we start using line charts
# this an overloaded call on our `DataView
# sym = chart.name # sym = chart.name
# mx, mn = np.nanmax(in_view[sym]), np.nanmin(in_view[sym]) # mx, mn = np.nanmax(in_view[sym]), np.nanmin(in_view[sym])
return last_bars_range, mx, mn return last_bars_range, mx, mn
chart.default_view()
last_bars_range, last_mx, last_mn = maxmin() last_bars_range, last_mx, last_mn = maxmin()
chart.default_view()
last, volume = ohlcv.array[-1][['close', 'volume']] last, volume = ohlcv.array[-1][['close', 'volume']]
l1 = L1Labels( l1 = L1Labels(
@ -962,6 +889,7 @@ async def chart_from_quotes(
async for quotes in stream: async for quotes in stream:
for sym, quote in quotes.items(): for sym, quote in quotes.items():
# print(f'CHART: {quote}')
for tick in quote.get('ticks', ()): for tick in quote.get('ticks', ()):
@ -970,14 +898,7 @@ async def chart_from_quotes(
price = tick.get('price') price = tick.get('price')
size = tick.get('size') size = tick.get('size')
# compute max and min trade values to display in view if ticktype in ('trade', 'utrade'):
# TODO: we need a streaming minmax algorithm here, see
# def above.
brange, mx_in_view, mn_in_view = maxmin()
l, lbar, rbar, r = brange
if ticktype in ('trade', 'utrade', 'last'):
array = ohlcv.array array = ohlcv.array
# update price sticky(s) # update price sticky(s)
@ -986,16 +907,25 @@ async def chart_from_quotes(
*last[['index', 'close']] *last[['index', 'close']]
) )
# plot bars
# update price bar # update price bar
chart.update_ohlc_from_array( chart.update_ohlc_from_array(
chart.name, chart.name,
array, array,
) )
if wap_in_history: # chart.update_curve_from_array(
# update vwap overlay line # chart.name,
chart.update_curve_from_array('bar_wap', ohlcv.array) # TODO: when we start using line charts
# np.array(array['close'], dtype=[(chart.name, 'f8')])
# if vwap_in_history:
# # update vwap overlay line
# chart.update_curve_from_array('vwap', ohlcv.array)
# compute max and min trade values to display in view
# TODO: we need a streaming minmax algorithm here, see
# def above.
brange, mx_in_view, mn_in_view = maxmin()
# XXX: prettty sure this is correct? # XXX: prettty sure this is correct?
# if ticktype in ('trade', 'last'): # if ticktype in ('trade', 'last'):
@ -1091,14 +1021,12 @@ async def spawn_fsps(
# spawn closure, can probably define elsewhere # spawn closure, can probably define elsewhere
async def spawn_fsp_daemon( async def spawn_fsp_daemon(
fsp_name: str, fsp_name,
display_name: str, conf,
conf: dict,
): ):
"""Start an fsp subactor async. """Start an fsp subactor async.
""" """
print(f'FSP NAME: {fsp_name}')
portal = await n.run_in_actor( portal = await n.run_in_actor(
# name as title of sub-chart # name as title of sub-chart
@ -1129,7 +1057,6 @@ async def spawn_fsps(
ln.start_soon( ln.start_soon(
spawn_fsp_daemon, spawn_fsp_daemon,
fsp_func_name, fsp_func_name,
display_name,
conf, conf,
) )
@ -1154,8 +1081,6 @@ async def update_signals(
) -> None: ) -> None:
"""FSP stream chart update loop. """FSP stream chart update loop.
This is called once for each entry in the fsp
config map.
""" """
shm = conf['shm'] shm = conf['shm']
@ -1169,7 +1094,6 @@ async def update_signals(
last_val_sticky = None last_val_sticky = None
else: else:
chart = linked_charts.add_plot( chart = linked_charts.add_plot(
name=fsp_func_name, name=fsp_func_name,
array=shm.array, array=shm.array,
@ -1188,7 +1112,6 @@ async def update_signals(
# fsp_func_name # fsp_func_name
) )
# read last value
array = shm.array array = shm.array
value = array[fsp_func_name][-1] value = array[fsp_func_name][-1]
@ -1196,8 +1119,7 @@ async def update_signals(
last_val_sticky.update_from_data(-1, value) last_val_sticky.update_from_data(-1, value)
chart.update_curve_from_array(fsp_func_name, array) chart.update_curve_from_array(fsp_func_name, array)
chart.default_view()
chart._shm = shm
# TODO: figure out if we can roll our own `FillToThreshold` to # TODO: figure out if we can roll our own `FillToThreshold` to
# get brush filled polygons for OS/OB conditions. # get brush filled polygons for OS/OB conditions.
@ -1210,42 +1132,23 @@ async def update_signals(
# graphics.curve.setFillLevel(50) # graphics.curve.setFillLevel(50)
# add moveable over-[sold/bought] lines # add moveable over-[sold/bought] lines
# and labels only for the 70/30 lines level_line(chart, 30)
level_line(chart, 20, show_label=False) level_line(chart, 70, orient_v='top')
level_line(chart, 30, orient_v='top')
level_line(chart, 70, orient_v='bottom')
level_line(chart, 80, orient_v='top', show_label=False)
chart._shm = shm
chart._set_yrange() chart._set_yrange()
stream = conf['stream'] stream = conf['stream']
# update chart graphics # update chart graphics
async for value in stream: async for value in stream:
# p = pg.debug.Profiler(disabled=False, delayed=False)
# TODO: provide a read sync mechanism to avoid this polling.
# the underlying issue is that a backfill and subsequent shm
# array first/last index update could result in an empty array
# read here since the stream is never torn down on the
# re-compute steps.
read_tries = 2
while read_tries > 0:
try:
# read last
array = shm.array array = shm.array
value = array[-1][fsp_func_name] value = array[-1][fsp_func_name]
break
except IndexError:
read_tries -= 1
continue
if last_val_sticky: if last_val_sticky:
last_val_sticky.update_from_data(-1, value) last_val_sticky.update_from_data(-1, value)
# update graphics
chart.update_curve_from_array(fsp_func_name, array) chart.update_curve_from_array(fsp_func_name, array)
# p('rendered rsi datum')
async def check_for_new_bars(feed, ohlcv, linked_charts): async def check_for_new_bars(feed, ohlcv, linked_charts):
@ -1296,7 +1199,7 @@ async def check_for_new_bars(feed, ohlcv, linked_charts):
# resize view # resize view
# price_chart._set_yrange() # price_chart._set_yrange()
for name in price_chart._overlays: for name, curve in price_chart._overlays.items():
price_chart.update_curve_from_array( price_chart.update_curve_from_array(
name, name,
@ -1304,16 +1207,15 @@ async def check_for_new_bars(feed, ohlcv, linked_charts):
) )
# # TODO: standard api for signal lookups per plot # # TODO: standard api for signal lookups per plot
# if name in price_chart._ohlc.dtype.fields: # if name in price_chart._array.dtype.fields:
# # should have already been incremented above # # should have already been incremented above
# price_chart.update_curve_from_array(name, price_chart._ohlc) # price_chart.update_curve_from_array(name, price_chart._array)
for name, chart in linked_charts.subplots.items(): for name, chart in linked_charts.subplots.items():
chart.update_curve_from_array(chart.name, chart._shm.array) chart.update_curve_from_array(chart.name, chart._shm.array)
# chart._set_yrange() # chart._set_yrange()
# shift the view if in follow mode
price_chart.increment_view() price_chart.increment_view()

View File

@ -20,8 +20,6 @@ Trio - Qt integration
Run ``trio`` in guest mode on top of the Qt event loop. Run ``trio`` in guest mode on top of the Qt event loop.
All global Qt runtime settings are mostly defined here. All global Qt runtime settings are mostly defined here.
""" """
import os
import signal
from functools import partial from functools import partial
import traceback import traceback
from typing import Tuple, Callable, Dict, Any from typing import Tuple, Callable, Dict, Any
@ -76,16 +74,11 @@ class MainWindow(QtGui.QMainWindow):
self.setMinimumSize(*self.size) self.setMinimumSize(*self.size)
self.setWindowTitle(self.title) self.setWindowTitle(self.title)
def closeEvent( def closeEvent(self, event: 'QCloseEvent') -> None:
self,
event: 'QCloseEvent'
) -> None:
"""Cancel the root actor asap. """Cancel the root actor asap.
""" """
# raising KBI seems to get intercepted by by Qt so just use the tractor.current_actor().cancel_soon()
# system.
os.kill(os.getpid(), signal.SIGINT)
def run_qtractor( def run_qtractor(
@ -135,14 +128,10 @@ def run_qtractor(
def done_callback(outcome): def done_callback(outcome):
print(f"Outcome: {outcome}")
if isinstance(outcome, Error): if isinstance(outcome, Error):
exc = outcome.error exc = outcome.error
if isinstance(outcome.error, KeyboardInterrupt):
# make it kinda look like ``trio``
print("Terminated!")
else:
traceback.print_exception(type(exc), exc, exc.__traceback__) traceback.print_exception(type(exc), exc, exc.__traceback__)
app.quit() app.quit()
@ -155,6 +144,9 @@ def run_qtractor(
instance = main_widget() instance = main_widget()
instance.window = window instance.window = window
# kill the app when root actor terminates
tractor._actor._lifetime_stack.callback(app.quit)
widgets = { widgets = {
'window': window, 'window': window,
'main': instance, 'main': instance,
@ -178,7 +170,6 @@ def run_qtractor(
main, main,
run_sync_soon_threadsafe=run_sync_soon_threadsafe, run_sync_soon_threadsafe=run_sync_soon_threadsafe,
done_callback=done_callback, done_callback=done_callback,
# restrict_keyboard_interrupt_to_checkpoints=True,
) )
window.main_widget = main_widget window.main_widget = main_widget

View File

@ -17,7 +17,7 @@
""" """
Chart graphics for displaying a slew of different data types. Chart graphics for displaying a slew of different data types.
""" """
import inspect
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
import numpy as np import numpy as np
@ -104,7 +104,7 @@ class LineDot(pg.CurvePoint):
# first = x[0] # first = x[0]
# i = index - first # i = index - first
i = index - x[0] i = index - x[0]
if i > 0 and i < len(y): if i > 0:
newPos = (index, y[i]) newPos = (index, y[i])
QtGui.QGraphicsItem.setPos(self, *newPos) QtGui.QGraphicsItem.setPos(self, *newPos)
return True return True
@ -123,8 +123,9 @@ _corner_margins = {
('top', 'left'): (-4, -5), ('top', 'left'): (-4, -5),
('top', 'right'): (4, -5), ('top', 'right'): (4, -5),
('bottom', 'left'): (-4, lambda font_size: font_size * 2), # TODO: pretty sure y here needs to be 2x font height
('bottom', 'right'): (4, lambda font_size: font_size * 2), ('bottom', 'left'): (-4, 14),
('bottom', 'right'): (4, 14),
} }
@ -141,10 +142,7 @@ class ContentsLabel(pg.LabelItem):
font_size: Optional[int] = None, font_size: Optional[int] = None,
) -> None: ) -> None:
font_size = font_size or _font.font.pixelSize() font_size = font_size or _font.font.pixelSize()
super().__init__( super().__init__(justify=justify_text, size=f'{str(font_size)}px')
justify=justify_text,
size=f'{str(font_size)}px'
)
# anchor to viewbox # anchor to viewbox
self.setParentItem(chart._vb) self.setParentItem(chart._vb)
@ -155,10 +153,6 @@ class ContentsLabel(pg.LabelItem):
index = (_corner_anchors[h], _corner_anchors[v]) index = (_corner_anchors[h], _corner_anchors[v])
margins = _corner_margins[(v, h)] margins = _corner_margins[(v, h)]
ydim = margins[1]
if inspect.isfunction(margins[1]):
margins = margins[0], ydim(font_size)
self.anchor(itemPos=index, parentPos=index, offset=margins) self.anchor(itemPos=index, parentPos=index, offset=margins)
def update_from_ohlc( def update_from_ohlc(
@ -417,6 +411,7 @@ def lines_from_ohlc(row: np.ndarray, w: float) -> Tuple[QLineF]:
return [hl, o, c] return [hl, o, c]
@timeit
@jit( @jit(
# TODO: for now need to construct this manually for readonly arrays, see # TODO: for now need to construct this manually for readonly arrays, see
# https://github.com/numba/numba/issues/4511 # https://github.com/numba/numba/issues/4511
@ -488,7 +483,7 @@ def path_arrays_from_ohlc(
return x, y, c return x, y, c
# @timeit @timeit
def gen_qpath( def gen_qpath(
data, data,
start, # XXX: do we need this? start, # XXX: do we need this?
@ -496,8 +491,6 @@ def gen_qpath(
) -> QtGui.QPainterPath: ) -> QtGui.QPainterPath:
x, y, c = path_arrays_from_ohlc(data, start, bar_gap=w) x, y, c = path_arrays_from_ohlc(data, start, bar_gap=w)
# TODO: numba the internals of this!
return pg.functions.arrayToQPath(x, y, connect=c) return pg.functions.arrayToQPath(x, y, connect=c)
@ -521,6 +514,7 @@ class BarItems(pg.GraphicsObject):
super().__init__() super().__init__()
self.last_bar = QtGui.QPicture() self.last_bar = QtGui.QPicture()
# self.history = QtGui.QPicture()
self.path = QtGui.QPainterPath() self.path = QtGui.QPainterPath()
# self._h_path = QtGui.QGraphicsPathItem(self.path) # self._h_path = QtGui.QGraphicsPathItem(self.path)
@ -543,7 +537,7 @@ class BarItems(pg.GraphicsObject):
self.start_index: int = 0 self.start_index: int = 0
self.stop_index: int = 0 self.stop_index: int = 0
# @timeit @timeit
def draw_from_data( def draw_from_data(
self, self,
data: np.ndarray, data: np.ndarray,
@ -600,6 +594,18 @@ class BarItems(pg.GraphicsObject):
p.end() p.end()
# @timeit # @timeit
# def draw_history(self) -> None:
# # TODO: avoid having to use a ```QPicture` to calc the
# # ``.boundingRect()``, use ``QGraphicsPathItem`` instead?
# # https://doc.qt.io/qt-5/qgraphicspathitem.html
# # self._h_path.setPath(self.path)
# p = QtGui.QPainter(self.history)
# p.setPen(self.bars_pen)
# p.drawPath(self.path)
# p.end()
@timeit
def update_from_array( def update_from_array(
self, self,
array: np.ndarray, array: np.ndarray,
@ -630,19 +636,26 @@ class BarItems(pg.GraphicsObject):
# only drawing as many bars as exactly specified. # only drawing as many bars as exactly specified.
if prepend_length: if prepend_length:
# breakpoint()
# new history was added and we need to render a new path # new history was added and we need to render a new path
new_bars = array[:prepend_length] new_bars = array[:prepend_length]
prepend_path = gen_qpath(new_bars, 0, self.w) prepend_path = gen_qpath(new_bars, 0, self.w)
# XXX: SOMETHING IS MAYBE FISHY HERE what with the old_path # XXX: SOMETHING IS FISHY HERE what with the old_path
# y value not matching the first value from # y value not matching the first value from
# array[prepend_length + 1] ??? # array[prepend_length + 1] ???
# update path # update path
old_path = self.path old_path = self.path
self.path = prepend_path self.path = prepend_path
# self.path.moveTo(float(index - self.w), float(new_bars[0]['open']))
# self.path.moveTo(
# float(istart - self.w),
# # float(array[prepend_length + 1]['open'])
# float(array[prepend_length]['open'])
# )
self.path.addPath(old_path) self.path.addPath(old_path)
# self.draw_history()
if append_length: if append_length:
# generate new lines objects for updatable "current bar" # generate new lines objects for updatable "current bar"
@ -659,8 +672,45 @@ class BarItems(pg.GraphicsObject):
self.path.moveTo(float(istop - self.w), float(new_bars[0]['open'])) self.path.moveTo(float(istop - self.w), float(new_bars[0]['open']))
self.path.addPath(append_path) self.path.addPath(append_path)
# self.draw_history()
self._xrange = first_index, last_index self._xrange = first_index, last_index
# if extra > 0:
# index = array['index']
# first, last = index[0], indext[-1]
# # if first < self.start_index:
# # length = self.start_index - first
# # prepend_path = gen_qpath(array[:sef:
# # generate new lines objects for updatable "current bar"
# self._last_bar_lines = lines_from_ohlc(array[-1], self.w)
# self.draw_last_bar()
# # generate new graphics to match provided array
# # path appending logic:
# # we need to get the previous "current bar(s)" for the time step
# # and convert it to a sub-path to append to the historical set
# new_history_istart = length - 2
# to_history = array[new_history_istart:new_history_istart + extra]
# new_history_qpath = gen_qpath(to_history, 0, self.w)
# # move to position of placement for the next bar in history
# # and append new sub-path
# new_bars = array[index:index + extra]
# # x, y coordinates for start of next open/left arm
# self.path.moveTo(float(index - self.w), float(new_bars[0]['open']))
# self.path.addPath(new_history_qpath)
# self.start_index += extra
# self.draw_history()
if just_history: if just_history:
self.update() self.update()
return return
@ -701,7 +751,7 @@ class BarItems(pg.GraphicsObject):
self.draw_last_bar() self.draw_last_bar()
self.update() self.update()
# @timeit @timeit
def paint(self, p, opt, widget): def paint(self, p, opt, widget):
# profiler = pg.debug.Profiler(disabled=False, delayed=False) # profiler = pg.debug.Profiler(disabled=False, delayed=False)
@ -717,9 +767,13 @@ class BarItems(pg.GraphicsObject):
# as is necesarry for what's in "view". Not sure if this will # as is necesarry for what's in "view". Not sure if this will
# lead to any perf gains other then when zoomed in to less bars # lead to any perf gains other then when zoomed in to less bars
# in view. # in view.
# p.drawPicture(0, 0, self.history)
p.drawPicture(0, 0, self.last_bar) p.drawPicture(0, 0, self.last_bar)
p.setPen(self.bars_pen) p.setPen(self.bars_pen)
# TODO: does it matter which we use?
# p.drawPath(self._h_path.path())
p.drawPath(self.path) p.drawPath(self.path)
# @timeit # @timeit
@ -740,6 +794,7 @@ class BarItems(pg.GraphicsObject):
# compute aggregate bounding rectangle # compute aggregate bounding rectangle
lb = self.last_bar.boundingRect() lb = self.last_bar.boundingRect()
hb = self.path.boundingRect() hb = self.path.boundingRect()
# hb = self._h_path.boundingRect()
return QtCore.QRectF( return QtCore.QRectF(
# top left # top left

View File

@ -29,8 +29,8 @@ from ..log import get_logger
log = get_logger(__name__) log = get_logger(__name__)
# chart-wide fonts specified in inches # chart-wide fonts specified in inches
_default_font_inches_we_like = 6 / 96 _default_font_inches_we_like = 0.0666
_down_2_font_inches_we_like = 5 / 96 _down_2_font_inches_we_like = 6 / 96
class DpiAwareFont: class DpiAwareFont: