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