wickra_core/indicators/
ma_envelope.rs1use crate::error::{Error, Result};
4use crate::indicators::sma::Sma;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct MaEnvelopeOutput {
11 pub upper: f64,
13 pub middle: f64,
15 pub lower: f64,
17}
18
19#[derive(Debug, Clone)]
48pub struct MaEnvelope {
49 sma: Sma,
50 percent: f64,
51}
52
53impl MaEnvelope {
54 pub fn new(period: usize, percent: f64) -> Result<Self> {
61 if !percent.is_finite() || percent <= 0.0 {
62 return Err(Error::NonPositiveMultiplier);
63 }
64 Ok(Self {
65 sma: Sma::new(period)?,
66 percent,
67 })
68 }
69
70 pub const fn period(&self) -> usize {
72 self.sma.period()
73 }
74
75 pub const fn percent(&self) -> f64 {
77 self.percent
78 }
79}
80
81impl Indicator for MaEnvelope {
82 type Input = f64;
83 type Output = MaEnvelopeOutput;
84
85 #[inline]
86 fn update(&mut self, input: f64) -> Option<MaEnvelopeOutput> {
87 let middle = self.sma.update(input)?;
88 Some(MaEnvelopeOutput {
89 upper: middle * (1.0 + self.percent),
90 middle,
91 lower: middle * (1.0 - self.percent),
92 })
93 }
94
95 fn reset(&mut self) {
96 self.sma.reset();
97 }
98
99 #[inline]
100 fn warmup_period(&self) -> usize {
101 self.sma.warmup_period()
102 }
103
104 #[inline]
105 fn is_ready(&self) -> bool {
106 self.sma.is_ready()
107 }
108
109 #[inline]
110 fn name(&self) -> &'static str {
111 "MaEnvelope"
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::traits::BatchExt;
119 use approx::assert_relative_eq;
120
121 #[test]
122 fn rejects_zero_period() {
123 assert!(matches!(MaEnvelope::new(0, 0.025), Err(Error::PeriodZero)));
124 }
125
126 #[test]
127 fn rejects_non_positive_percent() {
128 assert!(matches!(
129 MaEnvelope::new(20, 0.0),
130 Err(Error::NonPositiveMultiplier)
131 ));
132 assert!(matches!(
133 MaEnvelope::new(20, -0.1),
134 Err(Error::NonPositiveMultiplier)
135 ));
136 assert!(matches!(
137 MaEnvelope::new(20, f64::NAN),
138 Err(Error::NonPositiveMultiplier)
139 ));
140 }
141
142 #[test]
143 fn accessors_and_metadata() {
144 let env = MaEnvelope::new(20, 0.025).unwrap();
145 assert_eq!(env.period(), 20);
146 assert_relative_eq!(env.percent(), 0.025, epsilon = 1e-12);
147 assert_eq!(env.warmup_period(), 20);
148 assert_eq!(env.name(), "MaEnvelope");
149 assert!(!env.is_ready());
150 }
151
152 #[test]
153 fn constant_series_yields_flat_envelope() {
154 let mut env = MaEnvelope::new(5, 0.01).unwrap();
155 let last = env
156 .batch(&[100.0_f64; 20])
157 .into_iter()
158 .flatten()
159 .last()
160 .unwrap();
161 assert_relative_eq!(last.middle, 100.0, epsilon = 1e-12);
162 assert_relative_eq!(last.upper, 101.0, epsilon = 1e-12);
163 assert_relative_eq!(last.lower, 99.0, epsilon = 1e-12);
164 }
165
166 #[test]
167 fn warmup_returns_none() {
168 let mut env = MaEnvelope::new(5, 0.05).unwrap();
169 for v in [1.0, 2.0, 3.0, 4.0] {
170 assert!(env.update(v).is_none());
171 }
172 assert!(env.update(5.0).is_some());
173 }
174
175 #[test]
176 fn upper_above_middle_above_lower() {
177 let prices: Vec<f64> = (1..=80)
178 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0)
179 .collect();
180 let mut env = MaEnvelope::new(20, 0.025).unwrap();
181 for o in env.batch(&prices).into_iter().flatten() {
182 assert!(o.upper >= o.middle);
183 assert!(o.middle >= o.lower);
184 }
185 }
186
187 #[test]
188 fn batch_equals_streaming() {
189 let prices: Vec<f64> = (1..=50).map(|i| f64::from(i) * 0.7 + 100.0).collect();
190 let mut a = MaEnvelope::new(10, 0.03).unwrap();
191 let mut b = MaEnvelope::new(10, 0.03).unwrap();
192 assert_eq!(
193 a.batch(&prices),
194 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
195 );
196 }
197
198 #[test]
199 fn reset_clears_state() {
200 let mut env = MaEnvelope::new(5, 0.02).unwrap();
201 env.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
202 assert!(env.is_ready());
203 env.reset();
204 assert!(!env.is_ready());
205 assert_eq!(env.update(1.0), None);
206 }
207
208 #[test]
211 fn reference_values() {
212 let mut env = MaEnvelope::new(3, 0.10).unwrap();
213 let out = env.batch(&[10.0, 20.0, 30.0]);
214 assert!(out[0].is_none() && out[1].is_none());
215 let v = out[2].unwrap();
216 assert_relative_eq!(v.middle, 20.0, epsilon = 1e-12);
217 assert_relative_eq!(v.upper, 22.0, epsilon = 1e-12);
218 assert_relative_eq!(v.lower, 18.0, epsilon = 1e-12);
219 }
220}