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 fn update(&mut self, input: f64) -> Option<f64> {
123 if !input.is_finite() || input <= 0.0 {
126 return self.last;
127 }
128 let Some(prev) = self.prev_price else {
129 self.prev_price = Some(input);
130 return None;
131 };
132 self.prev_price = Some(input);
133 let r = (input / prev).ln();
136 let r_sq = r * r;
137 let var = match self.state {
138 None => self.unconditional,
140 Some((prev_var, prev_r_sq)) => {
141 self.omega + self.alpha * prev_r_sq + self.beta * prev_var
142 }
143 };
144 self.state = Some((var, r_sq));
145 let vol = var.sqrt();
148 self.last = Some(vol);
149 Some(vol)
150 }
151
152 fn reset(&mut self) {
153 self.prev_price = None;
154 self.state = None;
155 self.last = None;
156 }
157
158 fn warmup_period(&self) -> usize {
159 2
162 }
163
164 fn is_ready(&self) -> bool {
165 self.last.is_some()
166 }
167
168 fn name(&self) -> &'static str {
169 "Garch11"
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::traits::BatchExt;
177 use approx::assert_relative_eq;
178
179 #[test]
180 fn rejects_invalid_params() {
181 assert!(matches!(
182 Garch11::new(0.0, 0.1, 0.8),
183 Err(Error::InvalidParameter { .. })
184 ));
185 assert!(matches!(
186 Garch11::new(-1.0, 0.1, 0.8),
187 Err(Error::InvalidParameter { .. })
188 ));
189 assert!(matches!(
190 Garch11::new(0.001, -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.5, 0.5),
199 Err(Error::InvalidParameter { .. })
200 ));
201 assert!(matches!(
202 Garch11::new(f64::NAN, 0.1, 0.8),
203 Err(Error::InvalidParameter { .. })
204 ));
205 assert!(matches!(
206 Garch11::new(0.001, f64::INFINITY, 0.8),
207 Err(Error::InvalidParameter { .. })
208 ));
209 }
210
211 #[test]
212 fn accessors_and_metadata() {
213 let g = Garch11::new(0.001, 0.1, 0.85).unwrap();
214 assert_eq!(g.params(), (0.001, 0.1, 0.85));
215 assert_relative_eq!(g.unconditional_variance(), 0.001 / 0.05, epsilon = 1e-12);
216 assert_eq!(g.warmup_period(), 2);
217 assert_eq!(g.name(), "Garch11");
218 assert!(!g.is_ready());
219 assert_eq!(g.value(), None);
220 }
221
222 #[test]
223 fn first_emission_is_unconditional() {
224 let g = Garch11::new(0.002, 0.1, 0.85);
227 let mut g = g.unwrap();
228 assert_eq!(g.update(100.0), None);
229 let out = g.update(110.0).unwrap();
230 assert_relative_eq!(out, (0.002_f64 / 0.05).sqrt(), epsilon = 1e-12);
231 }
232
233 #[test]
234 fn known_value() {
235 let (omega, alpha, beta) = (0.002, 0.1, 0.85);
237 let mut g = Garch11::new(omega, alpha, beta).unwrap();
238 let out = g.batch(&[100.0, 110.0, 99.0]);
239 let uncond = omega / (1.0 - alpha - beta);
240 let r1 = (110.0_f64 / 100.0).ln();
241 assert_relative_eq!(out[1].unwrap(), uncond.sqrt(), epsilon = 1e-12);
242 let var2 = omega + alpha * r1 * r1 + beta * uncond;
243 assert_relative_eq!(out[2].unwrap(), var2.sqrt(), epsilon = 1e-12);
244 }
245
246 #[test]
247 fn flat_series_converges_to_long_run() {
248 let (omega, beta) = (0.002, 0.85);
252 let mut g = Garch11::new(omega, 0.10, beta).unwrap();
253 let out = g.batch(&[100.0; 400]);
254 let fixed_point = (omega / (1.0 - beta)).sqrt();
255 assert_relative_eq!(out.last().unwrap().unwrap(), fixed_point, epsilon = 1e-9);
256 }
257
258 #[test]
259 fn output_is_strictly_positive() {
260 let mut g = Garch11::new(0.000_002, 0.1, 0.88).unwrap();
261 let prices: Vec<f64> = (1..=200)
262 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 12.0)
263 .collect();
264 for v in g.batch(&prices).into_iter().flatten() {
265 assert!(
266 v > 0.0,
267 "GARCH volatility must be strictly positive, got {v}"
268 );
269 }
270 }
271
272 #[test]
273 fn ignores_non_finite_input() {
274 let mut g = Garch11::new(0.001, 0.1, 0.85).unwrap();
275 let out = g.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
276 let last = *out.last().unwrap();
277 assert!(last.is_some());
278 assert_eq!(g.update(f64::NAN), last);
279 assert_eq!(g.update(f64::INFINITY), last);
280 }
281
282 #[test]
283 fn skips_non_positive_prices() {
284 let mut g = Garch11::new(0.001, 0.1, 0.85).unwrap();
285 let warmup = g.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
286 let baseline = warmup.last().copied().flatten().expect("warmed up");
287 assert_eq!(g.update(-5.0), Some(baseline));
288 assert_eq!(g.update(0.0), Some(baseline));
289 let mut control = g.clone();
291 let after = g.update(21.0).expect("ready");
292 assert_eq!(control.update(21.0).expect("ready"), after);
293 }
294
295 #[test]
296 fn skips_non_positive_before_first_price() {
297 let mut g = Garch11::new(0.001, 0.1, 0.85).unwrap();
298 assert_eq!(g.update(0.0), None);
299 assert_eq!(g.update(f64::NAN), None);
300 assert_eq!(g.update(100.0), None);
301 assert!(g.update(110.0).is_some());
302 }
303
304 #[test]
305 fn reset_clears_state() {
306 let mut g = Garch11::new(0.001, 0.1, 0.85).unwrap();
307 g.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
308 assert!(g.is_ready());
309 g.reset();
310 assert!(!g.is_ready());
311 assert_eq!(g.value(), None);
312 assert_eq!(g.update(1.0), None);
313 }
314
315 #[test]
316 fn batch_equals_streaming() {
317 let prices: Vec<f64> = (1..=120)
318 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
319 .collect();
320 let batch = Garch11::new(0.000_002, 0.1, 0.88).unwrap().batch(&prices);
321 let mut b = Garch11::new(0.000_002, 0.1, 0.88).unwrap();
322 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
323 assert_eq!(batch, streamed);
324 }
325}