Compare commits

..

10 Commits

Author SHA1 Message Date
Tyler Goodlet cd79eabe61 Bring back down default bars frame count for ib 2022-04-07 14:21:11 -04:00
Tyler Goodlet 1d7adad2cc Add `ShmArray.last()` docstr 2022-04-07 14:20:40 -04:00
Tyler Goodlet f52367c19d Tweak log-scaler for more detail 2022-04-07 14:20:19 -04:00
Tyler Goodlet ba6ecbbba2 Drop `._ic` debugging prints 2022-04-07 14:15:18 -04:00
Tyler Goodlet 98f4364cd1 Display loop mega-cleanup
The most important changes include:
- iterating the new `Flow` type and updating graphics
- adding detailed profiling
- increasing the min uppx before graphics updates are throttled
- including the L1 spread in y-range calcs so that you never have the
  bid/ask go "out of view"..
- pass around `Flow`s instead of shms
- drop all the old prototyped downsampling code
2022-04-07 14:11:01 -04:00
Tyler Goodlet 64e77fd72a Allow passing a `plotItem` to `.draw_curve()`
If manually managing an overlay you'll likely call `.overlay_plotitem()`
and then a plotting method so we need to accept a plot item input so
that the chart's pi doesn't get assigned incorrectly in the `Flow` entry
(though it is by default if no input is provided).

More,
- add a `Flow.graphics` field and set it to the `pg.GraphicsObject`.
- make `Flow.maxmin()` return `None` in the "can't calculate" cases.
2022-04-07 11:13:02 -04:00
Tyler Goodlet 8701b2e9ca Fsp UI initialization updates
- set shm refs on `Flow` entries.
- don't run a graphics cycle on 'update' msgs from the engine
  if the containing chart is hidden.
- drop `volume` from flows map and disable auto-yranging
  once $vlm comes up.
2022-04-07 11:04:47 -04:00
Tyler Goodlet 23621131bb Disconnect signals in `ChartView.disable_auto_yrange()`
Allows for removing resize callbacks for a flow/overlay that you wish to
remove from view (eg. unit volume after dollar volume is up) and thus
less general interaction callback overhead for any plot you don't wish
to show or resize.

Further,
- drop the `autoscale_linked_plots` block for now since with
  multi-view-box overlays each register their own vb resize slots
- pull the graphics object from the chart's `Flow` map inside
  `.maybe_downsample_graphics()`
2022-04-07 10:58:09 -04:00
Tyler Goodlet d2c40d887c Fix bug where if `yrange` was passed the mxmin callback was still used.. 2022-04-06 17:11:15 -04:00
Tyler Goodlet ba78d563b8 Guard against zero px width 2022-04-06 17:10:19 -04:00
8 changed files with 276 additions and 269 deletions

View File

