Merge pull request #452 from pikers/l1_compaction

Compact L1 labels
backend_spec
goodboy 2023-02-13 11:21:26 -05:00 committed by GitHub
commit 42d2f9e461
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 225 additions and 156 deletions

View File

@ -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

View File

@ -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,
) )

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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()

View File

@ -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):
''' '''

View File

@ -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(

View File

@ -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,