Skip to main content

quantwave_core/indicators/
momentum.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2talib_1_in_1_out!(RSI, talib_rs::momentum::rsi, timeperiod: usize);
3impl From<usize> for RSI {
4    fn from(p: usize) -> Self {
5        Self::new(p)
6    }
7}
8talib_1_in_3_out!(MACD, talib_rs::momentum::macd, fastperiod: usize, slowperiod: usize, signalperiod: usize);
9talib_1_in_3_out!(MACDEXT, talib_rs::momentum::macd_ext, fastperiod: usize, fastmatype: talib_rs::MaType, slowperiod: usize, slowmatype: talib_rs::MaType, signalperiod: usize, signalmatype: talib_rs::MaType);
10talib_1_in_3_out!(MACDFIX, talib_rs::momentum::macd_fix, signalperiod: usize);
11
12talib_3_in_2_out!(STOCH, talib_rs::momentum::stoch, fastk_period: usize, slowk_period: usize, slowk_matype: talib_rs::MaType, slowd_period: usize, slowd_matype: talib_rs::MaType);
13talib_3_in_2_out!(STOCHF, talib_rs::momentum::stochf, fastk_period: usize, fastd_period: usize, fastd_matype: talib_rs::MaType);
14talib_1_in_2_out!(STOCHRSI, talib_rs::momentum::stochrsi, timeperiod: usize, fastk_period: usize, fastd_period: usize, fastd_matype: talib_rs::MaType);
15
16talib_3_in_1_out!(ADX, talib_rs::momentum::adx, timeperiod: usize);
17impl From<usize> for ADX {
18    fn from(p: usize) -> Self {
19        Self::new(p)
20    }
21}
22talib_3_in_1_out!(ADXR, talib_rs::momentum::adxr, timeperiod: usize);
23impl From<usize> for ADXR {
24    fn from(p: usize) -> Self {
25        Self::new(p)
26    }
27}
28talib_3_in_1_out!(CCI, talib_rs::momentum::cci, timeperiod: usize);
29impl From<usize> for CCI {
30    fn from(p: usize) -> Self {
31        Self::new(p)
32    }
33}
34talib_1_in_1_out!(MOM, talib_rs::momentum::mom, timeperiod: usize);
35impl From<usize> for MOM {
36    fn from(p: usize) -> Self {
37        Self::new(p)
38    }
39}
40talib_1_in_1_out!(ROC, talib_rs::momentum::roc, timeperiod: usize);
41impl From<usize> for ROC {
42    fn from(p: usize) -> Self {
43        Self::new(p)
44    }
45}
46talib_1_in_1_out!(ROCP, talib_rs::momentum::rocp, timeperiod: usize);
47impl From<usize> for ROCP {
48    fn from(p: usize) -> Self {
49        Self::new(p)
50    }
51}
52talib_1_in_1_out!(ROCR, talib_rs::momentum::rocr, timeperiod: usize);
53impl From<usize> for ROCR {
54    fn from(p: usize) -> Self {
55        Self::new(p)
56    }
57}
58talib_1_in_1_out!(ROCR100, talib_rs::momentum::rocr100, timeperiod: usize);
59impl From<usize> for ROCR100 {
60    fn from(p: usize) -> Self {
61        Self::new(p)
62    }
63}
64talib_3_in_1_out!(WILLR, talib_rs::momentum::willr, timeperiod: usize);
65impl From<usize> for WILLR {
66    fn from(p: usize) -> Self {
67        Self::new(p)
68    }
69}
70talib_1_in_1_out!(APO, talib_rs::momentum::apo, fastperiod: usize, slowperiod: usize, matype: talib_rs::MaType);
71talib_1_in_1_out!(PPO, talib_rs::momentum::ppo, fastperiod: usize, slowperiod: usize, matype: talib_rs::MaType);
72talib_4_in_1_out!(BOP, talib_rs::momentum::bop);
73impl Default for BOP {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78talib_1_in_1_out!(CMO, talib_rs::momentum::cmo, timeperiod: usize);
79impl From<usize> for CMO {
80    fn from(p: usize) -> Self {
81        Self::new(p)
82    }
83}
84talib_2_in_2_out!(AROON, talib_rs::momentum::aroon, timeperiod: usize);
85talib_2_in_1_out!(AROONOSC, talib_rs::momentum::aroon_osc, timeperiod: usize);
86talib_4_in_1_out!(MFI, talib_rs::momentum::mfi, timeperiod: usize);
87talib_1_in_1_out!(TRIX, talib_rs::momentum::trix, timeperiod: usize);
88impl From<usize> for TRIX {
89    fn from(p: usize) -> Self {
90        Self::new(p)
91    }
92}
93talib_3_in_1_out!(ULTOSC, talib_rs::momentum::ultosc, timeperiod1: usize, timeperiod2: usize, timeperiod3: usize);
94talib_3_in_1_out!(DX, talib_rs::momentum::dx, timeperiod: usize);
95impl From<usize> for DX {
96    fn from(p: usize) -> Self {
97        Self::new(p)
98    }
99}
100talib_3_in_1_out!(PLUS_DI, talib_rs::momentum::plus_di, timeperiod: usize);
101impl From<usize> for PLUS_DI {
102    fn from(p: usize) -> Self {
103        Self::new(p)
104    }
105}
106talib_3_in_1_out!(MINUS_DI, talib_rs::momentum::minus_di, timeperiod: usize);
107impl From<usize> for MINUS_DI {
108    fn from(p: usize) -> Self {
109        Self::new(p)
110    }
111}
112talib_2_in_1_out!(PLUS_DM, talib_rs::momentum::plus_dm, timeperiod: usize);
113talib_2_in_1_out!(MINUS_DM, talib_rs::momentum::minus_dm, timeperiod: usize);
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::traits::Next;
119    use proptest::prelude::*;
120
121    proptest! {
122        #[test]
123        fn test_rsi_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
124            let period = 14;
125            let mut rsi = RSI::new(period);
126            let streaming_results: Vec<f64> = input.iter().map(|&x| rsi.next(x)).collect();
127            let batch_results = talib_rs::momentum::rsi(&input, period).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
128
129            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
130                if s.is_nan() {
131                    assert!(b.is_nan());
132                } else {
133                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
134                }
135            }
136        }
137
138        #[test]
139        fn test_macd_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
140            let fast = 12;
141            let slow = 26;
142            let signal = 9;
143            let mut macd = MACD::new(fast, slow, signal);
144            let streaming_results: Vec<(f64, f64, f64)> = input.iter().map(|&x| macd.next(x)).collect();
145            let (b_macd, b_signal, b_hist) = talib_rs::momentum::macd(&input, fast, slow, signal).unwrap_or_else(|_| {
146                (vec![f64::NAN; input.len()], vec![f64::NAN; input.len()], vec![f64::NAN; input.len()])
147            });
148
149            for (i, (s_macd, s_signal, s_hist)) in streaming_results.into_iter().enumerate() {
150                if s_macd.is_nan() {
151                    assert!(b_macd[i].is_nan());
152                } else {
153                    approx::assert_relative_eq!(s_macd, b_macd[i], epsilon = 1e-6);
154                }
155                if s_signal.is_nan() {
156                    assert!(b_signal[i].is_nan());
157                } else {
158                    approx::assert_relative_eq!(s_signal, b_signal[i], epsilon = 1e-6);
159                }
160                if s_hist.is_nan() {
161                    assert!(b_hist[i].is_nan());
162                } else {
163                    approx::assert_relative_eq!(s_hist, b_hist[i], epsilon = 1e-6);
164                }
165            }
166        }
167
168        #[test]
169        fn test_stoch_parity(
170            h in prop::collection::vec(1.0..100.0, 1..100),
171            l in prop::collection::vec(1.0..100.0, 1..100),
172            c in prop::collection::vec(1.0..100.0, 1..100)
173        ) {
174            let len = h.len().min(l.len()).min(c.len());
175            if len == 0 { return Ok(()); }
176            let mut high = Vec::with_capacity(len);
177            let mut low = Vec::with_capacity(len);
178            let mut close = Vec::with_capacity(len);
179            for i in 0..len {
180                let val_h: f64 = h[i];
181                let val_l: f64 = l[i];
182                let val_c: f64 = c[i];
183                let max: f64 = val_h.max(val_l).max(val_c);
184                let min: f64 = val_h.min(val_l).min(val_c);
185                high.push(max);
186                low.push(min);
187                close.push(val_c);
188            }
189
190            let fastk = 5;
191            let slowk = 3;
192            let slowk_ma = talib_rs::MaType::Sma;
193            let slowd = 3;
194            let slowd_ma = talib_rs::MaType::Sma;
195
196            let mut stoch = STOCH::new(fastk, slowk, slowk_ma, slowd, slowd_ma);
197            let streaming_results: Vec<(f64, f64)> = (0..len).map(|i| stoch.next((high[i], low[i], close[i]))).collect();
198            let (b_k, b_d) = talib_rs::momentum::stoch(&high, &low, &close, fastk, slowk, slowk_ma, slowd, slowd_ma).unwrap_or_else(|_| {
199                (vec![f64::NAN; len], vec![f64::NAN; len])
200            });
201
202            for (i, (s_k, s_d)) in streaming_results.into_iter().enumerate() {
203                if s_k.is_nan() {
204                    assert!(b_k[i].is_nan());
205                } else {
206                    approx::assert_relative_eq!(s_k, b_k[i], epsilon = 1e-6);
207                }
208                if s_d.is_nan() {
209                    assert!(b_d[i].is_nan());
210                } else {
211                    approx::assert_relative_eq!(s_d, b_d[i], epsilon = 1e-6);
212                }
213            }
214        }
215
216        #[test]
217        fn test_adx_parity(
218            h in prop::collection::vec(1.0..100.0, 1..100),
219            l in prop::collection::vec(1.0..100.0, 1..100),
220            c in prop::collection::vec(1.0..100.0, 1..100)
221        ) {
222            let len = h.len().min(l.len()).min(c.len());
223            if len == 0 { return Ok(()); }
224            let mut high = Vec::with_capacity(len);
225            let mut low = Vec::with_capacity(len);
226            let mut close = Vec::with_capacity(len);
227            for i in 0..len {
228                let val_h: f64 = h[i];
229                let val_l: f64 = l[i];
230                let val_c: f64 = c[i];
231                let max: f64 = val_h.max(val_l).max(val_c);
232                let min: f64 = val_h.min(val_l).min(val_c);
233                high.push(max);
234                low.push(min);
235                close.push(val_c);
236            }
237
238            let period = 14;
239            let mut adx = ADX::new(period);
240            let streaming_results: Vec<f64> = (0..len).map(|i| adx.next((high[i], low[i], close[i]))).collect();
241            let batch_results = talib_rs::momentum::adx(&high, &low, &close, period).unwrap_or_else(|_| vec![f64::NAN; len]);
242
243            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
244                if s.is_nan() {
245                    assert!(b.is_nan());
246                } else {
247                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
248                }
249            }
250        }
251    }
252}
253
254pub const RSI_METADATA: IndicatorMetadata = IndicatorMetadata {
255    name: "Relative Strength Index (RSI)",
256    description: "A momentum oscillator that measures the speed and change of price movements.",
257    usage: "Use to identify overbought (>70) and oversold (<30) conditions. RSI divergences against price often signal impending trend reversals.",
258    keywords: &["momentum", "oscillator", "overbought", "oversold", "classic"],
259    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.",
260    params: &[ParamDef { name: "timeperiod", default: "14", description: "Lookback period" }],
261    formula_source: "https://www.investopedia.com/terms/r/rsi.asp",
262    formula_latex: r#"
263\[
264RS = \frac{Average Gain}{Average Loss} \\ RSI = 100 - \frac{100}{1 + RS}
265\]
266"#,
267    gold_standard_file: "rsi.json",
268    category: "Classic",
269};
270
271pub const MACD_METADATA: IndicatorMetadata = IndicatorMetadata {
272    name: "Moving Average Convergence Divergence (MACD)",
273    description: "A trend-following momentum indicator that shows the relationship between two moving averages.",
274    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.",
275    keywords: &["trend", "momentum", "moving-average", "classic"],
276    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",
277    params: &[
278        ParamDef { name: "fastperiod", default: "12", description: "Fast EMA period" },
279        ParamDef { name: "slowperiod", default: "26", description: "Slow EMA period" },
280        ParamDef { name: "signalperiod", default: "9", description: "Signal EMA period" },
281    ],
282    formula_source: "https://www.investopedia.com/terms/m/macd.asp",
283    formula_latex: r#"
284\[
285MACD = EMA(12) - EMA(26) \\ Signal = EMA(MACD, 9)
286\]
287"#,
288    gold_standard_file: "macd.json",
289    category: "Classic",
290};
291
292pub const STOCH_METADATA: IndicatorMetadata = IndicatorMetadata {
293    name: "Stochastic Oscillator",
294    description: "A momentum indicator comparing a particular closing price of a security to a range of its prices over a certain period of time.",
295    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.",
296    keywords: &["momentum", "oscillator", "overbought", "oversold", "classic"],
297    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",
298    params: &[
299        ParamDef { name: "fastk_period", default: "5", description: "Fast %K period" },
300        ParamDef { name: "slowk_period", default: "3", description: "Slow %K period" },
301        ParamDef { name: "slowd_period", default: "3", description: "Slow %D period" },
302    ],
303    formula_source: "https://www.investopedia.com/terms/s/stochasticoscillator.asp",
304    formula_latex: r#"
305\[
306\%K = 100 \times \frac{C - L14}{H14 - L14} \\ \%D = 3\text{-period SMA of } \%K
307\]
308"#,
309    gold_standard_file: "stoch.json",
310    category: "Classic",
311};
312
313pub const ADX_METADATA: IndicatorMetadata = IndicatorMetadata {
314    name: "Average Directional Index (ADX)",
315    description: "An indicator used to quantify trend strength without regard to trend direction.",
316    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.",
317    keywords: &["trend", "volatility", "classic", "wilder"],
318    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",
319    params: &[ParamDef { name: "timeperiod", default: "14", description: "Lookback period" }],
320    formula_source: "https://www.investopedia.com/terms/a/adx.asp",
321    formula_latex: r#"
322\[
323ADX = 100 \times \frac{\text{EMA}(|(+DI) - (-DI)| / |(+DI) + (-DI)|, n)}{n}
324\]
325"#,
326    gold_standard_file: "adx.json",
327    category: "Classic",
328};
329
330pub const CCI_METADATA: IndicatorMetadata = IndicatorMetadata {
331    name: "Commodity Channel Index (CCI)",
332    description: "A versatile indicator that can be used to identify a new trend or warn of extreme conditions.",
333    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.",
334    keywords: &["momentum", "oscillator", "classic", "mean-reversion"],
335    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",
336    params: &[ParamDef { name: "timeperiod", default: "14", description: "Lookback period" }],
337    formula_source: "https://www.investopedia.com/terms/c/commoditychannelindex.asp",
338    formula_latex: r#"
339\[
340CCI = \frac{Price - SMA}{0.015 \times \text{Mean Deviation}}
341\]
342"#,
343    gold_standard_file: "cci.json",
344    category: "Classic",
345};
346
347pub const WILLR_METADATA: IndicatorMetadata = IndicatorMetadata {
348    name: "Williams %R",
349    description: "A momentum indicator that measures overbought and oversold levels, similar to a stochastic oscillator.",
350    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.",
351    keywords: &["momentum", "oscillator", "overbought", "oversold", "classic"],
352    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",
353    params: &[ParamDef { name: "timeperiod", default: "14", description: "Lookback period" }],
354    formula_source: "https://www.investopedia.com/terms/w/williamsr.asp",
355    formula_latex: r#"
356\[
357\%R = \frac{\text{Highest High} - \text{Close}}{\text{Highest High} - \text{Lowest Low}} \times -100
358\]
359"#,
360    gold_standard_file: "willr.json",
361    category: "Classic",
362};
363
364pub const MFI_METADATA: IndicatorMetadata = IndicatorMetadata {
365    name: "Money Flow Index (MFI)",
366    description: "A technical oscillator that uses price and volume data for identifying overbought or oversold signals.",
367    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).",
368    keywords: &["momentum", "volume", "oscillator", "classic"],
369    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",
370    params: &[ParamDef { name: "timeperiod", default: "14", description: "Lookback period" }],
371    formula_source: "https://www.investopedia.com/terms/m/mfi.asp",
372    formula_latex: r#"
373\[
374\text{Money Ratio} = \frac{\text{Positive Money Flow}}{\text{Negative Money Flow}} \\ MFI = 100 - \frac{100}{1 + \text{Money Ratio}}
375\]
376"#,
377    gold_standard_file: "mfi.json",
378    category: "Classic",
379};
380
381pub const AROON_METADATA: IndicatorMetadata = IndicatorMetadata {
382    name: "Aroon Indicator",
383    description: "An indicator system that identifies when a new trend is beginning and the strength of the trend.",
384    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.",
385    keywords: &["trend", "classic", "breakout"],
386    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",
387    params: &[ParamDef { name: "timeperiod", default: "25", description: "Lookback period" }],
388    formula_source: "https://www.investopedia.com/terms/a/aroon.asp",
389    formula_latex: r#"
390\[
391\text{Aroon Up} = \frac{n - \text{Periods since n-period High}}{n} \times 100
392\]
393"#,
394    gold_standard_file: "aroon.json",
395    category: "Classic",
396};
397
398pub const ULTOSC_METADATA: IndicatorMetadata = IndicatorMetadata {
399    name: "Ultimate Oscillator",
400    description: "A momentum oscillator designed to capture momentum across three different timeframes.",
401    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.",
402    keywords: &["momentum", "oscillator", "classic", "multi-timeframe"],
403    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",
404    params: &[
405        ParamDef { name: "timeperiod1", default: "7", description: "Short period" },
406        ParamDef { name: "timeperiod2", default: "14", description: "Medium period" },
407        ParamDef { name: "timeperiod3", default: "28", description: "Long period" },
408    ],
409    formula_source: "https://www.investopedia.com/terms/u/ultimateoscillator.asp",
410    formula_latex: r#"
411\[
412\text{BP} = \text{Close} - \min(\text{Low}, \text{PrevClose}) \\ \text{TR} = \max(\text{High}, \text{PrevClose}) - \min(\text{Low}, \text{PrevClose})
413\]
414"#,
415    gold_standard_file: "ultosc.json",
416    category: "Classic",
417};
418
419pub const TRIX_METADATA: IndicatorMetadata = IndicatorMetadata {
420    name: "TRIX",
421    description: "A momentum oscillator that shows the percent rate of change of a triple exponentially smoothed moving average.",
422    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.",
423    keywords: &["momentum", "oscillator", "smoothing", "classic"],
424    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",
425    params: &[ParamDef { name: "timeperiod", default: "15", description: "Smoothing period" }],
426    formula_source: "https://www.investopedia.com/terms/t/trix.asp",
427    formula_latex: r#"
428\[
429TRIX = \frac{EMA3_t - EMA3_{t-1}}{EMA3_{t-1}} \times 100
430\]
431"#,
432    gold_standard_file: "trix.json",
433    category: "Classic",
434};
435
436pub const MOM_METADATA: IndicatorMetadata = IndicatorMetadata {
437    name: "Momentum (MOM)",
438    description: "A simple indicator that measures the amount that a security's price has changed over a given span of time.",
439    usage: "Use to measure the velocity of price changes. Positive values indicate an uptrend, while negative values indicate a downtrend.",
440    keywords: &["momentum", "classic", "trend"],
441    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",
442    params: &[ParamDef { name: "timeperiod", default: "10", description: "Lookback period" }],
443    formula_source: "https://www.investopedia.com/terms/m/momentum.asp",
444    formula_latex: r#"
445\[
446MOM = Price_t - Price_{t-n}
447\]
448"#,
449    gold_standard_file: "mom.json",
450    category: "Classic",
451};
452
453pub const ROC_METADATA: IndicatorMetadata = IndicatorMetadata {
454    name: "Rate of Change (ROC)",
455    description: "A momentum-based technical indicator that measures the percentage change in price between the current price and the price n periods ago.",
456    usage: "Use to measure the speed at which price is changing. It is often used to identify overbought/oversold conditions and trend reversals.",
457    keywords: &["momentum", "classic", "oscillator"],
458    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",
459    params: &[ParamDef { name: "timeperiod", default: "10", description: "Lookback period" }],
460    formula_source: "https://www.investopedia.com/terms/r/rateofchange.asp",
461    formula_latex: r#"
462\[
463ROC = \frac{Price_t - Price_{t-n}}{Price_{t-n}} \times 100
464\]
465"#,
466    gold_standard_file: "roc.json",
467    category: "Classic",
468};
469
470pub const CMO_METADATA: IndicatorMetadata = IndicatorMetadata {
471    name: "Chande Momentum Oscillator (CMO)",
472    description: "An advanced momentum oscillator developed by Tushar Chande that measures the difference between up and down days.",
473    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.",
474    keywords: &["momentum", "oscillator", "classic", "overbought", "oversold"],
475    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",
476    params: &[ParamDef { name: "timeperiod", default: "14", description: "Lookback period" }],
477    formula_source: "https://www.investopedia.com/terms/c/chandemomentumoscillator.asp",
478    formula_latex: r#"
479\[
480CMO = 100 \times \frac{S_u - S_d}{S_u + S_d}
481\]
482"#,
483    gold_standard_file: "cmo.json",
484    category: "Classic",
485};
486
487pub const APO_METADATA: IndicatorMetadata = IndicatorMetadata {
488    name: "Absolute Price Oscillator (APO)",
489    description: "Shows the absolute difference between two moving averages of different periods.",
490    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.",
491    keywords: &["trend", "momentum", "moving-average", "classic"],
492    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",
493    params: &[
494        ParamDef { name: "fastperiod", default: "12", description: "Fast period" },
495        ParamDef { name: "slowperiod", default: "26", description: "Slow period" },
496    ],
497    formula_source: "https://www.tradingview.com/support/solutions/43000501826-absolute-price-oscillator-apo/",
498    formula_latex: r#"
499\[
500APO = EMA(fast) - EMA(slow)
501\]
502"#,
503    gold_standard_file: "apo.json",
504    category: "Classic",
505};
506
507pub const PPO_METADATA: IndicatorMetadata = IndicatorMetadata {
508    name: "Percentage Price Oscillator (PPO)",
509    description: "A momentum oscillator that measures the difference between two moving averages as a percentage of the larger moving average.",
510    usage: "Use to compare trend momentum across different securities with varying price levels. PPO is the percentage version of MACD.",
511    keywords: &["trend", "momentum", "moving-average", "classic", "normalization"],
512    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",
513    params: &[
514        ParamDef { name: "fastperiod", default: "12", description: "Fast period" },
515        ParamDef { name: "slowperiod", default: "26", description: "Slow period" },
516    ],
517    formula_source: "https://www.investopedia.com/terms/p/ppo.asp",
518    formula_latex: r#"
519\[
520PPO = \frac{EMA(12) - EMA(26)}{EMA(26)} \times 100
521\]
522"#,
523    gold_standard_file: "ppo.json",
524    category: "Classic",
525};