|
| 1 | +use impl_new_derive::ImplNew; |
| 2 | +use ndarray::Array1; |
| 3 | +use ndarray_rand::RandomExt; |
| 4 | +use rand_distr::Normal; |
| 5 | + |
| 6 | +use crate::stochastic::Sampling; |
| 7 | + |
| 8 | +/// A generic Asymmetric GARCH(p,q) model (A-GARCH), |
| 9 | +/// allowing a separate "delta" term for negative-lag effects. |
| 10 | +/// |
| 11 | +/// One possible form: |
| 12 | +/// \[ |
| 13 | +/// \sigma_t^2 |
| 14 | +/// = \omega |
| 15 | +/// + \sum_{i=1}^p \Bigl[\alpha_i X_{t-i}^2 |
| 16 | +/// + \delta_i X_{t-i}^2 \mathbf{1}_{\{X_{t-i}<0\}}\Bigr] |
| 17 | +/// + \sum_{j=1}^q \beta_j \sigma_{t-j}^2, |
| 18 | +/// \quad X_t = \sigma_t \cdot z_t, \quad z_t \sim \mathcal{N}(0,1). |
| 19 | +/// \] |
| 20 | +/// |
| 21 | +/// # Parameters |
| 22 | +/// - `omega`: Constant term \(\omega\). |
| 23 | +/// - `alpha`: Array \(\{\alpha_1, \ldots, \alpha_p\}\) for positive squared terms. |
| 24 | +/// - `delta`: Array \(\{\delta_1, \ldots, \delta_p\}\) for negative-lag extra effect. |
| 25 | +/// - `beta`: Array \(\{\beta_1, \ldots, \beta_q\}\). |
| 26 | +/// - `n`: Length of the time series. |
| 27 | +/// - `m`: Optional batch size (unused by default). |
| 28 | +/// |
| 29 | +/// # Notes |
| 30 | +/// - This is essentially a T-GARCH-like structure but with different naming (`delta`). |
| 31 | +/// - Stationarity constraints typically require \(\sum \alpha_i + \tfrac{1}{2}\sum \delta_i + \sum \beta_j < 1\). |
| 32 | +#[derive(ImplNew)] |
| 33 | +pub struct AGARCH { |
| 34 | + pub omega: f64, |
| 35 | + pub alpha: Array1<f64>, |
| 36 | + pub delta: Array1<f64>, |
| 37 | + pub beta: Array1<f64>, |
| 38 | + pub n: usize, |
| 39 | + pub m: Option<usize>, |
| 40 | +} |
| 41 | + |
| 42 | +impl Sampling<f64> for AGARCH { |
| 43 | + fn sample(&self) -> Array1<f64> { |
| 44 | + let p = self.alpha.len(); |
| 45 | + let q = self.beta.len(); |
| 46 | + |
| 47 | + // Generate white noise |
| 48 | + let z = Array1::random(self.n, Normal::new(0.0, 1.0).unwrap()); |
| 49 | + |
| 50 | + // Arrays for X_t and sigma_t^2 |
| 51 | + let mut x = Array1::<f64>::zeros(self.n); |
| 52 | + let mut sigma2 = Array1::<f64>::zeros(self.n); |
| 53 | + |
| 54 | + // Summation for unconditional variance init |
| 55 | + let sum_alpha: f64 = self.alpha.iter().sum(); |
| 56 | + let sum_delta_half: f64 = self.delta.iter().sum::<f64>() * 0.5; |
| 57 | + let sum_beta: f64 = self.beta.iter().sum(); |
| 58 | + let denom = (1.0 - sum_alpha - sum_delta_half - sum_beta).max(1e-8); |
| 59 | + |
| 60 | + for t in 0..self.n { |
| 61 | + if t == 0 { |
| 62 | + sigma2[t] = self.omega / denom; |
| 63 | + } else { |
| 64 | + let mut var_t = self.omega; |
| 65 | + // p-lag terms |
| 66 | + for i in 1..=p { |
| 67 | + if t >= i { |
| 68 | + let x_lag = x[t - i]; |
| 69 | + let indicator = if x_lag < 0.0 { 1.0 } else { 0.0 }; |
| 70 | + |
| 71 | + var_t += |
| 72 | + self.alpha[i - 1] * x_lag.powi(2) + self.delta[i - 1] * x_lag.powi(2) * indicator; |
| 73 | + } |
| 74 | + } |
| 75 | + // q-lag terms |
| 76 | + for j in 1..=q { |
| 77 | + if t >= j { |
| 78 | + var_t += self.beta[j - 1] * sigma2[t - j]; |
| 79 | + } |
| 80 | + } |
| 81 | + sigma2[t] = var_t; |
| 82 | + } |
| 83 | + x[t] = sigma2[t].sqrt() * z[t]; |
| 84 | + } |
| 85 | + |
| 86 | + x |
| 87 | + } |
| 88 | + |
| 89 | + fn n(&self) -> usize { |
| 90 | + self.n |
| 91 | + } |
| 92 | + |
| 93 | + fn m(&self) -> Option<usize> { |
| 94 | + self.m |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +#[cfg(test)] |
| 99 | +mod tests { |
| 100 | + use ndarray::arr1; |
| 101 | + |
| 102 | + use crate::{ |
| 103 | + plot_1d, |
| 104 | + stochastic::{autoregressive::agrach::AGARCH, Sampling}, |
| 105 | + }; |
| 106 | + |
| 107 | + #[test] |
| 108 | + fn agarch_plot() { |
| 109 | + let alpha = arr1(&[0.05, 0.01]); // p=2 |
| 110 | + let delta = arr1(&[0.03, 0.01]); // p=2 |
| 111 | + let beta = arr1(&[0.8]); // q=1 |
| 112 | + let agarchpq = AGARCH::new(0.1, alpha, delta, beta, 100, None); |
| 113 | + plot_1d!(agarchpq.sample(), "A-GARCH(p,q) process"); |
| 114 | + } |
| 115 | +} |
0 commit comments