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