Skip to main content

vector_ta/indicators/
fibonacci_trailing_stop.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    detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes, make_uninit_matrix,
21};
22#[cfg(feature = "python")]
23use crate::utilities::kernel_validation::validate_kernel;
24#[cfg(not(target_arch = "wasm32"))]
25use rayon::prelude::*;
26use std::collections::VecDeque;
27use std::mem::ManuallyDrop;
28use thiserror::Error;
29
30const DEFAULT_LEFT_BARS: usize = 20;
31const DEFAULT_RIGHT_BARS: usize = 1;
32const DEFAULT_LEVEL: f64 = -0.382;
33const DEFAULT_TRIGGER: &str = "close";
34const FLOAT_TOL: f64 = 1e-12;
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37enum TriggerMode {
38    Close,
39    Wick,
40}
41
42impl TriggerMode {
43    #[inline(always)]
44    fn parse(value: &str) -> Option<Self> {
45        if value.eq_ignore_ascii_case("close") {
46            Some(Self::Close)
47        } else if value.eq_ignore_ascii_case("wick") {
48            Some(Self::Wick)
49        } else {
50            None
51        }
52    }
53
54    #[inline(always)]
55    fn as_str(self) -> &'static str {
56        match self {
57            Self::Close => "close",
58            Self::Wick => "wick",
59        }
60    }
61}
62
63#[derive(Debug, Clone)]
64pub enum FibonacciTrailingStopData<'a> {
65    Candles(&'a Candles),
66    Slices {
67        high: &'a [f64],
68        low: &'a [f64],
69        close: &'a [f64],
70    },
71}
72
73#[derive(Debug, Clone)]
74pub struct FibonacciTrailingStopOutput {
75    pub trailing_stop: Vec<f64>,
76    pub long_stop: Vec<f64>,
77    pub short_stop: Vec<f64>,
78    pub direction: Vec<f64>,
79}
80
81#[derive(Debug, Clone, Copy)]
82pub struct FibonacciTrailingStopPoint {
83    pub trailing_stop: f64,
84    pub long_stop: f64,
85    pub short_stop: f64,
86    pub direction: f64,
87}
88
89impl FibonacciTrailingStopPoint {
90    #[inline(always)]
91    fn nan() -> Self {
92        Self {
93            trailing_stop: f64::NAN,
94            long_stop: f64::NAN,
95            short_stop: f64::NAN,
96            direction: f64::NAN,
97        }
98    }
99}
100
101#[derive(Debug, Clone, PartialEq)]
102#[cfg_attr(
103    all(target_arch = "wasm32", feature = "wasm"),
104    derive(Serialize, Deserialize)
105)]
106pub struct FibonacciTrailingStopParams {
107    pub left_bars: Option<usize>,
108    pub right_bars: Option<usize>,
109    pub level: Option<f64>,
110    pub trigger: Option<String>,
111}
112
113impl Default for FibonacciTrailingStopParams {
114    fn default() -> Self {
115        Self {
116            left_bars: Some(DEFAULT_LEFT_BARS),
117            right_bars: Some(DEFAULT_RIGHT_BARS),
118            level: Some(DEFAULT_LEVEL),
119            trigger: Some(DEFAULT_TRIGGER.to_string()),
120        }
121    }
122}
123
124#[derive(Debug, Clone)]
125pub struct FibonacciTrailingStopInput<'a> {
126    pub data: FibonacciTrailingStopData<'a>,
127    pub params: FibonacciTrailingStopParams,
128}
129
130impl<'a> FibonacciTrailingStopInput<'a> {
131    #[inline]
132    pub fn from_candles(candles: &'a Candles, params: FibonacciTrailingStopParams) -> Self {
133        Self {
134            data: FibonacciTrailingStopData::Candles(candles),
135            params,
136        }
137    }
138
139    #[inline]
140    pub fn from_slices(
141        high: &'a [f64],
142        low: &'a [f64],
143        close: &'a [f64],
144        params: FibonacciTrailingStopParams,
145    ) -> Self {
146        Self {
147            data: FibonacciTrailingStopData::Slices { high, low, close },
148            params,
149        }
150    }
151
152    #[inline]
153    pub fn with_default_candles(candles: &'a Candles) -> Self {
154        Self::from_candles(candles, FibonacciTrailingStopParams::default())
155    }
156
157    #[inline]
158    pub fn as_slices(&self) -> (&'a [f64], &'a [f64], &'a [f64]) {
159        match &self.data {
160            FibonacciTrailingStopData::Candles(candles) => {
161                (&candles.high, &candles.low, &candles.close)
162            }
163            FibonacciTrailingStopData::Slices { high, low, close } => (high, low, close),
164        }
165    }
166}
167
168#[derive(Clone, Copy, Debug, Default)]
169pub struct FibonacciTrailingStopBuilder {
170    left_bars: Option<usize>,
171    right_bars: Option<usize>,
172    level: Option<f64>,
173    trigger: Option<TriggerMode>,
174    kernel: Kernel,
175}
176
177impl FibonacciTrailingStopBuilder {
178    #[inline]
179    pub fn new() -> Self {
180        Self::default()
181    }
182
183    #[inline]
184    pub fn left_bars(mut self, value: usize) -> Self {
185        self.left_bars = Some(value);
186        self
187    }
188
189    #[inline]
190    pub fn right_bars(mut self, value: usize) -> Self {
191        self.right_bars = Some(value);
192        self
193    }
194
195    #[inline]
196    pub fn level(mut self, value: f64) -> Self {
197        self.level = Some(value);
198        self
199    }
200
201    #[inline]
202    pub fn trigger(mut self, value: &str) -> Result<Self, FibonacciTrailingStopError> {
203        self.trigger = Some(TriggerMode::parse(value).ok_or_else(|| {
204            FibonacciTrailingStopError::InvalidTrigger {
205                trigger: value.to_string(),
206            }
207        })?);
208        Ok(self)
209    }
210
211    #[inline]
212    pub fn kernel(mut self, kernel: Kernel) -> Self {
213        self.kernel = kernel;
214        self
215    }
216
217    #[inline]
218    pub fn apply(
219        self,
220        candles: &Candles,
221    ) -> Result<FibonacciTrailingStopOutput, FibonacciTrailingStopError> {
222        let input = FibonacciTrailingStopInput::from_candles(
223            candles,
224            FibonacciTrailingStopParams {
225                left_bars: self.left_bars,
226                right_bars: self.right_bars,
227                level: self.level,
228                trigger: Some(
229                    self.trigger
230                        .unwrap_or(TriggerMode::Close)
231                        .as_str()
232                        .to_string(),
233                ),
234            },
235        );
236        fibonacci_trailing_stop_with_kernel(&input, self.kernel)
237    }
238
239    #[inline]
240    pub fn apply_slices(
241        self,
242        high: &[f64],
243        low: &[f64],
244        close: &[f64],
245    ) -> Result<FibonacciTrailingStopOutput, FibonacciTrailingStopError> {
246        let input = FibonacciTrailingStopInput::from_slices(
247            high,
248            low,
249            close,
250            FibonacciTrailingStopParams {
251                left_bars: self.left_bars,
252                right_bars: self.right_bars,
253                level: self.level,
254                trigger: Some(
255                    self.trigger
256                        .unwrap_or(TriggerMode::Close)
257                        .as_str()
258                        .to_string(),
259                ),
260            },
261        );
262        fibonacci_trailing_stop_with_kernel(&input, self.kernel)
263    }
264
265    #[inline]
266    pub fn into_stream(self) -> Result<FibonacciTrailingStopStream, FibonacciTrailingStopError> {
267        FibonacciTrailingStopStream::try_new(FibonacciTrailingStopParams {
268            left_bars: self.left_bars,
269            right_bars: self.right_bars,
270            level: self.level,
271            trigger: Some(
272                self.trigger
273                    .unwrap_or(TriggerMode::Close)
274                    .as_str()
275                    .to_string(),
276            ),
277        })
278    }
279}
280
281#[derive(Debug, Error)]
282pub enum FibonacciTrailingStopError {
283    #[error("fibonacci_trailing_stop: Input data slice is empty.")]
284    EmptyInputData,
285    #[error("fibonacci_trailing_stop: Input slice lengths differ: high={high_len}, low={low_len}, close={close_len}.")]
286    MismatchedInputLengths {
287        high_len: usize,
288        low_len: usize,
289        close_len: usize,
290    },
291    #[error("fibonacci_trailing_stop: All values are NaN.")]
292    AllValuesNaN,
293    #[error(
294        "fibonacci_trailing_stop: Invalid left_bars: left_bars = {left_bars}, data length = {data_len}"
295    )]
296    InvalidLeftBars { left_bars: usize, data_len: usize },
297    #[error(
298        "fibonacci_trailing_stop: Invalid right_bars: right_bars = {right_bars}, data length = {data_len}"
299    )]
300    InvalidRightBars { right_bars: usize, data_len: usize },
301    #[error("fibonacci_trailing_stop: Invalid level: {level}")]
302    InvalidLevel { level: f64 },
303    #[error("fibonacci_trailing_stop: Invalid trigger: {trigger}")]
304    InvalidTrigger { trigger: String },
305    #[error("fibonacci_trailing_stop: Not enough valid data: needed = {needed}, valid = {valid}")]
306    NotEnoughValidData { needed: usize, valid: usize },
307    #[error("fibonacci_trailing_stop: Output length mismatch: expected = {expected}")]
308    OutputLengthMismatch { expected: usize },
309    #[error("fibonacci_trailing_stop: Invalid range: start={start}, end={end}, step={step}")]
310    InvalidRange {
311        start: String,
312        end: String,
313        step: String,
314    },
315    #[error("fibonacci_trailing_stop: Invalid kernel for batch: {0:?}")]
316    InvalidKernelForBatch(Kernel),
317}
318
319#[derive(Clone, Copy, Debug)]
320struct ResolvedParams {
321    left_bars: usize,
322    right_bars: usize,
323    left_small: usize,
324    right_small: usize,
325    level: f64,
326    trigger: TriggerMode,
327}
328
329#[derive(Clone, Copy, Debug)]
330struct PivotPoint {
331    price: f64,
332    dir: i8,
333}
334
335#[derive(Clone, Debug)]
336struct CoreState {
337    trigger: TriggerMode,
338    level: f64,
339    dir: i8,
340    st: f64,
341    max_level: f64,
342    min_level: f64,
343    pivots: Vec<PivotPoint>,
344}
345
346impl CoreState {
347    #[inline]
348    fn new(high: f64, low: f64, close: f64, params: ResolvedParams) -> Self {
349        Self {
350            trigger: params.trigger,
351            level: params.level,
352            dir: 0,
353            st: close,
354            max_level: high,
355            min_level: low,
356            pivots: Vec::with_capacity(3),
357        }
358    }
359
360    #[inline]
361    fn update_pivots(&mut self, ph: Option<f64>, pl: Option<f64>) {
362        if let Some(value) = ph {
363            if let Some(first) = self.pivots.first_mut() {
364                if first.dir > 0 && value > first.price {
365                    first.price = value;
366                } else if first.dir < 0 && value > first.price {
367                    self.pivots.insert(
368                        0,
369                        PivotPoint {
370                            price: value,
371                            dir: 1,
372                        },
373                    );
374                }
375            } else {
376                self.pivots.push(PivotPoint {
377                    price: value,
378                    dir: 1,
379                });
380            }
381        }
382
383        if let Some(value) = pl {
384            if let Some(first) = self.pivots.first_mut() {
385                if first.dir < 0 && value < first.price {
386                    first.price = value;
387                } else if first.dir > 0 && value < first.price {
388                    self.pivots.insert(
389                        0,
390                        PivotPoint {
391                            price: value,
392                            dir: -1,
393                        },
394                    );
395                }
396            } else {
397                self.pivots.push(PivotPoint {
398                    price: value,
399                    dir: -1,
400                });
401            }
402        }
403
404        if self.pivots.len() > 3 {
405            self.pivots.truncate(3);
406        }
407    }
408
409    #[inline]
410    fn apply_bar(
411        &mut self,
412        high: f64,
413        low: f64,
414        close: f64,
415        ph: Option<f64>,
416        pl: Option<f64>,
417    ) -> FibonacciTrailingStopPoint {
418        self.update_pivots(ph, pl);
419
420        if self.pivots.len() >= 2 {
421            let p0 = self.pivots[0].price;
422            let p1 = self.pivots[1].price;
423            let mut max_value = p0.max(p1);
424            let mut min_value = p0.min(p1);
425            if self.pivots.len() == 2 {
426                self.st = (max_value + min_value) * 0.5;
427            }
428            let dif = max_value - min_value;
429            max_value += dif * self.level;
430            min_value -= dif * self.level;
431            self.max_level = max_value;
432            self.min_level = min_value;
433        }
434
435        let price = match self.trigger {
436            TriggerMode::Close => close,
437            TriggerMode::Wick => {
438                if self.dir < 1 {
439                    high
440                } else {
441                    low
442                }
443            }
444        };
445
446        if self.dir < 1 {
447            if price > self.st {
448                self.st = self.min_level;
449                self.dir = 1;
450            } else {
451                self.st = self.st.min(self.max_level);
452            }
453        }
454
455        if self.dir > -1 {
456            if price < self.st {
457                self.st = self.max_level;
458                self.dir = -1;
459            } else {
460                self.st = self.st.max(self.min_level);
461            }
462        }
463
464        FibonacciTrailingStopPoint {
465            trailing_stop: self.st,
466            long_stop: if self.dir == 1 { self.st } else { f64::NAN },
467            short_stop: if self.dir == -1 { self.st } else { f64::NAN },
468            direction: self.dir as f64,
469        }
470    }
471}
472
473#[inline(always)]
474fn first_valid_ohlc(high: &[f64], low: &[f64], close: &[f64]) -> usize {
475    for i in 0..high.len() {
476        if high[i].is_finite() && low[i].is_finite() && close[i].is_finite() {
477            return i;
478        }
479    }
480    high.len()
481}
482
483#[inline(always)]
484fn max_consecutive_valid_ohlc(high: &[f64], low: &[f64], close: &[f64]) -> usize {
485    let mut best = 0usize;
486    let mut run = 0usize;
487    for i in 0..high.len() {
488        if high[i].is_finite() && low[i].is_finite() && close[i].is_finite() {
489            run += 1;
490            if run > best {
491                best = run;
492            }
493        } else {
494            run = 0;
495        }
496    }
497    best
498}
499
500#[inline(always)]
501fn canonical_trigger_name(trigger: Option<&str>) -> String {
502    trigger.unwrap_or(DEFAULT_TRIGGER).to_ascii_lowercase()
503}
504
505#[inline]
506fn resolve_params(
507    params: &FibonacciTrailingStopParams,
508    data_len: Option<usize>,
509) -> Result<ResolvedParams, FibonacciTrailingStopError> {
510    let left_bars = params.left_bars.unwrap_or(DEFAULT_LEFT_BARS);
511    let right_bars = params.right_bars.unwrap_or(DEFAULT_RIGHT_BARS);
512    let level = params.level.unwrap_or(DEFAULT_LEVEL);
513    let trigger_name = canonical_trigger_name(params.trigger.as_deref());
514    let trigger = TriggerMode::parse(&trigger_name).ok_or_else(|| {
515        FibonacciTrailingStopError::InvalidTrigger {
516            trigger: trigger_name.clone(),
517        }
518    })?;
519
520    if left_bars == 0 {
521        return Err(FibonacciTrailingStopError::InvalidLeftBars {
522            left_bars,
523            data_len: data_len.unwrap_or(0),
524        });
525    }
526    if right_bars == 0 {
527        return Err(FibonacciTrailingStopError::InvalidRightBars {
528            right_bars,
529            data_len: data_len.unwrap_or(0),
530        });
531    }
532    if !level.is_finite() {
533        return Err(FibonacciTrailingStopError::InvalidLevel { level });
534    }
535    if let Some(len) = data_len {
536        let needed = left_bars + right_bars + 1;
537        if needed > len {
538            return Err(FibonacciTrailingStopError::NotEnoughValidData { needed, valid: len });
539        }
540    }
541
542    Ok(ResolvedParams {
543        left_bars,
544        right_bars,
545        left_small: ((left_bars + 1) / 2).max(1),
546        right_small: ((right_bars + 1) / 2).max(1),
547        level,
548        trigger,
549    })
550}
551
552#[inline(always)]
553fn confirmed_pivot_high_at(data: &[f64], idx: usize, left: usize, right: usize) -> Option<f64> {
554    if idx < right {
555        return None;
556    }
557    let center = idx - right;
558    if center < left || center + right >= data.len() {
559        return None;
560    }
561    let candidate = data[center];
562    if !candidate.is_finite() {
563        return None;
564    }
565    for &value in &data[(center - left)..=(center + right)] {
566        if !value.is_finite() || value > candidate {
567            return None;
568        }
569    }
570    Some(candidate)
571}
572
573#[inline(always)]
574fn confirmed_pivot_low_at(data: &[f64], idx: usize, left: usize, right: usize) -> Option<f64> {
575    if idx < right {
576        return None;
577    }
578    let center = idx - right;
579    if center < left || center + right >= data.len() {
580        return None;
581    }
582    let candidate = data[center];
583    if !candidate.is_finite() {
584        return None;
585    }
586    for &value in &data[(center - left)..=(center + right)] {
587        if !value.is_finite() || value < candidate {
588            return None;
589        }
590    }
591    Some(candidate)
592}
593
594#[inline(always)]
595fn buffer_pivot_high(data: &VecDeque<f64>, left: usize, right: usize) -> Option<f64> {
596    if data.len() < left + right + 1 {
597        return None;
598    }
599    let center = data.len() - 1 - right;
600    let candidate = data[center];
601    if !candidate.is_finite() {
602        return None;
603    }
604    for i in (center - left)..=(center + right) {
605        let value = data[i];
606        if !value.is_finite() || value > candidate {
607            return None;
608        }
609    }
610    Some(candidate)
611}
612
613#[inline(always)]
614fn buffer_pivot_low(data: &VecDeque<f64>, left: usize, right: usize) -> Option<f64> {
615    if data.len() < left + right + 1 {
616        return None;
617    }
618    let center = data.len() - 1 - right;
619    let candidate = data[center];
620    if !candidate.is_finite() {
621        return None;
622    }
623    for i in (center - left)..=(center + right) {
624        let value = data[i];
625        if !value.is_finite() || value < candidate {
626            return None;
627        }
628    }
629    Some(candidate)
630}
631
632#[derive(Clone, Debug)]
633pub struct FibonacciTrailingStopStream {
634    params: ResolvedParams,
635    state: Option<CoreState>,
636    high_buf: VecDeque<f64>,
637    low_buf: VecDeque<f64>,
638    max_window: usize,
639}
640
641impl FibonacciTrailingStopStream {
642    #[inline]
643    pub fn try_new(
644        params: FibonacciTrailingStopParams,
645    ) -> Result<Self, FibonacciTrailingStopError> {
646        let params = resolve_params(&params, None)?;
647        let max_window = (params.left_bars + params.right_bars + 1)
648            .max(params.left_small + params.right_small + 1);
649        Ok(Self {
650            params,
651            state: None,
652            high_buf: VecDeque::with_capacity(max_window),
653            low_buf: VecDeque::with_capacity(max_window),
654            max_window,
655        })
656    }
657
658    #[inline]
659    pub fn reset(&mut self) {
660        self.state = None;
661        self.high_buf.clear();
662        self.low_buf.clear();
663    }
664
665    #[inline]
666    pub fn update(
667        &mut self,
668        high: f64,
669        low: f64,
670        close: f64,
671    ) -> Option<FibonacciTrailingStopPoint> {
672        if !(high.is_finite() && low.is_finite() && close.is_finite()) {
673            self.reset();
674            return None;
675        }
676
677        self.high_buf.push_back(high);
678        self.low_buf.push_back(low);
679        if self.high_buf.len() > self.max_window {
680            self.high_buf.pop_front();
681            self.low_buf.pop_front();
682        }
683
684        let ph = buffer_pivot_high(
685            &self.high_buf,
686            self.params.left_bars,
687            self.params.right_bars,
688        );
689        let pl = buffer_pivot_low(&self.low_buf, self.params.left_bars, self.params.right_bars);
690
691        let state = self
692            .state
693            .get_or_insert_with(|| CoreState::new(high, low, close, self.params));
694        Some(state.apply_bar(high, low, close, ph, pl))
695    }
696}
697
698#[inline]
699fn fibonacci_trailing_stop_prepare<'a>(
700    input: &'a FibonacciTrailingStopInput,
701    kernel: Kernel,
702) -> Result<(&'a [f64], &'a [f64], &'a [f64], ResolvedParams, Kernel), FibonacciTrailingStopError> {
703    let (high, low, close) = input.as_slices();
704    if high.is_empty() || low.is_empty() || close.is_empty() {
705        return Err(FibonacciTrailingStopError::EmptyInputData);
706    }
707    if high.len() != low.len() || high.len() != close.len() {
708        return Err(FibonacciTrailingStopError::MismatchedInputLengths {
709            high_len: high.len(),
710            low_len: low.len(),
711            close_len: close.len(),
712        });
713    }
714
715    let first = first_valid_ohlc(high, low, close);
716    if first >= close.len() {
717        return Err(FibonacciTrailingStopError::AllValuesNaN);
718    }
719
720    let params = resolve_params(&input.params, Some(close.len()))?;
721    let needed = params.left_bars + params.right_bars + 1;
722    let valid = max_consecutive_valid_ohlc(high, low, close);
723    if valid < needed {
724        return Err(FibonacciTrailingStopError::NotEnoughValidData { needed, valid });
725    }
726
727    let chosen = match kernel {
728        Kernel::Auto => detect_best_kernel(),
729        other => other.to_non_batch(),
730    };
731    Ok((high, low, close, params, chosen))
732}
733
734fn fibonacci_trailing_stop_row_from_slices(
735    high: &[f64],
736    low: &[f64],
737    close: &[f64],
738    params: ResolvedParams,
739    trailing_stop: &mut [f64],
740    long_stop: &mut [f64],
741    short_stop: &mut [f64],
742    direction: &mut [f64],
743) {
744    trailing_stop.fill(f64::NAN);
745    long_stop.fill(f64::NAN);
746    short_stop.fill(f64::NAN);
747    direction.fill(f64::NAN);
748
749    let mut state: Option<CoreState> = None;
750    for i in 0..close.len() {
751        let h = high[i];
752        let l = low[i];
753        let c = close[i];
754        if !(h.is_finite() && l.is_finite() && c.is_finite()) {
755            state = None;
756            continue;
757        }
758
759        let ph = confirmed_pivot_high_at(high, i, params.left_bars, params.right_bars);
760        let pl = confirmed_pivot_low_at(low, i, params.left_bars, params.right_bars);
761
762        let point = state
763            .get_or_insert_with(|| CoreState::new(h, l, c, params))
764            .apply_bar(h, l, c, ph, pl);
765
766        trailing_stop[i] = point.trailing_stop;
767        long_stop[i] = point.long_stop;
768        short_stop[i] = point.short_stop;
769        direction[i] = point.direction;
770    }
771}
772
773#[inline]
774pub fn fibonacci_trailing_stop(
775    input: &FibonacciTrailingStopInput,
776) -> Result<FibonacciTrailingStopOutput, FibonacciTrailingStopError> {
777    fibonacci_trailing_stop_with_kernel(input, Kernel::Auto)
778}
779
780#[inline]
781pub fn fibonacci_trailing_stop_with_kernel(
782    input: &FibonacciTrailingStopInput,
783    kernel: Kernel,
784) -> Result<FibonacciTrailingStopOutput, FibonacciTrailingStopError> {
785    let (high, low, close, params, _chosen) = fibonacci_trailing_stop_prepare(input, kernel)?;
786    let len = close.len();
787    let mut trailing_stop = vec![f64::NAN; len];
788    let mut long_stop = vec![f64::NAN; len];
789    let mut short_stop = vec![f64::NAN; len];
790    let mut direction = vec![f64::NAN; len];
791    fibonacci_trailing_stop_row_from_slices(
792        high,
793        low,
794        close,
795        params,
796        &mut trailing_stop,
797        &mut long_stop,
798        &mut short_stop,
799        &mut direction,
800    );
801    Ok(FibonacciTrailingStopOutput {
802        trailing_stop,
803        long_stop,
804        short_stop,
805        direction,
806    })
807}
808
809#[inline]
810pub fn fibonacci_trailing_stop_into_slices(
811    trailing_stop: &mut [f64],
812    long_stop: &mut [f64],
813    short_stop: &mut [f64],
814    direction: &mut [f64],
815    input: &FibonacciTrailingStopInput,
816    kernel: Kernel,
817) -> Result<(), FibonacciTrailingStopError> {
818    let expected = input.as_slices().2.len();
819    if trailing_stop.len() != expected
820        || long_stop.len() != expected
821        || short_stop.len() != expected
822        || direction.len() != expected
823    {
824        return Err(FibonacciTrailingStopError::OutputLengthMismatch { expected });
825    }
826    let (high, low, close, params, _chosen) = fibonacci_trailing_stop_prepare(input, kernel)?;
827    fibonacci_trailing_stop_row_from_slices(
828        high,
829        low,
830        close,
831        params,
832        trailing_stop,
833        long_stop,
834        short_stop,
835        direction,
836    );
837    Ok(())
838}
839
840#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
841#[inline]
842pub fn fibonacci_trailing_stop_into(
843    input: &FibonacciTrailingStopInput,
844    trailing_stop: &mut [f64],
845    long_stop: &mut [f64],
846    short_stop: &mut [f64],
847    direction: &mut [f64],
848) -> Result<(), FibonacciTrailingStopError> {
849    fibonacci_trailing_stop_into_slices(
850        trailing_stop,
851        long_stop,
852        short_stop,
853        direction,
854        input,
855        Kernel::Auto,
856    )
857}
858
859#[derive(Debug, Clone, PartialEq)]
860#[cfg_attr(
861    all(target_arch = "wasm32", feature = "wasm"),
862    derive(Serialize, Deserialize)
863)]
864pub struct FibonacciTrailingStopBatchRange {
865    pub left_bars: (usize, usize, usize),
866    pub right_bars: (usize, usize, usize),
867    pub level: (f64, f64, f64),
868    pub trigger: Option<String>,
869}
870
871impl Default for FibonacciTrailingStopBatchRange {
872    fn default() -> Self {
873        Self {
874            left_bars: (DEFAULT_LEFT_BARS, DEFAULT_LEFT_BARS, 0),
875            right_bars: (DEFAULT_RIGHT_BARS, DEFAULT_RIGHT_BARS, 0),
876            level: (DEFAULT_LEVEL, DEFAULT_LEVEL, 0.0),
877            trigger: Some(DEFAULT_TRIGGER.to_string()),
878        }
879    }
880}
881
882#[derive(Debug, Clone)]
883pub struct FibonacciTrailingStopBatchOutput {
884    pub trailing_stop: Vec<f64>,
885    pub long_stop: Vec<f64>,
886    pub short_stop: Vec<f64>,
887    pub direction: Vec<f64>,
888    pub combos: Vec<FibonacciTrailingStopParams>,
889    pub rows: usize,
890    pub cols: usize,
891}
892
893#[derive(Clone, Debug, Default)]
894pub struct FibonacciTrailingStopBatchBuilder {
895    range: FibonacciTrailingStopBatchRange,
896    kernel: Kernel,
897}
898
899impl FibonacciTrailingStopBatchBuilder {
900    #[inline]
901    pub fn new() -> Self {
902        Self::default()
903    }
904
905    #[inline]
906    pub fn kernel(mut self, kernel: Kernel) -> Self {
907        self.kernel = kernel;
908        self
909    }
910
911    #[inline]
912    pub fn left_bars_range(mut self, start: usize, end: usize, step: usize) -> Self {
913        self.range.left_bars = (start, end, step);
914        self
915    }
916
917    #[inline]
918    pub fn right_bars_range(mut self, start: usize, end: usize, step: usize) -> Self {
919        self.range.right_bars = (start, end, step);
920        self
921    }
922
923    #[inline]
924    pub fn level_range(mut self, start: f64, end: f64, step: f64) -> Self {
925        self.range.level = (start, end, step);
926        self
927    }
928
929    #[inline]
930    pub fn trigger<T: Into<String>>(mut self, trigger: T) -> Self {
931        self.range.trigger = Some(trigger.into());
932        self
933    }
934
935    #[inline]
936    pub fn apply_slices(
937        self,
938        high: &[f64],
939        low: &[f64],
940        close: &[f64],
941    ) -> Result<FibonacciTrailingStopBatchOutput, FibonacciTrailingStopError> {
942        fibonacci_trailing_stop_batch_with_kernel(high, low, close, &self.range, self.kernel)
943    }
944
945    #[inline]
946    pub fn apply_candles(
947        self,
948        candles: &Candles,
949    ) -> Result<FibonacciTrailingStopBatchOutput, FibonacciTrailingStopError> {
950        self.apply_slices(&candles.high, &candles.low, &candles.close)
951    }
952}
953
954#[inline(always)]
955fn expand_axis_usize(
956    (start, end, step): (usize, usize, usize),
957) -> Result<Vec<usize>, FibonacciTrailingStopError> {
958    if step == 0 || start == end {
959        return Ok(vec![start]);
960    }
961    let mut out = Vec::new();
962    if start < end {
963        let mut value = start;
964        while value <= end {
965            out.push(value);
966            let next = value.saturating_add(step);
967            if next == value {
968                break;
969            }
970            value = next;
971        }
972    } else {
973        let mut value = start;
974        loop {
975            out.push(value);
976            if value == end {
977                break;
978            }
979            let next = value.saturating_sub(step);
980            if next == value || next < end {
981                break;
982            }
983            value = next;
984        }
985    }
986    if out.is_empty() {
987        return Err(FibonacciTrailingStopError::InvalidRange {
988            start: start.to_string(),
989            end: end.to_string(),
990            step: step.to_string(),
991        });
992    }
993    Ok(out)
994}
995
996#[inline(always)]
997fn expand_axis_f64(
998    start: f64,
999    end: f64,
1000    step: f64,
1001) -> Result<Vec<f64>, FibonacciTrailingStopError> {
1002    if !start.is_finite() || !end.is_finite() || !step.is_finite() || start > end {
1003        return Err(FibonacciTrailingStopError::InvalidRange {
1004            start: start.to_string(),
1005            end: end.to_string(),
1006            step: step.to_string(),
1007        });
1008    }
1009    if (start - end).abs() < FLOAT_TOL {
1010        if step.abs() > FLOAT_TOL {
1011            return Err(FibonacciTrailingStopError::InvalidRange {
1012                start: start.to_string(),
1013                end: end.to_string(),
1014                step: step.to_string(),
1015            });
1016        }
1017        return Ok(vec![start]);
1018    }
1019    if step <= 0.0 {
1020        return Err(FibonacciTrailingStopError::InvalidRange {
1021            start: start.to_string(),
1022            end: end.to_string(),
1023            step: step.to_string(),
1024        });
1025    }
1026    let mut out = Vec::new();
1027    let mut value = start;
1028    while value <= end + FLOAT_TOL {
1029        out.push(value.min(end));
1030        value += step;
1031    }
1032    if (out.last().copied().unwrap_or(start) - end).abs() > 1e-9 {
1033        return Err(FibonacciTrailingStopError::InvalidRange {
1034            start: start.to_string(),
1035            end: end.to_string(),
1036            step: step.to_string(),
1037        });
1038    }
1039    Ok(out)
1040}
1041
1042fn expand_grid_fibonacci_trailing_stop(
1043    sweep: &FibonacciTrailingStopBatchRange,
1044) -> Result<Vec<FibonacciTrailingStopParams>, FibonacciTrailingStopError> {
1045    let left_values = expand_axis_usize(sweep.left_bars)?;
1046    let right_values = expand_axis_usize(sweep.right_bars)?;
1047    let level_values = expand_axis_f64(sweep.level.0, sweep.level.1, sweep.level.2)?;
1048    let trigger_name = canonical_trigger_name(sweep.trigger.as_deref());
1049    let mut combos = Vec::with_capacity(
1050        left_values
1051            .len()
1052            .saturating_mul(right_values.len())
1053            .saturating_mul(level_values.len()),
1054    );
1055    for left_bars in left_values {
1056        for &right_bars in &right_values {
1057            for &level in &level_values {
1058                let params = FibonacciTrailingStopParams {
1059                    left_bars: Some(left_bars),
1060                    right_bars: Some(right_bars),
1061                    level: Some(level),
1062                    trigger: Some(trigger_name.clone()),
1063                };
1064                let _ = resolve_params(&params, None)?;
1065                combos.push(params);
1066            }
1067        }
1068    }
1069    Ok(combos)
1070}
1071
1072#[inline]
1073pub fn fibonacci_trailing_stop_batch_with_kernel(
1074    high: &[f64],
1075    low: &[f64],
1076    close: &[f64],
1077    sweep: &FibonacciTrailingStopBatchRange,
1078    kernel: Kernel,
1079) -> Result<FibonacciTrailingStopBatchOutput, FibonacciTrailingStopError> {
1080    let batch_kernel = match kernel {
1081        Kernel::Auto => detect_best_batch_kernel(),
1082        other if other.is_batch() => other,
1083        other => return Err(FibonacciTrailingStopError::InvalidKernelForBatch(other)),
1084    };
1085    fibonacci_trailing_stop_batch_par_slices(high, low, close, sweep, batch_kernel.to_non_batch())
1086}
1087
1088#[inline]
1089pub fn fibonacci_trailing_stop_batch_slices(
1090    high: &[f64],
1091    low: &[f64],
1092    close: &[f64],
1093    sweep: &FibonacciTrailingStopBatchRange,
1094    kernel: Kernel,
1095) -> Result<FibonacciTrailingStopBatchOutput, FibonacciTrailingStopError> {
1096    fibonacci_trailing_stop_batch_inner(high, low, close, sweep, kernel, false)
1097}
1098
1099#[inline]
1100pub fn fibonacci_trailing_stop_batch_par_slices(
1101    high: &[f64],
1102    low: &[f64],
1103    close: &[f64],
1104    sweep: &FibonacciTrailingStopBatchRange,
1105    kernel: Kernel,
1106) -> Result<FibonacciTrailingStopBatchOutput, FibonacciTrailingStopError> {
1107    fibonacci_trailing_stop_batch_inner(high, low, close, sweep, kernel, true)
1108}
1109
1110pub fn fibonacci_trailing_stop_batch_inner(
1111    high: &[f64],
1112    low: &[f64],
1113    close: &[f64],
1114    sweep: &FibonacciTrailingStopBatchRange,
1115    _kernel: Kernel,
1116    parallel: bool,
1117) -> Result<FibonacciTrailingStopBatchOutput, FibonacciTrailingStopError> {
1118    if high.is_empty() || low.is_empty() || close.is_empty() {
1119        return Err(FibonacciTrailingStopError::EmptyInputData);
1120    }
1121    if high.len() != low.len() || high.len() != close.len() {
1122        return Err(FibonacciTrailingStopError::MismatchedInputLengths {
1123            high_len: high.len(),
1124            low_len: low.len(),
1125            close_len: close.len(),
1126        });
1127    }
1128    let first = first_valid_ohlc(high, low, close);
1129    if first >= close.len() {
1130        return Err(FibonacciTrailingStopError::AllValuesNaN);
1131    }
1132
1133    let combos = expand_grid_fibonacci_trailing_stop(sweep)?;
1134    let rows = combos.len();
1135    let cols = close.len();
1136    let total = rows
1137        .checked_mul(cols)
1138        .ok_or(FibonacciTrailingStopError::OutputLengthMismatch {
1139            expected: usize::MAX,
1140        })?;
1141    let resolved = combos
1142        .iter()
1143        .map(|params| resolve_params(params, Some(cols)))
1144        .collect::<Result<Vec<_>, _>>()?;
1145    let max_valid = max_consecutive_valid_ohlc(high, low, close);
1146    for params in &resolved {
1147        let needed = params.left_bars + params.right_bars + 1;
1148        if max_valid < needed {
1149            return Err(FibonacciTrailingStopError::NotEnoughValidData {
1150                needed,
1151                valid: max_valid,
1152            });
1153        }
1154    }
1155
1156    let zero_prefixes = vec![0usize; rows];
1157    let mut trailing_stop_mu = make_uninit_matrix(rows, cols);
1158    init_matrix_prefixes(&mut trailing_stop_mu, cols, &zero_prefixes);
1159    let mut trailing_stop_guard = ManuallyDrop::new(trailing_stop_mu);
1160    let trailing_stop_out = unsafe {
1161        std::slice::from_raw_parts_mut(trailing_stop_guard.as_mut_ptr() as *mut f64, total)
1162    };
1163
1164    let mut long_stop_mu = make_uninit_matrix(rows, cols);
1165    init_matrix_prefixes(&mut long_stop_mu, cols, &zero_prefixes);
1166    let mut long_stop_guard = ManuallyDrop::new(long_stop_mu);
1167    let long_stop_out =
1168        unsafe { std::slice::from_raw_parts_mut(long_stop_guard.as_mut_ptr() as *mut f64, total) };
1169
1170    let mut short_stop_mu = make_uninit_matrix(rows, cols);
1171    init_matrix_prefixes(&mut short_stop_mu, cols, &zero_prefixes);
1172    let mut short_stop_guard = ManuallyDrop::new(short_stop_mu);
1173    let short_stop_out =
1174        unsafe { std::slice::from_raw_parts_mut(short_stop_guard.as_mut_ptr() as *mut f64, total) };
1175
1176    let mut direction_mu = make_uninit_matrix(rows, cols);
1177    init_matrix_prefixes(&mut direction_mu, cols, &zero_prefixes);
1178    let mut direction_guard = ManuallyDrop::new(direction_mu);
1179    let direction_out =
1180        unsafe { std::slice::from_raw_parts_mut(direction_guard.as_mut_ptr() as *mut f64, total) };
1181
1182    if parallel {
1183        #[cfg(not(target_arch = "wasm32"))]
1184        {
1185            let trailing_stop_ptr = trailing_stop_out.as_mut_ptr() as usize;
1186            let long_stop_ptr = long_stop_out.as_mut_ptr() as usize;
1187            let short_stop_ptr = short_stop_out.as_mut_ptr() as usize;
1188            let direction_ptr = direction_out.as_mut_ptr() as usize;
1189            resolved
1190                .par_iter()
1191                .enumerate()
1192                .for_each(|(row, params)| unsafe {
1193                    let start = row * cols;
1194                    fibonacci_trailing_stop_row_from_slices(
1195                        high,
1196                        low,
1197                        close,
1198                        *params,
1199                        std::slice::from_raw_parts_mut(
1200                            (trailing_stop_ptr as *mut f64).add(start),
1201                            cols,
1202                        ),
1203                        std::slice::from_raw_parts_mut(
1204                            (long_stop_ptr as *mut f64).add(start),
1205                            cols,
1206                        ),
1207                        std::slice::from_raw_parts_mut(
1208                            (short_stop_ptr as *mut f64).add(start),
1209                            cols,
1210                        ),
1211                        std::slice::from_raw_parts_mut(
1212                            (direction_ptr as *mut f64).add(start),
1213                            cols,
1214                        ),
1215                    );
1216                });
1217        }
1218
1219        #[cfg(target_arch = "wasm32")]
1220        for (row, params) in resolved.iter().enumerate() {
1221            let start = row * cols;
1222            let end = start + cols;
1223            fibonacci_trailing_stop_row_from_slices(
1224                high,
1225                low,
1226                close,
1227                *params,
1228                &mut trailing_stop_out[start..end],
1229                &mut long_stop_out[start..end],
1230                &mut short_stop_out[start..end],
1231                &mut direction_out[start..end],
1232            );
1233        }
1234    } else {
1235        for (row, params) in resolved.iter().enumerate() {
1236            let start = row * cols;
1237            let end = start + cols;
1238            fibonacci_trailing_stop_row_from_slices(
1239                high,
1240                low,
1241                close,
1242                *params,
1243                &mut trailing_stop_out[start..end],
1244                &mut long_stop_out[start..end],
1245                &mut short_stop_out[start..end],
1246                &mut direction_out[start..end],
1247            );
1248        }
1249    }
1250
1251    let trailing_stop = unsafe {
1252        Vec::from_raw_parts(
1253            trailing_stop_guard.as_mut_ptr() as *mut f64,
1254            trailing_stop_guard.len(),
1255            trailing_stop_guard.capacity(),
1256        )
1257    };
1258    let long_stop = unsafe {
1259        Vec::from_raw_parts(
1260            long_stop_guard.as_mut_ptr() as *mut f64,
1261            long_stop_guard.len(),
1262            long_stop_guard.capacity(),
1263        )
1264    };
1265    let short_stop = unsafe {
1266        Vec::from_raw_parts(
1267            short_stop_guard.as_mut_ptr() as *mut f64,
1268            short_stop_guard.len(),
1269            short_stop_guard.capacity(),
1270        )
1271    };
1272    let direction = unsafe {
1273        Vec::from_raw_parts(
1274            direction_guard.as_mut_ptr() as *mut f64,
1275            direction_guard.len(),
1276            direction_guard.capacity(),
1277        )
1278    };
1279    core::mem::forget(trailing_stop_guard);
1280    core::mem::forget(long_stop_guard);
1281    core::mem::forget(short_stop_guard);
1282    core::mem::forget(direction_guard);
1283
1284    Ok(FibonacciTrailingStopBatchOutput {
1285        trailing_stop,
1286        long_stop,
1287        short_stop,
1288        direction,
1289        combos,
1290        rows,
1291        cols,
1292    })
1293}
1294
1295pub fn fibonacci_trailing_stop_batch_inner_into(
1296    high: &[f64],
1297    low: &[f64],
1298    close: &[f64],
1299    sweep: &FibonacciTrailingStopBatchRange,
1300    kernel: Kernel,
1301    parallel: bool,
1302    trailing_stop: &mut [f64],
1303    long_stop: &mut [f64],
1304    short_stop: &mut [f64],
1305    direction: &mut [f64],
1306) -> Result<Vec<FibonacciTrailingStopParams>, FibonacciTrailingStopError> {
1307    let out = fibonacci_trailing_stop_batch_inner(high, low, close, sweep, kernel, parallel)?;
1308    let total = out.rows * out.cols;
1309    if trailing_stop.len() != total
1310        || long_stop.len() != total
1311        || short_stop.len() != total
1312        || direction.len() != total
1313    {
1314        return Err(FibonacciTrailingStopError::OutputLengthMismatch { expected: total });
1315    }
1316    trailing_stop.copy_from_slice(&out.trailing_stop);
1317    long_stop.copy_from_slice(&out.long_stop);
1318    short_stop.copy_from_slice(&out.short_stop);
1319    direction.copy_from_slice(&out.direction);
1320    Ok(out.combos)
1321}
1322
1323#[cfg(feature = "python")]
1324#[pyfunction(name = "fibonacci_trailing_stop")]
1325#[pyo3(signature = (
1326    high,
1327    low,
1328    close,
1329    left_bars=DEFAULT_LEFT_BARS,
1330    right_bars=DEFAULT_RIGHT_BARS,
1331    level=DEFAULT_LEVEL,
1332    trigger=DEFAULT_TRIGGER,
1333    kernel=None
1334))]
1335pub fn fibonacci_trailing_stop_py<'py>(
1336    py: Python<'py>,
1337    high: PyReadonlyArray1<'py, f64>,
1338    low: PyReadonlyArray1<'py, f64>,
1339    close: PyReadonlyArray1<'py, f64>,
1340    left_bars: usize,
1341    right_bars: usize,
1342    level: f64,
1343    trigger: &str,
1344    kernel: Option<&str>,
1345) -> PyResult<(
1346    Bound<'py, PyArray1<f64>>,
1347    Bound<'py, PyArray1<f64>>,
1348    Bound<'py, PyArray1<f64>>,
1349    Bound<'py, PyArray1<f64>>,
1350)> {
1351    let high = high.as_slice()?;
1352    let low = low.as_slice()?;
1353    let close = close.as_slice()?;
1354    let kernel = validate_kernel(kernel, false)?;
1355    let input = FibonacciTrailingStopInput::from_slices(
1356        high,
1357        low,
1358        close,
1359        FibonacciTrailingStopParams {
1360            left_bars: Some(left_bars),
1361            right_bars: Some(right_bars),
1362            level: Some(level),
1363            trigger: Some(trigger.to_string()),
1364        },
1365    );
1366    let out = py
1367        .allow_threads(|| fibonacci_trailing_stop_with_kernel(&input, kernel))
1368        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1369    Ok((
1370        out.trailing_stop.into_pyarray(py),
1371        out.long_stop.into_pyarray(py),
1372        out.short_stop.into_pyarray(py),
1373        out.direction.into_pyarray(py),
1374    ))
1375}
1376
1377#[cfg(feature = "python")]
1378#[pyclass(name = "FibonacciTrailingStopStream")]
1379pub struct FibonacciTrailingStopStreamPy {
1380    stream: FibonacciTrailingStopStream,
1381}
1382
1383#[cfg(feature = "python")]
1384#[pymethods]
1385impl FibonacciTrailingStopStreamPy {
1386    #[new]
1387    #[pyo3(signature = (
1388        left_bars=DEFAULT_LEFT_BARS,
1389        right_bars=DEFAULT_RIGHT_BARS,
1390        level=DEFAULT_LEVEL,
1391        trigger=DEFAULT_TRIGGER
1392    ))]
1393    fn new(left_bars: usize, right_bars: usize, level: f64, trigger: &str) -> PyResult<Self> {
1394        let stream = FibonacciTrailingStopStream::try_new(FibonacciTrailingStopParams {
1395            left_bars: Some(left_bars),
1396            right_bars: Some(right_bars),
1397            level: Some(level),
1398            trigger: Some(trigger.to_string()),
1399        })
1400        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1401        Ok(Self { stream })
1402    }
1403
1404    fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64, f64, f64)> {
1405        self.stream.update(high, low, close).map(|point| {
1406            (
1407                point.trailing_stop,
1408                point.long_stop,
1409                point.short_stop,
1410                point.direction,
1411            )
1412        })
1413    }
1414
1415    fn reset(&mut self) {
1416        self.stream.reset();
1417    }
1418}
1419
1420#[cfg(feature = "python")]
1421#[pyfunction(name = "fibonacci_trailing_stop_batch")]
1422#[pyo3(signature = (
1423    high,
1424    low,
1425    close,
1426    left_bars_range=(DEFAULT_LEFT_BARS, DEFAULT_LEFT_BARS, 0),
1427    right_bars_range=(DEFAULT_RIGHT_BARS, DEFAULT_RIGHT_BARS, 0),
1428    level_range=(DEFAULT_LEVEL, DEFAULT_LEVEL, 0.0),
1429    trigger=DEFAULT_TRIGGER,
1430    kernel=None
1431))]
1432pub fn fibonacci_trailing_stop_batch_py<'py>(
1433    py: Python<'py>,
1434    high: PyReadonlyArray1<'py, f64>,
1435    low: PyReadonlyArray1<'py, f64>,
1436    close: PyReadonlyArray1<'py, f64>,
1437    left_bars_range: (usize, usize, usize),
1438    right_bars_range: (usize, usize, usize),
1439    level_range: (f64, f64, f64),
1440    trigger: &str,
1441    kernel: Option<&str>,
1442) -> PyResult<Bound<'py, PyDict>> {
1443    let high = high.as_slice()?;
1444    let low = low.as_slice()?;
1445    let close = close.as_slice()?;
1446    let kernel = validate_kernel(kernel, true)?;
1447    let sweep = FibonacciTrailingStopBatchRange {
1448        left_bars: left_bars_range,
1449        right_bars: right_bars_range,
1450        level: level_range,
1451        trigger: Some(trigger.to_string()),
1452    };
1453    let combos = expand_grid_fibonacci_trailing_stop(&sweep)
1454        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1455    let rows = combos.len();
1456    let cols = close.len();
1457    let total = rows
1458        .checked_mul(cols)
1459        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1460
1461    let trailing_stop_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1462    let long_stop_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1463    let short_stop_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1464    let direction_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1465
1466    let trailing_stop_slice = unsafe { trailing_stop_arr.as_slice_mut()? };
1467    let long_stop_slice = unsafe { long_stop_arr.as_slice_mut()? };
1468    let short_stop_slice = unsafe { short_stop_arr.as_slice_mut()? };
1469    let direction_slice = unsafe { direction_arr.as_slice_mut()? };
1470
1471    let combos = py
1472        .allow_threads(|| {
1473            let batch_kernel = match kernel {
1474                Kernel::Auto => detect_best_batch_kernel(),
1475                other => other,
1476            };
1477            fibonacci_trailing_stop_batch_inner_into(
1478                high,
1479                low,
1480                close,
1481                &sweep,
1482                batch_kernel.to_non_batch(),
1483                true,
1484                trailing_stop_slice,
1485                long_stop_slice,
1486                short_stop_slice,
1487                direction_slice,
1488            )
1489        })
1490        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1491
1492    let dict = PyDict::new(py);
1493    dict.set_item("trailing_stop", trailing_stop_arr.reshape((rows, cols))?)?;
1494    dict.set_item("long_stop", long_stop_arr.reshape((rows, cols))?)?;
1495    dict.set_item("short_stop", short_stop_arr.reshape((rows, cols))?)?;
1496    dict.set_item("direction", direction_arr.reshape((rows, cols))?)?;
1497    dict.set_item(
1498        "left_bars",
1499        combos
1500            .iter()
1501            .map(|combo| combo.left_bars.unwrap_or(DEFAULT_LEFT_BARS) as u64)
1502            .collect::<Vec<_>>()
1503            .into_pyarray(py),
1504    )?;
1505    dict.set_item(
1506        "right_bars",
1507        combos
1508            .iter()
1509            .map(|combo| combo.right_bars.unwrap_or(DEFAULT_RIGHT_BARS) as u64)
1510            .collect::<Vec<_>>()
1511            .into_pyarray(py),
1512    )?;
1513    dict.set_item(
1514        "levels",
1515        combos
1516            .iter()
1517            .map(|combo| combo.level.unwrap_or(DEFAULT_LEVEL))
1518            .collect::<Vec<_>>()
1519            .into_pyarray(py),
1520    )?;
1521    dict.set_item("rows", rows)?;
1522    dict.set_item("cols", cols)?;
1523    Ok(dict)
1524}
1525
1526#[cfg(feature = "python")]
1527pub fn register_fibonacci_trailing_stop_module(
1528    module: &Bound<'_, pyo3::types::PyModule>,
1529) -> PyResult<()> {
1530    module.add_function(wrap_pyfunction!(fibonacci_trailing_stop_py, module)?)?;
1531    module.add_function(wrap_pyfunction!(fibonacci_trailing_stop_batch_py, module)?)?;
1532    module.add_class::<FibonacciTrailingStopStreamPy>()?;
1533    Ok(())
1534}
1535
1536#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1537#[derive(Serialize, Deserialize)]
1538pub struct FibonacciTrailingStopJsOutput {
1539    pub trailing_stop: Vec<f64>,
1540    pub long_stop: Vec<f64>,
1541    pub short_stop: Vec<f64>,
1542    pub direction: Vec<f64>,
1543}
1544
1545#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1546#[wasm_bindgen(js_name = "fibonacci_trailing_stop_js")]
1547pub fn fibonacci_trailing_stop_js(
1548    high: &[f64],
1549    low: &[f64],
1550    close: &[f64],
1551    left_bars: usize,
1552    right_bars: usize,
1553    level: f64,
1554    trigger: String,
1555) -> Result<JsValue, JsValue> {
1556    let input = FibonacciTrailingStopInput::from_slices(
1557        high,
1558        low,
1559        close,
1560        FibonacciTrailingStopParams {
1561            left_bars: Some(left_bars),
1562            right_bars: Some(right_bars),
1563            level: Some(level),
1564            trigger: Some(trigger),
1565        },
1566    );
1567    let out = fibonacci_trailing_stop(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
1568    serde_wasm_bindgen::to_value(&FibonacciTrailingStopJsOutput {
1569        trailing_stop: out.trailing_stop,
1570        long_stop: out.long_stop,
1571        short_stop: out.short_stop,
1572        direction: out.direction,
1573    })
1574    .map_err(|e| JsValue::from_str(&e.to_string()))
1575}
1576
1577#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1578#[wasm_bindgen]
1579pub fn fibonacci_trailing_stop_alloc(len: usize) -> *mut f64 {
1580    let mut vec = Vec::<f64>::with_capacity(len);
1581    let ptr = vec.as_mut_ptr();
1582    std::mem::forget(vec);
1583    ptr
1584}
1585
1586#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1587#[wasm_bindgen]
1588pub fn fibonacci_trailing_stop_free(ptr: *mut f64, len: usize) {
1589    if !ptr.is_null() {
1590        unsafe {
1591            let _ = Vec::from_raw_parts(ptr, len, len);
1592        }
1593    }
1594}
1595
1596#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1597fn has_duplicate_ptrs(ptrs: &[usize]) -> bool {
1598    for i in 0..ptrs.len() {
1599        for j in (i + 1)..ptrs.len() {
1600            if ptrs[i] == ptrs[j] {
1601                return true;
1602            }
1603        }
1604    }
1605    false
1606}
1607
1608#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1609#[wasm_bindgen]
1610pub fn fibonacci_trailing_stop_into(
1611    high_ptr: *const f64,
1612    low_ptr: *const f64,
1613    close_ptr: *const f64,
1614    trailing_stop_ptr: *mut f64,
1615    long_stop_ptr: *mut f64,
1616    short_stop_ptr: *mut f64,
1617    direction_ptr: *mut f64,
1618    len: usize,
1619    left_bars: usize,
1620    right_bars: usize,
1621    level: f64,
1622    trigger: String,
1623) -> Result<(), JsValue> {
1624    if high_ptr.is_null()
1625        || low_ptr.is_null()
1626        || close_ptr.is_null()
1627        || trailing_stop_ptr.is_null()
1628        || long_stop_ptr.is_null()
1629        || short_stop_ptr.is_null()
1630        || direction_ptr.is_null()
1631    {
1632        return Err(JsValue::from_str("Null pointer provided"));
1633    }
1634
1635    unsafe {
1636        let high = std::slice::from_raw_parts(high_ptr, len);
1637        let low = std::slice::from_raw_parts(low_ptr, len);
1638        let close = std::slice::from_raw_parts(close_ptr, len);
1639        let input = FibonacciTrailingStopInput::from_slices(
1640            high,
1641            low,
1642            close,
1643            FibonacciTrailingStopParams {
1644                left_bars: Some(left_bars),
1645                right_bars: Some(right_bars),
1646                level: Some(level),
1647                trigger: Some(trigger),
1648            },
1649        );
1650
1651        let output_ptrs = [
1652            trailing_stop_ptr as usize,
1653            long_stop_ptr as usize,
1654            short_stop_ptr as usize,
1655            direction_ptr as usize,
1656        ];
1657        let need_temp = output_ptrs.iter().any(|&ptr| {
1658            ptr == high_ptr as usize || ptr == low_ptr as usize || ptr == close_ptr as usize
1659        }) || has_duplicate_ptrs(&output_ptrs);
1660
1661        if need_temp {
1662            let mut trailing_stop = vec![0.0; len];
1663            let mut long_stop = vec![0.0; len];
1664            let mut short_stop = vec![0.0; len];
1665            let mut direction = vec![0.0; len];
1666            fibonacci_trailing_stop_into_slices(
1667                &mut trailing_stop,
1668                &mut long_stop,
1669                &mut short_stop,
1670                &mut direction,
1671                &input,
1672                Kernel::Auto,
1673            )
1674            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1675            std::slice::from_raw_parts_mut(trailing_stop_ptr, len).copy_from_slice(&trailing_stop);
1676            std::slice::from_raw_parts_mut(long_stop_ptr, len).copy_from_slice(&long_stop);
1677            std::slice::from_raw_parts_mut(short_stop_ptr, len).copy_from_slice(&short_stop);
1678            std::slice::from_raw_parts_mut(direction_ptr, len).copy_from_slice(&direction);
1679        } else {
1680            fibonacci_trailing_stop_into_slices(
1681                std::slice::from_raw_parts_mut(trailing_stop_ptr, len),
1682                std::slice::from_raw_parts_mut(long_stop_ptr, len),
1683                std::slice::from_raw_parts_mut(short_stop_ptr, len),
1684                std::slice::from_raw_parts_mut(direction_ptr, len),
1685                &input,
1686                Kernel::Auto,
1687            )
1688            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1689        }
1690    }
1691    Ok(())
1692}
1693
1694#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1695#[derive(Serialize, Deserialize)]
1696pub struct FibonacciTrailingStopBatchJsConfig {
1697    pub left_bars_range: Option<(usize, usize, usize)>,
1698    pub right_bars_range: Option<(usize, usize, usize)>,
1699    pub level_range: Option<(f64, f64, f64)>,
1700    pub trigger: Option<String>,
1701}
1702
1703#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1704#[derive(Serialize, Deserialize)]
1705pub struct FibonacciTrailingStopBatchJsOutput {
1706    pub trailing_stop: Vec<f64>,
1707    pub long_stop: Vec<f64>,
1708    pub short_stop: Vec<f64>,
1709    pub direction: Vec<f64>,
1710    pub combos: Vec<FibonacciTrailingStopParams>,
1711    pub left_bars: Vec<usize>,
1712    pub right_bars: Vec<usize>,
1713    pub levels: Vec<f64>,
1714    pub rows: usize,
1715    pub cols: usize,
1716}
1717
1718#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1719#[wasm_bindgen(js_name = "fibonacci_trailing_stop_batch_js")]
1720pub fn fibonacci_trailing_stop_batch_js(
1721    high: &[f64],
1722    low: &[f64],
1723    close: &[f64],
1724    config: JsValue,
1725) -> Result<JsValue, JsValue> {
1726    let config: FibonacciTrailingStopBatchJsConfig = serde_wasm_bindgen::from_value(config)
1727        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1728    let sweep = FibonacciTrailingStopBatchRange {
1729        left_bars: config
1730            .left_bars_range
1731            .unwrap_or((DEFAULT_LEFT_BARS, DEFAULT_LEFT_BARS, 0)),
1732        right_bars: config
1733            .right_bars_range
1734            .unwrap_or((DEFAULT_RIGHT_BARS, DEFAULT_RIGHT_BARS, 0)),
1735        level: config
1736            .level_range
1737            .unwrap_or((DEFAULT_LEVEL, DEFAULT_LEVEL, 0.0)),
1738        trigger: config.trigger.or_else(|| Some(DEFAULT_TRIGGER.to_string())),
1739    };
1740    let out = fibonacci_trailing_stop_batch_with_kernel(high, low, close, &sweep, Kernel::Auto)
1741        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1742    serde_wasm_bindgen::to_value(&FibonacciTrailingStopBatchJsOutput {
1743        left_bars: out
1744            .combos
1745            .iter()
1746            .map(|combo| combo.left_bars.unwrap_or(DEFAULT_LEFT_BARS))
1747            .collect(),
1748        right_bars: out
1749            .combos
1750            .iter()
1751            .map(|combo| combo.right_bars.unwrap_or(DEFAULT_RIGHT_BARS))
1752            .collect(),
1753        levels: out
1754            .combos
1755            .iter()
1756            .map(|combo| combo.level.unwrap_or(DEFAULT_LEVEL))
1757            .collect(),
1758        trailing_stop: out.trailing_stop,
1759        long_stop: out.long_stop,
1760        short_stop: out.short_stop,
1761        direction: out.direction,
1762        combos: out.combos,
1763        rows: out.rows,
1764        cols: out.cols,
1765    })
1766    .map_err(|e| JsValue::from_str(&e.to_string()))
1767}
1768
1769#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1770#[wasm_bindgen]
1771pub fn fibonacci_trailing_stop_batch_into(
1772    high_ptr: *const f64,
1773    low_ptr: *const f64,
1774    close_ptr: *const f64,
1775    trailing_stop_ptr: *mut f64,
1776    long_stop_ptr: *mut f64,
1777    short_stop_ptr: *mut f64,
1778    direction_ptr: *mut f64,
1779    len: usize,
1780    left_bars_start: usize,
1781    left_bars_end: usize,
1782    left_bars_step: usize,
1783    right_bars_start: usize,
1784    right_bars_end: usize,
1785    right_bars_step: usize,
1786    level_start: f64,
1787    level_end: f64,
1788    level_step: f64,
1789    trigger: String,
1790) -> Result<usize, JsValue> {
1791    if high_ptr.is_null()
1792        || low_ptr.is_null()
1793        || close_ptr.is_null()
1794        || trailing_stop_ptr.is_null()
1795        || long_stop_ptr.is_null()
1796        || short_stop_ptr.is_null()
1797        || direction_ptr.is_null()
1798    {
1799        return Err(JsValue::from_str("Null pointer provided"));
1800    }
1801
1802    let sweep = FibonacciTrailingStopBatchRange {
1803        left_bars: (left_bars_start, left_bars_end, left_bars_step),
1804        right_bars: (right_bars_start, right_bars_end, right_bars_step),
1805        level: (level_start, level_end, level_step),
1806        trigger: Some(trigger),
1807    };
1808
1809    unsafe {
1810        let high = std::slice::from_raw_parts(high_ptr, len);
1811        let low = std::slice::from_raw_parts(low_ptr, len);
1812        let close = std::slice::from_raw_parts(close_ptr, len);
1813        let combos = expand_grid_fibonacci_trailing_stop(&sweep)
1814            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1815        let rows = combos.len();
1816        let total = rows
1817            .checked_mul(len)
1818            .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1819
1820        let output_ptrs = [
1821            trailing_stop_ptr as usize,
1822            long_stop_ptr as usize,
1823            short_stop_ptr as usize,
1824            direction_ptr as usize,
1825        ];
1826        let need_temp = output_ptrs.iter().any(|&ptr| {
1827            ptr == high_ptr as usize || ptr == low_ptr as usize || ptr == close_ptr as usize
1828        }) || has_duplicate_ptrs(&output_ptrs);
1829
1830        if need_temp {
1831            let mut trailing_stop = vec![0.0; total];
1832            let mut long_stop = vec![0.0; total];
1833            let mut short_stop = vec![0.0; total];
1834            let mut direction = vec![0.0; total];
1835            let rows = fibonacci_trailing_stop_batch_inner_into(
1836                high,
1837                low,
1838                close,
1839                &sweep,
1840                Kernel::Auto,
1841                false,
1842                &mut trailing_stop,
1843                &mut long_stop,
1844                &mut short_stop,
1845                &mut direction,
1846            )
1847            .map_err(|e| JsValue::from_str(&e.to_string()))?
1848            .len();
1849            std::slice::from_raw_parts_mut(trailing_stop_ptr, total)
1850                .copy_from_slice(&trailing_stop);
1851            std::slice::from_raw_parts_mut(long_stop_ptr, total).copy_from_slice(&long_stop);
1852            std::slice::from_raw_parts_mut(short_stop_ptr, total).copy_from_slice(&short_stop);
1853            std::slice::from_raw_parts_mut(direction_ptr, total).copy_from_slice(&direction);
1854            Ok(rows)
1855        } else {
1856            let rows = fibonacci_trailing_stop_batch_inner_into(
1857                high,
1858                low,
1859                close,
1860                &sweep,
1861                Kernel::Auto,
1862                false,
1863                std::slice::from_raw_parts_mut(trailing_stop_ptr, total),
1864                std::slice::from_raw_parts_mut(long_stop_ptr, total),
1865                std::slice::from_raw_parts_mut(short_stop_ptr, total),
1866                std::slice::from_raw_parts_mut(direction_ptr, total),
1867            )
1868            .map_err(|e| JsValue::from_str(&e.to_string()))?
1869            .len();
1870            Ok(rows)
1871        }
1872    }
1873}
1874
1875#[cfg(test)]
1876mod tests {
1877    use super::*;
1878    use std::error::Error;
1879
1880    fn sample_candles(length: usize) -> Candles {
1881        let close = (0..length)
1882            .map(|i| {
1883                let x = i as f64;
1884                100.0 + x * 0.03 + (x * 0.18).sin() * 4.0 + (x * 0.051).cos() * 1.2
1885            })
1886            .collect::<Vec<_>>();
1887        let open = close.iter().map(|v| v - 0.25).collect::<Vec<_>>();
1888        let high = close
1889            .iter()
1890            .enumerate()
1891            .map(|(i, v)| v + 0.9 + (i as f64 * 0.07).cos().abs() * 0.3)
1892            .collect::<Vec<_>>();
1893        let low = close
1894            .iter()
1895            .enumerate()
1896            .map(|(i, v)| v - 0.85 - (i as f64 * 0.05).sin().abs() * 0.25)
1897            .collect::<Vec<_>>();
1898        let volume = vec![1_000.0; length];
1899        Candles::new((0..length as i64).collect(), open, high, low, close, volume)
1900    }
1901
1902    fn assert_series_eq(left: &[f64], right: &[f64], tol: f64) {
1903        assert_eq!(left.len(), right.len());
1904        for (&lhs, &rhs) in left.iter().zip(right.iter()) {
1905            assert!(
1906                (lhs.is_nan() && rhs.is_nan()) || (lhs - rhs).abs() <= tol,
1907                "series mismatch: left={lhs:?}, right={rhs:?}"
1908            );
1909        }
1910    }
1911
1912    #[test]
1913    fn fibonacci_trailing_stop_output_contract() {
1914        let candles = sample_candles(320);
1915        let out =
1916            fibonacci_trailing_stop(&FibonacciTrailingStopInput::with_default_candles(&candles))
1917                .unwrap();
1918        assert_eq!(out.trailing_stop.len(), candles.close.len());
1919        assert_eq!(out.long_stop.len(), candles.close.len());
1920        assert_eq!(out.short_stop.len(), candles.close.len());
1921        assert_eq!(out.direction.len(), candles.close.len());
1922        assert!(out.trailing_stop[0].is_finite());
1923        assert!(out.direction.iter().filter(|v| v.is_finite()).all(|v| {
1924            (*v + 1.0).abs() <= FLOAT_TOL || v.abs() <= FLOAT_TOL || (*v - 1.0).abs() <= FLOAT_TOL
1925        }));
1926    }
1927
1928    #[test]
1929    fn fibonacci_trailing_stop_rejects_invalid_params() {
1930        let candles = sample_candles(16);
1931        let err = fibonacci_trailing_stop(&FibonacciTrailingStopInput::from_candles(
1932            &candles,
1933            FibonacciTrailingStopParams {
1934                left_bars: Some(0),
1935                right_bars: Some(1),
1936                level: Some(DEFAULT_LEVEL),
1937                trigger: Some(DEFAULT_TRIGGER.to_string()),
1938            },
1939        ))
1940        .unwrap_err();
1941        assert!(matches!(
1942            err,
1943            FibonacciTrailingStopError::InvalidLeftBars { .. }
1944        ));
1945
1946        let err = fibonacci_trailing_stop(&FibonacciTrailingStopInput::from_candles(
1947            &candles,
1948            FibonacciTrailingStopParams {
1949                left_bars: Some(4),
1950                right_bars: Some(1),
1951                level: Some(f64::NAN),
1952                trigger: Some(DEFAULT_TRIGGER.to_string()),
1953            },
1954        ))
1955        .unwrap_err();
1956        assert!(matches!(
1957            err,
1958            FibonacciTrailingStopError::InvalidLevel { .. }
1959        ));
1960    }
1961
1962    #[test]
1963    fn fibonacci_trailing_stop_builder_matches_direct() -> Result<(), Box<dyn Error>> {
1964        let candles = sample_candles(320);
1965        let direct = fibonacci_trailing_stop(&FibonacciTrailingStopInput::from_candles(
1966            &candles,
1967            FibonacciTrailingStopParams {
1968                left_bars: Some(12),
1969                right_bars: Some(2),
1970                level: Some(-0.236),
1971                trigger: Some("wick".to_string()),
1972            },
1973        ))?;
1974        let built = FibonacciTrailingStopBuilder::new()
1975            .left_bars(12)
1976            .right_bars(2)
1977            .level(-0.236)
1978            .trigger("wick")?
1979            .apply(&candles)?;
1980
1981        assert_series_eq(&built.trailing_stop, &direct.trailing_stop, 1e-12);
1982        assert_series_eq(&built.long_stop, &direct.long_stop, 1e-12);
1983        assert_series_eq(&built.short_stop, &direct.short_stop, 1e-12);
1984        assert_series_eq(&built.direction, &direct.direction, 1e-12);
1985        Ok(())
1986    }
1987
1988    #[test]
1989    fn fibonacci_trailing_stop_stream_matches_batch_with_reset() -> Result<(), Box<dyn Error>> {
1990        let candles = sample_candles(240);
1991        let mut high = candles.high.clone();
1992        let mut low = candles.low.clone();
1993        let mut close = candles.close.clone();
1994        high[120] = f64::NAN;
1995        low[120] = f64::NAN;
1996        close[120] = f64::NAN;
1997
1998        let batch = fibonacci_trailing_stop(&FibonacciTrailingStopInput::from_slices(
1999            &high,
2000            &low,
2001            &close,
2002            FibonacciTrailingStopParams {
2003                left_bars: Some(10),
2004                right_bars: Some(2),
2005                level: Some(-0.382),
2006                trigger: Some("close".to_string()),
2007            },
2008        ))?;
2009
2010        let mut stream = FibonacciTrailingStopBuilder::new()
2011            .left_bars(10)
2012            .right_bars(2)
2013            .level(-0.382)
2014            .into_stream()?;
2015
2016        let mut trailing_stop = Vec::with_capacity(close.len());
2017        let mut long_stop = Vec::with_capacity(close.len());
2018        let mut short_stop = Vec::with_capacity(close.len());
2019        let mut direction = Vec::with_capacity(close.len());
2020
2021        for i in 0..close.len() {
2022            match stream.update(high[i], low[i], close[i]) {
2023                Some(point) => {
2024                    trailing_stop.push(point.trailing_stop);
2025                    long_stop.push(point.long_stop);
2026                    short_stop.push(point.short_stop);
2027                    direction.push(point.direction);
2028                }
2029                None => {
2030                    trailing_stop.push(f64::NAN);
2031                    long_stop.push(f64::NAN);
2032                    short_stop.push(f64::NAN);
2033                    direction.push(f64::NAN);
2034                }
2035            }
2036        }
2037
2038        assert_series_eq(&trailing_stop, &batch.trailing_stop, 1e-12);
2039        assert_series_eq(&long_stop, &batch.long_stop, 1e-12);
2040        assert_series_eq(&short_stop, &batch.short_stop, 1e-12);
2041        assert_series_eq(&direction, &batch.direction, 1e-12);
2042        Ok(())
2043    }
2044
2045    #[test]
2046    fn fibonacci_trailing_stop_into_matches_main_api() -> Result<(), Box<dyn Error>> {
2047        let candles = sample_candles(192);
2048        let input = FibonacciTrailingStopInput::from_candles(
2049            &candles,
2050            FibonacciTrailingStopParams {
2051                left_bars: Some(14),
2052                right_bars: Some(1),
2053                level: Some(-0.382),
2054                trigger: Some("close".to_string()),
2055            },
2056        );
2057        let direct = fibonacci_trailing_stop(&input)?;
2058        let mut trailing_stop = vec![f64::NAN; candles.close.len()];
2059        let mut long_stop = vec![f64::NAN; candles.close.len()];
2060        let mut short_stop = vec![f64::NAN; candles.close.len()];
2061        let mut direction = vec![f64::NAN; candles.close.len()];
2062
2063        fibonacci_trailing_stop_into_slices(
2064            &mut trailing_stop,
2065            &mut long_stop,
2066            &mut short_stop,
2067            &mut direction,
2068            &input,
2069            Kernel::Auto,
2070        )?;
2071
2072        assert_series_eq(&trailing_stop, &direct.trailing_stop, 1e-12);
2073        assert_series_eq(&long_stop, &direct.long_stop, 1e-12);
2074        assert_series_eq(&short_stop, &direct.short_stop, 1e-12);
2075        assert_series_eq(&direction, &direct.direction, 1e-12);
2076        Ok(())
2077    }
2078
2079    #[test]
2080    fn fibonacci_trailing_stop_batch_single_param_matches_single() -> Result<(), Box<dyn Error>> {
2081        let candles = sample_candles(200);
2082        let batch = FibonacciTrailingStopBatchBuilder::new()
2083            .left_bars_range(12, 12, 0)
2084            .right_bars_range(2, 2, 0)
2085            .level_range(-0.236, -0.236, 0.0)
2086            .trigger("wick")
2087            .apply_candles(&candles)?;
2088        let single = fibonacci_trailing_stop(&FibonacciTrailingStopInput::from_candles(
2089            &candles,
2090            FibonacciTrailingStopParams {
2091                left_bars: Some(12),
2092                right_bars: Some(2),
2093                level: Some(-0.236),
2094                trigger: Some("wick".to_string()),
2095            },
2096        ))?;
2097
2098        assert_eq!(batch.rows, 1);
2099        assert_eq!(batch.cols, candles.close.len());
2100        assert_eq!(batch.combos.len(), 1);
2101        assert_series_eq(
2102            &batch.trailing_stop[..batch.cols],
2103            &single.trailing_stop,
2104            1e-12,
2105        );
2106        assert_series_eq(&batch.long_stop[..batch.cols], &single.long_stop, 1e-12);
2107        assert_series_eq(&batch.short_stop[..batch.cols], &single.short_stop, 1e-12);
2108        assert_series_eq(&batch.direction[..batch.cols], &single.direction, 1e-12);
2109        Ok(())
2110    }
2111
2112    #[test]
2113    fn fibonacci_trailing_stop_batch_metadata() -> Result<(), Box<dyn Error>> {
2114        let candles = sample_candles(180);
2115        let out = FibonacciTrailingStopBatchBuilder::new()
2116            .left_bars_range(10, 12, 2)
2117            .right_bars_range(1, 2, 1)
2118            .level_range(-0.382, -0.236, 0.146)
2119            .apply_candles(&candles)?;
2120
2121        assert_eq!(out.rows, 8);
2122        assert_eq!(out.cols, candles.close.len());
2123        assert_eq!(out.trailing_stop.len(), out.rows * out.cols);
2124        assert_eq!(out.long_stop.len(), out.rows * out.cols);
2125        assert_eq!(out.short_stop.len(), out.rows * out.cols);
2126        assert_eq!(out.direction.len(), out.rows * out.cols);
2127        assert_eq!(out.combos.len(), out.rows);
2128        Ok(())
2129    }
2130}