use serde::Serialize;
use crate::stats::{mean, std_dev};
#[derive(Clone, Debug, Serialize)]
pub struct OosDecayReport {
pub window_metrics: Vec<f64>,
pub in_sample: f64,
pub out_of_sample: f64,
pub retention: f64,
pub monotone_decay: bool,
}
fn window_metric(returns: &[f64]) -> f64 {
let s = std_dev(returns);
if s > 1e-12 {
mean(returns) / s
} else {
mean(returns)
}
}
pub fn oos_decay(windows: &[Vec<f64>]) -> OosDecayReport {
let window_metrics: Vec<f64> = windows.iter().map(|w| window_metric(w)).collect();
let n = window_metrics.len();
if n == 0 {
return OosDecayReport {
window_metrics,
in_sample: 0.0,
out_of_sample: 0.0,
retention: 1.0,
monotone_decay: false,
};
}
let in_sample = window_metrics[0];
let out_of_sample = if n > 1 {
mean(&window_metrics[1..])
} else {
in_sample
};
let retention = if in_sample.abs() > 1e-12 {
out_of_sample / in_sample
} else {
1.0
};
let monotone_decay = n >= 2 && window_metrics.windows(2).all(|w| w[1] < w[0]);
OosDecayReport {
window_metrics,
in_sample,
out_of_sample,
retention,
monotone_decay,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decaying_edge_has_low_retention_and_monotone_flag() {
let windows = vec![vec![0.02; 50], vec![0.01; 50], vec![0.005; 50]];
let r = oos_decay(&windows);
assert!(r.retention < 1.0, "retention={}", r.retention);
assert!(r.monotone_decay);
}
#[test]
fn stable_edge_retains_and_is_not_flagged() {
let windows = vec![vec![0.01; 50], vec![0.01; 50]];
let r = oos_decay(&windows);
assert!((r.retention - 1.0).abs() < 1e-9);
assert!(!r.monotone_decay, "equal windows are not decay");
}
#[test]
fn improving_edge_retains_above_one() {
let windows = vec![vec![0.01; 50], vec![0.02; 50]];
let r = oos_decay(&windows);
assert!(r.retention > 1.0);
assert!(!r.monotone_decay);
}
}