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 fn update(&mut self, input: f64) -> Option<MaEnvelopeOutput> {
86 let middle = self.sma.update(input)?;
87 Some(MaEnvelopeOutput {
88 upper: middle * (1.0 + self.percent),
89 middle,
90 lower: middle * (1.0 - self.percent),
91 })
92 }
93
94 fn reset(&mut self) {
95 self.sma.reset();
96 }
97
98 fn warmup_period(&self) -> usize {
99 self.sma.warmup_period()
100 }
101
102 fn is_ready(&self) -> bool {
103 self.sma.is_ready()
104 }
105
106 fn name(&self) -> &'static str {
107 "MaEnvelope"
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::traits::BatchExt;
115 use approx::assert_relative_eq;
116
117 #[test]
118 fn rejects_zero_period() {
119 assert!(matches!(MaEnvelope::new(0, 0.025), Err(Error::PeriodZero)));
120 }
121
122 #[test]
123 fn rejects_non_positive_percent() {
124 assert!(matches!(
125 MaEnvelope::new(20, 0.0),
126 Err(Error::NonPositiveMultiplier)
127 ));
128 assert!(matches!(
129 MaEnvelope::new(20, -0.1),
130 Err(Error::NonPositiveMultiplier)
131 ));
132 assert!(matches!(
133 MaEnvelope::new(20, f64::NAN),
134 Err(Error::NonPositiveMultiplier)
135 ));
136 }
137
138 #[test]
139 fn accessors_and_metadata() {
140 let env = MaEnvelope::new(20, 0.025).unwrap();
141 assert_eq!(env.period(), 20);
142 assert_relative_eq!(env.percent(), 0.025, epsilon = 1e-12);
143 assert_eq!(env.warmup_period(), 20);
144 assert_eq!(env.name(), "MaEnvelope");
145 assert!(!env.is_ready());
146 }
147
148 #[test]
149 fn constant_series_yields_flat_envelope() {
150 let mut env = MaEnvelope::new(5, 0.01).unwrap();
151 let last = env
152 .batch(&[100.0_f64; 20])
153 .into_iter()
154 .flatten()
155 .last()
156 .unwrap();
157 assert_relative_eq!(last.middle, 100.0, epsilon = 1e-12);
158 assert_relative_eq!(last.upper, 101.0, epsilon = 1e-12);
159 assert_relative_eq!(last.lower, 99.0, epsilon = 1e-12);
160 }
161
162 #[test]
163 fn warmup_returns_none() {
164 let mut env = MaEnvelope::new(5, 0.05).unwrap();
165 for v in [1.0, 2.0, 3.0, 4.0] {
166 assert!(env.update(v).is_none());
167 }
168 assert!(env.update(5.0).is_some());
169 }
170
171 #[test]
172 fn upper_above_middle_above_lower() {
173 let prices: Vec<f64> = (1..=80)
174 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0)
175 .collect();
176 let mut env = MaEnvelope::new(20, 0.025).unwrap();
177 for o in env.batch(&prices).into_iter().flatten() {
178 assert!(o.upper >= o.middle);
179 assert!(o.middle >= o.lower);
180 }
181 }
182
183 #[test]
184 fn batch_equals_streaming() {
185 let prices: Vec<f64> = (1..=50).map(|i| f64::from(i) * 0.7 + 100.0).collect();
186 let mut a = MaEnvelope::new(10, 0.03).unwrap();
187 let mut b = MaEnvelope::new(10, 0.03).unwrap();
188 assert_eq!(
189 a.batch(&prices),
190 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
191 );
192 }
193
194 #[test]
195 fn reset_clears_state() {
196 let mut env = MaEnvelope::new(5, 0.02).unwrap();
197 env.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
198 assert!(env.is_ready());
199 env.reset();
200 assert!(!env.is_ready());
201 assert_eq!(env.update(1.0), None);
202 }
203
204 #[test]
207 fn reference_values() {
208 let mut env = MaEnvelope::new(3, 0.10).unwrap();
209 let out = env.batch(&[10.0, 20.0, 30.0]);
210 assert!(out[0].is_none() && out[1].is_none());
211 let v = out[2].unwrap();
212 assert_relative_eq!(v.middle, 20.0, epsilon = 1e-12);
213 assert_relative_eq!(v.upper, 22.0, epsilon = 1e-12);
214 assert_relative_eq!(v.lower, 18.0, epsilon = 1e-12);
215 }
216}