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.is_multiple_of(2) {
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 usage: "Use as an adaptive moving average that slows dramatically during consolidation and speeds up during trending phases. Outperforms fixed-period MAs in ranging markets by avoiding false crossovers.",
110 keywords: &["moving-average", "adaptive", "fractal", "smoothing"],
111 ehlers_summary: "The Fractal Adaptive Moving Average uses the fractal dimension of recent price action to adapt its smoothing constant. During trending markets the fractal dimension approaches 1 (a line) producing a fast-reacting EMA; during ranging markets the dimension approaches 2 (a plane) slowing the average dramatically to filter chop.",
112 params: &[ParamDef {
113 name: "length",
114 default: "16",
115 description: "Length (must be an even number; odd values will be incremented by 1).",
116 }],
117 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/implemented/FRAMA.pdf",
118 formula_latex: r#"
119\[
120D = \frac{\log(N_1 + N_2) - \log(N_3)}{\log(2)}
121\]
122\[
123\alpha = \exp(-4.6 (D - 1))
124\]
125\[
126\text{FRAMA}_t = \alpha P_t + (1 - \alpha) \text{FRAMA}_{t-1}
127\]
128"#,
129 gold_standard_file: "frama.json",
130 category: "Ehlers DSP",
131};
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use proptest::prelude::*;
137
138 fn frama_batch(data: &[(f64, f64, f64)], length: usize) -> Vec<f64> {
139 let mut indicator = FRAMA::new(length);
140 data.iter().map(|&x| indicator.next(x)).collect()
141 }
142
143 proptest! {
144 #[test]
145 fn test_frama_parity(input in prop::collection::vec((0.1..100.0, 0.1..100.0, 0.1..100.0), 1..100)) {
146 let mut adj_input = Vec::with_capacity(input.len());
148 for (h, l, p) in input {
149 let h_f64: f64 = h;
150 let l_f64: f64 = l;
151 let high = h_f64.max(l_f64);
152 let low = h_f64.min(l_f64);
153 adj_input.push((high, low, p));
154 }
155
156 let length = 16;
157 let mut streaming_ind = FRAMA::new(length);
158 let streaming_results: Vec<f64> = adj_input.iter().map(|&x| streaming_ind.next(x)).collect();
159 let batch_results = frama_batch(&adj_input, length);
160
161 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
162 approx::assert_relative_eq!(*s, *b, epsilon = 1e-6);
163 }
164 }
165 }
166}