@ -1592,7 +1592,7 @@ async def backfill_bars(
# on that until we have the `marketstore` daemon in place in which # on that until we have the `marketstore` daemon in place in which
# case the shm size will be driven by user config and available sys # case the shm size will be driven by user config and available sys
# memory. # memory.
count: int = 59, count: int = 22,
task_status: TaskStatus[trio.CancelScope] = trio.TASK_STATUS_IGNORED, task_status: TaskStatus[trio.CancelScope] = trio.TASK_STATUS_IGNORED,

View File

@ -240,7 +240,13 @@ class ShmArray:
def last( def last(
self, self,
length: int = 1, length: int = 1,
) -> np.ndarray: ) -> np.ndarray:
'''
Return the last ``length``'s worth of ("row") entries from the
array.
'''
return self.array[-length:] return self.array[-length:]
def push( def push(

View File

@ -658,102 +658,6 @@ class LinkedSplits(QWidget):
cpw.sidepane.setMaximumWidth(sp_w) cpw.sidepane.setMaximumWidth(sp_w)
# class FlowsTable(pydantic.BaseModel):
# '''
# 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] = {}
class Flow(msgspec.Struct): # , frozen=True):
'''
(FinancialSignal-)Flow compound type which wraps a real-time
graphics (curve) and its backing data stream together for high level
access and control.
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
is_ohlc: bool = False
# 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?
_shm: Optional[ShmArray] = None # currently, may be filled in "later"
# 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
@shm.setter
def shm(self, shm: ShmArray) -> ShmArray:
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)
result = self._mxmns.get(rkey)
if result:
return result
shm = self.shm
if shm is None:
# print(f'no shm {self.name}?')
return 0, 0
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:
# print(f'no data in view {self.name}?')
return 0, 0
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)
# else:
# ylow, yhigh = 0, 0
result = ylow, yhigh
if result != (0, 0):
self._mxmns[rkey] = result
if self.name == 'drk_vlm':
print(f'{self.name} mxmn @ {rkey} -> {result}')
return result
class ChartPlotWidget(pg.PlotWidget): class ChartPlotWidget(pg.PlotWidget):
''' '''
``GraphicsView`` subtype containing a single ``PlotItem``. ``GraphicsView`` subtype containing a single ``PlotItem``.
@ -1086,6 +990,7 @@ class ChartPlotWidget(pg.PlotWidget):
name=name, name=name,
plot=self.plotItem, plot=self.plotItem,
is_ohlc=True, is_ohlc=True,
graphics=graphics,
) )
self._add_sticky(name, bg_color='davies') self._add_sticky(name, bg_color='davies')
@ -1159,6 +1064,7 @@ class ChartPlotWidget(pg.PlotWidget):
overlay: bool = False, overlay: bool = False,
color: Optional[str] = None, color: Optional[str] = None,
add_label: bool = True, add_label: bool = True,
pi: Optional[pg.PlotItem] = None,
**pdi_kwargs, **pdi_kwargs,
@ -1203,12 +1109,13 @@ class ChartPlotWidget(pg.PlotWidget):
self._graphics[name] = curve self._graphics[name] = curve
self._arrays[data_key] = data self._arrays[data_key] = data
pi = self.plotItem pi = pi or self.plotItem
self._flows[data_key] = Flow( self._flows[data_key] = Flow(
name=name, name=name,
plot=pi, plot=pi,
is_ohlc=False, is_ohlc=False,
graphics=curve,
) )
# TODO: this probably needs its own method? # TODO: this probably needs its own method?
@ -1428,6 +1335,7 @@ class ChartPlotWidget(pg.PlotWidget):
''' '''
profiler = pg.debug.Profiler( profiler = pg.debug.Profiler(
msg=f'`{str(self)}.maxmin()` loop cycle for: `{self.name}`',
disabled=not pg_profile_enabled(), disabled=not pg_profile_enabled(),
gt=ms_slower_then, gt=ms_slower_then,
delayed=True, delayed=True,
@ -1444,16 +1352,113 @@ class ChartPlotWidget(pg.PlotWidget):
if ( if (
flow is None flow is None
): ):
print(f"flow {flow_key} doesn't exist in chart {self.name}") log.error(f"flow {flow_key} doesn't exist in chart {self.name} !?")
return 0, 0 res = 0, 0
else: else:
key = round(lbar), round(rbar) key = round(lbar), round(rbar)
res = flow.maxmin(*key) res = flow.maxmin(*key)
profiler(f'{key} max-min {res}') profiler(f'yrange mxmn: {key} -> {res}')
if res == (0, 0): if res == (None, None):
log.error( log.error(
f"{flow_key} -> (0, 0) for bars_range = {key}" f"{flow_key} no mxmn for bars_range => {key} !?"
) )
res = 0, 0
return res return res
# class FlowsTable(pydantic.BaseModel):
# '''
# 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] = {}
class Flow(msgspec.Struct): # , frozen=True):
'''
(FinancialSignal-)Flow compound type which wraps a real-time
graphics (curve) and its backing data stream together for high level
access and control.
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
is_ohlc: bool = False
graphics: pg.GraphicsObject
# 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?
_shm: Optional[ShmArray] = None # currently, may be filled in "later"
# 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
@shm.setter
def shm(self, shm: ShmArray) -> ShmArray:
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

View File

@ -260,12 +260,13 @@ def ds_m4(
if log_scale: if log_scale:
assert uppx, 'You must provide a `uppx` value to use log scaling!' assert uppx, 'You must provide a `uppx` value to use log scaling!'
# scaler = 2**7 / (1 + math.log(uppx, 2))
scaler = round( scaler = round(
max( max(
# NOTE: found that a 16x px width brought greater # NOTE: found that a 16x px width brought greater
# detail, likely due to dpi scaling? # detail, likely due to dpi scaling?
# px_width=px_width * 16, # px_width=px_width * 16,
2**6 / (1 + math.log(uppx, 2)), 2**7 / (1 + math.log(uppx, 2)),
1 1
) )
) )

View File

@ -408,7 +408,7 @@ class FastAppendCurve(pg.GraphicsObject):
self._in_ds = False self._in_ds = False
elif should_ds: elif should_ds and px_width:
x_out, y_out = self.downsample( x_out, y_out = self.downsample(
x_out, x_out,
y_out, y_out,

View File

@ -30,7 +30,6 @@ import numpy as np
import tractor import tractor
import trio import trio
import pyqtgraph as pg import pyqtgraph as pg
from PyQt5.QtCore import QLineF
from .. import brokers from .. import brokers
from ..data.feed import open_feed from ..data.feed import open_feed
@ -73,13 +72,20 @@ _tick_groups = {
} }
# TODO: delegate this to each `Flow.maxmin()` which includes
# caching and further we should implement the following stream based
# approach, likely with ``numba``:
# https://arxiv.org/abs/cs/0610046
# https://github.com/lemire/pythonmaxmin
def chart_maxmin( def chart_maxmin(
chart: ChartPlotWidget, chart: ChartPlotWidget,
ohlcv_shm: ShmArray, ohlcv_shm: ShmArray,
vlm_chart: Optional[ChartPlotWidget] = None, vlm_chart: Optional[ChartPlotWidget] = None,
) -> tuple[ ) -> tuple[
tuple[int, int, int, int], tuple[int, int, int, int],
float, float,
float, float,
float, float,
@ -88,11 +94,6 @@ def chart_maxmin(
Compute max and min datums "in view" for range limits. Compute max and min datums "in view" for range limits.
''' '''
# TODO: implement this
# https://arxiv.org/abs/cs/0610046
# https://github.com/lemire/pythonmaxmin
# array = chart._arrays[chart.name]
array = ohlcv_shm.array array = ohlcv_shm.array
ifirst = array[0]['index'] ifirst = array[0]['index']
@ -105,18 +106,23 @@ def chart_maxmin(
chart.default_view() chart.default_view()
return (last_bars_range, 0, 0, 0) return (last_bars_range, 0, 0, 0)
mx, mn = np.nanmax(in_view['high']), np.nanmin(in_view['low']) mx, mn = (
np.nanmax(in_view['high']),
# TODO: when we start using line charts, probably want to make np.nanmin(in_view['low'],)
# this an overloaded call on our `DataView )
# sym = chart.name
# mx, mn = np.nanmax(in_view[sym]), np.nanmin(in_view[sym])
mx_vlm_in_view = 0 mx_vlm_in_view = 0
if vlm_chart: if vlm_chart:
mx_vlm_in_view = np.max(in_view['volume']) mx_vlm_in_view = np.max(
in_view['volume']
)
return last_bars_range, mx, max(mn, 0), mx_vlm_in_view return (
last_bars_range,
mx,
max(mn, 0), # presuming price can't be negative?
mx_vlm_in_view,
)
@dataclass @dataclass
@ -272,8 +278,9 @@ async def graphics_update_loop(
chart.default_view() chart.default_view()
# main loop # main real-time quotes update loop
async for quotes in stream: async for quotes in stream:
ds.quotes = quotes ds.quotes = quotes
quote_period = time.time() - last_quote quote_period = time.time() - last_quote
quote_rate = round( quote_rate = round(
@ -310,28 +317,50 @@ def graphics_update_cycle(
wap_in_history: bool = False, wap_in_history: bool = False,
) -> None: ) -> None:
# TODO: eventually optimize this whole graphics stack with ``numba`` # TODO: eventually optimize this whole graphics stack with ``numba``
# hopefully XD # hopefully XD
chart = ds.chart
profiler = pg.debug.Profiler( profiler = pg.debug.Profiler(
msg=f'Graphics loop cycle for: `{chart.name}`',
disabled=True, # not pg_profile_enabled(), disabled=True, # not pg_profile_enabled(),
gt=1/12 * 1e3, gt=1/12 * 1e3,
# gt=ms_slower_then,
) )
# unpack multi-referenced components # unpack multi-referenced components
chart = ds.chart
vlm_chart = ds.vlm_chart vlm_chart = ds.vlm_chart
l1 = ds.l1 l1 = ds.l1
ohlcv = ds.ohlcv ohlcv = ds.ohlcv
array = ohlcv.array array = ohlcv.array
vars = ds.vars vars = ds.vars
tick_margin = vars['tick_margin'] tick_margin = vars['tick_margin']
update_uppx = 5 update_uppx = 6
for sym, quote in ds.quotes.items(): for sym, quote in ds.quotes.items():
# compute the first available graphic's x-units-per-pixel
xpx = vlm_chart.view.x_uppx()
# NOTE: vlm may be written by the ``brokerd`` backend
# event though a tick sample is not emitted.
# TODO: show dark trades differently
# https://github.com/pikers/piker/issues/116
# NOTE: this used to be implemented in a dedicated
# "increment task": ``check_for_new_bars()`` but it doesn't
# make sense to do a whole task switch when we can just do
# this simple index-diff and all the fsp sub-curve graphics
# are diffed on each draw cycle anyway; so updates to the
# "curve" length is already automatic.
# increment the view position by the sample offset.
i_step = ohlcv.index
i_diff = i_step - vars['i_last']
vars['i_last'] = i_step
( (
brange, brange,
mx_in_view, mx_in_view,
@ -344,83 +373,12 @@ def graphics_update_cycle(
mn = mn_in_view - tick_margin mn = mn_in_view - tick_margin
profiler('maxmin call') profiler('maxmin call')
# compute the first available graphic's x-units-per-pixel
xpx = vlm_chart.view.xs_in_px()
# print(f'vlm xpx {xpx}')
in_view = chart.in_view(ohlcv.array)
if lbar != rbar:
# view box width in pxs
w = chart.view.boundingRect().width()
# TODO: a better way to get this?
# i would guess the esiest way is to just
# get the ``.boundingRect()`` of the curve
# in view but maybe there's something smarter?
# Currently we're just mapping the rbar, lbar to
# pixels via:
cw = chart.view.mapViewToDevice(QLineF(lbar, 0, rbar, 0)).length()
# is this faster?
# cw = chart.mapFromView(QLineF(lbar, 0 , rbar, 0)).length()
profiler(
f'view width pxs: {w}\n'
f'curve width pxs: {cw}\n'
f'sliced in view: {in_view.size}'
)
# compress bars to m4 line(s) if uppx is high enough
# if in_view.size > cw:
# from ._compression import ds_m4, hl2mxmn
# mxmn, x = hl2mxmn(in_view)
# profiler('hl tracer')
# nb, x, y = ds_m4(
# x=x,
# y=mxmn,
# # TODO: this needs to actually be the width
# # in pixels of the visible curve since we don't
# # want to downsample any 'zeros' around the curve,
# # just the values that make up the curve graphic,
# # i think?
# px_width=cw,
# )
# profiler(
# 'm4 downsampled\n'
# f' ds bins: {nb}\n'
# f' x.shape: {x.shape}\n'
# f' y.shape: {y.shape}\n'
# f' x: {x}\n'
# f' y: {y}\n'
# )
# assert y.size == mxmn.size
# NOTE: vlm may be written by the ``brokerd`` backend
# event though a tick sample is not emitted.
# TODO: show dark trades differently
# https://github.com/pikers/piker/issues/116
# NOTE: this used to be implemented in a dedicated
# "increment tas": ``check_for_new_bars()`` but it doesn't
# make sense to do a whole task switch when we can just do
# this simple index-diff and all the fsp sub-curve graphics
# are diffed on each draw cycle anyway; so updates to the
# "curve" length is already automatic.
# increment the view position by the sample offset.
i_step = ohlcv.index
i_diff = i_step - vars['i_last']
vars['i_last'] = i_step
# don't real-time "shift" the curve to the # don't real-time "shift" the curve to the
# left under the following conditions: # left under the following conditions:
if ( if (
i_diff > 0 # no new sample step i_diff > 0 # no new sample step
and xpx < update_uppx # chart is zoomed out very far and xpx < update_uppx # chart is zoomed out very far
and r >= i_step # the last datum isn't in view and r >= i_step # the last datum is in view
): ):
# TODO: we should track and compute whether the last # TODO: we should track and compute whether the last
# pixel in a curve should show new data based on uppx # pixel in a curve should show new data based on uppx
@ -429,7 +387,9 @@ def graphics_update_cycle(
if vlm_chart: if vlm_chart:
# always update y-label # always update y-label
ds.vlm_sticky.update_from_data(*array[-1][['index', 'volume']]) ds.vlm_sticky.update_from_data(
*array[-1][['index', 'volume']]
)
if ( if (
# if zoomed out alot don't update the last "bar" # if zoomed out alot don't update the last "bar"
@ -460,17 +420,17 @@ def graphics_update_cycle(
mx_vlm_in_view != last_mx_vlm mx_vlm_in_view != last_mx_vlm
or mx_vlm_in_view > last_mx_vlm or mx_vlm_in_view > last_mx_vlm
): ):
# print(f'mx vlm: {last_mx_vlm} -> {mx_vlm_in_view}')
yrange = (0, mx_vlm_in_view * 1.375) yrange = (0, mx_vlm_in_view * 1.375)
vlm_chart.view._set_yrange( vlm_chart.view._set_yrange(
yrange=yrange, yrange=yrange,
) )
# print(f'mx vlm: {last_mx_vlm} -> {mx_vlm_in_view}')
vars['last_mx_vlm'] = mx_vlm_in_view vars['last_mx_vlm'] = mx_vlm_in_view
for curve_name, flow in vlm_chart._flows.items(): for curve_name, flow in vlm_chart._flows.items():
update_fsp_chart( update_fsp_chart(
vlm_chart, vlm_chart,
flow.shm, flow,
curve_name, curve_name,
array_key=curve_name, array_key=curve_name,
) )
@ -525,7 +485,6 @@ def graphics_update_cycle(
# current) tick first order as an optimization where we only # current) tick first order as an optimization where we only
# update from the last tick from each type class. # update from the last tick from each type class.
# last_clear_updated: bool = False # last_clear_updated: bool = False
# for typ, tick in reversed(lasts.items()):
# update ohlc sampled price bars # update ohlc sampled price bars
if ( if (
@ -537,7 +496,7 @@ def graphics_update_cycle(
array, array,
) )
# iterate in FIFO order per frame # iterate in FIFO order per tick-frame
for typ, tick in lasts.items(): for typ, tick in lasts.items():
price = tick.get('price') price = tick.get('price')
@ -608,42 +567,34 @@ def graphics_update_cycle(
if ( if (
(mx > vars['last_mx']) or (mn < vars['last_mn']) (mx > vars['last_mx']) or (mn < vars['last_mn'])
and not chart._static_yrange == 'axis' and not chart._static_yrange == 'axis'
and r > i_step # the last datum is in view
): ):
# print(f'new y range: {(mn, mx)}') main_vb = chart.view
chart.view._set_yrange( if (
yrange=(mn, mx), main_vb._ic is None
or not main_vb._ic.is_set()
):
main_vb._set_yrange(
# TODO: we should probably scale # TODO: we should probably scale
# the view margin based on the size # the view margin based on the size
# of the true range? This way you can # of the true range? This way you can
# slap in orders outside the current # slap in orders outside the current
# L1 (only) book range. # L1 (only) book range.
# range_margin=0.1, # range_margin=0.1,
yrange=(mn, mx),
) )
vars['last_mx'], vars['last_mn'] = mx, mn vars['last_mx'], vars['last_mn'] = mx, mn
# run synchronous update on all derived fsp subplots # run synchronous update on all linked flows
for name, subchart in ds.linked.subplots.items(): for curve_name, flow in chart._flows.items():
if name == 'volume': # TODO: should the "main" (aka source) flow be special?
if curve_name == chart.data_key:
continue continue
update_fsp_chart(
subchart,
subchart._shm,
# XXX: do we really needs seperate names here?
name,
array_key=name,
)
subchart.cv._set_yrange()
# TODO: all overlays on all subplots..
# run synchronous update on all derived overlays
for curve_name, flow in chart._flows.items():
update_fsp_chart( update_fsp_chart(
chart, chart,
flow.shm, flow,
curve_name, curve_name,
array_key=curve_name, array_key=curve_name,
) )
@ -737,6 +688,7 @@ async def display_symbol_data(
# TODO: a data view api that makes this less shit # TODO: a data view api that makes this less shit
chart._shm = ohlcv chart._shm = ohlcv
chart._flows[chart.data_key].shm = ohlcv
# NOTE: we must immediately tell Qt to show the OHLC chart # NOTE: we must immediately tell Qt to show the OHLC chart
# to avoid a race where the subplots get added/shown to # to avoid a race where the subplots get added/shown to
@ -793,7 +745,7 @@ async def display_symbol_data(
# that it isn't double rendered in the display loop # that it isn't double rendered in the display loop
# above since we do a maxmin calc on the volume data to # above since we do a maxmin calc on the volume data to
# determine if auto-range adjustements should be made. # determine if auto-range adjustements should be made.
linked.subplots.pop('volume', None) # linked.subplots.pop('volume', None)
# TODO: make this not so shit XD # TODO: make this not so shit XD
# close group status # close group status

View File

@ -72,12 +72,16 @@ def has_vlm(ohlcv: ShmArray) -> bool:
def update_fsp_chart( def update_fsp_chart(
chart: ChartPlotWidget, chart: ChartPlotWidget,
shm: ShmArray, flow,
graphics_name: str, graphics_name: str,
array_key: Optional[str], array_key: Optional[str],
) -> None: ) -> None:
shm = flow.shm
if not shm:
return
array = shm.array array = shm.array
last_row = try_read(array) last_row = try_read(array)
@ -271,6 +275,7 @@ async def run_fsp_ui(
# data looked up from the chart's internal array set. # data looked up from the chart's internal array set.
# TODO: we must get a data view api going STAT!! # TODO: we must get a data view api going STAT!!
chart._shm = shm chart._shm = shm
chart._flows[chart.data_key].shm = shm
# should **not** be the same sub-chart widget # should **not** be the same sub-chart widget
assert chart.name != linkedsplits.chart.name assert chart.name != linkedsplits.chart.name
@ -282,7 +287,7 @@ async def run_fsp_ui(
# first UI update, usually from shm pushed history # first UI update, usually from shm pushed history
update_fsp_chart( update_fsp_chart(
chart, chart,
shm, chart._flows[array_key],
name, name,
array_key=array_key, array_key=array_key,
) )
@ -441,6 +446,9 @@ class FspAdmin:
async with stream.subscribe() as stream: async with stream.subscribe() as stream:
async for msg in stream: async for msg in stream:
if msg == 'update': if msg == 'update':
# if the chart isn't hidden try to update
# the data on screen.
if not self.linked.isHidden():
log.info(f'Re-syncing graphics for fsp: {ns_path}') log.info(f'Re-syncing graphics for fsp: {ns_path}')
self.linked.graphics_cycle() self.linked.graphics_cycle()
else: else:
@ -631,6 +639,7 @@ async def open_vlm_displays(
# the curve item internals are pretty convoluted. # the curve item internals are pretty convoluted.
style='step', style='step',
) )
chart._flows['volume'].shm = ohlcv
# force 0 to always be in view # force 0 to always be in view
def maxmin( def maxmin(
@ -794,13 +803,16 @@ async def open_vlm_displays(
color=color, color=color,
step_mode=step_mode, step_mode=step_mode,
style=style, style=style,
pi=pi,
) )
# TODO: we need a better API to do this.. # TODO: we need a better API to do this..
# specially store ref to shm for lookup in display loop # specially store ref to shm for lookup in display loop
# since only a placeholder of `None` is entered in # since only a placeholder of `None` is entered in
# ``.draw_curve()``. # ``.draw_curve()``.
chart._flows[name].shm = shm flow = chart._flows[name]
assert flow.plot is pi
flow.shm = shm
chart_curves( chart_curves(
fields, fields,
@ -835,6 +847,9 @@ async def open_vlm_displays(
# liquidity events (well at least on low OHLC periods - 1s). # liquidity events (well at least on low OHLC periods - 1s).
vlm_curve.hide() vlm_curve.hide()
chart.removeItem(vlm_curve) chart.removeItem(vlm_curve)
chart._flows.pop('volume')
# avoid range sorting on volume once disabled
chart.view.disable_auto_yrange()
# Trade rate overlay # Trade rate overlay
# XXX: requires an additional overlay for # XXX: requires an additional overlay for
@ -875,7 +890,10 @@ async def open_vlm_displays(
style='dash', style='dash',
) )
for pi in (dvlm_pi, tr_pi): for pi in (
dvlm_pi,
tr_pi,
):
for name, axis_info in pi.axes.items(): for name, axis_info in pi.axes.items():
# lol this sux XD # lol this sux XD
axis = axis_info['item'] axis = axis_info['item']

View File

@ -393,6 +393,11 @@ class ChartView(ViewBox):
def start_ic( def start_ic(
self, self,
) -> None: ) -> None:
'''
Signal the beginning of a click-drag interaction
to any interested task waiters.
'''
if self._ic is None: if self._ic is None:
self.chart.pause_all_feeds() self.chart.pause_all_feeds()
self._ic = trio.Event() self._ic = trio.Event()
@ -400,13 +405,13 @@ class ChartView(ViewBox):
def signal_ic( def signal_ic(
self, self,
*args, *args,
# ev = None,
) -> None:
if args:
print(f'range change dun: {args}')
else:
print('proxy called')
) -> None:
'''
Signal the end of a click-drag interaction
to any waiters.
'''
if self._ic: if self._ic:
self._ic.set() self._ic.set()
self._ic = None self._ic = None
@ -674,7 +679,6 @@ class ChartView(ViewBox):
self.sigRangeChangedManually.emit(self.state['mouseEnabled']) self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
if ev.isFinish(): if ev.isFinish():
print('DRAG FINISH')
self.signal_ic() self.signal_ic()
# self._ic.set() # self._ic.set()
# self._ic = None # self._ic = None
@ -780,26 +784,31 @@ class ChartView(ViewBox):
# TODO: maybe should be a method on the # TODO: maybe should be a method on the
# chart widget/item? # chart widget/item?
# if False: # if False:
if autoscale_linked_plots: # if autoscale_linked_plots:
# avoid recursion by sibling plots # # avoid recursion by sibling plots
linked = self.linkedsplits # linked = self.linkedsplits
plots = list(linked.subplots.copy().values()) # plots = list(linked.subplots.copy().values())
main = linked.chart # main = linked.chart
if main: # if main:
plots.append(main) # plots.append(main)
for chart in plots: # for chart in plots:
if chart and not chart._static_yrange: # if chart and not chart._static_yrange:
chart.cv._set_yrange( # chart.cv._set_yrange(
bars_range=br, # bars_range=br,
autoscale_linked_plots=False, # autoscale_linked_plots=False,
) # )
profiler('autoscaled linked plots') # profiler('autoscaled linked plots')
if set_range: if set_range:
if not yrange:
# XXX: only compute the mxmn range
# if none is provided as input!
yrange = self._maxmin() yrange = self._maxmin()
if yrange is None: if yrange is None:
log.warning(f'No yrange provided for {self.name}!?')
return return
ylow, yhigh = yrange ylow, yhigh = yrange
@ -834,8 +843,17 @@ class ChartView(ViewBox):
if src_vb is None: if src_vb is None:
src_vb = self src_vb = self
# such that when a linked chart changes its range
# this local view is also automatically changed and
# resized to data.
src_vb.sigXRangeChanged.connect(self._set_yrange) src_vb.sigXRangeChanged.connect(self._set_yrange)
# splitter(s) resizing
src_vb.sigResized.connect(self._set_yrange)
# mouse wheel doesn't emit XRangeChanged
src_vb.sigRangeChangedManually.connect(self._set_yrange)
# TODO: a smarter way to avoid calling this needlessly? # TODO: a smarter way to avoid calling this needlessly?
# 2 things i can think of: # 2 things i can think of:
# - register downsample-able graphics specially and only # - register downsample-able graphics specially and only
@ -846,17 +864,24 @@ class ChartView(ViewBox):
self.maybe_downsample_graphics self.maybe_downsample_graphics
) )
# mouse wheel doesn't emit XRangeChanged
src_vb.sigRangeChangedManually.connect(self._set_yrange)
# splitter(s) resizing
src_vb.sigResized.connect(self._set_yrange)
def disable_auto_yrange( def disable_auto_yrange(
self, self,
) -> None: ) -> None:
self._chart._static_yrange = 'axis' # self._chart._static_yrange = 'axis'
self.sigXRangeChanged.disconnect(
self._set_yrange,
)
self.sigResized.disconnect(
self._set_yrange,
)
self.sigRangeChangedManually.disconnect(
self.maybe_downsample_graphics
)
self.sigRangeChangedManually.disconnect(
self._set_yrange,
)
def x_uppx(self) -> float: def x_uppx(self) -> float:
''' '''
@ -902,8 +927,8 @@ class ChartView(ViewBox):
linked = self.linkedsplits linked = self.linkedsplits
plots = linked.subplots | {chart.name: chart} plots = linked.subplots | {chart.name: chart}
for chart_name, chart in plots.items(): for chart_name, chart in plots.items():
for name, graphics in chart._graphics.items(): for name, flow in chart._flows.items():
# print(f'maybe ds chart:{name} graphic:{name}') graphics = flow.graphics
use_vr = False use_vr = False
if isinstance(graphics, BarItems): if isinstance(graphics, BarItems):