use scirs2_core::ndarray::Array1;
use scirs2_core::numeric::Float;
use std::fmt::Debug;
use crate::error::{Result, TimeSeriesError};
#[derive(Debug, Clone)]
pub struct GjrGarchParameters<F: Float> {
pub omega: F,
pub alpha: F,
pub beta: F,
pub gamma: F,
}
#[derive(Debug, Clone)]
pub struct GjrGarchResult<F: Float> {
pub parameters: GjrGarchParameters<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 GjrGarchModel<F: Float + Debug + std::iter::Sum> {
parameters: Option<GjrGarchParameters<F>>,
conditional_variance: Option<Array1<F>>,
fitted: bool,
}
impl<F: Float + Debug + Clone + std::iter::Sum> GjrGarchModel<F> {
pub fn new() -> Self {
Self {
parameters: None,
conditional_variance: None,
fitted: false,
}
}
pub fn fit(&mut self, returns: &Array1<F>) -> Result<GjrGarchResult<F>> {
if returns.len() < 10 {
return Err(TimeSeriesError::InsufficientData {
message: "Need at least 10 observations for GJR-GARCH".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.05).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_variance = centered_returns.mapv(|x| x.powi(2)).sum()
/ F::from(n - 1).expect("Failed to convert to float");
let mut conditional_variance = Array1::zeros(n);
conditional_variance[0] = initial_variance;
for i in 1..n {
let lagged_return = centered_returns[i - 1];
let lagged_variance = conditional_variance[i - 1];
let negative_indicator = if lagged_return < F::zero() {
F::one()
} else {
F::zero()
};
conditional_variance[i] = omega
+ alpha * lagged_return.powi(2) + gamma * negative_indicator * lagged_return.powi(2) + beta * lagged_variance; }
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 = GjrGarchParameters {
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(GjrGarchResult {
parameters,
conditional_variance,
standardized_residuals,
log_likelihood,
aic,
bic,
converged: true,
iterations: 1, })
}
pub fn forecast(&self, steps: usize) -> Result<Array1<F>> {
if !self.fitted {
return Err(TimeSeriesError::InvalidModel(
"GJR-GARCH model must be fitted before forecasting".to_string(),
));
}
let params = self.parameters.as_ref().expect("Operation failed");
let last_variance = *self
.conditional_variance
.as_ref()
.expect("Operation failed")
.last()
.expect("Operation failed");
let mut forecasts = Array1::zeros(steps);
let persistence = params.alpha
+ params.beta
+ params.gamma / F::from(2.0).expect("Failed to convert constant to float");
let long_run_variance = params.omega / (F::one() - persistence);
for i in 0..steps {
if i == 0 {
forecasts[i] = last_variance;
} else {
let decay_factor = persistence.powi(i as i32);
forecasts[i] =
long_run_variance + (forecasts[0] - long_run_variance) * decay_factor;
}
}
Ok(forecasts)
}
pub fn is_fitted(&self) -> bool {
self.fitted
}
pub fn get_parameters(&self) -> Option<&GjrGarchParameters<F>> {
self.parameters.as_ref()
}
pub fn get_conditional_variance(&self) -> Option<&Array1<F>> {
self.conditional_variance.as_ref()
}
}
impl<F: Float + Debug + Clone + std::iter::Sum> Default for GjrGarchModel<F> {
fn default() -> Self {
Self::new()
}
}
impl<F: Float + Debug + Clone + std::iter::Sum> GjrGarchModel<F> {
pub fn has_leverage_effect(&self) -> Option<bool> {
self.parameters.as_ref().map(|p| p.gamma > F::zero())
}
pub fn leverage_effect_magnitude(&self) -> Option<F> {
self.parameters.as_ref().map(|p| p.gamma)
}
pub fn volatility_persistence(&self) -> Option<F> {
self.parameters.as_ref().map(|p| {
p.alpha + p.beta + p.gamma / F::from(2.0).expect("Failed to convert constant to float")
})
}
pub fn long_run_variance(&self) -> Option<F> {
if let Some(params) = &self.parameters {
let persistence = params.alpha
+ params.beta
+ params.gamma / F::from(2.0).expect("Failed to convert constant to float");
if persistence < F::one() {
Some(params.omega / (F::one() - persistence))
} else {
None }
} else {
None
}
}
pub fn is_stationary(&self) -> Option<bool> {
self.volatility_persistence().map(|p| p < F::one())
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::arr1;
#[test]
fn test_gjr_garch_basic() {
let mut model = GjrGarchModel::<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 >= 0.0); assert!(result.log_likelihood.is_finite());
assert!(model.is_fitted());
}
#[test]
fn test_gjr_garch_forecasting() {
let mut model = GjrGarchModel::<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 forecasts = model.forecast(5).expect("Operation failed");
assert_eq!(forecasts.len(), 5);
assert!(forecasts.iter().all(|&x| x > 0.0));
}
#[test]
fn test_leverage_effect_detection() {
let mut model = GjrGarchModel::<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 has_leverage = model.has_leverage_effect();
assert!(has_leverage.is_some());
let leverage_magnitude = model.leverage_effect_magnitude();
assert!(leverage_magnitude.is_some());
let persistence = model.volatility_persistence();
assert!(persistence.is_some());
assert!(persistence.expect("Operation failed") < 1.0);
let long_run_var = model.long_run_variance();
assert!(long_run_var.is_some());
assert!(long_run_var.expect("Operation failed") > 0.0);
let is_stationary = model.is_stationary();
assert!(is_stationary == Some(true));
}
#[test]
fn test_insufficient_data() {
let mut model = GjrGarchModel::<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_unfitted_forecast() {
let model = GjrGarchModel::<f64>::new();
let result = model.forecast(5);
assert!(result.is_err());
}
#[test]
fn test_model_properties() {
let mut model = GjrGarchModel::<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,
]);
let result = model.fit(&returns).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 >= 0.0);
let persistence =
result.parameters.alpha + result.parameters.beta + result.parameters.gamma / 2.0;
assert!(persistence < 1.0);
assert!(result.aic.is_finite());
assert!(result.bic.is_finite());
}
#[test]
fn test_default_constructor() {
let model: GjrGarchModel<f64> = Default::default();
assert!(!model.is_fitted());
assert!(model.get_parameters().is_none());
assert!(model.get_conditional_variance().is_none());
}
}