Skip to main content

indicators/signal/
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 std::collections::{HashMap, VecDeque};
7
8use crate::error::IndicatorError;
9use crate::indicator::{Indicator, IndicatorOutput};
10use crate::registry::{param_f64, param_usize};
11use crate::types::Candle;
12
13// ── Params ────────────────────────────────────────────────────────────────────
14
15#[derive(Debug, Clone)]
16pub struct StructureParams {
17    /// Half-width of the pivot detection window (bars on each side of pivot).
18    pub swing_len: usize,
19    /// Minimum ATR multiple required between swings to qualify.
20    pub atr_mult: f64,
21}
22
23impl Default for StructureParams {
24    fn default() -> Self {
25        Self {
26            swing_len: 5,
27            atr_mult: 0.5,
28        }
29    }
30}
31
32// ── Indicator wrapper ─────────────────────────────────────────────────────────
33
34/// Batch `Indicator` adapter for [`MarketStructure`].
35///
36/// Replays candles through the structure engine and emits per-bar:
37/// `struct_bias`, `struct_fib618`, `struct_fib500`,
38/// `struct_in_discount`, `struct_in_premium`,
39/// `struct_bos`, `struct_choch`, `struct_confluence`.
40#[derive(Debug, Clone)]
41pub struct StructureIndicator {
42    pub params: StructureParams,
43}
44
45impl StructureIndicator {
46    pub fn new(params: StructureParams) -> Self {
47        Self { params }
48    }
49    pub fn with_defaults() -> Self {
50        Self::new(StructureParams::default())
51    }
52}
53
54impl Indicator for StructureIndicator {
55    fn name(&self) -> &'static str {
56        "Structure"
57    }
58    /// `swing_len * 4 + 10` mirrors the internal `maxlen` in [`MarketStructure`].
59    fn required_len(&self) -> usize {
60        self.params.swing_len * 4 + 10
61    }
62    fn required_columns(&self) -> &[&'static str] {
63        &["high", "low", "close"]
64    }
65
66    fn calculate(&self, candles: &[Candle]) -> Result<IndicatorOutput, IndicatorError> {
67        self.check_len(candles)?;
68        let p = &self.params;
69        let mut ms = MarketStructure::new(p.swing_len, p.atr_mult);
70        let n = candles.len();
71        let mut bias = vec![f64::NAN; n];
72        let mut fib618 = vec![f64::NAN; n];
73        let mut fib500 = vec![f64::NAN; n];
74        let mut in_discount = vec![f64::NAN; n];
75        let mut in_premium = vec![f64::NAN; n];
76        let mut bos = vec![f64::NAN; n];
77        let mut choch = vec![f64::NAN; n];
78        let mut confluence = vec![f64::NAN; n];
79        for (i, c) in candles.iter().enumerate() {
80            ms.update(c);
81            bias[i] = ms.bias as f64;
82            fib618[i] = ms.fib618.unwrap_or(f64::NAN);
83            fib500[i] = ms.fib500.unwrap_or(f64::NAN);
84            in_discount[i] = if ms.in_discount { 1.0 } else { 0.0 };
85            in_premium[i] = if ms.in_premium { 1.0 } else { 0.0 };
86            bos[i] = if ms.bos { 1.0 } else { 0.0 };
87            choch[i] = if ms.choch { 1.0 } else { 0.0 };
88            confluence[i] = ms.confluence;
89        }
90        Ok(IndicatorOutput::from_pairs([
91            ("struct_bias", bias),
92            ("struct_fib618", fib618),
93            ("struct_fib500", fib500),
94            ("struct_in_discount", in_discount),
95            ("struct_in_premium", in_premium),
96            ("struct_bos", bos),
97            ("struct_choch", choch),
98            ("struct_confluence", confluence),
99        ]))
100    }
101}
102
103// ── Registry factory ──────────────────────────────────────────────────────────
104
105pub fn factory<S: ::std::hash::BuildHasher>(params: &HashMap<String, String, S>) -> Result<Box<dyn Indicator>, IndicatorError> {
106    let swing_len = param_usize(params, "swing_len", 5)?;
107    let atr_mult = param_f64(params, "atr_mult", 0.5)?;
108    Ok(Box::new(StructureIndicator::new(StructureParams {
109        swing_len,
110        atr_mult,
111    })))
112}
113
114#[derive(Debug)]
115pub struct MarketStructure {
116    swing_len: usize,
117    atr_mult: f64,
118    maxlen: usize,
119
120    highs: VecDeque<f64>,
121    lows: VecDeque<f64>,
122    closes: VecDeque<f64>,
123
124    swing_hi: Option<f64>,
125    swing_lo: Option<f64>,
126    prev_swing_hi: Option<f64>,
127    prev_swing_lo: Option<f64>,
128    atr: Option<f64>,
129    bias_internal: i8,
130    fib_hi: Option<f64>,
131    fib_lo: Option<f64>,
132    fib_dir: i8,
133    last_broken_hi: Option<f64>,
134    last_broken_lo: Option<f64>,
135
136    // Published state
137    pub bias: i8,
138    pub fib618: Option<f64>,
139    pub fib500: Option<f64>,
140    pub fib382: Option<f64>,
141    pub fib786: Option<f64>,
142    pub in_discount: bool,
143    pub in_premium: bool,
144    pub bos: bool,
145    pub choch: bool,
146    pub choch_dir: i8,
147    /// 0–100 Fibonacci confluence score.
148    pub confluence: f64,
149}
150
151impl MarketStructure {
152    pub fn new(swing_len: usize, atr_mult_min: f64) -> Self {
153        let maxlen = swing_len * 4 + 10;
154        Self {
155            swing_len,
156            atr_mult: atr_mult_min,
157            maxlen,
158            highs: VecDeque::with_capacity(maxlen),
159            lows: VecDeque::with_capacity(maxlen),
160            closes: VecDeque::with_capacity(maxlen),
161            swing_hi: None,
162            swing_lo: None,
163            prev_swing_hi: None,
164            prev_swing_lo: None,
165            atr: None,
166            bias_internal: 0,
167            fib_hi: None,
168            fib_lo: None,
169            fib_dir: 0,
170            last_broken_hi: None,
171            last_broken_lo: None,
172            bias: 0,
173            fib618: None,
174            fib500: None,
175            fib382: None,
176            fib786: None,
177            in_discount: false,
178            in_premium: false,
179            bos: false,
180            choch: false,
181            choch_dir: 0,
182            confluence: 0.0,
183        }
184    }
185
186    pub fn update(&mut self, candle: &Candle) {
187        if self.highs.len() == self.maxlen {
188            self.highs.pop_front();
189        }
190        if self.lows.len() == self.maxlen {
191            self.lows.pop_front();
192        }
193        if self.closes.len() == self.maxlen {
194            self.closes.pop_front();
195        }
196        self.highs.push_back(candle.high);
197        self.lows.push_back(candle.low);
198        self.closes.push_back(candle.close);
199
200        // ATR (Wilder 1/14)
201        let prev_c = if self.closes.len() >= 2 {
202            *self.closes.iter().rev().nth(1).unwrap()
203        } else {
204            candle.close
205        };
206        let tr = (candle.high - candle.low)
207            .max((candle.high - prev_c).abs())
208            .max((candle.low - prev_c).abs());
209        self.atr = Some(match self.atr {
210            None => tr,
211            Some(prev) => prev / 14.0 + tr * (1.0 - 1.0 / 14.0),
212        });
213        let atr = self.atr.unwrap_or(1e-9).max(1e-9);
214
215        let ph = self.pivot_high();
216        let pl = self.pivot_low();
217
218        self.bos = false;
219        self.choch = false;
220        self.choch_dir = 0;
221
222        if let Some(ph_val) = ph {
223            let atr_ok = self
224                .swing_lo
225                .is_none_or(|slo| (ph_val - slo) >= atr * self.atr_mult);
226            if atr_ok {
227                self.prev_swing_hi = self.swing_hi;
228                self.swing_hi = Some(ph_val);
229            }
230        }
231        if let Some(pl_val) = pl {
232            let atr_ok = self
233                .swing_hi
234                .is_none_or(|shi| (shi - pl_val) >= atr * self.atr_mult);
235            if atr_ok {
236                self.prev_swing_lo = self.swing_lo;
237                self.swing_lo = Some(pl_val);
238            }
239        }
240
241        let cl = candle.close;
242
243        if let Some(shi) = self.swing_hi
244            && cl > shi
245            && self.last_broken_hi != Some(shi)
246        {
247            if self.bias_internal <= 0 {
248                self.choch = true;
249                self.choch_dir = 1;
250                self.fib_dir = 1;
251                self.fib_hi = Some(candle.high);
252                self.fib_lo = self.swing_lo;
253            } else {
254                self.bos = true;
255                self.fib_hi = Some(candle.high);
256                self.fib_lo = self.swing_lo;
257                self.fib_dir = 1;
258            }
259            self.bias_internal = 1;
260            self.last_broken_hi = Some(shi);
261        }
262        if let Some(slo) = self.swing_lo
263            && cl < slo
264            && self.last_broken_lo != Some(slo)
265        {
266            if self.bias_internal >= 0 {
267                self.choch = true;
268                self.choch_dir = -1;
269                self.fib_dir = -1;
270                self.fib_lo = Some(candle.low);
271                self.fib_hi = self.swing_hi;
272            } else {
273                self.bos = true;
274                self.fib_lo = Some(candle.low);
275                self.fib_hi = self.swing_hi;
276                self.fib_dir = -1;
277            }
278            self.bias_internal = -1;
279            self.last_broken_lo = Some(slo);
280        }
281
282        self.bias = self.bias_internal;
283
284        if let (Some(fh), Some(fl)) = (self.fib_hi, self.fib_lo)
285            && self.fib_dir != 0
286        {
287            self.compute_fibs(fh, fl, self.fib_dir);
288        }
289
290        if let (Some(f5), dir) = (self.fib500, self.fib_dir) {
291            if dir != 0 {
292                if dir == 1 {
293                    self.in_discount = cl <= f5;
294                    self.in_premium = cl > f5;
295                } else {
296                    self.in_premium = cl >= f5;
297                    self.in_discount = cl < f5;
298                }
299            }
300        } else {
301            self.in_discount = false;
302            self.in_premium = false;
303        }
304
305        // Fibonacci confluence score
306        let tol = atr * 0.3;
307        let mut score = 0.0_f64;
308        if self.fib382.is_some_and(|f| (cl - f).abs() < tol) {
309            score += 1.5;
310        }
311        if self.fib500.is_some_and(|f| (cl - f).abs() < tol) {
312            score += 2.0;
313        }
314        if self.fib618.is_some_and(|f| (cl - f).abs() < tol) {
315            score += 2.5;
316        }
317        if self.fib786.is_some_and(|f| (cl - f).abs() < tol) {
318            score += 1.5;
319        }
320        self.confluence = (score * 10.0).min(100.0);
321    }
322
323    fn pivot_high(&self) -> Option<f64> {
324        let arr: Vec<f64> = self.highs.iter().copied().collect();
325        let n = self.swing_len;
326        if arr.len() < 2 * n + 1 {
327            return None;
328        }
329        let mid = arr[arr.len() - n - 1];
330        let left_ok = (1..=n).all(|i| mid >= arr[arr.len() - n - 1 - i]);
331        let right_ok = (1..=n).all(|i| mid >= arr[arr.len() - n - 1 + i]);
332        if left_ok && right_ok { Some(mid) } else { None }
333    }
334
335    fn pivot_low(&self) -> Option<f64> {
336        let arr: Vec<f64> = self.lows.iter().copied().collect();
337        let n = self.swing_len;
338        if arr.len() < 2 * n + 1 {
339            return None;
340        }
341        let mid = arr[arr.len() - n - 1];
342        let left_ok = (1..=n).all(|i| mid <= arr[arr.len() - n - 1 - i]);
343        let right_ok = (1..=n).all(|i| mid <= arr[arr.len() - n - 1 + i]);
344        if left_ok && right_ok { Some(mid) } else { None }
345    }
346
347    fn compute_fibs(&mut self, hi: f64, lo: f64, direction: i8) {
348        let rng = hi - lo;
349        if rng <= 0.0 {
350            return;
351        }
352        if direction == 1 {
353            self.fib382 = Some(hi - rng * 0.382);
354            self.fib500 = Some(hi - rng * 0.500);
355            self.fib618 = Some(hi - rng * 0.618);
356            self.fib786 = Some(hi - rng * 0.786);
357        } else {
358            self.fib382 = Some(lo + rng * 0.382);
359            self.fib500 = Some(lo + rng * 0.500);
360            self.fib618 = Some(lo + rng * 0.618);
361            self.fib786 = Some(lo + rng * 0.786);
362        }
363    }
364}