finance_solution/stocks/ta/moving_average.rs
1//! # Simple & exponential moving averages (SMA / EMA)
2//!
3//! Teaching + production building blocks for price smoothers. **Batch** APIs
4//! ([`sma`], [`ema`]) and **incremental** APIs ([`SmaState`], [`EmaState`]) share one
5//! implementation path: batch is “create state → [`push_bars`](SmaState::push_bars)”.
6//!
7//! ## Word problem
8//!
9//! > A stock closed at 10, 11, 12, 13, 14 over five days. What is the 3-day SMA on
10//! > day 5?
11//!
12//! Expect: `(12 + 13 + 14) / 3 = 13`.
13//!
14//! ```
15//! use finance_solution::stocks::ta::sma;
16//! let closes = [10.0, 11.0, 12.0, 13.0, 14.0];
17//! let s = sma(&closes, 3).unwrap();
18//! // period index: 0 1 2 3 4
19//! // warm-up: None None Some Some Some
20//! assert_eq!(s[0], None);
21//! assert_eq!(s[1], None);
22//! assert!((s[2].unwrap() - 11.0).abs() < 1e-12); // (10+11+12)/3
23//! assert!((s[4].unwrap() - 13.0).abs() < 1e-12); // (12+13+14)/3
24//! ```
25//!
26//! ## Quant pattern — one pack, many symbols
27//!
28//! ```
29//! use finance_solution::stocks::ta::{SmaState, EmaState};
30//!
31//! // Live: hold state per symbol (your engine's HashMap)
32//! let mut sma20 = SmaState::new(20).unwrap();
33//! let mut ema20 = EmaState::new(20).unwrap();
34//! # let payload = [100.0, 100.5, 101.0];
35//! // One streaming payload with several bars:
36//! let _ = sma20.push_bars(&payload).unwrap();
37//! let _ = ema20.push_bars(&payload).unwrap();
38//! // Or single bar:
39//! let last_sma = sma20.push(101.2).unwrap(); // Option after warm-up
40//! ```
41//!
42//! ## Formulas
43//!
44//! **SMA** over window of length `n`:
45//!
46//! ```text
47//! SMA_t = (P_{t-n+1} + … + P_t) / n
48//! ```
49//!
50//! **EMA** with span `n` (α = 2/(n+1)), seed = SMA of first `n` closes:
51//!
52//! ```text
53//! EMA_seed = SMA(P_0..P_{n-1})
54//! EMA_t = α * P_t + (1-α) * EMA_{t-1}
55//! ```
56//!
57//! ## Warm-up
58//!
59//! Output length = input length. Indices `0 .. n-2` are `None` until the window is full.
60//!
61//! ## Related
62//!
63//! - Incremental: [`SmaState`], [`EmaState`] in [`crate::stocks::ta::state`]
64//! - Used by: Bollinger (SMA mid), Keltner/MACD (EMA)
65
66use crate::stocks::ta::ring::RingF64;
67use crate::util::error::{require_finite, FinanceError, FinanceResult};
68use crate::util::primitives::PeriodLength;
69
70// ---------------------------------------------------------------------------
71// Incremental state (canonical math path for SMA/EMA)
72// ---------------------------------------------------------------------------
73
74/// Incremental SMA. After warm-up, each [`SmaState::push`] is O(1).
75///
76/// Batch [`sma`] is implemented as `SmaState::new` + [`SmaState::push_bars`].
77///
78/// # Examples
79/// ```
80/// use finance_solution::stocks::ta::SmaState;
81/// let mut s = SmaState::new(3).unwrap();
82/// assert_eq!(s.push(1.0).unwrap(), None);
83/// assert_eq!(s.push(2.0).unwrap(), None);
84/// assert!((s.push(3.0).unwrap().unwrap() - 2.0).abs() < 1e-12);
85/// assert!((s.push(6.0).unwrap().unwrap() - 3.666666666666).abs() < 1e-9);
86/// ```
87#[derive(Clone, Debug)]
88pub struct SmaState {
89 period: usize,
90 ring: RingF64,
91}
92
93impl SmaState {
94 /// Fallible constructor (`period >= 1`). Named `new` → [`FinanceResult`] (not `try_new`).
95 pub fn new(period: usize) -> FinanceResult<Self> {
96 let period = PeriodLength::new(period)?.get();
97 Ok(Self {
98 period,
99 ring: RingF64::with_capacity(period),
100 })
101 }
102
103 pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
104 let mut s = Self::new(period)?;
105 s.push_bars(closes)?;
106 Ok(s)
107 }
108
109 pub fn period(&self) -> usize {
110 self.period
111 }
112
113 pub fn reset(&mut self) {
114 self.ring.clear();
115 }
116
117 /// Push one close. `None` until `period` samples seen.
118 pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
119 require_finite("close", close)?;
120 self.ring.push(close);
121 if self.ring.is_full() {
122 Ok(Some(self.ring.sum() / self.period as f64))
123 } else {
124 Ok(None)
125 }
126 }
127
128 /// Push many closes (one streaming payload). One output per input.
129 pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
130 let mut out = Vec::with_capacity(closes.len());
131 for &c in closes {
132 out.push(self.push(c)?);
133 }
134 Ok(out)
135 }
136
137 pub fn last(&self) -> Option<f64> {
138 if self.ring.is_full() {
139 Some(self.ring.sum() / self.period as f64)
140 } else {
141 None
142 }
143 }
144}
145
146/// Incremental EMA (α = 2/(period+1), seed = SMA of first `period` closes).
147///
148/// Batch [`ema`] uses this state end-to-end.
149///
150/// # Examples
151/// ```
152/// use finance_solution::stocks::ta::{EmaState, ema};
153/// let closes: Vec<f64> = (1..=20).map(|x| x as f64).collect();
154/// let batch = ema(&closes, 5).unwrap();
155/// let mut st = EmaState::new(5).unwrap();
156/// let mut last = None;
157/// for &c in &closes {
158/// last = st.push(c).unwrap();
159/// }
160/// assert!((last.unwrap() - batch[19].unwrap()).abs() < 1e-9);
161/// ```
162#[derive(Clone, Debug)]
163pub struct EmaState {
164 period: usize,
165 alpha: f64,
166 seed: RingF64,
167 value: Option<f64>,
168}
169
170impl EmaState {
171 pub fn new(period: usize) -> FinanceResult<Self> {
172 let period = PeriodLength::new(period)?.get();
173 Ok(Self {
174 period,
175 alpha: 2.0 / (period as f64 + 1.0),
176 seed: RingF64::with_capacity(period),
177 value: None,
178 })
179 }
180
181 pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
182 let mut s = Self::new(period)?;
183 s.push_bars(closes)?;
184 Ok(s)
185 }
186
187 pub fn period(&self) -> usize {
188 self.period
189 }
190
191 pub fn reset(&mut self) {
192 self.seed.clear();
193 self.value = None;
194 }
195
196 pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
197 require_finite("close", close)?;
198 if let Some(prev) = self.value {
199 let next = self.alpha * close + (1.0 - self.alpha) * prev;
200 self.value = Some(next);
201 return Ok(Some(next));
202 }
203 self.seed.push(close);
204 if self.seed.is_full() {
205 let seed = self.seed.sum() / self.period as f64;
206 self.value = Some(seed);
207 Ok(Some(seed))
208 } else {
209 Ok(None)
210 }
211 }
212
213 pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
214 let mut out = Vec::with_capacity(closes.len());
215 for &c in closes {
216 out.push(self.push(c)?);
217 }
218 Ok(out)
219 }
220
221 pub fn last(&self) -> Option<f64> {
222 self.value
223 }
224}
225
226/// SMA of `period` closes. Leading `period - 1` values are `None`.
227///
228/// Implemented via [`SmaState::push_bars`] so batch and streaming stay bit-identical.
229///
230/// # Errors
231/// Empty input, non-finite values, or `period == 0`.
232///
233/// # Examples
234/// ```
235/// use finance_solution::stocks::ta::sma;
236/// let c = [1.0, 2.0, 3.0, 4.0, 5.0];
237/// let s = sma(&c, 3).unwrap();
238/// assert_eq!(s[0], None);
239/// assert_eq!(s[1], None);
240/// assert!((s[2].unwrap() - 2.0).abs() < 1e-12); // (1+2+3)/3
241/// assert!((s[4].unwrap() - 4.0).abs() < 1e-12); // (3+4+5)/3
242/// ```
243///
244/// Short series (shorter than period) — all `None`, still `Ok`:
245/// ```
246/// use finance_solution::stocks::ta::sma;
247/// let s = sma(&[1.0, 2.0], 5).unwrap();
248/// assert!(s.iter().all(|x| x.is_none()));
249/// ```
250pub fn sma(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
251 validate_closes(closes)?;
252 let mut st = SmaState::new(period)?;
253 st.push_bars(closes)
254}
255
256/// EMA with span `period` (α = 2 / (period + 1)). Seed = SMA of the first `period` closes.
257///
258/// Implemented via [`EmaState::push_bars`].
259///
260/// # Errors
261/// Same domain as [`sma`].
262///
263/// # Examples
264/// ```
265/// use finance_solution::stocks::ta::ema;
266/// let c: Vec<f64> = (1..=20).map(|x| x as f64).collect();
267/// let e = ema(&c, 5).unwrap();
268/// assert!(e[3].is_none());
269/// assert!(e[4].is_some());
270/// assert!(e[19].unwrap().is_finite());
271/// ```
272pub fn ema(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
273 validate_closes(closes)?;
274 let mut st = EmaState::new(period)?;
275 st.push_bars(closes)
276}
277
278/// Last defined SMA value, if any.
279///
280/// Uses [`SmaState`] end-to-end (no intermediate full `Vec` of options beyond the push loop).
281/// Prefer holding an [`SmaState`] across live bars instead of calling this on growing history.
282///
283/// # Examples
284/// ```
285/// use finance_solution::stocks::ta::sma_last;
286/// assert_eq!(sma_last(&[1.0, 2.0, 3.0], 3).unwrap(), Some(2.0));
287/// assert_eq!(sma_last(&[1.0, 2.0], 3).unwrap(), None);
288/// ```
289#[inline]
290pub fn sma_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
291 validate_closes(closes)?;
292 let mut st = SmaState::new(period)?;
293 for &c in closes {
294 st.push(c)?;
295 }
296 Ok(st.last())
297}
298
299/// Last defined EMA value, if any (via [`EmaState`]).
300///
301/// # Examples
302/// ```
303/// use finance_solution::stocks::ta::{ema, ema_last};
304/// let c: Vec<f64> = (1..=15).map(|x| x as f64).collect();
305/// let series = ema(&c, 5).unwrap();
306/// let last = ema_last(&c, 5).unwrap();
307/// assert_eq!(last, series[14]);
308/// ```
309#[inline]
310pub fn ema_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
311 validate_closes(closes)?;
312 let mut st = EmaState::new(period)?;
313 for &c in closes {
314 st.push(c)?;
315 }
316 Ok(st.last())
317}
318
319fn validate_closes(closes: &[f64]) -> FinanceResult<()> {
320 if closes.is_empty() {
321 return Err(FinanceError::EmptyInput { what: "closes" });
322 }
323 for &c in closes {
324 require_finite("close", c)?;
325 }
326 Ok(())
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn sma_constant() {
335 let c = [10.0; 5];
336 let s = sma(&c, 3).unwrap();
337 assert_eq!(s[2], Some(10.0));
338 assert_eq!(s[4], Some(10.0));
339 }
340
341 #[test]
342 fn ema_runs() {
343 let c: Vec<_> = (1..=30).map(|x| x as f64).collect();
344 let e = ema(&c, 10).unwrap();
345 assert!(e[8].is_none());
346 assert!(e[9].is_some());
347 }
348
349 #[test]
350 fn rejects_zero_period() {
351 assert!(sma(&[1.0, 2.0], 0).is_err());
352 }
353
354 #[test]
355 fn last_matches_series_tail() {
356 let c: Vec<_> = (1..=25).map(|x| x as f64 * 0.5).collect();
357 let s = sma(&c, 7).unwrap();
358 assert_eq!(sma_last(&c, 7).unwrap(), s[24]);
359 let e = ema(&c, 7).unwrap();
360 assert_eq!(ema_last(&c, 7).unwrap(), e[24]);
361 }
362}