Skip to main content

vector_ta/indicators/
fibonacci_entry_bands.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::mem::{ManuallyDrop, MaybeUninit};
28use thiserror::Error;
29
30const DEFAULT_SOURCE: &str = "hlc3";
31const DEFAULT_LENGTH: usize = 21;
32const DEFAULT_ATR_LENGTH: usize = 14;
33const DEFAULT_USE_ATR: bool = true;
34const DEFAULT_TP_AGGRESSIVENESS: &str = "low";
35const MULT1: f64 = 0.618;
36const MULT2: f64 = 1.0;
37const MULT3: f64 = 1.618;
38const MULT4: f64 = 2.618;
39const FLOAT_TOL: f64 = 1e-12;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42enum SourceKind {
43    Open,
44    High,
45    Low,
46    Close,
47    Hl2,
48    Hlc3,
49    Ohlc4,
50    Hlcc4,
51}
52
53impl SourceKind {
54    #[inline(always)]
55    fn parse(value: &str) -> Option<Self> {
56        if value.eq_ignore_ascii_case("open") {
57            Some(Self::Open)
58        } else if value.eq_ignore_ascii_case("high") {
59            Some(Self::High)
60        } else if value.eq_ignore_ascii_case("low") {
61            Some(Self::Low)
62        } else if value.eq_ignore_ascii_case("close") {
63            Some(Self::Close)
64        } else if value.eq_ignore_ascii_case("hl2") {
65            Some(Self::Hl2)
66        } else if value.eq_ignore_ascii_case("hlc3") {
67            Some(Self::Hlc3)
68        } else if value.eq_ignore_ascii_case("ohlc4") {
69            Some(Self::Ohlc4)
70        } else if value.eq_ignore_ascii_case("hlcc4") || value.eq_ignore_ascii_case("hlcc") {
71            Some(Self::Hlcc4)
72        } else {
73            None
74        }
75    }
76
77    #[inline(always)]
78    fn as_str(self) -> &'static str {
79        match self {
80            Self::Open => "open",
81            Self::High => "high",
82            Self::Low => "low",
83            Self::Close => "close",
84            Self::Hl2 => "hl2",
85            Self::Hlc3 => "hlc3",
86            Self::Ohlc4 => "ohlc4",
87            Self::Hlcc4 => "hlcc4",
88        }
89    }
90
91    #[inline(always)]
92    fn needs_open(self) -> bool {
93        matches!(self, Self::Open | Self::Ohlc4)
94    }
95
96    #[inline(always)]
97    fn value(self, open: f64, high: f64, low: f64, close: f64) -> f64 {
98        match self {
99            Self::Open => open,
100            Self::High => high,
101            Self::Low => low,
102            Self::Close => close,
103            Self::Hl2 => 0.5 * (high + low),
104            Self::Hlc3 => (high + low + close) / 3.0,
105            Self::Ohlc4 => (open + high + low + close) * 0.25,
106            Self::Hlcc4 => (high + low + close + close) * 0.25,
107        }
108    }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112enum TpAggressiveness {
113    Low,
114    Medium,
115    High,
116}
117
118impl TpAggressiveness {
119    #[inline(always)]
120    fn parse(value: &str) -> Option<Self> {
121        if value.eq_ignore_ascii_case("low") {
122            Some(Self::Low)
123        } else if value.eq_ignore_ascii_case("medium") {
124            Some(Self::Medium)
125        } else if value.eq_ignore_ascii_case("high") {
126            Some(Self::High)
127        } else {
128            None
129        }
130    }
131
132    #[inline(always)]
133    fn as_str(self) -> &'static str {
134        match self {
135            Self::Low => "low",
136            Self::Medium => "medium",
137            Self::High => "high",
138        }
139    }
140}
141
142#[derive(Debug, Clone)]
143pub enum FibonacciEntryBandsData<'a> {
144    Candles(&'a Candles),
145    Slices {
146        open: &'a [f64],
147        high: &'a [f64],
148        low: &'a [f64],
149        close: &'a [f64],
150    },
151}
152
153#[derive(Debug, Clone)]
154pub struct FibonacciEntryBandsOutput {
155    pub basis: Vec<f64>,
156    pub trend: Vec<f64>,
157    pub upper_0618: Vec<f64>,
158    pub upper_1000: Vec<f64>,
159    pub upper_1618: Vec<f64>,
160    pub upper_2618: Vec<f64>,
161    pub lower_0618: Vec<f64>,
162    pub lower_1000: Vec<f64>,
163    pub lower_1618: Vec<f64>,
164    pub lower_2618: Vec<f64>,
165    pub tp_long_band: Vec<f64>,
166    pub tp_short_band: Vec<f64>,
167    pub long_entry: Vec<f64>,
168    pub short_entry: Vec<f64>,
169    pub rejection_long: Vec<f64>,
170    pub rejection_short: Vec<f64>,
171    pub long_bounce: Vec<f64>,
172    pub short_bounce: Vec<f64>,
173}
174
175#[derive(Debug, Clone, Copy)]
176pub struct FibonacciEntryBandsPoint {
177    pub basis: f64,
178    pub trend: f64,
179    pub upper_0618: f64,
180    pub upper_1000: f64,
181    pub upper_1618: f64,
182    pub upper_2618: f64,
183    pub lower_0618: f64,
184    pub lower_1000: f64,
185    pub lower_1618: f64,
186    pub lower_2618: f64,
187    pub tp_long_band: f64,
188    pub tp_short_band: f64,
189    pub long_entry: f64,
190    pub short_entry: f64,
191    pub rejection_long: f64,
192    pub rejection_short: f64,
193    pub long_bounce: f64,
194    pub short_bounce: f64,
195}
196
197impl FibonacciEntryBandsPoint {
198    #[inline(always)]
199    fn nan() -> Self {
200        Self {
201            basis: f64::NAN,
202            trend: f64::NAN,
203            upper_0618: f64::NAN,
204            upper_1000: f64::NAN,
205            upper_1618: f64::NAN,
206            upper_2618: f64::NAN,
207            lower_0618: f64::NAN,
208            lower_1000: f64::NAN,
209            lower_1618: f64::NAN,
210            lower_2618: f64::NAN,
211            tp_long_band: f64::NAN,
212            tp_short_band: f64::NAN,
213            long_entry: f64::NAN,
214            short_entry: f64::NAN,
215            rejection_long: f64::NAN,
216            rejection_short: f64::NAN,
217            long_bounce: f64::NAN,
218            short_bounce: f64::NAN,
219        }
220    }
221}
222
223#[derive(Debug, Clone, PartialEq)]
224#[cfg_attr(
225    all(target_arch = "wasm32", feature = "wasm"),
226    derive(Serialize, Deserialize)
227)]
228pub struct FibonacciEntryBandsParams {
229    pub source: Option<String>,
230    pub length: Option<usize>,
231    pub atr_length: Option<usize>,
232    pub use_atr: Option<bool>,
233    pub tp_aggressiveness: Option<String>,
234}
235
236impl Default for FibonacciEntryBandsParams {
237    fn default() -> Self {
238        Self {
239            source: Some(DEFAULT_SOURCE.to_string()),
240            length: Some(DEFAULT_LENGTH),
241            atr_length: Some(DEFAULT_ATR_LENGTH),
242            use_atr: Some(DEFAULT_USE_ATR),
243            tp_aggressiveness: Some(DEFAULT_TP_AGGRESSIVENESS.to_string()),
244        }
245    }
246}
247
248#[derive(Debug, Clone)]
249pub struct FibonacciEntryBandsInput<'a> {
250    pub data: FibonacciEntryBandsData<'a>,
251    pub params: FibonacciEntryBandsParams,
252}
253
254impl<'a> FibonacciEntryBandsInput<'a> {
255    #[inline]
256    pub fn from_candles(candles: &'a Candles, params: FibonacciEntryBandsParams) -> Self {
257        Self {
258            data: FibonacciEntryBandsData::Candles(candles),
259            params,
260        }
261    }
262
263    #[inline]
264    pub fn from_slices(
265        open: &'a [f64],
266        high: &'a [f64],
267        low: &'a [f64],
268        close: &'a [f64],
269        params: FibonacciEntryBandsParams,
270    ) -> Self {
271        Self {
272            data: FibonacciEntryBandsData::Slices {
273                open,
274                high,
275                low,
276                close,
277            },
278            params,
279        }
280    }
281
282    #[inline]
283    pub fn with_default_candles(candles: &'a Candles) -> Self {
284        Self::from_candles(candles, FibonacciEntryBandsParams::default())
285    }
286
287    #[inline]
288    pub fn as_slices(&self) -> (&'a [f64], &'a [f64], &'a [f64], &'a [f64]) {
289        match &self.data {
290            FibonacciEntryBandsData::Candles(candles) => {
291                (&candles.open, &candles.high, &candles.low, &candles.close)
292            }
293            FibonacciEntryBandsData::Slices {
294                open,
295                high,
296                low,
297                close,
298            } => (open, high, low, close),
299        }
300    }
301}
302
303#[derive(Clone, Debug)]
304pub struct FibonacciEntryBandsBuilder {
305    source: Option<SourceKind>,
306    length: Option<usize>,
307    atr_length: Option<usize>,
308    use_atr: Option<bool>,
309    tp_aggressiveness: Option<TpAggressiveness>,
310    kernel: Kernel,
311}
312
313impl Default for FibonacciEntryBandsBuilder {
314    fn default() -> Self {
315        Self {
316            source: None,
317            length: None,
318            atr_length: None,
319            use_atr: None,
320            tp_aggressiveness: None,
321            kernel: Kernel::Auto,
322        }
323    }
324}
325
326#[derive(Debug, Error)]
327pub enum FibonacciEntryBandsError {
328    #[error("fibonacci_entry_bands: Input data slice is empty.")]
329    EmptyInputData,
330    #[error("fibonacci_entry_bands: All values are NaN.")]
331    AllValuesNaN,
332    #[error(
333        "fibonacci_entry_bands: Inconsistent slice lengths: open={open_len}, high={high_len}, low={low_len}, close={close_len}"
334    )]
335    InconsistentSliceLengths {
336        open_len: usize,
337        high_len: usize,
338        low_len: usize,
339        close_len: usize,
340    },
341    #[error("fibonacci_entry_bands: Invalid length: length = {length}, data length = {data_len}")]
342    InvalidLength { length: usize, data_len: usize },
343    #[error(
344        "fibonacci_entry_bands: Invalid atr_length: atr_length = {atr_length}, data length = {data_len}"
345    )]
346    InvalidAtrLength { atr_length: usize, data_len: usize },
347    #[error(
348        "fibonacci_entry_bands: Invalid source: {source_name}. Supported: open, high, low, close, hl2, hlc3, ohlc4, hlcc4"
349    )]
350    InvalidSource { source_name: String },
351    #[error(
352        "fibonacci_entry_bands: Invalid TP aggressiveness: {tp_aggressiveness}. Supported: low, medium, high"
353    )]
354    InvalidTpAggressiveness { tp_aggressiveness: String },
355    #[error("fibonacci_entry_bands: Not enough valid data: needed = {needed}, valid = {valid}")]
356    NotEnoughValidData { needed: usize, valid: usize },
357    #[error("fibonacci_entry_bands: Output length mismatch: expected = {expected}")]
358    OutputLengthMismatch { expected: usize },
359    #[error("fibonacci_entry_bands: Invalid range: start={start}, end={end}, step={step}")]
360    InvalidRange {
361        start: String,
362        end: String,
363        step: String,
364    },
365    #[error("fibonacci_entry_bands: Invalid kernel for batch: {0:?}")]
366    InvalidKernelForBatch(Kernel),
367}
368
369#[derive(Debug, Clone, Copy)]
370struct ResolvedParams {
371    source: SourceKind,
372    length: usize,
373    atr_length: usize,
374    use_atr: bool,
375    tp_aggressiveness: TpAggressiveness,
376    ema_alpha: f64,
377}
378
379#[derive(Debug, Clone)]
380struct RollingStdev {
381    values: Vec<f64>,
382    head: usize,
383    count: usize,
384    sum: f64,
385    sum_sq: f64,
386}
387
388impl RollingStdev {
389    #[inline]
390    fn new(length: usize) -> Self {
391        Self {
392            values: vec![0.0; length.max(1)],
393            head: 0,
394            count: 0,
395            sum: 0.0,
396            sum_sq: 0.0,
397        }
398    }
399
400    #[inline]
401    fn reset(&mut self) {
402        self.head = 0;
403        self.count = 0;
404        self.sum = 0.0;
405        self.sum_sq = 0.0;
406    }
407
408    #[inline]
409    fn update(&mut self, value: f64) -> Option<f64> {
410        let len = self.values.len();
411        if self.count == len {
412            let old = self.values[self.head];
413            self.sum -= old;
414            self.sum_sq -= old * old;
415        } else {
416            self.count += 1;
417        }
418        self.values[self.head] = value;
419        self.head += 1;
420        if self.head == len {
421            self.head = 0;
422        }
423        self.sum += value;
424        self.sum_sq += value * value;
425        if self.count < len {
426            return None;
427        }
428        let mean = self.sum / len as f64;
429        Some((self.sum_sq / len as f64 - mean * mean).max(0.0).sqrt())
430    }
431}
432
433#[derive(Debug, Clone)]
434pub struct FibonacciEntryBandsStream {
435    params: ResolvedParams,
436    ema1: Option<f64>,
437    ema2: Option<f64>,
438    prev_basis: Option<f64>,
439    prev_prev_basis: Option<f64>,
440    trend: f64,
441    prev_close: Option<f64>,
442    atr_sum: f64,
443    atr_count: usize,
444    atr_value: Option<f64>,
445    stdev: RollingStdev,
446    prev_tp_long_band: Option<f64>,
447    prev_tp_short_band: Option<f64>,
448}
449
450impl FibonacciEntryBandsBuilder {
451    #[inline]
452    pub fn new() -> Self {
453        Self::default()
454    }
455
456    #[inline]
457    pub fn source(mut self, value: &str) -> Result<Self, FibonacciEntryBandsError> {
458        self.source = Some(parse_source(value)?);
459        Ok(self)
460    }
461
462    #[inline]
463    pub fn length(mut self, value: usize) -> Self {
464        self.length = Some(value);
465        self
466    }
467
468    #[inline]
469    pub fn atr_length(mut self, value: usize) -> Self {
470        self.atr_length = Some(value);
471        self
472    }
473
474    #[inline]
475    pub fn use_atr(mut self, value: bool) -> Self {
476        self.use_atr = Some(value);
477        self
478    }
479
480    #[inline]
481    pub fn tp_aggressiveness(mut self, value: &str) -> Result<Self, FibonacciEntryBandsError> {
482        self.tp_aggressiveness = Some(parse_tp_aggressiveness(value)?);
483        Ok(self)
484    }
485
486    #[inline]
487    pub fn kernel(mut self, kernel: Kernel) -> Self {
488        self.kernel = kernel;
489        self
490    }
491
492    #[inline]
493    pub fn apply(
494        self,
495        candles: &Candles,
496    ) -> Result<FibonacciEntryBandsOutput, FibonacciEntryBandsError> {
497        let input = FibonacciEntryBandsInput::from_candles(
498            candles,
499            FibonacciEntryBandsParams {
500                source: Some(self.source.unwrap_or(SourceKind::Hlc3).as_str().to_string()),
501                length: self.length,
502                atr_length: self.atr_length,
503                use_atr: self.use_atr,
504                tp_aggressiveness: Some(
505                    self.tp_aggressiveness
506                        .unwrap_or(TpAggressiveness::Low)
507                        .as_str()
508                        .to_string(),
509                ),
510            },
511        );
512        fibonacci_entry_bands_with_kernel(&input, self.kernel)
513    }
514
515    #[inline]
516    pub fn apply_slices(
517        self,
518        open: &[f64],
519        high: &[f64],
520        low: &[f64],
521        close: &[f64],
522    ) -> Result<FibonacciEntryBandsOutput, FibonacciEntryBandsError> {
523        let input = FibonacciEntryBandsInput::from_slices(
524            open,
525            high,
526            low,
527            close,
528            FibonacciEntryBandsParams {
529                source: Some(self.source.unwrap_or(SourceKind::Hlc3).as_str().to_string()),
530                length: self.length,
531                atr_length: self.atr_length,
532                use_atr: self.use_atr,
533                tp_aggressiveness: Some(
534                    self.tp_aggressiveness
535                        .unwrap_or(TpAggressiveness::Low)
536                        .as_str()
537                        .to_string(),
538                ),
539            },
540        );
541        fibonacci_entry_bands_with_kernel(&input, self.kernel)
542    }
543
544    #[inline]
545    pub fn into_stream(self) -> Result<FibonacciEntryBandsStream, FibonacciEntryBandsError> {
546        FibonacciEntryBandsStream::try_new(FibonacciEntryBandsParams {
547            source: Some(self.source.unwrap_or(SourceKind::Hlc3).as_str().to_string()),
548            length: self.length,
549            atr_length: self.atr_length,
550            use_atr: self.use_atr,
551            tp_aggressiveness: Some(
552                self.tp_aggressiveness
553                    .unwrap_or(TpAggressiveness::Low)
554                    .as_str()
555                    .to_string(),
556            ),
557        })
558    }
559}
560
561impl FibonacciEntryBandsStream {
562    #[inline]
563    pub fn try_new(params: FibonacciEntryBandsParams) -> Result<Self, FibonacciEntryBandsError> {
564        let resolved = resolve_params(&params, None)?;
565        Ok(Self::new_resolved(resolved))
566    }
567
568    #[inline]
569    fn new_resolved(params: ResolvedParams) -> Self {
570        Self {
571            params,
572            ema1: None,
573            ema2: None,
574            prev_basis: None,
575            prev_prev_basis: None,
576            trend: 0.0,
577            prev_close: None,
578            atr_sum: 0.0,
579            atr_count: 0,
580            atr_value: None,
581            stdev: RollingStdev::new(params.length),
582            prev_tp_long_band: None,
583            prev_tp_short_band: None,
584        }
585    }
586
587    #[inline]
588    pub fn reset(&mut self) {
589        self.stdev.reset();
590        *self = Self::new_resolved(self.params);
591    }
592
593    #[inline]
594    pub fn get_warmup_period(&self) -> usize {
595        let vol_warmup = if self.params.use_atr {
596            self.params.atr_length.saturating_sub(1)
597        } else {
598            self.params.length.saturating_sub(1)
599        };
600        vol_warmup.max(2)
601    }
602
603    #[inline]
604    pub fn update(
605        &mut self,
606        open: f64,
607        high: f64,
608        low: f64,
609        close: f64,
610    ) -> Option<FibonacciEntryBandsPoint> {
611        if !valid_bar(self.params.source, open, high, low, close) {
612            self.reset();
613            return None;
614        }
615
616        let source = self.params.source.value(open, high, low, close);
617        let ema1 = match self.ema1 {
618            Some(prev) => prev + self.params.ema_alpha * (source - prev),
619            None => source,
620        };
621        self.ema1 = Some(ema1);
622        let basis = match self.ema2 {
623            Some(prev) => prev + self.params.ema_alpha * (ema1 - prev),
624            None => ema1,
625        };
626        self.ema2 = Some(basis);
627
628        let long_entry = if let (Some(prev_basis), Some(prev_prev_basis)) =
629            (self.prev_basis, self.prev_prev_basis)
630        {
631            let curr_delta = basis - prev_basis;
632            let prev_delta = prev_basis - prev_prev_basis;
633            if curr_delta > FLOAT_TOL && prev_delta <= FLOAT_TOL {
634                1.0
635            } else {
636                0.0
637            }
638        } else {
639            f64::NAN
640        };
641
642        let short_entry = if let (Some(prev_basis), Some(prev_prev_basis)) =
643            (self.prev_basis, self.prev_prev_basis)
644        {
645            let curr_delta = basis - prev_basis;
646            let prev_delta = prev_basis - prev_prev_basis;
647            if curr_delta < -FLOAT_TOL && prev_delta >= -FLOAT_TOL {
648                1.0
649            } else {
650                0.0
651            }
652        } else {
653            f64::NAN
654        };
655
656        if let Some(prev_basis) = self.prev_basis {
657            if basis > prev_basis + FLOAT_TOL {
658                self.trend = 1.0;
659            } else if basis < prev_basis - FLOAT_TOL {
660                self.trend = -1.0;
661            }
662        }
663
664        let vol = if self.params.use_atr {
665            self.update_atr(high, low, close)
666        } else {
667            self.stdev.update(source)
668        };
669
670        let mut point = FibonacciEntryBandsPoint {
671            basis,
672            trend: self.trend,
673            upper_0618: f64::NAN,
674            upper_1000: f64::NAN,
675            upper_1618: f64::NAN,
676            upper_2618: f64::NAN,
677            lower_0618: f64::NAN,
678            lower_1000: f64::NAN,
679            lower_1618: f64::NAN,
680            lower_2618: f64::NAN,
681            tp_long_band: f64::NAN,
682            tp_short_band: f64::NAN,
683            long_entry,
684            short_entry,
685            rejection_long: f64::NAN,
686            rejection_short: f64::NAN,
687            long_bounce: if self.trend > 0.0 {
688                if low < basis - FLOAT_TOL && close > basis + FLOAT_TOL && !matches_true(long_entry)
689                {
690                    1.0
691                } else {
692                    0.0
693                }
694            } else {
695                0.0
696            },
697            short_bounce: if self.trend < 0.0 {
698                if high > basis + FLOAT_TOL
699                    && close < basis - FLOAT_TOL
700                    && !matches_true(short_entry)
701                {
702                    1.0
703                } else {
704                    0.0
705                }
706            } else {
707                0.0
708            },
709        };
710
711        if let Some(volatility) = vol.filter(|v| v.is_finite()) {
712            point.upper_0618 = basis + volatility * MULT1;
713            point.upper_1000 = basis + volatility * MULT2;
714            point.upper_1618 = basis + volatility * MULT3;
715            point.upper_2618 = basis + volatility * MULT4;
716            point.lower_0618 = basis - volatility * MULT1;
717            point.lower_1000 = basis - volatility * MULT2;
718            point.lower_1618 = basis - volatility * MULT3;
719            point.lower_2618 = basis - volatility * MULT4;
720
721            let (tp_long_band, tp_short_band) = match self.params.tp_aggressiveness {
722                TpAggressiveness::Low => (point.lower_2618, point.upper_2618),
723                TpAggressiveness::Medium => (point.lower_1000, point.upper_1000),
724                TpAggressiveness::High => (point.lower_0618, point.upper_0618),
725            };
726            point.tp_long_band = tp_long_band;
727            point.tp_short_band = tp_short_band;
728
729            point.rejection_long = if self.trend < 0.0
730                && crossunder(close, tp_long_band, self.prev_close, self.prev_tp_long_band)
731            {
732                1.0
733            } else {
734                0.0
735            };
736
737            point.rejection_short = if self.trend > 0.0
738                && crossover(
739                    close,
740                    tp_short_band,
741                    self.prev_close,
742                    self.prev_tp_short_band,
743                ) {
744                1.0
745            } else {
746                0.0
747            };
748        }
749
750        self.prev_prev_basis = self.prev_basis;
751        self.prev_basis = Some(basis);
752        self.prev_close = Some(close);
753        self.prev_tp_long_band = finite_option(point.tp_long_band);
754        self.prev_tp_short_band = finite_option(point.tp_short_band);
755
756        Some(point)
757    }
758
759    #[inline]
760    fn update_atr(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
761        let tr = match self.prev_close {
762            Some(prev_close) => {
763                let hl = high - low;
764                let hc = (high - prev_close).abs();
765                let lc = (low - prev_close).abs();
766                hl.max(hc).max(lc)
767            }
768            None => high - low,
769        };
770        if self.atr_count < self.params.atr_length {
771            self.atr_count += 1;
772            self.atr_sum += tr;
773            if self.atr_count == self.params.atr_length {
774                self.atr_value = Some(self.atr_sum / self.params.atr_length as f64);
775            }
776        } else if let Some(prev) = self.atr_value {
777            self.atr_value = Some(
778                ((self.params.atr_length - 1) as f64 * prev + tr) / self.params.atr_length as f64,
779            );
780        }
781        self.atr_value
782    }
783}
784
785#[inline(always)]
786fn finite_option(value: f64) -> Option<f64> {
787    if value.is_finite() {
788        Some(value)
789    } else {
790        None
791    }
792}
793
794#[inline(always)]
795fn matches_true(value: f64) -> bool {
796    value.is_finite() && value > 0.5
797}
798
799#[inline(always)]
800fn crossover(current_a: f64, current_b: f64, prev_a: Option<f64>, prev_b: Option<f64>) -> bool {
801    current_a.is_finite()
802        && current_b.is_finite()
803        && current_a > current_b + FLOAT_TOL
804        && prev_a
805            .zip(prev_b)
806            .map_or(false, |(a, b)| a <= b + FLOAT_TOL)
807}
808
809#[inline(always)]
810fn crossunder(current_a: f64, current_b: f64, prev_a: Option<f64>, prev_b: Option<f64>) -> bool {
811    current_a.is_finite()
812        && current_b.is_finite()
813        && current_a < current_b - FLOAT_TOL
814        && prev_a
815            .zip(prev_b)
816            .map_or(false, |(a, b)| a >= b - FLOAT_TOL)
817}
818
819#[inline(always)]
820fn parse_source(value: &str) -> Result<SourceKind, FibonacciEntryBandsError> {
821    SourceKind::parse(value).ok_or_else(|| FibonacciEntryBandsError::InvalidSource {
822        source_name: value.to_string(),
823    })
824}
825
826#[inline(always)]
827fn parse_tp_aggressiveness(value: &str) -> Result<TpAggressiveness, FibonacciEntryBandsError> {
828    TpAggressiveness::parse(value).ok_or_else(|| {
829        FibonacciEntryBandsError::InvalidTpAggressiveness {
830            tp_aggressiveness: value.to_string(),
831        }
832    })
833}
834
835#[inline(always)]
836fn valid_bar(source: SourceKind, open: f64, high: f64, low: f64, close: f64) -> bool {
837    high.is_finite()
838        && low.is_finite()
839        && close.is_finite()
840        && (!source.needs_open() || open.is_finite())
841}
842
843#[inline(always)]
844fn first_valid_ohlc(
845    source: SourceKind,
846    open: &[f64],
847    high: &[f64],
848    low: &[f64],
849    close: &[f64],
850) -> usize {
851    let len = close.len();
852    let mut i = 0usize;
853    while i < len {
854        if valid_bar(source, open[i], high[i], low[i], close[i]) {
855            return i;
856        }
857        i += 1;
858    }
859    len
860}
861
862#[inline(always)]
863fn max_consecutive_valid_ohlc(
864    source: SourceKind,
865    open: &[f64],
866    high: &[f64],
867    low: &[f64],
868    close: &[f64],
869) -> usize {
870    let len = close.len();
871    let mut best = 0usize;
872    let mut run = 0usize;
873    let mut i = 0usize;
874    while i < len {
875        if valid_bar(source, open[i], high[i], low[i], close[i]) {
876            run += 1;
877            if run > best {
878                best = run;
879            }
880        } else {
881            run = 0;
882        }
883        i += 1;
884    }
885    best
886}
887
888#[inline]
889fn resolve_params(
890    params: &FibonacciEntryBandsParams,
891    data_len: Option<usize>,
892) -> Result<ResolvedParams, FibonacciEntryBandsError> {
893    let source = parse_source(params.source.as_deref().unwrap_or(DEFAULT_SOURCE))?;
894    let length = params.length.unwrap_or(DEFAULT_LENGTH);
895    let atr_length = params.atr_length.unwrap_or(DEFAULT_ATR_LENGTH);
896    let use_atr = params.use_atr.unwrap_or(DEFAULT_USE_ATR);
897    let tp_aggressiveness = parse_tp_aggressiveness(
898        params
899            .tp_aggressiveness
900            .as_deref()
901            .unwrap_or(DEFAULT_TP_AGGRESSIVENESS),
902    )?;
903    let data_len = data_len.unwrap_or(0);
904
905    if length == 0 || (data_len != 0 && length > data_len) {
906        return Err(FibonacciEntryBandsError::InvalidLength { length, data_len });
907    }
908    if atr_length == 0 || (data_len != 0 && atr_length > data_len) {
909        return Err(FibonacciEntryBandsError::InvalidAtrLength {
910            atr_length,
911            data_len,
912        });
913    }
914
915    Ok(ResolvedParams {
916        source,
917        length,
918        atr_length,
919        use_atr,
920        tp_aggressiveness,
921        ema_alpha: 2.0 / (length as f64 + 1.0),
922    })
923}
924
925#[inline(always)]
926fn full_nan_vec(len: usize) -> Vec<f64> {
927    alloc_with_nan_prefix(len, len)
928}
929
930#[inline(always)]
931#[allow(clippy::too_many_arguments)]
932fn write_point(
933    point: FibonacciEntryBandsPoint,
934    idx: usize,
935    basis: &mut [f64],
936    trend: &mut [f64],
937    upper_0618: &mut [f64],
938    upper_1000: &mut [f64],
939    upper_1618: &mut [f64],
940    upper_2618: &mut [f64],
941    lower_0618: &mut [f64],
942    lower_1000: &mut [f64],
943    lower_1618: &mut [f64],
944    lower_2618: &mut [f64],
945    tp_long_band: &mut [f64],
946    tp_short_band: &mut [f64],
947    long_entry: &mut [f64],
948    short_entry: &mut [f64],
949    rejection_long: &mut [f64],
950    rejection_short: &mut [f64],
951    long_bounce: &mut [f64],
952    short_bounce: &mut [f64],
953) {
954    basis[idx] = point.basis;
955    trend[idx] = point.trend;
956    upper_0618[idx] = point.upper_0618;
957    upper_1000[idx] = point.upper_1000;
958    upper_1618[idx] = point.upper_1618;
959    upper_2618[idx] = point.upper_2618;
960    lower_0618[idx] = point.lower_0618;
961    lower_1000[idx] = point.lower_1000;
962    lower_1618[idx] = point.lower_1618;
963    lower_2618[idx] = point.lower_2618;
964    tp_long_band[idx] = point.tp_long_band;
965    tp_short_band[idx] = point.tp_short_band;
966    long_entry[idx] = point.long_entry;
967    short_entry[idx] = point.short_entry;
968    rejection_long[idx] = point.rejection_long;
969    rejection_short[idx] = point.rejection_short;
970    long_bounce[idx] = point.long_bounce;
971    short_bounce[idx] = point.short_bounce;
972}
973
974#[inline]
975#[allow(clippy::too_many_arguments)]
976fn fibonacci_entry_bands_row_from_slices(
977    open: &[f64],
978    high: &[f64],
979    low: &[f64],
980    close: &[f64],
981    params: ResolvedParams,
982    basis: &mut [f64],
983    trend: &mut [f64],
984    upper_0618: &mut [f64],
985    upper_1000: &mut [f64],
986    upper_1618: &mut [f64],
987    upper_2618: &mut [f64],
988    lower_0618: &mut [f64],
989    lower_1000: &mut [f64],
990    lower_1618: &mut [f64],
991    lower_2618: &mut [f64],
992    tp_long_band: &mut [f64],
993    tp_short_band: &mut [f64],
994    long_entry: &mut [f64],
995    short_entry: &mut [f64],
996    rejection_long: &mut [f64],
997    rejection_short: &mut [f64],
998    long_bounce: &mut [f64],
999    short_bounce: &mut [f64],
1000) {
1001    let mut stream = FibonacciEntryBandsStream::new_resolved(params);
1002    for i in 0..close.len() {
1003        let point = stream
1004            .update(open[i], high[i], low[i], close[i])
1005            .unwrap_or_else(FibonacciEntryBandsPoint::nan);
1006        write_point(
1007            point,
1008            i,
1009            basis,
1010            trend,
1011            upper_0618,
1012            upper_1000,
1013            upper_1618,
1014            upper_2618,
1015            lower_0618,
1016            lower_1000,
1017            lower_1618,
1018            lower_2618,
1019            tp_long_band,
1020            tp_short_band,
1021            long_entry,
1022            short_entry,
1023            rejection_long,
1024            rejection_short,
1025            long_bounce,
1026            short_bounce,
1027        );
1028    }
1029}
1030
1031#[inline]
1032fn fibonacci_entry_bands_prepare<'a>(
1033    input: &'a FibonacciEntryBandsInput,
1034    kernel: Kernel,
1035) -> Result<
1036    (
1037        &'a [f64],
1038        &'a [f64],
1039        &'a [f64],
1040        &'a [f64],
1041        ResolvedParams,
1042        Kernel,
1043    ),
1044    FibonacciEntryBandsError,
1045> {
1046    let (open, high, low, close) = input.as_slices();
1047    if open.is_empty() || high.is_empty() || low.is_empty() || close.is_empty() {
1048        return Err(FibonacciEntryBandsError::EmptyInputData);
1049    }
1050    if open.len() != high.len() || open.len() != low.len() || open.len() != close.len() {
1051        return Err(FibonacciEntryBandsError::InconsistentSliceLengths {
1052            open_len: open.len(),
1053            high_len: high.len(),
1054            low_len: low.len(),
1055            close_len: close.len(),
1056        });
1057    }
1058
1059    let params = resolve_params(&input.params, Some(close.len()))?;
1060    let first = first_valid_ohlc(params.source, open, high, low, close);
1061    if first >= close.len() {
1062        return Err(FibonacciEntryBandsError::AllValuesNaN);
1063    }
1064
1065    let valid = max_consecutive_valid_ohlc(params.source, open, high, low, close);
1066    let needed = if params.use_atr {
1067        params.atr_length
1068    } else {
1069        params.length
1070    };
1071    if valid < needed {
1072        return Err(FibonacciEntryBandsError::NotEnoughValidData { needed, valid });
1073    }
1074
1075    let chosen = match kernel {
1076        Kernel::Auto => detect_best_kernel(),
1077        other => other.to_non_batch(),
1078    };
1079    Ok((open, high, low, close, params, chosen))
1080}
1081
1082#[inline]
1083pub fn fibonacci_entry_bands(
1084    input: &FibonacciEntryBandsInput,
1085) -> Result<FibonacciEntryBandsOutput, FibonacciEntryBandsError> {
1086    fibonacci_entry_bands_with_kernel(input, Kernel::Auto)
1087}
1088
1089#[inline]
1090pub fn fibonacci_entry_bands_with_kernel(
1091    input: &FibonacciEntryBandsInput,
1092    kernel: Kernel,
1093) -> Result<FibonacciEntryBandsOutput, FibonacciEntryBandsError> {
1094    let (open, high, low, close, params, _chosen) = fibonacci_entry_bands_prepare(input, kernel)?;
1095    let len = close.len();
1096    let mut out = FibonacciEntryBandsOutput {
1097        basis: full_nan_vec(len),
1098        trend: full_nan_vec(len),
1099        upper_0618: full_nan_vec(len),
1100        upper_1000: full_nan_vec(len),
1101        upper_1618: full_nan_vec(len),
1102        upper_2618: full_nan_vec(len),
1103        lower_0618: full_nan_vec(len),
1104        lower_1000: full_nan_vec(len),
1105        lower_1618: full_nan_vec(len),
1106        lower_2618: full_nan_vec(len),
1107        tp_long_band: full_nan_vec(len),
1108        tp_short_band: full_nan_vec(len),
1109        long_entry: full_nan_vec(len),
1110        short_entry: full_nan_vec(len),
1111        rejection_long: full_nan_vec(len),
1112        rejection_short: full_nan_vec(len),
1113        long_bounce: full_nan_vec(len),
1114        short_bounce: full_nan_vec(len),
1115    };
1116    fibonacci_entry_bands_row_from_slices(
1117        open,
1118        high,
1119        low,
1120        close,
1121        params,
1122        &mut out.basis,
1123        &mut out.trend,
1124        &mut out.upper_0618,
1125        &mut out.upper_1000,
1126        &mut out.upper_1618,
1127        &mut out.upper_2618,
1128        &mut out.lower_0618,
1129        &mut out.lower_1000,
1130        &mut out.lower_1618,
1131        &mut out.lower_2618,
1132        &mut out.tp_long_band,
1133        &mut out.tp_short_band,
1134        &mut out.long_entry,
1135        &mut out.short_entry,
1136        &mut out.rejection_long,
1137        &mut out.rejection_short,
1138        &mut out.long_bounce,
1139        &mut out.short_bounce,
1140    );
1141    Ok(out)
1142}
1143
1144#[inline]
1145#[allow(clippy::too_many_arguments)]
1146pub fn fibonacci_entry_bands_into_slices(
1147    basis: &mut [f64],
1148    trend: &mut [f64],
1149    upper_0618: &mut [f64],
1150    upper_1000: &mut [f64],
1151    upper_1618: &mut [f64],
1152    upper_2618: &mut [f64],
1153    lower_0618: &mut [f64],
1154    lower_1000: &mut [f64],
1155    lower_1618: &mut [f64],
1156    lower_2618: &mut [f64],
1157    tp_long_band: &mut [f64],
1158    tp_short_band: &mut [f64],
1159    long_entry: &mut [f64],
1160    short_entry: &mut [f64],
1161    rejection_long: &mut [f64],
1162    rejection_short: &mut [f64],
1163    long_bounce: &mut [f64],
1164    short_bounce: &mut [f64],
1165    input: &FibonacciEntryBandsInput,
1166    kernel: Kernel,
1167) -> Result<(), FibonacciEntryBandsError> {
1168    let expected = input.as_slices().3.len();
1169    if basis.len() != expected
1170        || trend.len() != expected
1171        || upper_0618.len() != expected
1172        || upper_1000.len() != expected
1173        || upper_1618.len() != expected
1174        || upper_2618.len() != expected
1175        || lower_0618.len() != expected
1176        || lower_1000.len() != expected
1177        || lower_1618.len() != expected
1178        || lower_2618.len() != expected
1179        || tp_long_band.len() != expected
1180        || tp_short_band.len() != expected
1181        || long_entry.len() != expected
1182        || short_entry.len() != expected
1183        || rejection_long.len() != expected
1184        || rejection_short.len() != expected
1185        || long_bounce.len() != expected
1186        || short_bounce.len() != expected
1187    {
1188        return Err(FibonacciEntryBandsError::OutputLengthMismatch { expected });
1189    }
1190
1191    let (open, high, low, close, params, _chosen) = fibonacci_entry_bands_prepare(input, kernel)?;
1192    fibonacci_entry_bands_row_from_slices(
1193        open,
1194        high,
1195        low,
1196        close,
1197        params,
1198        basis,
1199        trend,
1200        upper_0618,
1201        upper_1000,
1202        upper_1618,
1203        upper_2618,
1204        lower_0618,
1205        lower_1000,
1206        lower_1618,
1207        lower_2618,
1208        tp_long_band,
1209        tp_short_band,
1210        long_entry,
1211        short_entry,
1212        rejection_long,
1213        rejection_short,
1214        long_bounce,
1215        short_bounce,
1216    );
1217    Ok(())
1218}
1219
1220#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1221#[inline]
1222#[allow(clippy::too_many_arguments)]
1223pub fn fibonacci_entry_bands_into(
1224    input: &FibonacciEntryBandsInput,
1225    basis: &mut [f64],
1226    trend: &mut [f64],
1227    upper_0618: &mut [f64],
1228    upper_1000: &mut [f64],
1229    upper_1618: &mut [f64],
1230    upper_2618: &mut [f64],
1231    lower_0618: &mut [f64],
1232    lower_1000: &mut [f64],
1233    lower_1618: &mut [f64],
1234    lower_2618: &mut [f64],
1235    tp_long_band: &mut [f64],
1236    tp_short_band: &mut [f64],
1237    long_entry: &mut [f64],
1238    short_entry: &mut [f64],
1239    rejection_long: &mut [f64],
1240    rejection_short: &mut [f64],
1241    long_bounce: &mut [f64],
1242    short_bounce: &mut [f64],
1243) -> Result<(), FibonacciEntryBandsError> {
1244    fibonacci_entry_bands_into_slices(
1245        basis,
1246        trend,
1247        upper_0618,
1248        upper_1000,
1249        upper_1618,
1250        upper_2618,
1251        lower_0618,
1252        lower_1000,
1253        lower_1618,
1254        lower_2618,
1255        tp_long_band,
1256        tp_short_band,
1257        long_entry,
1258        short_entry,
1259        rejection_long,
1260        rejection_short,
1261        long_bounce,
1262        short_bounce,
1263        input,
1264        Kernel::Auto,
1265    )
1266}
1267
1268#[derive(Debug, Clone, PartialEq)]
1269#[cfg_attr(
1270    all(target_arch = "wasm32", feature = "wasm"),
1271    derive(Serialize, Deserialize)
1272)]
1273pub struct FibonacciEntryBandsBatchRange {
1274    pub length: (usize, usize, usize),
1275    pub atr_length: (usize, usize, usize),
1276    pub source: String,
1277    pub use_atr: bool,
1278    pub tp_aggressiveness: String,
1279}
1280
1281impl Default for FibonacciEntryBandsBatchRange {
1282    fn default() -> Self {
1283        Self {
1284            length: (DEFAULT_LENGTH, DEFAULT_LENGTH, 0),
1285            atr_length: (DEFAULT_ATR_LENGTH, DEFAULT_ATR_LENGTH, 0),
1286            source: DEFAULT_SOURCE.to_string(),
1287            use_atr: DEFAULT_USE_ATR,
1288            tp_aggressiveness: DEFAULT_TP_AGGRESSIVENESS.to_string(),
1289        }
1290    }
1291}
1292
1293#[derive(Debug, Clone)]
1294pub struct FibonacciEntryBandsBatchOutput {
1295    pub basis: Vec<f64>,
1296    pub trend: Vec<f64>,
1297    pub upper_0618: Vec<f64>,
1298    pub upper_1000: Vec<f64>,
1299    pub upper_1618: Vec<f64>,
1300    pub upper_2618: Vec<f64>,
1301    pub lower_0618: Vec<f64>,
1302    pub lower_1000: Vec<f64>,
1303    pub lower_1618: Vec<f64>,
1304    pub lower_2618: Vec<f64>,
1305    pub tp_long_band: Vec<f64>,
1306    pub tp_short_band: Vec<f64>,
1307    pub long_entry: Vec<f64>,
1308    pub short_entry: Vec<f64>,
1309    pub rejection_long: Vec<f64>,
1310    pub rejection_short: Vec<f64>,
1311    pub long_bounce: Vec<f64>,
1312    pub short_bounce: Vec<f64>,
1313    pub combos: Vec<FibonacciEntryBandsParams>,
1314    pub rows: usize,
1315    pub cols: usize,
1316}
1317
1318#[derive(Clone, Debug, Default)]
1319pub struct FibonacciEntryBandsBatchBuilder {
1320    range: FibonacciEntryBandsBatchRange,
1321    kernel: Kernel,
1322}
1323
1324impl FibonacciEntryBandsBatchBuilder {
1325    #[inline]
1326    pub fn new() -> Self {
1327        Self::default()
1328    }
1329
1330    #[inline]
1331    pub fn kernel(mut self, kernel: Kernel) -> Self {
1332        self.kernel = kernel;
1333        self
1334    }
1335
1336    #[inline]
1337    pub fn length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1338        self.range.length = (start, end, step);
1339        self
1340    }
1341
1342    #[inline]
1343    pub fn atr_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1344        self.range.atr_length = (start, end, step);
1345        self
1346    }
1347
1348    #[inline]
1349    pub fn source(mut self, value: &str) -> Self {
1350        self.range.source = value.to_string();
1351        self
1352    }
1353
1354    #[inline]
1355    pub fn use_atr(mut self, value: bool) -> Self {
1356        self.range.use_atr = value;
1357        self
1358    }
1359
1360    #[inline]
1361    pub fn tp_aggressiveness(mut self, value: &str) -> Self {
1362        self.range.tp_aggressiveness = value.to_string();
1363        self
1364    }
1365
1366    #[inline]
1367    pub fn apply_slices(
1368        self,
1369        open: &[f64],
1370        high: &[f64],
1371        low: &[f64],
1372        close: &[f64],
1373    ) -> Result<FibonacciEntryBandsBatchOutput, FibonacciEntryBandsError> {
1374        fibonacci_entry_bands_batch_with_kernel(open, high, low, close, &self.range, self.kernel)
1375    }
1376
1377    #[inline]
1378    pub fn apply_candles(
1379        self,
1380        candles: &Candles,
1381    ) -> Result<FibonacciEntryBandsBatchOutput, FibonacciEntryBandsError> {
1382        fibonacci_entry_bands_batch_with_kernel(
1383            &candles.open,
1384            &candles.high,
1385            &candles.low,
1386            &candles.close,
1387            &self.range,
1388            self.kernel,
1389        )
1390    }
1391}
1392
1393#[inline]
1394fn expand_usize_range(
1395    name: &str,
1396    range: (usize, usize, usize),
1397) -> Result<Vec<usize>, FibonacciEntryBandsError> {
1398    let (start, end, step) = range;
1399    if start > end {
1400        return Err(FibonacciEntryBandsError::InvalidRange {
1401            start: format!("{name}={start}"),
1402            end: format!("{name}={end}"),
1403            step: format!("{name}={step}"),
1404        });
1405    }
1406    if start == end {
1407        return Ok(vec![start]);
1408    }
1409    if step == 0 {
1410        return Err(FibonacciEntryBandsError::InvalidRange {
1411            start: format!("{name}={start}"),
1412            end: format!("{name}={end}"),
1413            step: format!("{name}={step}"),
1414        });
1415    }
1416    let mut out = Vec::new();
1417    let mut value = start;
1418    while value <= end {
1419        out.push(value);
1420        match value.checked_add(step) {
1421            Some(next) if next > value => value = next,
1422            _ => break,
1423        }
1424    }
1425    Ok(out)
1426}
1427
1428#[inline]
1429fn expand_grid(
1430    sweep: &FibonacciEntryBandsBatchRange,
1431) -> Result<Vec<FibonacciEntryBandsParams>, FibonacciEntryBandsError> {
1432    let _ = parse_source(&sweep.source)?;
1433    let _ = parse_tp_aggressiveness(&sweep.tp_aggressiveness)?;
1434    let lengths = expand_usize_range("length", sweep.length)?;
1435    let atr_lengths = expand_usize_range("atr_length", sweep.atr_length)?;
1436    let mut combos = Vec::with_capacity(lengths.len().saturating_mul(atr_lengths.len()));
1437    for &length in &lengths {
1438        for &atr_length in &atr_lengths {
1439            combos.push(FibonacciEntryBandsParams {
1440                source: Some(sweep.source.clone()),
1441                length: Some(length),
1442                atr_length: Some(atr_length),
1443                use_atr: Some(sweep.use_atr),
1444                tp_aggressiveness: Some(sweep.tp_aggressiveness.clone()),
1445            });
1446        }
1447    }
1448    Ok(combos)
1449}
1450
1451#[inline(always)]
1452unsafe fn assume_init_vec(
1453    mut guard: ManuallyDrop<Vec<MaybeUninit<f64>>>,
1454    total: usize,
1455) -> Vec<f64> {
1456    Vec::from_raw_parts(guard.as_mut_ptr() as *mut f64, total, guard.capacity())
1457}
1458
1459#[inline]
1460fn validate_batch_kernel(kernel: Kernel) -> Result<Kernel, FibonacciEntryBandsError> {
1461    let chosen = match kernel {
1462        Kernel::Auto => detect_best_batch_kernel(),
1463        other => other,
1464    };
1465    match chosen {
1466        Kernel::Auto
1467        | Kernel::ScalarBatch
1468        | Kernel::Avx2Batch
1469        | Kernel::Avx512Batch
1470        | Kernel::Scalar
1471        | Kernel::Avx2
1472        | Kernel::Avx512 => Ok(chosen.to_non_batch()),
1473        other => Err(FibonacciEntryBandsError::InvalidKernelForBatch(other)),
1474    }
1475}
1476
1477#[inline]
1478#[allow(clippy::too_many_arguments)]
1479pub fn fibonacci_entry_bands_batch_with_kernel(
1480    open: &[f64],
1481    high: &[f64],
1482    low: &[f64],
1483    close: &[f64],
1484    sweep: &FibonacciEntryBandsBatchRange,
1485    kernel: Kernel,
1486) -> Result<FibonacciEntryBandsBatchOutput, FibonacciEntryBandsError> {
1487    let resolved = expand_grid(sweep)?;
1488    if open.is_empty() || high.is_empty() || low.is_empty() || close.is_empty() {
1489        return Err(FibonacciEntryBandsError::EmptyInputData);
1490    }
1491    if open.len() != high.len() || open.len() != low.len() || open.len() != close.len() {
1492        return Err(FibonacciEntryBandsError::InconsistentSliceLengths {
1493            open_len: open.len(),
1494            high_len: high.len(),
1495            low_len: low.len(),
1496            close_len: close.len(),
1497        });
1498    }
1499    let rows = resolved.len();
1500    let cols = close.len();
1501    let total = rows
1502        .checked_mul(cols)
1503        .ok_or(FibonacciEntryBandsError::OutputLengthMismatch {
1504            expected: usize::MAX,
1505        })?;
1506    let _kernel = validate_batch_kernel(kernel)?;
1507    let zero_prefixes = vec![0usize; rows];
1508
1509    macro_rules! alloc_matrix {
1510        ($name:ident, $guard:ident, $out:ident) => {
1511            let mut $name = make_uninit_matrix(rows, cols);
1512            init_matrix_prefixes(&mut $name, cols, &zero_prefixes);
1513            let mut $guard = ManuallyDrop::new($name);
1514            let $out =
1515                unsafe { std::slice::from_raw_parts_mut($guard.as_mut_ptr() as *mut f64, total) };
1516        };
1517    }
1518
1519    alloc_matrix!(basis_mu, basis_guard, basis_out);
1520    alloc_matrix!(trend_mu, trend_guard, trend_out);
1521    alloc_matrix!(upper_0618_mu, upper_0618_guard, upper_0618_out);
1522    alloc_matrix!(upper_1000_mu, upper_1000_guard, upper_1000_out);
1523    alloc_matrix!(upper_1618_mu, upper_1618_guard, upper_1618_out);
1524    alloc_matrix!(upper_2618_mu, upper_2618_guard, upper_2618_out);
1525    alloc_matrix!(lower_0618_mu, lower_0618_guard, lower_0618_out);
1526    alloc_matrix!(lower_1000_mu, lower_1000_guard, lower_1000_out);
1527    alloc_matrix!(lower_1618_mu, lower_1618_guard, lower_1618_out);
1528    alloc_matrix!(lower_2618_mu, lower_2618_guard, lower_2618_out);
1529    alloc_matrix!(tp_long_mu, tp_long_guard, tp_long_out);
1530    alloc_matrix!(tp_short_mu, tp_short_guard, tp_short_out);
1531    alloc_matrix!(long_entry_mu, long_entry_guard, long_entry_out);
1532    alloc_matrix!(short_entry_mu, short_entry_guard, short_entry_out);
1533    alloc_matrix!(rejection_long_mu, rejection_long_guard, rejection_long_out);
1534    alloc_matrix!(
1535        rejection_short_mu,
1536        rejection_short_guard,
1537        rejection_short_out
1538    );
1539    alloc_matrix!(long_bounce_mu, long_bounce_guard, long_bounce_out);
1540    alloc_matrix!(short_bounce_mu, short_bounce_guard, short_bounce_out);
1541
1542    #[cfg(not(target_arch = "wasm32"))]
1543    {
1544        let basis_ptr = basis_out.as_mut_ptr() as usize;
1545        let trend_ptr = trend_out.as_mut_ptr() as usize;
1546        let upper_0618_ptr = upper_0618_out.as_mut_ptr() as usize;
1547        let upper_1000_ptr = upper_1000_out.as_mut_ptr() as usize;
1548        let upper_1618_ptr = upper_1618_out.as_mut_ptr() as usize;
1549        let upper_2618_ptr = upper_2618_out.as_mut_ptr() as usize;
1550        let lower_0618_ptr = lower_0618_out.as_mut_ptr() as usize;
1551        let lower_1000_ptr = lower_1000_out.as_mut_ptr() as usize;
1552        let lower_1618_ptr = lower_1618_out.as_mut_ptr() as usize;
1553        let lower_2618_ptr = lower_2618_out.as_mut_ptr() as usize;
1554        let tp_long_ptr = tp_long_out.as_mut_ptr() as usize;
1555        let tp_short_ptr = tp_short_out.as_mut_ptr() as usize;
1556        let long_entry_ptr = long_entry_out.as_mut_ptr() as usize;
1557        let short_entry_ptr = short_entry_out.as_mut_ptr() as usize;
1558        let rejection_long_ptr = rejection_long_out.as_mut_ptr() as usize;
1559        let rejection_short_ptr = rejection_short_out.as_mut_ptr() as usize;
1560        let long_bounce_ptr = long_bounce_out.as_mut_ptr() as usize;
1561        let short_bounce_ptr = short_bounce_out.as_mut_ptr() as usize;
1562
1563        resolved
1564            .par_iter()
1565            .enumerate()
1566            .for_each(|(row, combo)| unsafe {
1567                let start = row * cols;
1568                let params = resolve_params(combo, Some(cols)).expect("validated");
1569                fibonacci_entry_bands_row_from_slices(
1570                    open,
1571                    high,
1572                    low,
1573                    close,
1574                    params,
1575                    &mut std::slice::from_raw_parts_mut(basis_ptr as *mut f64, total)
1576                        [start..start + cols],
1577                    &mut std::slice::from_raw_parts_mut(trend_ptr as *mut f64, total)
1578                        [start..start + cols],
1579                    &mut std::slice::from_raw_parts_mut(upper_0618_ptr as *mut f64, total)
1580                        [start..start + cols],
1581                    &mut std::slice::from_raw_parts_mut(upper_1000_ptr as *mut f64, total)
1582                        [start..start + cols],
1583                    &mut std::slice::from_raw_parts_mut(upper_1618_ptr as *mut f64, total)
1584                        [start..start + cols],
1585                    &mut std::slice::from_raw_parts_mut(upper_2618_ptr as *mut f64, total)
1586                        [start..start + cols],
1587                    &mut std::slice::from_raw_parts_mut(lower_0618_ptr as *mut f64, total)
1588                        [start..start + cols],
1589                    &mut std::slice::from_raw_parts_mut(lower_1000_ptr as *mut f64, total)
1590                        [start..start + cols],
1591                    &mut std::slice::from_raw_parts_mut(lower_1618_ptr as *mut f64, total)
1592                        [start..start + cols],
1593                    &mut std::slice::from_raw_parts_mut(lower_2618_ptr as *mut f64, total)
1594                        [start..start + cols],
1595                    &mut std::slice::from_raw_parts_mut(tp_long_ptr as *mut f64, total)
1596                        [start..start + cols],
1597                    &mut std::slice::from_raw_parts_mut(tp_short_ptr as *mut f64, total)
1598                        [start..start + cols],
1599                    &mut std::slice::from_raw_parts_mut(long_entry_ptr as *mut f64, total)
1600                        [start..start + cols],
1601                    &mut std::slice::from_raw_parts_mut(short_entry_ptr as *mut f64, total)
1602                        [start..start + cols],
1603                    &mut std::slice::from_raw_parts_mut(rejection_long_ptr as *mut f64, total)
1604                        [start..start + cols],
1605                    &mut std::slice::from_raw_parts_mut(rejection_short_ptr as *mut f64, total)
1606                        [start..start + cols],
1607                    &mut std::slice::from_raw_parts_mut(long_bounce_ptr as *mut f64, total)
1608                        [start..start + cols],
1609                    &mut std::slice::from_raw_parts_mut(short_bounce_ptr as *mut f64, total)
1610                        [start..start + cols],
1611                );
1612            });
1613    }
1614
1615    #[cfg(target_arch = "wasm32")]
1616    {
1617        for (row, combo) in resolved.iter().enumerate() {
1618            let start = row * cols;
1619            let params = resolve_params(combo, Some(cols))?;
1620            fibonacci_entry_bands_row_from_slices(
1621                open,
1622                high,
1623                low,
1624                close,
1625                params,
1626                &mut basis_out[start..start + cols],
1627                &mut trend_out[start..start + cols],
1628                &mut upper_0618_out[start..start + cols],
1629                &mut upper_1000_out[start..start + cols],
1630                &mut upper_1618_out[start..start + cols],
1631                &mut upper_2618_out[start..start + cols],
1632                &mut lower_0618_out[start..start + cols],
1633                &mut lower_1000_out[start..start + cols],
1634                &mut lower_1618_out[start..start + cols],
1635                &mut lower_2618_out[start..start + cols],
1636                &mut tp_long_out[start..start + cols],
1637                &mut tp_short_out[start..start + cols],
1638                &mut long_entry_out[start..start + cols],
1639                &mut short_entry_out[start..start + cols],
1640                &mut rejection_long_out[start..start + cols],
1641                &mut rejection_short_out[start..start + cols],
1642                &mut long_bounce_out[start..start + cols],
1643                &mut short_bounce_out[start..start + cols],
1644            );
1645        }
1646    }
1647
1648    Ok(FibonacciEntryBandsBatchOutput {
1649        basis: unsafe { assume_init_vec(basis_guard, total) },
1650        trend: unsafe { assume_init_vec(trend_guard, total) },
1651        upper_0618: unsafe { assume_init_vec(upper_0618_guard, total) },
1652        upper_1000: unsafe { assume_init_vec(upper_1000_guard, total) },
1653        upper_1618: unsafe { assume_init_vec(upper_1618_guard, total) },
1654        upper_2618: unsafe { assume_init_vec(upper_2618_guard, total) },
1655        lower_0618: unsafe { assume_init_vec(lower_0618_guard, total) },
1656        lower_1000: unsafe { assume_init_vec(lower_1000_guard, total) },
1657        lower_1618: unsafe { assume_init_vec(lower_1618_guard, total) },
1658        lower_2618: unsafe { assume_init_vec(lower_2618_guard, total) },
1659        tp_long_band: unsafe { assume_init_vec(tp_long_guard, total) },
1660        tp_short_band: unsafe { assume_init_vec(tp_short_guard, total) },
1661        long_entry: unsafe { assume_init_vec(long_entry_guard, total) },
1662        short_entry: unsafe { assume_init_vec(short_entry_guard, total) },
1663        rejection_long: unsafe { assume_init_vec(rejection_long_guard, total) },
1664        rejection_short: unsafe { assume_init_vec(rejection_short_guard, total) },
1665        long_bounce: unsafe { assume_init_vec(long_bounce_guard, total) },
1666        short_bounce: unsafe { assume_init_vec(short_bounce_guard, total) },
1667        combos: resolved,
1668        rows,
1669        cols,
1670    })
1671}
1672
1673#[inline]
1674#[allow(clippy::too_many_arguments)]
1675pub fn fibonacci_entry_bands_batch_inner_into(
1676    open: &[f64],
1677    high: &[f64],
1678    low: &[f64],
1679    close: &[f64],
1680    sweep: &FibonacciEntryBandsBatchRange,
1681    kernel: Kernel,
1682    basis: &mut [f64],
1683    trend: &mut [f64],
1684    upper_0618: &mut [f64],
1685    upper_1000: &mut [f64],
1686    upper_1618: &mut [f64],
1687    upper_2618: &mut [f64],
1688    lower_0618: &mut [f64],
1689    lower_1000: &mut [f64],
1690    lower_1618: &mut [f64],
1691    lower_2618: &mut [f64],
1692    tp_long_band: &mut [f64],
1693    tp_short_band: &mut [f64],
1694    long_entry: &mut [f64],
1695    short_entry: &mut [f64],
1696    rejection_long: &mut [f64],
1697    rejection_short: &mut [f64],
1698    long_bounce: &mut [f64],
1699    short_bounce: &mut [f64],
1700) -> Result<Vec<FibonacciEntryBandsParams>, FibonacciEntryBandsError> {
1701    let out = fibonacci_entry_bands_batch_with_kernel(open, high, low, close, sweep, kernel)?;
1702    let total = out.rows * out.cols;
1703    if basis.len() != total
1704        || trend.len() != total
1705        || upper_0618.len() != total
1706        || upper_1000.len() != total
1707        || upper_1618.len() != total
1708        || upper_2618.len() != total
1709        || lower_0618.len() != total
1710        || lower_1000.len() != total
1711        || lower_1618.len() != total
1712        || lower_2618.len() != total
1713        || tp_long_band.len() != total
1714        || tp_short_band.len() != total
1715        || long_entry.len() != total
1716        || short_entry.len() != total
1717        || rejection_long.len() != total
1718        || rejection_short.len() != total
1719        || long_bounce.len() != total
1720        || short_bounce.len() != total
1721    {
1722        return Err(FibonacciEntryBandsError::OutputLengthMismatch { expected: total });
1723    }
1724
1725    basis.copy_from_slice(&out.basis);
1726    trend.copy_from_slice(&out.trend);
1727    upper_0618.copy_from_slice(&out.upper_0618);
1728    upper_1000.copy_from_slice(&out.upper_1000);
1729    upper_1618.copy_from_slice(&out.upper_1618);
1730    upper_2618.copy_from_slice(&out.upper_2618);
1731    lower_0618.copy_from_slice(&out.lower_0618);
1732    lower_1000.copy_from_slice(&out.lower_1000);
1733    lower_1618.copy_from_slice(&out.lower_1618);
1734    lower_2618.copy_from_slice(&out.lower_2618);
1735    tp_long_band.copy_from_slice(&out.tp_long_band);
1736    tp_short_band.copy_from_slice(&out.tp_short_band);
1737    long_entry.copy_from_slice(&out.long_entry);
1738    short_entry.copy_from_slice(&out.short_entry);
1739    rejection_long.copy_from_slice(&out.rejection_long);
1740    rejection_short.copy_from_slice(&out.rejection_short);
1741    long_bounce.copy_from_slice(&out.long_bounce);
1742    short_bounce.copy_from_slice(&out.short_bounce);
1743    Ok(out.combos)
1744}
1745
1746#[cfg(feature = "python")]
1747fn output_to_py_dict<'py>(
1748    py: Python<'py>,
1749    output: FibonacciEntryBandsOutput,
1750) -> PyResult<Bound<'py, PyDict>> {
1751    let dict = PyDict::new(py);
1752    dict.set_item("basis", output.basis.into_pyarray(py))?;
1753    dict.set_item("trend", output.trend.into_pyarray(py))?;
1754    dict.set_item("upper_0618", output.upper_0618.into_pyarray(py))?;
1755    dict.set_item("upper_1000", output.upper_1000.into_pyarray(py))?;
1756    dict.set_item("upper_1618", output.upper_1618.into_pyarray(py))?;
1757    dict.set_item("upper_2618", output.upper_2618.into_pyarray(py))?;
1758    dict.set_item("lower_0618", output.lower_0618.into_pyarray(py))?;
1759    dict.set_item("lower_1000", output.lower_1000.into_pyarray(py))?;
1760    dict.set_item("lower_1618", output.lower_1618.into_pyarray(py))?;
1761    dict.set_item("lower_2618", output.lower_2618.into_pyarray(py))?;
1762    dict.set_item("tp_long_band", output.tp_long_band.into_pyarray(py))?;
1763    dict.set_item("tp_short_band", output.tp_short_band.into_pyarray(py))?;
1764    dict.set_item("long_entry", output.long_entry.into_pyarray(py))?;
1765    dict.set_item("short_entry", output.short_entry.into_pyarray(py))?;
1766    dict.set_item("rejection_long", output.rejection_long.into_pyarray(py))?;
1767    dict.set_item("rejection_short", output.rejection_short.into_pyarray(py))?;
1768    dict.set_item("long_bounce", output.long_bounce.into_pyarray(py))?;
1769    dict.set_item("short_bounce", output.short_bounce.into_pyarray(py))?;
1770    Ok(dict)
1771}
1772
1773#[cfg(feature = "python")]
1774fn point_to_py_dict<'py>(
1775    py: Python<'py>,
1776    point: FibonacciEntryBandsPoint,
1777) -> PyResult<Bound<'py, PyDict>> {
1778    let dict = PyDict::new(py);
1779    dict.set_item("basis", point.basis)?;
1780    dict.set_item("trend", point.trend)?;
1781    dict.set_item("upper_0618", point.upper_0618)?;
1782    dict.set_item("upper_1000", point.upper_1000)?;
1783    dict.set_item("upper_1618", point.upper_1618)?;
1784    dict.set_item("upper_2618", point.upper_2618)?;
1785    dict.set_item("lower_0618", point.lower_0618)?;
1786    dict.set_item("lower_1000", point.lower_1000)?;
1787    dict.set_item("lower_1618", point.lower_1618)?;
1788    dict.set_item("lower_2618", point.lower_2618)?;
1789    dict.set_item("tp_long_band", point.tp_long_band)?;
1790    dict.set_item("tp_short_band", point.tp_short_band)?;
1791    dict.set_item("long_entry", point.long_entry)?;
1792    dict.set_item("short_entry", point.short_entry)?;
1793    dict.set_item("rejection_long", point.rejection_long)?;
1794    dict.set_item("rejection_short", point.rejection_short)?;
1795    dict.set_item("long_bounce", point.long_bounce)?;
1796    dict.set_item("short_bounce", point.short_bounce)?;
1797    Ok(dict)
1798}
1799
1800#[cfg(feature = "python")]
1801#[pyclass(name = "FibonacciEntryBandsStream")]
1802pub struct FibonacciEntryBandsStreamPy {
1803    stream: FibonacciEntryBandsStream,
1804}
1805
1806#[cfg(feature = "python")]
1807#[pymethods]
1808impl FibonacciEntryBandsStreamPy {
1809    #[new]
1810    #[pyo3(signature = (source=DEFAULT_SOURCE, length=DEFAULT_LENGTH, atr_length=DEFAULT_ATR_LENGTH, use_atr=DEFAULT_USE_ATR, tp_aggressiveness=DEFAULT_TP_AGGRESSIVENESS))]
1811    fn new(
1812        source: &str,
1813        length: usize,
1814        atr_length: usize,
1815        use_atr: bool,
1816        tp_aggressiveness: &str,
1817    ) -> PyResult<Self> {
1818        Ok(Self {
1819            stream: FibonacciEntryBandsStream::try_new(FibonacciEntryBandsParams {
1820                source: Some(source.to_string()),
1821                length: Some(length),
1822                atr_length: Some(atr_length),
1823                use_atr: Some(use_atr),
1824                tp_aggressiveness: Some(tp_aggressiveness.to_string()),
1825            })
1826            .map_err(|e| PyValueError::new_err(e.to_string()))?,
1827        })
1828    }
1829
1830    fn update<'py>(
1831        &mut self,
1832        py: Python<'py>,
1833        open: f64,
1834        high: f64,
1835        low: f64,
1836        close: f64,
1837    ) -> PyResult<Option<Bound<'py, PyDict>>> {
1838        self.stream
1839            .update(open, high, low, close)
1840            .map(|point| point_to_py_dict(py, point))
1841            .transpose()
1842    }
1843
1844    fn reset(&mut self) {
1845        self.stream.reset();
1846    }
1847
1848    #[getter]
1849    fn warmup_period(&self) -> usize {
1850        self.stream.get_warmup_period()
1851    }
1852}
1853
1854#[cfg(feature = "python")]
1855#[pyfunction(name = "fibonacci_entry_bands")]
1856#[pyo3(signature = (open, high, low, close, source=DEFAULT_SOURCE, length=DEFAULT_LENGTH, atr_length=DEFAULT_ATR_LENGTH, use_atr=DEFAULT_USE_ATR, tp_aggressiveness=DEFAULT_TP_AGGRESSIVENESS, kernel=None))]
1857pub fn fibonacci_entry_bands_py<'py>(
1858    py: Python<'py>,
1859    open: PyReadonlyArray1<'py, f64>,
1860    high: PyReadonlyArray1<'py, f64>,
1861    low: PyReadonlyArray1<'py, f64>,
1862    close: PyReadonlyArray1<'py, f64>,
1863    source: &str,
1864    length: usize,
1865    atr_length: usize,
1866    use_atr: bool,
1867    tp_aggressiveness: &str,
1868    kernel: Option<&str>,
1869) -> PyResult<Bound<'py, PyDict>> {
1870    let input = FibonacciEntryBandsInput::from_slices(
1871        open.as_slice()?,
1872        high.as_slice()?,
1873        low.as_slice()?,
1874        close.as_slice()?,
1875        FibonacciEntryBandsParams {
1876            source: Some(source.to_string()),
1877            length: Some(length),
1878            atr_length: Some(atr_length),
1879            use_atr: Some(use_atr),
1880            tp_aggressiveness: Some(tp_aggressiveness.to_string()),
1881        },
1882    );
1883    let kern = validate_kernel(kernel, false)?;
1884    let output = py
1885        .allow_threads(|| fibonacci_entry_bands_with_kernel(&input, kern))
1886        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1887    output_to_py_dict(py, output)
1888}
1889
1890#[cfg(feature = "python")]
1891#[pyfunction(name = "fibonacci_entry_bands_batch")]
1892#[pyo3(signature = (open, high, low, close, length_range=(DEFAULT_LENGTH, DEFAULT_LENGTH, 0), atr_length_range=(DEFAULT_ATR_LENGTH, DEFAULT_ATR_LENGTH, 0), source=DEFAULT_SOURCE, use_atr=DEFAULT_USE_ATR, tp_aggressiveness=DEFAULT_TP_AGGRESSIVENESS, kernel=None))]
1893pub fn fibonacci_entry_bands_batch_py<'py>(
1894    py: Python<'py>,
1895    open: PyReadonlyArray1<'py, f64>,
1896    high: PyReadonlyArray1<'py, f64>,
1897    low: PyReadonlyArray1<'py, f64>,
1898    close: PyReadonlyArray1<'py, f64>,
1899    length_range: (usize, usize, usize),
1900    atr_length_range: (usize, usize, usize),
1901    source: &str,
1902    use_atr: bool,
1903    tp_aggressiveness: &str,
1904    kernel: Option<&str>,
1905) -> PyResult<Bound<'py, PyDict>> {
1906    let open = open.as_slice()?;
1907    let high = high.as_slice()?;
1908    let low = low.as_slice()?;
1909    let close = close.as_slice()?;
1910    let kern = validate_kernel(kernel, true)?;
1911    let sweep = FibonacciEntryBandsBatchRange {
1912        length: length_range,
1913        atr_length: atr_length_range,
1914        source: source.to_string(),
1915        use_atr,
1916        tp_aggressiveness: tp_aggressiveness.to_string(),
1917    };
1918    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1919    let rows = combos.len();
1920    let cols = close.len();
1921    let total = rows
1922        .checked_mul(cols)
1923        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1924
1925    macro_rules! arr {
1926        ($name:ident) => {
1927            let $name = unsafe { PyArray1::<f64>::new(py, [total], false) };
1928        };
1929    }
1930
1931    arr!(basis_arr);
1932    arr!(trend_arr);
1933    arr!(upper_0618_arr);
1934    arr!(upper_1000_arr);
1935    arr!(upper_1618_arr);
1936    arr!(upper_2618_arr);
1937    arr!(lower_0618_arr);
1938    arr!(lower_1000_arr);
1939    arr!(lower_1618_arr);
1940    arr!(lower_2618_arr);
1941    arr!(tp_long_arr);
1942    arr!(tp_short_arr);
1943    arr!(long_entry_arr);
1944    arr!(short_entry_arr);
1945    arr!(rejection_long_arr);
1946    arr!(rejection_short_arr);
1947    arr!(long_bounce_arr);
1948    arr!(short_bounce_arr);
1949
1950    let basis_slice = unsafe { basis_arr.as_slice_mut()? };
1951    let trend_slice = unsafe { trend_arr.as_slice_mut()? };
1952    let upper_0618_slice = unsafe { upper_0618_arr.as_slice_mut()? };
1953    let upper_1000_slice = unsafe { upper_1000_arr.as_slice_mut()? };
1954    let upper_1618_slice = unsafe { upper_1618_arr.as_slice_mut()? };
1955    let upper_2618_slice = unsafe { upper_2618_arr.as_slice_mut()? };
1956    let lower_0618_slice = unsafe { lower_0618_arr.as_slice_mut()? };
1957    let lower_1000_slice = unsafe { lower_1000_arr.as_slice_mut()? };
1958    let lower_1618_slice = unsafe { lower_1618_arr.as_slice_mut()? };
1959    let lower_2618_slice = unsafe { lower_2618_arr.as_slice_mut()? };
1960    let tp_long_slice = unsafe { tp_long_arr.as_slice_mut()? };
1961    let tp_short_slice = unsafe { tp_short_arr.as_slice_mut()? };
1962    let long_entry_slice = unsafe { long_entry_arr.as_slice_mut()? };
1963    let short_entry_slice = unsafe { short_entry_arr.as_slice_mut()? };
1964    let rejection_long_slice = unsafe { rejection_long_arr.as_slice_mut()? };
1965    let rejection_short_slice = unsafe { rejection_short_arr.as_slice_mut()? };
1966    let long_bounce_slice = unsafe { long_bounce_arr.as_slice_mut()? };
1967    let short_bounce_slice = unsafe { short_bounce_arr.as_slice_mut()? };
1968
1969    let combos = py
1970        .allow_threads(|| {
1971            fibonacci_entry_bands_batch_inner_into(
1972                open,
1973                high,
1974                low,
1975                close,
1976                &sweep,
1977                kern,
1978                basis_slice,
1979                trend_slice,
1980                upper_0618_slice,
1981                upper_1000_slice,
1982                upper_1618_slice,
1983                upper_2618_slice,
1984                lower_0618_slice,
1985                lower_1000_slice,
1986                lower_1618_slice,
1987                lower_2618_slice,
1988                tp_long_slice,
1989                tp_short_slice,
1990                long_entry_slice,
1991                short_entry_slice,
1992                rejection_long_slice,
1993                rejection_short_slice,
1994                long_bounce_slice,
1995                short_bounce_slice,
1996            )
1997        })
1998        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1999
2000    let dict = PyDict::new(py);
2001    dict.set_item("basis", basis_arr.reshape((rows, cols))?)?;
2002    dict.set_item("trend", trend_arr.reshape((rows, cols))?)?;
2003    dict.set_item("upper_0618", upper_0618_arr.reshape((rows, cols))?)?;
2004    dict.set_item("upper_1000", upper_1000_arr.reshape((rows, cols))?)?;
2005    dict.set_item("upper_1618", upper_1618_arr.reshape((rows, cols))?)?;
2006    dict.set_item("upper_2618", upper_2618_arr.reshape((rows, cols))?)?;
2007    dict.set_item("lower_0618", lower_0618_arr.reshape((rows, cols))?)?;
2008    dict.set_item("lower_1000", lower_1000_arr.reshape((rows, cols))?)?;
2009    dict.set_item("lower_1618", lower_1618_arr.reshape((rows, cols))?)?;
2010    dict.set_item("lower_2618", lower_2618_arr.reshape((rows, cols))?)?;
2011    dict.set_item("tp_long_band", tp_long_arr.reshape((rows, cols))?)?;
2012    dict.set_item("tp_short_band", tp_short_arr.reshape((rows, cols))?)?;
2013    dict.set_item("long_entry", long_entry_arr.reshape((rows, cols))?)?;
2014    dict.set_item("short_entry", short_entry_arr.reshape((rows, cols))?)?;
2015    dict.set_item("rejection_long", rejection_long_arr.reshape((rows, cols))?)?;
2016    dict.set_item(
2017        "rejection_short",
2018        rejection_short_arr.reshape((rows, cols))?,
2019    )?;
2020    dict.set_item("long_bounce", long_bounce_arr.reshape((rows, cols))?)?;
2021    dict.set_item("short_bounce", short_bounce_arr.reshape((rows, cols))?)?;
2022    dict.set_item(
2023        "lengths",
2024        combos
2025            .iter()
2026            .map(|combo| combo.length.unwrap_or(DEFAULT_LENGTH) as u64)
2027            .collect::<Vec<_>>()
2028            .into_pyarray(py),
2029    )?;
2030    dict.set_item(
2031        "atr_lengths",
2032        combos
2033            .iter()
2034            .map(|combo| combo.atr_length.unwrap_or(DEFAULT_ATR_LENGTH) as u64)
2035            .collect::<Vec<_>>()
2036            .into_pyarray(py),
2037    )?;
2038    dict.set_item(
2039        "sources",
2040        combos
2041            .iter()
2042            .map(|combo| combo.source.as_deref().unwrap_or(DEFAULT_SOURCE))
2043            .collect::<Vec<_>>(),
2044    )?;
2045    dict.set_item(
2046        "use_atr_flags",
2047        combos
2048            .iter()
2049            .map(|combo| combo.use_atr.unwrap_or(DEFAULT_USE_ATR))
2050            .collect::<Vec<_>>()
2051            .into_pyarray(py),
2052    )?;
2053    dict.set_item(
2054        "tp_aggressiveness_values",
2055        combos
2056            .iter()
2057            .map(|combo| {
2058                combo
2059                    .tp_aggressiveness
2060                    .clone()
2061                    .unwrap_or_else(|| DEFAULT_TP_AGGRESSIVENESS.to_string())
2062            })
2063            .collect::<Vec<_>>(),
2064    )?;
2065    dict.set_item("rows", rows)?;
2066    dict.set_item("cols", cols)?;
2067    Ok(dict)
2068}
2069
2070#[cfg(feature = "python")]
2071pub fn register_fibonacci_entry_bands_module(
2072    module: &Bound<'_, pyo3::types::PyModule>,
2073) -> PyResult<()> {
2074    module.add_function(wrap_pyfunction!(fibonacci_entry_bands_py, module)?)?;
2075    module.add_function(wrap_pyfunction!(fibonacci_entry_bands_batch_py, module)?)?;
2076    module.add_class::<FibonacciEntryBandsStreamPy>()?;
2077    Ok(())
2078}
2079
2080#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2081#[derive(Serialize, Deserialize)]
2082pub struct FibonacciEntryBandsJsOutput {
2083    pub basis: Vec<f64>,
2084    pub trend: Vec<f64>,
2085    pub upper_0618: Vec<f64>,
2086    pub upper_1000: Vec<f64>,
2087    pub upper_1618: Vec<f64>,
2088    pub upper_2618: Vec<f64>,
2089    pub lower_0618: Vec<f64>,
2090    pub lower_1000: Vec<f64>,
2091    pub lower_1618: Vec<f64>,
2092    pub lower_2618: Vec<f64>,
2093    pub tp_long_band: Vec<f64>,
2094    pub tp_short_band: Vec<f64>,
2095    pub long_entry: Vec<f64>,
2096    pub short_entry: Vec<f64>,
2097    pub rejection_long: Vec<f64>,
2098    pub rejection_short: Vec<f64>,
2099    pub long_bounce: Vec<f64>,
2100    pub short_bounce: Vec<f64>,
2101}
2102
2103#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2104#[wasm_bindgen(js_name = "fibonacci_entry_bands_js")]
2105pub fn fibonacci_entry_bands_js(
2106    open: &[f64],
2107    high: &[f64],
2108    low: &[f64],
2109    close: &[f64],
2110    source: &str,
2111    length: usize,
2112    atr_length: usize,
2113    use_atr: bool,
2114    tp_aggressiveness: &str,
2115) -> Result<JsValue, JsValue> {
2116    let input = FibonacciEntryBandsInput::from_slices(
2117        open,
2118        high,
2119        low,
2120        close,
2121        FibonacciEntryBandsParams {
2122            source: Some(source.to_string()),
2123            length: Some(length),
2124            atr_length: Some(atr_length),
2125            use_atr: Some(use_atr),
2126            tp_aggressiveness: Some(tp_aggressiveness.to_string()),
2127        },
2128    );
2129    let out = fibonacci_entry_bands(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
2130    serde_wasm_bindgen::to_value(&FibonacciEntryBandsJsOutput {
2131        basis: out.basis,
2132        trend: out.trend,
2133        upper_0618: out.upper_0618,
2134        upper_1000: out.upper_1000,
2135        upper_1618: out.upper_1618,
2136        upper_2618: out.upper_2618,
2137        lower_0618: out.lower_0618,
2138        lower_1000: out.lower_1000,
2139        lower_1618: out.lower_1618,
2140        lower_2618: out.lower_2618,
2141        tp_long_band: out.tp_long_band,
2142        tp_short_band: out.tp_short_band,
2143        long_entry: out.long_entry,
2144        short_entry: out.short_entry,
2145        rejection_long: out.rejection_long,
2146        rejection_short: out.rejection_short,
2147        long_bounce: out.long_bounce,
2148        short_bounce: out.short_bounce,
2149    })
2150    .map_err(|e| JsValue::from_str(&e.to_string()))
2151}
2152
2153#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2154#[wasm_bindgen]
2155pub fn fibonacci_entry_bands_alloc(len: usize) -> *mut f64 {
2156    let mut vec = Vec::<f64>::with_capacity(len);
2157    let ptr = vec.as_mut_ptr();
2158    std::mem::forget(vec);
2159    ptr
2160}
2161
2162#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2163#[wasm_bindgen]
2164pub fn fibonacci_entry_bands_free(ptr: *mut f64, len: usize) {
2165    if !ptr.is_null() {
2166        unsafe {
2167            let _ = Vec::from_raw_parts(ptr, len, len);
2168        }
2169    }
2170}
2171
2172#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2173fn has_duplicate_ptrs(ptrs: &[usize]) -> bool {
2174    for i in 0..ptrs.len() {
2175        for j in (i + 1)..ptrs.len() {
2176            if ptrs[i] == ptrs[j] {
2177                return true;
2178            }
2179        }
2180    }
2181    false
2182}
2183
2184#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2185#[wasm_bindgen]
2186#[allow(clippy::too_many_arguments)]
2187pub fn fibonacci_entry_bands_into(
2188    open_ptr: *const f64,
2189    high_ptr: *const f64,
2190    low_ptr: *const f64,
2191    close_ptr: *const f64,
2192    basis_ptr: *mut f64,
2193    trend_ptr: *mut f64,
2194    upper_0618_ptr: *mut f64,
2195    upper_1000_ptr: *mut f64,
2196    upper_1618_ptr: *mut f64,
2197    upper_2618_ptr: *mut f64,
2198    lower_0618_ptr: *mut f64,
2199    lower_1000_ptr: *mut f64,
2200    lower_1618_ptr: *mut f64,
2201    lower_2618_ptr: *mut f64,
2202    tp_long_ptr: *mut f64,
2203    tp_short_ptr: *mut f64,
2204    long_entry_ptr: *mut f64,
2205    short_entry_ptr: *mut f64,
2206    rejection_long_ptr: *mut f64,
2207    rejection_short_ptr: *mut f64,
2208    long_bounce_ptr: *mut f64,
2209    short_bounce_ptr: *mut f64,
2210    len: usize,
2211    source: &str,
2212    length: usize,
2213    atr_length: usize,
2214    use_atr: bool,
2215    tp_aggressiveness: &str,
2216) -> Result<(), JsValue> {
2217    let ptrs = [
2218        open_ptr as usize,
2219        high_ptr as usize,
2220        low_ptr as usize,
2221        close_ptr as usize,
2222        basis_ptr as usize,
2223        trend_ptr as usize,
2224        upper_0618_ptr as usize,
2225        upper_1000_ptr as usize,
2226        upper_1618_ptr as usize,
2227        upper_2618_ptr as usize,
2228        lower_0618_ptr as usize,
2229        lower_1000_ptr as usize,
2230        lower_1618_ptr as usize,
2231        lower_2618_ptr as usize,
2232        tp_long_ptr as usize,
2233        tp_short_ptr as usize,
2234        long_entry_ptr as usize,
2235        short_entry_ptr as usize,
2236        rejection_long_ptr as usize,
2237        rejection_short_ptr as usize,
2238        long_bounce_ptr as usize,
2239        short_bounce_ptr as usize,
2240    ];
2241    if ptrs.iter().any(|ptr| *ptr == 0) {
2242        return Err(JsValue::from_str("Null pointer provided"));
2243    }
2244
2245    unsafe {
2246        let open = std::slice::from_raw_parts(open_ptr, len);
2247        let high = std::slice::from_raw_parts(high_ptr, len);
2248        let low = std::slice::from_raw_parts(low_ptr, len);
2249        let close = std::slice::from_raw_parts(close_ptr, len);
2250        let input = FibonacciEntryBandsInput::from_slices(
2251            open,
2252            high,
2253            low,
2254            close,
2255            FibonacciEntryBandsParams {
2256                source: Some(source.to_string()),
2257                length: Some(length),
2258                atr_length: Some(atr_length),
2259                use_atr: Some(use_atr),
2260                tp_aggressiveness: Some(tp_aggressiveness.to_string()),
2261            },
2262        );
2263        let output_ptrs = &ptrs[4..];
2264        let need_temp = output_ptrs.iter().any(|ptr| {
2265            *ptr == open_ptr as usize
2266                || *ptr == high_ptr as usize
2267                || *ptr == low_ptr as usize
2268                || *ptr == close_ptr as usize
2269        }) || has_duplicate_ptrs(output_ptrs);
2270
2271        if need_temp {
2272            let mut basis = vec![0.0; len];
2273            let mut trend = vec![0.0; len];
2274            let mut upper_0618 = vec![0.0; len];
2275            let mut upper_1000 = vec![0.0; len];
2276            let mut upper_1618 = vec![0.0; len];
2277            let mut upper_2618 = vec![0.0; len];
2278            let mut lower_0618 = vec![0.0; len];
2279            let mut lower_1000 = vec![0.0; len];
2280            let mut lower_1618 = vec![0.0; len];
2281            let mut lower_2618 = vec![0.0; len];
2282            let mut tp_long = vec![0.0; len];
2283            let mut tp_short = vec![0.0; len];
2284            let mut long_entry = vec![0.0; len];
2285            let mut short_entry = vec![0.0; len];
2286            let mut rejection_long = vec![0.0; len];
2287            let mut rejection_short = vec![0.0; len];
2288            let mut long_bounce = vec![0.0; len];
2289            let mut short_bounce = vec![0.0; len];
2290            fibonacci_entry_bands_into_slices(
2291                &mut basis,
2292                &mut trend,
2293                &mut upper_0618,
2294                &mut upper_1000,
2295                &mut upper_1618,
2296                &mut upper_2618,
2297                &mut lower_0618,
2298                &mut lower_1000,
2299                &mut lower_1618,
2300                &mut lower_2618,
2301                &mut tp_long,
2302                &mut tp_short,
2303                &mut long_entry,
2304                &mut short_entry,
2305                &mut rejection_long,
2306                &mut rejection_short,
2307                &mut long_bounce,
2308                &mut short_bounce,
2309                &input,
2310                Kernel::Auto,
2311            )
2312            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2313            std::slice::from_raw_parts_mut(basis_ptr, len).copy_from_slice(&basis);
2314            std::slice::from_raw_parts_mut(trend_ptr, len).copy_from_slice(&trend);
2315            std::slice::from_raw_parts_mut(upper_0618_ptr, len).copy_from_slice(&upper_0618);
2316            std::slice::from_raw_parts_mut(upper_1000_ptr, len).copy_from_slice(&upper_1000);
2317            std::slice::from_raw_parts_mut(upper_1618_ptr, len).copy_from_slice(&upper_1618);
2318            std::slice::from_raw_parts_mut(upper_2618_ptr, len).copy_from_slice(&upper_2618);
2319            std::slice::from_raw_parts_mut(lower_0618_ptr, len).copy_from_slice(&lower_0618);
2320            std::slice::from_raw_parts_mut(lower_1000_ptr, len).copy_from_slice(&lower_1000);
2321            std::slice::from_raw_parts_mut(lower_1618_ptr, len).copy_from_slice(&lower_1618);
2322            std::slice::from_raw_parts_mut(lower_2618_ptr, len).copy_from_slice(&lower_2618);
2323            std::slice::from_raw_parts_mut(tp_long_ptr, len).copy_from_slice(&tp_long);
2324            std::slice::from_raw_parts_mut(tp_short_ptr, len).copy_from_slice(&tp_short);
2325            std::slice::from_raw_parts_mut(long_entry_ptr, len).copy_from_slice(&long_entry);
2326            std::slice::from_raw_parts_mut(short_entry_ptr, len).copy_from_slice(&short_entry);
2327            std::slice::from_raw_parts_mut(rejection_long_ptr, len)
2328                .copy_from_slice(&rejection_long);
2329            std::slice::from_raw_parts_mut(rejection_short_ptr, len)
2330                .copy_from_slice(&rejection_short);
2331            std::slice::from_raw_parts_mut(long_bounce_ptr, len).copy_from_slice(&long_bounce);
2332            std::slice::from_raw_parts_mut(short_bounce_ptr, len).copy_from_slice(&short_bounce);
2333        } else {
2334            fibonacci_entry_bands_into_slices(
2335                std::slice::from_raw_parts_mut(basis_ptr, len),
2336                std::slice::from_raw_parts_mut(trend_ptr, len),
2337                std::slice::from_raw_parts_mut(upper_0618_ptr, len),
2338                std::slice::from_raw_parts_mut(upper_1000_ptr, len),
2339                std::slice::from_raw_parts_mut(upper_1618_ptr, len),
2340                std::slice::from_raw_parts_mut(upper_2618_ptr, len),
2341                std::slice::from_raw_parts_mut(lower_0618_ptr, len),
2342                std::slice::from_raw_parts_mut(lower_1000_ptr, len),
2343                std::slice::from_raw_parts_mut(lower_1618_ptr, len),
2344                std::slice::from_raw_parts_mut(lower_2618_ptr, len),
2345                std::slice::from_raw_parts_mut(tp_long_ptr, len),
2346                std::slice::from_raw_parts_mut(tp_short_ptr, len),
2347                std::slice::from_raw_parts_mut(long_entry_ptr, len),
2348                std::slice::from_raw_parts_mut(short_entry_ptr, len),
2349                std::slice::from_raw_parts_mut(rejection_long_ptr, len),
2350                std::slice::from_raw_parts_mut(rejection_short_ptr, len),
2351                std::slice::from_raw_parts_mut(long_bounce_ptr, len),
2352                std::slice::from_raw_parts_mut(short_bounce_ptr, len),
2353                &input,
2354                Kernel::Auto,
2355            )
2356            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2357        }
2358    }
2359    Ok(())
2360}
2361
2362#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2363#[derive(Serialize, Deserialize)]
2364pub struct FibonacciEntryBandsBatchJsConfig {
2365    pub length_range: Option<(usize, usize, usize)>,
2366    pub atr_length_range: Option<(usize, usize, usize)>,
2367    pub source: Option<String>,
2368    pub use_atr: Option<bool>,
2369    pub tp_aggressiveness: Option<String>,
2370}
2371
2372#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2373#[derive(Serialize, Deserialize)]
2374pub struct FibonacciEntryBandsBatchJsOutput {
2375    pub basis: Vec<f64>,
2376    pub trend: Vec<f64>,
2377    pub upper_0618: Vec<f64>,
2378    pub upper_1000: Vec<f64>,
2379    pub upper_1618: Vec<f64>,
2380    pub upper_2618: Vec<f64>,
2381    pub lower_0618: Vec<f64>,
2382    pub lower_1000: Vec<f64>,
2383    pub lower_1618: Vec<f64>,
2384    pub lower_2618: Vec<f64>,
2385    pub tp_long_band: Vec<f64>,
2386    pub tp_short_band: Vec<f64>,
2387    pub long_entry: Vec<f64>,
2388    pub short_entry: Vec<f64>,
2389    pub rejection_long: Vec<f64>,
2390    pub rejection_short: Vec<f64>,
2391    pub long_bounce: Vec<f64>,
2392    pub short_bounce: Vec<f64>,
2393    pub combos: Vec<FibonacciEntryBandsParams>,
2394    pub lengths: Vec<usize>,
2395    pub atr_lengths: Vec<usize>,
2396    pub sources: Vec<String>,
2397    pub use_atr_flags: Vec<bool>,
2398    pub tp_aggressiveness_values: Vec<String>,
2399    pub rows: usize,
2400    pub cols: usize,
2401}
2402
2403#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2404#[wasm_bindgen(js_name = "fibonacci_entry_bands_batch_js")]
2405pub fn fibonacci_entry_bands_batch_js(
2406    open: &[f64],
2407    high: &[f64],
2408    low: &[f64],
2409    close: &[f64],
2410    config: JsValue,
2411) -> Result<JsValue, JsValue> {
2412    let config: FibonacciEntryBandsBatchJsConfig = serde_wasm_bindgen::from_value(config)
2413        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
2414    let sweep = FibonacciEntryBandsBatchRange {
2415        length: config
2416            .length_range
2417            .unwrap_or((DEFAULT_LENGTH, DEFAULT_LENGTH, 0)),
2418        atr_length: config
2419            .atr_length_range
2420            .unwrap_or((DEFAULT_ATR_LENGTH, DEFAULT_ATR_LENGTH, 0)),
2421        source: config.source.unwrap_or_else(|| DEFAULT_SOURCE.to_string()),
2422        use_atr: config.use_atr.unwrap_or(DEFAULT_USE_ATR),
2423        tp_aggressiveness: config
2424            .tp_aggressiveness
2425            .unwrap_or_else(|| DEFAULT_TP_AGGRESSIVENESS.to_string()),
2426    };
2427    let out = fibonacci_entry_bands_batch_with_kernel(open, high, low, close, &sweep, Kernel::Auto)
2428        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2429    serde_wasm_bindgen::to_value(&FibonacciEntryBandsBatchJsOutput {
2430        lengths: out
2431            .combos
2432            .iter()
2433            .map(|combo| combo.length.unwrap_or(DEFAULT_LENGTH))
2434            .collect(),
2435        atr_lengths: out
2436            .combos
2437            .iter()
2438            .map(|combo| combo.atr_length.unwrap_or(DEFAULT_ATR_LENGTH))
2439            .collect(),
2440        sources: out
2441            .combos
2442            .iter()
2443            .map(|combo| {
2444                combo
2445                    .source
2446                    .clone()
2447                    .unwrap_or_else(|| DEFAULT_SOURCE.to_string())
2448            })
2449            .collect(),
2450        use_atr_flags: out
2451            .combos
2452            .iter()
2453            .map(|combo| combo.use_atr.unwrap_or(DEFAULT_USE_ATR))
2454            .collect(),
2455        tp_aggressiveness_values: out
2456            .combos
2457            .iter()
2458            .map(|combo| {
2459                combo
2460                    .tp_aggressiveness
2461                    .clone()
2462                    .unwrap_or_else(|| DEFAULT_TP_AGGRESSIVENESS.to_string())
2463            })
2464            .collect(),
2465        basis: out.basis,
2466        trend: out.trend,
2467        upper_0618: out.upper_0618,
2468        upper_1000: out.upper_1000,
2469        upper_1618: out.upper_1618,
2470        upper_2618: out.upper_2618,
2471        lower_0618: out.lower_0618,
2472        lower_1000: out.lower_1000,
2473        lower_1618: out.lower_1618,
2474        lower_2618: out.lower_2618,
2475        tp_long_band: out.tp_long_band,
2476        tp_short_band: out.tp_short_band,
2477        long_entry: out.long_entry,
2478        short_entry: out.short_entry,
2479        rejection_long: out.rejection_long,
2480        rejection_short: out.rejection_short,
2481        long_bounce: out.long_bounce,
2482        short_bounce: out.short_bounce,
2483        combos: out.combos,
2484        rows: out.rows,
2485        cols: out.cols,
2486    })
2487    .map_err(|e| JsValue::from_str(&e.to_string()))
2488}
2489
2490#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2491#[wasm_bindgen]
2492#[allow(clippy::too_many_arguments)]
2493pub fn fibonacci_entry_bands_batch_into(
2494    open_ptr: *const f64,
2495    high_ptr: *const f64,
2496    low_ptr: *const f64,
2497    close_ptr: *const f64,
2498    basis_ptr: *mut f64,
2499    trend_ptr: *mut f64,
2500    upper_0618_ptr: *mut f64,
2501    upper_1000_ptr: *mut f64,
2502    upper_1618_ptr: *mut f64,
2503    upper_2618_ptr: *mut f64,
2504    lower_0618_ptr: *mut f64,
2505    lower_1000_ptr: *mut f64,
2506    lower_1618_ptr: *mut f64,
2507    lower_2618_ptr: *mut f64,
2508    tp_long_ptr: *mut f64,
2509    tp_short_ptr: *mut f64,
2510    long_entry_ptr: *mut f64,
2511    short_entry_ptr: *mut f64,
2512    rejection_long_ptr: *mut f64,
2513    rejection_short_ptr: *mut f64,
2514    long_bounce_ptr: *mut f64,
2515    short_bounce_ptr: *mut f64,
2516    len: usize,
2517    length_start: usize,
2518    length_end: usize,
2519    length_step: usize,
2520    atr_length_start: usize,
2521    atr_length_end: usize,
2522    atr_length_step: usize,
2523    source: &str,
2524    use_atr: bool,
2525    tp_aggressiveness: &str,
2526) -> Result<usize, JsValue> {
2527    let ptrs = [
2528        open_ptr as usize,
2529        high_ptr as usize,
2530        low_ptr as usize,
2531        close_ptr as usize,
2532        basis_ptr as usize,
2533        trend_ptr as usize,
2534        upper_0618_ptr as usize,
2535        upper_1000_ptr as usize,
2536        upper_1618_ptr as usize,
2537        upper_2618_ptr as usize,
2538        lower_0618_ptr as usize,
2539        lower_1000_ptr as usize,
2540        lower_1618_ptr as usize,
2541        lower_2618_ptr as usize,
2542        tp_long_ptr as usize,
2543        tp_short_ptr as usize,
2544        long_entry_ptr as usize,
2545        short_entry_ptr as usize,
2546        rejection_long_ptr as usize,
2547        rejection_short_ptr as usize,
2548        long_bounce_ptr as usize,
2549        short_bounce_ptr as usize,
2550    ];
2551    if ptrs.iter().any(|ptr| *ptr == 0) {
2552        return Err(JsValue::from_str("Null pointer provided"));
2553    }
2554
2555    let sweep = FibonacciEntryBandsBatchRange {
2556        length: (length_start, length_end, length_step),
2557        atr_length: (atr_length_start, atr_length_end, atr_length_step),
2558        source: source.to_string(),
2559        use_atr,
2560        tp_aggressiveness: tp_aggressiveness.to_string(),
2561    };
2562    let rows = expand_grid(&sweep)
2563        .map_err(|e| JsValue::from_str(&e.to_string()))?
2564        .len();
2565    let total = rows
2566        .checked_mul(len)
2567        .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
2568
2569    unsafe {
2570        let open = std::slice::from_raw_parts(open_ptr, len);
2571        let high = std::slice::from_raw_parts(high_ptr, len);
2572        let low = std::slice::from_raw_parts(low_ptr, len);
2573        let close = std::slice::from_raw_parts(close_ptr, len);
2574        let output_ptrs = &ptrs[4..];
2575        let need_temp = output_ptrs.iter().any(|ptr| {
2576            *ptr == open_ptr as usize
2577                || *ptr == high_ptr as usize
2578                || *ptr == low_ptr as usize
2579                || *ptr == close_ptr as usize
2580        }) || has_duplicate_ptrs(output_ptrs);
2581
2582        if need_temp {
2583            let mut basis = vec![0.0; total];
2584            let mut trend = vec![0.0; total];
2585            let mut upper_0618 = vec![0.0; total];
2586            let mut upper_1000 = vec![0.0; total];
2587            let mut upper_1618 = vec![0.0; total];
2588            let mut upper_2618 = vec![0.0; total];
2589            let mut lower_0618 = vec![0.0; total];
2590            let mut lower_1000 = vec![0.0; total];
2591            let mut lower_1618 = vec![0.0; total];
2592            let mut lower_2618 = vec![0.0; total];
2593            let mut tp_long = vec![0.0; total];
2594            let mut tp_short = vec![0.0; total];
2595            let mut long_entry = vec![0.0; total];
2596            let mut short_entry = vec![0.0; total];
2597            let mut rejection_long = vec![0.0; total];
2598            let mut rejection_short = vec![0.0; total];
2599            let mut long_bounce = vec![0.0; total];
2600            let mut short_bounce = vec![0.0; total];
2601            let rows = fibonacci_entry_bands_batch_inner_into(
2602                open,
2603                high,
2604                low,
2605                close,
2606                &sweep,
2607                Kernel::Auto,
2608                &mut basis,
2609                &mut trend,
2610                &mut upper_0618,
2611                &mut upper_1000,
2612                &mut upper_1618,
2613                &mut upper_2618,
2614                &mut lower_0618,
2615                &mut lower_1000,
2616                &mut lower_1618,
2617                &mut lower_2618,
2618                &mut tp_long,
2619                &mut tp_short,
2620                &mut long_entry,
2621                &mut short_entry,
2622                &mut rejection_long,
2623                &mut rejection_short,
2624                &mut long_bounce,
2625                &mut short_bounce,
2626            )
2627            .map_err(|e| JsValue::from_str(&e.to_string()))?
2628            .len();
2629            std::slice::from_raw_parts_mut(basis_ptr, total).copy_from_slice(&basis);
2630            std::slice::from_raw_parts_mut(trend_ptr, total).copy_from_slice(&trend);
2631            std::slice::from_raw_parts_mut(upper_0618_ptr, total).copy_from_slice(&upper_0618);
2632            std::slice::from_raw_parts_mut(upper_1000_ptr, total).copy_from_slice(&upper_1000);
2633            std::slice::from_raw_parts_mut(upper_1618_ptr, total).copy_from_slice(&upper_1618);
2634            std::slice::from_raw_parts_mut(upper_2618_ptr, total).copy_from_slice(&upper_2618);
2635            std::slice::from_raw_parts_mut(lower_0618_ptr, total).copy_from_slice(&lower_0618);
2636            std::slice::from_raw_parts_mut(lower_1000_ptr, total).copy_from_slice(&lower_1000);
2637            std::slice::from_raw_parts_mut(lower_1618_ptr, total).copy_from_slice(&lower_1618);
2638            std::slice::from_raw_parts_mut(lower_2618_ptr, total).copy_from_slice(&lower_2618);
2639            std::slice::from_raw_parts_mut(tp_long_ptr, total).copy_from_slice(&tp_long);
2640            std::slice::from_raw_parts_mut(tp_short_ptr, total).copy_from_slice(&tp_short);
2641            std::slice::from_raw_parts_mut(long_entry_ptr, total).copy_from_slice(&long_entry);
2642            std::slice::from_raw_parts_mut(short_entry_ptr, total).copy_from_slice(&short_entry);
2643            std::slice::from_raw_parts_mut(rejection_long_ptr, total)
2644                .copy_from_slice(&rejection_long);
2645            std::slice::from_raw_parts_mut(rejection_short_ptr, total)
2646                .copy_from_slice(&rejection_short);
2647            std::slice::from_raw_parts_mut(long_bounce_ptr, total).copy_from_slice(&long_bounce);
2648            std::slice::from_raw_parts_mut(short_bounce_ptr, total).copy_from_slice(&short_bounce);
2649            Ok(rows)
2650        } else {
2651            let rows = fibonacci_entry_bands_batch_inner_into(
2652                open,
2653                high,
2654                low,
2655                close,
2656                &sweep,
2657                Kernel::Auto,
2658                std::slice::from_raw_parts_mut(basis_ptr, total),
2659                std::slice::from_raw_parts_mut(trend_ptr, total),
2660                std::slice::from_raw_parts_mut(upper_0618_ptr, total),
2661                std::slice::from_raw_parts_mut(upper_1000_ptr, total),
2662                std::slice::from_raw_parts_mut(upper_1618_ptr, total),
2663                std::slice::from_raw_parts_mut(upper_2618_ptr, total),
2664                std::slice::from_raw_parts_mut(lower_0618_ptr, total),
2665                std::slice::from_raw_parts_mut(lower_1000_ptr, total),
2666                std::slice::from_raw_parts_mut(lower_1618_ptr, total),
2667                std::slice::from_raw_parts_mut(lower_2618_ptr, total),
2668                std::slice::from_raw_parts_mut(tp_long_ptr, total),
2669                std::slice::from_raw_parts_mut(tp_short_ptr, total),
2670                std::slice::from_raw_parts_mut(long_entry_ptr, total),
2671                std::slice::from_raw_parts_mut(short_entry_ptr, total),
2672                std::slice::from_raw_parts_mut(rejection_long_ptr, total),
2673                std::slice::from_raw_parts_mut(rejection_short_ptr, total),
2674                std::slice::from_raw_parts_mut(long_bounce_ptr, total),
2675                std::slice::from_raw_parts_mut(short_bounce_ptr, total),
2676            )
2677            .map_err(|e| JsValue::from_str(&e.to_string()))?
2678            .len();
2679            Ok(rows)
2680        }
2681    }
2682}
2683
2684#[cfg(test)]
2685mod tests {
2686    use super::*;
2687
2688    fn sample_ohlc(len: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
2689        let open: Vec<f64> = (0..len)
2690            .map(|i| {
2691                let x = i as f64;
2692                100.0 + x * 0.05 + (x * 0.09).sin() * 1.8 + (x * 0.021).cos() * 0.7
2693            })
2694            .collect();
2695        let close: Vec<f64> = open
2696            .iter()
2697            .enumerate()
2698            .map(|(i, &o)| o + (i as f64 * 0.13).cos() * 0.9)
2699            .collect();
2700        let high: Vec<f64> = open
2701            .iter()
2702            .zip(close.iter())
2703            .enumerate()
2704            .map(|(i, (&o, &c))| o.max(c) + 0.7 + (i as f64 * 0.05).sin().abs() * 0.2)
2705            .collect();
2706        let low: Vec<f64> = open
2707            .iter()
2708            .zip(close.iter())
2709            .enumerate()
2710            .map(|(i, (&o, &c))| o.min(c) - 0.7 - (i as f64 * 0.04).cos().abs() * 0.2)
2711            .collect();
2712        (open, high, low, close)
2713    }
2714
2715    fn assert_series_eq(left: &[f64], right: &[f64], tol: f64) {
2716        assert_eq!(left.len(), right.len());
2717        for (&lhs, &rhs) in left.iter().zip(right.iter()) {
2718            assert!(
2719                (lhs.is_nan() && rhs.is_nan()) || (lhs - rhs).abs() <= tol,
2720                "series mismatch: left={lhs:?}, right={rhs:?}"
2721            );
2722        }
2723    }
2724
2725    #[test]
2726    fn fibonacci_entry_bands_output_contract() {
2727        let (open, high, low, close) = sample_ohlc(320);
2728        let out = fibonacci_entry_bands(&FibonacciEntryBandsInput::from_slices(
2729            &open,
2730            &high,
2731            &low,
2732            &close,
2733            FibonacciEntryBandsParams::default(),
2734        ))
2735        .unwrap();
2736        assert_eq!(out.basis.len(), close.len());
2737        assert_eq!(out.upper_2618.len(), close.len());
2738        assert_eq!(out.long_entry.len(), close.len());
2739        assert!(out.basis.iter().any(|v| v.is_finite()));
2740    }
2741
2742    #[test]
2743    fn fibonacci_entry_bands_rejects_invalid_params() {
2744        let (open, high, low, close) = sample_ohlc(32);
2745        let err = fibonacci_entry_bands(&FibonacciEntryBandsInput::from_slices(
2746            &open,
2747            &high,
2748            &low,
2749            &close,
2750            FibonacciEntryBandsParams {
2751                source: Some("bad".to_string()),
2752                ..FibonacciEntryBandsParams::default()
2753            },
2754        ))
2755        .unwrap_err();
2756        assert!(matches!(
2757            err,
2758            FibonacciEntryBandsError::InvalidSource { .. }
2759        ));
2760
2761        let err = fibonacci_entry_bands(&FibonacciEntryBandsInput::from_slices(
2762            &open,
2763            &high,
2764            &low,
2765            &close,
2766            FibonacciEntryBandsParams {
2767                length: Some(0),
2768                ..FibonacciEntryBandsParams::default()
2769            },
2770        ))
2771        .unwrap_err();
2772        assert!(matches!(
2773            err,
2774            FibonacciEntryBandsError::InvalidLength { .. }
2775        ));
2776    }
2777
2778    #[test]
2779    fn fibonacci_entry_bands_stream_matches_batch_with_reset() {
2780        let (mut open, mut high, mut low, mut close) = sample_ohlc(220);
2781        open[110] = f64::NAN;
2782        high[110] = f64::NAN;
2783        low[110] = f64::NAN;
2784        close[110] = f64::NAN;
2785
2786        let params = FibonacciEntryBandsParams {
2787            source: Some("hlc3".to_string()),
2788            length: Some(16),
2789            atr_length: Some(11),
2790            use_atr: Some(true),
2791            tp_aggressiveness: Some("high".to_string()),
2792        };
2793        let batch = fibonacci_entry_bands(&FibonacciEntryBandsInput::from_slices(
2794            &open,
2795            &high,
2796            &low,
2797            &close,
2798            params.clone(),
2799        ))
2800        .unwrap();
2801        let mut stream = FibonacciEntryBandsStream::try_new(params).unwrap();
2802
2803        let mut basis = Vec::with_capacity(close.len());
2804        let mut trend = Vec::with_capacity(close.len());
2805        let mut tp_long = Vec::with_capacity(close.len());
2806        for i in 0..close.len() {
2807            if let Some(point) = stream.update(open[i], high[i], low[i], close[i]) {
2808                basis.push(point.basis);
2809                trend.push(point.trend);
2810                tp_long.push(point.tp_long_band);
2811            } else {
2812                basis.push(f64::NAN);
2813                trend.push(f64::NAN);
2814                tp_long.push(f64::NAN);
2815            }
2816        }
2817
2818        assert_series_eq(&basis, &batch.basis, 1e-12);
2819        assert_series_eq(&trend, &batch.trend, 1e-12);
2820        assert_series_eq(&tp_long, &batch.tp_long_band, 1e-12);
2821    }
2822
2823    #[test]
2824    fn fibonacci_entry_bands_into_matches_api() {
2825        let (open, high, low, close) = sample_ohlc(192);
2826        let input = FibonacciEntryBandsInput::from_slices(
2827            &open,
2828            &high,
2829            &low,
2830            &close,
2831            FibonacciEntryBandsParams {
2832                source: Some("hlc3".to_string()),
2833                length: Some(14),
2834                atr_length: Some(9),
2835                use_atr: Some(false),
2836                tp_aggressiveness: Some("low".to_string()),
2837            },
2838        );
2839        let direct = fibonacci_entry_bands(&input).unwrap();
2840        let mut basis = vec![0.0; close.len()];
2841        let mut trend = vec![0.0; close.len()];
2842        let mut upper_0618 = vec![0.0; close.len()];
2843        let mut upper_1000 = vec![0.0; close.len()];
2844        let mut upper_1618 = vec![0.0; close.len()];
2845        let mut upper_2618 = vec![0.0; close.len()];
2846        let mut lower_0618 = vec![0.0; close.len()];
2847        let mut lower_1000 = vec![0.0; close.len()];
2848        let mut lower_1618 = vec![0.0; close.len()];
2849        let mut lower_2618 = vec![0.0; close.len()];
2850        let mut tp_long = vec![0.0; close.len()];
2851        let mut tp_short = vec![0.0; close.len()];
2852        let mut long_entry = vec![0.0; close.len()];
2853        let mut short_entry = vec![0.0; close.len()];
2854        let mut rejection_long = vec![0.0; close.len()];
2855        let mut rejection_short = vec![0.0; close.len()];
2856        let mut long_bounce = vec![0.0; close.len()];
2857        let mut short_bounce = vec![0.0; close.len()];
2858
2859        fibonacci_entry_bands_into(
2860            &input,
2861            &mut basis,
2862            &mut trend,
2863            &mut upper_0618,
2864            &mut upper_1000,
2865            &mut upper_1618,
2866            &mut upper_2618,
2867            &mut lower_0618,
2868            &mut lower_1000,
2869            &mut lower_1618,
2870            &mut lower_2618,
2871            &mut tp_long,
2872            &mut tp_short,
2873            &mut long_entry,
2874            &mut short_entry,
2875            &mut rejection_long,
2876            &mut rejection_short,
2877            &mut long_bounce,
2878            &mut short_bounce,
2879        )
2880        .unwrap();
2881
2882        assert_series_eq(&basis, &direct.basis, 1e-12);
2883        assert_series_eq(&upper_2618, &direct.upper_2618, 1e-12);
2884        assert_series_eq(&short_bounce, &direct.short_bounce, 1e-12);
2885    }
2886
2887    #[test]
2888    fn fibonacci_entry_bands_batch_single_param_matches_single() {
2889        let (open, high, low, close) = sample_ohlc(160);
2890        let sweep = FibonacciEntryBandsBatchRange {
2891            length: (17, 17, 0),
2892            atr_length: (10, 10, 0),
2893            source: "hlc3".to_string(),
2894            use_atr: true,
2895            tp_aggressiveness: "medium".to_string(),
2896        };
2897        let batch = fibonacci_entry_bands_batch_with_kernel(
2898            &open,
2899            &high,
2900            &low,
2901            &close,
2902            &sweep,
2903            Kernel::Auto,
2904        )
2905        .unwrap();
2906        let single = fibonacci_entry_bands(&FibonacciEntryBandsInput::from_slices(
2907            &open,
2908            &high,
2909            &low,
2910            &close,
2911            FibonacciEntryBandsParams {
2912                source: Some("hlc3".to_string()),
2913                length: Some(17),
2914                atr_length: Some(10),
2915                use_atr: Some(true),
2916                tp_aggressiveness: Some("medium".to_string()),
2917            },
2918        ))
2919        .unwrap();
2920        assert_eq!(batch.rows, 1);
2921        assert_eq!(batch.cols, close.len());
2922        assert_series_eq(&batch.basis[..close.len()], &single.basis, 1e-12);
2923        assert_series_eq(
2924            &batch.rejection_short[..close.len()],
2925            &single.rejection_short,
2926            1e-12,
2927        );
2928    }
2929}