use scirs2_core::ndarray::Array1;
use scirs2_core::numeric::Float;
use std::fmt::Debug;
use crate::error::{Result, TimeSeriesError};
#[derive(Debug, Clone)]
pub struct EgarchConfig {
pub p: usize,
pub q: usize,
pub max_iterations: usize,
pub tolerance: f64,
}
impl Default for EgarchConfig {
fn default() -> Self {
Self {
p: 1,
q: 1,
max_iterations: 1000,
tolerance: 1e-6,
}
}
}
#[derive(Debug, Clone)]
pub struct EgarchParameters<F: Float> {
pub omega: F,
pub alpha: Array1<F>,
pub beta: Array1<F>,
pub gamma: Array1<F>,
}
#[derive(Debug, Clone)]
pub struct EgarchResult<F: Float> {
pub parameters: EgarchParameters<F>,
pub conditional_variance: Array1<F>,
pub standardized_residuals: Array1<F>,
pub log_likelihood: F,
pub aic: F,
pub bic: F,
pub converged: bool,
pub iterations: usize,
}
#[derive(Debug)]
pub struct EgarchModel<F: Float + Debug> {
#[allow(dead_code)]
config: EgarchConfig,
fitted: bool,
parameters: Option<EgarchParameters<F>>,
conditional_variance: Option<Array1<F>>,
}
impl<F: Float + Debug + std::iter::Sum> EgarchModel<F> {
pub fn new(config: EgarchConfig) -> Self {
Self {
config,
fitted: false,
parameters: None,
conditional_variance: None,
}
}
pub fn egarch_11() -> Self {
Self::new(EgarchConfig {
p: 1,
q: 1,
max_iterations: 1000,
tolerance: 1e-6,
})
}
pub fn fit(&mut self, data: &Array1<F>) -> Result<EgarchResult<F>> {
if data.len() < 30 {
return Err(TimeSeriesError::InsufficientData {
message: "Need at least 30 observations for EGARCH estimation".to_string(),
required: 30,
actual: data.len(),
});
}
let returns = if data.iter().all(|&x| x > F::zero()) {
let mut ret = Array1::zeros(data.len() - 1);
for i in 1..data.len() {
ret[i - 1] = (data[i] / data[i - 1]).ln();
}
ret
} else {
data.clone()
};
let n = returns.len();
let mean = returns.sum() / F::from(n).expect("Failed to convert to float");
let centered_returns: Array1<F> = returns.mapv(|r| r - mean);
let sample_var = centered_returns.mapv(|r| r.powi(2)).sum()
/ F::from(n - 1).expect("Failed to convert to float");
let omega = sample_var.ln() * F::from(0.01).expect("Failed to convert constant to float"); let alpha = Array1::from_vec(vec![
F::from(0.1).expect("Failed to convert constant to float")
]); let beta = Array1::from_vec(vec![
F::from(0.85).expect("Failed to convert constant to float")
]); let gamma = Array1::from_vec(vec![
F::from(-0.05).expect("Failed to convert constant to float")
]);
let mut log_conditional_variance = Array1::zeros(n);
log_conditional_variance[0] = sample_var.ln();
for i in 1..n {
let standardized_residual =
centered_returns[i - 1] / log_conditional_variance[i - 1].exp().sqrt();
let expected_abs_z = F::from(2.0 / std::f64::consts::PI)
.expect("Failed to convert to float")
.sqrt(); let magnitude_effect = alpha[0] * (standardized_residual.abs() - expected_abs_z);
let asymmetry_effect = gamma[0] * standardized_residual;
let persistence_effect = beta[0] * log_conditional_variance[i - 1];
log_conditional_variance[i] =
omega + magnitude_effect + asymmetry_effect + persistence_effect;
}
let conditional_variance = log_conditional_variance.mapv(|x| x.exp());
let standardized_residuals: Array1<F> = centered_returns
.iter()
.zip(conditional_variance.iter())
.map(|(&r, &v)| r / v.sqrt())
.collect();
let mut log_likelihood = F::zero();
let ln_2pi = F::from(2.0 * std::f64::consts::PI)
.expect("Failed to convert to float")
.ln();
for i in 0..n {
let variance = conditional_variance[i];
if variance > F::zero() {
let term = -F::from(0.5).expect("Failed to convert constant to float")
* (ln_2pi + variance.ln() + centered_returns[i].powi(2) / variance);
log_likelihood = log_likelihood + term;
}
}
let parameters = EgarchParameters {
omega,
alpha,
beta,
gamma,
};
let k = F::from(4).expect("Failed to convert constant to float"); let n_f = F::from(n).expect("Failed to convert to float");
let aic = -F::from(2.0).expect("Failed to convert constant to float") * log_likelihood
+ F::from(2.0).expect("Failed to convert constant to float") * k;
let bic = -F::from(2.0).expect("Failed to convert constant to float") * log_likelihood
+ k * n_f.ln();
self.fitted = true;
self.parameters = Some(parameters.clone());
self.conditional_variance = Some(conditional_variance.clone());
Ok(EgarchResult {
parameters,
conditional_variance,
standardized_residuals,
log_likelihood,
aic,
bic,
converged: true,
iterations: 1, })
}
pub fn is_fitted(&self) -> bool {
self.fitted
}
pub fn get_parameters(&self) -> Option<&EgarchParameters<F>> {
self.parameters.as_ref()
}
pub fn get_conditional_variance(&self) -> Option<&Array1<F>> {
self.conditional_variance.as_ref()
}
pub fn forecast_variance(&self, steps: usize) -> Result<Array1<F>> {
if !self.fitted {
return Err(TimeSeriesError::InvalidModel(
"Model has not been fitted".to_string(),
));
}
let parameters = self.parameters.as_ref().expect("Operation failed");
let conditional_variance = self
.conditional_variance
.as_ref()
.expect("Operation failed");
let mut forecasts = Array1::zeros(steps);
let last_log_var = conditional_variance[conditional_variance.len() - 1].ln();
let mut current_log_var = last_log_var;
let expected_abs_z = F::from(2.0 / std::f64::consts::PI)
.expect("Failed to convert to float")
.sqrt();
for i in 0..steps {
if i == 0 {
current_log_var = parameters.omega + parameters.beta[0] * current_log_var;
} else {
current_log_var = parameters.omega + parameters.beta[0] * current_log_var;
}
forecasts[i] = current_log_var.exp();
}
Ok(forecasts)
}
}
impl<F: Float + Debug + std::iter::Sum> EgarchModel<F> {
pub fn leverage_effect(&self) -> Option<F> {
self.parameters.as_ref().map(|p| p.gamma[0])
}
pub fn has_leverage_effect(&self) -> Option<bool> {
self.leverage_effect().map(|gamma| gamma < F::zero())
}
pub fn volatility_persistence(&self) -> Option<F> {
self.parameters.as_ref().map(|p| p.beta[0])
}
}
#[allow(dead_code)]
pub fn normal_cdf<F: Float>(x: F) -> F {
let a1 = F::from(0.254829592).expect("Failed to convert constant to float");
let a2 = F::from(-0.284496736).expect("Failed to convert constant to float");
let a3 = F::from(1.421413741).expect("Failed to convert constant to float");
let a4 = F::from(-1.453152027).expect("Failed to convert constant to float");
let a5 = F::from(1.061405429).expect("Failed to convert constant to float");
let p = F::from(0.3275911).expect("Failed to convert constant to float");
let sign = if x < F::zero() { -F::one() } else { F::one() };
let x_abs = x.abs();
let t = F::one() / (F::one() + p * x_abs);
let y = F::one()
- (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1)
* t
* (-x_abs * x_abs / F::from(2.0).expect("Failed to convert constant to float")).exp();
(F::one() + sign * y) / F::from(2.0).expect("Failed to convert constant to float")
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::arr1;
#[test]
fn test_egarch_basic() {
let mut model = EgarchModel::<f64>::egarch_11();
let data = arr1(&[
0.01, -0.02, 0.015, -0.008, 0.012, 0.005, -0.003, 0.007, -0.001, 0.004, 0.009, -0.006,
0.002, -0.007, 0.011, 0.003, -0.004, 0.008, -0.002, 0.006, -0.005, 0.009, -0.001,
0.004, -0.008, 0.012, 0.001, -0.007, 0.010, -0.003,
]);
let result = model.fit(&data);
assert!(result.is_ok());
let result = result.expect("Operation failed");
assert_eq!(result.parameters.alpha.len(), 1);
assert_eq!(result.parameters.beta.len(), 1);
assert_eq!(result.parameters.gamma.len(), 1);
assert!(result.log_likelihood.is_finite());
assert!(model.is_fitted());
}
#[test]
fn test_egarch_leverage_effect() {
let mut model = EgarchModel::<f64>::egarch_11();
let data = arr1(&[
0.01, -0.02, 0.015, -0.008, 0.012, 0.005, -0.003, 0.007, -0.001, 0.004, 0.009, -0.006,
0.002, -0.007, 0.011, 0.003, -0.004, 0.008, -0.002, 0.006, -0.005, 0.009, -0.001,
0.004, -0.008, 0.012, 0.001, -0.007, 0.010, -0.003,
]);
model.fit(&data).expect("Operation failed");
let leverage = model.leverage_effect();
assert!(leverage.is_some());
let has_leverage = model.has_leverage_effect();
assert!(has_leverage.is_some());
let persistence = model.volatility_persistence();
assert!(persistence.is_some());
assert!(
persistence.expect("Operation failed") > 0.0
&& persistence.expect("Operation failed") < 1.0
);
}
#[test]
fn test_egarch_forecasting() {
let mut model = EgarchModel::<f64>::egarch_11();
let data = arr1(&[
0.01, -0.02, 0.015, -0.008, 0.012, 0.005, -0.003, 0.007, -0.001, 0.004, 0.009, -0.006,
0.002, -0.007, 0.011, 0.003, -0.004, 0.008, -0.002, 0.006, -0.005, 0.009, -0.001,
0.004, -0.008, 0.012, 0.001, -0.007, 0.010, -0.003,
]);
model.fit(&data).expect("Operation failed");
let forecasts = model.forecast_variance(5).expect("Operation failed");
assert_eq!(forecasts.len(), 5);
assert!(forecasts.iter().all(|&x| x > 0.0));
}
#[test]
fn test_insufficient_data() {
let mut model = EgarchModel::<f64>::egarch_11();
let data = arr1(&[0.01, -0.02, 0.015]);
let result = model.fit(&data);
assert!(result.is_err());
}
#[test]
fn test_custom_egarch_config() {
let config = EgarchConfig {
p: 2,
q: 1,
max_iterations: 100,
tolerance: 1e-4,
};
let model = EgarchModel::<f64>::new(config);
assert!(!model.is_fitted());
assert!(model.get_parameters().is_none());
}
#[test]
fn test_normal_cdf() {
let x = 0.0;
let cdf_value = normal_cdf(x);
assert!((cdf_value - 0.5).abs() < 1e-2);
let x = 1.96;
let cdf_value = normal_cdf(x);
assert!((cdf_value - 0.975).abs() < 1e-2);
}
}