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