use super::linalg::ols;
pub fn newey_west_lrv(series: &[f64], bandwidth: Option<usize>) -> f64 {
let n = series.len();
if n < 2 {
return 0.0;
}
let mean = series.iter().sum::<f64>() / n as f64;
let dev: Vec<f64> = series.iter().map(|v| v - mean).collect();
let l = bandwidth.unwrap_or_else(|| {
let b = 4.0 * (n as f64 / 100.0).powf(2.0 / 9.0);
(b.floor() as usize).max(1).min(n - 1)
});
let gamma0 = dev.iter().map(|d| d * d).sum::<f64>() / n as f64;
let mut lrv = gamma0;
for lag in 1..=l.min(n - 1) {
let mut g = 0.0;
for t in lag..n {
g += dev[t] * dev[t - lag];
}
g /= n as f64;
let w = 1.0 - lag as f64 / (l as f64 + 1.0);
lrv += 2.0 * w * g;
}
lrv.max(0.0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdfTrend {
None,
Constant,
ConstantTrend,
}
#[derive(Debug, Clone, Copy)]
pub struct AdfResult {
pub t_stat: f64,
pub gamma: f64,
pub lags: usize,
pub n_eff: usize,
}
pub fn adf_regression(y: &[f64], trend: AdfTrend, lags: Option<usize>) -> Option<AdfResult> {
let n = y.len();
if n < 8 {
return None;
}
let dy: Vec<f64> = (1..n).map(|t| y[t] - y[t - 1]).collect();
let m = dy.len();
let p = lags.unwrap_or_else(|| {
let s = 12.0 * (n as f64 / 100.0).powf(0.25);
(s.floor() as usize).min(m / 3)
});
if m <= p + 2 {
return None;
}
let det = match trend {
AdfTrend::None => 0,
AdfTrend::Constant => 1,
AdfTrend::ConstantTrend => 2,
};
let n_cols = det + 1 + p; let start = p;
let rows = m - start;
if rows <= n_cols {
return None;
}
let mut xm = Vec::with_capacity(rows * n_cols);
let mut yv = Vec::with_capacity(rows);
let gamma_col = det; for t in start..m {
if det >= 1 {
xm.push(1.0);
}
if det == 2 {
xm.push(t as f64);
}
xm.push(y[t]);
for i in 1..=p {
xm.push(dy[t - i]);
}
yv.push(dy[t]);
}
let beta = ols(&xm, &yv, rows, n_cols)?;
let gamma = beta[gamma_col];
let mut sse = 0.0;
for r in 0..rows {
let row = &xm[r * n_cols..(r + 1) * n_cols];
let yhat: f64 = row.iter().zip(beta.iter()).map(|(a, b)| a * b).sum();
let e = yv[r] - yhat;
sse += e * e;
}
let dof = (rows - n_cols) as f64;
if dof <= 0.0 {
return None;
}
let sigma2 = sse / dof;
let xtx_inv_diag = xtx_inverse_diag(&xm, rows, n_cols, gamma_col)?;
let se = (sigma2 * xtx_inv_diag).sqrt();
if !se.is_finite() || se == 0.0 {
return None;
}
Some(AdfResult {
t_stat: gamma / se,
gamma,
lags: p,
n_eff: rows,
})
}
fn xtx_inverse_diag(x: &[f64], n_rows: usize, n_cols: usize, col: usize) -> Option<f64> {
let mut xtx = vec![0.0_f64; n_cols * n_cols];
for r in 0..n_rows {
let row = &x[r * n_cols..(r + 1) * n_cols];
for i in 0..n_cols {
for j in 0..n_cols {
xtx[i * n_cols + j] += row[i] * row[j];
}
}
}
let mut e = vec![0.0_f64; n_cols];
e[col] = 1.0;
let z = super::linalg::solve_linear_system(&mut xtx, &mut e, n_cols)?;
Some(z[col])
}
pub fn adf_critical_values(trend: AdfTrend) -> (f64, f64, f64) {
match trend {
AdfTrend::None => (-2.5658, -1.9393, -1.6156),
AdfTrend::Constant => (-3.4336, -2.8621, -2.5671),
AdfTrend::ConstantTrend => (-3.9638, -3.4126, -3.1279),
}
}
pub fn kpss_critical_values(trend: bool) -> (f64, f64, f64, f64) {
if trend {
(0.119, 0.146, 0.176, 0.216)
} else {
(0.347, 0.463, 0.574, 0.739)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64, tol: f64) {
assert!((a - b).abs() < tol, "expected {b}, got {a}");
}
fn lcg_noise(n: usize, seed: u64) -> Vec<f64> {
let mut s = seed;
let mut out = Vec::with_capacity(n);
for _ in 0..n {
s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
let u = ((s >> 33) as f64) / (1u64 << 31) as f64; out.push(u - 1.0); }
out
}
#[test]
fn newey_west_iid_approximates_variance() {
let s = [1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0];
let lrv = newey_west_lrv(&s, Some(0));
approx(lrv, 1.0, 1e-9);
}
#[test]
fn adf_rejects_on_stationary_series() {
let noise = lcg_noise(300, 12345);
let mut y = vec![0.0_f64];
for &e in &noise {
let prev = *y.last().unwrap();
y.push(0.2 * prev + e);
}
let res = adf_regression(&y, AdfTrend::Constant, None).unwrap();
let (c1, _c5, _c10) = adf_critical_values(AdfTrend::Constant);
assert!(
res.t_stat < c1,
"expected stationary reject: t_stat {} should be < {}",
res.t_stat,
c1
);
}
#[test]
fn adf_does_not_reject_random_walk() {
let noise = lcg_noise(300, 67890);
let mut y = vec![0.0_f64];
for &e in &noise {
let prev = *y.last().unwrap();
y.push(prev + e);
}
let res = adf_regression(&y, AdfTrend::Constant, None).unwrap();
let (_c1, c5, _c10) = adf_critical_values(AdfTrend::Constant);
assert!(
res.t_stat > c5,
"random walk should not reject at 5%: t_stat {} vs {}",
res.t_stat,
c5
);
}
}