1use crate::error::{Error, Result};
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone)]
48pub struct Jma {
49 period: usize,
50 phase: f64,
51 power: u32,
52 beta: f64,
53 alpha: f64,
54 phase_ratio: f64,
55 e0: f64,
56 e1: f64,
57 e2: f64,
58 output: Option<f64>,
59}
60
61impl Jma {
62 pub fn new(period: usize, phase: f64, power: u32) -> Result<Self> {
67 if period == 0 {
68 return Err(Error::PeriodZero);
69 }
70 if period > crate::error::MAX_PERIOD {
71 return Err(Error::InvalidPeriod {
72 message: crate::error::PERIOD_ABOVE_MAX,
73 });
74 }
75 if !phase.is_finite() {
76 return Err(Error::InvalidPeriod {
77 message: "JMA phase must be a finite value",
78 });
79 }
80 if !(1..=4).contains(&power) {
81 return Err(Error::InvalidPeriod {
82 message: "JMA power must be in 1..=4",
83 });
84 }
85 let len = period as f64 - 1.0;
86 let beta = 0.45 * len / (0.45 * len + 2.0);
87 let alpha = beta.powi(i32::try_from(power).expect("power is in 1..=4"));
88 let phase_ratio = (phase / 100.0 + 1.5).clamp(0.5, 2.5);
89 Ok(Self {
90 period,
91 phase,
92 power,
93 beta,
94 alpha,
95 phase_ratio,
96 e0: 0.0,
97 e1: 0.0,
98 e2: 0.0,
99 output: None,
100 })
101 }
102
103 pub fn classic() -> Self {
105 Self::new(14, 0.0, 2).expect("classic JMA parameters are valid")
106 }
107
108 pub const fn params(&self) -> (usize, f64, u32) {
110 (self.period, self.phase, self.power)
111 }
112}
113
114impl Indicator for Jma {
115 type Input = f64;
116 type Output = f64;
117
118 #[inline]
119 fn update(&mut self, input: f64) -> Option<f64> {
120 if !input.is_finite() {
121 return None;
122 }
123 let Some(prev_jma) = self.output else {
124 self.e0 = input;
127 self.output = Some(input);
128 return self.output;
129 };
130 self.e0 = (1.0 - self.alpha) * input + self.alpha * self.e0;
131 self.e1 = (input - self.e0) * (1.0 - self.beta) + self.beta * self.e1;
132 let one_minus_alpha = 1.0 - self.alpha;
133 self.e2 =
134 (self.e0 + self.phase_ratio * self.e1 - prev_jma) * one_minus_alpha * one_minus_alpha
135 + self.alpha * self.alpha * self.e2;
136 let next = prev_jma + self.e2;
137 self.output = Some(next);
138 Some(next)
139 }
140
141 fn reset(&mut self) {
142 self.e0 = 0.0;
143 self.e1 = 0.0;
144 self.e2 = 0.0;
145 self.output = None;
146 }
147
148 #[inline]
149 fn warmup_period(&self) -> usize {
150 1
151 }
152
153 #[inline]
154 fn is_ready(&self) -> bool {
155 self.output.is_some()
156 }
157
158 #[inline]
159 fn name(&self) -> &'static str {
160 "JMA"
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use crate::traits::BatchExt;
168 use approx::assert_relative_eq;
169
170 #[test]
171 fn rejects_zero_period() {
172 assert!(matches!(Jma::new(0, 0.0, 2), Err(Error::PeriodZero)));
173 }
174
175 #[test]
176 fn rejects_non_finite_phase() {
177 assert!(matches!(
178 Jma::new(14, f64::NAN, 2),
179 Err(Error::InvalidPeriod { .. })
180 ));
181 assert!(matches!(
182 Jma::new(14, f64::INFINITY, 2),
183 Err(Error::InvalidPeriod { .. })
184 ));
185 }
186
187 #[test]
188 fn rejects_invalid_power() {
189 assert!(matches!(
190 Jma::new(14, 0.0, 0),
191 Err(Error::InvalidPeriod { .. })
192 ));
193 assert!(matches!(
194 Jma::new(14, 0.0, 5),
195 Err(Error::InvalidPeriod { .. })
196 ));
197 }
198
199 #[test]
200 fn accessors_and_metadata() {
201 let jma = Jma::new(14, 0.0, 2).unwrap();
202 assert_eq!(jma.params(), (14, 0.0, 2));
203 assert_eq!(jma.warmup_period(), 1);
204 assert_eq!(jma.name(), "JMA");
205 }
206
207 #[test]
208 fn classic_factory() {
209 let jma = Jma::classic();
210 assert_eq!(jma.params(), (14, 0.0, 2));
211 }
212
213 #[test]
214 fn constant_series_yields_the_constant() {
215 let mut jma = Jma::new(14, 0.0, 2).unwrap();
218 let out = jma.batch(&[42.0_f64; 60]);
219 for x in out.iter().flatten() {
220 assert_relative_eq!(*x, 42.0, epsilon = 1e-12);
221 }
222 }
223
224 #[test]
225 fn extreme_phase_is_clamped() {
226 let mut a = Jma::new(14, 250.0, 2).unwrap();
229 let mut b = Jma::new(14, -250.0, 2).unwrap();
230 let prices: Vec<f64> = (1..=40).map(f64::from).collect();
231 for &p in &prices {
232 let va = a.update(p).unwrap();
233 let vb = b.update(p).unwrap();
234 assert!(va.is_finite(), "JMA(phase=+250) emitted {va}");
235 assert!(vb.is_finite(), "JMA(phase=-250) emitted {vb}");
236 }
237 }
238
239 #[test]
240 fn pure_uptrend_tracks_close() {
241 let mut jma = Jma::new(5, 0.0, 2).unwrap();
244 let prices: Vec<f64> = (1..=80).map(f64::from).collect();
245 let out = jma.batch(&prices);
246 let last = out.last().unwrap().unwrap();
247 let latest = *prices.last().unwrap();
248 assert!(
249 (latest - last).abs() < 5.0,
250 "JMA on a long clean uptrend should track close: {last} vs {latest}"
251 );
252 }
253
254 #[test]
255 fn batch_equals_streaming() {
256 let prices: Vec<f64> = (1..=80)
257 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0)
258 .collect();
259 let mut a = Jma::new(14, 0.0, 2).unwrap();
260 let mut b = Jma::new(14, 0.0, 2).unwrap();
261 assert_eq!(
262 a.batch(&prices),
263 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
264 );
265 }
266
267 #[test]
268 fn reset_clears_state() {
269 let mut jma = Jma::new(14, 0.0, 2).unwrap();
270 jma.batch(&(1..=30).map(f64::from).collect::<Vec<_>>());
271 assert!(jma.is_ready());
272 jma.reset();
273 assert!(!jma.is_ready());
274 assert_eq!(jma.e0, 0.0);
275 }
276
277 #[test]
278 fn ignores_non_finite_input() {
279 let mut jma = Jma::new(14, 0.0, 2).unwrap();
280 jma.batch(&(1..=15).map(f64::from).collect::<Vec<_>>());
281 jma.update(16.0).unwrap();
282 assert_eq!(jma.update(f64::NAN), None);
283 assert_eq!(jma.update(f64::INFINITY), None);
284 }
285
286 #[test]
287 fn period_one_is_pass_through() {
288 let mut jma = Jma::new(1, 0.0, 2).unwrap();
291 assert_eq!(jma.update(5.0), Some(5.0));
292 assert_relative_eq!(jma.update(10.0).unwrap(), 10.0, epsilon = 1e-12);
293 assert_relative_eq!(jma.update(7.0).unwrap(), 7.0, epsilon = 1e-12);
294 }
295}