3rdz the charm: log-linearize minor y-ranges to a major
In very close manner to the original (gut instinct) attempt, this properly (y-axis-vertically) aligns and scales overlaid curves according to what we are calling a "log-linearized y-range multi-plot" B) The basic idea is that a simple returns measure (eg. `R = (p1 - p0) / p0`) applied to all curves gives a constant output `R` no matter the price co-domain in use and thus gives a constant returns over all assets in view styled scaling; a intuitive visual of returns correlation. The reference point is for now the left-most point in view (or highest common index available to all curves), though we can make this a parameter based on user needs. A slew of debug `print()`s are left in for now until we iron out the remaining edge cases to do with re-scaling a major (dispersion) curve based on a minor now requiring a larger log-linear y-range from that previous major' range.multichartz
parent
84bd4e99ef
commit
0cdb065222
|
@ -20,6 +20,7 @@ Chart view box primitives
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
import math
|
||||||
import time
|
import time
|
||||||
from typing import (
|
from typing import (
|
||||||
Optional,
|
Optional,
|
||||||
|
@ -988,6 +989,13 @@ class ChartView(ViewBox):
|
||||||
|
|
||||||
profiler(f'<{chart_name}>.interact_graphics_cycle({name})')
|
profiler(f'<{chart_name}>.interact_graphics_cycle({name})')
|
||||||
|
|
||||||
|
# if no overlays, set lone chart's yrange and short circuit
|
||||||
|
if len(mxmn_groups) < 2:
|
||||||
|
viz.plot.vb._set_yrange(
|
||||||
|
yrange=yrange,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# proportional group auto-scaling per overlay set.
|
# proportional group auto-scaling per overlay set.
|
||||||
# -> loop through overlays on each multi-chart widget
|
# -> loop through overlays on each multi-chart widget
|
||||||
# and scale all y-ranges based on autoscale config.
|
# and scale all y-ranges based on autoscale config.
|
||||||
|
@ -1008,6 +1016,7 @@ class ChartView(ViewBox):
|
||||||
float, # y max
|
float, # y max
|
||||||
float, # y median
|
float, # y median
|
||||||
slice, # in-view array slice
|
slice, # in-view array slice
|
||||||
|
np.ndarray, # in-view array
|
||||||
],
|
],
|
||||||
] = {}
|
] = {}
|
||||||
max_start: float = 0
|
max_start: float = 0
|
||||||
|
@ -1020,7 +1029,6 @@ class ChartView(ViewBox):
|
||||||
(ymn, ymx),
|
(ymn, ymx),
|
||||||
) = out
|
) = out
|
||||||
|
|
||||||
|
|
||||||
x_start = ixrng[0]
|
x_start = ixrng[0]
|
||||||
max_start = max(x_start, max_start)
|
max_start = max(x_start, max_start)
|
||||||
|
|
||||||
|
@ -1032,27 +1040,28 @@ class ChartView(ViewBox):
|
||||||
# row_stop = arr[read_slc.stop - 1]
|
# row_stop = arr[read_slc.stop - 1]
|
||||||
|
|
||||||
if viz.is_ohlc:
|
if viz.is_ohlc:
|
||||||
y_median = np.median(in_view['close'])
|
y_med = np.median(in_view['close'])
|
||||||
y_start = row_start['open']
|
y_start = row_start['open']
|
||||||
else:
|
else:
|
||||||
y_median = np.median(in_view[viz.name])
|
y_med = np.median(in_view[viz.name])
|
||||||
y_start = row_start[viz.name]
|
y_start = row_start[viz.name]
|
||||||
# y_stop = row_stop[viz.name]
|
# y_stop = row_stop[viz.name]
|
||||||
|
|
||||||
|
print(
|
||||||
|
f'{viz.name} -> (x_start: {x_start}, y_start: {y_start}\n'
|
||||||
|
)
|
||||||
start_datums[viz.plot.vb] = (
|
start_datums[viz.plot.vb] = (
|
||||||
viz,
|
viz,
|
||||||
y_start,
|
y_start,
|
||||||
ymn,
|
ymn,
|
||||||
ymx,
|
ymx,
|
||||||
y_median,
|
y_med,
|
||||||
read_slc,
|
read_slc,
|
||||||
|
in_view,
|
||||||
)
|
)
|
||||||
|
|
||||||
# compute directional (up/down) y-range % swing/dispersion
|
# find curve with max dispersion
|
||||||
y_ref = y_median
|
disp = abs(ymx - ymn) / y_med
|
||||||
up_rng = (ymx - y_ref) / y_ref
|
|
||||||
down_rng = (ymn - y_ref) / y_ref
|
|
||||||
disp = abs(ymx - ymn) / y_ref
|
|
||||||
|
|
||||||
# track the "major" curve as the curve with most
|
# track the "major" curve as the curve with most
|
||||||
# dispersion.
|
# dispersion.
|
||||||
|
@ -1062,17 +1071,26 @@ class ChartView(ViewBox):
|
||||||
major_mn = ymn
|
major_mn = ymn
|
||||||
major_mx = ymx
|
major_mx = ymx
|
||||||
|
|
||||||
|
# compute directional (up/down) y-range % swing/dispersion
|
||||||
|
y_ref = y_med
|
||||||
|
up_rng = (ymx - y_ref) / y_ref
|
||||||
|
down_rng = (ymn - y_ref) / y_ref
|
||||||
|
|
||||||
mx_up_rng = max(mx_up_rng, up_rng)
|
mx_up_rng = max(mx_up_rng, up_rng)
|
||||||
mn_down_rng = min(mn_down_rng, down_rng)
|
mn_down_rng = min(mn_down_rng, down_rng)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
|
'####################\n'
|
||||||
f'{viz.name}@{chart_name} group mxmn calc\n'
|
f'{viz.name}@{chart_name} group mxmn calc\n'
|
||||||
|
'--------------------\nn'
|
||||||
f'y_start: {y_start}\n'
|
f'y_start: {y_start}\n'
|
||||||
f'ymn: {ymn}\n'
|
f'ymn: {ymn}\n'
|
||||||
f'ymx: {ymx}\n'
|
f'ymx: {ymx}\n'
|
||||||
f'mx_disp: {mx_disp}\n'
|
f'mx_disp: {mx_disp}\n'
|
||||||
|
'####################\n'
|
||||||
f'up %: {up_rng * 100}\n'
|
f'up %: {up_rng * 100}\n'
|
||||||
f'down %: {down_rng * 100}\n'
|
f'down %: {down_rng * 100}\n'
|
||||||
|
'####################\n'
|
||||||
f'mx up %: {mx_up_rng * 100}\n'
|
f'mx up %: {mx_up_rng * 100}\n'
|
||||||
f'mn down %: {mn_down_rng * 100}\n'
|
f'mn down %: {mn_down_rng * 100}\n'
|
||||||
)
|
)
|
||||||
|
@ -1084,109 +1102,114 @@ class ChartView(ViewBox):
|
||||||
y_start,
|
y_start,
|
||||||
y_min,
|
y_min,
|
||||||
y_max,
|
y_max,
|
||||||
y_median,
|
y_med,
|
||||||
read_slc,
|
read_slc,
|
||||||
|
minor_in_view,
|
||||||
)
|
)
|
||||||
) in start_datums.items():
|
) in start_datums.items():
|
||||||
|
|
||||||
# TODO: just use y_min / y_max directly for the major
|
# we use the ymn/mx verbatim from the major curve
|
||||||
# `Viz` instead of the below calc since it should be the
|
# (i.e. the curve measured to have the highest
|
||||||
# same output..
|
# dispersion in view).
|
||||||
symn = y_median * (1 + mn_down_rng)
|
if viz is major_viz:
|
||||||
symx = y_median * (1 + mx_up_rng)
|
ymn = y_min
|
||||||
|
ymx = y_max
|
||||||
if not (viz is major_viz):
|
|
||||||
|
|
||||||
# compute dispersion normed offsets at the start
|
|
||||||
# index of the smaller dispersion curve.
|
|
||||||
maj_viz_arr = major_viz.shm.array
|
|
||||||
|
|
||||||
|
else:
|
||||||
key = 'open' if viz.is_ohlc else viz.name
|
key = 'open' if viz.is_ohlc else viz.name
|
||||||
|
|
||||||
# handle case where major (dispersion) curve has
|
# handle case where major and minor curve(s) have
|
||||||
# a smaller domain then minor one(s).
|
# a disjoint x-domain (one curve is smaller in
|
||||||
istart = read_slc.start
|
# length then the other):
|
||||||
if read_slc.start > maj_viz_arr.size:
|
# - find the highest (time) index common to both
|
||||||
istart = 0
|
# curves.
|
||||||
|
# - slice out the first "intersecting" y-value from
|
||||||
|
# both curves for use in log-linear scaling such
|
||||||
|
# that the intersecting y-value is used as the
|
||||||
|
# reference point for scaling minor curve's
|
||||||
|
# y-range based on the major curves y-range.
|
||||||
|
abs_ifirst = minor_in_view[0]['index']
|
||||||
|
mshm = major_viz.shm
|
||||||
|
abs_i_start = max(
|
||||||
|
abs_ifirst,
|
||||||
|
mshm.array['index'][0],
|
||||||
|
)
|
||||||
|
# get intersection point y-values for both curves
|
||||||
|
y_maj_intersect = mshm._array[abs_i_start][key]
|
||||||
|
y_min_intersect = minor_in_view[abs_i_start - abs_ifirst]
|
||||||
|
|
||||||
maj_start_y = maj_viz_arr[istart][key]
|
# TODO: probably write this as a compile cpython or
|
||||||
|
# numba func.
|
||||||
|
|
||||||
maj_start_offset = maj_start_y / major_mn
|
# compute directional (up/down) y-range
|
||||||
maj_max_offset = major_mx / major_mn
|
# % swing/dispersion starting at the reference index
|
||||||
|
# determined by the above indexing arithmetic.
|
||||||
|
y_ref = y_maj_intersect
|
||||||
|
assert y_ref
|
||||||
|
r_up = (major_mx - y_ref) / y_ref
|
||||||
|
r_down = (major_mn - y_ref) / y_ref
|
||||||
|
ymn = y_start * (1 + r_down)
|
||||||
|
ymx = y_start * (1 + r_up)
|
||||||
|
|
||||||
# XXX: or this?
|
# XXX: handle out of view cases where minor curve
|
||||||
# maj_start_offset = (maj_start_y - major_mn) / major_mn
|
# now is outside the range of the major curve. in
|
||||||
# maj_max_offset = (major_mx - maj_start_y) / major_mn
|
# this case we then re-scale the major curve to
|
||||||
|
# include the range missing now enforced by the
|
||||||
|
# minor (now new major for this *side*). Note this
|
||||||
|
# is side (up/down) specific.
|
||||||
|
new_maj_mxmn: None | tuple[float, float] = None
|
||||||
|
if y_max > ymx:
|
||||||
|
y_ref = y_min_intersect[key]
|
||||||
|
r_up_minor = (y_max - y_ref) / y_ref
|
||||||
|
new_maj_ymx = y_maj_intersect * (1 + r_up_minor)
|
||||||
|
new_maj_mxmn = (major_mn, new_maj_ymx)
|
||||||
|
ymx = y_max
|
||||||
|
|
||||||
# XXX: or this?
|
print(
|
||||||
# major_disp_offset = (
|
f'{view.name} OUT OF RANGE:\n'
|
||||||
# (maj_viz_arr[istart][key] - major_mn)
|
f'MAJOR is {major_viz.name}\n'
|
||||||
# /
|
f'y_max:{y_max} > ymx:{ymx}\n'
|
||||||
# major_mn
|
)
|
||||||
# )
|
|
||||||
# minor_disp_offset_mn = (
|
|
||||||
# (y_start - y_min)
|
|
||||||
# /
|
|
||||||
# y_min
|
|
||||||
# )
|
|
||||||
# minor_disp_offset_mx = (
|
|
||||||
# (ymx - y_start)
|
|
||||||
# /
|
|
||||||
# y_min
|
|
||||||
|
|
||||||
# normed_disp_ratio = minor_disp_offset - major_disp_offset
|
if y_min < ymn:
|
||||||
|
y_ref = y_min_intersect[key]
|
||||||
|
r_down_minor = (y_min - y_ref) / y_ref
|
||||||
|
new_maj_ymn = y_maj_intersect * (1 + r_down_minor)
|
||||||
|
new_maj_mxmn = (
|
||||||
|
new_maj_ymn,
|
||||||
|
new_maj_ymx[1] if new_maj_mxmn else major_mx
|
||||||
|
)
|
||||||
|
ymn = y_min
|
||||||
|
|
||||||
|
print(
|
||||||
|
f'{view.name} OUT OF RANGE:\n'
|
||||||
|
f'MAJOR is {major_viz.name}\n'
|
||||||
|
f'y_min:{y_min} < ymn:{ymn}\n'
|
||||||
|
)
|
||||||
|
|
||||||
# adjust mxmn range to align curve start point in
|
if new_maj_mxmn:
|
||||||
# the minor overlay with the major one.
|
major_viz.plot.vb._set_yrange(
|
||||||
|
yrange=new_maj_mxmn,
|
||||||
# symn = symn * (1 + normed_disp_ratio)
|
)
|
||||||
# symx = symx * (1 + normed_disp_ratio)
|
|
||||||
|
|
||||||
# symn = symn - (symn * normed_disp_ratio)
|
|
||||||
# symx = symx - (symn * normed_disp_ratio)
|
|
||||||
|
|
||||||
# symn = y_min * maj_start_offset
|
|
||||||
# symx = y_min * maj_max_offset
|
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f'{view.name} APPLY group mxmn\n'
|
f'{view.name} APPLY group mxmn\n'
|
||||||
# f'disp offset ratio diff %: {normed_disp_ratio}\n'
|
|
||||||
# f'major disp offset %: {major_disp_offset}\n'
|
|
||||||
# f'minor disp offset %: {minor_disp_offset}\n'
|
|
||||||
f'y_start: {y_start}\n'
|
f'y_start: {y_start}\n'
|
||||||
f'mn_down_rng: {mn_down_rng * 100}\n'
|
f'mn_down_rng: {mn_down_rng * 100}\n'
|
||||||
f'mx_up_rng: {mx_up_rng * 100}\n'
|
f'mx_up_rng: {mx_up_rng * 100}\n'
|
||||||
f'scaled ymn: {symn}\n'
|
f'scaled ymn: {ymn}\n'
|
||||||
f'scaled ymx: {symx}\n'
|
f'scaled ymx: {ymx}\n'
|
||||||
f'scaled mx_disp: {mx_disp}\n'
|
f'scaled mx_disp: {mx_disp}\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
math.isinf(ymx)
|
||||||
|
or math.isinf(ymn)
|
||||||
|
):
|
||||||
|
breakpoint()
|
||||||
|
|
||||||
view._set_yrange(
|
view._set_yrange(
|
||||||
yrange=(symn, symx),
|
yrange=(ymn, ymx),
|
||||||
# range_margin=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# if 'mnq' in viz.name:
|
|
||||||
# print(
|
|
||||||
# f'AUTO-Y-RANGING: {viz.name}\n'
|
|
||||||
# f'i_read_range: {i_read_range}\n'
|
|
||||||
# f'ixrng: {ixrng}\n'
|
|
||||||
# f'yrange: {yrange}\n'
|
|
||||||
# )
|
|
||||||
# (
|
|
||||||
# view_xrange,
|
|
||||||
# view_yrange,
|
|
||||||
# ) = viz.plot.vb.viewRange()
|
|
||||||
# view_ymx = view_yrange[1]
|
|
||||||
# print(
|
|
||||||
# f'{viz.name}@{chart_name}\n'
|
|
||||||
# f' xRange -> {view_xrange}\n'
|
|
||||||
# f' yRange -> {view_yrange}\n'
|
|
||||||
# f' view y-max -> {view_ymx}\n'
|
|
||||||
# )
|
|
||||||
|
|
||||||
# if view_ymx != symx:
|
|
||||||
# breakpoint()
|
|
||||||
|
|
||||||
profiler.finish()
|
profiler.finish()
|
||||||
|
|
Loading…
Reference in New Issue