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 = if self.closes.len() >= 2 {
204            *self.closes.iter().rev().nth(1).unwrap()
205        } else {
206            candle.close
207        };
208        let tr = (candle.high - candle.low)
209            .max((candle.high - prev_c).abs())
210            .max((candle.low - prev_c).abs());
211        self.atr = Some(match self.atr {
212            None => tr,
213            Some(prev) => prev / 14.0 + tr * (1.0 - 1.0 / 14.0),
214        });
215        let atr = self.atr.unwrap_or(1e-9).max(1e-9);
216
217        let ph = self.pivot_high();
218        let pl = self.pivot_low();
219
220        self.bos = false;
221        self.choch = false;
222        self.choch_dir = 0;
223
224        if let Some(ph_val) = ph {
225            let atr_ok = self
226                .swing_lo
227                .is_none_or(|slo| (ph_val - slo) >= atr * self.atr_mult);
228            if atr_ok {
229                self.prev_swing_hi = self.swing_hi;
230                self.swing_hi = Some(ph_val);
231            }
232        }
233        if let Some(pl_val) = pl {
234            let atr_ok = self
235                .swing_hi
236                .is_none_or(|shi| (shi - pl_val) >= atr * self.atr_mult);
237            if atr_ok {
238                self.prev_swing_lo = self.swing_lo;
239                self.swing_lo = Some(pl_val);
240            }
241        }
242
243        let cl = candle.close;
244
245        if let Some(shi) = self.swing_hi
246            && cl > shi
247            && self.last_broken_hi != Some(shi)
248        {
249            if self.bias_internal <= 0 {
250                self.choch = true;
251                self.choch_dir = 1;
252                self.fib_dir = 1;
253                self.fib_hi = Some(candle.high);
254                self.fib_lo = self.swing_lo;
255            } else {
256                self.bos = true;
257                self.fib_hi = Some(candle.high);
258                self.fib_lo = self.swing_lo;
259                self.fib_dir = 1;
260            }
261            self.bias_internal = 1;
262            self.last_broken_hi = Some(shi);
263        }
264        if let Some(slo) = self.swing_lo
265            && cl < slo
266            && self.last_broken_lo != Some(slo)
267        {
268            if self.bias_internal >= 0 {
269                self.choch = true;
270                self.choch_dir = -1;
271                self.fib_dir = -1;
272                self.fib_lo = Some(candle.low);
273                self.fib_hi = self.swing_hi;
274            } else {
275                self.bos = true;
276                self.fib_lo = Some(candle.low);
277                self.fib_hi = self.swing_hi;
278                self.fib_dir = -1;
279            }
280            self.bias_internal = -1;
281            self.last_broken_lo = Some(slo);
282        }
283
284        self.bias = self.bias_internal;
285
286        if let (Some(fh), Some(fl)) = (self.fib_hi, self.fib_lo)
287            && self.fib_dir != 0
288        {
289            self.compute_fibs(fh, fl, self.fib_dir);
290        }
291
292        if let (Some(f5), dir) = (self.fib500, self.fib_dir) {
293            if dir != 0 {
294                if dir == 1 {
295                    self.in_discount = cl <= f5;
296                    self.in_premium = cl > f5;
297                } else {
298                    self.in_premium = cl >= f5;
299                    self.in_discount = cl < f5;
300                }
301            }
302        } else {
303            self.in_discount = false;
304            self.in_premium = false;
305        }
306
307        // Fibonacci confluence score
308        let tol = atr * 0.3;
309        let mut score = 0.0_f64;
310        if self.fib382.is_some_and(|f| (cl - f).abs() < tol) {
311            score += 1.5;
312        }
313        if self.fib500.is_some_and(|f| (cl - f).abs() < tol) {
314            score += 2.0;
315        }
316        if self.fib618.is_some_and(|f| (cl - f).abs() < tol) {
317            score += 2.5;
318        }
319        if self.fib786.is_some_and(|f| (cl - f).abs() < tol) {
320            score += 1.5;
321        }
322        self.confluence = (score * 10.0).min(100.0);
323    }
324
325    fn pivot_high(&self) -> Option<f64> {
326        let arr: Vec<f64> = self.highs.iter().copied().collect();
327        let n = self.swing_len;
328        if arr.len() < 2 * n + 1 {
329            return None;
330        }
331        let mid = arr[arr.len() - n - 1];
332        let left_ok = (1..=n).all(|i| mid >= arr[arr.len() - n - 1 - i]);
333        let right_ok = (1..=n).all(|i| mid >= arr[arr.len() - n - 1 + i]);
334        if left_ok && right_ok { Some(mid) } else { None }
335    }
336
337    fn pivot_low(&self) -> Option<f64> {
338        let arr: Vec<f64> = self.lows.iter().copied().collect();
339        let n = self.swing_len;
340        if arr.len() < 2 * n + 1 {
341            return None;
342        }
343        let mid = arr[arr.len() - n - 1];
344        let left_ok = (1..=n).all(|i| mid <= arr[arr.len() - n - 1 - i]);
345        let right_ok = (1..=n).all(|i| mid <= arr[arr.len() - n - 1 + i]);
346        if left_ok && right_ok { Some(mid) } else { None }
347    }
348
349    fn compute_fibs(&mut self, hi: f64, lo: f64, direction: i8) {
350        let rng = hi - lo;
351        if rng <= 0.0 {
352            return;
353        }
354        if direction == 1 {
355            self.fib382 = Some(hi - rng * 0.382);
356            self.fib500 = Some(hi - rng * 0.500);
357            self.fib618 = Some(hi - rng * 0.618);
358            self.fib786 = Some(hi - rng * 0.786);
359        } else {
360            self.fib382 = Some(lo + rng * 0.382);
361            self.fib500 = Some(lo + rng * 0.500);
362            self.fib618 = Some(lo + rng * 0.618);
363            self.fib786 = Some(lo + rng * 0.786);
364        }
365    }
366}