use crate::error::{Result, StatError};
use crate::parametric::Alternative;
use statrs::distribution::{ContinuousCDF, Normal};
#[derive(Debug, Clone, Copy)]
pub enum LossFunction {
SquaredError,
AbsoluteError,
}
#[derive(Debug, Clone, Copy, Default)]
pub enum VarEstimator {
#[default]
Acf,
Bartlett,
}
#[derive(Debug, Clone)]
pub struct DMResult {
pub statistic: f64,
pub p_value: f64,
pub horizon: usize,
pub loss_function: LossFunction,
pub varestimator: VarEstimator,
pub alternative: Alternative,
}
pub fn diebold_mariano(
e1: &[f64],
e2: &[f64],
loss: LossFunction,
h: usize,
alternative: Alternative,
varestimator: VarEstimator,
) -> Result<DMResult> {
let n = e1.len();
if n == 0 {
return Err(StatError::EmptyData);
}
if e1.len() != e2.len() {
return Err(StatError::InvalidParameter(
"Forecast error vectors must have equal length".to_string(),
));
}
if n < 3 {
return Err(StatError::InsufficientData { needed: 3, got: n });
}
let d: Vec<f64> = e1
.iter()
.zip(e2.iter())
.map(|(e1_t, e2_t)| {
let l1 = apply_loss(*e1_t, loss);
let l2 = apply_loss(*e2_t, loss);
l1 - l2
})
.collect();
let n_f = n as f64;
let d_bar: f64 = d.iter().sum::<f64>() / n_f;
let var_d_bar = match varestimator {
VarEstimator::Acf => dm_variance_acf(&d, h),
VarEstimator::Bartlett => dm_variance_bartlett(&d, h),
};
let dm_raw = if var_d_bar > 1e-30 {
d_bar / var_d_bar.sqrt()
} else {
0.0
};
let h_f = h as f64;
let correction = ((n_f + 1.0 - 2.0 * h_f + h_f * (h_f - 1.0) / n_f) / n_f).sqrt();
let dm_stat = dm_raw * correction;
let normal = Normal::new(0.0, 1.0).unwrap();
let p_value = match alternative {
Alternative::TwoSided => 2.0 * normal.sf(dm_stat.abs()),
Alternative::Less => normal.cdf(dm_stat),
Alternative::Greater => normal.sf(dm_stat),
};
Ok(DMResult {
statistic: dm_stat,
p_value,
horizon: h,
loss_function: loss,
varestimator,
alternative,
})
}
fn apply_loss(e: f64, loss: LossFunction) -> f64 {
match loss {
LossFunction::SquaredError => e * e,
LossFunction::AbsoluteError => e.abs(),
}
}
fn dm_variance_acf(d: &[f64], h: usize) -> f64 {
let n = d.len();
let n_f = n as f64;
let d_bar: f64 = d.iter().sum::<f64>() / n_f;
let d_centered: Vec<f64> = d.iter().map(|x| x - d_bar).collect();
let max_lag = if h > 0 { h - 1 } else { 0 };
let gamma_0: f64 = d_centered.iter().map(|x| x * x).sum::<f64>() / n_f;
let mut acov_sum = gamma_0;
for k in 1..=max_lag {
if k >= n {
break;
}
let gamma_k: f64 = d_centered
.iter()
.skip(k)
.zip(d_centered.iter())
.map(|(d_t, d_t_k)| d_t * d_t_k)
.sum::<f64>()
/ n_f;
acov_sum += 2.0 * gamma_k;
}
acov_sum / n_f
}
fn dm_variance_bartlett(d: &[f64], h: usize) -> f64 {
if h == 1 {
return dm_variance_acf(d, h);
}
let n = d.len();
let n_f = n as f64;
let h_f = h as f64;
let d_bar: f64 = d.iter().sum::<f64>() / n_f;
let d_centered: Vec<f64> = d.iter().map(|x| x - d_bar).collect();
let max_lag = h - 1;
let gamma_0: f64 = d_centered.iter().map(|x| x * x).sum::<f64>() / n_f;
let mut acov_sum = gamma_0;
for k in 1..=max_lag {
if k >= n {
break;
}
let w_k = 1.0 - (k as f64) / h_f;
let gamma_k: f64 = d_centered
.iter()
.skip(k)
.zip(d_centered.iter())
.map(|(d_t, d_t_k)| d_t * d_t_k)
.sum::<f64>()
/ n_f;
acov_sum += 2.0 * w_k * gamma_k;
}
acov_sum / n_f
}