1use crate::error::{Error, Result};
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone)]
48pub struct Garch11 {
49 omega: f64,
50 alpha: f64,
51 beta: f64,
52 unconditional: f64,
53 prev_price: Option<f64>,
54 state: Option<(f64, f64)>,
56 last: Option<f64>,
57}
58
59impl Garch11 {
60 pub fn new(omega: f64, alpha: f64, beta: f64) -> Result<Self> {
71 if !omega.is_finite() || !alpha.is_finite() || !beta.is_finite() {
72 return Err(Error::InvalidParameter {
73 message: "GARCH(1,1) parameters must be finite",
74 });
75 }
76 if omega <= 0.0 {
77 return Err(Error::InvalidParameter {
78 message: "GARCH(1,1) omega must be > 0",
79 });
80 }
81 if alpha < 0.0 || beta < 0.0 {
82 return Err(Error::InvalidParameter {
83 message: "GARCH(1,1) alpha and beta must be >= 0",
84 });
85 }
86 if alpha + beta >= 1.0 {
87 return Err(Error::InvalidParameter {
88 message: "GARCH(1,1) requires alpha + beta < 1 (covariance stationarity)",
89 });
90 }
91 Ok(Self {
92 omega,
93 alpha,
94 beta,
95 unconditional: omega / (1.0 - alpha - beta),
96 prev_price: None,
97 state: None,
98 last: None,
99 })
100 }
101
102 pub const fn params(&self) -> (f64, f64, f64) {
104 (self.omega, self.alpha, self.beta)
105 }
106
107 pub const fn unconditional_variance(&self) -> f64 {
109 self.unconditional
110 }
111
112 pub const fn value(&self) -> Option<f64> {
114 self.last
115 }
116}
117
118impl Indicator for Garch11 {
119 type Input = f64;
120 type Output = f64;
121
122 #[inline]
123 fn update(&mut self, input: f64) -> Option<f64> {
124 if !input.is_finite() || input <= 0.0 {
127 return None;
128 }
129 let Some(prev) = self.prev_price else {
130 self.prev_price = Some(input);
131 return None;
132 };
133 self.prev_price = Some(input);
134 let r = (input / prev).ln();
137 let r_sq = r * r;
138 let var = match self.state {
139 None => self.unconditional,
141 Some((prev_var, prev_r_sq)) => {
142 self.omega + self.alpha * prev_r_sq + self.beta * prev_var
143 }
144 };
145 self.state = Some((var, r_sq));
146 let vol = var.sqrt();
149 self.last = Some(vol);
150 Some(vol)
151 }
152
153 fn reset(&mut self) {
154 self.prev_price = None;
155 self.state = None;
156 self.last = None;
157 }
158
159 #[inline]
160 fn warmup_period(&self) -> usize {
161 2
164 }
165
166 #[inline]
167 fn is_ready(&self) -> bool {
168 self.last.is_some()
169 }
170
171 #[inline]
172 fn name(&self) -> &'static str {
173 "Garch11"
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::traits::BatchExt;
181 use approx::assert_relative_eq;
182
183 #[test]
184 fn rejects_invalid_params() {
185 assert!(matches!(
186 Garch11::new(0.0, 0.1, 0.8),
187 Err(Error::InvalidParameter { .. })
188 ));
189 assert!(matches!(
190 Garch11::new(-1.0, 0.1, 0.8),
191 Err(Error::InvalidParameter { .. })
192 ));
193 assert!(matches!(
194 Garch11::new(0.001, -0.1, 0.8),
195 Err(Error::InvalidParameter { .. })
196 ));
197 assert!(matches!(
198 Garch11::new(0.001, 0.1, -0.8),
199 Err(Error::InvalidParameter { .. })
200 ));
201 assert!(matches!(
202 Garch11::new(0.001, 0.5, 0.5),
203 Err(Error::InvalidParameter { .. })
204 ));
205 assert!(matches!(
206 Garch11::new(f64::NAN, 0.1, 0.8),
207 Err(Error::InvalidParameter { .. })
208 ));
209 assert!(matches!(
210 Garch11::new(0.001, f64::INFINITY, 0.8),
211 Err(Error::InvalidParameter { .. })
212 ));
213 }
214
215 #[test]
216 fn accessors_and_metadata() {
217 let g = Garch11::new(0.001, 0.1, 0.85).unwrap();
218 assert_eq!(g.params(), (0.001, 0.1, 0.85));
219 assert_relative_eq!(g.unconditional_variance(), 0.001 / 0.05, epsilon = 1e-12);
220 assert_eq!(g.warmup_period(), 2);
221 assert_eq!(g.name(), "Garch11");
222 assert!(!g.is_ready());
223 assert_eq!(g.value(), None);
224 }
225
226 #[test]
227 fn first_emission_is_unconditional() {
228 let g = Garch11::new(0.002, 0.1, 0.85);
231 let mut g = g.unwrap();
232 assert_eq!(g.update(100.0), None);
233 let out = g.update(110.0).unwrap();
234 assert_relative_eq!(out, (0.002_f64 / 0.05).sqrt(), epsilon = 1e-12);
235 }
236
237 #[test]
238 fn known_value() {
239 let (omega, alpha, beta) = (0.002, 0.1, 0.85);
241 let mut g = Garch11::new(omega, alpha, beta).unwrap();
242 let out = g.batch(&[100.0, 110.0, 99.0]);
243 let uncond = omega / (1.0 - alpha - beta);
244 let r1 = (110.0_f64 / 100.0).ln();
245 assert_relative_eq!(out[1].unwrap(), uncond.sqrt(), epsilon = 1e-12);
246 let var2 = omega + alpha * r1 * r1 + beta * uncond;
247 assert_relative_eq!(out[2].unwrap(), var2.sqrt(), epsilon = 1e-12);
248 }
249
250 #[test]
251 fn flat_series_converges_to_long_run() {
252 let (omega, beta) = (0.002, 0.85);
256 let mut g = Garch11::new(omega, 0.10, beta).unwrap();
257 let out = g.batch(&[100.0; 400]);
258 let fixed_point = (omega / (1.0 - beta)).sqrt();
259 assert_relative_eq!(out.last().unwrap().unwrap(), fixed_point, epsilon = 1e-9);
260 }
261
262 #[test]
263 fn output_is_strictly_positive() {
264 let mut g = Garch11::new(0.000_002, 0.1, 0.88).unwrap();
265 let prices: Vec<f64> = (1..=200)
266 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 12.0)
267 .collect();
268 for v in g.batch(&prices).into_iter().flatten() {
269 assert!(
270 v > 0.0,
271 "GARCH volatility must be strictly positive, got {v}"
272 );
273 }
274 }
275
276 #[test]
277 fn ignores_non_finite_input() {
278 let mut g = Garch11::new(0.001, 0.1, 0.85).unwrap();
279 let out = g.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
280 let last = *out.last().unwrap();
281 assert!(last.is_some());
282 assert_eq!(g.update(f64::NAN), None);
283 assert_eq!(g.update(f64::INFINITY), None);
284 }
285
286 #[test]
287 fn skips_non_positive_prices() {
288 let mut g = Garch11::new(0.001, 0.1, 0.85).unwrap();
289 let warmup = g.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
290 warmup.last().copied().flatten().expect("warmed up");
291 assert_eq!(g.update(-5.0), None);
292 assert_eq!(g.update(0.0), None);
293 let mut control = g.clone();
295 let after = g.update(21.0).expect("ready");
296 assert_eq!(control.update(21.0).expect("ready"), after);
297 }
298
299 #[test]
300 fn skips_non_positive_before_first_price() {
301 let mut g = Garch11::new(0.001, 0.1, 0.85).unwrap();
302 assert_eq!(g.update(0.0), None);
303 assert_eq!(g.update(f64::NAN), None);
304 assert_eq!(g.update(100.0), None);
305 assert!(g.update(110.0).is_some());
306 }
307
308 #[test]
309 fn reset_clears_state() {
310 let mut g = Garch11::new(0.001, 0.1, 0.85).unwrap();
311 g.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
312 assert!(g.is_ready());
313 g.reset();
314 assert!(!g.is_ready());
315 assert_eq!(g.value(), None);
316 assert_eq!(g.update(1.0), None);
317 }
318
319 #[test]
320 fn batch_equals_streaming() {
321 let prices: Vec<f64> = (1..=120)
322 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
323 .collect();
324 let batch = Garch11::new(0.000_002, 0.1, 0.88).unwrap().batch(&prices);
325 let mut b = Garch11::new(0.000_002, 0.1, 0.88).unwrap();
326 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
327 assert_eq!(batch, streamed);
328 }
329}