quantwave_core/indicators/
frama.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5#[derive(Debug, Clone)]
16pub struct FRAMA {
17 length: usize,
18 half_length: usize,
19 high_history: VecDeque<f64>,
20 low_history: VecDeque<f64>,
21 filt: f64,
22 initialized: bool,
23}
24
25impl FRAMA {
26 pub fn new(mut length: usize) -> Self {
27 if length % 2 != 0 {
29 length += 1;
30 }
31 let half_length = length / 2;
32
33 Self {
34 length,
35 half_length,
36 high_history: VecDeque::with_capacity(length),
37 low_history: VecDeque::with_capacity(length),
38 filt: 0.0,
39 initialized: false,
40 }
41 }
42}
43
44impl Next<(f64, f64, f64)> for FRAMA {
45 type Output = f64; fn next(&mut self, (high, low, price): (f64, f64, f64)) -> Self::Output {
48 if self.high_history.len() == self.length {
49 self.high_history.pop_back();
50 self.low_history.pop_back();
51 }
52
53 self.high_history.push_front(high);
54 self.low_history.push_front(low);
55
56 if self.high_history.len() < self.length {
58 self.filt = price;
59 return self.filt;
60 }
61
62 let mut hh1 = f64::MIN;
64 let mut ll1 = f64::MAX;
65 for i in 0..self.half_length {
66 hh1 = hh1.max(self.high_history[i]);
67 ll1 = ll1.min(self.low_history[i]);
68 }
69 let n1 = (hh1 - ll1) / (self.half_length as f64);
70
71 let mut hh2 = f64::MIN;
72 let mut ll2 = f64::MAX;
73 for i in self.half_length..self.length {
74 hh2 = hh2.max(self.high_history[i]);
75 ll2 = ll2.min(self.low_history[i]);
76 }
77 let n2 = (hh2 - ll2) / (self.half_length as f64);
78
79 let mut hh3 = f64::MIN;
80 let mut ll3 = f64::MAX;
81 for i in 0..self.length {
82 hh3 = hh3.max(self.high_history[i]);
83 ll3 = ll3.min(self.low_history[i]);
84 }
85 let n3 = (hh3 - ll3) / (self.length as f64);
86
87 let mut dimen = 1.0;
88 if n1 > 0.0 && n2 > 0.0 && n3 > 0.0 {
89 dimen = ((n1 + n2).ln() - n3.ln()) / std::f64::consts::LN_2;
90 }
91
92 let mut alpha = (-4.6 * (dimen - 1.0)).exp();
93 alpha = alpha.clamp(0.01, 1.0);
94
95 if !self.initialized {
96 self.filt = price;
97 self.initialized = true;
98 } else {
99 self.filt = alpha * price + (1.0 - alpha) * self.filt;
100 }
101
102 self.filt
103 }
104}
105
106pub const FRAMA_METADATA: IndicatorMetadata = IndicatorMetadata {
107 name: "Fractal Adaptive Moving Average",
108 description: "An adaptive moving average that uses the fractal dimension of prices to dynamically change its smoothing constant.",
109 params: &[ParamDef {
110 name: "length",
111 default: "16",
112 description: "Length (must be an even number; odd values will be incremented by 1).",
113 }],
114 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/implemented/FRAMA.pdf",
115 formula_latex: r#"
116\[
117D = \frac{\log(N_1 + N_2) - \log(N_3)}{\log(2)}
118\]
119\[
120\alpha = \exp(-4.6 (D - 1))
121\]
122\[
123\text{FRAMA}_t = \alpha P_t + (1 - \alpha) \text{FRAMA}_{t-1}
124\]
125"#,
126 gold_standard_file: "frama.json",
127 category: "Ehlers DSP",
128};
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use proptest::prelude::*;
134
135 fn frama_batch(data: &[(f64, f64, f64)], length: usize) -> Vec<f64> {
136 let mut indicator = FRAMA::new(length);
137 data.iter().map(|&x| indicator.next(x)).collect()
138 }
139
140 proptest! {
141 #[test]
142 fn test_frama_parity(input in prop::collection::vec((0.1..100.0, 0.1..100.0, 0.1..100.0), 1..100)) {
143 let mut adj_input = Vec::with_capacity(input.len());
145 for (h, l, p) in input {
146 let h_f64: f64 = h;
147 let l_f64: f64 = l;
148 let high = h_f64.max(l_f64);
149 let low = h_f64.min(l_f64);
150 adj_input.push((high, low, p));
151 }
152
153 let length = 16;
154 let mut streaming_ind = FRAMA::new(length);
155 let streaming_results: Vec<f64> = adj_input.iter().map(|&x| streaming_ind.next(x)).collect();
156 let batch_results = frama_batch(&adj_input, length);
157
158 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
159 approx::assert_relative_eq!(*s, *b, epsilon = 1e-6);
160 }
161 }
162 }
163}