use crate::core::error::{Error, Result};
use crate::time_series::analysis::ols_with_std_errors;
pub(super) fn adf_regression_statistic(values: &[f64], lags: usize) -> Result<f64> {
let n = values.len();
let dy: Vec<f64> = (1..n).map(|i| values[i] - values[i - 1]).collect();
let n_regressors = 2 + lags; let mut x_rows: Vec<Vec<f64>> = Vec::new();
let mut response: Vec<f64> = Vec::new();
for t in (lags + 1)..n {
let mut row = Vec::with_capacity(n_regressors);
row.push(1.0); row.push(values[t - 1]); for j in 1..=lags {
row.push(dy[t - 1 - j]); }
x_rows.push(row);
response.push(dy[t - 1]); }
if x_rows.len() <= n_regressors {
return Err(Error::InvalidInput(
"Insufficient observations to estimate the ADF regression".to_string(),
));
}
let (coefficients, std_errors) = ols_with_std_errors(&x_rows, &response)
.ok_or_else(|| Error::InvalidInput("ADF regression matrix is singular".to_string()))?;
let beta = coefficients[1];
let se = std_errors[1];
if !se.is_finite() || se <= 0.0 {
return Err(Error::InvalidInput(
"ADF regression produced a degenerate standard error".to_string(),
));
}
Ok(beta / se)
}
pub(super) fn adf_p_value(statistic: f64) -> f64 {
let anchors = [(-3.43_f64, 0.01_f64), (-2.86, 0.05), (-2.57, 0.10)];
if statistic <= anchors[0].0 {
let slope = (anchors[1].1 - anchors[0].1) / (anchors[1].0 - anchors[0].0);
(anchors[0].1 + slope * (statistic - anchors[0].0)).clamp(0.0001, 0.01)
} else if statistic <= anchors[1].0 {
let t = (statistic - anchors[0].0) / (anchors[1].0 - anchors[0].0);
anchors[0].1 + t * (anchors[1].1 - anchors[0].1)
} else if statistic <= anchors[2].0 {
let t = (statistic - anchors[1].0) / (anchors[2].0 - anchors[1].0);
anchors[1].1 + t * (anchors[2].1 - anchors[1].1)
} else {
let slope = (anchors[2].1 - anchors[1].1) / (anchors[2].0 - anchors[1].0);
(anchors[2].1 + slope * (statistic - anchors[2].0)).clamp(0.10, 0.999)
}
}
pub(crate) fn newey_west_long_run_variance(residuals: &[f64], bandwidth: usize) -> f64 {
let n = residuals.len();
if n == 0 {
return 0.0;
}
let nf = n as f64;
let mut lrv = residuals.iter().map(|e| e * e).sum::<f64>() / nf;
for j in 1..=bandwidth {
if j >= n {
break;
}
let mut gamma_j = 0.0;
for t in j..n {
gamma_j += residuals[t] * residuals[t - j];
}
gamma_j /= nf;
let weight = 1.0 - (j as f64) / ((bandwidth + 1) as f64);
lrv += 2.0 * weight * gamma_j;
}
lrv
}
pub(crate) fn poly(coeffs: &[f64], x: f64) -> f64 {
coeffs.iter().rev().fold(0.0, |acc, &c| acc * x + c)
}
pub(crate) fn newey_west_bandwidth(n: usize) -> usize {
if n == 0 {
return 0;
}
let l = 4.0 * (n as f64 / 100.0).powf(2.0 / 9.0);
(l.floor() as usize).min(n.saturating_sub(1))
}
pub(crate) fn inv_normal_cdf(p: f64) -> f64 {
if p <= 0.0 {
return f64::NEG_INFINITY;
}
if p >= 1.0 {
return f64::INFINITY;
}
const A: [f64; 6] = [
-3.969683028665376e+01,
2.209460984245205e+02,
-2.759285104469687e+02,
1.383577518672690e+02,
-3.066479806614716e+01,
2.506628277459239e+00,
];
const B: [f64; 5] = [
-5.447609879822406e+01,
1.615858368580409e+02,
-1.556989798598866e+02,
6.680131188771972e+01,
-1.328068155288572e+01,
];
const C: [f64; 6] = [
-7.784894002430293e-03,
-3.223964580411365e-01,
-2.400758277161838e+00,
-2.549732539343734e+00,
4.374664141464968e+00,
2.938163982698783e+00,
];
const D: [f64; 4] = [
7.784695709041462e-03,
3.224671290700398e-01,
2.445134137142996e+00,
3.754408661907416e+00,
];
let plow = 0.02425;
let phigh = 1.0 - plow;
if p < plow {
let q = (-2.0 * p.ln()).sqrt();
(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
/ ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
} else if p <= phigh {
let q = p - 0.5;
let r = q * q;
(((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
/ (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
} else {
let q = (-2.0 * (1.0 - p).ln()).sqrt();
-(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
/ ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
}
}
pub(crate) fn normal_critical_value(confidence_level: f64) -> Result<f64> {
if !(confidence_level > 0.0 && confidence_level < 1.0) {
return Err(Error::InvalidInput(format!(
"confidence_level must lie strictly between 0 and 1, got {confidence_level}"
)));
}
Ok(inv_normal_cdf(0.5 * (1.0 + confidence_level)))
}
pub(crate) fn kpss_critical_values(trend: &str) -> Result<(f64, f64, f64)> {
match trend {
"constant" => Ok((0.739, 0.463, 0.347)),
"linear" => Ok((0.216, 0.146, 0.119)),
other => Err(Error::InvalidInput(format!(
"KPSS trend specification must be \"constant\" or \"linear\", got \"{other}\""
))),
}
}
pub(crate) fn kpss_p_value_from_table(statistic: f64, c10: f64, c5: f64, c1: f64) -> f64 {
if statistic <= c10 {
0.10
} else if statistic <= c5 {
let t = (statistic - c10) / (c5 - c10);
0.10 + t * (0.05 - 0.10)
} else if statistic <= c1 {
let t = (statistic - c5) / (c1 - c5);
0.05 + t * (0.01 - 0.05)
} else {
0.01
}
}
pub(super) fn average_ranks(row: &[f64]) -> Vec<f64> {
let mut order: Vec<usize> = (0..row.len()).collect();
order.sort_by(|&a, &b| row[a].total_cmp(&row[b]).then(a.cmp(&b)));
let mut ranks = vec![0.0; row.len()];
let mut i = 0;
while i < order.len() {
let mut j = i + 1;
while j < order.len() && row[order[j]] == row[order[i]] {
j += 1;
}
let mid = ((i + 1 + j) as f64) / 2.0;
for &idx in &order[i..j] {
ranks[idx] = mid;
}
i = j;
}
ranks
}
pub(super) fn tie_sum(row: &[f64]) -> f64 {
let mut sorted: Vec<f64> = row.to_vec();
sorted.sort_by(|a, b| a.total_cmp(b));
let mut total = 0.0;
let mut i = 0;
while i < sorted.len() {
let mut j = i + 1;
while j < sorted.len() && sorted[j] == sorted[i] {
j += 1;
}
let t = (j - i) as f64;
total += t * t * t - t;
i = j;
}
total
}
pub(super) fn durbin_watson_bounds(n: usize) -> (f64, f64) {
const TABLE: [(f64, f64, f64); 43] = [
(15.0, 1.077, 1.361),
(16.0, 1.106, 1.371),
(17.0, 1.133, 1.381),
(18.0, 1.158, 1.391),
(19.0, 1.180, 1.401),
(20.0, 1.201, 1.411),
(21.0, 1.221, 1.420),
(22.0, 1.239, 1.429),
(23.0, 1.257, 1.437),
(24.0, 1.273, 1.446),
(25.0, 1.288, 1.454),
(26.0, 1.302, 1.461),
(27.0, 1.316, 1.469),
(28.0, 1.328, 1.476),
(29.0, 1.341, 1.483),
(30.0, 1.352, 1.489),
(31.0, 1.363, 1.496),
(32.0, 1.373, 1.502),
(33.0, 1.383, 1.508),
(34.0, 1.393, 1.514),
(35.0, 1.402, 1.519),
(36.0, 1.411, 1.525),
(37.0, 1.419, 1.530),
(38.0, 1.427, 1.535),
(39.0, 1.435, 1.540),
(40.0, 1.442, 1.544),
(45.0, 1.475, 1.566),
(50.0, 1.503, 1.585),
(55.0, 1.528, 1.601),
(60.0, 1.549, 1.616),
(65.0, 1.567, 1.629),
(70.0, 1.583, 1.641),
(75.0, 1.598, 1.652),
(80.0, 1.611, 1.662),
(85.0, 1.624, 1.671),
(90.0, 1.635, 1.679),
(95.0, 1.645, 1.687),
(100.0, 1.654, 1.694),
(150.0, 1.720, 1.746),
(200.0, 1.758, 1.778),
(500.0, 1.845, 1.851),
(1000.0, 1.888, 1.891),
(2000.0, 1.921, 1.922),
];
let nf = n as f64;
let first = TABLE[0];
let last = TABLE[TABLE.len() - 1];
if nf < first.0 || nf > last.0 {
let z = inv_normal_cdf(0.975);
let bound = (2.0 * (1.0 - z / nf.sqrt())).max(0.0);
return (bound, bound);
}
for pair in TABLE.windows(2) {
let (n_lo, dl_lo, du_lo) = pair[0];
let (n_hi, dl_hi, du_hi) = pair[1];
if nf >= n_lo && nf <= n_hi {
let t = if (n_hi - n_lo).abs() < f64::EPSILON {
0.0
} else {
(nf - n_lo) / (n_hi - n_lo)
};
return (dl_lo + t * (dl_hi - dl_lo), du_lo + t * (du_hi - du_lo));
}
}
(last.1, last.2)
}