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 = 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 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}