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 fn update(&mut self, input: (f64, f64)) -> Option<RelativeStrengthOutput> {
100 let (a, b) = input;
101 if b == 0.0 || !a.is_finite() || !b.is_finite() {
102 return None;
104 }
105 let ratio = a / b;
106 let ma = self.ma.update(ratio);
107 let rsi = self.rsi.update(ratio);
108 match (ma, rsi) {
109 (Some(ratio_ma), Some(ratio_rsi)) => Some(RelativeStrengthOutput {
110 ratio,
111 ratio_ma,
112 ratio_rsi,
113 }),
114 _ => None,
115 }
116 }
117
118 fn reset(&mut self) {
119 self.ma.reset();
120 self.rsi.reset();
121 }
122
123 fn warmup_period(&self) -> usize {
124 self.ma.warmup_period().max(self.rsi.warmup_period())
125 }
126
127 fn is_ready(&self) -> bool {
128 self.ma.is_ready() && self.rsi.is_ready()
129 }
130
131 fn name(&self) -> &'static str {
132 "RelativeStrengthAB"
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::traits::BatchExt;
140 use approx::assert_relative_eq;
141
142 #[test]
143 fn rejects_zero_periods() {
144 assert!(RelativeStrengthAB::new(0, 5).is_err());
145 assert!(RelativeStrengthAB::new(5, 0).is_err());
146 assert!(RelativeStrengthAB::new(5, 5).is_ok());
147 }
148
149 #[test]
150 fn accessors_and_metadata() {
151 let rs = RelativeStrengthAB::new(10, 14).unwrap();
152 assert_eq!(rs.ma_period(), 10);
153 assert_eq!(rs.rsi_period(), 14);
154 assert_eq!(rs.warmup_period(), 15);
156 assert_eq!(rs.name(), "RelativeStrengthAB");
157 }
158
159 #[test]
160 fn constant_ratio_is_flat() {
161 let pairs: Vec<(f64, f64)> = (0..20).map(|_| (200.0, 100.0)).collect();
163 let out = RelativeStrengthAB::new(5, 5)
164 .unwrap()
165 .batch(&pairs)
166 .into_iter()
167 .flatten()
168 .last()
169 .unwrap();
170 assert_relative_eq!(out.ratio, 2.0, epsilon = 1e-12);
171 assert_relative_eq!(out.ratio_ma, 2.0, epsilon = 1e-12);
172 assert_relative_eq!(out.ratio_rsi, 50.0, epsilon = 1e-9);
173 }
174
175 #[test]
176 fn rising_ratio_is_overbought() {
177 let pairs: Vec<(f64, f64)> = (0..20)
179 .map(|t| (100.0 + 2.0 * f64::from(t), 100.0))
180 .collect();
181 let out = RelativeStrengthAB::new(5, 5)
182 .unwrap()
183 .batch(&pairs)
184 .into_iter()
185 .flatten()
186 .last()
187 .unwrap();
188 assert!(out.ratio > 1.0);
189 assert_relative_eq!(out.ratio_rsi, 100.0, epsilon = 1e-9);
190 }
191
192 #[test]
193 fn zero_denominator_is_skipped() {
194 let mut rs = RelativeStrengthAB::new(3, 3).unwrap();
195 assert_eq!(rs.update((100.0, 0.0)), None);
197 assert_eq!(rs.update((f64::NAN, 100.0)), None);
198 assert!(!rs.is_ready());
199 for _ in 0..8 {
200 rs.update((150.0, 100.0));
201 }
202 assert!(rs.is_ready());
203 }
204
205 #[test]
206 fn reset_clears_state() {
207 let mut rs = RelativeStrengthAB::new(3, 3).unwrap();
208 for t in 0..10 {
209 rs.update((100.0 + f64::from(t), 100.0));
210 }
211 assert!(rs.is_ready());
212 rs.reset();
213 assert!(!rs.is_ready());
214 assert_eq!(rs.update((100.0, 100.0)), None);
215 }
216
217 #[test]
218 fn batch_equals_streaming() {
219 let pairs: Vec<(f64, f64)> = (0..60)
220 .map(|t| {
221 let tt = f64::from(t);
222 (
223 100.0 + 5.0 * (tt * 0.3).sin(),
224 100.0 + 2.0 * (tt * 0.2).cos(),
225 )
226 })
227 .collect();
228 let batch = RelativeStrengthAB::new(10, 14).unwrap().batch(&pairs);
229 let mut rs = RelativeStrengthAB::new(10, 14).unwrap();
230 let streamed: Vec<_> = pairs.iter().map(|p| rs.update(*p)).collect();
231 assert_eq!(batch, streamed);
232 }
233}