#[inline]
pub fn lag(values: &[f64], offset: usize) -> Vec<f64> {
let n = values.len();
let mut out = vec![f64::NAN; n];
if offset < n {
out[offset..n].copy_from_slice(&values[..(n - offset)]);
}
out
}
pub fn diff(values: &[f64], order: usize) -> Vec<f64> {
if order == 0 {
return values.to_vec();
}
let mut prev = values.to_vec();
for _ in 0..order {
let mut cur = vec![f64::NAN; prev.len()];
for i in 1..prev.len() {
cur[i] = prev[i] - prev[i - 1];
}
prev = cur;
}
prev
}
pub fn pct_change(values: &[f64]) -> Vec<f64> {
let n = values.len();
let mut out = vec![f64::NAN; n];
for i in 1..n {
let prev = values[i - 1];
out[i] = if prev.abs() < f64::EPSILON {
f64::NAN
} else {
(values[i] - prev) / prev
};
}
out
}
#[derive(Debug, Default, Clone, Copy)]
struct Moments {
n: usize,
mean: f64,
m2: f64,
}
impl Moments {
fn add(&mut self, x: f64) {
if !x.is_finite() {
return;
}
self.n += 1;
let d = x - self.mean;
self.mean += d / self.n as f64;
self.m2 += d * (x - self.mean);
}
fn remove(&mut self, x: f64) {
if !x.is_finite() {
return;
}
if self.n <= 1 {
*self = Self::default();
return;
}
let prev_mean = self.mean;
self.n -= 1;
self.mean -= (x - prev_mean) / self.n as f64;
self.m2 -= (x - prev_mean) * (x - self.mean);
if self.m2 < 0.0 {
self.m2 = 0.0;
}
}
fn from_slice(xs: &[f64]) -> Self {
let mut m = Self::default();
for &x in xs {
m.add(x);
}
m
}
fn sample_std(&self) -> f64 {
if self.n < 2 {
return f64::NAN;
}
(self.m2 / (self.n - 1) as f64).sqrt()
}
}
const RECOMPUTE_INTERVAL: usize = 1024;
#[must_use]
pub fn rolling_mean(values: &[f64], window: usize) -> Vec<f64> {
let n = values.len();
if window == 0 || window > n {
return vec![f64::NAN; n];
}
let mut out = vec![f64::NAN; n];
let mut m = Moments::from_slice(&values[..window]);
out[window - 1] = if m.n == 0 { f64::NAN } else { m.mean };
for i in window..n {
m.remove(values[i - window]);
m.add(values[i]);
if (i - window + 1).is_multiple_of(RECOMPUTE_INTERVAL) {
m = Moments::from_slice(&values[i + 1 - window..=i]);
}
out[i] = if m.n == 0 { f64::NAN } else { m.mean };
}
out
}
#[must_use]
pub fn rolling_std(values: &[f64], window: usize) -> Vec<f64> {
let n = values.len();
if window == 0 || window > n {
return vec![f64::NAN; n];
}
let mut out = vec![f64::NAN; n];
let mut m = Moments::from_slice(&values[..window]);
out[window - 1] = m.sample_std();
for i in window..n {
m.remove(values[i - window]);
m.add(values[i]);
if (i - window + 1).is_multiple_of(RECOMPUTE_INTERVAL) {
m = Moments::from_slice(&values[i + 1 - window..=i]);
}
out[i] = m.sample_std();
}
out
}
#[derive(Debug, Default, Clone, Copy)]
struct CoMoments {
n: usize,
mx: f64,
my: f64,
m2x: f64,
m2y: f64,
cxy: f64,
}
impl CoMoments {
fn add(&mut self, x: f64, y: f64) {
if !x.is_finite() || !y.is_finite() {
return;
}
self.n += 1;
let nf = self.n as f64;
let dx = x - self.mx;
let dy = y - self.my;
self.mx += dx / nf;
self.my += dy / nf;
self.m2x += dx * (x - self.mx);
self.m2y += dy * (y - self.my);
self.cxy += dx * (y - self.my);
}
fn remove(&mut self, x: f64, y: f64) {
if !x.is_finite() || !y.is_finite() {
return;
}
if self.n <= 1 {
*self = Self::default();
return;
}
let (px, py) = (self.mx, self.my);
self.n -= 1;
let nf = self.n as f64;
self.mx -= (x - px) / nf;
self.my -= (y - py) / nf;
self.m2x -= (x - px) * (x - self.mx);
self.m2y -= (y - py) * (y - self.my);
self.cxy -= (x - px) * (y - self.my);
if self.m2x < 0.0 {
self.m2x = 0.0;
}
if self.m2y < 0.0 {
self.m2y = 0.0;
}
}
fn from_slices(xs: &[f64], ys: &[f64]) -> Self {
let mut c = Self::default();
for (&x, &y) in xs.iter().zip(ys) {
c.add(x, y);
}
c
}
fn correlation(&self) -> f64 {
if self.n < 2 {
return f64::NAN;
}
let denom = (self.m2x * self.m2y).sqrt();
if denom < f64::EPSILON {
return f64::NAN;
}
self.cxy / denom
}
}
#[must_use]
pub fn rolling_corr(a: &[f64], b: &[f64], window: usize) -> Vec<f64> {
let n = a.len().min(b.len());
if window < 2 || window > n {
return vec![f64::NAN; n];
}
let mut out = vec![f64::NAN; n];
let mut c = CoMoments::from_slices(&a[..window], &b[..window]);
out[window - 1] = c.correlation();
for i in window..n {
c.remove(a[i - window], b[i - window]);
c.add(a[i], b[i]);
if (i - window + 1).is_multiple_of(RECOMPUTE_INTERVAL) {
c = CoMoments::from_slices(&a[i + 1 - window..=i], &b[i + 1 - window..=i]);
}
out[i] = c.correlation();
}
out
}
#[must_use]
pub fn zscore(values: &[f64]) -> Vec<f64> {
let n = values.len();
let m = Moments::from_slice(values);
let std = m.sample_std();
if !std.is_finite() || std < f64::EPSILON {
return vec![f64::NAN; n];
}
values
.iter()
.map(|v| {
if v.is_finite() {
(v - m.mean) / std
} else {
f64::NAN
}
})
.collect()
}
#[must_use]
pub fn ewm(values: &[f64], alpha: f64) -> Vec<f64> {
let n = values.len();
let alpha = alpha.clamp(f64::EPSILON, 1.0);
let mut out = vec![f64::NAN; n];
let mut state: Option<f64> = None;
for i in 0..n {
let v = values[i];
if v.is_finite() {
state = Some(match state {
None => v,
Some(prev) => alpha * v + (1.0 - alpha) * prev,
});
}
if let Some(cur) = state {
out[i] = cur;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64, eps: f64) -> bool {
if a.is_nan() && b.is_nan() {
return true;
}
(a - b).abs() < eps
}
#[test]
fn test_lag_basic() {
let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let l = lag(&v, 2);
assert!(l[0].is_nan());
assert!(l[1].is_nan());
assert_eq!(l[2], 1.0);
assert_eq!(l[3], 2.0);
assert_eq!(l[4], 3.0);
}
#[test]
fn test_lag_zero_offset() {
let v = vec![10.0, 20.0];
let l = lag(&v, 0);
assert_eq!(l, v);
}
#[test]
fn test_diff_order_1() {
let v = vec![1.0, 3.0, 6.0, 10.0];
let d = diff(&v, 1);
assert!(d[0].is_nan());
assert_eq!(d[1], 2.0);
assert_eq!(d[2], 3.0);
assert_eq!(d[3], 4.0);
}
#[test]
fn test_diff_order_2() {
let v = vec![1.0, 3.0, 6.0, 10.0, 15.0];
let d = diff(&v, 2);
assert!(d[0].is_nan());
assert!(d[1].is_nan());
assert_eq!(d[2], 1.0); assert_eq!(d[3], 1.0);
assert_eq!(d[4], 1.0);
}
#[test]
fn test_diff_order_0() {
let v = vec![1.0, 2.0, 3.0];
assert_eq!(diff(&v, 0), v);
}
#[test]
fn test_pct_change() {
let v = vec![100.0, 110.0, 99.0];
let p = pct_change(&v);
assert!(p[0].is_nan());
assert!(approx(p[1], 0.1, 1e-9));
assert!(approx(p[2], -0.1, 1e-9));
}
#[test]
fn test_pct_change_zero_denominator() {
let v = vec![0.0, 5.0];
let p = pct_change(&v);
assert!(p[1].is_nan());
}
#[test]
fn test_rolling_std() {
let v = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
let r = rolling_std(&v, 3);
assert!(r[0].is_nan());
assert!(r[1].is_nan());
let expected: f64 = ((2.0_f64 - 10.0 / 3.0).powi(2)
+ (4.0_f64 - 10.0 / 3.0).powi(2)
+ (4.0_f64 - 10.0 / 3.0).powi(2))
/ 2.0;
assert!(approx(r[2], expected.sqrt(), 1e-9));
}
#[test]
fn test_rolling_std_window_one() {
let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let r = rolling_std(&v, 1);
assert_eq!(r.len(), v.len());
for val in &r {
assert!(
val.is_nan(),
"rolling_std with window=1 is undefined, got {val}"
);
}
}
#[test]
fn test_rolling_corr_perfect() {
let a = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let b = vec![2.0, 4.0, 6.0, 8.0, 10.0];
let r = rolling_corr(&a, &b, 3);
assert!(r[0].is_nan());
assert!(r[1].is_nan());
for &v in &r[2..] {
assert!(approx(v, 1.0, 1e-9));
}
}
#[test]
fn test_rolling_corr_negative() {
let a = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let b = vec![5.0, 4.0, 3.0, 2.0, 1.0];
let r = rolling_corr(&a, &b, 3);
for &v in &r[2..] {
assert!(approx(v, -1.0, 1e-9));
}
}
#[test]
fn test_zscore() {
let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let z = zscore(&v);
assert_eq!(z.len(), 5);
let std_dev = 2.5_f64.sqrt();
assert!(approx(z[0], -2.0 / std_dev, 1e-9));
assert!(approx(z[2], 0.0, 1e-9));
assert!(approx(z[4], 2.0 / std_dev, 1e-9));
}
#[test]
fn test_zscore_constant() {
let v = vec![5.0, 5.0, 5.0];
let z = zscore(&v);
assert!(z.iter().all(|v| v.is_nan()));
}
#[test]
fn test_ewm() {
let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let e = ewm(&v, 0.5);
assert_eq!(e[0], 1.0);
assert!(approx(e[1], 1.5, 1e-9)); assert!(approx(e[2], 2.25, 1e-9)); }
#[test]
fn test_ewm_alpha_one() {
let v = vec![1.0, 10.0, 100.0];
let e = ewm(&v, 1.0);
assert_eq!(e, v);
}
#[test]
fn test_ewm_empty() {
let e = ewm(&[], 0.5);
assert!(e.is_empty());
}
}