Skip to main content

quantwave_core/indicators/
fractals.rs

1use crate::indicators::metadata::IndicatorMetadata;
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5/// Bill Williams Fractals
6/// Identifies a bearish (up) fractal if High[t-2] is greater than High[t-4, t-3, t-1, t].
7/// Identifies a bullish (down) fractal if Low[t-2] is less than Low[t-4, t-3, t-1, t].
8/// The output is (Bearish, Bullish) meaning (Up Fractal, Down Fractal) at the current bar
9/// which validates the fractal that formed 2 bars ago.
10#[derive(Debug, Clone)]
11pub struct BillWilliamsFractals {
12    highs: VecDeque<f64>,
13    lows: VecDeque<f64>,
14}
15
16impl Default for BillWilliamsFractals {
17    fn default() -> Self {
18        Self::new()
19    }
20}
21
22impl BillWilliamsFractals {
23    pub fn new() -> Self {
24        Self {
25            highs: VecDeque::with_capacity(5),
26            lows: VecDeque::with_capacity(5),
27        }
28    }
29}
30
31impl Next<(f64, f64)> for BillWilliamsFractals {
32    type Output = (bool, bool); // (Bearish/Up, Bullish/Down)
33
34    fn next(&mut self, (high, low): (f64, f64)) -> Self::Output {
35        self.highs.push_back(high);
36        self.lows.push_back(low);
37
38        if self.highs.len() > 5 {
39            self.highs.pop_front();
40            self.lows.pop_front();
41        }
42
43        if self.highs.len() < 5 {
44            return (false, false);
45        }
46
47        let bearish = self.highs[2] > self.highs[0]
48            && self.highs[2] > self.highs[1]
49            && self.highs[2] > self.highs[3]
50            && self.highs[2] > self.highs[4];
51
52        let bullish = self.lows[2] < self.lows[0]
53            && self.lows[2] < self.lows[1]
54            && self.lows[2] < self.lows[3]
55            && self.lows[2] < self.lows[4];
56
57        (bearish, bullish)
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use proptest::prelude::*;
65    use serde::Deserialize;
66    use std::fs;
67    use std::path::Path;
68
69    #[derive(Debug, Deserialize)]
70    struct FractalCase {
71        high: Vec<f64>,
72        low: Vec<f64>,
73        expected_bearish: Vec<bool>,
74        expected_bullish: Vec<bool>,
75    }
76
77    #[test]
78    fn test_fractals_gold_standard() {
79        let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
80        let manifest_path = Path::new(&manifest_dir);
81        let path = manifest_path.join("tests/gold_standard/fractals.json");
82        let path = if path.exists() {
83            path
84        } else {
85            manifest_path
86                .parent()
87                .unwrap()
88                .join("tests/gold_standard/fractals.json")
89        };
90        let content = fs::read_to_string(path).unwrap();
91        let case: FractalCase = serde_json::from_str(&content).unwrap();
92
93        let mut fractals = BillWilliamsFractals::new();
94        for i in 0..case.high.len() {
95            let (bearish, bullish) = fractals.next((case.high[i], case.low[i]));
96            assert_eq!(bearish, case.expected_bearish[i]);
97            assert_eq!(bullish, case.expected_bullish[i]);
98        }
99    }
100
101    fn fractals_batch(data: Vec<(f64, f64)>) -> Vec<(bool, bool)> {
102        let mut fractals = BillWilliamsFractals::new();
103        data.into_iter().map(|x| fractals.next(x)).collect()
104    }
105
106    proptest! {
107        #[test]
108        fn test_fractals_parity(input in prop::collection::vec((0.0..100.0, 0.0..100.0), 1..100)) {
109            let mut adj_input = Vec::with_capacity(input.len());
110            for (h, l) in input {
111                let h_f: f64 = h;
112                let l_f: f64 = l;
113                let high = h_f.max(l_f);
114                let low = l_f.min(h_f);
115                adj_input.push((high, low));
116            }
117
118            let mut fractals = BillWilliamsFractals::new();
119            let mut streaming_results = Vec::with_capacity(adj_input.len());
120            for &val in &adj_input {
121                streaming_results.push(fractals.next(val));
122            }
123
124            let batch_results = fractals_batch(adj_input);
125
126            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
127                assert_eq!(s.0, b.0);
128                assert_eq!(s.1, b.1);
129            }
130        }
131    }
132
133    #[test]
134    fn test_fractals_basic() {
135        let mut f = BillWilliamsFractals::new();
136        let h = vec![10.0, 11.0, 15.0, 12.0, 10.0];
137        let l = vec![5.0, 6.0, 2.0, 6.0, 7.0];
138
139        for i in 0..4 {
140            let (bear, bull) = f.next((h[i], l[i]));
141            assert!(!bear);
142            assert!(!bull);
143        }
144
145        let (bear, bull) = f.next((h[4], l[4]));
146        assert!(bear); // 15.0 > all
147        assert!(bull); // 2.0 < all
148    }
149}
150
151pub const FRACTALS_METADATA: IndicatorMetadata = IndicatorMetadata {
152    name: "Bill Williams Fractals",
153    description: "Fractals are indicators on candlestick charts that identify reversal points in the market.",
154    params: &[],
155    formula_source: "https://www.investopedia.com/terms/f/fractal.asp",
156    formula_latex: r#"
157\[
158\text{Up Fractal} = \text{High} > \text{High}_{t-1, t-2, t+1, t+2}
159\]
160"#,
161    gold_standard_file: "fractals.json",
162    category: "Classic",
163};