Skip to main content

indicators/
structure.rs

1//! Layer 7 — Market Structure + Fibonacci Engine.
2//!
3//! Detects swing highs/lows, identifies Break of Structure (BOS) and
4//! Change of Character (CHoCH), and computes Fibonacci retracement levels.
5
6use crate::types::Candle;
7use std::collections::VecDeque;
8
9pub struct MarketStructure {
10    swing_len: usize,
11    atr_mult: f64,
12    maxlen: usize,
13
14    highs: VecDeque<f64>,
15    lows: VecDeque<f64>,
16    closes: VecDeque<f64>,
17
18    swing_hi: Option<f64>,
19    swing_lo: Option<f64>,
20    prev_swing_hi: Option<f64>,
21    prev_swing_lo: Option<f64>,
22    atr: Option<f64>,
23    bias_internal: i8,
24    fib_hi: Option<f64>,
25    fib_lo: Option<f64>,
26    fib_dir: i8,
27    last_broken_hi: Option<f64>,
28    last_broken_lo: Option<f64>,
29
30    // Published state
31    pub bias: i8,
32    pub fib618: Option<f64>,
33    pub fib500: Option<f64>,
34    pub fib382: Option<f64>,
35    pub fib786: Option<f64>,
36    pub in_discount: bool,
37    pub in_premium: bool,
38    pub bos: bool,
39    pub choch: bool,
40    pub choch_dir: i8,
41    /// 0–100 Fibonacci confluence score.
42    pub confluence: f64,
43}
44
45impl MarketStructure {
46    pub fn new(swing_len: usize, atr_mult_min: f64) -> Self {
47        let maxlen = swing_len * 4 + 10;
48        Self {
49            swing_len,
50            atr_mult: atr_mult_min,
51            maxlen,
52            highs: VecDeque::with_capacity(maxlen),
53            lows: VecDeque::with_capacity(maxlen),
54            closes: VecDeque::with_capacity(maxlen),
55            swing_hi: None,
56            swing_lo: None,
57            prev_swing_hi: None,
58            prev_swing_lo: None,
59            atr: None,
60            bias_internal: 0,
61            fib_hi: None,
62            fib_lo: None,
63            fib_dir: 0,
64            last_broken_hi: None,
65            last_broken_lo: None,
66            bias: 0,
67            fib618: None,
68            fib500: None,
69            fib382: None,
70            fib786: None,
71            in_discount: false,
72            in_premium: false,
73            bos: false,
74            choch: false,
75            choch_dir: 0,
76            confluence: 0.0,
77        }
78    }
79
80    pub fn update(&mut self, candle: &Candle) {
81        if self.highs.len() == self.maxlen {
82            self.highs.pop_front();
83        }
84        if self.lows.len() == self.maxlen {
85            self.lows.pop_front();
86        }
87        if self.closes.len() == self.maxlen {
88            self.closes.pop_front();
89        }
90        self.highs.push_back(candle.high);
91        self.lows.push_back(candle.low);
92        self.closes.push_back(candle.close);
93
94        // ATR (Wilder 1/14)
95        let prev_c = if self.closes.len() >= 2 {
96            *self.closes.iter().rev().nth(1).unwrap()
97        } else {
98            candle.close
99        };
100        let tr = (candle.high - candle.low)
101            .max((candle.high - prev_c).abs())
102            .max((candle.low - prev_c).abs());
103        self.atr = Some(match self.atr {
104            None => tr,
105            Some(prev) => prev / 14.0 + tr * (1.0 - 1.0 / 14.0),
106        });
107        let atr = self.atr.unwrap_or(1e-9).max(1e-9);
108
109        let ph = self.pivot_high();
110        let pl = self.pivot_low();
111
112        self.bos = false;
113        self.choch = false;
114        self.choch_dir = 0;
115
116        if let Some(ph_val) = ph {
117            let atr_ok = self
118                .swing_lo
119                .is_none_or(|slo| (ph_val - slo) >= atr * self.atr_mult);
120            if atr_ok {
121                self.prev_swing_hi = self.swing_hi;
122                self.swing_hi = Some(ph_val);
123            }
124        }
125        if let Some(pl_val) = pl {
126            let atr_ok = self
127                .swing_hi
128                .is_none_or(|shi| (shi - pl_val) >= atr * self.atr_mult);
129            if atr_ok {
130                self.prev_swing_lo = self.swing_lo;
131                self.swing_lo = Some(pl_val);
132            }
133        }
134
135        let cl = candle.close;
136
137        if let Some(shi) = self.swing_hi
138            && cl > shi
139            && self.last_broken_hi != Some(shi)
140        {
141            if self.bias_internal <= 0 {
142                self.choch = true;
143                self.choch_dir = 1;
144                self.fib_dir = 1;
145                self.fib_hi = Some(candle.high);
146                self.fib_lo = self.swing_lo;
147            } else {
148                self.bos = true;
149                self.fib_hi = Some(candle.high);
150                self.fib_lo = self.swing_lo;
151                self.fib_dir = 1;
152            }
153            self.bias_internal = 1;
154            self.last_broken_hi = Some(shi);
155        }
156        if let Some(slo) = self.swing_lo
157            && cl < slo
158            && self.last_broken_lo != Some(slo)
159        {
160            if self.bias_internal >= 0 {
161                self.choch = true;
162                self.choch_dir = -1;
163                self.fib_dir = -1;
164                self.fib_lo = Some(candle.low);
165                self.fib_hi = self.swing_hi;
166            } else {
167                self.bos = true;
168                self.fib_lo = Some(candle.low);
169                self.fib_hi = self.swing_hi;
170                self.fib_dir = -1;
171            }
172            self.bias_internal = -1;
173            self.last_broken_lo = Some(slo);
174        }
175
176        self.bias = self.bias_internal;
177
178        if let (Some(fh), Some(fl)) = (self.fib_hi, self.fib_lo)
179            && self.fib_dir != 0
180        {
181            self.compute_fibs(fh, fl, self.fib_dir);
182        }
183
184        if let (Some(f5), dir) = (self.fib500, self.fib_dir) {
185            if dir != 0 {
186                if dir == 1 {
187                    self.in_discount = cl <= f5;
188                    self.in_premium = cl > f5;
189                } else {
190                    self.in_premium = cl >= f5;
191                    self.in_discount = cl < f5;
192                }
193            }
194        } else {
195            self.in_discount = false;
196            self.in_premium = false;
197        }
198
199        // Fibonacci confluence score
200        let tol = atr * 0.3;
201        let mut score = 0.0_f64;
202        if self.fib382.is_some_and(|f| (cl - f).abs() < tol) {
203            score += 1.5;
204        }
205        if self.fib500.is_some_and(|f| (cl - f).abs() < tol) {
206            score += 2.0;
207        }
208        if self.fib618.is_some_and(|f| (cl - f).abs() < tol) {
209            score += 2.5;
210        }
211        if self.fib786.is_some_and(|f| (cl - f).abs() < tol) {
212            score += 1.5;
213        }
214        self.confluence = (score * 10.0).min(100.0);
215    }
216
217    fn pivot_high(&self) -> Option<f64> {
218        let arr: Vec<f64> = self.highs.iter().copied().collect();
219        let n = self.swing_len;
220        if arr.len() < 2 * n + 1 {
221            return None;
222        }
223        let mid = arr[arr.len() - n - 1];
224        let left_ok = (1..=n).all(|i| mid >= arr[arr.len() - n - 1 - i]);
225        let right_ok = (1..=n).all(|i| mid >= arr[arr.len() - n - 1 + i]);
226        if left_ok && right_ok { Some(mid) } else { None }
227    }
228
229    fn pivot_low(&self) -> Option<f64> {
230        let arr: Vec<f64> = self.lows.iter().copied().collect();
231        let n = self.swing_len;
232        if arr.len() < 2 * n + 1 {
233            return None;
234        }
235        let mid = arr[arr.len() - n - 1];
236        let left_ok = (1..=n).all(|i| mid <= arr[arr.len() - n - 1 - i]);
237        let right_ok = (1..=n).all(|i| mid <= arr[arr.len() - n - 1 + i]);
238        if left_ok && right_ok { Some(mid) } else { None }
239    }
240
241    fn compute_fibs(&mut self, hi: f64, lo: f64, direction: i8) {
242        let rng = hi - lo;
243        if rng <= 0.0 {
244            return;
245        }
246        if direction == 1 {
247            self.fib382 = Some(hi - rng * 0.382);
248            self.fib500 = Some(hi - rng * 0.500);
249            self.fib618 = Some(hi - rng * 0.618);
250            self.fib786 = Some(hi - rng * 0.786);
251        } else {
252            self.fib382 = Some(lo + rng * 0.382);
253            self.fib500 = Some(lo + rng * 0.500);
254            self.fib618 = Some(lo + rng * 0.618);
255            self.fib786 = Some(lo + rng * 0.786);
256        }
257    }
258}