Skip to main content

quantwave_core/indicators/
rsmk.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::EMA;
3use crate::traits::Next;
4use std::collections::VecDeque;
5
6pub const METADATA: IndicatorMetadata = IndicatorMetadata {
7    name: "Relative Strength Markos Katsanos",
8    description: "An improved relative strength indicator that compares a security to a benchmark, separating periods of strong and weak relative performance.",
9    usage: "Use as a momentum-based relative strength indicator. Values above zero indicate the security is outperforming the benchmark over the specified period.",
10    keywords: &["relative strength", "momentum", "benchmark", "katsanos"],
11    ehlers_summary: "RSMK calculates the log-ratio momentum of a security relative to a benchmark (e.g., SPY). It measures the difference between current log-relative strength and its value N bars ago, then smooths it with an EMA. This approach identifies trends in relative performance with less lag than traditional methods.",
12    params: &[
13        ParamDef {
14            name: "length",
15            default: "90",
16            description: "Momentum lookback period",
17        },
18        ParamDef {
19            name: "ema_length",
20            default: "3",
21            description: "EMA smoothing period",
22        },
23    ],
24    formula_source: "TASC March 2020",
25    formula_latex: r#"
26\[
27RSMK = EMA(\ln(\frac{P_t}{B_t}) - \ln(\frac{P_{t-n}}{B_{t-n}}), m) \times 100
28\]
29"#,
30    gold_standard_file: "rsmk_90_3.json",
31    category: "Momentum",
32};
33
34/// Relative Strength Markos Katsanos (RSMK)
35///
36/// Compares a security to a benchmark using log-momentum and EMA smoothing.
37#[derive(Debug, Clone)]
38pub struct RSMK {
39    length: usize,
40    ema: EMA,
41    log_val_window: VecDeque<f64>,
42}
43
44impl RSMK {
45    pub fn new(length: usize, ema_length: usize) -> Self {
46        Self {
47            length,
48            ema: EMA::new(ema_length),
49            log_val_window: VecDeque::with_capacity(length + 1),
50        }
51    }
52}
53
54impl Next<(f64, f64)> for RSMK {
55    type Output = f64;
56
57    fn next(&mut self, (price, benchmark): (f64, f64)) -> Self::Output {
58        if price <= 0.0 || benchmark <= 0.0 {
59            // In case of invalid inputs, we might want to push a default or handle it.
60            // But log(ratio) will fail.
61            return 0.0;
62        }
63
64        let log_val = (price / benchmark).ln();
65        self.log_val_window.push_back(log_val);
66
67        if self.log_val_window.len() <= self.length {
68            // Not enough data for momentum calculation.
69            // We still feed the EMA to initialize it, though the momentum is effectively 0 or based on the first available.
70            // Katsanos code: RSMK = XAverage( LogVal - LogVal[Length], EMALength ) * 100 ;
71            // If LogVal[Length] is not available, LogVal - LogVal[Length] is undefined.
72            return 0.0;
73        }
74
75        let old_log_val = self.log_val_window.pop_front().unwrap();
76        let momentum = log_val - old_log_val;
77        
78        self.ema.next(momentum) * 100.0
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn test_rsmk_basic() {
88        let mut rsmk = RSMK::new(2, 3);
89        
90        // Data: (Price, Benchmark)
91        // LogVal = ln(Price/Benchmark)
92        
93        // Bar 1: (10, 100) -> LogVal = ln(0.1) = -2.302585
94        assert_eq!(rsmk.next((10.0, 100.0)), 0.0);
95        
96        // Bar 2: (11, 100) -> LogVal = ln(0.11) = -2.207275
97        assert_eq!(rsmk.next((11.0, 100.0)), 0.0);
98        
99        // Bar 3: (12, 100) -> LogVal = ln(0.12) = -2.120264
100        // Momentum = ln(0.12) - ln(0.1) = -2.120264 - (-2.302585) = 0.182321
101        // EMA(1, 0.182321) = 0.182321
102        // RSMK = 0.182321 * 100 = 18.2321
103        let val = rsmk.next((12.0, 100.0));
104        approx::assert_relative_eq!(val, 18.232155, epsilon = 1e-6);
105        
106        // Bar 4: (12, 110) -> LogVal = ln(12/110) = -2.215574
107        // Momentum = ln(12/110) - ln(0.11) = -2.215574 - (-2.207275) = -0.008299
108        // EMA(prev=0.182321, curr=-0.008299, length=3)
109        // alpha = 2/(3+1) = 0.5
110        // EMA = 0.5 * (-0.008299) + 0.5 * (0.182321) = 0.087011
111        // RSMK = 8.7011
112        let val = rsmk.next((12.0, 110.0));
113        approx::assert_relative_eq!(val, 8.7011, epsilon = 1e-4);
114    }
115}