finance_solution/stocks/ta/
moving_average.rs1use crate::stocks::ta::ring::RingF64;
72use crate::util::error::{require_finite, FinanceError, FinanceResult};
73use crate::util::primitives::PeriodLength;
74
75#[derive(Clone, Debug)]
93pub struct SmaState {
94 period: usize,
95 ring: RingF64,
96}
97
98impl SmaState {
99 pub fn new(period: usize) -> FinanceResult<Self> {
101 let period = PeriodLength::new(period)?.get();
102 Ok(Self {
103 period,
104 ring: RingF64::with_capacity(period),
105 })
106 }
107
108 pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
109 let mut s = Self::new(period)?;
110 s.push_bars(closes)?;
111 Ok(s)
112 }
113
114 pub fn period(&self) -> usize {
115 self.period
116 }
117
118 pub fn reset(&mut self) {
119 self.ring.clear();
120 }
121
122 pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
124 require_finite("close", close)?;
125 let _ = self.ring.push(close);
126 if self.ring.is_full() {
127 Ok(Some(self.ring.sum() / self.period as f64))
128 } else {
129 Ok(None)
130 }
131 }
132
133 pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
135 let mut out = Vec::with_capacity(closes.len());
136 for &c in closes {
137 out.push(self.push(c)?);
138 }
139 Ok(out)
140 }
141
142 pub fn last(&self) -> Option<f64> {
143 if self.ring.is_full() {
144 Some(self.ring.sum() / self.period as f64)
145 } else {
146 None
147 }
148 }
149}
150
151#[derive(Clone, Debug)]
168pub struct EmaState {
169 period: usize,
170 alpha: f64,
171 seed: RingF64,
172 value: Option<f64>,
173}
174
175impl EmaState {
176 pub fn new(period: usize) -> FinanceResult<Self> {
177 let period = PeriodLength::new(period)?.get();
178 Ok(Self {
179 period,
180 alpha: 2.0 / (period as f64 + 1.0),
181 seed: RingF64::with_capacity(period),
182 value: None,
183 })
184 }
185
186 pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
187 let mut s = Self::new(period)?;
188 s.push_bars(closes)?;
189 Ok(s)
190 }
191
192 pub fn period(&self) -> usize {
193 self.period
194 }
195
196 pub fn reset(&mut self) {
197 self.seed.clear();
198 self.value = None;
199 }
200
201 pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
202 require_finite("close", close)?;
203 if let Some(prev) = self.value {
204 let next = self.alpha * close + (1.0 - self.alpha) * prev;
205 self.value = Some(next);
206 return Ok(Some(next));
207 }
208 let _ = self.seed.push(close);
209 if self.seed.is_full() {
210 let seed = self.seed.sum() / self.period as f64;
211 self.value = Some(seed);
212 Ok(Some(seed))
213 } else {
214 Ok(None)
215 }
216 }
217
218 pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
219 let mut out = Vec::with_capacity(closes.len());
220 for &c in closes {
221 out.push(self.push(c)?);
222 }
223 Ok(out)
224 }
225
226 pub fn last(&self) -> Option<f64> {
227 self.value
228 }
229}
230
231pub fn sma(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
256 validate_closes(closes)?;
257 let mut st = SmaState::new(period)?;
258 st.push_bars(closes)
259}
260
261pub fn ema(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
278 validate_closes(closes)?;
279 let mut st = EmaState::new(period)?;
280 st.push_bars(closes)
281}
282
283#[inline]
295pub fn sma_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
296 validate_closes(closes)?;
297 let mut st = SmaState::new(period)?;
298 for &c in closes {
299 st.push(c)?;
300 }
301 Ok(st.last())
302}
303
304#[inline]
315pub fn ema_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
316 validate_closes(closes)?;
317 let mut st = EmaState::new(period)?;
318 for &c in closes {
319 st.push(c)?;
320 }
321 Ok(st.last())
322}
323
324#[derive(Clone, Debug)]
337pub struct WmaState {
338 period: usize,
339 ring: RingF64,
340 weight_sum: f64,
341 weighted: Option<f64>,
343}
344
345impl WmaState {
346 pub fn new(period: usize) -> FinanceResult<Self> {
347 let period = PeriodLength::new(period)?.get();
348 let weight_sum = (period * (period + 1)) as f64 / 2.0;
349 Ok(Self {
350 period,
351 ring: RingF64::with_capacity(period),
352 weight_sum,
353 weighted: None,
354 })
355 }
356
357 pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
358 let mut s = Self::new(period)?;
359 s.push_bars(closes)?;
360 Ok(s)
361 }
362
363 pub fn period(&self) -> usize {
364 self.period
365 }
366
367 pub fn reset(&mut self) {
368 self.ring.clear();
369 self.weighted = None;
370 }
371
372 pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
373 require_finite("close", close)?;
374 let n = self.period as f64;
375 if let Some(w) = self.weighted {
376 let sum_before = self.ring.sum();
378 let _old = self.ring.push(close);
379 let w_new = w - sum_before + n * close;
381 self.weighted = Some(w_new);
382 Ok(Some(w_new / self.weight_sum))
383 } else {
384 let _ = self.ring.push(close);
385 if !self.ring.is_full() {
386 return Ok(None);
387 }
388 let mut ordered = Vec::with_capacity(self.period);
390 self.ring.copy_ordered(&mut ordered);
391 let mut num = 0.0;
392 for (i, &p) in ordered.iter().enumerate() {
393 num += (i + 1) as f64 * p;
394 }
395 self.weighted = Some(num);
396 Ok(Some(num / self.weight_sum))
397 }
398 }
399
400 pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
401 let mut out = Vec::with_capacity(closes.len());
402 for &c in closes {
403 out.push(self.push(c)?);
404 }
405 Ok(out)
406 }
407
408 pub fn last(&self) -> Option<f64> {
409 self.weighted.map(|w| w / self.weight_sum)
410 }
411}
412
413pub fn wma(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
415 validate_closes(closes)?;
416 let mut st = WmaState::new(period)?;
417 st.push_bars(closes)
418}
419
420pub fn wma_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
421 validate_closes(closes)?;
422 let mut st = WmaState::new(period)?;
423 for &c in closes {
424 st.push(c)?;
425 }
426 Ok(st.last())
427}
428
429#[derive(Clone, Debug)]
442pub struct HmaState {
443 period: usize,
444 half: WmaState,
445 full: WmaState,
446 sqrt_wma: WmaState,
447 last: Option<f64>,
448}
449
450impl HmaState {
451 pub fn new(period: usize) -> FinanceResult<Self> {
452 let period = PeriodLength::new(period)?.get();
453 if period < 2 {
454 return Err(FinanceError::Unsolvable {
455 message: "HMA period must be >= 2",
456 });
457 }
458 let half_n = (period / 2).max(1);
459 let sqrt_n = ((period as f64).sqrt().floor() as usize).max(1);
460 Ok(Self {
461 period,
462 half: WmaState::new(half_n)?,
463 full: WmaState::new(period)?,
464 sqrt_wma: WmaState::new(sqrt_n)?,
465 last: None,
466 })
467 }
468
469 pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
470 let mut s = Self::new(period)?;
471 s.push_bars(closes)?;
472 Ok(s)
473 }
474
475 pub fn period(&self) -> usize {
476 self.period
477 }
478
479 pub fn reset(&mut self) {
480 self.half.reset();
481 self.full.reset();
482 self.sqrt_wma.reset();
483 self.last = None;
484 }
485
486 pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
487 let wh = self.half.push(close)?;
488 let wf = self.full.push(close)?;
489 let out = match (wh, wf) {
490 (Some(h), Some(f)) => {
491 let raw = 2.0 * h - f;
492 self.sqrt_wma.push(raw)?
493 }
494 _ => None,
495 };
496 self.last = out;
497 Ok(out)
498 }
499
500 pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
501 let mut out = Vec::with_capacity(closes.len());
502 for &c in closes {
503 out.push(self.push(c)?);
504 }
505 Ok(out)
506 }
507
508 pub fn last(&self) -> Option<f64> {
509 self.last.or_else(|| self.sqrt_wma.last())
511 }
512}
513
514pub fn hma(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
525 validate_closes(closes)?;
526 let mut st = HmaState::new(period)?;
527 st.push_bars(closes)
528}
529
530pub fn hma_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
531 validate_closes(closes)?;
532 let mut st = HmaState::new(period)?;
533 for &c in closes {
534 st.push(c)?;
535 }
536 Ok(st.last())
537}
538
539fn validate_closes(closes: &[f64]) -> FinanceResult<()> {
540 if closes.is_empty() {
541 return Err(FinanceError::EmptyInput { what: "closes" });
542 }
543 for &c in closes {
544 require_finite("close", c)?;
545 }
546 Ok(())
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552
553 #[test]
554 fn sma_constant() {
555 let c = [10.0; 5];
556 let s = sma(&c, 3).unwrap();
557 assert_eq!(s[2], Some(10.0));
558 assert_eq!(s[4], Some(10.0));
559 }
560
561 #[test]
562 fn ema_runs() {
563 let c: Vec<_> = (1..=30).map(|x| x as f64).collect();
564 let e = ema(&c, 10).unwrap();
565 assert!(e[8].is_none());
566 assert!(e[9].is_some());
567 }
568
569 #[test]
570 fn rejects_zero_period() {
571 assert!(sma(&[1.0, 2.0], 0).is_err());
572 }
573
574 #[test]
575 fn sma_period_one_is_identity() {
576 let c = [1.0, 2.0, 3.0];
577 let s = sma(&c, 1).unwrap();
578 assert_eq!(s[0], Some(1.0));
579 assert_eq!(s[2], Some(3.0));
580 }
581
582 #[test]
583 fn ema_seed_is_sma() {
584 let c = [1.0, 2.0, 3.0, 4.0, 5.0];
585 let e = ema(&c, 3).unwrap();
586 assert!((e[2].unwrap() - 2.0).abs() < 1e-12);
588 }
589
590 #[test]
591 fn empty_series_err() {
592 assert!(sma(&[], 3).is_err());
593 assert!(ema(&[], 3).is_err());
594 }
595
596 #[test]
597 fn nan_close_err() {
598 assert!(sma(&[1.0, f64::NAN], 2).is_err());
599 }
600
601 #[test]
602 fn last_matches_series_tail() {
603 let c: Vec<_> = (1..=25).map(|x| x as f64 * 0.5).collect();
604 let s = sma(&c, 7).unwrap();
605 assert_eq!(sma_last(&c, 7).unwrap(), s[24]);
606 let e = ema(&c, 7).unwrap();
607 assert_eq!(ema_last(&c, 7).unwrap(), e[24]);
608 }
609
610 #[test]
611 fn wma_weights_newest_heavier() {
612 let s = wma(&[1.0, 2.0, 3.0], 3).unwrap();
614 assert!((s[2].unwrap() - 14.0 / 6.0).abs() < 1e-12);
615 }
616
617 #[test]
618 fn hma_state_parity() {
619 let c: Vec<f64> = (1..=50).map(|x| 100.0 + x as f64 * 0.1).collect();
620 let batch = hma(&c, 16).unwrap();
621 let st = HmaState::from_history(16, &c).unwrap();
622 assert!((batch.last().unwrap().unwrap() - st.last().unwrap()).abs() < 1e-9);
623 }
624
625 #[test]
626 fn hma_rejects_period_one() {
627 assert!(HmaState::new(1).is_err());
628 }
629
630 #[test]
631 fn hma_tracks_rising_path() {
632 let c: Vec<f64> = (1..=60).map(|x| x as f64).collect();
633 let h = hma(&c, 9).unwrap();
634 let last = h.iter().rev().find_map(|x| *x).unwrap();
635 assert!(last > 50.0, "hma last={last}");
637 }
638
639 #[test]
640 fn wma_last_matches_series() {
641 let c: Vec<f64> = (1..=20).map(|x| x as f64).collect();
642 let s = wma(&c, 5).unwrap();
643 assert_eq!(wma_last(&c, 5).unwrap(), s[19]);
644 }
645
646 #[test]
647 fn hma_reset_clears() {
648 let c: Vec<f64> = (1..=30).map(|x| x as f64).collect();
649 let mut st = HmaState::from_history(9, &c).unwrap();
650 assert!(st.last().is_some());
651 st.reset();
652 assert!(st.last().is_none());
653 }
654}