commit
42d2f9e461
|
@ -306,7 +306,10 @@ class DynamicDateAxis(Axis):
|
|||
times = array['time']
|
||||
i_0, i_l = times[0], times[-1]
|
||||
|
||||
# edge cases
|
||||
if (
|
||||
not indexes
|
||||
or
|
||||
(indexes[0] < i_0
|
||||
and indexes[-1] < i_l)
|
||||
or
|
||||
|
|
|
@ -829,8 +829,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
sig_mouse_leave = QtCore.pyqtSignal(object)
|
||||
sig_mouse_enter = QtCore.pyqtSignal(object)
|
||||
|
||||
_l1_labels: L1Labels = None
|
||||
|
||||
mode_name: str = 'view'
|
||||
|
||||
# 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
|
||||
# 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')
|
||||
|
||||
r_axis_x = ryaxis.pos().x()
|
||||
up_to_l1_sc = r_axis_x - l1_len
|
||||
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(
|
||||
# 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).
|
||||
axis.add_sticky(
|
||||
pi=pi,
|
||||
bg_color=color,
|
||||
fg_color='black',
|
||||
# bg_color=color,
|
||||
digits=digits,
|
||||
)
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ class LineDot(pg.CurvePoint):
|
|||
|
||||
plot: ChartPlotWidget, # type: ingore # noqa
|
||||
pos=None,
|
||||
color: str = 'default_light',
|
||||
color: str = 'bracket',
|
||||
|
||||
) -> None:
|
||||
# scale from dpi aware font size
|
||||
|
@ -349,7 +349,7 @@ class Cursor(pg.GraphicsObject):
|
|||
# XXX: not sure why these are instance variables?
|
||||
# It's not like we can change them on the fly..?
|
||||
self.pen = pg.mkPen(
|
||||
color=hcolor('default'),
|
||||
color=hcolor('bracket'),
|
||||
style=QtCore.Qt.DashLine,
|
||||
)
|
||||
self.lines_pen = pg.mkPen(
|
||||
|
@ -365,7 +365,7 @@ class Cursor(pg.GraphicsObject):
|
|||
self._lw = self.pixelWidth() * self.lines_pen.width()
|
||||
|
||||
# xhair label's color name
|
||||
self.label_color: str = 'default'
|
||||
self.label_color: str = 'bracket'
|
||||
|
||||
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):
|
||||
'''
|
||||
(Data) "Visualization" compound type which wraps a real-time
|
||||
|
@ -284,15 +287,33 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
reset: bool = False,
|
||||
|
||||
) -> float:
|
||||
|
||||
# attempt to dectect the best step size by scanning a sample of
|
||||
# the source data.
|
||||
if self._index_step is None:
|
||||
|
||||
index = self.shm.array[self.index_field]
|
||||
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)
|
||||
if (
|
||||
mxdiff < 1
|
||||
or 1 < mxdiff < 60
|
||||
):
|
||||
# TODO: remove this once we're sure the above scan loop
|
||||
# is rock solid.
|
||||
breakpoint()
|
||||
|
||||
return self._index_step
|
||||
|
|
|
@ -328,10 +328,6 @@ async def graphics_update_loop(
|
|||
digits=symbol.tick_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:
|
||||
# - 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()
|
||||
|
||||
for sym, quote in quotes.items():
|
||||
ds = dss[sym]
|
||||
for fqsn, quote in quotes.items():
|
||||
ds = dss[fqsn]
|
||||
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
|
||||
# pause feed(s)
|
||||
|
@ -449,16 +445,21 @@ async def graphics_update_loop(
|
|||
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,
|
||||
|
||||
) -> 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
|
||||
# TODO: just pass this as a direct ref to avoid so many attr accesses?
|
||||
hist_chart = ds.godwidget.hist_linked.chart
|
||||
|
||||
flume = ds.flume
|
||||
|
@ -471,10 +472,7 @@ def graphics_update_cycle(
|
|||
msg=f'Graphics loop cycle for: `{chart.name}`',
|
||||
delayed=True,
|
||||
disabled=not pg_profile_enabled(),
|
||||
# disabled=True,
|
||||
ms_threshold=ms_slower_then,
|
||||
|
||||
# ms_threshold=1/12 * 1e3,
|
||||
)
|
||||
|
||||
# unpack multi-referenced components
|
||||
|
@ -554,48 +552,34 @@ def graphics_update_cycle(
|
|||
|
||||
profiler('view incremented')
|
||||
|
||||
# from pprint import pformat
|
||||
# frame_counts = {
|
||||
# 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', ())
|
||||
# iterate frames of ticks-by-type such that we only update graphics
|
||||
# using the last update per type where possible.
|
||||
ticks_by_type = quote.get('tbt', {})
|
||||
|
||||
# for tick in ticks_frame:
|
||||
for typ, ticks in ticks_by_type.items():
|
||||
|
||||
# NOTE: ticks are `.append()`-ed to the `ticks_by_type: dict` by the
|
||||
# `._sampling.uniform_rate_send()` loop
|
||||
tick = ticks[-1]
|
||||
# typ = tick.get('type')
|
||||
tick = ticks[-1] # get most recent value
|
||||
|
||||
price = tick.get('price')
|
||||
size = tick.get('size')
|
||||
|
||||
# compute max and min prices (including bid/ask) from
|
||||
# tick frames to determine the y-range for chart
|
||||
# auto-scaling.
|
||||
# TODO: we need a streaming minmax algo here, see def above.
|
||||
if liv:
|
||||
if (
|
||||
liv
|
||||
|
||||
# TODO: make sure IB doesn't send ``-1``!
|
||||
and price > 0
|
||||
):
|
||||
mx = max(price + tick_margin, mx)
|
||||
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:
|
||||
# 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)
|
||||
end_ic = array[-1][[
|
||||
|
@ -610,10 +594,7 @@ def graphics_update_cycle(
|
|||
chart.update_graphics_from_flow('bar_wap')
|
||||
|
||||
# L1 book label-line updates
|
||||
# XXX: is this correct for ib?
|
||||
# if ticktype in ('trade', 'last'):
|
||||
# if ticktype in ('last',): # 'size'):
|
||||
if typ in ('last',): # 'size'):
|
||||
if typ in ('last',):
|
||||
|
||||
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
|
||||
# the relevant L1 queue?
|
||||
# the relevant L1 queue manually ourselves?
|
||||
# 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 (
|
||||
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})
|
||||
|
||||
elif (
|
||||
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})
|
||||
|
||||
# check for y-range re-size
|
||||
if (mx > varz['last_mx']) or (mn < varz['last_mn']):
|
||||
# check for y-autorange re-size
|
||||
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 (
|
||||
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
|
||||
and not chart._static_yrange == 'axis'
|
||||
):
|
||||
# main_vb = chart.view
|
||||
main_vb = chart._vizs[fqsn].plot.vb
|
||||
if (
|
||||
main_vb._ic is None
|
||||
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(
|
||||
# TODO: we should probably scale
|
||||
# the view margin based on the size
|
||||
|
@ -670,11 +675,10 @@ def graphics_update_cycle(
|
|||
# slap in orders outside the current
|
||||
# L1 (only) book range.
|
||||
# range_margin=0.1,
|
||||
# yrange=(mn, mx),
|
||||
yrange=yr
|
||||
)
|
||||
|
||||
# check if slow chart needs a resize
|
||||
|
||||
hist_viz = hist_chart._vizs[fqsn]
|
||||
(
|
||||
_,
|
||||
|
@ -691,7 +695,7 @@ def graphics_update_cycle(
|
|||
if hist_liv:
|
||||
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
|
||||
|
||||
# run synchronous update on all linked viz
|
||||
|
@ -767,10 +771,8 @@ def graphics_update_cycle(
|
|||
if (
|
||||
mx_vlm_in_view != varz['last_mx_vlm']
|
||||
):
|
||||
yrange = (0, mx_vlm_in_view * 1.375)
|
||||
vlm_chart.view._set_yrange(
|
||||
yrange=yrange,
|
||||
)
|
||||
vlm_yr = (0, mx_vlm_in_view * 1.375)
|
||||
vlm_chart.view._set_yrange(yrange=vlm_yr)
|
||||
profiler('`vlm_chart.view._set_yrange()`')
|
||||
# print(f'mx vlm: {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
|
||||
)
|
||||
|
||||
# (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.
|
||||
# - load this from a config.toml!
|
||||
# - allow dyanmic configuration from chart UI?
|
||||
|
@ -1129,6 +1135,8 @@ async def display_symbol_data(
|
|||
# and sub-charts for FSPs
|
||||
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
|
||||
hist_linked._symbol = flume.symbol
|
||||
|
||||
|
@ -1219,9 +1227,6 @@ async def display_symbol_data(
|
|||
# get a new color from the 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
|
||||
hist_ohlcv: ShmArray = flume.hist_shm
|
||||
|
||||
|
@ -1232,9 +1237,14 @@ async def display_symbol_data(
|
|||
name=fqsn,
|
||||
axis_title=fqsn,
|
||||
)
|
||||
hist_pi.hideAxis('left')
|
||||
# only show a singleton bottom-bottom axis by default.
|
||||
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(
|
||||
fqsn,
|
||||
hist_ohlcv,
|
||||
|
@ -1311,36 +1321,14 @@ async def display_symbol_data(
|
|||
|
||||
# XXX: if we wanted it at the bottom?
|
||||
# rt_linked.splitter.addWidget(hist_linked)
|
||||
rt_linked.focus()
|
||||
|
||||
godwidget.resize_all()
|
||||
|
||||
# add all additional symbols as overlays
|
||||
# greedily do a view range default and pane resizing
|
||||
# on startup before loading the order-mode machinery.
|
||||
for fqsn, flume in feed.flumes.items():
|
||||
|
||||
# size view to data prior to order mode init
|
||||
rt_chart.default_view()
|
||||
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
|
||||
# commented out / we ever needed it XD
|
||||
|
@ -1350,21 +1338,11 @@ async def display_symbol_data(
|
|||
# determine if auto-range adjustements should be made.
|
||||
# rt_linked.subplots.pop('volume', None)
|
||||
|
||||
# TODO: make this not so shit XD
|
||||
# close group status
|
||||
# sbar._status_groups[loading_sym_key][1]()
|
||||
|
||||
hist_chart.default_view()
|
||||
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()
|
||||
await trio.sleep(0)
|
||||
|
||||
await link_views_with_region(
|
||||
rt_chart,
|
||||
|
@ -1372,7 +1350,7 @@ async def display_symbol_data(
|
|||
flume,
|
||||
)
|
||||
|
||||
# start graphics update loop after receiving first live quote
|
||||
# start update loop task
|
||||
ln.start_soon(
|
||||
graphics_update_loop,
|
||||
ln,
|
||||
|
@ -1383,20 +1361,31 @@ async def display_symbol_data(
|
|||
vlm_charts,
|
||||
)
|
||||
|
||||
# boot order-mode
|
||||
order_ctl_symbol: str = fqsns[0]
|
||||
mode: OrderMode
|
||||
async with (
|
||||
open_order_mode(
|
||||
feed,
|
||||
godwidget,
|
||||
fqsns[-1],
|
||||
fqsns[0],
|
||||
order_mode_started
|
||||
) as 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.view.enable_auto_yrange()
|
||||
|
||||
hist_chart.default_view()
|
||||
hist_chart.view.enable_auto_yrange()
|
||||
|
||||
godwidget.resize_all()
|
||||
|
||||
await trio.sleep_forever() # let the app run.. bby
|
||||
|
|
|
@ -661,6 +661,12 @@ async def open_vlm_displays(
|
|||
# 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
|
||||
# usually data provided directly with OHLC history.
|
||||
shm = ohlcv
|
||||
|
@ -755,7 +761,7 @@ async def open_vlm_displays(
|
|||
|
||||
{ # fsp engine conf
|
||||
'func_name': 'dolla_vlm',
|
||||
'zero_on_step': True,
|
||||
'zero_on_step': False,
|
||||
'params': {
|
||||
'price_func': {
|
||||
'default_value': 'chl3',
|
||||
|
@ -769,7 +775,7 @@ async def open_vlm_displays(
|
|||
# FIXME: we should error on starting the same fsp right
|
||||
# since it might collide with existing shm.. or wait we
|
||||
# had this before??
|
||||
# dolla_vlm,
|
||||
# dolla_vlm
|
||||
|
||||
tasks_ready.append(started)
|
||||
# profiler(f'created shm for fsp actor: {display_name}')
|
||||
|
@ -786,19 +792,24 @@ async def open_vlm_displays(
|
|||
dvlm_pi = vlm_chart.overlay_plotitem(
|
||||
'dolla_vlm',
|
||||
index=0, # place axis on inside (nearest to chart)
|
||||
|
||||
axis_title=' $vlm',
|
||||
axis_side='right',
|
||||
axis_side='left',
|
||||
|
||||
axis_kwargs={
|
||||
'typical_max_str': ' 100.0 M ',
|
||||
'formatter': partial(
|
||||
humanize,
|
||||
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')
|
||||
|
||||
# all to be overlayed curve names
|
||||
fields = [
|
||||
'dolla_vlm',
|
||||
|
@ -823,12 +834,6 @@ async def open_vlm_displays(
|
|||
# add custom auto range handler
|
||||
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
|
||||
def chart_curves(
|
||||
names: list[str],
|
||||
|
@ -879,7 +884,7 @@ async def open_vlm_displays(
|
|||
flow_rates,
|
||||
{ # fsp engine conf
|
||||
'func_name': 'flow_rates',
|
||||
'zero_on_step': False,
|
||||
'zero_on_step': True,
|
||||
},
|
||||
# loglevel,
|
||||
)
|
||||
|
@ -913,8 +918,8 @@ async def open_vlm_displays(
|
|||
# TODO: dynamically update period (and thus this axis?)
|
||||
# title from user input.
|
||||
axis_title='clears',
|
||||
|
||||
axis_side='left',
|
||||
|
||||
axis_kwargs={
|
||||
'typical_max_str': ' 10.0 M ',
|
||||
'formatter': partial(
|
||||
|
|
|
@ -30,19 +30,20 @@ from ._pg_overrides import PlotItem
|
|||
|
||||
|
||||
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
|
||||
multiple fields.
|
||||
|
||||
|
||||
TODO: replace the rectangle-text part with our new ``Label`` type.
|
||||
|
||||
"""
|
||||
_x_margin = 0
|
||||
_y_margin = 0
|
||||
'''
|
||||
_x_br_offset: float = -16
|
||||
_y_txt_h_scaling: float = 2
|
||||
|
||||
# adjustment "further away from" anchor point
|
||||
_x_offset = 9
|
||||
_x_offset = 0
|
||||
_y_offset = 0
|
||||
|
||||
# fields to be displayed in the label string
|
||||
|
@ -58,12 +59,12 @@ class LevelLabel(YAxisLabel):
|
|||
chart,
|
||||
parent,
|
||||
|
||||
color: str = 'bracket',
|
||||
color: str = 'default_light',
|
||||
|
||||
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
|
||||
# such that they don't collide with the L1/L2 lines/prices
|
||||
|
@ -99,13 +100,15 @@ class LevelLabel(YAxisLabel):
|
|||
|
||||
self._h_shift = {
|
||||
'left': -1.,
|
||||
'right': 0.
|
||||
'right': 0.,
|
||||
}[orient_h]
|
||||
|
||||
self.fields = self._fields.copy()
|
||||
# ensure default format fields are in correct
|
||||
self.set_fmt_str(self._fmt_str, self.fields)
|
||||
|
||||
self.setZValue(10)
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
return self._hcolor
|
||||
|
@ -113,7 +116,10 @@ class LevelLabel(YAxisLabel):
|
|||
@color.setter
|
||||
def color(self, color: str) -> None:
|
||||
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):
|
||||
"""Tiis is a ``.sigRangeChanged()`` handler.
|
||||
|
@ -125,10 +131,11 @@ class LevelLabel(YAxisLabel):
|
|||
self,
|
||||
fields: dict = 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.
|
||||
|
||||
"""
|
||||
'''
|
||||
self.fields.update(fields)
|
||||
level = self.fields['level']
|
||||
|
||||
|
@ -175,7 +182,8 @@ class LevelLabel(YAxisLabel):
|
|||
fields: dict,
|
||||
):
|
||||
# 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()
|
||||
h, w = br.height(), br.width()
|
||||
|
@ -188,14 +196,14 @@ class LevelLabel(YAxisLabel):
|
|||
self,
|
||||
p: QtGui.QPainter,
|
||||
rect: QtCore.QRectF
|
||||
) -> None:
|
||||
p.setPen(self._pen)
|
||||
|
||||
) -> None:
|
||||
|
||||
p.setPen(self._pen)
|
||||
rect = self.rect
|
||||
|
||||
if self._orient_v == 'bottom':
|
||||
lp, rp = rect.topLeft(), rect.topRight()
|
||||
# p.drawLine(rect.topLeft(), rect.topRight())
|
||||
|
||||
elif self._orient_v == 'top':
|
||||
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:
|
||||
self._pen = pen
|
||||
self.update()
|
||||
|
@ -247,9 +260,10 @@ class L1Label(LevelLabel):
|
|||
|
||||
|
||||
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__(
|
||||
self,
|
||||
plotitem: PlotItem,
|
||||
|
@ -265,15 +279,17 @@ class L1Labels:
|
|||
'chart': plotitem,
|
||||
'parent': raxis,
|
||||
|
||||
'opacity': 1,
|
||||
'opacity': .9,
|
||||
'font_size': font_size,
|
||||
'fg_color': chart.pen_color,
|
||||
'bg_color': chart.view_color,
|
||||
'fg_color': 'default_light',
|
||||
'bg_color': chart.view_color, # normally 'papas_special'
|
||||
}
|
||||
|
||||
# TODO: add humanized source-asset
|
||||
# info format.
|
||||
fmt_str = (
|
||||
' {size:.{size_digits}f} x '
|
||||
'{level:,.{level_digits}f} '
|
||||
' {size:.{size_digits}f} u'
|
||||
# '{level:,.{level_digits}f} '
|
||||
)
|
||||
fields = {
|
||||
'level': 0,
|
||||
|
@ -286,12 +302,17 @@ class L1Labels:
|
|||
orient_v='bottom',
|
||||
**kwargs,
|
||||
)
|
||||
bid.set_fmt_str(fmt_str=fmt_str, fields=fields)
|
||||
bid.set_fmt_str(
|
||||
fmt_str='\n' + fmt_str,
|
||||
fields=fields,
|
||||
)
|
||||
bid.show()
|
||||
|
||||
ask = self.ask_label = L1Label(
|
||||
orient_v='top',
|
||||
**kwargs,
|
||||
)
|
||||
ask.set_fmt_str(fmt_str=fmt_str, fields=fields)
|
||||
ask.set_fmt_str(
|
||||
fmt_str=fmt_str,
|
||||
fields=fields)
|
||||
ask.show()
|
||||
|
|
|
@ -233,6 +233,36 @@ class Label:
|
|||
def delete(self) -> None:
|
||||
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):
|
||||
'''
|
||||
|
|
|
@ -349,7 +349,7 @@ class OrderMode:
|
|||
|
||||
'''
|
||||
if not order:
|
||||
staged = self._staged_order
|
||||
staged: Order = self._staged_order
|
||||
# apply order fields for ems
|
||||
oid = str(uuid.uuid4())
|
||||
order = staged.copy()
|
||||
|
@ -703,7 +703,6 @@ async def open_order_mode(
|
|||
|
||||
# symbol id
|
||||
symbol = chart.linked.symbol
|
||||
symkey = symbol.front_fqsn()
|
||||
|
||||
# map of per-provider account keys to position tracker instances
|
||||
trackers: dict[str, PositionTracker] = {}
|
||||
|
@ -864,7 +863,7 @@ async def open_order_mode(
|
|||
# the expected symbol key in its positions msg.
|
||||
for (broker, acctid), msgs in position_msgs.items():
|
||||
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(
|
||||
mode,
|
||||
book,
|
||||
|
|
Loading…
Reference in New Issue