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