Skip to main content

vector_ta/indicators/
market_structure_confluence.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9#[cfg(feature = "python")]
10use pyo3::wrap_pyfunction;
11
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use serde::{Deserialize, Serialize};
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use wasm_bindgen::prelude::*;
16
17use crate::utilities::data_loader::Candles;
18use crate::utilities::enums::Kernel;
19use crate::utilities::helpers::{
20    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
21    make_uninit_matrix,
22};
23#[cfg(feature = "python")]
24use crate::utilities::kernel_validation::validate_kernel;
25#[cfg(not(target_arch = "wasm32"))]
26use rayon::prelude::*;
27use std::collections::VecDeque;
28use std::mem::{ManuallyDrop, MaybeUninit};
29use thiserror::Error;
30
31const DEFAULT_SWING_SIZE: usize = 10;
32const DEFAULT_BOS_CONFIRMATION: &str = "Candle Close";
33const DEFAULT_BASIS_LENGTH: usize = 100;
34const DEFAULT_ATR_LENGTH: usize = 14;
35const DEFAULT_ATR_SMOOTH: usize = 21;
36const DEFAULT_VOL_MULT: f64 = 2.0;
37const EPS: f64 = 1e-12;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum MarketStructureConfluenceBosConfirmation {
41    CandleClose,
42    Wicks,
43}
44
45impl MarketStructureConfluenceBosConfirmation {
46    #[inline(always)]
47    fn parse(value: &str) -> Option<Self> {
48        match value {
49            "Candle Close" | "candle_close" | "candle close" => Some(Self::CandleClose),
50            "Wicks" | "wicks" => Some(Self::Wicks),
51            _ => None,
52        }
53    }
54}
55
56#[derive(Debug, Clone)]
57pub enum MarketStructureConfluenceData<'a> {
58    Candles {
59        candles: &'a Candles,
60    },
61    Slices {
62        high: &'a [f64],
63        low: &'a [f64],
64        close: &'a [f64],
65    },
66}
67
68#[derive(Debug, Clone)]
69pub struct MarketStructureConfluenceOutput {
70    pub basis: Vec<f64>,
71    pub upper_band: Vec<f64>,
72    pub lower_band: Vec<f64>,
73    pub structure_direction: Vec<f64>,
74    pub bullish_arrow: Vec<f64>,
75    pub bearish_arrow: Vec<f64>,
76    pub bullish_change: Vec<f64>,
77    pub bearish_change: Vec<f64>,
78    pub hh: Vec<f64>,
79    pub lh: Vec<f64>,
80    pub hl: Vec<f64>,
81    pub ll: Vec<f64>,
82    pub bullish_bos: Vec<f64>,
83    pub bullish_choch: Vec<f64>,
84    pub bearish_bos: Vec<f64>,
85    pub bearish_choch: Vec<f64>,
86}
87
88#[derive(Debug, Clone)]
89#[cfg_attr(
90    all(target_arch = "wasm32", feature = "wasm"),
91    derive(Serialize, Deserialize)
92)]
93pub struct MarketStructureConfluenceParams {
94    pub swing_size: Option<usize>,
95    pub bos_confirmation: Option<String>,
96    pub basis_length: Option<usize>,
97    pub atr_length: Option<usize>,
98    pub atr_smooth: Option<usize>,
99    pub vol_mult: Option<f64>,
100}
101
102impl Default for MarketStructureConfluenceParams {
103    fn default() -> Self {
104        Self {
105            swing_size: Some(DEFAULT_SWING_SIZE),
106            bos_confirmation: Some(DEFAULT_BOS_CONFIRMATION.to_string()),
107            basis_length: Some(DEFAULT_BASIS_LENGTH),
108            atr_length: Some(DEFAULT_ATR_LENGTH),
109            atr_smooth: Some(DEFAULT_ATR_SMOOTH),
110            vol_mult: Some(DEFAULT_VOL_MULT),
111        }
112    }
113}
114
115#[derive(Debug, Clone)]
116pub struct MarketStructureConfluenceInput<'a> {
117    pub data: MarketStructureConfluenceData<'a>,
118    pub params: MarketStructureConfluenceParams,
119}
120
121impl<'a> MarketStructureConfluenceInput<'a> {
122    #[inline]
123    pub fn from_candles(candles: &'a Candles, params: MarketStructureConfluenceParams) -> Self {
124        Self {
125            data: MarketStructureConfluenceData::Candles { candles },
126            params,
127        }
128    }
129
130    #[inline]
131    pub fn from_slices(
132        high: &'a [f64],
133        low: &'a [f64],
134        close: &'a [f64],
135        params: MarketStructureConfluenceParams,
136    ) -> Self {
137        Self {
138            data: MarketStructureConfluenceData::Slices { high, low, close },
139            params,
140        }
141    }
142
143    #[inline]
144    pub fn with_default_candles(candles: &'a Candles) -> Self {
145        Self::from_candles(candles, MarketStructureConfluenceParams::default())
146    }
147}
148
149#[derive(Clone, Debug)]
150pub struct MarketStructureConfluenceBuilder {
151    swing_size: Option<usize>,
152    bos_confirmation: Option<String>,
153    basis_length: Option<usize>,
154    atr_length: Option<usize>,
155    atr_smooth: Option<usize>,
156    vol_mult: Option<f64>,
157    kernel: Kernel,
158}
159
160impl Default for MarketStructureConfluenceBuilder {
161    fn default() -> Self {
162        Self {
163            swing_size: None,
164            bos_confirmation: None,
165            basis_length: None,
166            atr_length: None,
167            atr_smooth: None,
168            vol_mult: None,
169            kernel: Kernel::Auto,
170        }
171    }
172}
173
174impl MarketStructureConfluenceBuilder {
175    #[inline(always)]
176    pub fn new() -> Self {
177        Self::default()
178    }
179
180    #[inline(always)]
181    pub fn swing_size(mut self, value: usize) -> Self {
182        self.swing_size = Some(value);
183        self
184    }
185
186    #[inline(always)]
187    pub fn bos_confirmation<S: Into<String>>(mut self, value: S) -> Self {
188        self.bos_confirmation = Some(value.into());
189        self
190    }
191
192    #[inline(always)]
193    pub fn basis_length(mut self, value: usize) -> Self {
194        self.basis_length = Some(value);
195        self
196    }
197
198    #[inline(always)]
199    pub fn atr_length(mut self, value: usize) -> Self {
200        self.atr_length = Some(value);
201        self
202    }
203
204    #[inline(always)]
205    pub fn atr_smooth(mut self, value: usize) -> Self {
206        self.atr_smooth = Some(value);
207        self
208    }
209
210    #[inline(always)]
211    pub fn vol_mult(mut self, value: f64) -> Self {
212        self.vol_mult = Some(value);
213        self
214    }
215
216    #[inline(always)]
217    pub fn kernel(mut self, value: Kernel) -> Self {
218        self.kernel = value;
219        self
220    }
221}
222
223#[derive(Debug, Error)]
224pub enum MarketStructureConfluenceError {
225    #[error("market_structure_confluence: input data slice is empty")]
226    EmptyInputData,
227    #[error("market_structure_confluence: data length mismatch: high={high}, low={low}, close={close}")]
228    DataLengthMismatch { high: usize, low: usize, close: usize },
229    #[error("market_structure_confluence: all values are NaN")]
230    AllValuesNaN,
231    #[error("market_structure_confluence: invalid swing_size: swing_size = {swing_size}, data length = {data_len}")]
232    InvalidSwingSize { swing_size: usize, data_len: usize },
233    #[error("market_structure_confluence: invalid bos_confirmation: {bos_confirmation}")]
234    InvalidBosConfirmation { bos_confirmation: String },
235    #[error("market_structure_confluence: invalid basis_length: basis_length = {basis_length}, data length = {data_len}")]
236    InvalidBasisLength { basis_length: usize, data_len: usize },
237    #[error("market_structure_confluence: invalid atr_length: atr_length = {atr_length}, data length = {data_len}")]
238    InvalidAtrLength { atr_length: usize, data_len: usize },
239    #[error("market_structure_confluence: invalid atr_smooth: atr_smooth = {atr_smooth}, data length = {data_len}")]
240    InvalidAtrSmooth { atr_smooth: usize, data_len: usize },
241    #[error("market_structure_confluence: invalid vol_mult: {vol_mult}")]
242    InvalidVolMult { vol_mult: f64 },
243    #[error("market_structure_confluence: not enough valid data: needed = {needed}, valid = {valid}")]
244    NotEnoughValidData { needed: usize, valid: usize },
245    #[error("market_structure_confluence: output length mismatch: expected {expected}, got {got}")]
246    OutputLengthMismatch { expected: usize, got: usize },
247    #[error("market_structure_confluence: invalid range: start={start}, end={end}, step={step}")]
248    InvalidRange {
249        start: String,
250        end: String,
251        step: String,
252    },
253    #[error("market_structure_confluence: invalid kernel for batch: {0:?}")]
254    InvalidKernelForBatch(Kernel),
255}
256
257#[derive(Clone, Copy, Debug)]
258struct ResolvedParams {
259    swing_size: usize,
260    bos_confirmation: MarketStructureConfluenceBosConfirmation,
261    basis_length: usize,
262    atr_length: usize,
263    atr_smooth: usize,
264    vol_mult: f64,
265}
266
267#[derive(Clone, Debug)]
268struct PreparedInput<'a> {
269    high: &'a [f64],
270    low: &'a [f64],
271    close: &'a [f64],
272    len: usize,
273    params: ResolvedParams,
274    warmup: usize,
275}
276
277#[derive(Clone, Copy, Debug)]
278struct MarketStructureConfluencePoint {
279    basis: f64,
280    upper_band: f64,
281    lower_band: f64,
282    structure_direction: f64,
283    bullish_arrow: f64,
284    bearish_arrow: f64,
285    bullish_change: f64,
286    bearish_change: f64,
287    hh: f64,
288    lh: f64,
289    hl: f64,
290    ll: f64,
291    bullish_bos: f64,
292    bullish_choch: f64,
293    bearish_bos: f64,
294    bearish_choch: f64,
295}
296
297#[derive(Clone, Copy, Debug, PartialEq)]
298pub struct MarketStructureConfluenceStreamOutput {
299    pub basis: f64,
300    pub upper_band: f64,
301    pub lower_band: f64,
302    pub structure_direction: f64,
303    pub bullish_arrow: f64,
304    pub bearish_arrow: f64,
305    pub bullish_change: f64,
306    pub bearish_change: f64,
307    pub hh: f64,
308    pub lh: f64,
309    pub hl: f64,
310    pub ll: f64,
311    pub bullish_bos: f64,
312    pub bullish_choch: f64,
313    pub bearish_bos: f64,
314    pub bearish_choch: f64,
315}
316
317#[derive(Clone, Debug)]
318struct AtrState {
319    period: usize,
320    count: usize,
321    sum: f64,
322    value: Option<f64>,
323    prev_close: Option<f64>,
324}
325
326impl AtrState {
327    #[inline(always)]
328    fn new(period: usize) -> Self {
329        Self {
330            period,
331            count: 0,
332            sum: 0.0,
333            value: None,
334            prev_close: None,
335        }
336    }
337
338    #[inline(always)]
339    fn reset(&mut self) {
340        self.count = 0;
341        self.sum = 0.0;
342        self.value = None;
343        self.prev_close = None;
344    }
345
346    #[inline(always)]
347    fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
348        let tr = if let Some(prev_close) = self.prev_close {
349            let hl = high - low;
350            let hc = (high - prev_close).abs();
351            let lc = (low - prev_close).abs();
352            hl.max(hc).max(lc)
353        } else {
354            high - low
355        };
356        self.prev_close = Some(close);
357        if let Some(prev) = self.value {
358            let next = ((prev * (self.period as f64 - 1.0)) + tr) / self.period as f64;
359            self.value = Some(next);
360            Some(next)
361        } else {
362            self.count += 1;
363            self.sum += tr;
364            if self.count == self.period {
365                let seeded = self.sum / self.period as f64;
366                self.value = Some(seeded);
367                Some(seeded)
368            } else {
369                None
370            }
371        }
372    }
373}
374
375#[derive(Clone, Debug)]
376struct RollingSma {
377    period: usize,
378    buffer: VecDeque<f64>,
379    sum: f64,
380}
381
382impl RollingSma {
383    #[inline(always)]
384    fn new(period: usize) -> Self {
385        Self {
386            period,
387            buffer: VecDeque::with_capacity(period),
388            sum: 0.0,
389        }
390    }
391
392    #[inline(always)]
393    fn reset(&mut self) {
394        self.buffer.clear();
395        self.sum = 0.0;
396    }
397
398    #[inline(always)]
399    fn update(&mut self, value: f64) -> Option<f64> {
400        if self.buffer.len() == self.period {
401            if let Some(old) = self.buffer.pop_front() {
402                self.sum -= old;
403            }
404        }
405        self.buffer.push_back(value);
406        self.sum += value;
407        if self.buffer.len() < self.period {
408            None
409        } else {
410            Some(self.sum / self.period as f64)
411        }
412    }
413}
414
415#[derive(Clone, Debug)]
416struct WmaState {
417    period: usize,
418    buffer: Vec<f64>,
419    pos: usize,
420    len: usize,
421    sum: f64,
422    weighted_sum: f64,
423    divisor: f64,
424}
425
426impl WmaState {
427    #[inline(always)]
428    fn new(period: usize) -> Self {
429        Self {
430            period,
431            buffer: vec![0.0; period],
432            pos: 0,
433            len: 0,
434            sum: 0.0,
435            weighted_sum: 0.0,
436            divisor: (period as f64) * (period as f64 + 1.0) * 0.5,
437        }
438    }
439
440    #[inline(always)]
441    fn reset(&mut self) {
442        self.buffer.fill(0.0);
443        self.pos = 0;
444        self.len = 0;
445        self.sum = 0.0;
446        self.weighted_sum = 0.0;
447    }
448
449    #[inline(always)]
450    fn update(&mut self, value: f64) -> Option<f64> {
451        if self.len < self.period {
452            self.buffer[self.pos] = value;
453            self.pos = (self.pos + 1) % self.period;
454            self.len += 1;
455            self.sum += value;
456            self.weighted_sum += self.len as f64 * value;
457            if self.len == self.period {
458                Some(self.weighted_sum / self.divisor)
459            } else {
460                None
461            }
462        } else {
463            let old = self.buffer[self.pos];
464            let old_sum = self.sum;
465            self.buffer[self.pos] = value;
466            self.pos = (self.pos + 1) % self.period;
467            self.weighted_sum = self.weighted_sum - old_sum + self.period as f64 * value;
468            self.sum = old_sum - old + value;
469            Some(self.weighted_sum / self.divisor)
470        }
471    }
472}
473
474#[derive(Clone, Debug)]
475struct PivotDetector {
476    period: usize,
477    values: VecDeque<(f64, usize)>,
478    is_high: bool,
479}
480
481impl PivotDetector {
482    #[inline(always)]
483    fn new(period: usize, is_high: bool) -> Self {
484        Self {
485            period,
486            values: VecDeque::with_capacity(period * 2 + 1),
487            is_high,
488        }
489    }
490
491    #[inline(always)]
492    fn reset(&mut self) {
493        self.values.clear();
494    }
495
496    #[inline(always)]
497    fn update(&mut self, value: f64, index: usize) -> Option<(f64, usize)> {
498        self.values.push_back((value, index));
499        let needed = self.period * 2 + 1;
500        if self.values.len() < needed {
501            return None;
502        }
503        let (center_value, center_index) = self.values[self.period];
504        let mut ok = center_value.is_finite();
505        if ok {
506            for (i, (other, _)) in self.values.iter().enumerate() {
507                if i == self.period {
508                    continue;
509                }
510                if !other.is_finite() {
511                    ok = false;
512                    break;
513                }
514                if self.is_high {
515                    if *other > center_value {
516                        ok = false;
517                        break;
518                    }
519                } else if *other < center_value {
520                    ok = false;
521                    break;
522                }
523            }
524        }
525        self.values.pop_front();
526        if ok {
527            Some((center_value, center_index))
528        } else {
529            None
530        }
531    }
532}
533
534#[derive(Clone, Debug)]
535struct MarketStructureConfluenceCore {
536    params: ResolvedParams,
537    basis_state: WmaState,
538    atr_state: AtrState,
539    svol_state: RollingSma,
540    piv_high: PivotDetector,
541    piv_low: PivotDetector,
542    index: usize,
543    prev_high: Option<f64>,
544    prev_low: Option<f64>,
545    prev_high_idx: Option<usize>,
546    prev_low_idx: Option<usize>,
547    high_active: bool,
548    low_active: bool,
549    prev_break_dir: i32,
550}
551
552impl MarketStructureConfluenceCore {
553    #[inline(always)]
554    fn new(params: ResolvedParams) -> Self {
555        Self {
556            basis_state: WmaState::new(params.basis_length),
557            atr_state: AtrState::new(params.atr_length),
558            svol_state: RollingSma::new(params.atr_smooth),
559            piv_high: PivotDetector::new(params.swing_size, true),
560            piv_low: PivotDetector::new(params.swing_size, false),
561            params,
562            index: 0,
563            prev_high: None,
564            prev_low: None,
565            prev_high_idx: None,
566            prev_low_idx: None,
567            high_active: false,
568            low_active: false,
569            prev_break_dir: 0,
570        }
571    }
572
573    #[inline(always)]
574    fn reset(&mut self) {
575        self.basis_state.reset();
576        self.atr_state.reset();
577        self.svol_state.reset();
578        self.piv_high.reset();
579        self.piv_low.reset();
580        self.index = 0;
581        self.prev_high = None;
582        self.prev_low = None;
583        self.prev_high_idx = None;
584        self.prev_low_idx = None;
585        self.high_active = false;
586        self.low_active = false;
587        self.prev_break_dir = 0;
588    }
589
590    #[inline(always)]
591    fn update(
592        &mut self,
593        high: f64,
594        low: f64,
595        close: f64,
596    ) -> Option<MarketStructureConfluencePoint> {
597        let basis = self.basis_state.update(close);
598        let svol = self
599            .atr_state
600            .update(high, low, close)
601            .and_then(|atr| self.svol_state.update(atr));
602
603        let mut hh = 0.0;
604        let mut lh = 0.0;
605        let mut hl = 0.0;
606        let mut ll = 0.0;
607
608        if let Some((pivot_high, pivot_idx)) = self.piv_high.update(high, self.index) {
609            let is_hh = self.prev_high.map(|value| pivot_high >= value).unwrap_or(true);
610            if is_hh {
611                hh = 1.0;
612            } else {
613                lh = 1.0;
614            }
615            self.prev_high = Some(pivot_high);
616            self.prev_high_idx = Some(pivot_idx);
617            self.high_active = true;
618        }
619
620        if let Some((pivot_low, pivot_idx)) = self.piv_low.update(low, self.index) {
621            let is_hl = self.prev_low.map(|value| pivot_low >= value).unwrap_or(true);
622            if is_hl {
623                hl = 1.0;
624            } else {
625                ll = 1.0;
626            }
627            self.prev_low = Some(pivot_low);
628            self.prev_low_idx = Some(pivot_idx);
629            self.low_active = true;
630        }
631
632        let high_src = match self.params.bos_confirmation {
633            MarketStructureConfluenceBosConfirmation::CandleClose => close,
634            MarketStructureConfluenceBosConfirmation::Wicks => high,
635        };
636        let low_src = match self.params.bos_confirmation {
637            MarketStructureConfluenceBosConfirmation::CandleClose => close,
638            MarketStructureConfluenceBosConfirmation::Wicks => low,
639        };
640
641        let mut high_broken = false;
642        let mut low_broken = false;
643        if self.high_active {
644            if let Some(prev_high) = self.prev_high {
645                if high_src > prev_high {
646                    high_broken = true;
647                    self.high_active = false;
648                }
649            }
650        }
651        if self.low_active {
652            if let Some(prev_low) = self.prev_low {
653                if low_src < prev_low {
654                    low_broken = true;
655                    self.low_active = false;
656                }
657            }
658        }
659
660        let mut bullish_change = 0.0;
661        let mut bearish_change = 0.0;
662        let mut bullish_bos = 0.0;
663        let mut bullish_choch = 0.0;
664        let mut bearish_bos = 0.0;
665        let mut bearish_choch = 0.0;
666
667        if high_broken {
668            let last_break_dir = self.prev_break_dir;
669            if last_break_dir == -1 {
670                bullish_choch = 1.0;
671            } else {
672                bullish_bos = 1.0;
673            }
674            if last_break_dir == -1 || last_break_dir == 0 {
675                bullish_change = 1.0;
676            }
677            self.prev_break_dir = 1;
678        }
679
680        if low_broken {
681            let last_break_dir = self.prev_break_dir;
682            if last_break_dir == 1 {
683                bearish_choch = 1.0;
684            } else {
685                bearish_bos = 1.0;
686            }
687            if last_break_dir == 1 || last_break_dir == 0 {
688                bearish_change = 1.0;
689            }
690            self.prev_break_dir = -1;
691        }
692
693        self.index += 1;
694
695        let (basis, svol) = match (basis, svol) {
696            (Some(basis), Some(svol)) => (basis, svol),
697            _ => return None,
698        };
699
700        let upper_band = basis + self.params.vol_mult * svol;
701        let lower_band = basis - self.params.vol_mult * svol;
702        let structure_direction = self.prev_break_dir as f64;
703        let bullish_arrow = if self.prev_break_dir == 1 && low < lower_band && high > lower_band {
704            1.0
705        } else {
706            0.0
707        };
708        let bearish_arrow = if self.prev_break_dir == -1 && low < upper_band && high > upper_band {
709            1.0
710        } else {
711            0.0
712        };
713
714        Some(MarketStructureConfluencePoint {
715            basis,
716            upper_band,
717            lower_band,
718            structure_direction,
719            bullish_arrow,
720            bearish_arrow,
721            bullish_change,
722            bearish_change,
723            hh,
724            lh,
725            hl,
726            ll,
727            bullish_bos,
728            bullish_choch,
729            bearish_bos,
730            bearish_choch,
731        })
732    }
733}
734
735#[derive(Clone, Debug)]
736pub struct MarketStructureConfluenceStream {
737    core: MarketStructureConfluenceCore,
738}
739
740impl MarketStructureConfluenceStream {
741    #[inline]
742    pub fn try_new(
743        params: MarketStructureConfluenceParams,
744    ) -> Result<Self, MarketStructureConfluenceError> {
745        let resolved = resolve_params(params, usize::MAX)?;
746        Ok(Self {
747            core: MarketStructureConfluenceCore::new(resolved),
748        })
749    }
750
751    #[inline(always)]
752    pub fn update(
753        &mut self,
754        high: f64,
755        low: f64,
756        close: f64,
757    ) -> Option<MarketStructureConfluenceStreamOutput> {
758        self.core
759            .update(high, low, close)
760            .map(|point| MarketStructureConfluenceStreamOutput {
761                basis: point.basis,
762                upper_band: point.upper_band,
763                lower_band: point.lower_band,
764                structure_direction: point.structure_direction,
765                bullish_arrow: point.bullish_arrow,
766                bearish_arrow: point.bearish_arrow,
767                bullish_change: point.bullish_change,
768                bearish_change: point.bearish_change,
769                hh: point.hh,
770                lh: point.lh,
771                hl: point.hl,
772                ll: point.ll,
773                bullish_bos: point.bullish_bos,
774                bullish_choch: point.bullish_choch,
775                bearish_bos: point.bearish_bos,
776                bearish_choch: point.bearish_choch,
777            })
778    }
779}
780
781#[inline]
782pub fn market_structure_confluence(
783    input: &MarketStructureConfluenceInput<'_>,
784) -> Result<MarketStructureConfluenceOutput, MarketStructureConfluenceError> {
785    market_structure_confluence_with_kernel(input, Kernel::Auto)
786}
787
788#[inline]
789pub fn market_structure_confluence_with_kernel(
790    input: &MarketStructureConfluenceInput<'_>,
791    kernel: Kernel,
792) -> Result<MarketStructureConfluenceOutput, MarketStructureConfluenceError> {
793    let prepared = prepare_input(input, kernel)?;
794    let mut basis = alloc_with_nan_prefix(prepared.len, prepared.warmup);
795    let mut upper_band = alloc_with_nan_prefix(prepared.len, prepared.warmup);
796    let mut lower_band = alloc_with_nan_prefix(prepared.len, prepared.warmup);
797    let mut structure_direction = alloc_with_nan_prefix(prepared.len, prepared.warmup);
798    let mut bullish_arrow = alloc_with_nan_prefix(prepared.len, prepared.warmup);
799    let mut bearish_arrow = alloc_with_nan_prefix(prepared.len, prepared.warmup);
800    let mut bullish_change = alloc_with_nan_prefix(prepared.len, prepared.warmup);
801    let mut bearish_change = alloc_with_nan_prefix(prepared.len, prepared.warmup);
802    let mut hh = alloc_with_nan_prefix(prepared.len, prepared.warmup);
803    let mut lh = alloc_with_nan_prefix(prepared.len, prepared.warmup);
804    let mut hl = alloc_with_nan_prefix(prepared.len, prepared.warmup);
805    let mut ll = alloc_with_nan_prefix(prepared.len, prepared.warmup);
806    let mut bullish_bos = alloc_with_nan_prefix(prepared.len, prepared.warmup);
807    let mut bullish_choch = alloc_with_nan_prefix(prepared.len, prepared.warmup);
808    let mut bearish_bos = alloc_with_nan_prefix(prepared.len, prepared.warmup);
809    let mut bearish_choch = alloc_with_nan_prefix(prepared.len, prepared.warmup);
810
811    market_structure_confluence_into_slices(
812        input,
813        kernel,
814        &mut basis,
815        &mut upper_band,
816        &mut lower_band,
817        &mut structure_direction,
818        &mut bullish_arrow,
819        &mut bearish_arrow,
820        &mut bullish_change,
821        &mut bearish_change,
822        &mut hh,
823        &mut lh,
824        &mut hl,
825        &mut ll,
826        &mut bullish_bos,
827        &mut bullish_choch,
828        &mut bearish_bos,
829        &mut bearish_choch,
830    )?;
831
832    Ok(MarketStructureConfluenceOutput {
833        basis,
834        upper_band,
835        lower_band,
836        structure_direction,
837        bullish_arrow,
838        bearish_arrow,
839        bullish_change,
840        bearish_change,
841        hh,
842        lh,
843        hl,
844        ll,
845        bullish_bos,
846        bullish_choch,
847        bearish_bos,
848        bearish_choch,
849    })
850}
851
852#[allow(clippy::too_many_arguments)]
853#[inline]
854pub fn market_structure_confluence_into(
855    input: &MarketStructureConfluenceInput<'_>,
856    basis: &mut [f64],
857    upper_band: &mut [f64],
858    lower_band: &mut [f64],
859    structure_direction: &mut [f64],
860    bullish_arrow: &mut [f64],
861    bearish_arrow: &mut [f64],
862    bullish_change: &mut [f64],
863    bearish_change: &mut [f64],
864    hh: &mut [f64],
865    lh: &mut [f64],
866    hl: &mut [f64],
867    ll: &mut [f64],
868    bullish_bos: &mut [f64],
869    bullish_choch: &mut [f64],
870    bearish_bos: &mut [f64],
871    bearish_choch: &mut [f64],
872) -> Result<(), MarketStructureConfluenceError> {
873    market_structure_confluence_into_slices(
874        input,
875        Kernel::Auto,
876        basis,
877        upper_band,
878        lower_band,
879        structure_direction,
880        bullish_arrow,
881        bearish_arrow,
882        bullish_change,
883        bearish_change,
884        hh,
885        lh,
886        hl,
887        ll,
888        bullish_bos,
889        bullish_choch,
890        bearish_bos,
891        bearish_choch,
892    )
893}
894
895#[allow(clippy::too_many_arguments)]
896#[inline]
897pub fn market_structure_confluence_into_slices(
898    input: &MarketStructureConfluenceInput<'_>,
899    kernel: Kernel,
900    basis: &mut [f64],
901    upper_band: &mut [f64],
902    lower_band: &mut [f64],
903    structure_direction: &mut [f64],
904    bullish_arrow: &mut [f64],
905    bearish_arrow: &mut [f64],
906    bullish_change: &mut [f64],
907    bearish_change: &mut [f64],
908    hh: &mut [f64],
909    lh: &mut [f64],
910    hl: &mut [f64],
911    ll: &mut [f64],
912    bullish_bos: &mut [f64],
913    bullish_choch: &mut [f64],
914    bearish_bos: &mut [f64],
915    bearish_choch: &mut [f64],
916) -> Result<(), MarketStructureConfluenceError> {
917    let prepared = prepare_input(input, kernel)?;
918    let got = *[
919        basis.len(),
920        upper_band.len(),
921        lower_band.len(),
922        structure_direction.len(),
923        bullish_arrow.len(),
924        bearish_arrow.len(),
925        bullish_change.len(),
926        bearish_change.len(),
927        hh.len(),
928        lh.len(),
929        hl.len(),
930        ll.len(),
931        bullish_bos.len(),
932        bullish_choch.len(),
933        bearish_bos.len(),
934        bearish_choch.len(),
935    ]
936    .iter()
937    .min()
938    .unwrap_or(&0);
939    if basis.len() != prepared.len
940        || upper_band.len() != prepared.len
941        || lower_band.len() != prepared.len
942        || structure_direction.len() != prepared.len
943        || bullish_arrow.len() != prepared.len
944        || bearish_arrow.len() != prepared.len
945        || bullish_change.len() != prepared.len
946        || bearish_change.len() != prepared.len
947        || hh.len() != prepared.len
948        || lh.len() != prepared.len
949        || hl.len() != prepared.len
950        || ll.len() != prepared.len
951        || bullish_bos.len() != prepared.len
952        || bullish_choch.len() != prepared.len
953        || bearish_bos.len() != prepared.len
954        || bearish_choch.len() != prepared.len
955    {
956        return Err(MarketStructureConfluenceError::OutputLengthMismatch {
957            expected: prepared.len,
958            got,
959        });
960    }
961
962    compute_into_slices(
963        &prepared,
964        basis,
965        upper_band,
966        lower_band,
967        structure_direction,
968        bullish_arrow,
969        bearish_arrow,
970        bullish_change,
971        bearish_change,
972        hh,
973        lh,
974        hl,
975        ll,
976        bullish_bos,
977        bullish_choch,
978        bearish_bos,
979        bearish_choch,
980    )
981}
982
983#[inline]
984fn resolve_data<'a>(
985    input: &'a MarketStructureConfluenceInput<'a>,
986) -> Result<(&'a [f64], &'a [f64], &'a [f64]), MarketStructureConfluenceError> {
987    match &input.data {
988        MarketStructureConfluenceData::Candles { candles } => Ok((
989            candles.high.as_slice(),
990            candles.low.as_slice(),
991            candles.close.as_slice(),
992        )),
993        MarketStructureConfluenceData::Slices { high, low, close } => {
994            if high.len() != low.len() || high.len() != close.len() {
995                return Err(MarketStructureConfluenceError::DataLengthMismatch {
996                    high: high.len(),
997                    low: low.len(),
998                    close: close.len(),
999                });
1000            }
1001            Ok((high, low, close))
1002        }
1003    }
1004}
1005
1006#[inline]
1007fn resolve_params(
1008    params: MarketStructureConfluenceParams,
1009    data_len: usize,
1010) -> Result<ResolvedParams, MarketStructureConfluenceError> {
1011    let swing_size = params.swing_size.unwrap_or(DEFAULT_SWING_SIZE);
1012    let bos_confirmation_raw = params
1013        .bos_confirmation
1014        .unwrap_or_else(|| DEFAULT_BOS_CONFIRMATION.to_string());
1015    let bos_confirmation = MarketStructureConfluenceBosConfirmation::parse(&bos_confirmation_raw)
1016        .ok_or(MarketStructureConfluenceError::InvalidBosConfirmation {
1017            bos_confirmation: bos_confirmation_raw.clone(),
1018        })?;
1019    let basis_length = params.basis_length.unwrap_or(DEFAULT_BASIS_LENGTH);
1020    let atr_length = params.atr_length.unwrap_or(DEFAULT_ATR_LENGTH);
1021    let atr_smooth = params.atr_smooth.unwrap_or(DEFAULT_ATR_SMOOTH);
1022    let vol_mult = params.vol_mult.unwrap_or(DEFAULT_VOL_MULT);
1023
1024    if swing_size < 2 || (data_len != usize::MAX && swing_size * 2 + 1 > data_len) {
1025        return Err(MarketStructureConfluenceError::InvalidSwingSize {
1026            swing_size,
1027            data_len,
1028        });
1029    }
1030    if basis_length == 0 || (data_len != usize::MAX && basis_length > data_len) {
1031        return Err(MarketStructureConfluenceError::InvalidBasisLength {
1032            basis_length,
1033            data_len,
1034        });
1035    }
1036    if atr_length == 0 || (data_len != usize::MAX && atr_length > data_len) {
1037        return Err(MarketStructureConfluenceError::InvalidAtrLength {
1038            atr_length,
1039            data_len,
1040        });
1041    }
1042    if atr_smooth == 0 || (data_len != usize::MAX && atr_smooth > data_len) {
1043        return Err(MarketStructureConfluenceError::InvalidAtrSmooth {
1044            atr_smooth,
1045            data_len,
1046        });
1047    }
1048    if !vol_mult.is_finite() || vol_mult < 0.0 {
1049        return Err(MarketStructureConfluenceError::InvalidVolMult { vol_mult });
1050    }
1051
1052    Ok(ResolvedParams {
1053        swing_size,
1054        bos_confirmation,
1055        basis_length,
1056        atr_length,
1057        atr_smooth,
1058        vol_mult,
1059    })
1060}
1061
1062#[inline]
1063fn prepare_input<'a>(
1064    input: &'a MarketStructureConfluenceInput<'a>,
1065    kernel: Kernel,
1066) -> Result<PreparedInput<'a>, MarketStructureConfluenceError> {
1067    let (high, low, close) = resolve_data(input)?;
1068    let len = close.len();
1069    if len == 0 {
1070        return Err(MarketStructureConfluenceError::EmptyInputData);
1071    }
1072    let first = (0..len)
1073        .find(|&i| high[i].is_finite() && low[i].is_finite() && close[i].is_finite())
1074        .ok_or(MarketStructureConfluenceError::AllValuesNaN)?;
1075    let params = resolve_params(input.params.clone(), len)?;
1076    let valid = (first..len)
1077        .filter(|&i| high[i].is_finite() && low[i].is_finite() && close[i].is_finite())
1078        .count();
1079    let needed = (params.swing_size * 2 + 1)
1080        .max(params.basis_length)
1081        .max(params.atr_length + params.atr_smooth - 1);
1082    if valid < needed {
1083        return Err(MarketStructureConfluenceError::NotEnoughValidData { needed, valid });
1084    }
1085    let _chosen = match kernel {
1086        Kernel::Auto => detect_best_kernel(),
1087        value => value,
1088    };
1089    Ok(PreparedInput {
1090        high,
1091        low,
1092        close,
1093        len,
1094        params,
1095        warmup: first
1096            + (params.swing_size * 2)
1097                .max(params.basis_length.saturating_sub(1))
1098                .max(params.atr_length + params.atr_smooth - 2),
1099    })
1100}
1101
1102#[allow(clippy::too_many_arguments)]
1103#[inline(always)]
1104fn compute_into_slices(
1105    prepared: &PreparedInput<'_>,
1106    dst_basis: &mut [f64],
1107    dst_upper_band: &mut [f64],
1108    dst_lower_band: &mut [f64],
1109    dst_structure_direction: &mut [f64],
1110    dst_bullish_arrow: &mut [f64],
1111    dst_bearish_arrow: &mut [f64],
1112    dst_bullish_change: &mut [f64],
1113    dst_bearish_change: &mut [f64],
1114    dst_hh: &mut [f64],
1115    dst_lh: &mut [f64],
1116    dst_hl: &mut [f64],
1117    dst_ll: &mut [f64],
1118    dst_bullish_bos: &mut [f64],
1119    dst_bullish_choch: &mut [f64],
1120    dst_bearish_bos: &mut [f64],
1121    dst_bearish_choch: &mut [f64],
1122) -> Result<(), MarketStructureConfluenceError> {
1123    dst_basis.fill(f64::NAN);
1124    dst_upper_band.fill(f64::NAN);
1125    dst_lower_band.fill(f64::NAN);
1126    dst_structure_direction.fill(f64::NAN);
1127    dst_bullish_arrow.fill(f64::NAN);
1128    dst_bearish_arrow.fill(f64::NAN);
1129    dst_bullish_change.fill(f64::NAN);
1130    dst_bearish_change.fill(f64::NAN);
1131    dst_hh.fill(f64::NAN);
1132    dst_lh.fill(f64::NAN);
1133    dst_hl.fill(f64::NAN);
1134    dst_ll.fill(f64::NAN);
1135    dst_bullish_bos.fill(f64::NAN);
1136    dst_bullish_choch.fill(f64::NAN);
1137    dst_bearish_bos.fill(f64::NAN);
1138    dst_bearish_choch.fill(f64::NAN);
1139
1140    let mut core = MarketStructureConfluenceCore::new(prepared.params);
1141    core.reset();
1142    for i in 0..prepared.len {
1143        let Some(point) = core.update(prepared.high[i], prepared.low[i], prepared.close[i]) else {
1144            continue;
1145        };
1146        dst_basis[i] = point.basis;
1147        dst_upper_band[i] = point.upper_band;
1148        dst_lower_band[i] = point.lower_band;
1149        dst_structure_direction[i] = point.structure_direction;
1150        dst_bullish_arrow[i] = point.bullish_arrow;
1151        dst_bearish_arrow[i] = point.bearish_arrow;
1152        dst_bullish_change[i] = point.bullish_change;
1153        dst_bearish_change[i] = point.bearish_change;
1154        dst_hh[i] = point.hh;
1155        dst_lh[i] = point.lh;
1156        dst_hl[i] = point.hl;
1157        dst_ll[i] = point.ll;
1158        dst_bullish_bos[i] = point.bullish_bos;
1159        dst_bullish_choch[i] = point.bullish_choch;
1160        dst_bearish_bos[i] = point.bearish_bos;
1161        dst_bearish_choch[i] = point.bearish_choch;
1162    }
1163    Ok(())
1164}
1165
1166#[derive(Clone, Debug)]
1167pub struct MarketStructureConfluenceBatchRange {
1168    pub swing_size: (usize, usize, usize),
1169    pub bos_confirmation: Vec<String>,
1170    pub basis_length: (usize, usize, usize),
1171    pub atr_length: (usize, usize, usize),
1172    pub atr_smooth: (usize, usize, usize),
1173    pub vol_mult: (f64, f64, f64),
1174}
1175
1176impl Default for MarketStructureConfluenceBatchRange {
1177    fn default() -> Self {
1178        Self {
1179            swing_size: (DEFAULT_SWING_SIZE, DEFAULT_SWING_SIZE, 0),
1180            bos_confirmation: vec![DEFAULT_BOS_CONFIRMATION.to_string()],
1181            basis_length: (DEFAULT_BASIS_LENGTH, DEFAULT_BASIS_LENGTH, 0),
1182            atr_length: (DEFAULT_ATR_LENGTH, DEFAULT_ATR_LENGTH, 0),
1183            atr_smooth: (DEFAULT_ATR_SMOOTH, DEFAULT_ATR_SMOOTH, 0),
1184            vol_mult: (DEFAULT_VOL_MULT, DEFAULT_VOL_MULT, 0.0),
1185        }
1186    }
1187}
1188
1189#[derive(Clone, Debug)]
1190pub struct MarketStructureConfluenceBatchOutput {
1191    pub basis: Vec<f64>,
1192    pub upper_band: Vec<f64>,
1193    pub lower_band: Vec<f64>,
1194    pub structure_direction: Vec<f64>,
1195    pub bullish_arrow: Vec<f64>,
1196    pub bearish_arrow: Vec<f64>,
1197    pub bullish_change: Vec<f64>,
1198    pub bearish_change: Vec<f64>,
1199    pub hh: Vec<f64>,
1200    pub lh: Vec<f64>,
1201    pub hl: Vec<f64>,
1202    pub ll: Vec<f64>,
1203    pub bullish_bos: Vec<f64>,
1204    pub bullish_choch: Vec<f64>,
1205    pub bearish_bos: Vec<f64>,
1206    pub bearish_choch: Vec<f64>,
1207    pub combos: Vec<MarketStructureConfluenceParams>,
1208    pub rows: usize,
1209    pub cols: usize,
1210}
1211
1212#[derive(Clone, Debug)]
1213pub struct MarketStructureConfluenceBatchBuilder {
1214    range: MarketStructureConfluenceBatchRange,
1215    kernel: Kernel,
1216}
1217
1218impl Default for MarketStructureConfluenceBatchBuilder {
1219    fn default() -> Self {
1220        Self {
1221            range: MarketStructureConfluenceBatchRange::default(),
1222            kernel: Kernel::Auto,
1223        }
1224    }
1225}
1226
1227impl MarketStructureConfluenceBatchBuilder {
1228    #[inline(always)]
1229    pub fn new() -> Self {
1230        Self::default()
1231    }
1232
1233    #[inline(always)]
1234    pub fn range(mut self, value: MarketStructureConfluenceBatchRange) -> Self {
1235        self.range = value;
1236        self
1237    }
1238
1239    #[inline(always)]
1240    pub fn kernel(mut self, value: Kernel) -> Self {
1241        self.kernel = value;
1242        self
1243    }
1244
1245    #[inline(always)]
1246    pub fn apply(
1247        self,
1248        candles: &Candles,
1249    ) -> Result<MarketStructureConfluenceBatchOutput, MarketStructureConfluenceError> {
1250        self.apply_slices(
1251            candles.high.as_slice(),
1252            candles.low.as_slice(),
1253            candles.close.as_slice(),
1254        )
1255    }
1256
1257    #[inline(always)]
1258    pub fn apply_slices(
1259        self,
1260        high: &[f64],
1261        low: &[f64],
1262        close: &[f64],
1263    ) -> Result<MarketStructureConfluenceBatchOutput, MarketStructureConfluenceError> {
1264        market_structure_confluence_batch_with_kernel(high, low, close, &self.range, self.kernel)
1265    }
1266}
1267
1268fn axis_usize(
1269    (start, end, step): (usize, usize, usize),
1270) -> Result<Vec<usize>, MarketStructureConfluenceError> {
1271    if step == 0 || start == end {
1272        return Ok(vec![start]);
1273    }
1274    let mut out = Vec::new();
1275    if start <= end {
1276        let mut current = start;
1277        while current <= end {
1278            out.push(current);
1279            match current.checked_add(step) {
1280                Some(next) => current = next,
1281                None => break,
1282            }
1283        }
1284    } else {
1285        let mut current = start;
1286        while current >= end {
1287            out.push(current);
1288            match current.checked_sub(step) {
1289                Some(next) => current = next,
1290                None => break,
1291            }
1292            if current < end {
1293                break;
1294            }
1295        }
1296    }
1297    if out.is_empty() {
1298        return Err(MarketStructureConfluenceError::InvalidRange {
1299            start: start.to_string(),
1300            end: end.to_string(),
1301            step: step.to_string(),
1302        });
1303    }
1304    Ok(out)
1305}
1306
1307fn axis_f64(
1308    (start, end, step): (f64, f64, f64),
1309) -> Result<Vec<f64>, MarketStructureConfluenceError> {
1310    if !start.is_finite() || !end.is_finite() || !step.is_finite() {
1311        return Err(MarketStructureConfluenceError::InvalidRange {
1312            start: start.to_string(),
1313            end: end.to_string(),
1314            step: step.to_string(),
1315        });
1316    }
1317    if step.abs() < EPS || (start - end).abs() < EPS {
1318        return Ok(vec![start]);
1319    }
1320    let dir = if end >= start { 1.0 } else { -1.0 };
1321    let step_eff = dir * step.abs();
1322    let mut current = start;
1323    let mut out = Vec::new();
1324    if dir > 0.0 {
1325        while current <= end + EPS {
1326            out.push(current);
1327            current += step_eff;
1328        }
1329    } else {
1330        while current >= end - EPS {
1331            out.push(current);
1332            current += step_eff;
1333        }
1334    }
1335    if out.is_empty() {
1336        return Err(MarketStructureConfluenceError::InvalidRange {
1337            start: start.to_string(),
1338            end: end.to_string(),
1339            step: step.to_string(),
1340        });
1341    }
1342    Ok(out)
1343}
1344
1345fn expand_grid(
1346    range: &MarketStructureConfluenceBatchRange,
1347) -> Result<Vec<MarketStructureConfluenceParams>, MarketStructureConfluenceError> {
1348    let swing_sizes = axis_usize(range.swing_size)?;
1349    let bos_confirmations = if range.bos_confirmation.is_empty() {
1350        vec![DEFAULT_BOS_CONFIRMATION.to_string()]
1351    } else {
1352        range.bos_confirmation.clone()
1353    };
1354    let basis_lengths = axis_usize(range.basis_length)?;
1355    let atr_lengths = axis_usize(range.atr_length)?;
1356    let atr_smooths = axis_usize(range.atr_smooth)?;
1357    let vol_mults = axis_f64(range.vol_mult)?;
1358
1359    let total = swing_sizes
1360        .len()
1361        .checked_mul(bos_confirmations.len())
1362        .and_then(|n| n.checked_mul(basis_lengths.len()))
1363        .and_then(|n| n.checked_mul(atr_lengths.len()))
1364        .and_then(|n| n.checked_mul(atr_smooths.len()))
1365        .and_then(|n| n.checked_mul(vol_mults.len()))
1366        .ok_or_else(|| MarketStructureConfluenceError::InvalidRange {
1367            start: range.swing_size.0.to_string(),
1368            end: range.swing_size.1.to_string(),
1369            step: range.swing_size.2.to_string(),
1370        })?;
1371
1372    let mut out = Vec::with_capacity(total);
1373    for &swing_size in &swing_sizes {
1374        for bos_confirmation in &bos_confirmations {
1375            for &basis_length in &basis_lengths {
1376                for &atr_length in &atr_lengths {
1377                    for &atr_smooth in &atr_smooths {
1378                        for &vol_mult in &vol_mults {
1379                            out.push(MarketStructureConfluenceParams {
1380                                swing_size: Some(swing_size),
1381                                bos_confirmation: Some(bos_confirmation.clone()),
1382                                basis_length: Some(basis_length),
1383                                atr_length: Some(atr_length),
1384                                atr_smooth: Some(atr_smooth),
1385                                vol_mult: Some(vol_mult),
1386                            });
1387                        }
1388                    }
1389                }
1390            }
1391        }
1392    }
1393    Ok(out)
1394}
1395
1396#[allow(clippy::too_many_arguments)]
1397#[inline]
1398pub fn market_structure_confluence_batch_with_kernel(
1399    high: &[f64],
1400    low: &[f64],
1401    close: &[f64],
1402    range: &MarketStructureConfluenceBatchRange,
1403    kernel: Kernel,
1404) -> Result<MarketStructureConfluenceBatchOutput, MarketStructureConfluenceError> {
1405    if high.is_empty() || low.is_empty() || close.is_empty() {
1406        return Err(MarketStructureConfluenceError::EmptyInputData);
1407    }
1408    if high.len() != low.len() || high.len() != close.len() {
1409        return Err(MarketStructureConfluenceError::DataLengthMismatch {
1410            high: high.len(),
1411            low: low.len(),
1412            close: close.len(),
1413        });
1414    }
1415
1416    let batch_kernel = match kernel {
1417        Kernel::Auto => detect_best_batch_kernel(),
1418        value if value.is_batch() => value,
1419        _ => return Err(MarketStructureConfluenceError::InvalidKernelForBatch(kernel)),
1420    };
1421    let single_kernel = batch_kernel.to_non_batch();
1422    let combos = expand_grid(range)?;
1423    let rows = combos.len();
1424    let cols = close.len();
1425    let first = (0..cols)
1426        .find(|&i| high[i].is_finite() && low[i].is_finite() && close[i].is_finite())
1427        .ok_or(MarketStructureConfluenceError::AllValuesNaN)?;
1428    let warmups: Vec<usize> = combos
1429        .iter()
1430        .map(|combo| {
1431            let swing_size = combo.swing_size.unwrap_or(DEFAULT_SWING_SIZE);
1432            let basis_length = combo.basis_length.unwrap_or(DEFAULT_BASIS_LENGTH);
1433            let atr_length = combo.atr_length.unwrap_or(DEFAULT_ATR_LENGTH);
1434            let atr_smooth = combo.atr_smooth.unwrap_or(DEFAULT_ATR_SMOOTH);
1435            first
1436                + (swing_size * 2)
1437                    .max(basis_length.saturating_sub(1))
1438                    .max(atr_length + atr_smooth - 2)
1439        })
1440        .collect();
1441
1442    let mut basis_mu = make_uninit_matrix(rows, cols);
1443    let mut upper_band_mu = make_uninit_matrix(rows, cols);
1444    let mut lower_band_mu = make_uninit_matrix(rows, cols);
1445    let mut structure_direction_mu = make_uninit_matrix(rows, cols);
1446    let mut bullish_arrow_mu = make_uninit_matrix(rows, cols);
1447    let mut bearish_arrow_mu = make_uninit_matrix(rows, cols);
1448    let mut bullish_change_mu = make_uninit_matrix(rows, cols);
1449    let mut bearish_change_mu = make_uninit_matrix(rows, cols);
1450    let mut hh_mu = make_uninit_matrix(rows, cols);
1451    let mut lh_mu = make_uninit_matrix(rows, cols);
1452    let mut hl_mu = make_uninit_matrix(rows, cols);
1453    let mut ll_mu = make_uninit_matrix(rows, cols);
1454    let mut bullish_bos_mu = make_uninit_matrix(rows, cols);
1455    let mut bullish_choch_mu = make_uninit_matrix(rows, cols);
1456    let mut bearish_bos_mu = make_uninit_matrix(rows, cols);
1457    let mut bearish_choch_mu = make_uninit_matrix(rows, cols);
1458
1459    init_matrix_prefixes(&mut basis_mu, cols, &warmups);
1460    init_matrix_prefixes(&mut upper_band_mu, cols, &warmups);
1461    init_matrix_prefixes(&mut lower_band_mu, cols, &warmups);
1462    init_matrix_prefixes(&mut structure_direction_mu, cols, &warmups);
1463    init_matrix_prefixes(&mut bullish_arrow_mu, cols, &warmups);
1464    init_matrix_prefixes(&mut bearish_arrow_mu, cols, &warmups);
1465    init_matrix_prefixes(&mut bullish_change_mu, cols, &warmups);
1466    init_matrix_prefixes(&mut bearish_change_mu, cols, &warmups);
1467    init_matrix_prefixes(&mut hh_mu, cols, &warmups);
1468    init_matrix_prefixes(&mut lh_mu, cols, &warmups);
1469    init_matrix_prefixes(&mut hl_mu, cols, &warmups);
1470    init_matrix_prefixes(&mut ll_mu, cols, &warmups);
1471    init_matrix_prefixes(&mut bullish_bos_mu, cols, &warmups);
1472    init_matrix_prefixes(&mut bullish_choch_mu, cols, &warmups);
1473    init_matrix_prefixes(&mut bearish_bos_mu, cols, &warmups);
1474    init_matrix_prefixes(&mut bearish_choch_mu, cols, &warmups);
1475
1476    let mut basis_guard = ManuallyDrop::new(basis_mu);
1477    let mut upper_band_guard = ManuallyDrop::new(upper_band_mu);
1478    let mut lower_band_guard = ManuallyDrop::new(lower_band_mu);
1479    let mut structure_direction_guard = ManuallyDrop::new(structure_direction_mu);
1480    let mut bullish_arrow_guard = ManuallyDrop::new(bullish_arrow_mu);
1481    let mut bearish_arrow_guard = ManuallyDrop::new(bearish_arrow_mu);
1482    let mut bullish_change_guard = ManuallyDrop::new(bullish_change_mu);
1483    let mut bearish_change_guard = ManuallyDrop::new(bearish_change_mu);
1484    let mut hh_guard = ManuallyDrop::new(hh_mu);
1485    let mut lh_guard = ManuallyDrop::new(lh_mu);
1486    let mut hl_guard = ManuallyDrop::new(hl_mu);
1487    let mut ll_guard = ManuallyDrop::new(ll_mu);
1488    let mut bullish_bos_guard = ManuallyDrop::new(bullish_bos_mu);
1489    let mut bullish_choch_guard = ManuallyDrop::new(bullish_choch_mu);
1490    let mut bearish_bos_guard = ManuallyDrop::new(bearish_bos_mu);
1491    let mut bearish_choch_guard = ManuallyDrop::new(bearish_choch_mu);
1492
1493    let basis_all = unsafe { mu_slice_as_f64_slice_mut(&mut basis_guard) };
1494    let upper_band_all = unsafe { mu_slice_as_f64_slice_mut(&mut upper_band_guard) };
1495    let lower_band_all = unsafe { mu_slice_as_f64_slice_mut(&mut lower_band_guard) };
1496    let structure_direction_all =
1497        unsafe { mu_slice_as_f64_slice_mut(&mut structure_direction_guard) };
1498    let bullish_arrow_all = unsafe { mu_slice_as_f64_slice_mut(&mut bullish_arrow_guard) };
1499    let bearish_arrow_all = unsafe { mu_slice_as_f64_slice_mut(&mut bearish_arrow_guard) };
1500    let bullish_change_all = unsafe { mu_slice_as_f64_slice_mut(&mut bullish_change_guard) };
1501    let bearish_change_all = unsafe { mu_slice_as_f64_slice_mut(&mut bearish_change_guard) };
1502    let hh_all = unsafe { mu_slice_as_f64_slice_mut(&mut hh_guard) };
1503    let lh_all = unsafe { mu_slice_as_f64_slice_mut(&mut lh_guard) };
1504    let hl_all = unsafe { mu_slice_as_f64_slice_mut(&mut hl_guard) };
1505    let ll_all = unsafe { mu_slice_as_f64_slice_mut(&mut ll_guard) };
1506    let bullish_bos_all = unsafe { mu_slice_as_f64_slice_mut(&mut bullish_bos_guard) };
1507    let bullish_choch_all = unsafe { mu_slice_as_f64_slice_mut(&mut bullish_choch_guard) };
1508    let bearish_bos_all = unsafe { mu_slice_as_f64_slice_mut(&mut bearish_bos_guard) };
1509    let bearish_choch_all = unsafe { mu_slice_as_f64_slice_mut(&mut bearish_choch_guard) };
1510
1511    let run_row = |row: usize,
1512                   basis_row: &mut [f64],
1513                   upper_band_row: &mut [f64],
1514                   lower_band_row: &mut [f64],
1515                   structure_direction_row: &mut [f64],
1516                   bullish_arrow_row: &mut [f64],
1517                   bearish_arrow_row: &mut [f64],
1518                   bullish_change_row: &mut [f64],
1519                   bearish_change_row: &mut [f64],
1520                   hh_row: &mut [f64],
1521                   lh_row: &mut [f64],
1522                   hl_row: &mut [f64],
1523                   ll_row: &mut [f64],
1524                   bullish_bos_row: &mut [f64],
1525                   bullish_choch_row: &mut [f64],
1526                   bearish_bos_row: &mut [f64],
1527                   bearish_choch_row: &mut [f64]|
1528     -> Result<(), MarketStructureConfluenceError> {
1529        let input =
1530            MarketStructureConfluenceInput::from_slices(high, low, close, combos[row].clone());
1531        market_structure_confluence_into_slices(
1532            &input,
1533            single_kernel,
1534            basis_row,
1535            upper_band_row,
1536            lower_band_row,
1537            structure_direction_row,
1538            bullish_arrow_row,
1539            bearish_arrow_row,
1540            bullish_change_row,
1541            bearish_change_row,
1542            hh_row,
1543            lh_row,
1544            hl_row,
1545            ll_row,
1546            bullish_bos_row,
1547            bullish_choch_row,
1548            bearish_bos_row,
1549            bearish_choch_row,
1550        )
1551    };
1552
1553    #[cfg(not(target_arch = "wasm32"))]
1554    {
1555        basis_all
1556            .par_chunks_mut(cols)
1557            .zip(upper_band_all.par_chunks_mut(cols))
1558            .zip(lower_band_all.par_chunks_mut(cols))
1559            .zip(structure_direction_all.par_chunks_mut(cols))
1560            .zip(bullish_arrow_all.par_chunks_mut(cols))
1561            .zip(bearish_arrow_all.par_chunks_mut(cols))
1562            .zip(bullish_change_all.par_chunks_mut(cols))
1563            .zip(bearish_change_all.par_chunks_mut(cols))
1564            .zip(hh_all.par_chunks_mut(cols))
1565            .zip(lh_all.par_chunks_mut(cols))
1566            .zip(hl_all.par_chunks_mut(cols))
1567            .zip(ll_all.par_chunks_mut(cols))
1568            .zip(bullish_bos_all.par_chunks_mut(cols))
1569            .zip(bullish_choch_all.par_chunks_mut(cols))
1570            .zip(bearish_bos_all.par_chunks_mut(cols))
1571            .zip(bearish_choch_all.par_chunks_mut(cols))
1572            .enumerate()
1573            .try_for_each(
1574                |(
1575                    row,
1576                    (
1577                        (
1578                            (
1579                                (
1580                                    (
1581                                        (
1582                                            (
1583                                                (
1584                                                    (
1585                                                        (
1586                                                            (
1587                                                                (
1588                                                                    (
1589                                                                        (
1590                                                                            (basis_row, upper_band_row),
1591                                                                            lower_band_row,
1592                                                                        ),
1593                                                                        structure_direction_row,
1594                                                                    ),
1595                                                                    bullish_arrow_row,
1596                                                                ),
1597                                                                bearish_arrow_row,
1598                                                            ),
1599                                                            bullish_change_row,
1600                                                        ),
1601                                                        bearish_change_row,
1602                                                    ),
1603                                                    hh_row,
1604                                                ),
1605                                                lh_row,
1606                                            ),
1607                                            hl_row,
1608                                        ),
1609                                        ll_row,
1610                                    ),
1611                                    bullish_bos_row,
1612                                ),
1613                                bullish_choch_row,
1614                            ),
1615                            bearish_bos_row,
1616                        ),
1617                        bearish_choch_row,
1618                    ),
1619                )| {
1620                    run_row(
1621                        row,
1622                        basis_row,
1623                        upper_band_row,
1624                        lower_band_row,
1625                        structure_direction_row,
1626                        bullish_arrow_row,
1627                        bearish_arrow_row,
1628                        bullish_change_row,
1629                        bearish_change_row,
1630                        hh_row,
1631                        lh_row,
1632                        hl_row,
1633                        ll_row,
1634                        bullish_bos_row,
1635                        bullish_choch_row,
1636                        bearish_bos_row,
1637                        bearish_choch_row,
1638                    )
1639                },
1640            )?;
1641    }
1642
1643    #[cfg(target_arch = "wasm32")]
1644    {
1645        for row in 0..rows {
1646            let start = row * cols;
1647            let end = start + cols;
1648            run_row(
1649                row,
1650                &mut basis_all[start..end],
1651                &mut upper_band_all[start..end],
1652                &mut lower_band_all[start..end],
1653                &mut structure_direction_all[start..end],
1654                &mut bullish_arrow_all[start..end],
1655                &mut bearish_arrow_all[start..end],
1656                &mut bullish_change_all[start..end],
1657                &mut bearish_change_all[start..end],
1658                &mut hh_all[start..end],
1659                &mut lh_all[start..end],
1660                &mut hl_all[start..end],
1661                &mut ll_all[start..end],
1662                &mut bullish_bos_all[start..end],
1663                &mut bullish_choch_all[start..end],
1664                &mut bearish_bos_all[start..end],
1665                &mut bearish_choch_all[start..end],
1666            )?;
1667        }
1668    }
1669
1670    let basis = unsafe { assume_init_vec(basis_guard) };
1671    let upper_band = unsafe { assume_init_vec(upper_band_guard) };
1672    let lower_band = unsafe { assume_init_vec(lower_band_guard) };
1673    let structure_direction = unsafe { assume_init_vec(structure_direction_guard) };
1674    let bullish_arrow = unsafe { assume_init_vec(bullish_arrow_guard) };
1675    let bearish_arrow = unsafe { assume_init_vec(bearish_arrow_guard) };
1676    let bullish_change = unsafe { assume_init_vec(bullish_change_guard) };
1677    let bearish_change = unsafe { assume_init_vec(bearish_change_guard) };
1678    let hh = unsafe { assume_init_vec(hh_guard) };
1679    let lh = unsafe { assume_init_vec(lh_guard) };
1680    let hl = unsafe { assume_init_vec(hl_guard) };
1681    let ll = unsafe { assume_init_vec(ll_guard) };
1682    let bullish_bos = unsafe { assume_init_vec(bullish_bos_guard) };
1683    let bullish_choch = unsafe { assume_init_vec(bullish_choch_guard) };
1684    let bearish_bos = unsafe { assume_init_vec(bearish_bos_guard) };
1685    let bearish_choch = unsafe { assume_init_vec(bearish_choch_guard) };
1686
1687    Ok(MarketStructureConfluenceBatchOutput {
1688        basis,
1689        upper_band,
1690        lower_band,
1691        structure_direction,
1692        bullish_arrow,
1693        bearish_arrow,
1694        bullish_change,
1695        bearish_change,
1696        hh,
1697        lh,
1698        hl,
1699        ll,
1700        bullish_bos,
1701        bullish_choch,
1702        bearish_bos,
1703        bearish_choch,
1704        combos,
1705        rows,
1706        cols,
1707    })
1708}
1709
1710#[inline(always)]
1711unsafe fn mu_slice_as_f64_slice_mut(buf: &mut ManuallyDrop<Vec<MaybeUninit<f64>>>) -> &mut [f64] {
1712    std::slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut f64, buf.len())
1713}
1714
1715#[inline(always)]
1716unsafe fn assume_init_vec(buf: ManuallyDrop<Vec<MaybeUninit<f64>>>) -> Vec<f64> {
1717    let mut buf = buf;
1718    Vec::from_raw_parts(buf.as_mut_ptr() as *mut f64, buf.len(), buf.capacity())
1719}
1720
1721#[cfg(feature = "python")]
1722#[pyfunction(name = "market_structure_confluence")]
1723#[pyo3(signature = (high, low, close, swing_size=DEFAULT_SWING_SIZE, bos_confirmation=DEFAULT_BOS_CONFIRMATION, basis_length=DEFAULT_BASIS_LENGTH, atr_length=DEFAULT_ATR_LENGTH, atr_smooth=DEFAULT_ATR_SMOOTH, vol_mult=DEFAULT_VOL_MULT, kernel=None))]
1724pub fn market_structure_confluence_py<'py>(
1725    py: Python<'py>,
1726    high: PyReadonlyArray1<'py, f64>,
1727    low: PyReadonlyArray1<'py, f64>,
1728    close: PyReadonlyArray1<'py, f64>,
1729    swing_size: usize,
1730    bos_confirmation: &str,
1731    basis_length: usize,
1732    atr_length: usize,
1733    atr_smooth: usize,
1734    vol_mult: f64,
1735    kernel: Option<&str>,
1736) -> PyResult<Bound<'py, PyDict>> {
1737    let high = high.as_slice()?;
1738    let low = low.as_slice()?;
1739    let close = close.as_slice()?;
1740    let kernel = validate_kernel(kernel, false)?;
1741    let input = MarketStructureConfluenceInput::from_slices(
1742        high,
1743        low,
1744        close,
1745        MarketStructureConfluenceParams {
1746            swing_size: Some(swing_size),
1747            bos_confirmation: Some(bos_confirmation.to_string()),
1748            basis_length: Some(basis_length),
1749            atr_length: Some(atr_length),
1750            atr_smooth: Some(atr_smooth),
1751            vol_mult: Some(vol_mult),
1752        },
1753    );
1754    let output = py
1755        .allow_threads(|| market_structure_confluence_with_kernel(&input, kernel))
1756        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1757    let dict = PyDict::new(py);
1758    dict.set_item("basis", output.basis.into_pyarray(py))?;
1759    dict.set_item("upper_band", output.upper_band.into_pyarray(py))?;
1760    dict.set_item("lower_band", output.lower_band.into_pyarray(py))?;
1761    dict.set_item("structure_direction", output.structure_direction.into_pyarray(py))?;
1762    dict.set_item("bullish_arrow", output.bullish_arrow.into_pyarray(py))?;
1763    dict.set_item("bearish_arrow", output.bearish_arrow.into_pyarray(py))?;
1764    dict.set_item("bullish_change", output.bullish_change.into_pyarray(py))?;
1765    dict.set_item("bearish_change", output.bearish_change.into_pyarray(py))?;
1766    dict.set_item("hh", output.hh.into_pyarray(py))?;
1767    dict.set_item("lh", output.lh.into_pyarray(py))?;
1768    dict.set_item("hl", output.hl.into_pyarray(py))?;
1769    dict.set_item("ll", output.ll.into_pyarray(py))?;
1770    dict.set_item("bullish_bos", output.bullish_bos.into_pyarray(py))?;
1771    dict.set_item("bullish_choch", output.bullish_choch.into_pyarray(py))?;
1772    dict.set_item("bearish_bos", output.bearish_bos.into_pyarray(py))?;
1773    dict.set_item("bearish_choch", output.bearish_choch.into_pyarray(py))?;
1774    Ok(dict)
1775}
1776
1777#[cfg(feature = "python")]
1778#[pyfunction(name = "market_structure_confluence_batch")]
1779#[pyo3(signature = (high, low, close, swing_size_range=(DEFAULT_SWING_SIZE, DEFAULT_SWING_SIZE, 0), bos_confirmation_options=vec![DEFAULT_BOS_CONFIRMATION.to_string()], basis_length_range=(DEFAULT_BASIS_LENGTH, DEFAULT_BASIS_LENGTH, 0), atr_length_range=(DEFAULT_ATR_LENGTH, DEFAULT_ATR_LENGTH, 0), atr_smooth_range=(DEFAULT_ATR_SMOOTH, DEFAULT_ATR_SMOOTH, 0), vol_mult_range=(DEFAULT_VOL_MULT, DEFAULT_VOL_MULT, 0.0), kernel=None))]
1780pub fn market_structure_confluence_batch_py<'py>(
1781    py: Python<'py>,
1782    high: PyReadonlyArray1<'py, f64>,
1783    low: PyReadonlyArray1<'py, f64>,
1784    close: PyReadonlyArray1<'py, f64>,
1785    swing_size_range: (usize, usize, usize),
1786    bos_confirmation_options: Vec<String>,
1787    basis_length_range: (usize, usize, usize),
1788    atr_length_range: (usize, usize, usize),
1789    atr_smooth_range: (usize, usize, usize),
1790    vol_mult_range: (f64, f64, f64),
1791    kernel: Option<&str>,
1792) -> PyResult<Bound<'py, PyDict>> {
1793    let high = high.as_slice()?;
1794    let low = low.as_slice()?;
1795    let close = close.as_slice()?;
1796    let kernel = validate_kernel(kernel, true)?;
1797    let output = py
1798        .allow_threads(|| {
1799            market_structure_confluence_batch_with_kernel(
1800                high,
1801                low,
1802                close,
1803                &MarketStructureConfluenceBatchRange {
1804                    swing_size: swing_size_range,
1805                    bos_confirmation: bos_confirmation_options,
1806                    basis_length: basis_length_range,
1807                    atr_length: atr_length_range,
1808                    atr_smooth: atr_smooth_range,
1809                    vol_mult: vol_mult_range,
1810                },
1811                kernel,
1812            )
1813        })
1814        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1815
1816    let total = output.rows * output.cols;
1817    let arrays = [
1818        unsafe { PyArray1::<f64>::new(py, [total], false) },
1819        unsafe { PyArray1::<f64>::new(py, [total], false) },
1820        unsafe { PyArray1::<f64>::new(py, [total], false) },
1821        unsafe { PyArray1::<f64>::new(py, [total], false) },
1822        unsafe { PyArray1::<f64>::new(py, [total], false) },
1823        unsafe { PyArray1::<f64>::new(py, [total], false) },
1824        unsafe { PyArray1::<f64>::new(py, [total], false) },
1825        unsafe { PyArray1::<f64>::new(py, [total], false) },
1826        unsafe { PyArray1::<f64>::new(py, [total], false) },
1827        unsafe { PyArray1::<f64>::new(py, [total], false) },
1828        unsafe { PyArray1::<f64>::new(py, [total], false) },
1829        unsafe { PyArray1::<f64>::new(py, [total], false) },
1830        unsafe { PyArray1::<f64>::new(py, [total], false) },
1831        unsafe { PyArray1::<f64>::new(py, [total], false) },
1832        unsafe { PyArray1::<f64>::new(py, [total], false) },
1833        unsafe { PyArray1::<f64>::new(py, [total], false) },
1834    ];
1835    unsafe { arrays[0].as_slice_mut()? }.copy_from_slice(&output.basis);
1836    unsafe { arrays[1].as_slice_mut()? }.copy_from_slice(&output.upper_band);
1837    unsafe { arrays[2].as_slice_mut()? }.copy_from_slice(&output.lower_band);
1838    unsafe { arrays[3].as_slice_mut()? }.copy_from_slice(&output.structure_direction);
1839    unsafe { arrays[4].as_slice_mut()? }.copy_from_slice(&output.bullish_arrow);
1840    unsafe { arrays[5].as_slice_mut()? }.copy_from_slice(&output.bearish_arrow);
1841    unsafe { arrays[6].as_slice_mut()? }.copy_from_slice(&output.bullish_change);
1842    unsafe { arrays[7].as_slice_mut()? }.copy_from_slice(&output.bearish_change);
1843    unsafe { arrays[8].as_slice_mut()? }.copy_from_slice(&output.hh);
1844    unsafe { arrays[9].as_slice_mut()? }.copy_from_slice(&output.lh);
1845    unsafe { arrays[10].as_slice_mut()? }.copy_from_slice(&output.hl);
1846    unsafe { arrays[11].as_slice_mut()? }.copy_from_slice(&output.ll);
1847    unsafe { arrays[12].as_slice_mut()? }.copy_from_slice(&output.bullish_bos);
1848    unsafe { arrays[13].as_slice_mut()? }.copy_from_slice(&output.bullish_choch);
1849    unsafe { arrays[14].as_slice_mut()? }.copy_from_slice(&output.bearish_bos);
1850    unsafe { arrays[15].as_slice_mut()? }.copy_from_slice(&output.bearish_choch);
1851
1852    let dict = PyDict::new(py);
1853    dict.set_item("basis", arrays[0].reshape((output.rows, output.cols))?)?;
1854    dict.set_item("upper_band", arrays[1].reshape((output.rows, output.cols))?)?;
1855    dict.set_item("lower_band", arrays[2].reshape((output.rows, output.cols))?)?;
1856    dict.set_item(
1857        "structure_direction",
1858        arrays[3].reshape((output.rows, output.cols))?,
1859    )?;
1860    dict.set_item("bullish_arrow", arrays[4].reshape((output.rows, output.cols))?)?;
1861    dict.set_item("bearish_arrow", arrays[5].reshape((output.rows, output.cols))?)?;
1862    dict.set_item("bullish_change", arrays[6].reshape((output.rows, output.cols))?)?;
1863    dict.set_item("bearish_change", arrays[7].reshape((output.rows, output.cols))?)?;
1864    dict.set_item("hh", arrays[8].reshape((output.rows, output.cols))?)?;
1865    dict.set_item("lh", arrays[9].reshape((output.rows, output.cols))?)?;
1866    dict.set_item("hl", arrays[10].reshape((output.rows, output.cols))?)?;
1867    dict.set_item("ll", arrays[11].reshape((output.rows, output.cols))?)?;
1868    dict.set_item("bullish_bos", arrays[12].reshape((output.rows, output.cols))?)?;
1869    dict.set_item("bullish_choch", arrays[13].reshape((output.rows, output.cols))?)?;
1870    dict.set_item("bearish_bos", arrays[14].reshape((output.rows, output.cols))?)?;
1871    dict.set_item("bearish_choch", arrays[15].reshape((output.rows, output.cols))?)?;
1872    dict.set_item(
1873        "swing_sizes",
1874        output
1875            .combos
1876            .iter()
1877            .map(|combo| combo.swing_size.unwrap_or(DEFAULT_SWING_SIZE) as u64)
1878            .collect::<Vec<_>>()
1879            .into_pyarray(py),
1880    )?;
1881    dict.set_item(
1882        "bos_confirmations",
1883        output
1884            .combos
1885            .iter()
1886            .map(|combo| combo.bos_confirmation.clone().unwrap_or_else(|| DEFAULT_BOS_CONFIRMATION.to_string()))
1887            .collect::<Vec<_>>(),
1888    )?;
1889    dict.set_item(
1890        "basis_lengths",
1891        output
1892            .combos
1893            .iter()
1894            .map(|combo| combo.basis_length.unwrap_or(DEFAULT_BASIS_LENGTH) as u64)
1895            .collect::<Vec<_>>()
1896            .into_pyarray(py),
1897    )?;
1898    dict.set_item(
1899        "atr_lengths",
1900        output
1901            .combos
1902            .iter()
1903            .map(|combo| combo.atr_length.unwrap_or(DEFAULT_ATR_LENGTH) as u64)
1904            .collect::<Vec<_>>()
1905            .into_pyarray(py),
1906    )?;
1907    dict.set_item(
1908        "atr_smooths",
1909        output
1910            .combos
1911            .iter()
1912            .map(|combo| combo.atr_smooth.unwrap_or(DEFAULT_ATR_SMOOTH) as u64)
1913            .collect::<Vec<_>>()
1914            .into_pyarray(py),
1915    )?;
1916    dict.set_item(
1917        "vol_mults",
1918        output
1919            .combos
1920            .iter()
1921            .map(|combo| combo.vol_mult.unwrap_or(DEFAULT_VOL_MULT))
1922            .collect::<Vec<_>>()
1923            .into_pyarray(py),
1924    )?;
1925    dict.set_item("rows", output.rows)?;
1926    dict.set_item("cols", output.cols)?;
1927    Ok(dict)
1928}
1929
1930#[cfg(feature = "python")]
1931#[pyclass(name = "MarketStructureConfluenceStream")]
1932pub struct MarketStructureConfluenceStreamPy {
1933    stream: MarketStructureConfluenceStream,
1934}
1935
1936#[cfg(feature = "python")]
1937#[pymethods]
1938impl MarketStructureConfluenceStreamPy {
1939    #[new]
1940    #[pyo3(signature = (swing_size=DEFAULT_SWING_SIZE, bos_confirmation=DEFAULT_BOS_CONFIRMATION, basis_length=DEFAULT_BASIS_LENGTH, atr_length=DEFAULT_ATR_LENGTH, atr_smooth=DEFAULT_ATR_SMOOTH, vol_mult=DEFAULT_VOL_MULT))]
1941    fn new(
1942        swing_size: usize,
1943        bos_confirmation: &str,
1944        basis_length: usize,
1945        atr_length: usize,
1946        atr_smooth: usize,
1947        vol_mult: f64,
1948    ) -> PyResult<Self> {
1949        let stream = MarketStructureConfluenceStream::try_new(MarketStructureConfluenceParams {
1950            swing_size: Some(swing_size),
1951            bos_confirmation: Some(bos_confirmation.to_string()),
1952            basis_length: Some(basis_length),
1953            atr_length: Some(atr_length),
1954            atr_smooth: Some(atr_smooth),
1955            vol_mult: Some(vol_mult),
1956        })
1957        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1958        Ok(Self { stream })
1959    }
1960
1961    fn update(&mut self, high: f64, low: f64, close: f64) -> Option<Vec<f64>> {
1962        self.stream.update(high, low, close).map(|output| {
1963            vec![
1964                output.basis,
1965                output.upper_band,
1966                output.lower_band,
1967                output.structure_direction,
1968                output.bullish_arrow,
1969                output.bearish_arrow,
1970                output.bullish_change,
1971                output.bearish_change,
1972                output.hh,
1973                output.lh,
1974                output.hl,
1975                output.ll,
1976                output.bullish_bos,
1977                output.bullish_choch,
1978                output.bearish_bos,
1979                output.bearish_choch,
1980            ]
1981        })
1982    }
1983}
1984
1985#[cfg(feature = "python")]
1986pub fn register_market_structure_confluence_module(
1987    m: &Bound<'_, PyModule>,
1988) -> PyResult<()> {
1989    m.add_function(wrap_pyfunction!(market_structure_confluence_py, m)?)?;
1990    m.add_function(wrap_pyfunction!(market_structure_confluence_batch_py, m)?)?;
1991    m.add_class::<MarketStructureConfluenceStreamPy>()?;
1992    Ok(())
1993}
1994
1995#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1996#[derive(Serialize, Deserialize)]
1997pub struct MarketStructureConfluenceJsOutput {
1998    pub basis: Vec<f64>,
1999    pub upper_band: Vec<f64>,
2000    pub lower_band: Vec<f64>,
2001    pub structure_direction: Vec<f64>,
2002    pub bullish_arrow: Vec<f64>,
2003    pub bearish_arrow: Vec<f64>,
2004    pub bullish_change: Vec<f64>,
2005    pub bearish_change: Vec<f64>,
2006    pub hh: Vec<f64>,
2007    pub lh: Vec<f64>,
2008    pub hl: Vec<f64>,
2009    pub ll: Vec<f64>,
2010    pub bullish_bos: Vec<f64>,
2011    pub bullish_choch: Vec<f64>,
2012    pub bearish_bos: Vec<f64>,
2013    pub bearish_choch: Vec<f64>,
2014}
2015
2016#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2017#[wasm_bindgen(js_name = market_structure_confluence_js)]
2018pub fn market_structure_confluence_js(
2019    high: &[f64],
2020    low: &[f64],
2021    close: &[f64],
2022    swing_size: usize,
2023    bos_confirmation: String,
2024    basis_length: usize,
2025    atr_length: usize,
2026    atr_smooth: usize,
2027    vol_mult: f64,
2028) -> Result<JsValue, JsValue> {
2029    let input = MarketStructureConfluenceInput::from_slices(
2030        high,
2031        low,
2032        close,
2033        MarketStructureConfluenceParams {
2034            swing_size: Some(swing_size),
2035            bos_confirmation: Some(bos_confirmation),
2036            basis_length: Some(basis_length),
2037            atr_length: Some(atr_length),
2038            atr_smooth: Some(atr_smooth),
2039            vol_mult: Some(vol_mult),
2040        },
2041    );
2042    let output = market_structure_confluence_with_kernel(&input, Kernel::Auto)
2043        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2044    serde_wasm_bindgen::to_value(&MarketStructureConfluenceJsOutput {
2045        basis: output.basis,
2046        upper_band: output.upper_band,
2047        lower_band: output.lower_band,
2048        structure_direction: output.structure_direction,
2049        bullish_arrow: output.bullish_arrow,
2050        bearish_arrow: output.bearish_arrow,
2051        bullish_change: output.bullish_change,
2052        bearish_change: output.bearish_change,
2053        hh: output.hh,
2054        lh: output.lh,
2055        hl: output.hl,
2056        ll: output.ll,
2057        bullish_bos: output.bullish_bos,
2058        bullish_choch: output.bullish_choch,
2059        bearish_bos: output.bearish_bos,
2060        bearish_choch: output.bearish_choch,
2061    })
2062    .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
2063}
2064
2065#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2066#[derive(Serialize, Deserialize)]
2067pub struct MarketStructureConfluenceBatchConfig {
2068    pub swing_size_range: (usize, usize, usize),
2069    pub bos_confirmation_options: Vec<String>,
2070    pub basis_length_range: (usize, usize, usize),
2071    pub atr_length_range: (usize, usize, usize),
2072    pub atr_smooth_range: (usize, usize, usize),
2073    pub vol_mult_range: (f64, f64, f64),
2074}
2075
2076#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2077#[derive(Serialize, Deserialize)]
2078pub struct MarketStructureConfluenceBatchJsOutput {
2079    pub basis: Vec<f64>,
2080    pub upper_band: Vec<f64>,
2081    pub lower_band: Vec<f64>,
2082    pub structure_direction: Vec<f64>,
2083    pub bullish_arrow: Vec<f64>,
2084    pub bearish_arrow: Vec<f64>,
2085    pub bullish_change: Vec<f64>,
2086    pub bearish_change: Vec<f64>,
2087    pub hh: Vec<f64>,
2088    pub lh: Vec<f64>,
2089    pub hl: Vec<f64>,
2090    pub ll: Vec<f64>,
2091    pub bullish_bos: Vec<f64>,
2092    pub bullish_choch: Vec<f64>,
2093    pub bearish_bos: Vec<f64>,
2094    pub bearish_choch: Vec<f64>,
2095    pub swing_sizes: Vec<usize>,
2096    pub bos_confirmations: Vec<String>,
2097    pub basis_lengths: Vec<usize>,
2098    pub atr_lengths: Vec<usize>,
2099    pub atr_smooths: Vec<usize>,
2100    pub vol_mults: Vec<f64>,
2101    pub rows: usize,
2102    pub cols: usize,
2103}
2104
2105#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2106#[wasm_bindgen(js_name = market_structure_confluence_batch)]
2107pub fn market_structure_confluence_batch_js(
2108    high: &[f64],
2109    low: &[f64],
2110    close: &[f64],
2111    config: JsValue,
2112) -> Result<JsValue, JsValue> {
2113    let cfg: MarketStructureConfluenceBatchConfig =
2114        serde_wasm_bindgen::from_value(config).map_err(|e| JsValue::from_str(&e.to_string()))?;
2115    let output = market_structure_confluence_batch_with_kernel(
2116        high,
2117        low,
2118        close,
2119        &MarketStructureConfluenceBatchRange {
2120            swing_size: cfg.swing_size_range,
2121            bos_confirmation: cfg.bos_confirmation_options,
2122            basis_length: cfg.basis_length_range,
2123            atr_length: cfg.atr_length_range,
2124            atr_smooth: cfg.atr_smooth_range,
2125            vol_mult: cfg.vol_mult_range,
2126        },
2127        Kernel::Auto,
2128    )
2129    .map_err(|e| JsValue::from_str(&e.to_string()))?;
2130
2131    serde_wasm_bindgen::to_value(&MarketStructureConfluenceBatchJsOutput {
2132        basis: output.basis,
2133        upper_band: output.upper_band,
2134        lower_band: output.lower_band,
2135        structure_direction: output.structure_direction,
2136        bullish_arrow: output.bullish_arrow,
2137        bearish_arrow: output.bearish_arrow,
2138        bullish_change: output.bullish_change,
2139        bearish_change: output.bearish_change,
2140        hh: output.hh,
2141        lh: output.lh,
2142        hl: output.hl,
2143        ll: output.ll,
2144        bullish_bos: output.bullish_bos,
2145        bullish_choch: output.bullish_choch,
2146        bearish_bos: output.bearish_bos,
2147        bearish_choch: output.bearish_choch,
2148        swing_sizes: output
2149            .combos
2150            .iter()
2151            .map(|combo| combo.swing_size.unwrap_or(DEFAULT_SWING_SIZE))
2152            .collect(),
2153        bos_confirmations: output
2154            .combos
2155            .iter()
2156            .map(|combo| combo.bos_confirmation.clone().unwrap_or_else(|| DEFAULT_BOS_CONFIRMATION.to_string()))
2157            .collect(),
2158        basis_lengths: output
2159            .combos
2160            .iter()
2161            .map(|combo| combo.basis_length.unwrap_or(DEFAULT_BASIS_LENGTH))
2162            .collect(),
2163        atr_lengths: output
2164            .combos
2165            .iter()
2166            .map(|combo| combo.atr_length.unwrap_or(DEFAULT_ATR_LENGTH))
2167            .collect(),
2168        atr_smooths: output
2169            .combos
2170            .iter()
2171            .map(|combo| combo.atr_smooth.unwrap_or(DEFAULT_ATR_SMOOTH))
2172            .collect(),
2173        vol_mults: output
2174            .combos
2175            .iter()
2176            .map(|combo| combo.vol_mult.unwrap_or(DEFAULT_VOL_MULT))
2177            .collect(),
2178        rows: output.rows,
2179        cols: output.cols,
2180    })
2181    .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
2182}
2183
2184#[cfg(test)]
2185mod tests {
2186    use super::*;
2187
2188    fn sample_ohlc() -> (Vec<f64>, Vec<f64>, Vec<f64>) {
2189        let mut high = Vec::with_capacity(420);
2190        let mut low = Vec::with_capacity(420);
2191        let mut close = Vec::with_capacity(420);
2192        for i in 0..420 {
2193            let base = 100.0 + i as f64 * 0.08 + (i as f64 * 0.11).sin() * 1.7;
2194            let c = base + (i as f64 * 0.07).cos() * 0.45;
2195            let h = c + 0.8 + (i as f64 * 0.09).sin().abs() * 0.4;
2196            let l = c - 0.8 - (i as f64 * 0.13).cos().abs() * 0.35;
2197            high.push(h);
2198            low.push(l);
2199            close.push(c);
2200        }
2201        (high, low, close)
2202    }
2203
2204    #[test]
2205    fn market_structure_confluence_into_matches_single() {
2206        let (high, low, close) = sample_ohlc();
2207        let input = MarketStructureConfluenceInput::from_slices(
2208            &high,
2209            &low,
2210            &close,
2211            MarketStructureConfluenceParams::default(),
2212        );
2213        let out = market_structure_confluence_with_kernel(&input, Kernel::Scalar).expect("single");
2214        let mut basis = vec![0.0; close.len()];
2215        let mut upper_band = vec![0.0; close.len()];
2216        let mut lower_band = vec![0.0; close.len()];
2217        let mut structure_direction = vec![0.0; close.len()];
2218        let mut bullish_arrow = vec![0.0; close.len()];
2219        let mut bearish_arrow = vec![0.0; close.len()];
2220        let mut bullish_change = vec![0.0; close.len()];
2221        let mut bearish_change = vec![0.0; close.len()];
2222        let mut hh = vec![0.0; close.len()];
2223        let mut lh = vec![0.0; close.len()];
2224        let mut hl = vec![0.0; close.len()];
2225        let mut ll = vec![0.0; close.len()];
2226        let mut bullish_bos = vec![0.0; close.len()];
2227        let mut bullish_choch = vec![0.0; close.len()];
2228        let mut bearish_bos = vec![0.0; close.len()];
2229        let mut bearish_choch = vec![0.0; close.len()];
2230
2231        market_structure_confluence_into_slices(
2232            &input,
2233            Kernel::Scalar,
2234            &mut basis,
2235            &mut upper_band,
2236            &mut lower_band,
2237            &mut structure_direction,
2238            &mut bullish_arrow,
2239            &mut bearish_arrow,
2240            &mut bullish_change,
2241            &mut bearish_change,
2242            &mut hh,
2243            &mut lh,
2244            &mut hl,
2245            &mut ll,
2246            &mut bullish_bos,
2247            &mut bullish_choch,
2248            &mut bearish_bos,
2249            &mut bearish_choch,
2250        )
2251        .expect("into");
2252
2253        for i in 0..close.len() {
2254            for (lhs, rhs) in [
2255                (out.basis[i], basis[i]),
2256                (out.upper_band[i], upper_band[i]),
2257                (out.lower_band[i], lower_band[i]),
2258                (out.structure_direction[i], structure_direction[i]),
2259                (out.bullish_arrow[i], bullish_arrow[i]),
2260                (out.bearish_arrow[i], bearish_arrow[i]),
2261                (out.bullish_change[i], bullish_change[i]),
2262                (out.bearish_change[i], bearish_change[i]),
2263                (out.hh[i], hh[i]),
2264                (out.lh[i], lh[i]),
2265                (out.hl[i], hl[i]),
2266                (out.ll[i], ll[i]),
2267                (out.bullish_bos[i], bullish_bos[i]),
2268                (out.bullish_choch[i], bullish_choch[i]),
2269                (out.bearish_bos[i], bearish_bos[i]),
2270                (out.bearish_choch[i], bearish_choch[i]),
2271            ] {
2272                if lhs.is_nan() {
2273                    assert!(rhs.is_nan());
2274                } else {
2275                    assert!((lhs - rhs).abs() <= 1e-12);
2276                }
2277            }
2278        }
2279    }
2280
2281    #[test]
2282    fn market_structure_confluence_stream_matches_batch() {
2283        let (high, low, close) = sample_ohlc();
2284        let input = MarketStructureConfluenceInput::from_slices(
2285            &high,
2286            &low,
2287            &close,
2288            MarketStructureConfluenceParams::default(),
2289        );
2290        let out = market_structure_confluence(&input).expect("batch");
2291        let mut stream =
2292            MarketStructureConfluenceStream::try_new(MarketStructureConfluenceParams::default())
2293                .expect("stream");
2294        let mut collected = Vec::with_capacity(close.len());
2295        for i in 0..close.len() {
2296            collected.push(stream.update(high[i], low[i], close[i]));
2297        }
2298        for i in 0..close.len() {
2299            let Some(point) = collected[i] else {
2300                assert!(out.basis[i].is_nan());
2301                continue;
2302            };
2303            for (lhs, rhs) in [
2304                (point.basis, out.basis[i]),
2305                (point.upper_band, out.upper_band[i]),
2306                (point.lower_band, out.lower_band[i]),
2307                (point.structure_direction, out.structure_direction[i]),
2308                (point.bullish_arrow, out.bullish_arrow[i]),
2309                (point.bearish_arrow, out.bearish_arrow[i]),
2310                (point.bullish_change, out.bullish_change[i]),
2311                (point.bearish_change, out.bearish_change[i]),
2312                (point.hh, out.hh[i]),
2313                (point.lh, out.lh[i]),
2314                (point.hl, out.hl[i]),
2315                (point.ll, out.ll[i]),
2316                (point.bullish_bos, out.bullish_bos[i]),
2317                (point.bullish_choch, out.bullish_choch[i]),
2318                (point.bearish_bos, out.bearish_bos[i]),
2319                (point.bearish_choch, out.bearish_choch[i]),
2320            ] {
2321                if rhs.is_nan() {
2322                    assert!(lhs.is_nan());
2323                } else {
2324                    assert!((lhs - rhs).abs() <= 1e-12);
2325                }
2326            }
2327        }
2328    }
2329
2330    #[test]
2331    fn market_structure_confluence_batch_first_row_matches_single() {
2332        let (high, low, close) = sample_ohlc();
2333        let single = market_structure_confluence(&MarketStructureConfluenceInput::from_slices(
2334            &high,
2335            &low,
2336            &close,
2337            MarketStructureConfluenceParams::default(),
2338        ))
2339        .expect("single");
2340        let batch = market_structure_confluence_batch_with_kernel(
2341            &high,
2342            &low,
2343            &close,
2344            &MarketStructureConfluenceBatchRange {
2345                swing_size: (10, 12, 2),
2346                bos_confirmation: vec!["Candle Close".to_string()],
2347                basis_length: (100, 100, 0),
2348                atr_length: (14, 14, 0),
2349                atr_smooth: (21, 21, 0),
2350                vol_mult: (2.0, 2.0, 0.0),
2351            },
2352            Kernel::ScalarBatch,
2353        )
2354        .expect("batch");
2355        assert_eq!(batch.rows, 2);
2356        assert_eq!(batch.cols, close.len());
2357        for i in 0..close.len() {
2358            let idx = i;
2359            for (lhs, rhs) in [
2360                (single.basis[i], batch.basis[idx]),
2361                (single.upper_band[i], batch.upper_band[idx]),
2362                (single.lower_band[i], batch.lower_band[idx]),
2363                (single.structure_direction[i], batch.structure_direction[idx]),
2364                (single.bullish_arrow[i], batch.bullish_arrow[idx]),
2365                (single.bearish_arrow[i], batch.bearish_arrow[idx]),
2366                (single.bullish_change[i], batch.bullish_change[idx]),
2367                (single.bearish_change[i], batch.bearish_change[idx]),
2368                (single.hh[i], batch.hh[idx]),
2369                (single.lh[i], batch.lh[idx]),
2370                (single.hl[i], batch.hl[idx]),
2371                (single.ll[i], batch.ll[idx]),
2372                (single.bullish_bos[i], batch.bullish_bos[idx]),
2373                (single.bullish_choch[i], batch.bullish_choch[idx]),
2374                (single.bearish_bos[i], batch.bearish_bos[idx]),
2375                (single.bearish_choch[i], batch.bearish_choch[idx]),
2376            ] {
2377                if lhs.is_nan() {
2378                    assert!(rhs.is_nan());
2379                } else {
2380                    assert!((lhs - rhs).abs() <= 1e-12);
2381                }
2382            }
2383        }
2384    }
2385
2386    #[test]
2387    fn market_structure_confluence_rejects_invalid_params() {
2388        let (high, low, close) = sample_ohlc();
2389        let err = market_structure_confluence(&MarketStructureConfluenceInput::from_slices(
2390            &high,
2391            &low,
2392            &close,
2393            MarketStructureConfluenceParams {
2394                swing_size: Some(1),
2395                ..MarketStructureConfluenceParams::default()
2396            },
2397        ))
2398        .expect_err("invalid swing size");
2399        assert!(err.to_string().contains("invalid swing_size"));
2400
2401        let err = MarketStructureConfluenceStream::try_new(MarketStructureConfluenceParams {
2402            bos_confirmation: Some("bad".to_string()),
2403            ..MarketStructureConfluenceParams::default()
2404        })
2405        .expect_err("invalid confirmation");
2406        assert!(err.to_string().contains("invalid bos_confirmation"));
2407    }
2408}