2022-04-14 13:38:25 +00:00
|
|
|
# piker: trading gear for hackers
|
|
|
|
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
|
|
|
|
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
'''
|
|
|
|
High level streaming graphics primitives.
|
|
|
|
|
|
|
|
This is an intermediate layer which associates real-time low latency
|
|
|
|
graphics primitives with underlying FSP related data structures for fast
|
|
|
|
incremental update.
|
|
|
|
|
|
|
|
'''
|
2022-04-16 19:22:11 +00:00
|
|
|
from __future__ import annotations
|
2022-04-20 15:43:47 +00:00
|
|
|
from functools import partial
|
2022-04-14 13:38:25 +00:00
|
|
|
from typing import (
|
|
|
|
Optional,
|
|
|
|
Callable,
|
|
|
|
)
|
|
|
|
|
|
|
|
import msgspec
|
|
|
|
import numpy as np
|
2022-04-20 15:43:47 +00:00
|
|
|
from numpy.lib import recfunctions as rfn
|
2022-04-14 13:38:25 +00:00
|
|
|
import pyqtgraph as pg
|
|
|
|
from PyQt5.QtGui import QPainterPath
|
2022-04-26 12:34:53 +00:00
|
|
|
from PyQt5.QtCore import (
|
|
|
|
# Qt,
|
|
|
|
QLineF,
|
|
|
|
# QSizeF,
|
|
|
|
QRectF,
|
|
|
|
# QPointF,
|
|
|
|
)
|
2022-04-14 13:38:25 +00:00
|
|
|
|
|
|
|
from ..data._sharedmem import (
|
|
|
|
ShmArray,
|
2022-04-23 03:02:02 +00:00
|
|
|
# open_shm_array,
|
2022-04-14 13:38:25 +00:00
|
|
|
)
|
2022-05-13 20:16:17 +00:00
|
|
|
from .._profile import (
|
|
|
|
pg_profile_enabled,
|
2022-05-15 19:21:25 +00:00
|
|
|
# ms_slower_then,
|
|
|
|
)
|
|
|
|
from ._pathops import (
|
|
|
|
gen_ohlc_qpath,
|
2022-05-15 19:45:06 +00:00
|
|
|
ohlc_to_line,
|
2022-05-13 20:16:17 +00:00
|
|
|
)
|
2022-04-16 19:22:11 +00:00
|
|
|
from ._ohlc import (
|
|
|
|
BarItems,
|
|
|
|
)
|
|
|
|
from ._curve import (
|
|
|
|
FastAppendCurve,
|
|
|
|
)
|
2022-04-20 15:43:47 +00:00
|
|
|
from ..log import get_logger
|
|
|
|
|
|
|
|
|
|
|
|
log = get_logger(__name__)
|
2022-04-14 13:38:25 +00:00
|
|
|
|
|
|
|
# class FlowsTable(msgspec.Struct):
|
|
|
|
# '''
|
|
|
|
# Data-AGGRegate: high level API onto multiple (categorized)
|
|
|
|
# ``Flow``s with high level processing routines for
|
|
|
|
# multi-graphics computations and display.
|
|
|
|
|
|
|
|
# '''
|
|
|
|
# flows: dict[str, np.ndarray] = {}
|
|
|
|
|
2022-04-16 19:22:11 +00:00
|
|
|
# @classmethod
|
|
|
|
# def from_token(
|
|
|
|
# cls,
|
|
|
|
# shm_token: tuple[
|
|
|
|
# str,
|
|
|
|
# str,
|
|
|
|
# tuple[str, str],
|
|
|
|
# ],
|
|
|
|
|
|
|
|
# ) -> Renderer:
|
|
|
|
|
|
|
|
# shm = attach_shm_array(token)
|
|
|
|
# return cls(shm)
|
|
|
|
|
2022-04-14 13:38:25 +00:00
|
|
|
|
2022-04-20 15:43:47 +00:00
|
|
|
def rowarr_to_path(
|
|
|
|
rows_array: np.ndarray,
|
|
|
|
x_basis: np.ndarray,
|
|
|
|
flow: Flow,
|
|
|
|
|
|
|
|
) -> QPainterPath:
|
|
|
|
|
|
|
|
# TODO: we could in theory use ``numba`` to flatten
|
|
|
|
# if needed?
|
|
|
|
|
|
|
|
# to 1d
|
|
|
|
y = rows_array.flatten()
|
|
|
|
|
|
|
|
return pg.functions.arrayToQPath(
|
|
|
|
# these get passed at render call time
|
|
|
|
x=x_basis[:y.size],
|
|
|
|
y=y,
|
|
|
|
connect='all',
|
|
|
|
finiteCheck=False,
|
|
|
|
path=flow.path,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-04-23 03:02:02 +00:00
|
|
|
def mk_ohlc_flat_copy(
|
2022-04-20 15:43:47 +00:00
|
|
|
ohlc_shm: ShmArray,
|
|
|
|
|
|
|
|
# XXX: we bind this in currently..
|
2022-04-23 03:02:02 +00:00
|
|
|
# x_basis: np.ndarray,
|
2022-04-20 15:43:47 +00:00
|
|
|
|
|
|
|
# vr: Optional[slice] = None,
|
|
|
|
|
2022-04-23 03:02:02 +00:00
|
|
|
) -> tuple[np.ndarray, np.ndarray]:
|
2022-04-20 15:43:47 +00:00
|
|
|
'''
|
|
|
|
Return flattened-non-copy view into an OHLC shm array.
|
|
|
|
|
|
|
|
'''
|
|
|
|
ohlc = ohlc_shm._array[['open', 'high', 'low', 'close']]
|
|
|
|
# if vr:
|
|
|
|
# ohlc = ohlc[vr]
|
|
|
|
# x = x_basis[vr]
|
|
|
|
|
|
|
|
unstructured = rfn.structured_to_unstructured(
|
|
|
|
ohlc,
|
|
|
|
copy=False,
|
|
|
|
)
|
|
|
|
# breakpoint()
|
|
|
|
y = unstructured.flatten()
|
2022-04-23 03:02:02 +00:00
|
|
|
# x = x_basis[:y.size]
|
|
|
|
return y
|
2022-04-20 15:43:47 +00:00
|
|
|
|
|
|
|
|
2022-04-14 13:38:25 +00:00
|
|
|
class Flow(msgspec.Struct): # , frozen=True):
|
|
|
|
'''
|
2022-04-20 15:43:47 +00:00
|
|
|
(Financial Signal-)Flow compound type which wraps a real-time
|
|
|
|
shm array stream with displayed graphics (curves, charts)
|
|
|
|
for high level access and control as well as efficient incremental
|
|
|
|
update.
|
2022-04-14 13:38:25 +00:00
|
|
|
|
|
|
|
The intention is for this type to eventually be capable of shm-passing
|
|
|
|
of incrementally updated graphics stream data between actors.
|
|
|
|
|
|
|
|
'''
|
|
|
|
name: str
|
|
|
|
plot: pg.PlotItem
|
2022-04-16 19:22:11 +00:00
|
|
|
graphics: pg.GraphicsObject
|
|
|
|
_shm: ShmArray
|
|
|
|
|
2022-04-14 13:38:25 +00:00
|
|
|
is_ohlc: bool = False
|
|
|
|
render: bool = True # toggle for display loop
|
2022-04-24 16:33:25 +00:00
|
|
|
gy: Optional[ShmArray] = None
|
|
|
|
gx: Optional[np.ndarray] = None
|
2022-04-23 03:02:02 +00:00
|
|
|
_iflat_last: int = 0
|
|
|
|
_iflat_first: int = 0
|
2022-04-14 13:38:25 +00:00
|
|
|
|
2022-04-16 19:22:11 +00:00
|
|
|
_last_uppx: float = 0
|
|
|
|
_in_ds: bool = False
|
|
|
|
|
|
|
|
_graphics_tranform_fn: Optional[Callable[ShmArray, np.ndarray]] = None
|
|
|
|
|
|
|
|
# map from uppx -> (downsampled data, incremental graphics)
|
2022-04-20 15:43:47 +00:00
|
|
|
_src_r: Optional[Renderer] = None
|
2022-04-16 19:22:11 +00:00
|
|
|
_render_table: dict[
|
|
|
|
Optional[int],
|
|
|
|
tuple[Renderer, pg.GraphicsItem],
|
|
|
|
] = {}
|
2022-04-14 13:38:25 +00:00
|
|
|
|
|
|
|
# TODO: hackery to be able to set a shm later
|
|
|
|
# but whilst also allowing this type to hashable,
|
|
|
|
# likely will require serializable token that is used to attach
|
|
|
|
# to the underlying shm ref after startup?
|
2022-04-16 19:22:11 +00:00
|
|
|
# _shm: Optional[ShmArray] = None # currently, may be filled in "later"
|
2022-04-14 13:38:25 +00:00
|
|
|
|
|
|
|
# last read from shm (usually due to an update call)
|
|
|
|
_last_read: Optional[np.ndarray] = None
|
|
|
|
|
|
|
|
# cache of y-range values per x-range input.
|
|
|
|
_mxmns: dict[tuple[int, int], tuple[float, float]] = {}
|
|
|
|
|
|
|
|
@property
|
|
|
|
def shm(self) -> ShmArray:
|
|
|
|
return self._shm
|
|
|
|
|
|
|
|
# TODO: remove this and only allow setting through
|
|
|
|
# private ``._shm`` attr?
|
|
|
|
@shm.setter
|
|
|
|
def shm(self, shm: ShmArray) -> ShmArray:
|
|
|
|
print(f'{self.name} DO NOT SET SHM THIS WAY!?')
|
|
|
|
self._shm = shm
|
|
|
|
|
|
|
|
def maxmin(
|
|
|
|
self,
|
|
|
|
lbar,
|
|
|
|
rbar,
|
|
|
|
|
|
|
|
) -> tuple[float, float]:
|
|
|
|
'''
|
|
|
|
Compute the cached max and min y-range values for a given
|
|
|
|
x-range determined by ``lbar`` and ``rbar``.
|
|
|
|
|
|
|
|
'''
|
|
|
|
rkey = (lbar, rbar)
|
|
|
|
cached_result = self._mxmns.get(rkey)
|
|
|
|
if cached_result:
|
|
|
|
return cached_result
|
|
|
|
|
|
|
|
shm = self.shm
|
|
|
|
if shm is None:
|
|
|
|
mxmn = None
|
|
|
|
|
|
|
|
else: # new block for profiling?..
|
|
|
|
arr = shm.array
|
|
|
|
|
|
|
|
# build relative indexes into shm array
|
|
|
|
# TODO: should we just add/use a method
|
|
|
|
# on the shm to do this?
|
|
|
|
ifirst = arr[0]['index']
|
|
|
|
slice_view = arr[
|
|
|
|
lbar - ifirst:
|
|
|
|
(rbar - ifirst) + 1
|
|
|
|
]
|
|
|
|
|
|
|
|
if not slice_view.size:
|
|
|
|
mxmn = None
|
|
|
|
|
|
|
|
else:
|
|
|
|
if self.is_ohlc:
|
|
|
|
ylow = np.min(slice_view['low'])
|
|
|
|
yhigh = np.max(slice_view['high'])
|
|
|
|
|
|
|
|
else:
|
|
|
|
view = slice_view[self.name]
|
|
|
|
ylow = np.min(view)
|
|
|
|
yhigh = np.max(view)
|
|
|
|
|
|
|
|
mxmn = ylow, yhigh
|
|
|
|
|
|
|
|
if mxmn is not None:
|
|
|
|
# cache new mxmn result
|
|
|
|
self._mxmns[rkey] = mxmn
|
|
|
|
|
|
|
|
return mxmn
|
|
|
|
|
|
|
|
def view_range(self) -> tuple[int, int]:
|
|
|
|
'''
|
|
|
|
Return the indexes in view for the associated
|
|
|
|
plot displaying this flow's data.
|
|
|
|
|
|
|
|
'''
|
|
|
|
vr = self.plot.viewRect()
|
|
|
|
return int(vr.left()), int(vr.right())
|
|
|
|
|
|
|
|
def datums_range(self) -> tuple[
|
|
|
|
int, int, int, int, int, int
|
|
|
|
]:
|
|
|
|
'''
|
|
|
|
Return a range tuple for the datums present in view.
|
|
|
|
|
|
|
|
'''
|
|
|
|
l, r = self.view_range()
|
|
|
|
|
|
|
|
# TODO: avoid this and have shm passed
|
|
|
|
# in earlier.
|
|
|
|
if self.shm is None:
|
|
|
|
# haven't initialized the flow yet
|
|
|
|
return (0, l, 0, 0, r, 0)
|
|
|
|
|
|
|
|
array = self.shm.array
|
|
|
|
index = array['index']
|
|
|
|
start = index[0]
|
|
|
|
end = index[-1]
|
|
|
|
lbar = max(l, start)
|
|
|
|
rbar = min(r, end)
|
|
|
|
return (
|
|
|
|
start, l, lbar, rbar, r, end,
|
|
|
|
)
|
|
|
|
|
|
|
|
def read(self) -> tuple[
|
|
|
|
int, int, np.ndarray,
|
|
|
|
int, int, np.ndarray,
|
|
|
|
]:
|
2022-04-20 15:43:47 +00:00
|
|
|
# read call
|
2022-04-14 13:38:25 +00:00
|
|
|
array = self.shm.array
|
2022-04-20 15:43:47 +00:00
|
|
|
|
2022-04-14 13:38:25 +00:00
|
|
|
indexes = array['index']
|
|
|
|
ifirst = indexes[0]
|
|
|
|
ilast = indexes[-1]
|
|
|
|
|
|
|
|
ifirst, l, lbar, rbar, r, ilast = self.datums_range()
|
|
|
|
|
|
|
|
# get read-relative indices adjusting
|
|
|
|
# for master shm index.
|
|
|
|
lbar_i = max(l, ifirst) - ifirst
|
|
|
|
rbar_i = min(r, ilast) - ifirst
|
|
|
|
|
|
|
|
# TODO: we could do it this way as well no?
|
|
|
|
# to_draw = array[lbar - ifirst:(rbar - ifirst) + 1]
|
|
|
|
in_view = array[lbar_i: rbar_i + 1]
|
|
|
|
|
|
|
|
return (
|
|
|
|
# abs indices + full data set
|
|
|
|
ifirst, ilast, array,
|
|
|
|
|
|
|
|
# relative indices + in view datums
|
|
|
|
lbar_i, rbar_i, in_view,
|
|
|
|
)
|
|
|
|
|
|
|
|
def update_graphics(
|
|
|
|
self,
|
|
|
|
use_vr: bool = True,
|
|
|
|
render: bool = True,
|
|
|
|
array_key: Optional[str] = None,
|
|
|
|
|
2022-04-29 15:24:21 +00:00
|
|
|
profiler: Optional[pg.debug.Profiler] = None,
|
2022-04-20 15:43:47 +00:00
|
|
|
|
2022-04-14 13:38:25 +00:00
|
|
|
**kwargs,
|
|
|
|
|
|
|
|
) -> pg.GraphicsObject:
|
|
|
|
'''
|
|
|
|
Read latest datums from shm and render to (incrementally)
|
|
|
|
render to graphics.
|
|
|
|
|
|
|
|
'''
|
2022-04-20 15:43:47 +00:00
|
|
|
|
2022-05-13 20:16:17 +00:00
|
|
|
# profiler = profiler or pg.debug.Profiler(
|
|
|
|
profiler = pg.debug.Profiler(
|
2022-04-20 15:43:47 +00:00
|
|
|
msg=f'Flow.update_graphics() for {self.name}',
|
|
|
|
disabled=not pg_profile_enabled(),
|
2022-05-13 20:16:17 +00:00
|
|
|
# disabled=False,
|
|
|
|
ms_threshold=4,
|
|
|
|
# ms_threshold=ms_slower_then,
|
2022-04-20 15:43:47 +00:00
|
|
|
)
|
2022-04-14 13:38:25 +00:00
|
|
|
# shm read and slice to view
|
2022-04-20 15:43:47 +00:00
|
|
|
read = (
|
|
|
|
xfirst, xlast, array,
|
|
|
|
ivl, ivr, in_view,
|
|
|
|
) = self.read()
|
|
|
|
profiler('read src shm data')
|
2022-04-14 13:38:25 +00:00
|
|
|
|
|
|
|
if (
|
|
|
|
not in_view.size
|
|
|
|
or not render
|
|
|
|
):
|
|
|
|
return self.graphics
|
|
|
|
|
|
|
|
graphics = self.graphics
|
|
|
|
if isinstance(graphics, BarItems):
|
2022-04-16 19:22:11 +00:00
|
|
|
|
2022-04-20 15:43:47 +00:00
|
|
|
# if no source data renderer exists create one.
|
|
|
|
r = self._src_r
|
2022-04-16 19:22:11 +00:00
|
|
|
if not r:
|
2022-04-20 15:43:47 +00:00
|
|
|
# OHLC bars path renderer
|
|
|
|
r = self._src_r = Renderer(
|
2022-04-16 19:22:11 +00:00
|
|
|
flow=self,
|
2022-04-20 15:43:47 +00:00
|
|
|
# TODO: rename this to something with ohlc
|
2022-05-15 18:30:13 +00:00
|
|
|
draw_path=gen_ohlc_qpath,
|
2022-04-16 19:22:11 +00:00
|
|
|
last_read=read,
|
|
|
|
)
|
2022-04-20 15:43:47 +00:00
|
|
|
|
2022-04-16 19:22:11 +00:00
|
|
|
ds_curve_r = Renderer(
|
|
|
|
flow=self,
|
2022-04-20 15:43:47 +00:00
|
|
|
|
|
|
|
# just swap in the flat view
|
2022-04-24 16:33:25 +00:00
|
|
|
# data_t=lambda array: self.gy.array,
|
2022-04-16 19:22:11 +00:00
|
|
|
last_read=read,
|
2022-04-20 15:43:47 +00:00
|
|
|
draw_path=partial(
|
|
|
|
rowarr_to_path,
|
|
|
|
x_basis=None,
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
curve = FastAppendCurve(
|
|
|
|
name='OHLC',
|
|
|
|
color=graphics._color,
|
2022-04-16 19:22:11 +00:00
|
|
|
)
|
2022-04-20 15:43:47 +00:00
|
|
|
curve.hide()
|
|
|
|
self.plot.addItem(curve)
|
2022-04-16 19:22:11 +00:00
|
|
|
|
|
|
|
# baseline "line" downsampled OHLC curve that should
|
|
|
|
# kick on only when we reach a certain uppx threshold.
|
|
|
|
self._render_table[0] = (
|
|
|
|
ds_curve_r,
|
2022-04-20 15:43:47 +00:00
|
|
|
curve,
|
2022-04-16 19:22:11 +00:00
|
|
|
)
|
|
|
|
|
2022-04-20 15:43:47 +00:00
|
|
|
dsc_r, curve = self._render_table[0]
|
|
|
|
|
2022-04-16 19:22:11 +00:00
|
|
|
# do checks for whether or not we require downsampling:
|
|
|
|
# - if we're **not** downsampling then we simply want to
|
|
|
|
# render the bars graphics curve and update..
|
|
|
|
# - if insteam we are in a downsamplig state then we to
|
2022-05-10 21:57:14 +00:00
|
|
|
x_gt = 6
|
2022-04-20 15:43:47 +00:00
|
|
|
uppx = curve.x_uppx()
|
|
|
|
in_line = should_line = curve.isVisible()
|
|
|
|
if (
|
|
|
|
should_line
|
|
|
|
and uppx < x_gt
|
|
|
|
):
|
2022-04-23 03:02:02 +00:00
|
|
|
print('FLIPPING TO BARS')
|
2022-04-20 15:43:47 +00:00
|
|
|
should_line = False
|
|
|
|
|
|
|
|
elif (
|
|
|
|
not should_line
|
|
|
|
and uppx >= x_gt
|
|
|
|
):
|
2022-04-23 03:02:02 +00:00
|
|
|
print('FLIPPING TO LINE')
|
2022-04-20 15:43:47 +00:00
|
|
|
should_line = True
|
|
|
|
|
|
|
|
profiler(f'ds logic complete line={should_line}')
|
|
|
|
|
|
|
|
# do graphics updates
|
|
|
|
if should_line:
|
|
|
|
|
2022-04-23 03:02:02 +00:00
|
|
|
fields = ['open', 'high', 'low', 'close']
|
2022-04-24 16:33:25 +00:00
|
|
|
if self.gy is None:
|
2022-04-23 03:02:02 +00:00
|
|
|
# create a flattened view onto the OHLC array
|
|
|
|
# which can be read as a line-style format
|
|
|
|
shm = self.shm
|
2022-05-15 19:45:06 +00:00
|
|
|
(
|
|
|
|
self._iflat_first,
|
|
|
|
self._iflat_last,
|
|
|
|
self.gx,
|
|
|
|
self.gy,
|
|
|
|
) = ohlc_to_line(shm)
|
|
|
|
|
|
|
|
# self.gy = self.shm.ustruct(fields)
|
|
|
|
# first = self._iflat_first = self.shm._first.value
|
|
|
|
# last = self._iflat_last = self.shm._last.value
|
|
|
|
|
|
|
|
# # write pushed data to flattened copy
|
|
|
|
# self.gy[first:last] = rfn.structured_to_unstructured(
|
|
|
|
# self.shm.array[fields]
|
|
|
|
# )
|
|
|
|
|
|
|
|
# # generate an flat-interpolated x-domain
|
|
|
|
# self.gx = (
|
|
|
|
# np.broadcast_to(
|
|
|
|
# shm._array['index'][:, None],
|
|
|
|
# (
|
|
|
|
# shm._array.size,
|
|
|
|
# # 4, # only ohlc
|
|
|
|
# self.gy.shape[1],
|
|
|
|
# ),
|
|
|
|
# ) + np.array([-0.5, 0, 0, 0.5])
|
|
|
|
# )
|
|
|
|
# assert self.gy.any()
|
2022-04-23 03:02:02 +00:00
|
|
|
|
|
|
|
# print(f'unstruct diff: {time.time() - start}')
|
|
|
|
# profiler('read unstr view bars to line')
|
2022-04-24 16:33:25 +00:00
|
|
|
# start = self.gy._first.value
|
2022-05-10 21:57:14 +00:00
|
|
|
# update flatted ohlc copy
|
2022-04-23 03:02:02 +00:00
|
|
|
(
|
|
|
|
iflat_first,
|
|
|
|
iflat,
|
|
|
|
ishm_last,
|
|
|
|
ishm_first,
|
|
|
|
) = (
|
|
|
|
self._iflat_first,
|
|
|
|
self._iflat_last,
|
|
|
|
self.shm._last.value,
|
|
|
|
self.shm._first.value
|
|
|
|
)
|
|
|
|
|
|
|
|
# check for shm prepend updates since last read.
|
|
|
|
if iflat_first != ishm_first:
|
|
|
|
|
|
|
|
# write newly prepended data to flattened copy
|
2022-04-24 16:33:25 +00:00
|
|
|
self.gy[
|
2022-04-23 03:02:02 +00:00
|
|
|
ishm_first:iflat_first
|
|
|
|
] = rfn.structured_to_unstructured(
|
2022-04-26 12:34:53 +00:00
|
|
|
self.shm._array[fields][ishm_first:iflat_first]
|
2022-04-23 03:02:02 +00:00
|
|
|
)
|
|
|
|
self._iflat_first = ishm_first
|
|
|
|
|
2022-04-24 16:33:25 +00:00
|
|
|
# # flat = self.gy = self.shm.unstruct_view(fields)
|
|
|
|
# self.gy = self.shm.ustruct(fields)
|
2022-04-23 03:02:02 +00:00
|
|
|
# # self._iflat_last = self.shm._last.value
|
|
|
|
|
|
|
|
# # self._iflat_first = self.shm._first.value
|
|
|
|
# # do an update for the most recent prepend
|
|
|
|
# # index
|
|
|
|
# iflat = ishm_first
|
|
|
|
|
2022-05-10 21:57:14 +00:00
|
|
|
to_update = rfn.structured_to_unstructured(
|
2022-04-23 03:02:02 +00:00
|
|
|
self.shm._array[iflat:ishm_last][fields]
|
2022-05-10 21:57:14 +00:00
|
|
|
)
|
|
|
|
|
2022-04-24 16:33:25 +00:00
|
|
|
self.gy[iflat:ishm_last][:] = to_update
|
2022-05-10 21:57:14 +00:00
|
|
|
profiler('updated ustruct OHLC data')
|
|
|
|
|
2022-04-23 03:02:02 +00:00
|
|
|
# slice out up-to-last step contents
|
2022-04-24 16:33:25 +00:00
|
|
|
y_flat = self.gy[ishm_first:ishm_last]
|
|
|
|
x_flat = self.gx[ishm_first:ishm_last]
|
2022-05-10 21:57:14 +00:00
|
|
|
|
2022-04-23 03:02:02 +00:00
|
|
|
# update local last-index tracking
|
|
|
|
self._iflat_last = ishm_last
|
2022-05-10 21:57:14 +00:00
|
|
|
|
2022-04-23 03:02:02 +00:00
|
|
|
# reshape to 1d for graphics rendering
|
2022-05-10 21:57:14 +00:00
|
|
|
y = y_flat.reshape(-1)
|
|
|
|
x = x_flat.reshape(-1)
|
|
|
|
profiler('flattened ustruct OHLC data')
|
|
|
|
|
2022-04-23 03:02:02 +00:00
|
|
|
# do all the same for only in-view data
|
2022-05-10 21:57:14 +00:00
|
|
|
y_iv_flat = y_flat[ivl:ivr]
|
|
|
|
x_iv_flat = x_flat[ivl:ivr]
|
|
|
|
y_iv = y_iv_flat.reshape(-1)
|
|
|
|
x_iv = x_iv_flat.reshape(-1)
|
|
|
|
profiler('flattened ustruct in-view OHLC data')
|
|
|
|
|
2022-04-23 03:02:02 +00:00
|
|
|
# legacy full-recompute-everytime method
|
2022-05-10 21:57:14 +00:00
|
|
|
# x, y = ohlc_flatten(array)
|
|
|
|
# x_iv, y_iv = ohlc_flatten(in_view)
|
|
|
|
# profiler('flattened OHLC data')
|
2022-04-20 15:43:47 +00:00
|
|
|
|
|
|
|
curve.update_from_array(
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
x_iv=x_iv,
|
2022-05-10 21:57:14 +00:00
|
|
|
y_iv=y_iv,
|
|
|
|
view_range=(ivl, ivr), # hack
|
2022-04-20 15:43:47 +00:00
|
|
|
profiler=profiler,
|
2022-04-26 12:34:53 +00:00
|
|
|
# should_redraw=False,
|
2022-04-29 15:24:21 +00:00
|
|
|
|
|
|
|
# NOTE: already passed through by display loop?
|
|
|
|
# do_append=uppx < 16,
|
|
|
|
**kwargs,
|
2022-04-20 15:43:47 +00:00
|
|
|
)
|
2022-04-23 03:02:02 +00:00
|
|
|
curve.show()
|
2022-04-20 15:43:47 +00:00
|
|
|
profiler('updated ds curve')
|
|
|
|
|
|
|
|
else:
|
|
|
|
# render incremental or in-view update
|
|
|
|
# and apply ouput (path) to graphics.
|
|
|
|
path, last = r.render(
|
|
|
|
read,
|
|
|
|
only_in_view=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
graphics.path = path
|
|
|
|
graphics.draw_last(last)
|
|
|
|
|
|
|
|
# NOTE: on appends we used to have to flip the coords
|
|
|
|
# cache thought it doesn't seem to be required any more?
|
|
|
|
# graphics.setCacheMode(QtWidgets.QGraphicsItem.NoCache)
|
|
|
|
# graphics.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
|
|
|
|
|
2022-05-10 21:57:14 +00:00
|
|
|
# graphics.prepareGeometryChange()
|
2022-04-20 15:43:47 +00:00
|
|
|
graphics.update()
|
|
|
|
|
|
|
|
if (
|
|
|
|
not in_line
|
|
|
|
and should_line
|
|
|
|
):
|
|
|
|
# change to line graphic
|
|
|
|
|
|
|
|
log.info(
|
|
|
|
f'downsampling to line graphic {self.name}'
|
|
|
|
)
|
|
|
|
graphics.hide()
|
|
|
|
# graphics.update()
|
|
|
|
curve.show()
|
|
|
|
curve.update()
|
|
|
|
|
|
|
|
elif in_line and not should_line:
|
|
|
|
log.info(f'showing bars graphic {self.name}')
|
|
|
|
curve.hide()
|
|
|
|
graphics.show()
|
|
|
|
graphics.update()
|
|
|
|
|
2022-04-16 19:22:11 +00:00
|
|
|
# update our pre-downsample-ready data and then pass that
|
|
|
|
# new data the downsampler algo for incremental update.
|
|
|
|
|
2022-04-20 15:43:47 +00:00
|
|
|
# graphics.update_from_array(
|
|
|
|
# array,
|
|
|
|
# in_view,
|
|
|
|
# view_range=(ivl, ivr) if use_vr else None,
|
2022-04-14 13:38:25 +00:00
|
|
|
|
2022-04-20 15:43:47 +00:00
|
|
|
# **kwargs,
|
|
|
|
# )
|
2022-04-14 13:38:25 +00:00
|
|
|
|
2022-04-20 15:43:47 +00:00
|
|
|
# generate and apply path to graphics obj
|
|
|
|
# graphics.path, last = r.render(
|
|
|
|
# read,
|
|
|
|
# only_in_view=True,
|
|
|
|
# )
|
|
|
|
# graphics.draw_last(last)
|
2022-04-16 19:22:11 +00:00
|
|
|
|
2022-04-14 13:38:25 +00:00
|
|
|
else:
|
2022-04-26 12:34:53 +00:00
|
|
|
# ``FastAppendCurve`` case:
|
2022-04-16 19:22:11 +00:00
|
|
|
array_key = array_key or self.name
|
2022-04-29 15:24:21 +00:00
|
|
|
uppx = graphics.x_uppx()
|
2022-05-13 20:16:17 +00:00
|
|
|
profiler('read uppx')
|
2022-04-16 19:22:11 +00:00
|
|
|
|
2022-04-24 16:33:25 +00:00
|
|
|
if graphics._step_mode and self.gy is None:
|
2022-04-26 12:34:53 +00:00
|
|
|
self._iflat_first = self.shm._first.value
|
2022-04-24 16:33:25 +00:00
|
|
|
|
|
|
|
# create a flattened view onto the OHLC array
|
|
|
|
# which can be read as a line-style format
|
|
|
|
shm = self.shm
|
|
|
|
|
|
|
|
# fields = ['index', array_key]
|
2022-04-26 12:34:53 +00:00
|
|
|
i = shm._array['index'].copy()
|
|
|
|
out = shm._array[array_key].copy()
|
|
|
|
|
|
|
|
self.gx = np.broadcast_to(
|
|
|
|
i[:, None],
|
|
|
|
(i.size, 2),
|
|
|
|
) + np.array([-0.5, 0.5])
|
|
|
|
|
|
|
|
# self.gy = np.broadcast_to(
|
|
|
|
# out[:, None], (out.size, 2),
|
|
|
|
# )
|
|
|
|
self.gy = np.empty((len(out), 2), dtype=out.dtype)
|
|
|
|
self.gy[:] = out[:, np.newaxis]
|
2022-04-24 16:33:25 +00:00
|
|
|
|
2022-04-26 12:34:53 +00:00
|
|
|
# start y at origin level
|
|
|
|
self.gy[0, 0] = 0
|
2022-05-13 20:16:17 +00:00
|
|
|
profiler('generated step mode data')
|
2022-04-26 12:34:53 +00:00
|
|
|
|
2022-04-24 16:33:25 +00:00
|
|
|
if graphics._step_mode:
|
|
|
|
(
|
|
|
|
iflat_first,
|
|
|
|
iflat,
|
|
|
|
ishm_last,
|
|
|
|
ishm_first,
|
|
|
|
) = (
|
|
|
|
self._iflat_first,
|
|
|
|
self._iflat_last,
|
|
|
|
self.shm._last.value,
|
|
|
|
self.shm._first.value
|
|
|
|
)
|
|
|
|
|
2022-04-26 12:34:53 +00:00
|
|
|
il = max(iflat - 1, 0)
|
2022-05-13 20:16:17 +00:00
|
|
|
profiler('read step mode incr update indices')
|
2022-04-26 12:34:53 +00:00
|
|
|
|
2022-04-24 16:33:25 +00:00
|
|
|
# check for shm prepend updates since last read.
|
|
|
|
if iflat_first != ishm_first:
|
|
|
|
|
2022-04-26 12:34:53 +00:00
|
|
|
print(f'prepend {array_key}')
|
|
|
|
|
2022-05-01 23:13:21 +00:00
|
|
|
# i_prepend = self.shm._array['index'][
|
|
|
|
# ishm_first:iflat_first]
|
|
|
|
y_prepend = self.shm._array[array_key][
|
|
|
|
ishm_first:iflat_first
|
|
|
|
]
|
2022-04-26 12:34:53 +00:00
|
|
|
|
|
|
|
y2_prepend = np.broadcast_to(
|
|
|
|
y_prepend[:, None], (y_prepend.size, 2),
|
2022-04-24 16:33:25 +00:00
|
|
|
)
|
2022-04-26 12:34:53 +00:00
|
|
|
|
|
|
|
# write newly prepended data to flattened copy
|
|
|
|
self.gy[ishm_first:iflat_first] = y2_prepend
|
2022-04-24 16:33:25 +00:00
|
|
|
self._iflat_first = ishm_first
|
2022-05-13 20:16:17 +00:00
|
|
|
profiler('prepended step mode history')
|
2022-04-26 12:34:53 +00:00
|
|
|
|
|
|
|
append_diff = ishm_last - iflat
|
|
|
|
if append_diff:
|
|
|
|
|
|
|
|
# slice up to the last datum since last index/append update
|
2022-05-01 23:13:21 +00:00
|
|
|
# new_x = self.shm._array[il:ishm_last]['index']
|
|
|
|
new_y = self.shm._array[il:ishm_last][array_key]
|
2022-04-26 12:34:53 +00:00
|
|
|
|
|
|
|
new_y2 = np.broadcast_to(
|
|
|
|
new_y[:, None], (new_y.size, 2),
|
2022-04-24 16:33:25 +00:00
|
|
|
)
|
2022-04-26 12:34:53 +00:00
|
|
|
self.gy[il:ishm_last] = new_y2
|
2022-04-24 16:33:25 +00:00
|
|
|
profiler('updated step curve data')
|
|
|
|
|
2022-04-26 12:34:53 +00:00
|
|
|
# print(
|
|
|
|
# f'append size: {append_diff}\n'
|
|
|
|
# f'new_x: {new_x}\n'
|
|
|
|
# f'new_y: {new_y}\n'
|
|
|
|
# f'new_y2: {new_y2}\n'
|
|
|
|
# f'new gy: {gy}\n'
|
|
|
|
# )
|
|
|
|
|
|
|
|
# update local last-index tracking
|
|
|
|
self._iflat_last = ishm_last
|
|
|
|
|
2022-04-24 16:33:25 +00:00
|
|
|
# slice out up-to-last step contents
|
2022-04-26 12:34:53 +00:00
|
|
|
x_step = self.gx[ishm_first:ishm_last+2]
|
2022-05-01 23:13:21 +00:00
|
|
|
# shape to 1d
|
2022-04-24 16:33:25 +00:00
|
|
|
x = x_step.reshape(-1)
|
2022-05-13 20:16:17 +00:00
|
|
|
profiler('sliced step x')
|
2022-04-26 12:34:53 +00:00
|
|
|
|
|
|
|
y_step = self.gy[ishm_first:ishm_last+2]
|
|
|
|
lasts = self.shm.array[['index', array_key]]
|
|
|
|
last = lasts[array_key][-1]
|
|
|
|
y_step[-1] = last
|
2022-05-01 23:13:21 +00:00
|
|
|
# shape to 1d
|
2022-04-24 16:33:25 +00:00
|
|
|
y = y_step.reshape(-1)
|
|
|
|
|
2022-04-26 12:34:53 +00:00
|
|
|
# s = 6
|
|
|
|
# print(f'lasts: {x[-2*s:]}, {y[-2*s:]}')
|
|
|
|
|
2022-05-13 20:16:17 +00:00
|
|
|
profiler('sliced step y')
|
2022-04-24 16:33:25 +00:00
|
|
|
|
|
|
|
# do all the same for only in-view data
|
2022-04-26 12:34:53 +00:00
|
|
|
ys_iv = y_step[ivl:ivr+1]
|
|
|
|
xs_iv = x_step[ivl:ivr+1]
|
|
|
|
y_iv = ys_iv.reshape(ys_iv.size)
|
|
|
|
x_iv = xs_iv.reshape(xs_iv.size)
|
|
|
|
# print(
|
|
|
|
# f'ys_iv : {ys_iv[-s:]}\n'
|
|
|
|
# f'y_iv: {y_iv[-s:]}\n'
|
|
|
|
# f'xs_iv: {xs_iv[-s:]}\n'
|
|
|
|
# f'x_iv: {x_iv[-s:]}\n'
|
|
|
|
# )
|
2022-05-13 20:16:17 +00:00
|
|
|
profiler('sliced in view step data')
|
2022-04-24 16:33:25 +00:00
|
|
|
|
|
|
|
# legacy full-recompute-everytime method
|
|
|
|
# x, y = ohlc_flatten(array)
|
|
|
|
# x_iv, y_iv = ohlc_flatten(in_view)
|
|
|
|
# profiler('flattened OHLC data')
|
2022-04-26 12:34:53 +00:00
|
|
|
|
|
|
|
x_last = array['index'][-1]
|
|
|
|
y_last = array[array_key][-1]
|
|
|
|
graphics._last_line = QLineF(
|
|
|
|
x_last - 0.5, 0,
|
|
|
|
x_last + 0.5, 0,
|
|
|
|
)
|
|
|
|
graphics._last_step_rect = QRectF(
|
|
|
|
x_last - 0.5, 0,
|
|
|
|
x_last + 0.5, y_last,
|
|
|
|
)
|
|
|
|
# graphics.update()
|
|
|
|
|
|
|
|
graphics.update_from_array(
|
|
|
|
x=x,
|
|
|
|
y=y,
|
|
|
|
|
|
|
|
x_iv=x_iv,
|
|
|
|
y_iv=y_iv,
|
|
|
|
|
|
|
|
view_range=(ivl, ivr) if use_vr else None,
|
|
|
|
|
|
|
|
draw_last=False,
|
|
|
|
slice_to_head=-2,
|
|
|
|
|
|
|
|
should_redraw=bool(append_diff),
|
2022-04-29 15:24:21 +00:00
|
|
|
|
|
|
|
# NOTE: already passed through by display loop?
|
|
|
|
# do_append=uppx < 16,
|
2022-05-13 20:16:17 +00:00
|
|
|
profiler=profiler,
|
2022-04-26 12:34:53 +00:00
|
|
|
|
|
|
|
**kwargs
|
|
|
|
)
|
2022-05-13 20:16:17 +00:00
|
|
|
profiler('updated step mode curve')
|
2022-04-26 12:34:53 +00:00
|
|
|
# graphics.reset_cache()
|
|
|
|
# print(
|
|
|
|
# f"path br: {graphics.path.boundingRect()}\n",
|
|
|
|
# # f"fast path br: {graphics.fast_path.boundingRect()}",
|
|
|
|
# f"last rect br: {graphics._last_step_rect}\n",
|
|
|
|
# f"full br: {graphics._br}\n",
|
|
|
|
# )
|
|
|
|
|
2022-04-24 16:33:25 +00:00
|
|
|
else:
|
|
|
|
x = array['index']
|
|
|
|
y = array[array_key]
|
|
|
|
x_iv = in_view['index']
|
|
|
|
y_iv = in_view[array_key]
|
2022-05-13 20:16:17 +00:00
|
|
|
profiler('sliced input arrays')
|
2022-04-24 16:33:25 +00:00
|
|
|
|
2022-04-26 12:34:53 +00:00
|
|
|
# graphics.draw_last(x, y)
|
2022-04-24 16:33:25 +00:00
|
|
|
|
2022-04-26 12:34:53 +00:00
|
|
|
graphics.update_from_array(
|
|
|
|
x=x,
|
|
|
|
y=y,
|
2022-04-14 13:38:25 +00:00
|
|
|
|
2022-04-26 12:34:53 +00:00
|
|
|
x_iv=x_iv,
|
|
|
|
y_iv=y_iv,
|
|
|
|
|
|
|
|
view_range=(ivl, ivr) if use_vr else None,
|
2022-04-14 13:38:25 +00:00
|
|
|
|
2022-04-29 15:24:21 +00:00
|
|
|
# NOTE: already passed through by display loop?
|
|
|
|
# do_append=uppx < 16,
|
2022-05-13 20:16:17 +00:00
|
|
|
profiler=profiler,
|
2022-04-26 12:34:53 +00:00
|
|
|
**kwargs
|
|
|
|
)
|
2022-05-15 19:21:25 +00:00
|
|
|
profiler('`graphics.update_from_array()` complete')
|
2022-04-14 13:38:25 +00:00
|
|
|
|
|
|
|
return graphics
|
|
|
|
|
|
|
|
|
2022-04-16 19:22:11 +00:00
|
|
|
class Renderer(msgspec.Struct):
|
2022-04-14 13:38:25 +00:00
|
|
|
|
2022-04-16 19:22:11 +00:00
|
|
|
flow: Flow
|
2022-04-14 13:38:25 +00:00
|
|
|
|
2022-04-16 19:22:11 +00:00
|
|
|
# called to render path graphics
|
2022-04-20 15:43:47 +00:00
|
|
|
draw_path: Callable[np.ndarray, QPainterPath]
|
2022-04-14 13:38:25 +00:00
|
|
|
|
2022-04-20 15:43:47 +00:00
|
|
|
# called on input data but before any graphics format
|
|
|
|
# conversions or processing.
|
|
|
|
data_t: Optional[Callable[ShmArray, np.ndarray]] = None
|
|
|
|
data_t_shm: Optional[ShmArray] = None
|
2022-04-14 13:38:25 +00:00
|
|
|
|
2022-04-20 15:43:47 +00:00
|
|
|
# called on the final data (transform) output to convert
|
|
|
|
# to "graphical data form" a format that can be passed to
|
|
|
|
# the ``.draw()`` implementation.
|
|
|
|
graphics_t: Optional[Callable[ShmArray, np.ndarray]] = None
|
|
|
|
graphics_t_shm: Optional[ShmArray] = None
|
|
|
|
|
|
|
|
# path graphics update implementation methods
|
2022-04-16 19:22:11 +00:00
|
|
|
prepend_fn: Optional[Callable[QPainterPath, QPainterPath]] = None
|
|
|
|
append_fn: Optional[Callable[QPainterPath, QPainterPath]] = None
|
2022-04-14 13:38:25 +00:00
|
|
|
|
2022-04-16 19:22:11 +00:00
|
|
|
# last array view read
|
|
|
|
last_read: Optional[np.ndarray] = None
|
2022-04-14 13:38:25 +00:00
|
|
|
|
2022-04-20 15:43:47 +00:00
|
|
|
# output graphics rendering, the main object
|
|
|
|
# processed in ``QGraphicsObject.paint()``
|
2022-04-16 19:22:11 +00:00
|
|
|
path: Optional[QPainterPath] = None
|
2022-04-14 13:38:25 +00:00
|
|
|
|
2022-04-16 19:22:11 +00:00
|
|
|
# def diff(
|
|
|
|
# self,
|
|
|
|
# latest_read: tuple[np.ndarray],
|
|
|
|
|
|
|
|
# ) -> tuple[np.ndarray]:
|
|
|
|
# # blah blah blah
|
|
|
|
# # do diffing for prepend, append and last entry
|
|
|
|
# return (
|
|
|
|
# to_prepend
|
|
|
|
# to_append
|
|
|
|
# last,
|
|
|
|
# )
|
2022-04-14 13:38:25 +00:00
|
|
|
|
|
|
|
def render(
|
|
|
|
self,
|
|
|
|
|
2022-04-20 15:43:47 +00:00
|
|
|
new_read,
|
|
|
|
|
2022-04-16 19:22:11 +00:00
|
|
|
# only render datums "in view" of the ``ChartView``
|
2022-04-20 15:43:47 +00:00
|
|
|
only_in_view: bool = False,
|
2022-04-16 19:22:11 +00:00
|
|
|
|
2022-04-14 13:38:25 +00:00
|
|
|
) -> list[QPainterPath]:
|
|
|
|
'''
|
|
|
|
Render the current graphics path(s)
|
|
|
|
|
2022-04-18 12:30:28 +00:00
|
|
|
There are (at least) 3 stages from source data to graphics data:
|
|
|
|
- a data transform (which can be stored in additional shm)
|
|
|
|
- a graphics transform which converts discrete basis data to
|
|
|
|
a `float`-basis view-coords graphics basis. (eg. ``ohlc_flatten()``,
|
|
|
|
``step_path_arrays_from_1d()``, etc.)
|
|
|
|
|
|
|
|
- blah blah blah (from notes)
|
|
|
|
|
2022-04-14 13:38:25 +00:00
|
|
|
'''
|
2022-04-16 19:22:11 +00:00
|
|
|
# do full source data render to path
|
2022-05-10 21:57:14 +00:00
|
|
|
(
|
2022-04-20 15:43:47 +00:00
|
|
|
xfirst, xlast, array,
|
|
|
|
ivl, ivr, in_view,
|
|
|
|
) = self.last_read
|
2022-04-16 19:22:11 +00:00
|
|
|
|
|
|
|
if only_in_view:
|
|
|
|
array = in_view
|
2022-04-20 15:43:47 +00:00
|
|
|
# # get latest data from flow shm
|
|
|
|
# self.last_read = (
|
|
|
|
# xfirst, xlast, array, ivl, ivr, in_view
|
|
|
|
# ) = new_read
|
2022-04-16 19:22:11 +00:00
|
|
|
|
2022-04-20 15:43:47 +00:00
|
|
|
if self.path is None or only_in_view:
|
2022-04-16 19:22:11 +00:00
|
|
|
# redraw the entire source data if we have either of:
|
|
|
|
# - no prior path graphic rendered or,
|
|
|
|
# - we always intend to re-render the data only in view
|
|
|
|
|
2022-04-20 15:43:47 +00:00
|
|
|
# data transform: convert source data to a format
|
|
|
|
# expected to be incrementally updates and later rendered
|
|
|
|
# to a more graphics native format.
|
|
|
|
if self.data_t:
|
|
|
|
array = self.data_t(array)
|
|
|
|
|
|
|
|
# maybe allocate shm for data transform output
|
|
|
|
# if self.data_t_shm is None:
|
|
|
|
# fshm = self.flow.shm
|
|
|
|
|
|
|
|
# shm, opened = maybe_open_shm_array(
|
|
|
|
# f'{self.flow.name}_data_t',
|
|
|
|
# # TODO: create entry for each time frame
|
|
|
|
# dtype=array.dtype,
|
|
|
|
# readonly=False,
|
|
|
|
# )
|
|
|
|
# assert opened
|
|
|
|
# shm.push(array)
|
|
|
|
# self.data_t_shm = shm
|
2022-04-16 19:22:11 +00:00
|
|
|
|
|
|
|
elif self.path:
|
|
|
|
print(f'inremental update not supported yet {self.flow.name}')
|
|
|
|
# TODO: do incremental update
|
|
|
|
# prepend, append, last = self.diff(self.flow.read())
|
|
|
|
|
|
|
|
# do path generation for each segment
|
|
|
|
# and then push into graphics object.
|
|
|
|
|
2022-04-20 15:43:47 +00:00
|
|
|
hist, last = array[:-1], array[-1]
|
|
|
|
|
|
|
|
# call path render func on history
|
|
|
|
self.path = self.draw_path(hist)
|
|
|
|
|
|
|
|
self.last_read = new_read
|
2022-04-16 19:22:11 +00:00
|
|
|
return self.path, last
|