Skip to main content

kestrel_chartkit/indicator/
relative_strength.rs

1use std::collections::HashMap;
2
3use crate::indicator::{Indicator, IndicatorAlert, IndicatorOutput};
4use crate::model::Bar;
5
6/// Relative Strength Ratio & Momentum vs. Benchmark (e.g., Asset / BTC or Stock / SPY).
7pub struct RelativeStrengthEngine {
8    lookback: usize,
9    own_closes: Vec<f64>,
10    bench_closes: Vec<f64>,
11    alerts: Vec<IndicatorAlert>,
12}
13
14impl RelativeStrengthEngine {
15    pub fn new(lookback: usize) -> Self {
16        Self {
17            lookback,
18            own_closes: Vec::new(),
19            bench_closes: Vec::new(),
20            alerts: Vec::new(),
21        }
22    }
23
24    /// Dual-bar update for multi-series Relative Strength comparison.
25    pub fn update(&mut self, own_bar: &Bar, benchmark_bar: &Bar) -> Option<IndicatorOutput> {
26        self.own_closes.push(own_bar.close);
27        self.bench_closes.push(benchmark_bar.close);
28
29        if self.own_closes.len() > self.lookback + 1 {
30            self.own_closes.remove(0);
31            self.bench_closes.remove(0);
32        }
33
34        self.alerts.clear();
35        if self.own_closes.len() < self.lookback + 1 {
36            return None;
37        }
38
39        let curr_ratio = if benchmark_bar.close > 0.0 {
40            own_bar.close / benchmark_bar.close
41        } else {
42            1.0
43        };
44
45        let past_own = self.own_closes[0];
46        let past_bench = self.bench_closes[0];
47
48        let own_perf = if past_own > 0.0 {
49            (own_bar.close - past_own) / past_own
50        } else {
51            0.0
52        };
53        let bench_perf = if past_bench > 0.0 {
54            (benchmark_bar.close - past_bench) / past_bench
55        } else {
56            0.0
57        };
58
59        let alpha = (own_perf - bench_perf) * 100.0;
60
61        let mut extra = HashMap::new();
62        extra.insert("ratio".to_string(), curr_ratio);
63        extra.insert("own_perf_pct".to_string(), own_perf * 100.0);
64        extra.insert("bench_perf_pct".to_string(), bench_perf * 100.0);
65        extra.insert("alpha_pct".to_string(), alpha);
66
67        if alpha > 5.0 {
68            self.alerts.push(IndicatorAlert::new(
69                "outperforming_benchmark",
70                format!("Outperforming Benchmark by +{:.2}%", alpha),
71                0.85,
72            ));
73        } else if alpha < -5.0 {
74            self.alerts.push(IndicatorAlert::new(
75                "underperforming_benchmark",
76                format!("Underperforming Benchmark by {:.2}%", alpha),
77                0.85,
78            ));
79        }
80
81        Some(IndicatorOutput::with_extra(alpha, extra))
82    }
83}
84
85impl Indicator for RelativeStrengthEngine {
86    fn name(&self) -> &str {
87        "relative_strength"
88    }
89
90    fn warmup_period(&self) -> usize {
91        self.lookback + 1
92    }
93
94    fn reset(&mut self) {
95        self.own_closes.clear();
96        self.bench_closes.clear();
97        self.alerts.clear();
98    }
99
100    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
101        // Fallback single-bar interface treating benchmark as static/1.0
102        self.update(bar, bar)
103    }
104
105    fn alerts(&self) -> Vec<IndicatorAlert> {
106        self.alerts.clone()
107    }
108}