Make graphics-update-loop multi-sym aware B)
Initial support for real-time multi-symbol overlay charts using an aggregate feed delivered by `Feed.open_multi_stream()`. The setup steps for constructing the overlayed plot items is still very very rough and will likely provide incentive for better refactoring high level "charting APIs". For each fqsn passed into `display_symbol_data()` we now synchronously, - create a single call to `LinkedSplits.plot_ohlc_main() -> `ChartPlotWidget` where we cache the chart in scope and for all other "sibling" fqsns we, - make a call to `ChartPlotWidget.overlay_plotitem()` -> `PlotItem`, hide its axes, make another call with this plotitem input to `ChartPlotWidget.draw_curve()`, set a sym-specific view box auto-yrange maxmin callback, register the plotitem in a global `pis: dict[str, list[pgo.PlotItem, pgo.PlotItem]] = {}` Once all plots have been created we then asynchronously for each symbol, - maybe create a volume chart and register it in a similar task-global table: `vlms: dict[str, ChartPlotWidget] = {}` - start fsp displays for each symbol Then common entrypoints are entered once for all symbols: - a single `graphics_update_loop()` loop-task is started wherein real-time graphics update components for each symbol are created, * `L1Labels` * y-axis last clearing price stickies * `maxmin()` auto-ranger * `DisplayState` (stored in a table `dss: dict[str, DisplayState] = {}`) * an `increment_history_view()` task and a single call to `Feed.open_multi_stream()` is used to create a symbol-multiplexed quote stream which drives a single loop over all symbols wherein for each quote the appropriate components are looked up and passed to `graphics_update_cycle()`. - a single call to `open_order_mode()` is made with the first symbol provided as input, though eventually we want to support passing in the entire list. Further internal implementation details: - special tweaks to the `pg.LinearRegionItem` setup wherein the region is added with a zero opacity and *after* all plotitem overlays to avoid and issue where overlays weren't being shown within the region area in the history chart. - all symbol-specific graphics oriented update calls are adjusted to pass in the fqsn: * `update_fsp_chart()` * `ChartView._set_yrange()` * ChartPlotWidget.update_graphics_from_flow()` - avoid a double increment on sample step updates by not calling the increment on any vlm chart since it seems the vlm-ohlc chart linking already takes care of this now? - use global counters for the last epoch time step to avoid incrementing all views more then once per new time step given underlying shm array buffers may be on different array-index values from one another.multi_symbol_input
parent
be3dc69290
commit
7aec238f5f
|
@ -36,6 +36,9 @@ from ..data.feed import (
|
|||
Flume,
|
||||
)
|
||||
from ..data.types import Struct
|
||||
from ..data._sharedmem import (
|
||||
ShmArray,
|
||||
)
|
||||
from ._axes import YAxisLabel
|
||||
from ._chart import (
|
||||
ChartPlotWidget,
|
||||
|
@ -50,14 +53,12 @@ from ._fsp import (
|
|||
has_vlm,
|
||||
open_vlm_displays,
|
||||
)
|
||||
from ..data._sharedmem import (
|
||||
ShmArray,
|
||||
)
|
||||
from ..data._source import tf_in_1s
|
||||
from ._forms import (
|
||||
FieldsForm,
|
||||
mk_order_pane_layout,
|
||||
)
|
||||
from . import _pg_overrides as pgo
|
||||
# from ..data._source import tf_in_1s
|
||||
from .order_mode import (
|
||||
open_order_mode,
|
||||
OrderMode,
|
||||
|
@ -90,7 +91,8 @@ _tick_groups = {
|
|||
# https://github.com/lemire/pythonmaxmin
|
||||
def chart_maxmin(
|
||||
chart: ChartPlotWidget,
|
||||
ohlcv_shm: ShmArray,
|
||||
fqsn: str,
|
||||
# ohlcv_shm: ShmArray,
|
||||
vlm_chart: Optional[ChartPlotWidget] = None,
|
||||
|
||||
) -> tuple[
|
||||
|
@ -106,7 +108,7 @@ def chart_maxmin(
|
|||
|
||||
'''
|
||||
last_bars_range = chart.bars_range()
|
||||
out = chart.maxmin()
|
||||
out = chart.maxmin(name=fqsn)
|
||||
|
||||
if out is None:
|
||||
return (last_bars_range, 0, 0, 0)
|
||||
|
@ -131,6 +133,10 @@ def chart_maxmin(
|
|||
)
|
||||
|
||||
|
||||
_i_last: int = 0
|
||||
_i_last_append: int = 0
|
||||
|
||||
|
||||
class DisplayState(Struct):
|
||||
'''
|
||||
Chart-local real-time graphics state container.
|
||||
|
@ -140,6 +146,7 @@ class DisplayState(Struct):
|
|||
quotes: dict[str, Any]
|
||||
|
||||
maxmin: Callable
|
||||
flume: Flume
|
||||
ohlcv: ShmArray
|
||||
hist_ohlcv: ShmArray
|
||||
|
||||
|
@ -173,14 +180,18 @@ class DisplayState(Struct):
|
|||
|
||||
update_state: bool = True,
|
||||
update_uppx: float = 16,
|
||||
is_1m: bool = False,
|
||||
|
||||
) -> tuple:
|
||||
|
||||
shm = shm or self.ohlcv
|
||||
chart = chart or self.chart
|
||||
state = state or self.vars
|
||||
# state = state or self.vars
|
||||
|
||||
if not update_state:
|
||||
if (
|
||||
not update_state
|
||||
and state
|
||||
):
|
||||
state = state.copy()
|
||||
|
||||
# compute the first available graphic's x-units-per-pixel
|
||||
|
@ -194,26 +205,56 @@ class DisplayState(Struct):
|
|||
# "curve" length is already automatic.
|
||||
|
||||
# increment the view position by the sample offset.
|
||||
i_step = shm.index
|
||||
i_diff = i_step - state['i_last']
|
||||
state['i_last'] = i_step
|
||||
# i_step = shm.index
|
||||
i_step = shm.array[-1]['time']
|
||||
# i_diff = i_step - state['i_last']
|
||||
# state['i_last'] = i_step
|
||||
global _i_last, _i_last_append
|
||||
i_diff = i_step - _i_last
|
||||
# update global state
|
||||
if (
|
||||
# state is None
|
||||
not is_1m
|
||||
and i_diff > 0
|
||||
):
|
||||
_i_last = i_step
|
||||
|
||||
append_diff = i_step - state['i_last_append']
|
||||
# append_diff = i_step - state['i_last_append']
|
||||
append_diff = i_step - _i_last_append
|
||||
|
||||
# real-time update necessary?
|
||||
_, _, _, r = chart.bars_range()
|
||||
liv = r >= shm.index
|
||||
|
||||
# update the "last datum" (aka extending the flow graphic with
|
||||
# new data) only if the number of unit steps is >= the number of
|
||||
# such unit steps per pixel (aka uppx). Iow, if the zoom level
|
||||
# is such that a datum(s) update to graphics wouldn't span
|
||||
# to a new pixel, we don't update yet.
|
||||
do_append = (append_diff >= uppx)
|
||||
if do_append:
|
||||
state['i_last_append'] = i_step
|
||||
do_append = (
|
||||
append_diff >= uppx
|
||||
and i_diff
|
||||
)
|
||||
if (
|
||||
do_append
|
||||
and not is_1m
|
||||
):
|
||||
_i_last_append = i_step
|
||||
# fqsn = self.flume.symbol.fqsn
|
||||
# print(
|
||||
# f'DOING APPEND => {fqsn}\n'
|
||||
# f'i_step:{i_step}\n'
|
||||
# f'i_diff:{i_diff}\n'
|
||||
# f'last:{_i_last}\n'
|
||||
# f'last_append:{_i_last_append}\n'
|
||||
# f'append_diff:{append_diff}\n'
|
||||
# f'r: {r}\n'
|
||||
# f'liv: {liv}\n'
|
||||
# f'uppx: {uppx}\n'
|
||||
# )
|
||||
|
||||
do_rt_update = uppx < update_uppx
|
||||
|
||||
_, _, _, r = chart.bars_range()
|
||||
liv = r >= i_step
|
||||
|
||||
# TODO: pack this into a struct
|
||||
return (
|
||||
uppx,
|
||||
|
@ -229,9 +270,10 @@ async def graphics_update_loop(
|
|||
|
||||
nurse: trio.Nursery,
|
||||
godwidget: GodWidget,
|
||||
flume: Flume,
|
||||
feed: Feed,
|
||||
pis: dict[str, list[pgo.PlotItem, pgo.PlotItem]] = {},
|
||||
wap_in_history: bool = False,
|
||||
vlm_chart: Optional[ChartPlotWidget] = None,
|
||||
vlm_charts: dict[str, ChartPlotWidget] = {},
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
@ -255,27 +297,34 @@ async def graphics_update_loop(
|
|||
|
||||
fast_chart = linked.chart
|
||||
hist_chart = godwidget.hist_linked.chart
|
||||
assert hist_chart
|
||||
|
||||
dss: dict[str, DisplayState] = {}
|
||||
for fqsn, flume in feed.flumes.items():
|
||||
ohlcv = flume.rt_shm
|
||||
hist_ohlcv = flume.hist_shm
|
||||
symbol = flume.symbol
|
||||
fqsn = symbol.fqsn
|
||||
|
||||
# update last price sticky
|
||||
last_price_sticky = fast_chart.plotItem.getAxis(
|
||||
'right')._stickies.get(fast_chart.name)
|
||||
fast_pi = fast_chart._flows[fqsn].plot
|
||||
last_price_sticky = fast_pi.getAxis('right')._stickies[fqsn]
|
||||
last_price_sticky.update_from_data(
|
||||
*ohlcv.array[-1][['index', 'close']]
|
||||
)
|
||||
last_price_sticky.show()
|
||||
|
||||
hist_last_price_sticky = hist_chart.plotItem.getAxis(
|
||||
'right')._stickies.get(hist_chart.name)
|
||||
slow_pi = hist_chart._flows[fqsn].plot
|
||||
hist_last_price_sticky = slow_pi.getAxis('right')._stickies[fqsn]
|
||||
hist_last_price_sticky.update_from_data(
|
||||
*hist_ohlcv.array[-1][['index', 'close']]
|
||||
)
|
||||
|
||||
vlm_chart = vlm_charts[fqsn]
|
||||
maxmin = partial(
|
||||
chart_maxmin,
|
||||
fast_chart,
|
||||
ohlcv,
|
||||
fqsn,
|
||||
vlm_chart,
|
||||
)
|
||||
last_bars_range: tuple[float, float]
|
||||
|
@ -288,15 +337,18 @@ async def graphics_update_loop(
|
|||
|
||||
last, volume = ohlcv.array[-1][['close', 'volume']]
|
||||
|
||||
symbol = fast_chart.linked.symbol
|
||||
symbol = flume.symbol
|
||||
|
||||
l1 = L1Labels(
|
||||
fast_chart.plotItem,
|
||||
fast_pi,
|
||||
# determine precision/decimal lengths
|
||||
digits=symbol.tick_size_digits,
|
||||
size_digits=symbol.lot_size_digits,
|
||||
)
|
||||
fast_chart._l1_labels = l1
|
||||
# TODO: this is just wrong now since we can have multiple L1-label
|
||||
# sets, so instead we should have the l1 associated with the
|
||||
# plotitem or y-axis likely?
|
||||
# fast_chart._l1_labels = l1
|
||||
|
||||
# TODO:
|
||||
# - in theory we should be able to read buffer data faster
|
||||
|
@ -306,17 +358,19 @@ async def graphics_update_loop(
|
|||
# levels this might be dark volume we need to
|
||||
# present differently -> likely dark vlm
|
||||
|
||||
tick_size = fast_chart.linked.symbol.tick_size
|
||||
tick_size = symbol.tick_size
|
||||
tick_margin = 3 * tick_size
|
||||
|
||||
fast_chart.show()
|
||||
last_quote = time.time()
|
||||
# global _i_last
|
||||
i_last = ohlcv.index
|
||||
|
||||
ds = linked.display_state = DisplayState(**{
|
||||
dss[fqsn] = ds = linked.display_state = DisplayState(**{
|
||||
'godwidget': godwidget,
|
||||
'quotes': {},
|
||||
'maxmin': maxmin,
|
||||
'flume': flume,
|
||||
'ohlcv': ohlcv,
|
||||
'hist_ohlcv': hist_ohlcv,
|
||||
'chart': fast_chart,
|
||||
|
@ -335,8 +389,8 @@ async def graphics_update_loop(
|
|||
})
|
||||
|
||||
if vlm_chart:
|
||||
vlm_sticky = vlm_chart.plotItem.getAxis(
|
||||
'right')._stickies.get('volume')
|
||||
vlm_pi = vlm_chart._flows['volume'].plot
|
||||
vlm_sticky = vlm_pi.getAxis('right')._stickies['volume']
|
||||
ds.vlm_chart = vlm_chart
|
||||
ds.vlm_sticky = vlm_sticky
|
||||
|
||||
|
@ -379,6 +433,7 @@ async def graphics_update_loop(
|
|||
chart=hist_chart,
|
||||
shm=ds.hist_ohlcv,
|
||||
state=state,
|
||||
is_1m=True,
|
||||
# update_state=False,
|
||||
)
|
||||
# print(
|
||||
|
@ -391,16 +446,20 @@ async def graphics_update_loop(
|
|||
do_append
|
||||
and liv
|
||||
):
|
||||
hist_chart.increment_view(steps=i_diff)
|
||||
hist_chart.view._set_yrange(yrange=hist_chart.maxmin())
|
||||
# hist_chart.increment_view(steps=i_diff)
|
||||
flow = hist_chart._flows[fqsn]
|
||||
flow.plot.vb._set_yrange(
|
||||
# yrange=hist_chart.maxmin(name=fqsn)
|
||||
)
|
||||
# hist_chart.view._set_yrange(yrange=hist_chart.maxmin())
|
||||
|
||||
nurse.start_soon(increment_history_view)
|
||||
|
||||
# main real-time quotes update loop
|
||||
stream: tractor.MsgStream = flume.stream
|
||||
stream: tractor.MsgStream
|
||||
async with feed.open_multi_stream() as stream:
|
||||
assert stream
|
||||
async for quotes in stream:
|
||||
|
||||
ds.quotes = quotes
|
||||
quote_period = time.time() - last_quote
|
||||
quote_rate = round(
|
||||
1/quote_period, 1) if quote_period > 0 else float('inf')
|
||||
|
@ -416,11 +475,22 @@ async def graphics_update_loop(
|
|||
|
||||
last_quote = time.time()
|
||||
|
||||
# chart isn't active/shown so skip render cycle and pause feed(s)
|
||||
if fast_chart.linked.isHidden():
|
||||
# print('skipping update')
|
||||
fast_chart.pause_all_feeds()
|
||||
for sym, quote in quotes.items():
|
||||
ds = dss[sym]
|
||||
ds.quotes = quote
|
||||
|
||||
rt_pi, hist_pi = pis[sym]
|
||||
|
||||
# chart isn't active/shown so skip render cycle and
|
||||
# pause feed(s)
|
||||
if (
|
||||
fast_chart.linked.isHidden()
|
||||
or not rt_pi.isVisible()
|
||||
):
|
||||
print(f'{fqsn} skipping update -> CHART HIDDEN')
|
||||
continue
|
||||
# if fast_chart.linked.isHidden():
|
||||
# fast_chart.pause_all_feeds()
|
||||
|
||||
# ic = fast_chart.view._ic
|
||||
# if ic:
|
||||
|
@ -429,11 +499,15 @@ async def graphics_update_loop(
|
|||
# fast_chart.resume_all_feeds()
|
||||
|
||||
# sync call to update all graphics/UX components.
|
||||
graphics_update_cycle(ds)
|
||||
graphics_update_cycle(
|
||||
ds,
|
||||
quote,
|
||||
)
|
||||
|
||||
|
||||
def graphics_update_cycle(
|
||||
ds: DisplayState,
|
||||
quote: dict,
|
||||
wap_in_history: bool = False,
|
||||
trigger_all: bool = False, # flag used by prepend history updates
|
||||
prepend_update_index: Optional[int] = None,
|
||||
|
@ -445,6 +519,12 @@ def graphics_update_cycle(
|
|||
chart = ds.chart
|
||||
# TODO: just pass this as a direct ref to avoid so many attr accesses?
|
||||
hist_chart = ds.godwidget.hist_linked.chart
|
||||
assert hist_chart
|
||||
|
||||
flume = ds.flume
|
||||
sym = flume.symbol
|
||||
fqsn = sym.fqsn
|
||||
main_flow = chart._flows[fqsn]
|
||||
|
||||
profiler = Profiler(
|
||||
msg=f'Graphics loop cycle for: `{chart.name}`',
|
||||
|
@ -461,13 +541,13 @@ def graphics_update_cycle(
|
|||
|
||||
# rt "HFT" chart
|
||||
l1 = ds.l1
|
||||
ohlcv = ds.ohlcv
|
||||
# ohlcv = ds.ohlcv
|
||||
ohlcv = flume.rt_shm
|
||||
array = ohlcv.array
|
||||
|
||||
vars = ds.vars
|
||||
tick_margin = vars['tick_margin']
|
||||
|
||||
for sym, quote in ds.quotes.items():
|
||||
(
|
||||
uppx,
|
||||
liv,
|
||||
|
@ -477,6 +557,41 @@ def graphics_update_cycle(
|
|||
do_rt_update,
|
||||
) = ds.incr_info()
|
||||
|
||||
# don't real-time "shift" the curve to the
|
||||
# left unless we get one of the following:
|
||||
if (
|
||||
(
|
||||
do_append
|
||||
and liv
|
||||
)
|
||||
or trigger_all
|
||||
):
|
||||
# print(f'INCREMENTING {fqsn}')
|
||||
chart.increment_view(steps=i_diff)
|
||||
main_flow.plot.vb._set_yrange(
|
||||
# yrange=(mn, mx),
|
||||
)
|
||||
|
||||
# NOTE: since vlm and ohlc charts are axis linked now we don't
|
||||
# need the double increment request?
|
||||
# if vlm_chart:
|
||||
# vlm_chart.increment_view(steps=i_diff)
|
||||
|
||||
profiler('view incremented')
|
||||
|
||||
ticks_frame = quote.get('ticks', ())
|
||||
|
||||
frames_by_type: dict[str, dict] = {}
|
||||
lasts = {}
|
||||
|
||||
# build tick-type "frames" of tick sequences since
|
||||
# likely the tick arrival rate is higher then our
|
||||
# (throttled) quote stream rate.
|
||||
|
||||
# iterate in FIFO order per tick-frame
|
||||
# if sym != fqsn:
|
||||
# continue
|
||||
|
||||
# TODO: we should only run mxmn when we know
|
||||
# an update is due via ``do_append`` above.
|
||||
(
|
||||
|
@ -488,7 +603,6 @@ def graphics_update_cycle(
|
|||
l, lbar, rbar, r = brange
|
||||
mx = mx_in_view + tick_margin
|
||||
mn = mn_in_view - tick_margin
|
||||
|
||||
profiler('`ds.maxmin()` call')
|
||||
|
||||
if (
|
||||
|
@ -502,35 +616,13 @@ def graphics_update_cycle(
|
|||
log.debug('Skipping prepend graphics cycle: frame not in view')
|
||||
return
|
||||
|
||||
# don't real-time "shift" the curve to the
|
||||
# left unless we get one of the following:
|
||||
if (
|
||||
(do_append and liv)
|
||||
or trigger_all
|
||||
):
|
||||
chart.increment_view(steps=i_diff)
|
||||
chart.view._set_yrange(yrange=(mn, mx))
|
||||
|
||||
if vlm_chart:
|
||||
vlm_chart.increment_view(steps=i_diff)
|
||||
|
||||
profiler('view incremented')
|
||||
|
||||
ticks_frame = quote.get('ticks', ())
|
||||
|
||||
frames_by_type: dict[str, dict] = {}
|
||||
lasts = {}
|
||||
|
||||
# build tick-type "frames" of tick sequences since
|
||||
# likely the tick arrival rate is higher then our
|
||||
# (throttled) quote stream rate.
|
||||
for tick in ticks_frame:
|
||||
price = tick.get('price')
|
||||
ticktype = tick.get('type')
|
||||
|
||||
if ticktype == 'n/a' or price == -1:
|
||||
# okkk..
|
||||
continue
|
||||
# if ticktype == 'n/a' or price == -1:
|
||||
# # okkk..
|
||||
# continue
|
||||
|
||||
# keys are entered in olded-event-inserted-first order
|
||||
# since we iterate ``ticks_frame`` in standard order
|
||||
|
@ -545,9 +637,11 @@ def graphics_update_cycle(
|
|||
# frame_counts = {
|
||||
# typ: len(frame) for typ, frame in frames_by_type.items()
|
||||
# }
|
||||
# print(f'{pformat(frame_counts)}')
|
||||
# print(f'framed: {pformat(frames_by_type)}')
|
||||
# print(f'lasts: {pformat(lasts)}')
|
||||
# print(
|
||||
# f'{pformat(frame_counts)}\n'
|
||||
# f'framed: {pformat(frames_by_type)}\n'
|
||||
# f'lasts: {pformat(lasts)}\n'
|
||||
# )
|
||||
|
||||
# TODO: eventually we want to separate out the utrade (aka
|
||||
# dark vlm prices) here and show them as an additional
|
||||
|
@ -566,11 +660,15 @@ def graphics_update_cycle(
|
|||
or trigger_all
|
||||
):
|
||||
chart.update_graphics_from_flow(
|
||||
chart.name,
|
||||
fqsn,
|
||||
# chart.name,
|
||||
do_append=do_append,
|
||||
)
|
||||
main_flow.draw_last(array_key=fqsn)
|
||||
|
||||
hist_chart.update_graphics_from_flow(
|
||||
chart.name,
|
||||
fqsn,
|
||||
# chart.name,
|
||||
do_append=do_append,
|
||||
)
|
||||
|
||||
|
@ -578,7 +676,6 @@ def graphics_update_cycle(
|
|||
# since the current range should at least be updated
|
||||
# to it's max/min on the last pixel.
|
||||
|
||||
# iterate in FIFO order per tick-frame
|
||||
for typ, tick in lasts.items():
|
||||
|
||||
price = tick.get('price')
|
||||
|
@ -606,19 +703,13 @@ def graphics_update_cycle(
|
|||
# set.
|
||||
|
||||
# update price sticky(s)
|
||||
end = array[-1]
|
||||
ds.last_price_sticky.update_from_data(
|
||||
*end[['index', 'close']]
|
||||
)
|
||||
ds.hist_last_price_sticky.update_from_data(
|
||||
*end[['index', 'close']]
|
||||
)
|
||||
end_ic = array[-1][['index', 'close']]
|
||||
ds.last_price_sticky.update_from_data(*end_ic)
|
||||
ds.hist_last_price_sticky.update_from_data(*end_ic)
|
||||
|
||||
if wap_in_history:
|
||||
# update vwap overlay line
|
||||
chart.update_graphics_from_flow(
|
||||
'bar_wap',
|
||||
)
|
||||
chart.update_graphics_from_flow('bar_wap')
|
||||
|
||||
# L1 book label-line updates
|
||||
# XXX: is this correct for ib?
|
||||
|
@ -667,7 +758,8 @@ def graphics_update_cycle(
|
|||
liv
|
||||
and not chart._static_yrange == 'axis'
|
||||
):
|
||||
main_vb = chart.view
|
||||
# main_vb = chart.view
|
||||
main_vb = chart._flows[fqsn].plot.vb
|
||||
if (
|
||||
main_vb._ic is None
|
||||
or not main_vb._ic.is_set()
|
||||
|
@ -680,7 +772,7 @@ def graphics_update_cycle(
|
|||
# slap in orders outside the current
|
||||
# L1 (only) book range.
|
||||
# range_margin=0.1,
|
||||
yrange=(mn, mx),
|
||||
# yrange=(mn, mx),
|
||||
)
|
||||
|
||||
# check if slow chart needs a resize
|
||||
|
@ -695,9 +787,13 @@ def graphics_update_cycle(
|
|||
chart=hist_chart,
|
||||
shm=ds.hist_ohlcv,
|
||||
update_state=False,
|
||||
is_1m=True,
|
||||
)
|
||||
if hist_liv:
|
||||
hist_chart.view._set_yrange(yrange=hist_chart.maxmin())
|
||||
flow = hist_chart._flows[fqsn]
|
||||
flow.plot.vb._set_yrange(
|
||||
# yrange=hist_chart.maxmin(name=fqsn),
|
||||
)
|
||||
|
||||
# XXX: update this every draw cycle to make L1-always-in-view work.
|
||||
vars['last_mx'], vars['last_mn'] = mx, mn
|
||||
|
@ -706,7 +802,11 @@ def graphics_update_cycle(
|
|||
# TODO: should the "main" (aka source) flow be special?
|
||||
for curve_name, flow in chart._flows.items():
|
||||
# update any overlayed fsp flows
|
||||
if curve_name != chart.data_key:
|
||||
if (
|
||||
# curve_name != chart.data_key
|
||||
curve_name != fqsn
|
||||
and not flow.is_ohlc
|
||||
):
|
||||
update_fsp_chart(
|
||||
chart,
|
||||
flow,
|
||||
|
@ -719,9 +819,9 @@ def graphics_update_cycle(
|
|||
# px column to give the user the mx/mn
|
||||
# range of that set.
|
||||
if (
|
||||
not do_append
|
||||
liv
|
||||
# and not do_append
|
||||
# and not do_rt_update
|
||||
and liv
|
||||
):
|
||||
flow.draw_last(
|
||||
array_key=curve_name,
|
||||
|
@ -773,14 +873,18 @@ def graphics_update_cycle(
|
|||
# print(f'mx vlm: {last_mx_vlm} -> {mx_vlm_in_view}')
|
||||
vars['last_mx_vlm'] = mx_vlm_in_view
|
||||
|
||||
# update all downstream FSPs
|
||||
for curve_name, flow in vlm_chart._flows.items():
|
||||
|
||||
if (
|
||||
curve_name != 'volume' and
|
||||
flow.render and (
|
||||
liv and
|
||||
do_rt_update or do_append
|
||||
curve_name not in {'volume', fqsn}
|
||||
and flow.render
|
||||
and (
|
||||
liv and do_rt_update
|
||||
or do_append
|
||||
)
|
||||
# and not flow.is_ohlc
|
||||
# and curve_name != fqsn
|
||||
):
|
||||
update_fsp_chart(
|
||||
vlm_chart,
|
||||
|
@ -836,11 +940,12 @@ async def link_views_with_region(
|
|||
pen=pg.mkPen(hcolor('gunmetal')),
|
||||
brush=pg.mkBrush(hcolor('default_darkest')),
|
||||
)
|
||||
region.setZValue(10) # put linear region "in front" in layer terms
|
||||
|
||||
region.setOpacity(0)
|
||||
hist_pi.addItem(region, ignoreBounds=True)
|
||||
region.setOpacity(6/16)
|
||||
|
||||
flow = rt_chart._flows[hist_chart.name]
|
||||
flow = rt_chart._flows[flume.symbol.fqsn]
|
||||
assert flow
|
||||
|
||||
# XXX: no idea why this doesn't work but it's causing
|
||||
|
@ -948,6 +1053,32 @@ async def link_views_with_region(
|
|||
# region.sigRegionChangeFinished.connect(update_pi_from_region)
|
||||
|
||||
|
||||
# force 0 to always be in view
|
||||
def multi_maxmin(
|
||||
chart: ChartPlotWidget,
|
||||
names: list[str],
|
||||
|
||||
) -> tuple[float, float]:
|
||||
'''
|
||||
Flows "group" maxmin loop; assumes all named flows
|
||||
are in the same co-domain and thus can be sorted
|
||||
as one set.
|
||||
|
||||
Iterates all the named flows and calls the chart
|
||||
api to find their range values and return.
|
||||
|
||||
TODO: really we should probably have a more built-in API
|
||||
for this?
|
||||
|
||||
'''
|
||||
mx = 0
|
||||
for name in names:
|
||||
ymn, ymx = chart.maxmin(name=name)
|
||||
mx = max(mx, ymx)
|
||||
|
||||
return 0, mx
|
||||
|
||||
|
||||
async def display_symbol_data(
|
||||
godwidget: GodWidget,
|
||||
fqsns: list[str],
|
||||
|
@ -974,13 +1105,13 @@ async def display_symbol_data(
|
|||
# )
|
||||
|
||||
for fqsn in fqsns:
|
||||
|
||||
loading_sym_key = sbar.open_status(
|
||||
f'loading {fqsn} ->',
|
||||
group_key=True
|
||||
)
|
||||
|
||||
feed: Feed
|
||||
# assert len(fqsns) == 2
|
||||
async with open_feed(
|
||||
fqsns,
|
||||
loglevel=loglevel,
|
||||
|
@ -991,34 +1122,51 @@ async def display_symbol_data(
|
|||
|
||||
) as feed:
|
||||
|
||||
# TODO: right now we only show one symbol on charts, but
|
||||
# overlays are coming muy pronto guey..
|
||||
assert len(feed.flumes) == 1
|
||||
flume = list(feed.flumes.values())[0]
|
||||
# use expanded contract symbols passed back from feed layer.
|
||||
fqsns = list(feed.flumes.keys())
|
||||
|
||||
# step_size_s = 1
|
||||
# tf_key = tf_in_1s[step_size_s]
|
||||
godwidget.window.setWindowTitle(
|
||||
f'{fqsns} '
|
||||
# f'tick:{symbol.tick_size} '
|
||||
# f'step:{tf_key} '
|
||||
)
|
||||
# generate order mode side-pane UI
|
||||
# A ``FieldsForm`` form to configure order entry
|
||||
# and add as next-to-y-axis singleton pane
|
||||
pp_pane: FieldsForm = mk_order_pane_layout(godwidget)
|
||||
godwidget.pp_pane = pp_pane
|
||||
|
||||
# create top history view chart above the "main rt chart".
|
||||
rt_linked = godwidget.rt_linked
|
||||
hist_linked = godwidget.hist_linked
|
||||
|
||||
# NOTE: here we insert the slow-history chart set into
|
||||
# the fast chart's splitter -> so it's a splitter of charts
|
||||
# inside the first widget slot of a splitter of charts XD
|
||||
rt_linked.splitter.insertWidget(0, hist_linked)
|
||||
|
||||
rt_chart: None | ChartPlotWidget = None
|
||||
hist_chart: None | ChartPlotWidget = None
|
||||
|
||||
bg_chart_color = 'grayest'
|
||||
bg_last_bar_color = 'grayer'
|
||||
|
||||
pis: dict[str, list[pgo.PlotItem, pgo.PlotItem]] = {}
|
||||
|
||||
# load in ohlc data to a common linked but split chart set.
|
||||
for fqsn, flume in feed.flumes.items():
|
||||
rt_linked._symbol = flume.symbol
|
||||
hist_linked._symbol = flume.symbol
|
||||
|
||||
ohlcv: ShmArray = flume.rt_shm
|
||||
hist_ohlcv: ShmArray = flume.hist_shm
|
||||
|
||||
symbol = flume.symbol
|
||||
fqsn = symbol.fqsn
|
||||
brokername = symbol.brokers[0]
|
||||
|
||||
step_size_s = 1
|
||||
tf_key = tf_in_1s[step_size_s]
|
||||
|
||||
# load in symbol's ohlc data
|
||||
godwidget.window.setWindowTitle(
|
||||
f'{fqsn} '
|
||||
f'tick:{symbol.tick_size} '
|
||||
f'step:{tf_key} '
|
||||
)
|
||||
|
||||
rt_linked = godwidget.rt_linked
|
||||
rt_linked._symbol = symbol
|
||||
|
||||
# create top history view chart above the "main rt chart".
|
||||
hist_linked = godwidget.hist_linked
|
||||
hist_linked._symbol = symbol
|
||||
if hist_chart is None:
|
||||
hist_chart = hist_linked.plot_ohlc_main(
|
||||
symbol,
|
||||
hist_ohlcv,
|
||||
|
@ -1027,26 +1175,103 @@ async def display_symbol_data(
|
|||
# sidepane=False,
|
||||
sidepane=godwidget.search,
|
||||
)
|
||||
pis.setdefault(fqsn, [None, None])[1] = hist_chart.plotItem
|
||||
else:
|
||||
hist_pi = hist_chart.overlay_plotitem(
|
||||
name=fqsn,
|
||||
axis_title=fqsn,
|
||||
)
|
||||
hist_pi.hideAxis('left')
|
||||
hist_pi.hideAxis('bottom')
|
||||
|
||||
curve, _ = hist_chart.draw_curve(
|
||||
name=fqsn,
|
||||
shm=hist_ohlcv,
|
||||
array_key=fqsn,
|
||||
overlay=hist_pi,
|
||||
pi=hist_pi,
|
||||
is_ohlc=True,
|
||||
|
||||
color=bg_chart_color,
|
||||
last_bar_color=bg_last_bar_color,
|
||||
)
|
||||
|
||||
hist_pi.vb.maxmin = partial(
|
||||
hist_chart.maxmin,
|
||||
name=fqsn,
|
||||
)
|
||||
# TODO: we need a better API to do this..
|
||||
# specially store ref to shm for lookup in display loop
|
||||
# since only a placeholder of `None` is entered in
|
||||
# ``.draw_curve()``.
|
||||
flow = hist_chart._flows[fqsn]
|
||||
assert flow.plot is hist_pi
|
||||
|
||||
# group_mxmn = partial(
|
||||
# multi_maxmin,
|
||||
# names=fqsns,
|
||||
# chart=hist_chart,
|
||||
# )
|
||||
# hist_pi.vb._maxmin = group_mxmn
|
||||
pis.setdefault(fqsn, [None, None])[1] = hist_pi
|
||||
|
||||
# don't show when not focussed
|
||||
hist_linked.cursor.always_show_xlabel = False
|
||||
|
||||
# generate order mode side-pane UI
|
||||
# A ``FieldsForm`` form to configure order entry
|
||||
# and add as next-to-y-axis singleton pane
|
||||
pp_pane: FieldsForm = mk_order_pane_layout(godwidget)
|
||||
godwidget.pp_pane = pp_pane
|
||||
|
||||
# create main OHLC chart
|
||||
ohlc_chart = rt_linked.plot_ohlc_main(
|
||||
if rt_chart is None:
|
||||
rt_chart = rt_linked.plot_ohlc_main(
|
||||
symbol,
|
||||
ohlcv,
|
||||
# in the case of history chart we explicitly set `False`
|
||||
# to avoid internal pane creation.
|
||||
sidepane=pp_pane,
|
||||
)
|
||||
pis.setdefault(fqsn, [None, None])[0] = rt_chart.plotItem
|
||||
else:
|
||||
rt_pi = rt_chart.overlay_plotitem(
|
||||
name=fqsn,
|
||||
axis_title=fqsn,
|
||||
)
|
||||
|
||||
ohlc_chart._feeds[symbol.key] = feed
|
||||
ohlc_chart.setFocus()
|
||||
rt_pi.hideAxis('left')
|
||||
rt_pi.hideAxis('bottom')
|
||||
|
||||
curve, _ = rt_chart.draw_curve(
|
||||
name=fqsn,
|
||||
shm=ohlcv,
|
||||
array_key=fqsn,
|
||||
overlay=rt_pi,
|
||||
pi=rt_pi,
|
||||
is_ohlc=True,
|
||||
|
||||
color=bg_chart_color,
|
||||
last_bar_color=bg_last_bar_color,
|
||||
)
|
||||
rt_pi.vb.maxmin = partial(
|
||||
rt_chart.maxmin,
|
||||
name=fqsn,
|
||||
)
|
||||
# multi_maxmin,
|
||||
# names=fqsn
|
||||
|
||||
# TODO: we need a better API to do this..
|
||||
# specially store ref to shm for lookup in display loop
|
||||
# since only a placeholder of `None` is entered in
|
||||
# ``.draw_curve()``.
|
||||
flow = rt_chart._flows[fqsn]
|
||||
assert flow.plot is rt_pi
|
||||
|
||||
# group_mxmn = partial(
|
||||
# multi_maxmin,
|
||||
# names=fqsns,
|
||||
# chart=rt_chart,
|
||||
# )
|
||||
# rt_pi.vb._maxmin = group_mxmn
|
||||
pis.setdefault(fqsn, [None, None])[0] = rt_pi
|
||||
|
||||
rt_chart._feeds[symbol.key] = feed
|
||||
rt_chart.setFocus()
|
||||
|
||||
# XXX: FOR SOME REASON THIS IS CAUSING HANGZ!?!
|
||||
# plot historical vwap if available
|
||||
|
@ -1056,7 +1281,7 @@ async def display_symbol_data(
|
|||
# and 'bar_wap' in bars.dtype.fields
|
||||
# ):
|
||||
# wap_in_history = True
|
||||
# ohlc_chart.draw_curve(
|
||||
# rt_chart.draw_curve(
|
||||
# name='bar_wap',
|
||||
# shm=ohlcv,
|
||||
# color='default_light',
|
||||
|
@ -1070,28 +1295,31 @@ async def display_symbol_data(
|
|||
rt_linked.focus()
|
||||
await trio.sleep(0)
|
||||
|
||||
# NOTE: here we insert the slow-history chart set into
|
||||
# the fast chart's splitter -> so it's a splitter of charts
|
||||
# inside the first widget slot of a splitter of charts XD
|
||||
rt_linked.splitter.insertWidget(0, hist_linked)
|
||||
# XXX: if we wanted it at the bottom?
|
||||
# rt_linked.splitter.addWidget(hist_linked)
|
||||
rt_linked.focus()
|
||||
|
||||
godwidget.resize_all()
|
||||
|
||||
vlm_chart: Optional[ChartPlotWidget] = None
|
||||
vlms: dict[str, ChartPlotWidget] = {}
|
||||
async with trio.open_nursery() as ln:
|
||||
for fqsn, flume in feed.flumes.items():
|
||||
|
||||
vlm_chart: Optional[ChartPlotWidget] = None
|
||||
ohlcv: ShmArray = flume.rt_shm
|
||||
hist_ohlcv: ShmArray = flume.hist_shm
|
||||
|
||||
symbol = flume.symbol
|
||||
brokername = symbol.brokers[0]
|
||||
# if available load volume related built-in display(s)
|
||||
if (
|
||||
not symbol.broker_info[brokername].get('no_vlm', False)
|
||||
and has_vlm(ohlcv)
|
||||
):
|
||||
vlm_chart = await ln.start(
|
||||
vlms[fqsn] = vlm_chart = await ln.start(
|
||||
open_vlm_displays,
|
||||
rt_linked,
|
||||
ohlcv,
|
||||
flume,
|
||||
)
|
||||
|
||||
# load (user's) FSP set (otherwise known as "indicators")
|
||||
|
@ -1099,25 +1327,13 @@ async def display_symbol_data(
|
|||
ln.start_soon(
|
||||
start_fsp_displays,
|
||||
rt_linked,
|
||||
ohlcv,
|
||||
flume,
|
||||
loading_sym_key,
|
||||
loglevel,
|
||||
)
|
||||
|
||||
# start graphics update loop after receiving first live quote
|
||||
ln.start_soon(
|
||||
graphics_update_loop,
|
||||
ln,
|
||||
godwidget,
|
||||
flume,
|
||||
wap_in_history,
|
||||
vlm_chart,
|
||||
)
|
||||
|
||||
await trio.sleep(0)
|
||||
|
||||
# size view to data prior to order mode init
|
||||
ohlc_chart.default_view()
|
||||
rt_chart.default_view()
|
||||
rt_linked.graphics_cycle()
|
||||
await trio.sleep(0)
|
||||
|
||||
|
@ -1130,26 +1346,9 @@ async def display_symbol_data(
|
|||
|
||||
godwidget.resize_all()
|
||||
|
||||
await link_views_with_region(
|
||||
ohlc_chart,
|
||||
hist_chart,
|
||||
flume,
|
||||
)
|
||||
|
||||
mode: OrderMode
|
||||
async with (
|
||||
open_order_mode(
|
||||
feed,
|
||||
godwidget,
|
||||
fqsn,
|
||||
order_mode_started
|
||||
) as mode
|
||||
):
|
||||
if not vlm_chart:
|
||||
# trigger another view reset if no sub-chart
|
||||
ohlc_chart.default_view()
|
||||
|
||||
rt_linked.mode = mode
|
||||
rt_chart.default_view()
|
||||
|
||||
# let Qt run to render all widgets and make sure the
|
||||
# sidepanes line up vertically.
|
||||
|
@ -1168,7 +1367,7 @@ async def display_symbol_data(
|
|||
|
||||
# TODO: make this not so shit XD
|
||||
# close group status
|
||||
sbar._status_groups[loading_sym_key][1]()
|
||||
# sbar._status_groups[loading_sym_key][1]()
|
||||
|
||||
hist_linked.graphics_cycle()
|
||||
await trio.sleep(0)
|
||||
|
@ -1181,5 +1380,35 @@ async def display_symbol_data(
|
|||
)
|
||||
godwidget.resize_all()
|
||||
|
||||
await link_views_with_region(
|
||||
rt_chart,
|
||||
hist_chart,
|
||||
flume,
|
||||
)
|
||||
|
||||
# start graphics update loop after receiving first live quote
|
||||
ln.start_soon(
|
||||
graphics_update_loop,
|
||||
ln,
|
||||
godwidget,
|
||||
feed,
|
||||
pis,
|
||||
wap_in_history,
|
||||
vlms,
|
||||
)
|
||||
|
||||
await trio.sleep(0)
|
||||
|
||||
mode: OrderMode
|
||||
async with (
|
||||
open_order_mode(
|
||||
feed,
|
||||
godwidget,
|
||||
fqsns[-1],
|
||||
order_mode_started
|
||||
) as mode
|
||||
):
|
||||
rt_linked.mode = mode
|
||||
|
||||
# let the app run.. bby
|
||||
await trio.sleep_forever()
|
||||
|
|
Loading…
Reference in New Issue