wickra_core/indicators/
frama.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
43pub struct Frama {
44 period: usize,
45 half: usize,
46 window: VecDeque<f64>,
47 current: Option<f64>,
48}
49
50impl Frama {
51 pub fn new(period: usize) -> Result<Self> {
55 if period == 0 {
56 return Err(Error::PeriodZero);
57 }
58 if period > crate::error::MAX_PERIOD {
59 return Err(Error::InvalidPeriod {
60 message: crate::error::PERIOD_ABOVE_MAX,
61 });
62 }
63 if period < 2 {
64 return Err(Error::InvalidPeriod {
65 message: "FRAMA period must be at least 2",
66 });
67 }
68 if period % 2 != 0 {
69 return Err(Error::InvalidPeriod {
70 message: "FRAMA period must be even",
71 });
72 }
73 Ok(Self {
74 period,
75 half: period / 2,
76 window: VecDeque::with_capacity(period),
77 current: None,
78 })
79 }
80
81 pub const fn period(&self) -> usize {
83 self.period
84 }
85}
86
87impl Indicator for Frama {
88 type Input = f64;
89 type Output = f64;
90
91 fn update(&mut self, input: f64) -> Option<f64> {
92 if !input.is_finite() {
93 return None;
94 }
95 if self.window.len() == self.period {
96 self.window.pop_front();
97 }
98 self.window.push_back(input);
99 if self.window.len() < self.period {
100 return None;
101 }
102
103 let half = self.half;
104 let mut h_first = f64::NEG_INFINITY;
105 let mut l_first = f64::INFINITY;
106 let mut h_second = f64::NEG_INFINITY;
107 let mut l_second = f64::INFINITY;
108 let mut h_whole = f64::NEG_INFINITY;
109 let mut l_whole = f64::INFINITY;
110 for (i, &p) in self.window.iter().enumerate() {
111 if p > h_whole {
112 h_whole = p;
113 }
114 if p < l_whole {
115 l_whole = p;
116 }
117 if i < half {
118 if p > h_first {
119 h_first = p;
120 }
121 if p < l_first {
122 l_first = p;
123 }
124 } else {
125 if p > h_second {
126 h_second = p;
127 }
128 if p < l_second {
129 l_second = p;
130 }
131 }
132 }
133
134 let half_f = half as f64;
135 let period_f = self.period as f64;
136 let n1 = (h_first - l_first) / half_f;
137 let n2 = (h_second - l_second) / half_f;
138 let n3 = (h_whole - l_whole) / period_f;
139
140 let alpha = if n1 > 0.0 && n2 > 0.0 && n3 > 0.0 {
141 let d = ((n1 + n2).ln() - n3.ln()) / 2.0_f64.ln();
142 (-4.6 * (d - 1.0)).exp().clamp(0.01, 1.0)
143 } else {
144 0.01
147 };
148
149 let prev = self.current.unwrap_or(input);
150 let next = alpha * input + (1.0 - alpha) * prev;
151 self.current = Some(next);
152 Some(next)
153 }
154
155 fn reset(&mut self) {
156 self.window.clear();
157 self.current = None;
158 }
159
160 #[inline]
161 fn warmup_period(&self) -> usize {
162 self.period
163 }
164
165 #[inline]
166 fn is_ready(&self) -> bool {
167 self.current.is_some()
168 }
169
170 #[inline]
171 fn name(&self) -> &'static str {
172 "FRAMA"
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::traits::BatchExt;
180 use approx::assert_relative_eq;
181
182 #[test]
183 fn rejects_zero_period() {
184 assert!(matches!(Frama::new(0), Err(Error::PeriodZero)));
185 }
186
187 #[test]
188 fn rejects_invalid_period() {
189 assert!(matches!(Frama::new(1), Err(Error::InvalidPeriod { .. })));
190 assert!(matches!(Frama::new(3), Err(Error::InvalidPeriod { .. })));
191 assert!(matches!(Frama::new(15), Err(Error::InvalidPeriod { .. })));
192 }
193
194 #[test]
195 fn accessors_and_metadata() {
196 let frama = Frama::new(16).unwrap();
197 assert_eq!(frama.period(), 16);
198 assert_eq!(frama.warmup_period(), 16);
199 assert_eq!(frama.name(), "FRAMA");
200 }
201
202 #[test]
203 fn constant_series_yields_the_constant() {
204 let mut frama = Frama::new(4).unwrap();
207 let out = frama.batch(&[42.0_f64; 30]);
208 for v in out.iter().skip(3).flatten() {
209 assert_relative_eq!(*v, 42.0, epsilon = 1e-12);
210 }
211 }
212
213 #[test]
214 fn warmup_emits_first_value_at_period() {
215 let mut frama = Frama::new(4).unwrap();
216 assert_eq!(frama.update(1.0), None);
217 assert_eq!(frama.update(2.0), None);
218 assert_eq!(frama.update(3.0), None);
219 assert!(frama.update(4.0).is_some());
220 }
221
222 #[test]
223 fn pure_uptrend_alpha_close_to_one() {
224 let mut frama = Frama::new(4).unwrap();
227 let prices: Vec<f64> = (1..=8).map(f64::from).collect();
228 let out = frama.batch(&prices);
229 let last = out.last().unwrap().unwrap();
230 assert!(
231 (last - 8.0).abs() < 0.05,
232 "FRAMA on a clean uptrend should hug the latest close: {last}"
233 );
234 }
235
236 #[test]
237 fn batch_equals_streaming() {
238 let prices: Vec<f64> = (1..=80)
239 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0)
240 .collect();
241 let mut a = Frama::new(8).unwrap();
242 let mut b = Frama::new(8).unwrap();
243 assert_eq!(
244 a.batch(&prices),
245 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
246 );
247 }
248
249 #[test]
250 fn reset_clears_state() {
251 let mut frama = Frama::new(4).unwrap();
252 frama.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
253 assert!(frama.is_ready());
254 frama.reset();
255 assert!(!frama.is_ready());
256 assert_eq!(frama.update(1.0), None);
257 }
258
259 #[test]
260 fn ignores_non_finite_input() {
261 let mut frama = Frama::new(4).unwrap();
262 frama.batch(&[1.0, 2.0, 3.0, 4.0]);
263 frama.update(5.0).unwrap();
264 assert_eq!(frama.update(f64::NAN), None);
265 assert_eq!(frama.update(f64::INFINITY), None);
266 }
267}