finance_solution/stocks/ta/
momentum.rs1use crate::stocks::ta::common::{opt_cell, validate_series};
51use crate::stocks::ta::ring::RingF64;
52use crate::util::error::{require_finite, FinanceResult};
53use crate::util::primitives::PeriodLength;
54use crate::{columns_with_strings, print_table_locale_opt};
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
58pub struct MomParams {
59 pub period: usize,
60}
61
62impl MomParams {
63 pub const fn new(period: usize) -> Self {
64 Self { period }
65 }
66
67 pub const fn period_10() -> Self {
68 Self { period: 10 }
69 }
70
71 pub const fn period_12() -> Self {
72 Self { period: 12 }
73 }
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
77pub struct ValidatedMom {
78 params: MomParams,
79}
80
81impl ValidatedMom {
82 pub fn new(params: MomParams) -> FinanceResult<Self> {
83 PeriodLength::new(params.period)?;
84 Ok(Self { params })
85 }
86
87 pub fn params(self) -> MomParams {
88 self.params
89 }
90
91 pub fn compute_mom(self, closes: &[f64]) -> FinanceResult<MomSeries> {
92 mom_validated(closes, self)
93 }
94
95 pub fn compute_roc(self, closes: &[f64]) -> FinanceResult<RocSeries> {
96 roc_validated(closes, self)
97 }
98
99 pub fn compute_rocp(self, closes: &[f64]) -> FinanceResult<RocpSeries> {
100 rocp_validated(closes, self)
101 }
102}
103
104#[derive(Clone, Debug, PartialEq)]
105pub struct MomSeries {
106 pub mom: Vec<Option<f64>>,
107 pub params: MomParams,
108}
109
110#[derive(Clone, Debug, PartialEq)]
111pub struct RocSeries {
112 pub roc: Vec<Option<f64>>,
113 pub params: MomParams,
114}
115
116#[derive(Clone, Debug, PartialEq)]
117pub struct RocpSeries {
118 pub rocp: Vec<Option<f64>>,
119 pub params: MomParams,
120}
121
122#[derive(Clone, Copy, Debug, PartialEq)]
124pub struct MomBarOutput {
125 pub mom: Option<f64>,
126 pub roc: Option<f64>,
127 pub rocp: Option<f64>,
128}
129
130#[derive(Clone, Debug)]
134pub struct MomState {
135 params: MomParams,
136 lag: RingF64,
138 last: Option<MomBarOutput>,
139}
140
141impl MomState {
142 pub fn new(params: MomParams) -> FinanceResult<Self> {
143 let _ = ValidatedMom::new(params)?;
144 Ok(Self {
145 params,
146 lag: RingF64::with_capacity(params.period),
147 last: None,
148 })
149 }
150
151 pub fn from_history(params: MomParams, closes: &[f64]) -> FinanceResult<Self> {
152 let mut s = Self::new(params)?;
153 let _ = s.push_bars(closes)?;
154 Ok(s)
155 }
156
157 pub fn params(&self) -> MomParams {
158 self.params
159 }
160
161 pub fn reset(&mut self) {
162 self.lag.clear();
163 self.last = None;
164 }
165
166 pub fn push(&mut self, close: f64) -> FinanceResult<Option<MomBarOutput>> {
167 require_finite("close", close)?;
168 let out = if self.lag.is_full() {
169 let lag = self.lag.oldest().unwrap();
170 let mom = close - lag;
171 let (roc, rocp) = if lag == 0.0 {
172 (None, None)
173 } else {
174 let frac = (close - lag) / lag;
175 (Some(100.0 * frac), Some(frac))
176 };
177 let bar = MomBarOutput {
178 mom: Some(mom),
179 roc,
180 rocp,
181 };
182 self.last = Some(bar);
183 Some(bar)
184 } else {
185 self.last = None;
186 None
187 };
188 let _ = self.lag.push(close);
189 Ok(out)
190 }
191
192 pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<MomBarOutput>>> {
193 validate_series("close", closes)?;
194 let mut out = Vec::with_capacity(closes.len());
195 for &c in closes {
196 out.push(self.push(c)?);
197 }
198 Ok(out)
199 }
200
201 pub fn last(&self) -> Option<MomBarOutput> {
202 self.last
203 }
204}
205
206pub fn mom(closes: &[f64], params: MomParams) -> FinanceResult<MomSeries> {
207 ValidatedMom::new(params)?.compute_mom(closes)
208}
209
210pub fn roc(closes: &[f64], params: MomParams) -> FinanceResult<RocSeries> {
211 ValidatedMom::new(params)?.compute_roc(closes)
212}
213
214pub fn rocp(closes: &[f64], params: MomParams) -> FinanceResult<RocpSeries> {
215 ValidatedMom::new(params)?.compute_rocp(closes)
216}
217
218fn mom_validated(closes: &[f64], eng: ValidatedMom) -> FinanceResult<MomSeries> {
219 let mut st = MomState::new(eng.params)?;
220 let bars = st.push_bars(closes)?;
221 Ok(MomSeries {
222 mom: bars.iter().map(|b| b.and_then(|x| x.mom)).collect(),
223 params: eng.params,
224 })
225}
226
227fn roc_validated(closes: &[f64], eng: ValidatedMom) -> FinanceResult<RocSeries> {
228 let mut st = MomState::new(eng.params)?;
229 let bars = st.push_bars(closes)?;
230 Ok(RocSeries {
231 roc: bars.iter().map(|b| b.and_then(|x| x.roc)).collect(),
232 params: eng.params,
233 })
234}
235
236fn rocp_validated(closes: &[f64], eng: ValidatedMom) -> FinanceResult<RocpSeries> {
237 let mut st = MomState::new(eng.params)?;
238 let bars = st.push_bars(closes)?;
239 Ok(RocpSeries {
240 rocp: bars.iter().map(|b| b.and_then(|x| x.rocp)).collect(),
241 params: eng.params,
242 })
243}
244
245#[derive(Clone, Debug)]
246pub struct MomSolution {
247 series: MomSeries,
248 roc: RocSeries,
249 close: Vec<f64>,
250 formula: String,
251}
252
253impl MomSolution {
254 pub fn series(&self) -> &MomSeries {
255 &self.series
256 }
257 pub fn roc_series(&self) -> &RocSeries {
258 &self.roc
259 }
260 pub fn formula(&self) -> &str {
261 &self.formula
262 }
263
264 pub fn print_table(&self) {
265 self.print_table_locale_opt(None, None);
266 }
267
268 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
269 self.print_table_locale_opt(Some(locale), Some(precision));
270 }
271
272 fn print_table_locale_opt(
273 &self,
274 locale: Option<&num_format::Locale>,
275 precision: Option<usize>,
276 ) {
277 let columns = columns_with_strings(&[
278 ("period", "i", true),
279 ("close", "f", true),
280 ("mom", "f", true),
281 ("roc", "f", true),
282 ]);
283 let data = self
284 .close
285 .iter()
286 .enumerate()
287 .map(|(i, c)| {
288 vec![
289 i.to_string(),
290 c.to_string(),
291 opt_cell(self.series.mom[i]),
292 opt_cell(self.roc.roc[i]),
293 ]
294 })
295 .collect();
296 print_table_locale_opt(&columns, data, locale, precision);
297 }
298}
299
300pub fn mom_solution(closes: &[f64], params: MomParams) -> FinanceResult<MomSolution> {
308 let series = mom(closes, params)?;
309 let roc = roc(closes, params)?;
310 Ok(MomSolution {
311 series,
312 roc,
313 close: closes.to_vec(),
314 formula: format!(
315 "MOM = C - C[n-{}]; ROC% = 100*(C-C_lag)/C_lag",
316 params.period
317 ),
318 })
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn mom_word_problem() {
327 let c = [1.0, 2.0, 3.0, 4.0, 10.0];
328 let s = mom(&c, MomParams::new(2)).unwrap();
329 assert!(s.mom[0].is_none());
330 assert!(s.mom[1].is_none());
331 assert!((s.mom[2].unwrap() - 2.0).abs() < 1e-12); assert!((s.mom[4].unwrap() - 7.0).abs() < 1e-12); }
334
335 #[test]
336 fn roc_percent() {
337 let c = [100.0, 100.0, 110.0];
338 let s = roc(&c, MomParams::new(2)).unwrap();
339 assert!((s.roc[2].unwrap() - 10.0).abs() < 1e-12);
340 }
341
342 #[test]
343 fn zero_lag_roc_none() {
344 let c = [0.0, 1.0, 2.0];
345 let s = roc(&c, MomParams::new(2)).unwrap();
346 assert!(s.roc[2].is_none());
347 }
348
349 #[test]
350 fn state_parity() {
351 let c: Vec<_> = (0..30).map(|i| 100.0 + i as f64 * 0.5).collect();
352 let p = MomParams::period_10();
353 let batch = mom(&c, p).unwrap();
354 let mut st = MomState::new(p).unwrap();
355 for i in 0..c.len() {
356 let o = st.push(c[i]).unwrap();
357 match (o.and_then(|b| b.mom), batch.mom[i]) {
358 (None, None) => {}
359 (Some(a), Some(b)) => assert!((a - b).abs() < 1e-12),
360 other => panic!("{other:?}"),
361 }
362 }
363 }
364}