1use 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#[derive(Debug, Clone)]
16pub struct StructureParams {
17 pub swing_len: usize,
19 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#[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 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
103pub 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 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 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 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 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}