commit
42d2f9e461
|
@ -306,7 +306,10 @@ class DynamicDateAxis(Axis):
|
||||||
times = array['time']
|
times = array['time']
|
||||||
i_0, i_l = times[0], times[-1]
|
i_0, i_l = times[0], times[-1]
|
||||||
|
|
||||||
|
# edge cases
|
||||||
if (
|
if (
|
||||||
|
not indexes
|
||||||
|
or
|
||||||
(indexes[0] < i_0
|
(indexes[0] < i_0
|
||||||
and indexes[-1] < i_l)
|
and indexes[-1] < i_l)
|
||||||
or
|
or
|
||||||
|
|
|
@ -829,8 +829,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
sig_mouse_leave = QtCore.pyqtSignal(object)
|
sig_mouse_leave = QtCore.pyqtSignal(object)
|
||||||
sig_mouse_enter = QtCore.pyqtSignal(object)
|
sig_mouse_enter = QtCore.pyqtSignal(object)
|
||||||
|
|
||||||
_l1_labels: L1Labels = None
|
|
||||||
|
|
||||||
mode_name: str = 'view'
|
mode_name: str = 'view'
|
||||||
|
|
||||||
# TODO: can take a ``background`` color setting - maybe there's
|
# TODO: can take a ``background`` color setting - maybe there's
|
||||||
|
@ -986,13 +984,15 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
'''
|
'''
|
||||||
# TODO: compute some sensible maximum value here
|
# TODO: compute some sensible maximum value here
|
||||||
# and use a humanized scheme to limit to that length.
|
# and use a humanized scheme to limit to that length.
|
||||||
l1_len = self._max_l1_line_len
|
from ._l1 import L1Label
|
||||||
|
l1_len = abs(L1Label._x_br_offset)
|
||||||
ryaxis = self.getAxis('right')
|
ryaxis = self.getAxis('right')
|
||||||
|
|
||||||
r_axis_x = ryaxis.pos().x()
|
r_axis_x = ryaxis.pos().x()
|
||||||
up_to_l1_sc = r_axis_x - l1_len
|
up_to_l1_sc = r_axis_x - l1_len
|
||||||
marker_right = up_to_l1_sc - (1.375 * 2 * marker_size)
|
marker_right = up_to_l1_sc - (1.375 * 2 * marker_size)
|
||||||
line_end = marker_right - (6/16 * marker_size)
|
# line_end = marker_right - (6/16 * marker_size)
|
||||||
|
line_end = marker_right - marker_size
|
||||||
|
|
||||||
# print(
|
# print(
|
||||||
# f'r_axis_x: {r_axis_x}\n'
|
# f'r_axis_x: {r_axis_x}\n'
|
||||||
|
@ -1231,7 +1231,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# (we need something that avoids clutter on x-axis).
|
# (we need something that avoids clutter on x-axis).
|
||||||
axis.add_sticky(
|
axis.add_sticky(
|
||||||
pi=pi,
|
pi=pi,
|
||||||
bg_color=color,
|
fg_color='black',
|
||||||
|
# bg_color=color,
|
||||||
digits=digits,
|
digits=digits,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,7 @@ class LineDot(pg.CurvePoint):
|
||||||
|
|
||||||
plot: ChartPlotWidget, # type: ingore # noqa
|
plot: ChartPlotWidget, # type: ingore # noqa
|
||||||
pos=None,
|
pos=None,
|
||||||
color: str = 'default_light',
|
color: str = 'bracket',
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
# scale from dpi aware font size
|
# scale from dpi aware font size
|
||||||
|
@ -349,7 +349,7 @@ class Cursor(pg.GraphicsObject):
|
||||||
# XXX: not sure why these are instance variables?
|
# XXX: not sure why these are instance variables?
|
||||||
# It's not like we can change them on the fly..?
|
# It's not like we can change them on the fly..?
|
||||||
self.pen = pg.mkPen(
|
self.pen = pg.mkPen(
|
||||||
color=hcolor('default'),
|
color=hcolor('bracket'),
|
||||||
style=QtCore.Qt.DashLine,
|
style=QtCore.Qt.DashLine,
|
||||||
)
|
)
|
||||||
self.lines_pen = pg.mkPen(
|
self.lines_pen = pg.mkPen(
|
||||||
|
@ -365,7 +365,7 @@ class Cursor(pg.GraphicsObject):
|
||||||
self._lw = self.pixelWidth() * self.lines_pen.width()
|
self._lw = self.pixelWidth() * self.lines_pen.width()
|
||||||
|
|
||||||
# xhair label's color name
|
# xhair label's color name
|
||||||
self.label_color: str = 'default'
|
self.label_color: str = 'bracket'
|
||||||
|
|
||||||
self._y_label_update: bool = True
|
self._y_label_update: bool = True
|
||||||
|
|
||||||
|
|
|
@ -217,6 +217,9 @@ def render_baritems(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_sample_rates: set[float] = {1, 60}
|
||||||
|
|
||||||
|
|
||||||
class Viz(msgspec.Struct): # , frozen=True):
|
class Viz(msgspec.Struct): # , frozen=True):
|
||||||
'''
|
'''
|
||||||
(Data) "Visualization" compound type which wraps a real-time
|
(Data) "Visualization" compound type which wraps a real-time
|
||||||
|
@ -284,15 +287,33 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
reset: bool = False,
|
reset: bool = False,
|
||||||
|
|
||||||
) -> float:
|
) -> float:
|
||||||
|
|
||||||
|
# attempt to dectect the best step size by scanning a sample of
|
||||||
|
# the source data.
|
||||||
if self._index_step is None:
|
if self._index_step is None:
|
||||||
|
|
||||||
index = self.shm.array[self.index_field]
|
index = self.shm.array[self.index_field]
|
||||||
isample = index[:16]
|
isample = index[:16]
|
||||||
mxdiff = np.diff(isample).max()
|
|
||||||
|
mxdiff: None | float = None
|
||||||
|
for step in np.diff(isample):
|
||||||
|
if step in _sample_rates:
|
||||||
|
if (
|
||||||
|
mxdiff is not None
|
||||||
|
and step != mxdiff
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
f'Multiple step sizes detected? {mxdiff}, {step}'
|
||||||
|
)
|
||||||
|
mxdiff = step
|
||||||
|
|
||||||
self._index_step = max(mxdiff, 1)
|
self._index_step = max(mxdiff, 1)
|
||||||
if (
|
if (
|
||||||
mxdiff < 1
|
mxdiff < 1
|
||||||
or 1 < mxdiff < 60
|
or 1 < mxdiff < 60
|
||||||
):
|
):
|
||||||
|
# TODO: remove this once we're sure the above scan loop
|
||||||
|
# is rock solid.
|
||||||
breakpoint()
|
breakpoint()
|
||||||
|
|
||||||
return self._index_step
|
return self._index_step
|
||||||
|
|
|
@ -328,10 +328,6 @@ async def graphics_update_loop(
|
||||||
digits=symbol.tick_size_digits,
|
digits=symbol.tick_size_digits,
|
||||||
size_digits=symbol.lot_size_digits,
|
size_digits=symbol.lot_size_digits,
|
||||||
)
|
)
|
||||||
# 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:
|
# TODO:
|
||||||
# - in theory we should be able to read buffer data faster
|
# - in theory we should be able to read buffer data faster
|
||||||
|
@ -416,11 +412,11 @@ async def graphics_update_loop(
|
||||||
|
|
||||||
last_quote_s = time.time()
|
last_quote_s = time.time()
|
||||||
|
|
||||||
for sym, quote in quotes.items():
|
for fqsn, quote in quotes.items():
|
||||||
ds = dss[sym]
|
ds = dss[fqsn]
|
||||||
ds.quotes = quote
|
ds.quotes = quote
|
||||||
|
|
||||||
rt_pi, hist_pi = pis[sym]
|
rt_pi, hist_pi = pis[fqsn]
|
||||||
|
|
||||||
# chart isn't active/shown so skip render cycle and
|
# chart isn't active/shown so skip render cycle and
|
||||||
# pause feed(s)
|
# pause feed(s)
|
||||||
|
@ -449,16 +445,21 @@ async def graphics_update_loop(
|
||||||
def graphics_update_cycle(
|
def graphics_update_cycle(
|
||||||
ds: DisplayState,
|
ds: DisplayState,
|
||||||
quote: dict,
|
quote: dict,
|
||||||
|
|
||||||
wap_in_history: bool = False,
|
wap_in_history: bool = False,
|
||||||
trigger_all: bool = False, # flag used by prepend history updates
|
trigger_all: bool = False, # flag used by prepend history updates
|
||||||
prepend_update_index: Optional[int] = None,
|
prepend_update_index: Optional[int] = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
# TODO: eventually optimize this whole graphics stack with ``numba``
|
|
||||||
# hopefully XD
|
# TODO: SPEEDing this all up..
|
||||||
|
# - optimize this whole graphics stack with ``numba`` hopefully
|
||||||
|
# or at least a little `mypyc` B)
|
||||||
|
# - pass more direct refs as input to avoid so many attr accesses?
|
||||||
|
# - use a streaming minmax algo and drop the use of the
|
||||||
|
# state-tracking ``chart_maxmin()`` routine from above?
|
||||||
|
|
||||||
chart = ds.chart
|
chart = ds.chart
|
||||||
# TODO: just pass this as a direct ref to avoid so many attr accesses?
|
|
||||||
hist_chart = ds.godwidget.hist_linked.chart
|
hist_chart = ds.godwidget.hist_linked.chart
|
||||||
|
|
||||||
flume = ds.flume
|
flume = ds.flume
|
||||||
|
@ -471,10 +472,7 @@ def graphics_update_cycle(
|
||||||
msg=f'Graphics loop cycle for: `{chart.name}`',
|
msg=f'Graphics loop cycle for: `{chart.name}`',
|
||||||
delayed=True,
|
delayed=True,
|
||||||
disabled=not pg_profile_enabled(),
|
disabled=not pg_profile_enabled(),
|
||||||
# disabled=True,
|
|
||||||
ms_threshold=ms_slower_then,
|
ms_threshold=ms_slower_then,
|
||||||
|
|
||||||
# ms_threshold=1/12 * 1e3,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# unpack multi-referenced components
|
# unpack multi-referenced components
|
||||||
|
@ -554,48 +552,34 @@ def graphics_update_cycle(
|
||||||
|
|
||||||
profiler('view incremented')
|
profiler('view incremented')
|
||||||
|
|
||||||
# from pprint import pformat
|
# iterate frames of ticks-by-type such that we only update graphics
|
||||||
# frame_counts = {
|
# using the last update per type where possible.
|
||||||
# typ: len(frame) for typ, frame in frames_by_type.items()
|
|
||||||
# }
|
|
||||||
# print(
|
|
||||||
# f'{pformat(frame_counts)}\n'
|
|
||||||
# f'framed: {pformat(frames_by_type)}\n'
|
|
||||||
# f'lasts: {pformat(lasts)}\n'
|
|
||||||
# )
|
|
||||||
# for typ, tick in lasts.items():
|
|
||||||
# ticks_frame = quote.get('ticks', ())
|
|
||||||
ticks_by_type = quote.get('tbt', {})
|
ticks_by_type = quote.get('tbt', {})
|
||||||
|
|
||||||
# for tick in ticks_frame:
|
|
||||||
for typ, ticks in ticks_by_type.items():
|
for typ, ticks in ticks_by_type.items():
|
||||||
|
|
||||||
# NOTE: ticks are `.append()`-ed to the `ticks_by_type: dict` by the
|
# NOTE: ticks are `.append()`-ed to the `ticks_by_type: dict` by the
|
||||||
# `._sampling.uniform_rate_send()` loop
|
# `._sampling.uniform_rate_send()` loop
|
||||||
tick = ticks[-1]
|
tick = ticks[-1] # get most recent value
|
||||||
# typ = tick.get('type')
|
|
||||||
price = tick.get('price')
|
price = tick.get('price')
|
||||||
size = tick.get('size')
|
size = tick.get('size')
|
||||||
|
|
||||||
# compute max and min prices (including bid/ask) from
|
# compute max and min prices (including bid/ask) from
|
||||||
# tick frames to determine the y-range for chart
|
# tick frames to determine the y-range for chart
|
||||||
# auto-scaling.
|
# auto-scaling.
|
||||||
# TODO: we need a streaming minmax algo here, see def above.
|
if (
|
||||||
if liv:
|
liv
|
||||||
|
|
||||||
|
# TODO: make sure IB doesn't send ``-1``!
|
||||||
|
and price > 0
|
||||||
|
):
|
||||||
mx = max(price + tick_margin, mx)
|
mx = max(price + tick_margin, mx)
|
||||||
mn = min(price - tick_margin, mn)
|
mn = min(price - tick_margin, mn)
|
||||||
|
|
||||||
|
# clearing price update:
|
||||||
|
# generally, we only want to update grahpics from the *last*
|
||||||
|
# tick event once - thus showing the most recent state.
|
||||||
if typ in clear_types:
|
if typ in clear_types:
|
||||||
# XXX: if we only wanted to update graphics from the
|
|
||||||
# "current"/"latest received" clearing price tick
|
|
||||||
# once (see alt iteration order above).
|
|
||||||
# if last_clear_updated:
|
|
||||||
# continue
|
|
||||||
|
|
||||||
# last_clear_updated = True
|
|
||||||
# we only want to update grahpics from the *last*
|
|
||||||
# tick event that falls under the "clearing price"
|
|
||||||
# set.
|
|
||||||
|
|
||||||
# update price sticky(s)
|
# update price sticky(s)
|
||||||
end_ic = array[-1][[
|
end_ic = array[-1][[
|
||||||
|
@ -610,10 +594,7 @@ def graphics_update_cycle(
|
||||||
chart.update_graphics_from_flow('bar_wap')
|
chart.update_graphics_from_flow('bar_wap')
|
||||||
|
|
||||||
# L1 book label-line updates
|
# L1 book label-line updates
|
||||||
# XXX: is this correct for ib?
|
if typ in ('last',):
|
||||||
# if ticktype in ('trade', 'last'):
|
|
||||||
# if ticktype in ('last',): # 'size'):
|
|
||||||
if typ in ('last',): # 'size'):
|
|
||||||
|
|
||||||
label = {
|
label = {
|
||||||
l1.ask_label.fields['level']: l1.ask_label,
|
l1.ask_label.fields['level']: l1.ask_label,
|
||||||
|
@ -629,40 +610,64 @@ def graphics_update_cycle(
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: on trades should we be knocking down
|
# TODO: on trades should we be knocking down
|
||||||
# the relevant L1 queue?
|
# the relevant L1 queue manually ourselves?
|
||||||
# label.size -= size
|
# label.size -= size
|
||||||
|
|
||||||
|
# NOTE: right now we always update the y-axis labels
|
||||||
|
# despite the last datum not being in view. Ideally
|
||||||
|
# we have a guard for this when we detect that the range
|
||||||
|
# of those values is not in view and then we disable these
|
||||||
|
# blocks.
|
||||||
elif (
|
elif (
|
||||||
typ in _tick_groups['asks']
|
typ in _tick_groups['asks']
|
||||||
# TODO: instead we could check if the price is in the
|
|
||||||
# y-view-range?
|
|
||||||
and liv
|
|
||||||
):
|
):
|
||||||
l1.ask_label.update_fields({'level': price, 'size': size})
|
l1.ask_label.update_fields({'level': price, 'size': size})
|
||||||
|
|
||||||
elif (
|
elif (
|
||||||
typ in _tick_groups['bids']
|
typ in _tick_groups['bids']
|
||||||
# TODO: instead we could check if the price is in the
|
|
||||||
# y-view-range?
|
|
||||||
and liv
|
|
||||||
):
|
):
|
||||||
l1.bid_label.update_fields({'level': price, 'size': size})
|
l1.bid_label.update_fields({'level': price, 'size': size})
|
||||||
|
|
||||||
# check for y-range re-size
|
# check for y-autorange re-size
|
||||||
if (mx > varz['last_mx']) or (mn < varz['last_mn']):
|
lmx = varz['last_mx']
|
||||||
|
lmn = varz['last_mn']
|
||||||
|
mx_diff = mx - lmx
|
||||||
|
mn_diff = mn - lmn
|
||||||
|
|
||||||
# fast chart resize case
|
if (
|
||||||
|
mx_diff
|
||||||
|
or mn_diff
|
||||||
|
):
|
||||||
if (
|
if (
|
||||||
|
abs(mx_diff) > .25 * lmx
|
||||||
|
or
|
||||||
|
abs(mn_diff) > .25 * lmn
|
||||||
|
):
|
||||||
|
log.error(
|
||||||
|
f'WTF MN/MX IS WAY OFF:\n'
|
||||||
|
f'lmn: {lmn}\n'
|
||||||
|
f'mn: {mn}\n'
|
||||||
|
f'lmx: {lmx}\n'
|
||||||
|
f'mx: {mx}\n'
|
||||||
|
f'mx_diff: {mx_diff}\n'
|
||||||
|
f'mn_diff: {mn_diff}\n'
|
||||||
|
)
|
||||||
|
# fast chart resize case
|
||||||
|
elif (
|
||||||
liv
|
liv
|
||||||
and not chart._static_yrange == 'axis'
|
and not chart._static_yrange == 'axis'
|
||||||
):
|
):
|
||||||
# main_vb = chart.view
|
|
||||||
main_vb = chart._vizs[fqsn].plot.vb
|
main_vb = chart._vizs[fqsn].plot.vb
|
||||||
if (
|
if (
|
||||||
main_vb._ic is None
|
main_vb._ic is None
|
||||||
or not main_vb._ic.is_set()
|
or not main_vb._ic.is_set()
|
||||||
):
|
):
|
||||||
# print(f'updating range due to mxmn')
|
yr = (mn, mx)
|
||||||
|
# print(
|
||||||
|
# f'updating y-range due to mxmn\n'
|
||||||
|
# f'{fqsn}: {yr}'
|
||||||
|
# )
|
||||||
|
|
||||||
main_vb._set_yrange(
|
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
|
||||||
|
@ -670,11 +675,10 @@ def graphics_update_cycle(
|
||||||
# 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),
|
yrange=yr
|
||||||
)
|
)
|
||||||
|
|
||||||
# check if slow chart needs a resize
|
# check if slow chart needs a resize
|
||||||
|
|
||||||
hist_viz = hist_chart._vizs[fqsn]
|
hist_viz = hist_chart._vizs[fqsn]
|
||||||
(
|
(
|
||||||
_,
|
_,
|
||||||
|
@ -691,7 +695,7 @@ def graphics_update_cycle(
|
||||||
if hist_liv:
|
if hist_liv:
|
||||||
hist_viz.plot.vb._set_yrange()
|
hist_viz.plot.vb._set_yrange()
|
||||||
|
|
||||||
# XXX: update this every draw cycle to make L1-always-in-view work.
|
# XXX: update this every draw cycle to make
|
||||||
varz['last_mx'], varz['last_mn'] = mx, mn
|
varz['last_mx'], varz['last_mn'] = mx, mn
|
||||||
|
|
||||||
# run synchronous update on all linked viz
|
# run synchronous update on all linked viz
|
||||||
|
@ -767,10 +771,8 @@ def graphics_update_cycle(
|
||||||
if (
|
if (
|
||||||
mx_vlm_in_view != varz['last_mx_vlm']
|
mx_vlm_in_view != varz['last_mx_vlm']
|
||||||
):
|
):
|
||||||
yrange = (0, mx_vlm_in_view * 1.375)
|
vlm_yr = (0, mx_vlm_in_view * 1.375)
|
||||||
vlm_chart.view._set_yrange(
|
vlm_chart.view._set_yrange(yrange=vlm_yr)
|
||||||
yrange=yrange,
|
|
||||||
)
|
|
||||||
profiler('`vlm_chart.view._set_yrange()`')
|
profiler('`vlm_chart.view._set_yrange()`')
|
||||||
# print(f'mx vlm: {last_mx_vlm} -> {mx_vlm_in_view}')
|
# print(f'mx vlm: {last_mx_vlm} -> {mx_vlm_in_view}')
|
||||||
varz['last_mx_vlm'] = mx_vlm_in_view
|
varz['last_mx_vlm'] = mx_vlm_in_view
|
||||||
|
@ -1047,6 +1049,10 @@ async def display_symbol_data(
|
||||||
group_key=True
|
group_key=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# (TODO: make this not so shit XD)
|
||||||
|
# close group status once a symbol feed fully loads to view.
|
||||||
|
# sbar._status_groups[loading_sym_key][1]()
|
||||||
|
|
||||||
# TODO: ctl over update loop's maximum frequency.
|
# TODO: ctl over update loop's maximum frequency.
|
||||||
# - load this from a config.toml!
|
# - load this from a config.toml!
|
||||||
# - allow dyanmic configuration from chart UI?
|
# - allow dyanmic configuration from chart UI?
|
||||||
|
@ -1129,6 +1135,8 @@ async def display_symbol_data(
|
||||||
# and sub-charts for FSPs
|
# and sub-charts for FSPs
|
||||||
fqsn, flume = fitems[0]
|
fqsn, flume = fitems[0]
|
||||||
|
|
||||||
|
# TODO NOTE: THIS CONTROLS WHAT SYMBOL IS USED FOR ORDER MODE
|
||||||
|
# SUBMISSIONS, we need to make this switch based on selection.
|
||||||
rt_linked._symbol = flume.symbol
|
rt_linked._symbol = flume.symbol
|
||||||
hist_linked._symbol = flume.symbol
|
hist_linked._symbol = flume.symbol
|
||||||
|
|
||||||
|
@ -1219,9 +1227,6 @@ async def display_symbol_data(
|
||||||
# get a new color from the palette
|
# get a new color from the palette
|
||||||
bg_chart_color, bg_last_bar_color = next(palette)
|
bg_chart_color, bg_last_bar_color = next(palette)
|
||||||
|
|
||||||
rt_linked._symbol = flume.symbol
|
|
||||||
hist_linked._symbol = flume.symbol
|
|
||||||
|
|
||||||
ohlcv: ShmArray = flume.rt_shm
|
ohlcv: ShmArray = flume.rt_shm
|
||||||
hist_ohlcv: ShmArray = flume.hist_shm
|
hist_ohlcv: ShmArray = flume.hist_shm
|
||||||
|
|
||||||
|
@ -1232,9 +1237,14 @@ async def display_symbol_data(
|
||||||
name=fqsn,
|
name=fqsn,
|
||||||
axis_title=fqsn,
|
axis_title=fqsn,
|
||||||
)
|
)
|
||||||
hist_pi.hideAxis('left')
|
# only show a singleton bottom-bottom axis by default.
|
||||||
hist_pi.hideAxis('bottom')
|
hist_pi.hideAxis('bottom')
|
||||||
|
|
||||||
|
# XXX: TODO: THIS WILL CAUSE A GAP ON OVERLAYS,
|
||||||
|
# i think it needs to be "removed" instead when there
|
||||||
|
# are none?
|
||||||
|
hist_pi.hideAxis('left')
|
||||||
|
|
||||||
viz = hist_chart.draw_curve(
|
viz = hist_chart.draw_curve(
|
||||||
fqsn,
|
fqsn,
|
||||||
hist_ohlcv,
|
hist_ohlcv,
|
||||||
|
@ -1311,36 +1321,14 @@ async def display_symbol_data(
|
||||||
|
|
||||||
# XXX: if we wanted it at the bottom?
|
# XXX: if we wanted it at the bottom?
|
||||||
# rt_linked.splitter.addWidget(hist_linked)
|
# rt_linked.splitter.addWidget(hist_linked)
|
||||||
rt_linked.focus()
|
|
||||||
|
|
||||||
godwidget.resize_all()
|
# greedily do a view range default and pane resizing
|
||||||
|
# on startup before loading the order-mode machinery.
|
||||||
# add all additional symbols as overlays
|
|
||||||
for fqsn, flume in feed.flumes.items():
|
for fqsn, flume in feed.flumes.items():
|
||||||
|
|
||||||
# size view to data prior to order mode init
|
# size view to data prior to order mode init
|
||||||
rt_chart.default_view()
|
rt_chart.default_view()
|
||||||
rt_linked.graphics_cycle()
|
rt_linked.graphics_cycle()
|
||||||
await trio.sleep(0)
|
|
||||||
|
|
||||||
hist_chart.default_view(
|
|
||||||
bars_from_y=int(len(hist_ohlcv.array)), # size to data
|
|
||||||
y_offset=6116*2, # push it a little away from the y-axis
|
|
||||||
)
|
|
||||||
hist_linked.graphics_cycle()
|
|
||||||
await trio.sleep(0)
|
|
||||||
|
|
||||||
godwidget.resize_all()
|
|
||||||
|
|
||||||
# trigger another view reset if no sub-chart
|
|
||||||
hist_chart.default_view()
|
|
||||||
rt_chart.default_view()
|
|
||||||
# let qt run to render all widgets and make sure the
|
|
||||||
# sidepanes line up vertically.
|
|
||||||
await trio.sleep(0)
|
|
||||||
|
|
||||||
# dynamic resize steps
|
|
||||||
godwidget.resize_all()
|
|
||||||
|
|
||||||
# TODO: look into this because not sure why it was
|
# TODO: look into this because not sure why it was
|
||||||
# commented out / we ever needed it XD
|
# commented out / we ever needed it XD
|
||||||
|
@ -1350,21 +1338,11 @@ async def display_symbol_data(
|
||||||
# determine if auto-range adjustements should be made.
|
# determine if auto-range adjustements should be made.
|
||||||
# rt_linked.subplots.pop('volume', None)
|
# rt_linked.subplots.pop('volume', None)
|
||||||
|
|
||||||
# TODO: make this not so shit XD
|
hist_chart.default_view()
|
||||||
# close group status
|
|
||||||
# sbar._status_groups[loading_sym_key][1]()
|
|
||||||
|
|
||||||
hist_linked.graphics_cycle()
|
hist_linked.graphics_cycle()
|
||||||
rt_chart.default_view()
|
|
||||||
await trio.sleep(0)
|
|
||||||
|
|
||||||
bars_in_mem = int(len(hist_ohlcv.array))
|
|
||||||
hist_chart.default_view(
|
|
||||||
bars_from_y=bars_in_mem, # size to data
|
|
||||||
# push it 1/16th away from the y-axis
|
|
||||||
y_offset=round(bars_in_mem / 16),
|
|
||||||
)
|
|
||||||
godwidget.resize_all()
|
godwidget.resize_all()
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
await link_views_with_region(
|
await link_views_with_region(
|
||||||
rt_chart,
|
rt_chart,
|
||||||
|
@ -1372,7 +1350,7 @@ async def display_symbol_data(
|
||||||
flume,
|
flume,
|
||||||
)
|
)
|
||||||
|
|
||||||
# start graphics update loop after receiving first live quote
|
# start update loop task
|
||||||
ln.start_soon(
|
ln.start_soon(
|
||||||
graphics_update_loop,
|
graphics_update_loop,
|
||||||
ln,
|
ln,
|
||||||
|
@ -1383,20 +1361,31 @@ async def display_symbol_data(
|
||||||
vlm_charts,
|
vlm_charts,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# boot order-mode
|
||||||
|
order_ctl_symbol: str = fqsns[0]
|
||||||
mode: OrderMode
|
mode: OrderMode
|
||||||
async with (
|
async with (
|
||||||
open_order_mode(
|
open_order_mode(
|
||||||
feed,
|
feed,
|
||||||
godwidget,
|
godwidget,
|
||||||
fqsns[-1],
|
fqsns[0],
|
||||||
order_mode_started
|
order_mode_started
|
||||||
) as mode
|
) as mode
|
||||||
):
|
):
|
||||||
|
|
||||||
rt_linked.mode = mode
|
rt_linked.mode = mode
|
||||||
|
|
||||||
|
viz = rt_chart.get_viz(order_ctl_symbol)
|
||||||
|
viz.plot.setFocus()
|
||||||
|
|
||||||
|
# default view adjuments and sidepane alignment
|
||||||
|
# as final default UX touch.
|
||||||
rt_chart.default_view()
|
rt_chart.default_view()
|
||||||
rt_chart.view.enable_auto_yrange()
|
rt_chart.view.enable_auto_yrange()
|
||||||
|
|
||||||
hist_chart.default_view()
|
hist_chart.default_view()
|
||||||
hist_chart.view.enable_auto_yrange()
|
hist_chart.view.enable_auto_yrange()
|
||||||
|
|
||||||
|
godwidget.resize_all()
|
||||||
|
|
||||||
await trio.sleep_forever() # let the app run.. bby
|
await trio.sleep_forever() # let the app run.. bby
|
||||||
|
|
|
@ -661,6 +661,12 @@ async def open_vlm_displays(
|
||||||
# str(period_param.default)
|
# str(period_param.default)
|
||||||
# )
|
# )
|
||||||
|
|
||||||
|
# use slightly less light (then bracket) gray
|
||||||
|
# for volume from "main exchange" and a more "bluey"
|
||||||
|
# gray for "dark" vlm.
|
||||||
|
vlm_color = 'i3'
|
||||||
|
dark_vlm_color = 'charcoal'
|
||||||
|
|
||||||
# built-in vlm which we plot ASAP since it's
|
# built-in vlm which we plot ASAP since it's
|
||||||
# usually data provided directly with OHLC history.
|
# usually data provided directly with OHLC history.
|
||||||
shm = ohlcv
|
shm = ohlcv
|
||||||
|
@ -755,7 +761,7 @@ async def open_vlm_displays(
|
||||||
|
|
||||||
{ # fsp engine conf
|
{ # fsp engine conf
|
||||||
'func_name': 'dolla_vlm',
|
'func_name': 'dolla_vlm',
|
||||||
'zero_on_step': True,
|
'zero_on_step': False,
|
||||||
'params': {
|
'params': {
|
||||||
'price_func': {
|
'price_func': {
|
||||||
'default_value': 'chl3',
|
'default_value': 'chl3',
|
||||||
|
@ -769,7 +775,7 @@ async def open_vlm_displays(
|
||||||
# FIXME: we should error on starting the same fsp right
|
# FIXME: we should error on starting the same fsp right
|
||||||
# since it might collide with existing shm.. or wait we
|
# since it might collide with existing shm.. or wait we
|
||||||
# had this before??
|
# had this before??
|
||||||
# dolla_vlm,
|
# dolla_vlm
|
||||||
|
|
||||||
tasks_ready.append(started)
|
tasks_ready.append(started)
|
||||||
# profiler(f'created shm for fsp actor: {display_name}')
|
# profiler(f'created shm for fsp actor: {display_name}')
|
||||||
|
@ -786,19 +792,24 @@ async def open_vlm_displays(
|
||||||
dvlm_pi = vlm_chart.overlay_plotitem(
|
dvlm_pi = vlm_chart.overlay_plotitem(
|
||||||
'dolla_vlm',
|
'dolla_vlm',
|
||||||
index=0, # place axis on inside (nearest to chart)
|
index=0, # place axis on inside (nearest to chart)
|
||||||
|
|
||||||
axis_title=' $vlm',
|
axis_title=' $vlm',
|
||||||
axis_side='right',
|
axis_side='left',
|
||||||
|
|
||||||
axis_kwargs={
|
axis_kwargs={
|
||||||
'typical_max_str': ' 100.0 M ',
|
'typical_max_str': ' 100.0 M ',
|
||||||
'formatter': partial(
|
'formatter': partial(
|
||||||
humanize,
|
humanize,
|
||||||
digits=2,
|
digits=2,
|
||||||
),
|
),
|
||||||
|
'text_color': vlm_color,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
dvlm_pi.hideAxis('left')
|
# TODO: should this maybe be implicit based on input args to
|
||||||
|
# `.overlay_plotitem()` above?
|
||||||
dvlm_pi.hideAxis('bottom')
|
dvlm_pi.hideAxis('bottom')
|
||||||
|
|
||||||
# all to be overlayed curve names
|
# all to be overlayed curve names
|
||||||
fields = [
|
fields = [
|
||||||
'dolla_vlm',
|
'dolla_vlm',
|
||||||
|
@ -823,12 +834,6 @@ async def open_vlm_displays(
|
||||||
# add custom auto range handler
|
# add custom auto range handler
|
||||||
dvlm_pi.vb._maxmin = group_mxmn
|
dvlm_pi.vb._maxmin = group_mxmn
|
||||||
|
|
||||||
# use slightly less light (then bracket) gray
|
|
||||||
# for volume from "main exchange" and a more "bluey"
|
|
||||||
# gray for "dark" vlm.
|
|
||||||
vlm_color = 'i3'
|
|
||||||
dark_vlm_color = 'charcoal'
|
|
||||||
|
|
||||||
# add dvlm (step) curves to common view
|
# add dvlm (step) curves to common view
|
||||||
def chart_curves(
|
def chart_curves(
|
||||||
names: list[str],
|
names: list[str],
|
||||||
|
@ -879,7 +884,7 @@ async def open_vlm_displays(
|
||||||
flow_rates,
|
flow_rates,
|
||||||
{ # fsp engine conf
|
{ # fsp engine conf
|
||||||
'func_name': 'flow_rates',
|
'func_name': 'flow_rates',
|
||||||
'zero_on_step': False,
|
'zero_on_step': True,
|
||||||
},
|
},
|
||||||
# loglevel,
|
# loglevel,
|
||||||
)
|
)
|
||||||
|
@ -913,8 +918,8 @@ async def open_vlm_displays(
|
||||||
# TODO: dynamically update period (and thus this axis?)
|
# TODO: dynamically update period (and thus this axis?)
|
||||||
# title from user input.
|
# title from user input.
|
||||||
axis_title='clears',
|
axis_title='clears',
|
||||||
|
|
||||||
axis_side='left',
|
axis_side='left',
|
||||||
|
|
||||||
axis_kwargs={
|
axis_kwargs={
|
||||||
'typical_max_str': ' 10.0 M ',
|
'typical_max_str': ' 10.0 M ',
|
||||||
'formatter': partial(
|
'formatter': partial(
|
||||||
|
|
|
@ -30,19 +30,20 @@ from ._pg_overrides import PlotItem
|
||||||
|
|
||||||
|
|
||||||
class LevelLabel(YAxisLabel):
|
class LevelLabel(YAxisLabel):
|
||||||
"""Y-axis (vertically) oriented, horizontal label that sticks to
|
'''
|
||||||
|
Y-axis (vertically) oriented, horizontal label that sticks to
|
||||||
where it's placed despite chart resizing and supports displaying
|
where it's placed despite chart resizing and supports displaying
|
||||||
multiple fields.
|
multiple fields.
|
||||||
|
|
||||||
|
|
||||||
TODO: replace the rectangle-text part with our new ``Label`` type.
|
TODO: replace the rectangle-text part with our new ``Label`` type.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
_x_margin = 0
|
_x_br_offset: float = -16
|
||||||
_y_margin = 0
|
_y_txt_h_scaling: float = 2
|
||||||
|
|
||||||
# adjustment "further away from" anchor point
|
# adjustment "further away from" anchor point
|
||||||
_x_offset = 9
|
_x_offset = 0
|
||||||
_y_offset = 0
|
_y_offset = 0
|
||||||
|
|
||||||
# fields to be displayed in the label string
|
# fields to be displayed in the label string
|
||||||
|
@ -58,12 +59,12 @@ class LevelLabel(YAxisLabel):
|
||||||
chart,
|
chart,
|
||||||
parent,
|
parent,
|
||||||
|
|
||||||
color: str = 'bracket',
|
color: str = 'default_light',
|
||||||
|
|
||||||
orient_v: str = 'bottom',
|
orient_v: str = 'bottom',
|
||||||
orient_h: str = 'left',
|
orient_h: str = 'right',
|
||||||
|
|
||||||
opacity: float = 0,
|
opacity: float = 1,
|
||||||
|
|
||||||
# makes order line labels offset from their parent axis
|
# makes order line labels offset from their parent axis
|
||||||
# such that they don't collide with the L1/L2 lines/prices
|
# such that they don't collide with the L1/L2 lines/prices
|
||||||
|
@ -99,13 +100,15 @@ class LevelLabel(YAxisLabel):
|
||||||
|
|
||||||
self._h_shift = {
|
self._h_shift = {
|
||||||
'left': -1.,
|
'left': -1.,
|
||||||
'right': 0.
|
'right': 0.,
|
||||||
}[orient_h]
|
}[orient_h]
|
||||||
|
|
||||||
self.fields = self._fields.copy()
|
self.fields = self._fields.copy()
|
||||||
# ensure default format fields are in correct
|
# ensure default format fields are in correct
|
||||||
self.set_fmt_str(self._fmt_str, self.fields)
|
self.set_fmt_str(self._fmt_str, self.fields)
|
||||||
|
|
||||||
|
self.setZValue(10)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def color(self):
|
def color(self):
|
||||||
return self._hcolor
|
return self._hcolor
|
||||||
|
@ -113,7 +116,10 @@ class LevelLabel(YAxisLabel):
|
||||||
@color.setter
|
@color.setter
|
||||||
def color(self, color: str) -> None:
|
def color(self, color: str) -> None:
|
||||||
self._hcolor = color
|
self._hcolor = color
|
||||||
self._pen = self.pen = pg.mkPen(hcolor(color))
|
self._pen = self.pen = pg.mkPen(
|
||||||
|
hcolor(color),
|
||||||
|
width=3,
|
||||||
|
)
|
||||||
|
|
||||||
def update_on_resize(self, vr, r):
|
def update_on_resize(self, vr, r):
|
||||||
"""Tiis is a ``.sigRangeChanged()`` handler.
|
"""Tiis is a ``.sigRangeChanged()`` handler.
|
||||||
|
@ -125,10 +131,11 @@ class LevelLabel(YAxisLabel):
|
||||||
self,
|
self,
|
||||||
fields: dict = None,
|
fields: dict = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update the label's text contents **and** position from
|
'''
|
||||||
|
Update the label's text contents **and** position from
|
||||||
a view box coordinate datum.
|
a view box coordinate datum.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
self.fields.update(fields)
|
self.fields.update(fields)
|
||||||
level = self.fields['level']
|
level = self.fields['level']
|
||||||
|
|
||||||
|
@ -175,7 +182,8 @@ class LevelLabel(YAxisLabel):
|
||||||
fields: dict,
|
fields: dict,
|
||||||
):
|
):
|
||||||
# use space as e3 delim
|
# use space as e3 delim
|
||||||
self.label_str = self._fmt_str.format(**fields).replace(',', ' ')
|
self.label_str = self._fmt_str.format(
|
||||||
|
**fields).replace(',', ' ')
|
||||||
|
|
||||||
br = self.boundingRect()
|
br = self.boundingRect()
|
||||||
h, w = br.height(), br.width()
|
h, w = br.height(), br.width()
|
||||||
|
@ -188,14 +196,14 @@ class LevelLabel(YAxisLabel):
|
||||||
self,
|
self,
|
||||||
p: QtGui.QPainter,
|
p: QtGui.QPainter,
|
||||||
rect: QtCore.QRectF
|
rect: QtCore.QRectF
|
||||||
) -> None:
|
|
||||||
p.setPen(self._pen)
|
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
p.setPen(self._pen)
|
||||||
rect = self.rect
|
rect = self.rect
|
||||||
|
|
||||||
if self._orient_v == 'bottom':
|
if self._orient_v == 'bottom':
|
||||||
lp, rp = rect.topLeft(), rect.topRight()
|
lp, rp = rect.topLeft(), rect.topRight()
|
||||||
# p.drawLine(rect.topLeft(), rect.topRight())
|
|
||||||
|
|
||||||
elif self._orient_v == 'top':
|
elif self._orient_v == 'top':
|
||||||
lp, rp = rect.bottomLeft(), rect.bottomRight()
|
lp, rp = rect.bottomLeft(), rect.bottomRight()
|
||||||
|
@ -209,6 +217,11 @@ class LevelLabel(YAxisLabel):
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
p.fillRect(
|
||||||
|
self.rect,
|
||||||
|
self.bg_color,
|
||||||
|
)
|
||||||
|
|
||||||
def highlight(self, pen) -> None:
|
def highlight(self, pen) -> None:
|
||||||
self._pen = pen
|
self._pen = pen
|
||||||
self.update()
|
self.update()
|
||||||
|
@ -247,9 +260,10 @@ class L1Label(LevelLabel):
|
||||||
|
|
||||||
|
|
||||||
class L1Labels:
|
class L1Labels:
|
||||||
"""Level 1 bid ask labels for dynamic update on price-axis.
|
'''
|
||||||
|
Level 1 bid ask labels for dynamic update on price-axis.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
plotitem: PlotItem,
|
plotitem: PlotItem,
|
||||||
|
@ -265,15 +279,17 @@ class L1Labels:
|
||||||
'chart': plotitem,
|
'chart': plotitem,
|
||||||
'parent': raxis,
|
'parent': raxis,
|
||||||
|
|
||||||
'opacity': 1,
|
'opacity': .9,
|
||||||
'font_size': font_size,
|
'font_size': font_size,
|
||||||
'fg_color': chart.pen_color,
|
'fg_color': 'default_light',
|
||||||
'bg_color': chart.view_color,
|
'bg_color': chart.view_color, # normally 'papas_special'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# TODO: add humanized source-asset
|
||||||
|
# info format.
|
||||||
fmt_str = (
|
fmt_str = (
|
||||||
' {size:.{size_digits}f} x '
|
' {size:.{size_digits}f} u'
|
||||||
'{level:,.{level_digits}f} '
|
# '{level:,.{level_digits}f} '
|
||||||
)
|
)
|
||||||
fields = {
|
fields = {
|
||||||
'level': 0,
|
'level': 0,
|
||||||
|
@ -286,12 +302,17 @@ class L1Labels:
|
||||||
orient_v='bottom',
|
orient_v='bottom',
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
bid.set_fmt_str(fmt_str=fmt_str, fields=fields)
|
bid.set_fmt_str(
|
||||||
|
fmt_str='\n' + fmt_str,
|
||||||
|
fields=fields,
|
||||||
|
)
|
||||||
bid.show()
|
bid.show()
|
||||||
|
|
||||||
ask = self.ask_label = L1Label(
|
ask = self.ask_label = L1Label(
|
||||||
orient_v='top',
|
orient_v='top',
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
ask.set_fmt_str(fmt_str=fmt_str, fields=fields)
|
ask.set_fmt_str(
|
||||||
|
fmt_str=fmt_str,
|
||||||
|
fields=fields)
|
||||||
ask.show()
|
ask.show()
|
||||||
|
|
|
@ -233,6 +233,36 @@ class Label:
|
||||||
def delete(self) -> None:
|
def delete(self) -> None:
|
||||||
self.vb.scene().removeItem(self.txt)
|
self.vb.scene().removeItem(self.txt)
|
||||||
|
|
||||||
|
# NOTE: pulled out from ``ChartPlotWidget`` from way way old code.
|
||||||
|
# def _label_h(self, yhigh: float, ylow: float) -> float:
|
||||||
|
# # compute contents label "height" in view terms
|
||||||
|
# # to avoid having data "contents" overlap with them
|
||||||
|
# if self._labels:
|
||||||
|
# label = self._labels[self.name][0]
|
||||||
|
|
||||||
|
# rect = label.itemRect()
|
||||||
|
# tl, br = rect.topLeft(), rect.bottomRight()
|
||||||
|
# vb = self.plotItem.vb
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# # on startup labels might not yet be rendered
|
||||||
|
# top, bottom = (vb.mapToView(tl).y(), vb.mapToView(br).y())
|
||||||
|
|
||||||
|
# # XXX: magic hack, how do we compute exactly?
|
||||||
|
# label_h = (top - bottom) * 0.42
|
||||||
|
|
||||||
|
# except np.linalg.LinAlgError:
|
||||||
|
# label_h = 0
|
||||||
|
# else:
|
||||||
|
# label_h = 0
|
||||||
|
|
||||||
|
# # print(f'label height {self.name}: {label_h}')
|
||||||
|
|
||||||
|
# if label_h > yhigh - ylow:
|
||||||
|
# label_h = 0
|
||||||
|
|
||||||
|
# print(f"bounds (ylow, yhigh): {(ylow, yhigh)}")
|
||||||
|
|
||||||
|
|
||||||
class FormatLabel(QLabel):
|
class FormatLabel(QLabel):
|
||||||
'''
|
'''
|
||||||
|
|
|
@ -361,8 +361,8 @@ class PlotItemOverlay:
|
||||||
if not sub_handlers:
|
if not sub_handlers:
|
||||||
|
|
||||||
src_handler = getattr(
|
src_handler = getattr(
|
||||||
root.vb,
|
root.vb,
|
||||||
ev_name,
|
ev_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
def broadcast(
|
def broadcast(
|
||||||
|
|
|
@ -349,7 +349,7 @@ class OrderMode:
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if not order:
|
if not order:
|
||||||
staged = self._staged_order
|
staged: Order = self._staged_order
|
||||||
# apply order fields for ems
|
# apply order fields for ems
|
||||||
oid = str(uuid.uuid4())
|
oid = str(uuid.uuid4())
|
||||||
order = staged.copy()
|
order = staged.copy()
|
||||||
|
@ -703,7 +703,6 @@ async def open_order_mode(
|
||||||
|
|
||||||
# symbol id
|
# symbol id
|
||||||
symbol = chart.linked.symbol
|
symbol = chart.linked.symbol
|
||||||
symkey = symbol.front_fqsn()
|
|
||||||
|
|
||||||
# map of per-provider account keys to position tracker instances
|
# map of per-provider account keys to position tracker instances
|
||||||
trackers: dict[str, PositionTracker] = {}
|
trackers: dict[str, PositionTracker] = {}
|
||||||
|
@ -864,7 +863,7 @@ async def open_order_mode(
|
||||||
# the expected symbol key in its positions msg.
|
# the expected symbol key in its positions msg.
|
||||||
for (broker, acctid), msgs in position_msgs.items():
|
for (broker, acctid), msgs in position_msgs.items():
|
||||||
for msg in msgs:
|
for msg in msgs:
|
||||||
log.info(f'Loading pp for {symkey}:\n{pformat(msg)}')
|
log.info(f'Loading pp for {acctid}@{broker}:\n{pformat(msg)}')
|
||||||
await process_trade_msg(
|
await process_trade_msg(
|
||||||
mode,
|
mode,
|
||||||
book,
|
book,
|
||||||
|
|
Loading…
Reference in New Issue