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>(
106    params: &HashMap<String, String, S>,
107) -> Result<Box<dyn Indicator>, IndicatorError> {
108    let swing_len = param_usize(params, "swing_len", 5)?;
109    let atr_mult = param_f64(params, "atr_mult", 0.5)?;
110    Ok(Box::new(StructureIndicator::new(StructureParams {
111        swing_len,
112        atr_mult,
113    })))
114}
115
116#[derive(Debug)]
117pub struct MarketStructure {
118    swing_len: usize,
119    atr_mult: f64,
120    maxlen: usize,
121
122    highs: VecDeque<f64>,
123    lows: VecDeque<f64>,
124    closes: VecDeque<f64>,
125
126    swing_hi: Option<f64>,
127    swing_lo: Option<f64>,
128    prev_swing_hi: Option<f64>,
129    prev_swing_lo: Option<f64>,
130    atr: Option<f64>,
131    bias_internal: i8,
132    fib_hi: Option<f64>,
133    fib_lo: Option<f64>,
134    fib_dir: i8,
135    last_broken_hi: Option<f64>,
136    last_broken_lo: Option<f64>,
137
138    // Published state
139    pub bias: i8,
140    pub fib618: Option<f64>,
141    pub fib500: Option<f64>,
142    pub fib382: Option<f64>,
143    pub fib786: Option<f64>,
144    pub in_discount: bool,
145    pub in_premium: bool,
146    pub bos: bool,
147    pub choch: bool,
148    pub choch_dir: i8,
149    /// 0–100 Fibonacci confluence score.
150    pub confluence: f64,
151}
152
153impl MarketStructure {
154    pub fn new(swing_len: usize, atr_mult_min: f64) -> Self {
155        let maxlen = swing_len * 4 + 10;
156        Self {
157            swing_len,
158            atr_mult: atr_mult_min,
159            maxlen,
160            highs: VecDeque::with_capacity(maxlen),
161            lows: VecDeque::with_capacity(maxlen),
162            closes: VecDeque::with_capacity(maxlen),
163            swing_hi: None,
164            swing_lo: None,
165            prev_swing_hi: None,
166            prev_swing_lo: None,
167            atr: None,
168            bias_internal: 0,
169            fib_hi: None,
170            fib_lo: None,
171            fib_dir: 0,
172            last_broken_hi: None,
173            last_broken_lo: None,
174            bias: 0,
175            fib618: None,
176            fib500: None,
177            fib382: None,
178            fib786: None,
179            in_discount: false,
180            in_premium: false,
181            bos: false,
182            choch: false,
183            choch_dir: 0,
184            confluence: 0.0,
185        }
186    }
187
188    pub fn update(&mut self, candle: &Candle) {
189        if self.highs.len() == self.maxlen {
190            self.highs.pop_front();
191        }
192        if self.lows.len() == self.maxlen {
193            self.lows.pop_front();
194        }
195        if self.closes.len() == self.maxlen {
196            self.closes.pop_front();
197        }
198        self.highs.push_back(candle.high);
199        self.lows.push_back(candle.low);
200        self.closes.push_back(candle.close);
201
202        // ATR (Wilder 1/14)
203        let prev_c = self
204            .closes
205            .iter()
206            .rev()
207            .nth(1)
208            .copied()
209            .unwrap_or(candle.close);
210        let tr = (candle.high - candle.low)
211            .max((candle.high - prev_c).abs())
212            .max((candle.low - prev_c).abs());
213        self.atr = Some(match self.atr {
214            None => tr,
215            Some(prev) => tr / 14.0 + prev * (1.0 - 1.0 / 14.0),
216        });
217        let atr = self.atr.unwrap_or(1e-9).max(1e-9);
218
219        let ph = self.pivot_high();
220        let pl = self.pivot_low();
221
222        self.bos = false;
223        self.choch = false;
224        self.choch_dir = 0;
225
226        if let Some(ph_val) = ph {
227            let atr_ok = self
228                .swing_lo
229                .is_none_or(|slo| (ph_val - slo) >= atr * self.atr_mult);
230            if atr_ok {
231                self.prev_swing_hi = self.swing_hi;
232                self.swing_hi = Some(ph_val);
233            }
234        }
235        if let Some(pl_val) = pl {
236            let atr_ok = self
237                .swing_hi
238                .is_none_or(|shi| (shi - pl_val) >= atr * self.atr_mult);
239            if atr_ok {
240                self.prev_swing_lo = self.swing_lo;
241                self.swing_lo = Some(pl_val);
242            }
243        }
244
245        let cl = candle.close;
246
247        if let Some(shi) = self.swing_hi
248            && cl > shi
249            && self.last_broken_hi != Some(shi)
250        {
251            if self.bias_internal <= 0 {
252                self.choch = true;
253                self.choch_dir = 1;
254                self.fib_dir = 1;
255                self.fib_hi = Some(candle.high);
256                self.fib_lo = self.swing_lo;
257            } else {
258                self.bos = true;
259                self.fib_hi = Some(candle.high);
260                self.fib_lo = self.swing_lo;
261                self.fib_dir = 1;
262            }
263            self.bias_internal = 1;
264            self.last_broken_hi = Some(shi);
265        }
266        if let Some(slo) = self.swing_lo
267            && cl < slo
268            && self.last_broken_lo != Some(slo)
269        {
270            if self.bias_internal >= 0 {
271                self.choch = true;
272                self.choch_dir = -1;
273                self.fib_dir = -1;
274                self.fib_lo = Some(candle.low);
275                self.fib_hi = self.swing_hi;
276            } else {
277                self.bos = true;
278                self.fib_lo = Some(candle.low);
279                self.fib_hi = self.swing_hi;
280                self.fib_dir = -1;
281            }
282            self.bias_internal = -1;
283            self.last_broken_lo = Some(slo);
284        }
285
286        self.bias = self.bias_internal;
287
288        if let (Some(fh), Some(fl)) = (self.fib_hi, self.fib_lo)
289            && self.fib_dir != 0
290        {
291            self.compute_fibs(fh, fl, self.fib_dir);
292        }
293
294        if let (Some(f5), dir) = (self.fib500, self.fib_dir) {
295            if dir != 0 {
296                if dir == 1 {
297                    self.in_discount = cl <= f5;
298                    self.in_premium = cl > f5;
299                } else {
300                    self.in_premium = cl >= f5;
301                    self.in_discount = cl < f5;
302                }
303            }
304        } else {
305            self.in_discount = false;
306            self.in_premium = false;
307        }
308
309        // Fibonacci confluence score
310        let tol = atr * 0.3;
311        let mut score = 0.0_f64;
312        if self.fib382.is_some_and(|f| (cl - f).abs() < tol) {
313            score += 1.5;
314        }
315        if self.fib500.is_some_and(|f| (cl - f).abs() < tol) {
316            score += 2.0;
317        }
318        if self.fib618.is_some_and(|f| (cl - f).abs() < tol) {
319            score += 2.5;
320        }
321        if self.fib786.is_some_and(|f| (cl - f).abs() < tol) {
322            score += 1.5;
323        }
324        self.confluence = (score * 10.0).min(100.0);
325    }
326
327    fn pivot_high(&self) -> Option<f64> {
328        let arr: Vec<f64> = self.highs.iter().copied().collect();
329        let n = self.swing_len;
330        if arr.len() < 2 * n + 1 {
331            return None;
332        }
333        let mid = arr[arr.len() - n - 1];
334        let left_ok = (1..=n).all(|i| mid >= arr[arr.len() - n - 1 - i]);
335        let right_ok = (1..=n).all(|i| mid >= arr[arr.len() - n - 1 + i]);
336        if left_ok && right_ok { Some(mid) } else { None }
337    }
338
339    fn pivot_low(&self) -> Option<f64> {
340        let arr: Vec<f64> = self.lows.iter().copied().collect();
341        let n = self.swing_len;
342        if arr.len() < 2 * n + 1 {
343            return None;
344        }
345        let mid = arr[arr.len() - n - 1];
346        let left_ok = (1..=n).all(|i| mid <= arr[arr.len() - n - 1 - i]);
347        let right_ok = (1..=n).all(|i| mid <= arr[arr.len() - n - 1 + i]);
348        if left_ok && right_ok { Some(mid) } else { None }
349    }
350
351    fn compute_fibs(&mut self, hi: f64, lo: f64, direction: i8) {
352        let rng = hi - lo;
353        if rng <= 0.0 {
354            return;
355        }
356        if direction == 1 {
357            self.fib382 = Some(hi - rng * 0.382);
358            self.fib500 = Some(hi - rng * 0.500);
359            self.fib618 = Some(hi - rng * 0.618);
360            self.fib786 = Some(hi - rng * 0.786);
361        } else {
362            self.fib382 = Some(lo + rng * 0.382);
363            self.fib500 = Some(lo + rng * 0.500);
364            self.fib618 = Some(lo + rng * 0.618);
365            self.fib786 = Some(lo + rng * 0.786);
366        }
367    }
368}