use scirs2_core::ndarray::Array1;
use scirs2_core::numeric::Float;
use std::fmt::Debug;
use crate::error::{Result, TimeSeriesError};
#[derive(Debug, Clone)]
pub struct AparchParameters<F: Float> {
pub omega: F,
pub alpha: F,
pub beta: F,
pub gamma: F,
pub delta: F,
}
#[derive(Debug, Clone)]
pub struct AparchResult<F: Float> {
pub parameters: AparchParameters<F>,
pub conditional_variance: Array1<F>,
pub conditional_std: 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 AparchModel<F: Float + Debug + std::iter::Sum> {
parameters: Option<AparchParameters<F>>,
conditional_std: Option<Array1<F>>,
fitted: bool,
}
impl<F: Float + Debug + Clone + std::iter::Sum> AparchModel<F> {
pub fn new() -> Self {
Self {
parameters: None,
conditional_std: None,
fitted: false,
}
}
pub fn fit(&mut self, returns: &Array1<F>) -> Result<AparchResult<F>> {
if returns.len() < 10 {
return Err(TimeSeriesError::InsufficientData {
message: "Need at least 10 observations for APARCH".to_string(),
required: 10,
actual: returns.len(),
});
}
let n = returns.len();
let omega = F::from(0.00001).expect("Failed to convert constant to float"); let alpha = F::from(0.05).expect("Failed to convert constant to float"); let beta = F::from(0.90).expect("Failed to convert constant to float"); let gamma = F::from(0.1).expect("Failed to convert constant to float"); let delta = F::from(2.0).expect("Failed to convert constant to float");
let mean = returns.sum() / F::from(n).expect("Failed to convert to float");
let centered_returns: Array1<F> = returns.mapv(|x| x - mean);
let initial_std = (centered_returns.mapv(|x| x.powi(2)).sum()
/ F::from(n - 1).expect("Failed to convert to float"))
.sqrt();
let mut conditional_std = Array1::zeros(n);
conditional_std[0] = initial_std;
for i in 1..n {
let lagged_return = centered_returns[i - 1];
let lagged_std = conditional_std[i - 1];
let abs_innovation = lagged_return.abs();
let sign_adjustment = if lagged_return < F::zero() {
abs_innovation - gamma * lagged_return
} else {
abs_innovation + gamma * lagged_return
};
let innovation_power =
if delta == F::from(2.0).expect("Failed to convert constant to float") {
sign_adjustment.powi(2)
} else {
sign_adjustment.powf(delta)
};
let std_power = if delta == F::from(2.0).expect("Failed to convert constant to float") {
lagged_std.powi(2)
} else {
lagged_std.powf(delta)
};
let new_std_power = omega + alpha * innovation_power + beta * std_power;
conditional_std[i] =
if delta == F::from(2.0).expect("Failed to convert constant to float") {
new_std_power
.sqrt()
.max(F::from(1e-8).expect("Failed to convert constant to float"))
} else {
new_std_power
.powf(F::one() / delta)
.max(F::from(1e-8).expect("Failed to convert constant to float"))
};
}
let conditional_variance = conditional_std.mapv(|x| x.powi(2));
let standardized_residuals: Array1<F> = centered_returns
.iter()
.zip(conditional_std.iter())
.map(|(&r, &s)| r / s)
.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 std_dev = conditional_std[i];
if std_dev > F::zero() {
let term = -F::from(0.5).expect("Failed to convert constant to float")
* (ln_2pi
+ F::from(2.0).expect("Failed to convert constant to float")
* std_dev.ln()
+ centered_returns[i].powi(2) / std_dev.powi(2));
log_likelihood = log_likelihood + term;
}
}
let parameters = AparchParameters {
omega,
alpha,
beta,
gamma,
delta,
};
let k = F::from(5).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_std = Some(conditional_std.clone());
Ok(AparchResult {
parameters,
conditional_variance,
conditional_std,
standardized_residuals,
log_likelihood,
aic,
bic,
converged: true,
iterations: 1, })
}
pub fn is_fitted(&self) -> bool {
self.fitted
}
pub fn get_parameters(&self) -> Option<&AparchParameters<F>> {
self.parameters.as_ref()
}
pub fn get_conditional_std(&self) -> Option<&Array1<F>> {
self.conditional_std.as_ref()
}
pub fn get_conditional_variance(&self) -> Option<Array1<F>> {
self.conditional_std
.as_ref()
.map(|std| std.mapv(|x| x.powi(2)))
}
}
impl<F: Float + Debug + Clone + std::iter::Sum> Default for AparchModel<F> {
fn default() -> Self {
Self::new()
}
}
impl<F: Float + Debug + Clone + std::iter::Sum> AparchModel<F> {
pub fn classify_model(&self) -> Option<String> {
self.parameters.as_ref().map(|p| {
let delta = p.delta;
let gamma = p.gamma;
if (delta - F::from(2.0).expect("Failed to convert constant to float")).abs()
< F::from(0.1).expect("Failed to convert constant to float")
{
if gamma.abs() < F::from(0.01).expect("Failed to convert constant to float") {
"Standard GARCH".to_string()
} else {
"GJR-GARCH (Threshold GARCH)".to_string()
}
} else if (delta - F::one()).abs()
< F::from(0.1).expect("Failed to convert constant to float")
{
if gamma.abs() < F::from(0.01).expect("Failed to convert constant to float") {
"AVGARCH (Absolute Value GARCH)".to_string()
} else {
"TARCH (Threshold ARCH)".to_string()
}
} else {
"General APARCH".to_string()
}
})
}
pub fn has_asymmetric_effects(&self) -> Option<bool> {
self.parameters
.as_ref()
.map(|p| p.gamma.abs() > F::from(0.01).expect("Failed to convert constant to float"))
}
pub fn asymmetry_parameter(&self) -> Option<F> {
self.parameters.as_ref().map(|p| p.gamma)
}
pub fn power_parameter(&self) -> Option<F> {
self.parameters.as_ref().map(|p| p.delta)
}
pub fn persistence(&self) -> Option<F> {
self.parameters.as_ref().map(|p| {
p.alpha + p.beta
})
}
pub fn is_likely_stationary(&self) -> Option<bool> {
self.persistence().map(|p| p < F::one())
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::arr1;
#[test]
fn test_aparch_basic() {
let mut model = AparchModel::<f64>::new();
let returns = arr1(&[
0.01, -0.02, 0.015, -0.008, 0.012, 0.005, -0.003, 0.007, -0.001, 0.004,
]);
assert!(!model.is_fitted());
let result = model.fit(&returns);
assert!(result.is_ok());
let result = result.expect("Operation failed");
assert!(result.parameters.omega > 0.0);
assert!(result.parameters.alpha > 0.0);
assert!(result.parameters.beta > 0.0);
assert!(result.parameters.gamma > -1.0 && result.parameters.gamma < 1.0);
assert!(result.parameters.delta > 0.0);
assert!(result.log_likelihood.is_finite());
assert!(model.is_fitted());
}
#[test]
fn test_aparch_properties() {
let mut model = AparchModel::<f64>::new();
let returns = arr1(&[
0.01, -0.02, 0.015, -0.008, 0.012, 0.005, -0.003, 0.007, -0.001, 0.004,
]);
model.fit(&returns).expect("Operation failed");
let classification = model.classify_model();
assert!(classification.is_some());
let has_asymmetry = model.has_asymmetric_effects();
assert!(has_asymmetry.is_some());
let gamma = model.asymmetry_parameter();
assert!(gamma.is_some());
let delta = model.power_parameter();
assert!(delta.is_some());
assert_eq!(delta.expect("Operation failed"), 2.0);
let persistence = model.persistence();
assert!(persistence.is_some());
assert!(persistence.expect("Operation failed") > 0.0);
let is_stationary = model.is_likely_stationary();
assert!(is_stationary == Some(true));
}
#[test]
fn test_aparch_variance_consistency() {
let mut model = AparchModel::<f64>::new();
let returns = arr1(&[
0.01, -0.02, 0.015, -0.008, 0.012, 0.005, -0.003, 0.007, -0.001, 0.004,
]);
let result = model.fit(&returns).expect("Operation failed");
let variance_from_std = model.get_conditional_variance().expect("Operation failed");
let variance_direct = result.conditional_variance;
for i in 0..variance_from_std.len() {
assert!((variance_from_std[i] - variance_direct[i]).abs() < 1e-10);
}
}
#[test]
fn test_insufficient_data() {
let mut model = AparchModel::<f64>::new();
let returns = arr1(&[0.01, -0.02]);
let result = model.fit(&returns);
assert!(result.is_err());
assert!(!model.is_fitted());
}
#[test]
fn test_model_getters() {
let model = AparchModel::<f64>::new();
assert!(model.get_parameters().is_none());
assert!(model.get_conditional_std().is_none());
assert!(model.get_conditional_variance().is_none());
let mut fitted_model = AparchModel::<f64>::new();
let returns = arr1(&[
0.01, -0.02, 0.015, -0.008, 0.012, 0.005, -0.003, 0.007, -0.001, 0.004,
]);
fitted_model.fit(&returns).expect("Operation failed");
assert!(fitted_model.get_parameters().is_some());
assert!(fitted_model.get_conditional_std().is_some());
assert!(fitted_model.get_conditional_variance().is_some());
}
#[test]
fn test_default_constructor() {
let model: AparchModel<f64> = Default::default();
assert!(!model.is_fitted());
assert!(model.get_parameters().is_none());
}
#[test]
fn test_model_classification() {
let mut model = AparchModel::<f64>::new();
let returns = 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,
]);
model.fit(&returns).expect("Operation failed");
let classification = model.classify_model().expect("Operation failed");
assert!(classification.contains("GARCH"));
}
}