fix(chart): mean-decimate the bounded exposure series instead of a min/max band
The serve-time decimation (#108) reduced every series by per-bucket min/max. That is the right envelope for a cumulative equity curve but wrong for the bounded exposure stream (C10, f64 ∈ [-1,+1]): over a multi-year window each ~hundreds-of-bars bucket straddles sign flips, so min/max is ±1 in nearly every bucket and the exposure collapses to a solid -1..+1 band that reads as per-point oscillation — even for a calm strategy (a 50/200 member flips only 0.81% of bars yet still bands out). Make decimation tap-aware (RED-first, #111): a new `ReduceKind {MinMax, Mean}` on each Series, set by the chart builders via `reduce_for_tap` (exposure -> Mean, else MinMax). `decimate` honours it — a Mean series emits the per-bucket mean (its net/duty-cycle level) in both spine slots, a MinMax series keeps min/max. The shared spine, the equity rendering, and the page payload are unchanged (reduce is server-side only, `#[serde(skip)]`). Verified on real GER40 1y M1: the served exposure series goes from a ±1 band to a smooth net-level line in [-0.38, +0.34]. Rendering the min/max envelope honestly (range bars / OHLC, vs today's polyline) is the deferred other half — filed as #112. Verified: cargo test --workspace = 447 passed / 0 failed (incl. the RED-then-GREEN decimate_mean_reduces_a_bipolar_series_to_its_bucket_level); clippy -D warnings clean. closes #111 refs #112
This commit is contained in:
@@ -123,14 +123,36 @@ pub enum ChartMode {
|
||||
Panels,
|
||||
}
|
||||
|
||||
/// How `decimate` reduces a series within each bucket (#111). An *envelope* series
|
||||
/// (a cumulative equity curve) keeps its min+max so drawdown spikes survive; a
|
||||
/// *level* series (the bounded exposure, f64 ∈ [-1,+1]) takes the per-bucket mean,
|
||||
/// so a high-flip signal shows its net/duty-cycle level instead of collapsing to a
|
||||
/// -1..+1 band (every bucket of a multi-year exposure straddles many flips, so
|
||||
/// min/max would be ±1 everywhere). Server-side only — never serialized into the
|
||||
/// page (the viewer reads decimated points, not how they were reduced). Rendering
|
||||
/// min/max as range bars / OHLC is a separate follow-up; this only fixes the
|
||||
/// reduction.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum ReduceKind {
|
||||
/// Per-bucket min and max — the envelope (equity, unbounded cumulative).
|
||||
#[default]
|
||||
MinMax,
|
||||
/// Per-bucket mean — the net level (exposure, bounded ∈ [-1,+1]).
|
||||
Mean,
|
||||
}
|
||||
|
||||
/// One chart series: a name, its own y-scale id (so disparate magnitudes stay
|
||||
/// legible), and one `Option<f64>` per shared-x slot (`null` = a gap where the tap
|
||||
/// did not fire at that timestamp).
|
||||
/// legible), one `Option<f64>` per shared-x slot (`null` = a gap where the tap did
|
||||
/// not fire at that timestamp), and how `decimate` reduces it per bucket.
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct Series {
|
||||
pub name: String,
|
||||
pub y_scale_id: String,
|
||||
pub points: Vec<Option<f64>>,
|
||||
/// Per-bucket reduction for `decimate` (#111); server-side only, not part of the
|
||||
/// served payload.
|
||||
#[serde(skip)]
|
||||
pub reduce: ReduceKind,
|
||||
}
|
||||
|
||||
/// Run-context for the page header (#102): serialized into `window.AURA_TRACES.meta`
|
||||
@@ -253,8 +275,8 @@ mod tests {
|
||||
ChartData {
|
||||
xs: vec![1, 2, 3],
|
||||
series: vec![
|
||||
Series { name: "equity".into(), y_scale_id: "y_0".into(), points: vec![Some(0.0), Some(0.001), Some(0.004)] },
|
||||
Series { name: "exposure".into(), y_scale_id: "y_1".into(), points: vec![Some(1.0), Some(1.0), Some(-1.0)] },
|
||||
Series { name: "equity".into(), y_scale_id: "y_0".into(), points: vec![Some(0.0), Some(0.001), Some(0.004)], reduce: ReduceKind::MinMax },
|
||||
Series { name: "exposure".into(), y_scale_id: "y_1".into(), points: vec![Some(1.0), Some(1.0), Some(-1.0)], reduce: ReduceKind::Mean },
|
||||
],
|
||||
meta: ChartMeta {
|
||||
kind: "run".into(),
|
||||
|
||||
Reference in New Issue
Block a user