Skip to main content

quantwave_core/indicators/
momentum.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2pub use crate::indicators::incremental::macd::MACD;
3pub use crate::indicators::incremental::rsi::RSI;
4impl From<usize> for RSI {
5    fn from(p: usize) -> Self {
6        Self::new(p)
7    }
8}
9pub use crate::indicators::incremental::macd_ext::{MACDEXT, MACDFIX};
10
11pub use crate::indicators::incremental::stoch::{STOCH, STOCHF, STOCHRSI};
12pub use crate::indicators::incremental::dmi::{ADX, ADXR, DX, MINUS_DI, PLUS_DI};
13impl From<usize> for ADX {
14    fn from(p: usize) -> Self {
15        Self::new(p)
16    }
17}
18impl From<usize> for ADXR {
19    fn from(p: usize) -> Self {
20        Self::new(p)
21    }
22}
23pub use crate::indicators::incremental::cci::CCI;
24impl From<usize> for CCI {
25    fn from(p: usize) -> Self {
26        Self::new(p)
27    }
28}
29pub use crate::indicators::incremental::mom::{MOM, ROC, ROCP, ROCR, ROCR100};
30impl From<usize> for MOM {
31    fn from(p: usize) -> Self {
32        Self::new(p)
33    }
34}
35impl From<usize> for ROC {
36    fn from(p: usize) -> Self {
37        Self::new(p)
38    }
39}
40impl From<usize> for ROCP {
41    fn from(p: usize) -> Self {
42        Self::new(p)
43    }
44}
45impl From<usize> for ROCR {
46    fn from(p: usize) -> Self {
47        Self::new(p)
48    }
49}
50impl From<usize> for ROCR100 {
51    fn from(p: usize) -> Self {
52        Self::new(p)
53    }
54}
55pub use crate::indicators::incremental::willr::WILLR;
56impl From<usize> for WILLR {
57    fn from(p: usize) -> Self {
58        Self::new(p)
59    }
60}
61pub use crate::indicators::incremental::apo::{APO, PPO};
62pub use crate::indicators::incremental::simple::BOP;
63pub use crate::indicators::incremental::cmo::CMO;
64impl From<usize> for CMO {
65    fn from(p: usize) -> Self {
66        Self::new(p)
67    }
68}
69pub use crate::indicators::incremental::aroon::{AROON, AROONOSC};
70impl From<usize> for AROON {
71    fn from(p: usize) -> Self {
72        Self::new(p)
73    }
74}
75impl From<usize> for AROONOSC {
76    fn from(p: usize) -> Self {
77        Self::new(p)
78    }
79}
80pub use crate::indicators::incremental::simple::MFI;
81impl From<usize> for MFI {
82    fn from(p: usize) -> Self {
83        Self::new(p)
84    }
85}
86pub use crate::indicators::incremental::trix::TRIX;
87impl From<usize> for TRIX {
88    fn from(p: usize) -> Self {
89        Self::new(p)
90    }
91}
92pub use crate::indicators::incremental::ultosc::ULTOSC;
93impl From<usize> for DX {
94    fn from(p: usize) -> Self {
95        Self::new(p)
96    }
97}
98impl From<usize> for PLUS_DI {
99    fn from(p: usize) -> Self {
100        Self::new(p)
101    }
102}
103impl From<usize> for MINUS_DI {
104    fn from(p: usize) -> Self {
105        Self::new(p)
106    }
107}
108pub use crate::indicators::incremental::dm::{MINUS_DM, PLUS_DM};
109impl From<usize> for PLUS_DM {
110    fn from(p: usize) -> Self {
111        Self::new(p)
112    }
113}
114impl From<usize> for MINUS_DM {
115    fn from(p: usize) -> Self {
116        Self::new(p)
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::traits::Next;
124    use proptest::prelude::*;
125
126    proptest! {
127        #[test]
128        fn test_rsi_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
129            let period = 14;
130            let mut rsi = RSI::new(period);
131            let streaming_results: Vec<f64> = input.iter().map(|&x| rsi.next(x)).collect();
132            let batch_results = talib_rs::momentum::rsi(&input, period).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
133
134            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
135                if s.is_nan() {
136                    assert!(b.is_nan());
137                } else {
138                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
139                }
140            }
141        }
142
143        #[test]
144        fn test_macd_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
145            let fast = 12;
146            let slow = 26;
147            let signal = 9;
148            let mut macd = MACD::new(fast, slow, signal);
149            let streaming_results: Vec<(f64, f64, f64)> = input.iter().map(|&x| macd.next(x)).collect();
150            let (b_macd, b_signal, b_hist) = talib_rs::momentum::macd(&input, fast, slow, signal).unwrap_or_else(|_| {
151                (vec![f64::NAN; input.len()], vec![f64::NAN; input.len()], vec![f64::NAN; input.len()])
152            });
153
154            for (i, (s_macd, s_signal, s_hist)) in streaming_results.into_iter().enumerate() {
155                if s_macd.is_nan() {
156                    assert!(b_macd[i].is_nan());
157                } else {
158                    approx::assert_relative_eq!(s_macd, b_macd[i], epsilon = 1e-6);
159                }
160                if s_signal.is_nan() {
161                    assert!(b_signal[i].is_nan());
162                } else {
163                    approx::assert_relative_eq!(s_signal, b_signal[i], epsilon = 1e-6);
164                }
165                if s_hist.is_nan() {
166                    assert!(b_hist[i].is_nan());
167                } else {
168                    approx::assert_relative_eq!(s_hist, b_hist[i], epsilon = 1e-6);
169                }
170            }
171        }
172
173        #[test]
174        fn test_stoch_parity(
175            h in prop::collection::vec(1.0..100.0, 1..100),
176            l in prop::collection::vec(1.0..100.0, 1..100),
177            c in prop::collection::vec(1.0..100.0, 1..100)
178        ) {
179            let len = h.len().min(l.len()).min(c.len());
180            if len == 0 { return Ok(()); }
181            let mut high = Vec::with_capacity(len);
182            let mut low = Vec::with_capacity(len);
183            let mut close = Vec::with_capacity(len);
184            for i in 0..len {
185                let val_h: f64 = h[i];
186                let val_l: f64 = l[i];
187                let val_c: f64 = c[i];
188                let max: f64 = val_h.max(val_l).max(val_c);
189                let min: f64 = val_h.min(val_l).min(val_c);
190                high.push(max);
191                low.push(min);
192                close.push(val_c);
193            }
194
195            let fastk = 5;
196            let slowk = 3;
197            let slowk_ma = talib_rs::MaType::Sma;
198            let slowd = 3;
199            let slowd_ma = talib_rs::MaType::Sma;
200
201            let mut stoch = STOCH::new(fastk, slowk, slowk_ma, slowd, slowd_ma);
202            let streaming_results: Vec<(f64, f64)> = (0..len).map(|i| stoch.next((high[i], low[i], close[i]))).collect();
203            let (b_k, b_d) = talib_rs::momentum::stoch(&high, &low, &close, fastk, slowk, slowk_ma, slowd, slowd_ma).unwrap_or_else(|_| {
204                (vec![f64::NAN; len], vec![f64::NAN; len])
205            });
206
207            for (i, (s_k, s_d)) in streaming_results.into_iter().enumerate() {
208                if s_k.is_nan() {
209                    assert!(b_k[i].is_nan());
210                } else {
211                    approx::assert_relative_eq!(s_k, b_k[i], epsilon = 1e-6);
212                }
213                if s_d.is_nan() {
214                    assert!(b_d[i].is_nan());
215                } else {
216                    approx::assert_relative_eq!(s_d, b_d[i], epsilon = 1e-6);
217                }
218            }
219        }
220
221        #[test]
222        fn test_adx_parity(
223            h in prop::collection::vec(1.0..100.0, 1..100),
224            l in prop::collection::vec(1.0..100.0, 1..100),
225            c in prop::collection::vec(1.0..100.0, 1..100)
226        ) {
227            let len = h.len().min(l.len()).min(c.len());
228            if len == 0 { return Ok(()); }
229            let mut high = Vec::with_capacity(len);
230            let mut low = Vec::with_capacity(len);
231            let mut close = Vec::with_capacity(len);
232            for i in 0..len {
233                let val_h: f64 = h[i];
234                let val_l: f64 = l[i];
235                let val_c: f64 = c[i];
236                let max: f64 = val_h.max(val_l).max(val_c);
237                let min: f64 = val_h.min(val_l).min(val_c);
238                high.push(max);
239                low.push(min);
240                close.push(val_c);
241            }
242
243            let period = 14;
244            let mut adx = ADX::new(period);
245            let streaming_results: Vec<f64> = (0..len).map(|i| adx.next((high[i], low[i], close[i]))).collect();
246            let batch_results = talib_rs::momentum::adx(&high, &low, &close, period).unwrap_or_else(|_| vec![f64::NAN; len]);
247
248            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
249                if s.is_nan() {
250                    assert!(b.is_nan());
251                } else {
252                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
253                }
254            }
255        }
256    }
257}
258
259pub const RSI_METADATA: IndicatorMetadata = IndicatorMetadata {
260    name: "Relative Strength Index (RSI)",
261    description: "A momentum oscillator that measures the speed and change of price movements.",
262    usage: "Use to identify overbought (>70) and oversold (<30) conditions. RSI divergences against price often signal impending trend reversals.",
263    keywords: &[
264        "momentum",
265        "oscillator",
266        "overbought",
267        "oversold",
268        "classic",
269    ],
270    ehlers_summary: "Developed by J. Welles Wilder in New Concepts in Technical Trading Systems (1978), the RSI compares the magnitude of recent gains to recent losses to determine overbought and oversold conditions of an asset. It remains the most widely used momentum oscillator in modern technical analysis.",
271    params: &[ParamDef {
272        name: "timeperiod",
273        default: "14",
274        description: "Lookback period",
275    }],
276    formula_source: "https://www.investopedia.com/terms/r/rsi.asp",
277    formula_latex: r#"
278\[
279RS = \frac{Average Gain}{Average Loss} \\ RSI = 100 - \frac{100}{1 + RS}
280\]
281"#,
282    gold_standard_file: "rsi.json",
283    category: "Classic",
284};
285
286pub const MACD_METADATA: IndicatorMetadata = IndicatorMetadata {
287    name: "Moving Average Convergence Divergence (MACD)",
288    description: "A trend-following momentum indicator that shows the relationship between two moving averages.",
289    usage: "Use to identify trend direction and momentum. Crossovers of the MACD line and signal line provide entry and exit signals, while the histogram shows the strength of the trend.",
290    keywords: &["trend", "momentum", "moving-average", "classic"],
291    ehlers_summary: "Gerald Appel developed the MACD in the late 1970s. It is calculated by subtracting the 26-period EMA from the 12-period EMA. A nine-day EMA of the MACD, called the 'signal line,' is then plotted on top of the MACD line, which can function as a trigger for buy and sell signals. — Investopedia",
292    params: &[
293        ParamDef {
294            name: "fastperiod",
295            default: "12",
296            description: "Fast EMA period",
297        },
298        ParamDef {
299            name: "slowperiod",
300            default: "26",
301            description: "Slow EMA period",
302        },
303        ParamDef {
304            name: "signalperiod",
305            default: "9",
306            description: "Signal EMA period",
307        },
308    ],
309    formula_source: "https://www.investopedia.com/terms/m/macd.asp",
310    formula_latex: r#"
311\[
312MACD = EMA(12) - EMA(26) \\ Signal = EMA(MACD, 9)
313\]
314"#,
315    gold_standard_file: "macd.json",
316    category: "Classic",
317};
318
319pub const STOCH_METADATA: IndicatorMetadata = IndicatorMetadata {
320    name: "Stochastic Oscillator",
321    description: "A momentum indicator comparing a particular closing price of a security to a range of its prices over a certain period of time.",
322    usage: "Use to identify trend reversals by looking for crossovers and overbought/oversold levels. The %K and %D lines indicate when the momentum is shifting relative to the recent price range.",
323    keywords: &[
324        "momentum",
325        "oscillator",
326        "overbought",
327        "oversold",
328        "classic",
329    ],
330    ehlers_summary: "George Lane developed the Stochastic Oscillator in the 1950s. It is based on the observation that in an uptrend, prices tend to close near their high, and in a downtrend, they tend to close near their low. The sensitivity of the oscillator to market movements is reducible by adjusting the time period or by taking a moving average of the result. — StockCharts ChartSchool",
331    params: &[
332        ParamDef {
333            name: "fastk_period",
334            default: "5",
335            description: "Fast %K period",
336        },
337        ParamDef {
338            name: "slowk_period",
339            default: "3",
340            description: "Slow %K period",
341        },
342        ParamDef {
343            name: "slowd_period",
344            default: "3",
345            description: "Slow %D period",
346        },
347    ],
348    formula_source: "https://www.investopedia.com/terms/s/stochasticoscillator.asp",
349    formula_latex: r#"
350\[
351\%K = 100 \times \frac{C - L14}{H14 - L14} \\ \%D = 3\text{-period SMA of } \%K
352\]
353"#,
354    gold_standard_file: "stoch.json",
355    category: "Classic",
356};
357
358pub const ADX_METADATA: IndicatorMetadata = IndicatorMetadata {
359    name: "Average Directional Index (ADX)",
360    description: "An indicator used to quantify trend strength without regard to trend direction.",
361    usage: "Use to determine if the market is trending or ranging. ADX values above 25 indicate a strong trend, while values below 20 indicate a weak or non-trending market.",
362    keywords: &["trend", "volatility", "classic", "wilder"],
363    ehlers_summary: "Developed by J. Welles Wilder, the ADX is derived from two other indicators, also developed by Wilder: the Positive Directional Indicator (+DI) and the Negative Directional Indicator (-DI). While +DI and -DI indicate trend direction, ADX measures the strength of that trend. — StockCharts ChartSchool",
364    params: &[ParamDef {
365        name: "timeperiod",
366        default: "14",
367        description: "Lookback period",
368    }],
369    formula_source: "https://www.investopedia.com/terms/a/adx.asp",
370    formula_latex: r#"
371\[
372ADX = 100 \times \frac{\text{EMA}(|(+DI) - (-DI)| / |(+DI) + (-DI)|, n)}{n}
373\]
374"#,
375    gold_standard_file: "adx.json",
376    category: "Classic",
377};
378
379pub const CCI_METADATA: IndicatorMetadata = IndicatorMetadata {
380    name: "Commodity Channel Index (CCI)",
381    description: "A versatile indicator that can be used to identify a new trend or warn of extreme conditions.",
382    usage: "Use to identify cyclical turns in commodities or stocks. Readings above +100 imply a strong uptrend, while readings below -100 imply a strong downtrend.",
383    keywords: &["momentum", "oscillator", "classic", "mean-reversion"],
384    ehlers_summary: "Developed by Donald Lambert in 1980, the CCI measures the current price level relative to an average price level over a given period. CCI is relatively high when prices are far above their average and relatively low when prices are far below their average. — StockCharts ChartSchool",
385    params: &[ParamDef {
386        name: "timeperiod",
387        default: "14",
388        description: "Lookback period",
389    }],
390    formula_source: "https://www.investopedia.com/terms/c/commoditychannelindex.asp",
391    formula_latex: r#"
392\[
393CCI = \frac{Price - SMA}{0.015 \times \text{Mean Deviation}}
394\]
395"#,
396    gold_standard_file: "cci.json",
397    category: "Classic",
398};
399
400pub const WILLR_METADATA: IndicatorMetadata = IndicatorMetadata {
401    name: "Williams %R",
402    description: "A momentum indicator that measures overbought and oversold levels, similar to a stochastic oscillator.",
403    usage: "Use to identify entry and exit points in the market. Readings from 0 to -20 are considered overbought, while readings from -80 to -100 are considered oversold.",
404    keywords: &[
405        "momentum",
406        "oscillator",
407        "overbought",
408        "oversold",
409        "classic",
410    ],
411    ehlers_summary: "Developed by Larry Williams, %R compares the closing price of a stock to the high-low range over a specific period, typically 14 days. It is used to determine when a stock might be overbought or oversold and to identify potential trend reversals. — StockCharts ChartSchool",
412    params: &[ParamDef {
413        name: "timeperiod",
414        default: "14",
415        description: "Lookback period",
416    }],
417    formula_source: "https://www.investopedia.com/terms/w/williamsr.asp",
418    formula_latex: r#"
419\[
420\%R = \frac{\text{Highest High} - \text{Close}}{\text{Highest High} - \text{Lowest Low}} \times -100
421\]
422"#,
423    gold_standard_file: "willr.json",
424    category: "Classic",
425};
426
427pub const MFI_METADATA: IndicatorMetadata = IndicatorMetadata {
428    name: "Money Flow Index (MFI)",
429    description: "A technical oscillator that uses price and volume data for identifying overbought or oversold signals.",
430    usage: "Use as a volume-weighted RSI. Divergences between MFI and price can signal potential reversals, especially when the MFI is in extreme territory (>80 or <20).",
431    keywords: &["momentum", "volume", "oscillator", "classic"],
432    ehlers_summary: "The Money Flow Index (MFI) is a momentum indicator that measures the inflow and outflow of money into an asset over a specific period of time. It is related to the RSI but incorporates volume, whereas the RSI only considers price. — Investopedia",
433    params: &[ParamDef {
434        name: "timeperiod",
435        default: "14",
436        description: "Lookback period",
437    }],
438    formula_source: "https://www.investopedia.com/terms/m/mfi.asp",
439    formula_latex: r#"
440\[
441\text{Money Ratio} = \frac{\text{Positive Money Flow}}{\text{Negative Money Flow}} \\ MFI = 100 - \frac{100}{1 + \text{Money Ratio}}
442\]
443"#,
444    gold_standard_file: "mfi.json",
445    category: "Classic",
446};
447
448pub const AROON_METADATA: IndicatorMetadata = IndicatorMetadata {
449    name: "Aroon Indicator",
450    description: "An indicator system that identifies when a new trend is beginning and the strength of the trend.",
451    usage: "Use to identify when a security is trending and when it is in a range-bound period. Aroon Up crossing above Aroon Down signals the start of a new uptrend.",
452    keywords: &["trend", "classic", "breakout"],
453    ehlers_summary: "Developed by Tushar Chande in 1995, the Aroon indicator focuses on the time between highs and the time between lows over a given period. The idea is that strong uptrends will regularly see new highs, and strong downtrends will regularly see new lows. — StockCharts ChartSchool",
454    params: &[ParamDef {
455        name: "timeperiod",
456        default: "25",
457        description: "Lookback period",
458    }],
459    formula_source: "https://www.investopedia.com/terms/a/aroon.asp",
460    formula_latex: r#"
461\[
462\text{Aroon Up} = \frac{n - \text{Periods since n-period High}}{n} \times 100
463\]
464"#,
465    gold_standard_file: "aroon.json",
466    category: "Classic",
467};
468
469pub const ULTOSC_METADATA: IndicatorMetadata = IndicatorMetadata {
470    name: "Ultimate Oscillator",
471    description: "A momentum oscillator designed to capture momentum across three different timeframes.",
472    usage: "Use to avoid the pitfalls of oscillators that are limited to a single timeframe. Buy signals are generated when there is bullish divergence between price and the indicator.",
473    keywords: &["momentum", "oscillator", "classic", "multi-timeframe"],
474    ehlers_summary: "Developed by Larry Williams in 1976, the Ultimate Oscillator uses weighted averages of three different timeframes to reduce the volatility and false signals common in other oscillators. It remains a staple for identifying divergence across short, medium, and long-term price action. — StockCharts ChartSchool",
475    params: &[
476        ParamDef {
477            name: "timeperiod1",
478            default: "7",
479            description: "Short period",
480        },
481        ParamDef {
482            name: "timeperiod2",
483            default: "14",
484            description: "Medium period",
485        },
486        ParamDef {
487            name: "timeperiod3",
488            default: "28",
489            description: "Long period",
490        },
491    ],
492    formula_source: "https://www.investopedia.com/terms/u/ultimateoscillator.asp",
493    formula_latex: r#"
494\[
495\text{BP} = \text{Close} - \min(\text{Low}, \text{PrevClose}) \\ \text{TR} = \max(\text{High}, \text{PrevClose}) - \min(\text{Low}, \text{PrevClose})
496\]
497"#,
498    gold_standard_file: "ultosc.json",
499    category: "Classic",
500};
501
502pub const TRIX_METADATA: IndicatorMetadata = IndicatorMetadata {
503    name: "TRIX",
504    description: "A momentum oscillator that shows the percent rate of change of a triple exponentially smoothed moving average.",
505    usage: "Use to filter out market noise and identify trend reversals. TRIX crossings of the zero line or a signal line can provide trade entries.",
506    keywords: &["momentum", "oscillator", "smoothing", "classic"],
507    ehlers_summary: "Developed by Jack Hutson in the early 1980s, TRIX is a powerful momentum oscillator that effectively filters out minor price fluctuations. By triple-smoothing an EMA, it emphasizes the underlying trend and provides a clear signal when the trend changes direction. — StockCharts ChartSchool",
508    params: &[ParamDef {
509        name: "timeperiod",
510        default: "15",
511        description: "Smoothing period",
512    }],
513    formula_source: "https://www.investopedia.com/terms/t/trix.asp",
514    formula_latex: r#"
515\[
516TRIX = \frac{EMA3_t - EMA3_{t-1}}{EMA3_{t-1}} \times 100
517\]
518"#,
519    gold_standard_file: "trix.json",
520    category: "Classic",
521};
522
523pub const MOM_METADATA: IndicatorMetadata = IndicatorMetadata {
524    name: "Momentum (MOM)",
525    description: "A simple indicator that measures the amount that a security's price has changed over a given span of time.",
526    usage: "Use to measure the velocity of price changes. Positive values indicate an uptrend, while negative values indicate a downtrend.",
527    keywords: &["momentum", "classic", "trend"],
528    ehlers_summary: "Momentum is one of the most basic and powerful concepts in technical analysis. It measures the rate of change of an asset's price, providing a clear indication of trend strength and potential exhaustion before the actual price reversal occurs. — StockCharts ChartSchool",
529    params: &[ParamDef {
530        name: "timeperiod",
531        default: "10",
532        description: "Lookback period",
533    }],
534    formula_source: "https://www.investopedia.com/terms/m/momentum.asp",
535    formula_latex: r#"
536\[
537MOM = Price_t - Price_{t-n}
538\]
539"#,
540    gold_standard_file: "mom.json",
541    category: "Classic",
542};
543
544pub const ROC_METADATA: IndicatorMetadata = IndicatorMetadata {
545    name: "Rate of Change (ROC)",
546    description: "A momentum-based technical indicator that measures the percentage change in price between the current price and the price n periods ago.",
547    usage: "Use to measure the speed at which price is changing. It is often used to identify overbought/oversold conditions and trend reversals.",
548    keywords: &["momentum", "classic", "oscillator"],
549    ehlers_summary: "The Rate of Change (ROC) indicator is a pure momentum oscillator that measures the percentage change in price from one period to the next. It is highly effective at identifying the velocity of a move and anticipating when that velocity is slowing down. — StockCharts ChartSchool",
550    params: &[ParamDef {
551        name: "timeperiod",
552        default: "10",
553        description: "Lookback period",
554    }],
555    formula_source: "https://www.investopedia.com/terms/r/rateofchange.asp",
556    formula_latex: r#"
557\[
558ROC = \frac{Price_t - Price_{t-n}}{Price_{t-n}} \times 100
559\]
560"#,
561    gold_standard_file: "roc.json",
562    category: "Classic",
563};
564
565pub const CMO_METADATA: IndicatorMetadata = IndicatorMetadata {
566    name: "Chande Momentum Oscillator (CMO)",
567    description: "An advanced momentum oscillator developed by Tushar Chande that measures the difference between up and down days.",
568    usage: "Use to identify extreme overbought and oversold conditions. CMO is more sensitive to price action than RSI as it uses unsmoothed data in its internal calculations.",
569    keywords: &[
570        "momentum",
571        "oscillator",
572        "classic",
573        "overbought",
574        "oversold",
575    ],
576    ehlers_summary: "Developed by Tushar Chande in 1994, the CMO is similar to the RSI but uses the net sum of up and down moves in both the numerator and denominator. This makes it more sensitive to price movements and useful for identifying short-term overextensions in the market. — The New Technical Trader",
577    params: &[ParamDef {
578        name: "timeperiod",
579        default: "14",
580        description: "Lookback period",
581    }],
582    formula_source: "https://www.investopedia.com/terms/c/chandemomentumoscillator.asp",
583    formula_latex: r#"
584\[
585CMO = 100 \times \frac{S_u - S_d}{S_u + S_d}
586\]
587"#,
588    gold_standard_file: "cmo.json",
589    category: "Classic",
590};
591
592pub const APO_METADATA: IndicatorMetadata = IndicatorMetadata {
593    name: "Absolute Price Oscillator (APO)",
594    description: "Shows the absolute difference between two moving averages of different periods.",
595    usage: "Use to identify trend crossovers and momentum. It is essentially a MACD without the signal line, showing the raw distance between fast and slow averages.",
596    keywords: &["trend", "momentum", "moving-average", "classic"],
597    ehlers_summary: "The Absolute Price Oscillator (APO) is based on the difference between two exponential moving averages. It is a trend-following indicator that signals a change in direction when the fast EMA crosses the slow EMA, providing a clear visual of trend development. — TA-Lib Documentation",
598    params: &[
599        ParamDef {
600            name: "fastperiod",
601            default: "12",
602            description: "Fast period",
603        },
604        ParamDef {
605            name: "slowperiod",
606            default: "26",
607            description: "Slow period",
608        },
609    ],
610    formula_source: "https://www.tradingview.com/support/solutions/43000501826-absolute-price-oscillator-apo/",
611    formula_latex: r#"
612\[
613APO = EMA(fast) - EMA(slow)
614\]
615"#,
616    gold_standard_file: "apo.json",
617    category: "Classic",
618};
619
620pub const PPO_METADATA: IndicatorMetadata = IndicatorMetadata {
621    name: "Percentage Price Oscillator (PPO)",
622    description: "A momentum oscillator that measures the difference between two moving averages as a percentage of the larger moving average.",
623    usage: "Use to compare trend momentum across different securities with varying price levels. PPO is the percentage version of MACD.",
624    keywords: &[
625        "trend",
626        "momentum",
627        "moving-average",
628        "classic",
629        "normalization",
630    ],
631    ehlers_summary: "The Percentage Price Oscillator (PPO) is identical to the MACD, except that it measures the difference between two moving averages as a percentage. This allows for comparison across different stocks regardless of their price, making it a superior tool for relative strength analysis. — StockCharts ChartSchool",
632    params: &[
633        ParamDef {
634            name: "fastperiod",
635            default: "12",
636            description: "Fast period",
637        },
638        ParamDef {
639            name: "slowperiod",
640            default: "26",
641            description: "Slow period",
642        },
643    ],
644    formula_source: "https://www.investopedia.com/terms/p/ppo.asp",
645    formula_latex: r#"
646\[
647PPO = \frac{EMA(12) - EMA(26)}{EMA(26)} \times 100
648\]
649"#,
650    gold_standard_file: "ppo.json",
651    category: "Classic",
652};