wickra_core/indicators/
relative_strength_ab.rs1use crate::error::Result;
4use crate::indicators::{Rsi, Sma};
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct RelativeStrengthOutput {
10 pub ratio: f64,
12 pub ratio_ma: f64,
14 pub ratio_rsi: f64,
16}
17
18#[derive(Debug, Clone)]
58pub struct RelativeStrengthAB {
59 ma_period: usize,
60 rsi_period: usize,
61 ma: Sma,
62 rsi: Rsi,
63}
64
65impl RelativeStrengthAB {
66 pub fn new(ma_period: usize, rsi_period: usize) -> Result<Self> {
75 Ok(Self {
76 ma_period,
77 rsi_period,
78 ma: Sma::new(ma_period)?,
79 rsi: Rsi::new(rsi_period)?,
80 })
81 }
82
83 pub const fn ma_period(&self) -> usize {
85 self.ma_period
86 }
87
88 pub const fn rsi_period(&self) -> usize {
90 self.rsi_period
91 }
92}
93
94impl Indicator for RelativeStrengthAB {
95 type Input = (f64, f64);
97 type Output = RelativeStrengthOutput;
98
99 #[inline]
100 fn update(&mut self, input: (f64, f64)) -> Option<RelativeStrengthOutput> {
101 let (a, b) = input;
102 if b == 0.0 || !a.is_finite() || !b.is_finite() {
103 return None;
105 }
106 let ratio = a / b;
107 let ma = self.ma.update(ratio);
108 let rsi = self.rsi.update(ratio);
109 match (ma, rsi) {
110 (Some(ratio_ma), Some(ratio_rsi)) => Some(RelativeStrengthOutput {
111 ratio,
112 ratio_ma,
113 ratio_rsi,
114 }),
115 _ => None,
116 }
117 }
118
119 fn reset(&mut self) {
120 self.ma.reset();
121 self.rsi.reset();
122 }
123
124 #[inline]
125 fn warmup_period(&self) -> usize {
126 self.ma.warmup_period().max(self.rsi.warmup_period())
127 }
128
129 #[inline]
130 fn is_ready(&self) -> bool {
131 self.ma.is_ready() && self.rsi.is_ready()
132 }
133
134 #[inline]
135 fn name(&self) -> &'static str {
136 "RelativeStrengthAB"
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::traits::BatchExt;
144 use approx::assert_relative_eq;
145
146 #[test]
147 fn rejects_zero_periods() {
148 assert!(RelativeStrengthAB::new(0, 5).is_err());
149 assert!(RelativeStrengthAB::new(5, 0).is_err());
150 assert!(RelativeStrengthAB::new(5, 5).is_ok());
151 }
152
153 #[test]
154 fn accessors_and_metadata() {
155 let rs = RelativeStrengthAB::new(10, 14).unwrap();
156 assert_eq!(rs.ma_period(), 10);
157 assert_eq!(rs.rsi_period(), 14);
158 assert_eq!(rs.warmup_period(), 15);
160 assert_eq!(rs.name(), "RelativeStrengthAB");
161 }
162
163 #[test]
164 fn constant_ratio_is_flat() {
165 let pairs: Vec<(f64, f64)> = (0..20).map(|_| (200.0, 100.0)).collect();
167 let out = RelativeStrengthAB::new(5, 5)
168 .unwrap()
169 .batch(&pairs)
170 .into_iter()
171 .flatten()
172 .last()
173 .unwrap();
174 assert_relative_eq!(out.ratio, 2.0, epsilon = 1e-12);
175 assert_relative_eq!(out.ratio_ma, 2.0, epsilon = 1e-12);
176 assert_relative_eq!(out.ratio_rsi, 50.0, epsilon = 1e-9);
177 }
178
179 #[test]
180 fn rising_ratio_is_overbought() {
181 let pairs: Vec<(f64, f64)> = (0..20)
183 .map(|t| (100.0 + 2.0 * f64::from(t), 100.0))
184 .collect();
185 let out = RelativeStrengthAB::new(5, 5)
186 .unwrap()
187 .batch(&pairs)
188 .into_iter()
189 .flatten()
190 .last()
191 .unwrap();
192 assert!(out.ratio > 1.0);
193 assert_relative_eq!(out.ratio_rsi, 100.0, epsilon = 1e-9);
194 }
195
196 #[test]
197 fn zero_denominator_is_skipped() {
198 let mut rs = RelativeStrengthAB::new(3, 3).unwrap();
199 assert_eq!(rs.update((100.0, 0.0)), None);
201 assert_eq!(rs.update((f64::NAN, 100.0)), None);
202 assert!(!rs.is_ready());
203 for _ in 0..8 {
204 rs.update((150.0, 100.0));
205 }
206 assert!(rs.is_ready());
207 }
208
209 #[test]
210 fn reset_clears_state() {
211 let mut rs = RelativeStrengthAB::new(3, 3).unwrap();
212 for t in 0..10 {
213 rs.update((100.0 + f64::from(t), 100.0));
214 }
215 assert!(rs.is_ready());
216 rs.reset();
217 assert!(!rs.is_ready());
218 assert_eq!(rs.update((100.0, 100.0)), None);
219 }
220
221 #[test]
222 fn batch_equals_streaming() {
223 let pairs: Vec<(f64, f64)> = (0..60)
224 .map(|t| {
225 let tt = f64::from(t);
226 (
227 100.0 + 5.0 * (tt * 0.3).sin(),
228 100.0 + 2.0 * (tt * 0.2).cos(),
229 )
230 })
231 .collect();
232 let batch = RelativeStrengthAB::new(10, 14).unwrap().batch(&pairs);
233 let mut rs = RelativeStrengthAB::new(10, 14).unwrap();
234 let streamed: Vec<_> = pairs.iter().map(|p| rs.update(*p)).collect();
235 assert_eq!(batch, streamed);
236 }
237}