quantwave_core/indicators/
fractals.rs1use crate::indicators::metadata::IndicatorMetadata;
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5#[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); 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); assert!(bull); }
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};