Skip to main content

vector_ta/indicators/
fvg_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
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::{source_type, Candles};
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
19    make_uninit_matrix,
20};
21#[cfg(feature = "python")]
22use crate::utilities::kernel_validation::validate_kernel;
23
24use std::convert::AsRef;
25use std::error::Error;
26use thiserror::Error;
27
28#[derive(Debug, Clone)]
29pub struct FvgTrailingStopOutput {
30    pub upper: Vec<f64>,
31    pub lower: Vec<f64>,
32    pub upper_ts: Vec<f64>,
33    pub lower_ts: Vec<f64>,
34}
35
36#[derive(Debug, Clone)]
37#[cfg_attr(
38    all(target_arch = "wasm32", feature = "wasm"),
39    derive(Serialize, Deserialize)
40)]
41pub struct FvgTrailingStopParams {
42    pub unmitigated_fvg_lookback: Option<usize>,
43    pub smoothing_length: Option<usize>,
44    pub reset_on_cross: Option<bool>,
45}
46
47impl Default for FvgTrailingStopParams {
48    fn default() -> Self {
49        Self {
50            unmitigated_fvg_lookback: Some(5),
51            smoothing_length: Some(9),
52            reset_on_cross: Some(false),
53        }
54    }
55}
56
57#[derive(Debug, Clone)]
58pub enum FvgTrailingStopData<'a> {
59    Candles(&'a Candles),
60    Slices {
61        high: &'a [f64],
62        low: &'a [f64],
63        close: &'a [f64],
64    },
65}
66
67#[inline]
68fn first_valid_ohlc(high: &[f64], low: &[f64], close: &[f64]) -> usize {
69    for i in 0..high.len() {
70        if !high[i].is_nan() && !low[i].is_nan() && !close[i].is_nan() {
71            return i;
72        }
73    }
74    usize::MAX
75}
76
77#[derive(Debug, Clone)]
78pub struct FvgTrailingStopInput<'a> {
79    pub data: FvgTrailingStopData<'a>,
80    pub params: FvgTrailingStopParams,
81}
82
83impl<'a> FvgTrailingStopInput<'a> {
84    pub fn from_candles(candles: &'a Candles, params: FvgTrailingStopParams) -> Self {
85        Self {
86            data: FvgTrailingStopData::Candles(candles),
87            params,
88        }
89    }
90
91    pub fn from_slices(
92        high: &'a [f64],
93        low: &'a [f64],
94        close: &'a [f64],
95        params: FvgTrailingStopParams,
96    ) -> Self {
97        Self {
98            data: FvgTrailingStopData::Slices { high, low, close },
99            params,
100        }
101    }
102
103    pub fn with_default_candles(candles: &'a Candles) -> Self {
104        Self::from_candles(candles, FvgTrailingStopParams::default())
105    }
106
107    pub fn get_lookback(&self) -> usize {
108        self.params.unmitigated_fvg_lookback.unwrap_or(5)
109    }
110
111    pub fn get_smoothing(&self) -> usize {
112        self.params.smoothing_length.unwrap_or(9)
113    }
114
115    pub fn get_reset_on_cross(&self) -> bool {
116        self.params.reset_on_cross.unwrap_or(false)
117    }
118
119    pub fn as_slices(&self) -> (&'a [f64], &'a [f64], &'a [f64]) {
120        match &self.data {
121            FvgTrailingStopData::Candles(c) => (&c.high, &c.low, &c.close),
122            FvgTrailingStopData::Slices { high, low, close } => (high, low, close),
123        }
124    }
125}
126
127#[derive(Debug, Error)]
128pub enum FvgTrailingStopError {
129    #[error("fvg_trailing_stop: Input data slice is empty.")]
130    EmptyInputData,
131
132    #[error("fvg_trailing_stop: All values are NaN.")]
133    AllValuesNaN,
134
135    #[error("fvg_trailing_stop: Invalid period: period = {period}, data length = {data_len}")]
136    InvalidPeriod { period: usize, data_len: usize },
137
138    #[error("fvg_trailing_stop: Not enough valid data: needed = {needed}, valid = {valid}")]
139    NotEnoughValidData { needed: usize, valid: usize },
140
141    #[error("fvg_trailing_stop: Invalid smoothing_length: {smoothing}")]
142    InvalidSmoothingLength { smoothing: usize },
143
144    #[error("fvg_trailing_stop: Invalid unmitigated_fvg_lookback: {lookback}")]
145    InvalidLookback { lookback: usize },
146
147    #[error("fvg_trailing_stop: Output length mismatch: expected {expected}, got {got}")]
148    OutputLengthMismatch { expected: usize, got: usize },
149
150    #[error("fvg_trailing_stop: Invalid range: start={start}, end={end}, step={step}")]
151    InvalidRange {
152        start: usize,
153        end: usize,
154        step: usize,
155    },
156
157    #[error("fvg_trailing_stop: Invalid kernel for batch path: {0:?}")]
158    InvalidKernelForBatch(Kernel),
159}
160
161#[inline]
162fn fvg_ts_scalar(
163    high: &[f64],
164    low: &[f64],
165    close: &[f64],
166    lookback: usize,
167    smoothing_len: usize,
168    reset_on_cross: bool,
169    upper: &mut [f64],
170    lower: &mut [f64],
171    upper_ts: &mut [f64],
172    lower_ts: &mut [f64],
173) {
174    let len = high.len();
175    debug_assert_eq!(len, low.len());
176    debug_assert_eq!(len, close.len());
177    debug_assert_eq!(len, upper.len());
178    debug_assert_eq!(len, lower.len());
179    debug_assert_eq!(len, upper_ts.len());
180    debug_assert_eq!(len, lower_ts.len());
181
182    let mut bull_buf = vec![0.0f64; lookback];
183    let mut bear_buf = vec![0.0f64; lookback];
184    let mut bull_len: usize = 0;
185    let mut bear_len: usize = 0;
186
187    let mut last_bull_non_na: Option<usize> = None;
188    let mut last_bear_non_na: Option<usize> = None;
189
190    let w = smoothing_len;
191    let mut bull_ring_vals = vec![0.0f64; w];
192    let mut bull_ring_nan = vec![false; w];
193    let mut bear_ring_vals = vec![0.0f64; w];
194    let mut bear_ring_nan = vec![false; w];
195
196    let mut bull_sum = 0.0f64;
197    let mut bear_sum = 0.0f64;
198    let mut bull_nan_cnt = 0usize;
199    let mut bear_nan_cnt = 0usize;
200    let mut bull_ring_count = 0usize;
201    let mut bear_ring_count = 0usize;
202    let mut bull_ring_idx = 0usize;
203    let mut bear_ring_idx = 0usize;
204
205    let mut os: Option<i8> = None;
206    let mut ts: Option<f64> = None;
207    let mut ts_prev: Option<f64> = None;
208
209    for i in 0..len {
210        if i >= 2 && !high[i - 2].is_nan() && !low[i - 2].is_nan() && !close[i - 1].is_nan() {
211            if low[i] > high[i - 2] && close[i - 1] > high[i - 2] {
212                if bull_len < lookback {
213                    bull_buf[bull_len] = high[i - 2];
214                    bull_len += 1;
215                } else {
216                    for k in 1..lookback {
217                        bull_buf[k - 1] = bull_buf[k];
218                    }
219                    bull_buf[lookback - 1] = high[i - 2];
220                }
221            }
222            if high[i] < low[i - 2] && close[i - 1] < low[i - 2] {
223                if bear_len < lookback {
224                    bear_buf[bear_len] = low[i - 2];
225                    bear_len += 1;
226                } else {
227                    for k in 1..lookback {
228                        bear_buf[k - 1] = bear_buf[k];
229                    }
230                    bear_buf[lookback - 1] = low[i - 2];
231                }
232            }
233        }
234
235        let c = close[i];
236
237        let mut new_bull_len = 0usize;
238        let mut bull_acc = 0.0f64;
239        for k in 0..bull_len {
240            let v = bull_buf[k];
241            if c >= v {
242                bull_buf[new_bull_len] = v;
243                new_bull_len += 1;
244                bull_acc += v;
245            }
246        }
247        bull_len = new_bull_len;
248
249        let mut new_bear_len = 0usize;
250        let mut bear_acc = 0.0f64;
251        for k in 0..bear_len {
252            let v = bear_buf[k];
253            if c <= v {
254                bear_buf[new_bear_len] = v;
255                new_bear_len += 1;
256                bear_acc += v;
257            }
258        }
259        bear_len = new_bear_len;
260
261        let bull_avg = if bull_len > 0 {
262            bull_acc / (bull_len as f64)
263        } else {
264            f64::NAN
265        };
266        let bear_avg = if bear_len > 0 {
267            bear_acc / (bear_len as f64)
268        } else {
269            f64::NAN
270        };
271
272        if !bull_avg.is_nan() {
273            last_bull_non_na = Some(i);
274        }
275        if !bear_avg.is_nan() {
276            last_bear_non_na = Some(i);
277        }
278
279        let bull_bs = if bull_avg.is_nan() {
280            match last_bull_non_na {
281                Some(last) => ((i - last).max(1)).min(w),
282                None => 1,
283            }
284        } else {
285            1
286        };
287        let bear_bs = if bear_avg.is_nan() {
288            match last_bear_non_na {
289                Some(last) => ((i - last).max(1)).min(w),
290                None => 1,
291            }
292        } else {
293            1
294        };
295
296        let bull_sma = if bull_avg.is_nan() && (i + 1) >= bull_bs {
297            let mut s = 0.0f64;
298            let start = i + 1 - bull_bs;
299            for j in start..=i {
300                s += close[j];
301            }
302            s / (bull_bs as f64)
303        } else {
304            f64::NAN
305        };
306        let bear_sma = if bear_avg.is_nan() && (i + 1) >= bear_bs {
307            let mut s = 0.0f64;
308            let start = i + 1 - bear_bs;
309            for j in start..=i {
310                s += close[j];
311            }
312            s / (bear_bs as f64)
313        } else {
314            f64::NAN
315        };
316
317        let x_bull = if !bull_avg.is_nan() {
318            bull_avg
319        } else {
320            bull_sma
321        };
322        let x_bear = if !bear_avg.is_nan() {
323            bear_avg
324        } else {
325            bear_sma
326        };
327
328        if bull_ring_count < w {
329            let is_nan = x_bull.is_nan();
330            bull_ring_nan[bull_ring_count] = is_nan;
331            bull_ring_vals[bull_ring_count] = if is_nan { 0.0 } else { x_bull };
332            if is_nan {
333                bull_nan_cnt += 1;
334            } else {
335                bull_sum += x_bull;
336            }
337            bull_ring_count += 1;
338        } else {
339            let idx = bull_ring_idx;
340            if bull_ring_nan[idx] {
341                bull_nan_cnt -= 1;
342            } else {
343                bull_sum -= bull_ring_vals[idx];
344            }
345            let is_nan = x_bull.is_nan();
346            bull_ring_nan[idx] = is_nan;
347            if is_nan {
348                bull_ring_vals[idx] = 0.0;
349                bull_nan_cnt += 1;
350            } else {
351                bull_ring_vals[idx] = x_bull;
352                bull_sum += x_bull;
353            }
354            bull_ring_idx = if idx + 1 == w { 0 } else { idx + 1 };
355        }
356
357        if bear_ring_count < w {
358            let is_nan = x_bear.is_nan();
359            bear_ring_nan[bear_ring_count] = is_nan;
360            bear_ring_vals[bear_ring_count] = if is_nan { 0.0 } else { x_bear };
361            if is_nan {
362                bear_nan_cnt += 1;
363            } else {
364                bear_sum += x_bear;
365            }
366            bear_ring_count += 1;
367        } else {
368            let idx = bear_ring_idx;
369            if bear_ring_nan[idx] {
370                bear_nan_cnt -= 1;
371            } else {
372                bear_sum -= bear_ring_vals[idx];
373            }
374            let is_nan = x_bear.is_nan();
375            bear_ring_nan[idx] = is_nan;
376            if is_nan {
377                bear_ring_vals[idx] = 0.0;
378                bear_nan_cnt += 1;
379            } else {
380                bear_ring_vals[idx] = x_bear;
381                bear_sum += x_bear;
382            }
383            bear_ring_idx = if idx + 1 == w { 0 } else { idx + 1 };
384        }
385
386        let bull_disp = if bull_ring_count >= w && bull_nan_cnt == 0 {
387            bull_sum / (w as f64)
388        } else {
389            f64::NAN
390        };
391        let bear_disp = if bear_ring_count >= w && bear_nan_cnt == 0 {
392            bear_sum / (w as f64)
393        } else {
394            f64::NAN
395        };
396
397        let prev_os = os;
398        let next_os = if !bear_disp.is_nan() && c > bear_disp {
399            Some(1)
400        } else if !bull_disp.is_nan() && c < bull_disp {
401            Some(-1)
402        } else {
403            os
404        };
405        os = next_os;
406
407        if let (Some(cur), Some(prev)) = (os, prev_os) {
408            if cur == 1 && prev != 1 {
409                ts = Some(bull_disp);
410            } else if cur == -1 && prev != -1 {
411                ts = Some(bear_disp);
412            } else if cur == 1 {
413                if let Some(t) = ts {
414                    ts = Some(bull_disp.max(t));
415                }
416            } else if cur == -1 {
417                if let Some(t) = ts {
418                    ts = Some(bear_disp.min(t));
419                }
420            }
421        } else {
422            if os == Some(1) {
423                if let Some(t) = ts {
424                    ts = Some(bull_disp.max(t));
425                }
426            }
427            if os == Some(-1) {
428                if let Some(t) = ts {
429                    ts = Some(bear_disp.min(t));
430                }
431            }
432        }
433
434        if reset_on_cross {
435            if os == Some(1) {
436                if let Some(t) = ts {
437                    if c < t {
438                        ts = None;
439                    }
440                } else if !bear_disp.is_nan() && c > bear_disp {
441                    ts = Some(bull_disp);
442                }
443            } else if os == Some(-1) {
444                if let Some(t) = ts {
445                    if c > t {
446                        ts = None;
447                    }
448                } else if !bull_disp.is_nan() && c < bull_disp {
449                    ts = Some(bear_disp);
450                }
451            }
452        }
453
454        let show = ts.is_some() || ts_prev.is_some();
455        let ts_nz = if ts.is_some() { ts } else { ts_prev };
456
457        if os == Some(1) && show {
458            upper[i] = f64::NAN;
459            lower[i] = bull_disp;
460            upper_ts[i] = f64::NAN;
461            lower_ts[i] = ts_nz.unwrap_or(f64::NAN);
462        } else if os == Some(-1) && show {
463            upper[i] = bear_disp;
464            lower[i] = f64::NAN;
465            upper_ts[i] = ts_nz.unwrap_or(f64::NAN);
466            lower_ts[i] = f64::NAN;
467        } else {
468            upper[i] = f64::NAN;
469            lower[i] = f64::NAN;
470            upper_ts[i] = f64::NAN;
471            lower_ts[i] = f64::NAN;
472        }
473
474        ts_prev = ts;
475    }
476}
477
478#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
479#[target_feature(enable = "avx2,fma")]
480unsafe fn fvg_ts_avx2(
481    high: &[f64],
482    low: &[f64],
483    close: &[f64],
484    lookback: usize,
485    smoothing_len: usize,
486    reset_on_cross: bool,
487    upper: &mut [f64],
488    lower: &mut [f64],
489    upper_ts: &mut [f64],
490    lower_ts: &mut [f64],
491) {
492    fvg_ts_scalar(
493        high,
494        low,
495        close,
496        lookback,
497        smoothing_len,
498        reset_on_cross,
499        upper,
500        lower,
501        upper_ts,
502        lower_ts,
503    );
504}
505
506#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
507#[target_feature(enable = "avx512f")]
508unsafe fn fvg_ts_avx512(
509    high: &[f64],
510    low: &[f64],
511    close: &[f64],
512    lookback: usize,
513    smoothing_len: usize,
514    reset_on_cross: bool,
515    upper: &mut [f64],
516    lower: &mut [f64],
517    upper_ts: &mut [f64],
518    lower_ts: &mut [f64],
519) {
520    fvg_ts_scalar(
521        high,
522        low,
523        close,
524        lookback,
525        smoothing_len,
526        reset_on_cross,
527        upper,
528        lower,
529        upper_ts,
530        lower_ts,
531    );
532}
533
534#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
535#[inline]
536unsafe fn fvg_ts_simd128(
537    high: &[f64],
538    low: &[f64],
539    close: &[f64],
540    lookback: usize,
541    smoothing_len: usize,
542    reset_on_cross: bool,
543    upper: &mut [f64],
544    lower: &mut [f64],
545    upper_ts: &mut [f64],
546    lower_ts: &mut [f64],
547) {
548    fvg_ts_scalar(
549        high,
550        low,
551        close,
552        lookback,
553        smoothing_len,
554        reset_on_cross,
555        upper,
556        lower,
557        upper_ts,
558        lower_ts,
559    );
560}
561
562#[inline]
563fn fvg_ts_prepare<'a>(
564    input: &'a FvgTrailingStopInput,
565) -> Result<(&'a [f64], &'a [f64], &'a [f64], usize, usize, bool, usize), FvgTrailingStopError> {
566    let (h, l, c) = input.as_slices();
567    if h.is_empty() || l.is_empty() || c.is_empty() {
568        return Err(FvgTrailingStopError::EmptyInputData);
569    }
570    let len = h.len();
571    if len != l.len() || len != c.len() {
572        return Err(FvgTrailingStopError::InvalidPeriod {
573            period: len,
574            data_len: len,
575        });
576    }
577    let first = first_valid_ohlc(h, l, c);
578    if first == usize::MAX {
579        return Err(FvgTrailingStopError::AllValuesNaN);
580    }
581    let lookback = input.get_lookback();
582    let smoothing_len = input.get_smoothing();
583
584    if lookback == 0 {
585        return Err(FvgTrailingStopError::InvalidLookback { lookback });
586    }
587    if smoothing_len == 0 {
588        return Err(FvgTrailingStopError::InvalidSmoothingLength {
589            smoothing: smoothing_len,
590        });
591    }
592
593    let need = 2 + smoothing_len.saturating_sub(1);
594    if len - first < need {
595        return Err(FvgTrailingStopError::NotEnoughValidData {
596            needed: need,
597            valid: len - first,
598        });
599    }
600    let reset_on_cross = input.get_reset_on_cross();
601    Ok((h, l, c, lookback, smoothing_len, reset_on_cross, first))
602}
603
604#[inline]
605fn fvg_ts_compute_into(
606    high: &[f64],
607    low: &[f64],
608    close: &[f64],
609    lookback: usize,
610    smoothing_len: usize,
611    reset_on_cross: bool,
612    upper: &mut [f64],
613    lower: &mut [f64],
614    upper_ts: &mut [f64],
615    lower_ts: &mut [f64],
616    kernel: Kernel,
617) {
618    unsafe {
619        #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
620        {
621            if matches!(kernel, Kernel::Scalar | Kernel::ScalarBatch) {
622                fvg_ts_simd128(
623                    high,
624                    low,
625                    close,
626                    lookback,
627                    smoothing_len,
628                    reset_on_cross,
629                    upper,
630                    lower,
631                    upper_ts,
632                    lower_ts,
633                );
634                return;
635            }
636        }
637
638        match kernel {
639            Kernel::Scalar | Kernel::ScalarBatch => fvg_ts_scalar(
640                high,
641                low,
642                close,
643                lookback,
644                smoothing_len,
645                reset_on_cross,
646                upper,
647                lower,
648                upper_ts,
649                lower_ts,
650            ),
651            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
652            Kernel::Avx2 | Kernel::Avx2Batch => fvg_ts_avx2(
653                high,
654                low,
655                close,
656                lookback,
657                smoothing_len,
658                reset_on_cross,
659                upper,
660                lower,
661                upper_ts,
662                lower_ts,
663            ),
664            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
665            Kernel::Avx512 | Kernel::Avx512Batch => fvg_ts_avx512(
666                high,
667                low,
668                close,
669                lookback,
670                smoothing_len,
671                reset_on_cross,
672                upper,
673                lower,
674                upper_ts,
675                lower_ts,
676            ),
677            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
678            Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
679                fvg_ts_scalar(
680                    high,
681                    low,
682                    close,
683                    lookback,
684                    smoothing_len,
685                    reset_on_cross,
686                    upper,
687                    lower,
688                    upper_ts,
689                    lower_ts,
690                )
691            }
692            _ => unreachable!(),
693        }
694    }
695}
696
697#[inline]
698pub fn fvg_trailing_stop(
699    input: &FvgTrailingStopInput,
700) -> Result<FvgTrailingStopOutput, FvgTrailingStopError> {
701    fvg_trailing_stop_with_kernel(input, Kernel::Auto)
702}
703
704pub fn fvg_trailing_stop_with_kernel(
705    input: &FvgTrailingStopInput,
706    kernel: Kernel,
707) -> Result<FvgTrailingStopOutput, FvgTrailingStopError> {
708    let (h, l, c, lookback, smoothing_len, reset_on_cross, first) = fvg_ts_prepare(input)?;
709    let len = h.len();
710    let warm = (first + 2 + smoothing_len.saturating_sub(1)).min(len);
711
712    let mut upper = alloc_with_nan_prefix(len, warm);
713    let mut lower = alloc_with_nan_prefix(len, warm);
714    let mut upper_ts = alloc_with_nan_prefix(len, warm);
715    let mut lower_ts = alloc_with_nan_prefix(len, warm);
716
717    let chosen = match kernel {
718        Kernel::Auto => Kernel::Scalar,
719        k => k,
720    };
721
722    fvg_ts_compute_into(
723        h,
724        l,
725        c,
726        lookback,
727        smoothing_len,
728        reset_on_cross,
729        &mut upper,
730        &mut lower,
731        &mut upper_ts,
732        &mut lower_ts,
733        chosen,
734    );
735
736    Ok(FvgTrailingStopOutput {
737        upper,
738        lower,
739        upper_ts,
740        lower_ts,
741    })
742}
743
744#[inline]
745pub fn fvg_trailing_stop_into(
746    input: &FvgTrailingStopInput,
747    upper: &mut [f64],
748    lower: &mut [f64],
749    upper_ts: &mut [f64],
750    lower_ts: &mut [f64],
751) -> Result<(), FvgTrailingStopError> {
752    fvg_trailing_stop_into_slices(upper, lower, upper_ts, lower_ts, input, Kernel::Auto)
753}
754
755#[inline]
756pub fn fvg_trailing_stop_into_slices(
757    upper: &mut [f64],
758    lower: &mut [f64],
759    upper_ts: &mut [f64],
760    lower_ts: &mut [f64],
761    input: &FvgTrailingStopInput,
762    kernel: Kernel,
763) -> Result<(), FvgTrailingStopError> {
764    let (h, l, c, lookback, smoothing_len, reset_on_cross, first) = fvg_ts_prepare(input)?;
765    let len = h.len();
766    if [upper.len(), lower.len(), upper_ts.len(), lower_ts.len()]
767        .iter()
768        .any(|&n| n != len)
769    {
770        return Err(FvgTrailingStopError::OutputLengthMismatch {
771            expected: len,
772            got: upper
773                .len()
774                .min(lower.len())
775                .min(upper_ts.len())
776                .min(lower_ts.len()),
777        });
778    }
779    let chosen = match kernel {
780        Kernel::Auto => Kernel::Scalar,
781        k => k,
782    };
783
784    fvg_ts_compute_into(
785        h,
786        l,
787        c,
788        lookback,
789        smoothing_len,
790        reset_on_cross,
791        upper,
792        lower,
793        upper_ts,
794        lower_ts,
795        chosen,
796    );
797
798    let warm = (first + 2 + smoothing_len.saturating_sub(1)).min(len);
799    for dst in [upper, lower, upper_ts, lower_ts] {
800        for v in &mut dst[..warm] {
801            *v = f64::NAN;
802        }
803    }
804    Ok(())
805}
806
807#[derive(Clone, Debug)]
808pub struct FvgTsBatchRange {
809    pub lookback: (usize, usize, usize),
810    pub smoothing: (usize, usize, usize),
811    pub reset_on_cross: (bool, bool),
812}
813
814impl Default for FvgTsBatchRange {
815    fn default() -> Self {
816        Self {
817            lookback: (5, 254, 1),
818            smoothing: (9, 9, 0),
819            reset_on_cross: (false, false),
820        }
821    }
822}
823
824#[derive(Clone, Debug)]
825pub struct FvgTsBatchOutput {
826    pub values: Vec<f64>,
827    pub combos: Vec<FvgTrailingStopParams>,
828    pub rows: usize,
829    pub cols: usize,
830}
831
832impl FvgTsBatchOutput {
833    pub fn row_for_params(&self, p: &FvgTrailingStopParams) -> Option<usize> {
834        self.combos.iter().position(|c| {
835            c.unmitigated_fvg_lookback.unwrap_or(5) == p.unmitigated_fvg_lookback.unwrap_or(5)
836                && c.smoothing_length.unwrap_or(9) == p.smoothing_length.unwrap_or(9)
837                && c.reset_on_cross.unwrap_or(false) == p.reset_on_cross.unwrap_or(false)
838        })
839    }
840
841    pub fn values_for(
842        &self,
843        p: &FvgTrailingStopParams,
844    ) -> Option<(&[f64], &[f64], &[f64], &[f64])> {
845        let r = self.row_for_params(p)?;
846        let cols = self.cols;
847        let base = r * 4 * cols;
848        Some((
849            &self.values[base..base + cols],
850            &self.values[base + cols..base + 2 * cols],
851            &self.values[base + 2 * cols..base + 3 * cols],
852            &self.values[base + 3 * cols..base + 4 * cols],
853        ))
854    }
855}
856
857#[inline]
858fn expand_axis_usize(
859    (start, end, step): (usize, usize, usize),
860) -> Result<Vec<usize>, FvgTrailingStopError> {
861    if step == 0 {
862        return Ok(vec![start]);
863    }
864    let mut out = Vec::new();
865    if start <= end {
866        let mut v = start;
867        while v <= end {
868            out.push(v);
869            match v.checked_add(step) {
870                Some(nv) => v = nv,
871                None => break,
872            }
873        }
874    } else {
875        let mut v = start;
876        loop {
877            if v < end {
878                break;
879            }
880            out.push(v);
881            match v.checked_sub(step) {
882                Some(next) => v = next,
883                None => break,
884            }
885        }
886    }
887    if out.is_empty() {
888        return Err(FvgTrailingStopError::InvalidRange { start, end, step });
889    }
890    Ok(out)
891}
892
893#[inline]
894fn expand_grid_ts(r: &FvgTsBatchRange) -> Result<Vec<FvgTrailingStopParams>, FvgTrailingStopError> {
895    let looks = expand_axis_usize(r.lookback)?;
896    let smooths = expand_axis_usize(r.smoothing)?;
897    let mut resets = Vec::new();
898    if r.reset_on_cross.0 {
899        resets.push(false);
900    }
901    if r.reset_on_cross.1 {
902        resets.push(true);
903    }
904    if resets.is_empty() {
905        resets.push(false);
906    }
907
908    let mut v = Vec::with_capacity(
909        looks
910            .len()
911            .saturating_mul(smooths.len())
912            .saturating_mul(resets.len()),
913    );
914    for &lb in &looks {
915        for &sm in &smooths {
916            for &rs in &resets {
917                v.push(FvgTrailingStopParams {
918                    unmitigated_fvg_lookback: Some(lb),
919                    smoothing_length: Some(sm),
920                    reset_on_cross: Some(rs),
921                });
922            }
923        }
924    }
925    if v.is_empty() {
926        return Err(FvgTrailingStopError::InvalidRange {
927            start: r.lookback.0,
928            end: r.lookback.1,
929            step: r.lookback.2,
930        });
931    }
932    Ok(v)
933}
934
935#[inline(always)]
936pub fn fvg_ts_batch_inner_into(
937    h: &[f64],
938    l: &[f64],
939    c: &[f64],
940    sweep: &FvgTsBatchRange,
941    kern: Kernel,
942    parallel: bool,
943    out: &mut [f64],
944) -> Result<Vec<FvgTrailingStopParams>, FvgTrailingStopError> {
945    if !matches!(
946        kern,
947        Kernel::Auto | Kernel::ScalarBatch | Kernel::Avx2Batch | Kernel::Avx512Batch
948    ) {
949        return Err(FvgTrailingStopError::InvalidKernelForBatch(kern));
950    }
951
952    if h.is_empty() || l.is_empty() || c.is_empty() {
953        return Err(FvgTrailingStopError::EmptyInputData);
954    }
955    let len = h.len();
956    if len != l.len() || len != c.len() {
957        return Err(FvgTrailingStopError::InvalidPeriod {
958            period: len,
959            data_len: len,
960        });
961    }
962
963    let combos = expand_grid_ts(sweep)?;
964    let rows = combos.len();
965    let cols = len;
966    let expected = rows
967        .checked_mul(4)
968        .and_then(|x| x.checked_mul(cols))
969        .ok_or_else(|| FvgTrailingStopError::InvalidRange {
970            start: rows,
971            end: cols,
972            step: 4,
973        })?;
974    if out.len() != expected {
975        return Err(FvgTrailingStopError::OutputLengthMismatch {
976            expected,
977            got: out.len(),
978        });
979    }
980
981    let first = first_valid_ohlc(h, l, c);
982    if first == usize::MAX {
983        return Err(FvgTrailingStopError::AllValuesNaN);
984    }
985
986    let mut max_sm = 0usize;
987    for prm in &combos {
988        let look = prm.unmitigated_fvg_lookback.unwrap_or(5);
989        if look == 0 {
990            return Err(FvgTrailingStopError::InvalidLookback { lookback: look });
991        }
992        let sm = prm.smoothing_length.unwrap_or(9);
993        if sm == 0 {
994            return Err(FvgTrailingStopError::InvalidSmoothingLength { smoothing: sm });
995        }
996        if sm > max_sm {
997            max_sm = sm;
998        }
999    }
1000    let need = 2 + max_sm.saturating_sub(1);
1001    if len - first < need {
1002        return Err(FvgTrailingStopError::NotEnoughValidData {
1003            needed: need,
1004            valid: len - first,
1005        });
1006    }
1007
1008    let _chosen = match kern {
1009        Kernel::Auto => detect_best_batch_kernel(),
1010        k => k,
1011    };
1012
1013    let mut bull_cand = vec![f64::NAN; len];
1014    let mut bear_cand = vec![f64::NAN; len];
1015    if len >= 3 {
1016        for i in 2..len {
1017            let hi2 = h[i - 2];
1018            let lo2 = l[i - 2];
1019            let cm1 = c[i - 1];
1020            let hi = h[i];
1021            let lo = l[i];
1022            if !(hi2.is_nan() || lo2.is_nan() || cm1.is_nan()) {
1023                if lo > hi2 && cm1 > hi2 {
1024                    bull_cand[i] = hi2;
1025                }
1026                if hi < lo2 && cm1 < lo2 {
1027                    bear_cand[i] = lo2;
1028                }
1029            }
1030        }
1031    }
1032
1033    let mut pref_sum_close = vec![0.0f64; len + 1];
1034    let mut pref_nan_count = vec![0usize; len + 1];
1035    for i in 0..len {
1036        let is_nan = c[i].is_nan();
1037        pref_sum_close[i + 1] = pref_sum_close[i] + if is_nan { 0.0 } else { c[i] };
1038        pref_nan_count[i + 1] = pref_nan_count[i] + if is_nan { 1 } else { 0 };
1039    }
1040
1041    let do_one = |row: usize, dst: &mut [f64]| {
1042        let look = combos[row].unmitigated_fvg_lookback.unwrap();
1043        let sm = combos[row].smoothing_length.unwrap_or(9);
1044        let rst = combos[row].reset_on_cross.unwrap_or(false);
1045        let warm = (first + 2 + sm.saturating_sub(1)).min(cols);
1046        let (u_block, rest) = dst.split_at_mut(cols);
1047        let (l_block, rest) = rest.split_at_mut(cols);
1048        let (uts_block, lts_block) = rest.split_at_mut(cols);
1049
1050        let mut bull_buf = vec![0.0f64; look];
1051        let mut bear_buf = vec![0.0f64; look];
1052        let mut bull_len = 0usize;
1053        let mut bear_len = 0usize;
1054        let mut last_bull_non_na: Option<usize> = None;
1055        let mut last_bear_non_na: Option<usize> = None;
1056
1057        let mut bull_ring_vals = vec![0.0f64; sm];
1058        let mut bull_ring_nan = vec![false; sm];
1059        let mut bear_ring_vals = vec![0.0f64; sm];
1060        let mut bear_ring_nan = vec![false; sm];
1061        let mut bull_sum = 0.0f64;
1062        let mut bear_sum = 0.0f64;
1063        let mut bull_nan_cnt = 0usize;
1064        let mut bear_nan_cnt = 0usize;
1065        let mut bull_ring_count = 0usize;
1066        let mut bear_ring_count = 0usize;
1067        let mut bull_ring_idx = 0usize;
1068        let mut bear_ring_idx = 0usize;
1069
1070        let mut os: Option<i8> = None;
1071        let mut ts: Option<f64> = None;
1072        let mut ts_prev: Option<f64> = None;
1073
1074        for i in 0..cols {
1075            let bc = bull_cand[i];
1076            if !bc.is_nan() {
1077                if bull_len < look {
1078                    bull_buf[bull_len] = bc;
1079                    bull_len += 1;
1080                } else {
1081                    for k in 1..look {
1082                        bull_buf[k - 1] = bull_buf[k];
1083                    }
1084                    bull_buf[look - 1] = bc;
1085                }
1086            }
1087            let ec = bear_cand[i];
1088            if !ec.is_nan() {
1089                if bear_len < look {
1090                    bear_buf[bear_len] = ec;
1091                    bear_len += 1;
1092                } else {
1093                    for k in 1..look {
1094                        bear_buf[k - 1] = bear_buf[k];
1095                    }
1096                    bear_buf[look - 1] = ec;
1097                }
1098            }
1099
1100            let price = c[i];
1101            let mut new_bull_len = 0usize;
1102            let mut bull_acc = 0.0f64;
1103            for k in 0..bull_len {
1104                let v = bull_buf[k];
1105                if price >= v {
1106                    bull_buf[new_bull_len] = v;
1107                    new_bull_len += 1;
1108                    bull_acc += v;
1109                }
1110            }
1111            bull_len = new_bull_len;
1112
1113            let mut new_bear_len = 0usize;
1114            let mut bear_acc = 0.0f64;
1115            for k in 0..bear_len {
1116                let v = bear_buf[k];
1117                if price <= v {
1118                    bear_buf[new_bear_len] = v;
1119                    new_bear_len += 1;
1120                    bear_acc += v;
1121                }
1122            }
1123            bear_len = new_bear_len;
1124
1125            let bull_avg = if bull_len > 0 {
1126                bull_acc / (bull_len as f64)
1127            } else {
1128                f64::NAN
1129            };
1130            let bear_avg = if bear_len > 0 {
1131                bear_acc / (bear_len as f64)
1132            } else {
1133                f64::NAN
1134            };
1135            if !bull_avg.is_nan() {
1136                last_bull_non_na = Some(i);
1137            }
1138            if !bear_avg.is_nan() {
1139                last_bear_non_na = Some(i);
1140            }
1141
1142            let bull_bs = if bull_avg.is_nan() {
1143                match last_bull_non_na {
1144                    Some(last) => ((i - last).max(1)).min(sm),
1145                    None => 1,
1146                }
1147            } else {
1148                1
1149            };
1150            let bear_bs = if bear_avg.is_nan() {
1151                match last_bear_non_na {
1152                    Some(last) => ((i - last).max(1)).min(sm),
1153                    None => 1,
1154                }
1155            } else {
1156                1
1157            };
1158
1159            let bull_sma = if bull_avg.is_nan() && (i + 1) >= bull_bs {
1160                let s = pref_sum_close[i + 1] - pref_sum_close[i + 1 - bull_bs];
1161                let nans = pref_nan_count[i + 1] - pref_nan_count[i + 1 - bull_bs];
1162                if nans == 0 {
1163                    s / (bull_bs as f64)
1164                } else {
1165                    f64::NAN
1166                }
1167            } else {
1168                f64::NAN
1169            };
1170            let bear_sma = if bear_avg.is_nan() && (i + 1) >= bear_bs {
1171                let s = pref_sum_close[i + 1] - pref_sum_close[i + 1 - bear_bs];
1172                let nans = pref_nan_count[i + 1] - pref_nan_count[i + 1 - bear_bs];
1173                if nans == 0 {
1174                    s / (bear_bs as f64)
1175                } else {
1176                    f64::NAN
1177                }
1178            } else {
1179                f64::NAN
1180            };
1181
1182            let x_bull = if !bull_avg.is_nan() {
1183                bull_avg
1184            } else {
1185                bull_sma
1186            };
1187            let x_bear = if !bear_avg.is_nan() {
1188                bear_avg
1189            } else {
1190                bear_sma
1191            };
1192
1193            if bull_ring_count < sm {
1194                let is_nan = x_bull.is_nan();
1195                bull_ring_nan[bull_ring_count] = is_nan;
1196                bull_ring_vals[bull_ring_count] = if is_nan { 0.0 } else { x_bull };
1197                if is_nan {
1198                    bull_nan_cnt += 1
1199                } else {
1200                    bull_sum += x_bull
1201                }
1202                bull_ring_count += 1;
1203            } else {
1204                let idx = bull_ring_idx;
1205                if bull_ring_nan[idx] {
1206                    bull_nan_cnt -= 1
1207                } else {
1208                    bull_sum -= bull_ring_vals[idx]
1209                }
1210                let is_nan = x_bull.is_nan();
1211                bull_ring_nan[idx] = is_nan;
1212                if is_nan {
1213                    bull_ring_vals[idx] = 0.0;
1214                    bull_nan_cnt += 1
1215                } else {
1216                    bull_ring_vals[idx] = x_bull;
1217                    bull_sum += x_bull
1218                }
1219                bull_ring_idx = if idx + 1 == sm { 0 } else { idx + 1 };
1220            }
1221
1222            if bear_ring_count < sm {
1223                let is_nan = x_bear.is_nan();
1224                bear_ring_nan[bear_ring_count] = is_nan;
1225                bear_ring_vals[bear_ring_count] = if is_nan { 0.0 } else { x_bear };
1226                if is_nan {
1227                    bear_nan_cnt += 1
1228                } else {
1229                    bear_sum += x_bear
1230                }
1231                bear_ring_count += 1;
1232            } else {
1233                let idx = bear_ring_idx;
1234                if bear_ring_nan[idx] {
1235                    bear_nan_cnt -= 1
1236                } else {
1237                    bear_sum -= bear_ring_vals[idx]
1238                }
1239                let is_nan = x_bear.is_nan();
1240                bear_ring_nan[idx] = is_nan;
1241                if is_nan {
1242                    bear_ring_vals[idx] = 0.0;
1243                    bear_nan_cnt += 1
1244                } else {
1245                    bear_ring_vals[idx] = x_bear;
1246                    bear_sum += x_bear
1247                }
1248                bear_ring_idx = if idx + 1 == sm { 0 } else { idx + 1 };
1249            }
1250
1251            let bull_disp = if bull_ring_count >= sm && bull_nan_cnt == 0 {
1252                bull_sum / (sm as f64)
1253            } else {
1254                f64::NAN
1255            };
1256            let bear_disp = if bear_ring_count >= sm && bear_nan_cnt == 0 {
1257                bear_sum / (sm as f64)
1258            } else {
1259                f64::NAN
1260            };
1261
1262            let prev_os = os;
1263            let next_os = if !bear_disp.is_nan() && price > bear_disp {
1264                Some(1)
1265            } else if !bull_disp.is_nan() && price < bull_disp {
1266                Some(-1)
1267            } else {
1268                os
1269            };
1270            os = next_os;
1271
1272            if let (Some(cur), Some(prev)) = (os, prev_os) {
1273                if cur == 1 && prev != 1 {
1274                    ts = Some(bull_disp);
1275                } else if cur == -1 && prev != -1 {
1276                    ts = Some(bear_disp);
1277                } else if cur == 1 {
1278                    if let Some(t) = ts {
1279                        ts = Some(bull_disp.max(t));
1280                    }
1281                } else if cur == -1 {
1282                    if let Some(t) = ts {
1283                        ts = Some(bear_disp.min(t));
1284                    }
1285                }
1286            } else {
1287                if os == Some(1) {
1288                    if let Some(t) = ts {
1289                        ts = Some(bull_disp.max(t));
1290                    }
1291                }
1292                if os == Some(-1) {
1293                    if let Some(t) = ts {
1294                        ts = Some(bear_disp.min(t));
1295                    }
1296                }
1297            }
1298
1299            if rst {
1300                if os == Some(1) {
1301                    if let Some(t) = ts {
1302                        if price < t {
1303                            ts = None;
1304                        }
1305                    } else if !bear_disp.is_nan() && price > bear_disp {
1306                        ts = Some(bull_disp);
1307                    }
1308                } else if os == Some(-1) {
1309                    if let Some(t) = ts {
1310                        if price > t {
1311                            ts = None;
1312                        }
1313                    } else if !bull_disp.is_nan() && price < bull_disp {
1314                        ts = Some(bear_disp);
1315                    }
1316                }
1317            }
1318
1319            let show = ts.is_some() || ts_prev.is_some();
1320            let ts_nz = if ts.is_some() { ts } else { ts_prev };
1321            if os == Some(1) && show {
1322                u_block[i] = f64::NAN;
1323                l_block[i] = bull_disp;
1324                uts_block[i] = f64::NAN;
1325                lts_block[i] = ts_nz.unwrap_or(f64::NAN);
1326            } else if os == Some(-1) && show {
1327                u_block[i] = bear_disp;
1328                l_block[i] = f64::NAN;
1329                uts_block[i] = ts_nz.unwrap_or(f64::NAN);
1330                lts_block[i] = f64::NAN;
1331            } else {
1332                u_block[i] = f64::NAN;
1333                l_block[i] = f64::NAN;
1334                uts_block[i] = f64::NAN;
1335                lts_block[i] = f64::NAN;
1336            }
1337            ts_prev = ts;
1338        }
1339
1340        for buf in [u_block, l_block, uts_block, lts_block] {
1341            for v in &mut buf[..warm] {
1342                *v = f64::NAN;
1343            }
1344        }
1345    };
1346
1347    #[cfg(not(target_arch = "wasm32"))]
1348    if parallel {
1349        use rayon::prelude::*;
1350        out.par_chunks_mut(4 * cols)
1351            .enumerate()
1352            .for_each(|(row, dst)| do_one(row, dst));
1353    } else {
1354        out.chunks_mut(4 * cols)
1355            .enumerate()
1356            .for_each(|(row, dst)| do_one(row, dst));
1357    }
1358
1359    #[cfg(target_arch = "wasm32")]
1360    out.chunks_mut(4 * cols)
1361        .enumerate()
1362        .for_each(|(row, dst)| do_one(row, dst));
1363
1364    Ok(combos)
1365}
1366
1367pub fn fvg_trailing_stop_batch_with_kernel(
1368    high: &[f64],
1369    low: &[f64],
1370    close: &[f64],
1371    sweep: &FvgTsBatchRange,
1372    kernel: Kernel,
1373) -> Result<FvgTsBatchOutput, FvgTrailingStopError> {
1374    if high.is_empty() || low.is_empty() || close.is_empty() {
1375        return Err(FvgTrailingStopError::EmptyInputData);
1376    }
1377    let len = high.len();
1378    if len != low.len() || len != close.len() {
1379        return Err(FvgTrailingStopError::InvalidPeriod {
1380            period: len,
1381            data_len: len,
1382        });
1383    }
1384    if !matches!(
1385        kernel,
1386        Kernel::Auto | Kernel::ScalarBatch | Kernel::Avx2Batch | Kernel::Avx512Batch
1387    ) {
1388        return Err(FvgTrailingStopError::InvalidKernelForBatch(kernel));
1389    }
1390
1391    let combos = expand_grid_ts(sweep)?;
1392    let rows = combos.len();
1393    let cols = len;
1394
1395    let first = first_valid_ohlc(high, low, close);
1396    if first == usize::MAX {
1397        return Err(FvgTrailingStopError::AllValuesNaN);
1398    }
1399    let mut max_sm = 0usize;
1400    let mut warms = Vec::with_capacity(4 * rows);
1401    for prm in &combos {
1402        let look = prm.unmitigated_fvg_lookback.unwrap_or(5);
1403        if look == 0 {
1404            return Err(FvgTrailingStopError::InvalidLookback { lookback: look });
1405        }
1406        let sm = prm.smoothing_length.unwrap_or(9);
1407        if sm == 0 {
1408            return Err(FvgTrailingStopError::InvalidSmoothingLength { smoothing: sm });
1409        }
1410        if sm > max_sm {
1411            max_sm = sm;
1412        }
1413        let w = (first + 2 + sm.saturating_sub(1)).min(cols);
1414        warms.extend_from_slice(&[w, w, w, w]);
1415    }
1416    let need = 2 + max_sm.saturating_sub(1);
1417    if cols - first < need {
1418        return Err(FvgTrailingStopError::NotEnoughValidData {
1419            needed: need,
1420            valid: cols - first,
1421        });
1422    }
1423
1424    let rows4 = rows
1425        .checked_mul(4)
1426        .ok_or_else(|| FvgTrailingStopError::InvalidRange {
1427            start: rows,
1428            end: 4,
1429            step: 1,
1430        })?;
1431    let mut buf_mu = make_uninit_matrix(rows4, cols);
1432    init_matrix_prefixes(&mut buf_mu, cols, &warms);
1433
1434    let flat: &mut [f64] =
1435        unsafe { core::slice::from_raw_parts_mut(buf_mu.as_mut_ptr() as *mut f64, buf_mu.len()) };
1436    let used = fvg_ts_batch_inner_into(high, low, close, sweep, kernel, true, flat)?;
1437
1438    let values = unsafe {
1439        Vec::from_raw_parts(
1440            buf_mu.as_mut_ptr() as *mut f64,
1441            buf_mu.len(),
1442            buf_mu.capacity(),
1443        )
1444    };
1445    core::mem::forget(buf_mu);
1446
1447    Ok(FvgTsBatchOutput {
1448        values,
1449        combos: used,
1450        rows,
1451        cols,
1452    })
1453}
1454
1455#[derive(Clone, Debug, Default)]
1456pub struct FvgTsBatchBuilder {
1457    range: FvgTsBatchRange,
1458    kernel: Kernel,
1459}
1460
1461impl FvgTsBatchBuilder {
1462    pub fn new() -> Self {
1463        Self::default()
1464    }
1465
1466    pub fn lookback_range(mut self, start: usize, end: usize, step: usize) -> Self {
1467        self.range.lookback = (start, end, step);
1468        self
1469    }
1470
1471    pub fn smoothing_range(mut self, start: usize, end: usize, step: usize) -> Self {
1472        self.range.smoothing = (start, end, step);
1473        self
1474    }
1475
1476    pub fn reset_toggle(mut self, include_false: bool, include_true: bool) -> Self {
1477        self.range.reset_on_cross = (include_false, include_true);
1478        self
1479    }
1480
1481    pub fn kernel(mut self, k: Kernel) -> Self {
1482        self.kernel = k;
1483        self
1484    }
1485
1486    pub fn apply_candles(self, c: &Candles) -> Result<FvgTsBatchOutput, FvgTrailingStopError> {
1487        fvg_trailing_stop_batch_with_kernel(&c.high, &c.low, &c.close, &self.range, self.kernel)
1488    }
1489
1490    pub fn apply_slices(
1491        self,
1492        high: &[f64],
1493        low: &[f64],
1494        close: &[f64],
1495    ) -> Result<FvgTsBatchOutput, FvgTrailingStopError> {
1496        fvg_trailing_stop_batch_with_kernel(high, low, close, &self.range, self.kernel)
1497    }
1498
1499    pub fn with_default_candles(c: &Candles) -> Result<FvgTsBatchOutput, FvgTrailingStopError> {
1500        FvgTsBatchBuilder::new()
1501            .kernel(Kernel::Auto)
1502            .apply_candles(c)
1503    }
1504
1505    pub fn with_default_slices(
1506        high: &[f64],
1507        low: &[f64],
1508        close: &[f64],
1509    ) -> Result<FvgTsBatchOutput, FvgTrailingStopError> {
1510        FvgTsBatchBuilder::new()
1511            .kernel(Kernel::Auto)
1512            .apply_slices(high, low, close)
1513    }
1514}
1515
1516use core::cmp::Ordering;
1517use std::cmp::Reverse;
1518use std::collections::BinaryHeap;
1519
1520#[inline]
1521fn f64_to_bits_pos(v: f64) -> u64 {
1522    debug_assert!(v.is_finite() && v >= 0.0);
1523    v.to_bits()
1524}
1525
1526#[derive(Copy, Clone, Debug)]
1527struct Slot {
1528    val: f64,
1529    alive: bool,
1530    stamp: u32,
1531}
1532
1533#[derive(Copy, Clone, Eq, PartialEq, Debug)]
1534struct HeapItem {
1535    bits: u64,
1536    slot: u32,
1537    stamp: u32,
1538    seq: u32,
1539}
1540impl Ord for HeapItem {
1541    #[inline]
1542    fn cmp(&self, other: &Self) -> Ordering {
1543        self.bits
1544            .cmp(&other.bits)
1545            .then(self.seq.cmp(&other.seq))
1546            .then(self.slot.cmp(&other.slot))
1547    }
1548}
1549impl PartialOrd for HeapItem {
1550    #[inline]
1551    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1552        Some(self.cmp(other))
1553    }
1554}
1555
1556pub struct FvgTrailingStopStream {
1557    lookback: usize,
1558    smoothing_len: usize,
1559    reset_on_cross: bool,
1560
1561    bull_slots: Vec<Slot>,
1562    bull_head: usize,
1563    bull_occ: usize,
1564    bull_sum: f64,
1565    bull_cnt: u32,
1566    bull_heap: BinaryHeap<HeapItem>,
1567    bull_seq: u32,
1568
1569    bear_slots: Vec<Slot>,
1570    bear_head: usize,
1571    bear_occ: usize,
1572    bear_sum: f64,
1573    bear_cnt: u32,
1574    bear_heap: BinaryHeap<Reverse<HeapItem>>,
1575    bear_seq: u32,
1576
1577    last_bull_non_na: Option<usize>,
1578    last_bear_non_na: Option<usize>,
1579
1580    xbull_vals: Vec<f64>,
1581    xbull_idx: usize,
1582    xbull_filled: usize,
1583    xbull_sum: f64,
1584    xbull_nan: u32,
1585
1586    xbear_vals: Vec<f64>,
1587    xbear_idx: usize,
1588    xbear_filled: usize,
1589    xbear_sum: f64,
1590    xbear_nan: u32,
1591
1592    pref_sum_ring: Vec<f64>,
1593    pref_nan_ring: Vec<u32>,
1594    pref_idx: usize,
1595    pref_sum_total: f64,
1596    pref_nan_total: u32,
1597
1598    os: Option<i8>,
1599    ts: Option<f64>,
1600    ts_prev: Option<f64>,
1601    bar_count: usize,
1602
1603    hi_m2: f64,
1604    hi_m1: f64,
1605    lo_m2: f64,
1606    lo_m1: f64,
1607    cl_m1: f64,
1608
1609    inv_w: f64,
1610}
1611
1612impl FvgTrailingStopStream {
1613    pub fn try_new(params: FvgTrailingStopParams) -> Result<Self, FvgTrailingStopError> {
1614        let lookback = params.unmitigated_fvg_lookback.unwrap_or(5);
1615        let smoothing_len = params.smoothing_length.unwrap_or(9);
1616        if lookback == 0 {
1617            return Err(FvgTrailingStopError::InvalidLookback { lookback });
1618        }
1619        if smoothing_len == 0 {
1620            return Err(FvgTrailingStopError::InvalidSmoothingLength {
1621                smoothing: smoothing_len,
1622            });
1623        }
1624
1625        let mut bull_slots = Vec::with_capacity(lookback);
1626        bull_slots.resize(
1627            lookback,
1628            Slot {
1629                val: f64::NAN,
1630                alive: false,
1631                stamp: 0,
1632            },
1633        );
1634        let mut bear_slots = Vec::with_capacity(lookback);
1635        bear_slots.resize(
1636            lookback,
1637            Slot {
1638                val: f64::NAN,
1639                alive: false,
1640                stamp: 0,
1641            },
1642        );
1643
1644        let mut xbull_vals = Vec::with_capacity(smoothing_len);
1645        xbull_vals.resize(smoothing_len, f64::NAN);
1646        let mut xbear_vals = Vec::with_capacity(smoothing_len);
1647        xbear_vals.resize(smoothing_len, f64::NAN);
1648
1649        let mut pref_sum_ring = Vec::with_capacity(smoothing_len + 1);
1650        pref_sum_ring.resize(smoothing_len + 1, 0.0);
1651        let mut pref_nan_ring = Vec::with_capacity(smoothing_len + 1);
1652        pref_nan_ring.resize(smoothing_len + 1, 0);
1653
1654        Ok(Self {
1655            lookback,
1656            smoothing_len,
1657            reset_on_cross: params.reset_on_cross.unwrap_or(false),
1658
1659            bull_slots,
1660            bull_head: 0,
1661            bull_occ: 0,
1662            bull_sum: 0.0,
1663            bull_cnt: 0,
1664            bull_heap: BinaryHeap::new(),
1665            bull_seq: 0,
1666
1667            bear_slots,
1668            bear_head: 0,
1669            bear_occ: 0,
1670            bear_sum: 0.0,
1671            bear_cnt: 0,
1672            bear_heap: BinaryHeap::new(),
1673            bear_seq: 0,
1674
1675            last_bull_non_na: None,
1676            last_bear_non_na: None,
1677
1678            xbull_vals,
1679            xbull_idx: 0,
1680            xbull_filled: 0,
1681            xbull_sum: 0.0,
1682            xbull_nan: 0,
1683
1684            xbear_vals,
1685            xbear_idx: 0,
1686            xbear_filled: 0,
1687            xbear_sum: 0.0,
1688            xbear_nan: 0,
1689
1690            pref_sum_ring,
1691            pref_nan_ring,
1692            pref_idx: 0,
1693            pref_sum_total: 0.0,
1694            pref_nan_total: 0,
1695
1696            os: None,
1697            ts: None,
1698            ts_prev: None,
1699            bar_count: 0,
1700
1701            hi_m2: f64::NAN,
1702            hi_m1: f64::NAN,
1703            lo_m2: f64::NAN,
1704            lo_m1: f64::NAN,
1705            cl_m1: f64::NAN,
1706
1707            inv_w: 1.0 / (smoothing_len as f64),
1708        })
1709    }
1710
1711    #[inline(always)]
1712    fn bull_push(&mut self, v: f64) {
1713        if v.is_nan() {
1714            return;
1715        }
1716        let idx = self.bull_head;
1717        if self.bull_occ == self.lookback {
1718            let s = &mut self.bull_slots[idx];
1719            if s.alive {
1720                self.bull_sum -= s.val;
1721                self.bull_cnt -= 1;
1722                s.alive = false;
1723            }
1724        } else {
1725            self.bull_occ += 1;
1726        }
1727        self.bull_seq = self.bull_seq.wrapping_add(1);
1728        let stamp = self.bull_seq;
1729
1730        self.bull_slots[idx] = Slot {
1731            val: v,
1732            alive: true,
1733            stamp,
1734        };
1735        self.bull_sum += v;
1736        self.bull_cnt += 1;
1737
1738        self.bull_heap.push(HeapItem {
1739            bits: f64_to_bits_pos(v),
1740            slot: idx as u32,
1741            stamp,
1742            seq: stamp,
1743        });
1744
1745        self.bull_head = if idx + 1 == self.lookback { 0 } else { idx + 1 };
1746    }
1747
1748    #[inline(always)]
1749    fn bear_push(&mut self, v: f64) {
1750        if v.is_nan() {
1751            return;
1752        }
1753        let idx = self.bear_head;
1754        if self.bear_occ == self.lookback {
1755            let s = &mut self.bear_slots[idx];
1756            if s.alive {
1757                self.bear_sum -= s.val;
1758                self.bear_cnt -= 1;
1759                s.alive = false;
1760            }
1761        } else {
1762            self.bear_occ += 1;
1763        }
1764        self.bear_seq = self.bear_seq.wrapping_add(1);
1765        let stamp = self.bear_seq;
1766
1767        self.bear_slots[idx] = Slot {
1768            val: v,
1769            alive: true,
1770            stamp,
1771        };
1772        self.bear_sum += v;
1773        self.bear_cnt += 1;
1774
1775        let item = HeapItem {
1776            bits: f64_to_bits_pos(v),
1777            slot: idx as u32,
1778            stamp,
1779            seq: stamp,
1780        };
1781        self.bear_heap.push(Reverse(item));
1782
1783        self.bear_head = if idx + 1 == self.lookback { 0 } else { idx + 1 };
1784    }
1785
1786    #[inline(always)]
1787    fn bull_sweep(&mut self, close: f64) {
1788        while let Some(top) = self.bull_heap.peek().copied() {
1789            let v = f64::from_bits(top.bits);
1790            if !(v > close) {
1791                break;
1792            }
1793            self.bull_heap.pop();
1794            let idx = top.slot as usize;
1795            if idx < self.bull_slots.len() {
1796                let s = &mut self.bull_slots[idx];
1797                if s.alive && s.stamp == top.stamp {
1798                    s.alive = false;
1799                    self.bull_sum -= s.val;
1800                    self.bull_cnt -= 1;
1801                }
1802            }
1803        }
1804    }
1805
1806    #[inline(always)]
1807    fn bear_sweep(&mut self, close: f64) {
1808        while let Some(Reverse(top)) = self.bear_heap.peek().copied() {
1809            let v = f64::from_bits(top.bits);
1810            if !(v < close) {
1811                break;
1812            }
1813            self.bear_heap.pop();
1814            let idx = top.slot as usize;
1815            if idx < self.bear_slots.len() {
1816                let s = &mut self.bear_slots[idx];
1817                if s.alive && s.stamp == top.stamp {
1818                    s.alive = false;
1819                    self.bear_sum -= s.val;
1820                    self.bear_cnt -= 1;
1821                }
1822            }
1823        }
1824    }
1825
1826    #[inline(always)]
1827    fn push_x_and_smooth(
1828        vals: &mut [f64],
1829        idx: &mut usize,
1830        filled: &mut usize,
1831        sum: &mut f64,
1832        nan: &mut u32,
1833        w: usize,
1834        inv_w: f64,
1835        x: f64,
1836    ) -> f64 {
1837        let pos = *idx;
1838
1839        if *filled == w {
1840            let old = vals[pos];
1841            if old.is_nan() {
1842                *nan -= 1;
1843            } else {
1844                *sum -= old;
1845            }
1846        } else {
1847            *filled += 1;
1848        }
1849
1850        vals[pos] = x;
1851        if x.is_nan() {
1852            *nan += 1;
1853        } else {
1854            *sum += x;
1855        }
1856
1857        *idx = if pos + 1 == w { 0 } else { pos + 1 };
1858
1859        if *filled == w && *nan == 0 {
1860            *sum * inv_w
1861        } else {
1862            f64::NAN
1863        }
1864    }
1865
1866    #[inline(always)]
1867    fn prefix_add_close(
1868        pref_sum_ring: &mut [f64],
1869        pref_nan_ring: &mut [u32],
1870        pref_idx: &mut usize,
1871        pref_sum_total: &mut f64,
1872        pref_nan_total: &mut u32,
1873        w: usize,
1874        close: f64,
1875    ) {
1876        let add = if close.is_nan() { 0.0 } else { close };
1877        let add_nan = if close.is_nan() { 1 } else { 0 };
1878        *pref_sum_total += add;
1879        *pref_nan_total += add_nan;
1880
1881        let ring_len = w + 1;
1882        let next = if *pref_idx + 1 == ring_len {
1883            0
1884        } else {
1885            *pref_idx + 1
1886        };
1887        pref_sum_ring[next] = *pref_sum_total;
1888        pref_nan_ring[next] = *pref_nan_total;
1889        *pref_idx = next;
1890    }
1891
1892    #[inline(always)]
1893    fn prefix_last_bs(
1894        pref_sum_ring: &[f64],
1895        pref_nan_ring: &[u32],
1896        pref_idx: usize,
1897        w: usize,
1898        bs: usize,
1899    ) -> (f64, u32) {
1900        debug_assert!(bs >= 1 && bs <= w);
1901        let ring_len = w + 1;
1902        let prev = (pref_idx + ring_len - bs) % ring_len;
1903        let s = pref_sum_ring[pref_idx] - pref_sum_ring[prev];
1904        let nans = pref_nan_ring[pref_idx] - pref_nan_ring[prev];
1905        (s, nans)
1906    }
1907
1908    pub fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64, f64, f64)> {
1909        Self::prefix_add_close(
1910            &mut self.pref_sum_ring,
1911            &mut self.pref_nan_ring,
1912            &mut self.pref_idx,
1913            &mut self.pref_sum_total,
1914            &mut self.pref_nan_total,
1915            self.smoothing_len,
1916            close,
1917        );
1918
1919        if self.bar_count >= 2
1920            && self.hi_m2.is_finite()
1921            && self.lo_m2.is_finite()
1922            && self.cl_m1.is_finite()
1923        {
1924            if low > self.hi_m2 && self.cl_m1 > self.hi_m2 {
1925                self.bull_push(self.hi_m2);
1926            }
1927            if high < self.lo_m2 && self.cl_m1 < self.lo_m2 {
1928                self.bear_push(self.lo_m2);
1929            }
1930        }
1931
1932        self.bull_sweep(close);
1933        self.bear_sweep(close);
1934
1935        let bull_avg = if self.bull_cnt > 0 {
1936            self.bull_sum / (self.bull_cnt as f64)
1937        } else {
1938            f64::NAN
1939        };
1940        let bear_avg = if self.bear_cnt > 0 {
1941            self.bear_sum / (self.bear_cnt as f64)
1942        } else {
1943            f64::NAN
1944        };
1945        if !bull_avg.is_nan() {
1946            self.last_bull_non_na = Some(self.bar_count);
1947        }
1948        if !bear_avg.is_nan() {
1949            self.last_bear_non_na = Some(self.bar_count);
1950        }
1951
1952        let bull_bs = if bull_avg.is_nan() {
1953            match self.last_bull_non_na {
1954                Some(last) => ((self.bar_count - last).max(1)).min(self.smoothing_len),
1955                None => 1,
1956            }
1957        } else {
1958            1
1959        };
1960        let bear_bs = if bear_avg.is_nan() {
1961            match self.last_bear_non_na {
1962                Some(last) => ((self.bar_count - last).max(1)).min(self.smoothing_len),
1963                None => 1,
1964            }
1965        } else {
1966            1
1967        };
1968
1969        let bull_sma = if bull_avg.is_nan() {
1970            let (s, nans) = Self::prefix_last_bs(
1971                &self.pref_sum_ring,
1972                &self.pref_nan_ring,
1973                self.pref_idx,
1974                self.smoothing_len,
1975                bull_bs,
1976            );
1977            if nans == 0 {
1978                s / (bull_bs as f64)
1979            } else {
1980                f64::NAN
1981            }
1982        } else {
1983            f64::NAN
1984        };
1985        let bear_sma = if bear_avg.is_nan() {
1986            let (s, nans) = Self::prefix_last_bs(
1987                &self.pref_sum_ring,
1988                &self.pref_nan_ring,
1989                self.pref_idx,
1990                self.smoothing_len,
1991                bear_bs,
1992            );
1993            if nans == 0 {
1994                s / (bear_bs as f64)
1995            } else {
1996                f64::NAN
1997            }
1998        } else {
1999            f64::NAN
2000        };
2001
2002        let x_bull = if !bull_avg.is_nan() {
2003            bull_avg
2004        } else {
2005            bull_sma
2006        };
2007        let x_bear = if !bear_avg.is_nan() {
2008            bear_avg
2009        } else {
2010            bear_sma
2011        };
2012
2013        let bull_disp = Self::push_x_and_smooth(
2014            &mut self.xbull_vals,
2015            &mut self.xbull_idx,
2016            &mut self.xbull_filled,
2017            &mut self.xbull_sum,
2018            &mut self.xbull_nan,
2019            self.smoothing_len,
2020            self.inv_w,
2021            x_bull,
2022        );
2023        let bear_disp = Self::push_x_and_smooth(
2024            &mut self.xbear_vals,
2025            &mut self.xbear_idx,
2026            &mut self.xbear_filled,
2027            &mut self.xbear_sum,
2028            &mut self.xbear_nan,
2029            self.smoothing_len,
2030            self.inv_w,
2031            x_bear,
2032        );
2033
2034        let prev_os = self.os;
2035        let next_os = if !bear_disp.is_nan() && close > bear_disp {
2036            Some(1)
2037        } else if !bull_disp.is_nan() && close < bull_disp {
2038            Some(-1)
2039        } else {
2040            self.os
2041        };
2042        self.os = next_os;
2043
2044        if let (Some(cur), Some(prev)) = (self.os, prev_os) {
2045            if cur == 1 && prev != 1 {
2046                self.ts = Some(bull_disp);
2047            } else if cur == -1 && prev != -1 {
2048                self.ts = Some(bear_disp);
2049            } else if cur == 1 {
2050                if let Some(t) = self.ts {
2051                    self.ts = Some(bull_disp.max(t));
2052                }
2053            } else if cur == -1 {
2054                if let Some(t) = self.ts {
2055                    self.ts = Some(bear_disp.min(t));
2056                }
2057            }
2058        } else {
2059            if self.os == Some(1) {
2060                if let Some(t) = self.ts {
2061                    self.ts = Some(bull_disp.max(t));
2062                }
2063            }
2064            if self.os == Some(-1) {
2065                if let Some(t) = self.ts {
2066                    self.ts = Some(bear_disp.min(t));
2067                }
2068            }
2069        }
2070
2071        if self.reset_on_cross {
2072            if self.os == Some(1) {
2073                if let Some(t) = self.ts {
2074                    if close < t {
2075                        self.ts = None;
2076                    }
2077                } else if !bear_disp.is_nan() && close > bear_disp {
2078                    self.ts = Some(bull_disp);
2079                }
2080            } else if self.os == Some(-1) {
2081                if let Some(t) = self.ts {
2082                    if close > t {
2083                        self.ts = None;
2084                    }
2085                } else if !bull_disp.is_nan() && close < bull_disp {
2086                    self.ts = Some(bear_disp);
2087                }
2088            }
2089        }
2090
2091        let show = self.ts.is_some() || self.ts_prev.is_some();
2092        let ts_nz = self.ts.or(self.ts_prev);
2093
2094        let (mut upper, mut lower, mut upper_ts, mut lower_ts) =
2095            (f64::NAN, f64::NAN, f64::NAN, f64::NAN);
2096
2097        if self.os == Some(1) && show {
2098            lower = bull_disp;
2099            lower_ts = ts_nz.unwrap_or(f64::NAN);
2100        } else if self.os == Some(-1) && show {
2101            upper = bear_disp;
2102            upper_ts = ts_nz.unwrap_or(f64::NAN);
2103        }
2104
2105        self.ts_prev = self.ts;
2106
2107        self.hi_m2 = self.hi_m1;
2108        self.hi_m1 = high;
2109        self.lo_m2 = self.lo_m1;
2110        self.lo_m1 = low;
2111        self.cl_m1 = close;
2112
2113        self.bar_count += 1;
2114
2115        if self.bar_count >= self.smoothing_len + 2 {
2116            Some((upper, lower, upper_ts, lower_ts))
2117        } else {
2118            None
2119        }
2120    }
2121}
2122
2123#[cfg(feature = "python")]
2124#[pyfunction(name = "fvg_trailing_stop")]
2125#[pyo3(signature = (high, low, close, unmitigated_fvg_lookback, smoothing_length, reset_on_cross, kernel=None))]
2126pub fn fvg_trailing_stop_py<'py>(
2127    py: Python<'py>,
2128    high: PyReadonlyArray1<'py, f64>,
2129    low: PyReadonlyArray1<'py, f64>,
2130    close: PyReadonlyArray1<'py, f64>,
2131    unmitigated_fvg_lookback: usize,
2132    smoothing_length: usize,
2133    reset_on_cross: bool,
2134    kernel: Option<&str>,
2135) -> PyResult<(
2136    Bound<'py, PyArray1<f64>>,
2137    Bound<'py, PyArray1<f64>>,
2138    Bound<'py, PyArray1<f64>>,
2139    Bound<'py, PyArray1<f64>>,
2140)> {
2141    use numpy::IntoPyArray;
2142    let (h, l, c) = (high.as_slice()?, low.as_slice()?, close.as_slice()?);
2143    let kern = validate_kernel(kernel, false)?;
2144    let params = FvgTrailingStopParams {
2145        unmitigated_fvg_lookback: Some(unmitigated_fvg_lookback),
2146        smoothing_length: Some(smoothing_length),
2147        reset_on_cross: Some(reset_on_cross),
2148    };
2149    let input = FvgTrailingStopInput::from_slices(h, l, c, params);
2150    let out = py
2151        .allow_threads(|| fvg_trailing_stop_with_kernel(&input, kern))
2152        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2153    Ok((
2154        out.upper.into_pyarray(py),
2155        out.lower.into_pyarray(py),
2156        out.upper_ts.into_pyarray(py),
2157        out.lower_ts.into_pyarray(py),
2158    ))
2159}
2160
2161#[cfg(all(feature = "python", feature = "cuda"))]
2162use crate::utilities::dlpack_cuda::{make_device_array_py, DeviceArrayF32Py};
2163#[cfg(all(feature = "python", feature = "cuda"))]
2164#[cfg(all(feature = "python", feature = "cuda"))]
2165#[pyfunction(name = "fvg_trailing_stop_cuda_batch_dev")]
2166#[pyo3(signature = (high, low, close, lookback_range, smoothing_range, reset_toggle, device_id=0))]
2167pub fn fvg_trailing_stop_cuda_batch_dev_py(
2168    py: Python<'_>,
2169    high: PyReadonlyArray1<'_, f32>,
2170    low: PyReadonlyArray1<'_, f32>,
2171    close: PyReadonlyArray1<'_, f32>,
2172    lookback_range: (usize, usize, usize),
2173    smoothing_range: (usize, usize, usize),
2174    reset_toggle: (bool, bool),
2175    device_id: usize,
2176) -> PyResult<(
2177    DeviceArrayF32Py,
2178    DeviceArrayF32Py,
2179    DeviceArrayF32Py,
2180    DeviceArrayF32Py,
2181)> {
2182    use crate::cuda::cuda_available;
2183    if !cuda_available() {
2184        return Err(PyValueError::new_err("CUDA not available"));
2185    }
2186    let (h, l, c) = (high.as_slice()?, low.as_slice()?, close.as_slice()?);
2187    let sweep = FvgTsBatchRange {
2188        lookback: lookback_range,
2189        smoothing: smoothing_range,
2190        reset_on_cross: reset_toggle,
2191    };
2192    let (u, lwr, uts, lts) = py.allow_threads(|| {
2193        let cuda = crate::cuda::fvg_trailing_stop_wrapper::CudaFvgTs::new(device_id)
2194            .map_err(|e| PyValueError::new_err(e.to_string()))?;
2195        let batch = cuda
2196            .fvg_ts_batch_dev(h, l, c, &sweep)
2197            .map_err(|e| PyValueError::new_err(e.to_string()))?;
2198        Ok::<_, PyErr>((batch.upper, batch.lower, batch.upper_ts, batch.lower_ts))
2199    })?;
2200    let upper_dev = make_device_array_py(device_id, u)?;
2201    let lower_dev = make_device_array_py(device_id, lwr)?;
2202    let upper_ts_dev = make_device_array_py(device_id, uts)?;
2203    let lower_ts_dev = make_device_array_py(device_id, lts)?;
2204    Ok((upper_dev, lower_dev, upper_ts_dev, lower_ts_dev))
2205}
2206
2207#[cfg(all(feature = "python", feature = "cuda"))]
2208#[pyfunction(name = "fvg_trailing_stop_cuda_many_series_one_param_dev")]
2209#[pyo3(signature = (high_tm, low_tm, close_tm, cols, rows, unmitigated_fvg_lookback, smoothing_length, reset_on_cross, device_id=0))]
2210pub fn fvg_trailing_stop_cuda_many_series_one_param_dev_py(
2211    py: Python<'_>,
2212    high_tm: PyReadonlyArray1<'_, f32>,
2213    low_tm: PyReadonlyArray1<'_, f32>,
2214    close_tm: PyReadonlyArray1<'_, f32>,
2215    cols: usize,
2216    rows: usize,
2217    unmitigated_fvg_lookback: usize,
2218    smoothing_length: usize,
2219    reset_on_cross: bool,
2220    device_id: usize,
2221) -> PyResult<(
2222    DeviceArrayF32Py,
2223    DeviceArrayF32Py,
2224    DeviceArrayF32Py,
2225    DeviceArrayF32Py,
2226)> {
2227    use crate::cuda::cuda_available;
2228    if !cuda_available() {
2229        return Err(PyValueError::new_err("CUDA not available"));
2230    }
2231    let (h, l, c) = (
2232        high_tm.as_slice()?,
2233        low_tm.as_slice()?,
2234        close_tm.as_slice()?,
2235    );
2236    if h.len() != l.len() || h.len() != c.len() || h.len() != cols * rows {
2237        return Err(PyValueError::new_err(
2238            "time-major arrays must match cols*rows",
2239        ));
2240    }
2241    let params = FvgTrailingStopParams {
2242        unmitigated_fvg_lookback: Some(unmitigated_fvg_lookback),
2243        smoothing_length: Some(smoothing_length),
2244        reset_on_cross: Some(reset_on_cross),
2245    };
2246    let (u, lw, uts, lts) = py.allow_threads(|| {
2247        let cuda = crate::cuda::fvg_trailing_stop_wrapper::CudaFvgTs::new(device_id)
2248            .map_err(|e| PyValueError::new_err(e.to_string()))?;
2249        cuda.fvg_ts_many_series_one_param_time_major_dev(h, l, c, cols, rows, &params)
2250            .map_err(|e| PyValueError::new_err(e.to_string()))
2251    })?;
2252    let upper_dev = make_device_array_py(device_id, u)?;
2253    let lower_dev = make_device_array_py(device_id, lw)?;
2254    let upper_ts_dev = make_device_array_py(device_id, uts)?;
2255    let lower_ts_dev = make_device_array_py(device_id, lts)?;
2256    Ok((upper_dev, lower_dev, upper_ts_dev, lower_ts_dev))
2257}
2258
2259#[cfg(feature = "python")]
2260#[pyfunction(name = "fvg_trailing_stop_batch")]
2261#[pyo3(signature = (high, low, close, lookback_range, smoothing_range, reset_toggle, kernel=None))]
2262pub fn fvg_trailing_stop_batch_py<'py>(
2263    py: Python<'py>,
2264    high: PyReadonlyArray1<'py, f64>,
2265    low: PyReadonlyArray1<'py, f64>,
2266    close: PyReadonlyArray1<'py, f64>,
2267    lookback_range: (usize, usize, usize),
2268    smoothing_range: (usize, usize, usize),
2269    reset_toggle: (bool, bool),
2270    kernel: Option<&str>,
2271) -> PyResult<Bound<'py, PyDict>> {
2272    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
2273
2274    let (h, l, c) = (high.as_slice()?, low.as_slice()?, close.as_slice()?);
2275    let sweep = FvgTsBatchRange {
2276        lookback: lookback_range,
2277        smoothing: smoothing_range,
2278        reset_on_cross: reset_toggle,
2279    };
2280    let kern = validate_kernel(kernel, true)?;
2281
2282    let combos = expand_grid_ts(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
2283    let rows = combos.len();
2284    let cols = h.len();
2285    let rows4 = rows
2286        .checked_mul(4)
2287        .ok_or_else(|| PyValueError::new_err("rows*4 overflow"))?;
2288    let total = rows4
2289        .checked_mul(cols)
2290        .ok_or_else(|| PyValueError::new_err("rows*4*cols overflow"))?;
2291
2292    let flat = unsafe { PyArray1::<f64>::new(py, [total], false) };
2293    let flat_mut = unsafe { flat.as_slice_mut()? };
2294
2295    py.allow_threads(|| fvg_ts_batch_inner_into(h, l, c, &sweep, kern, true, flat_mut))
2296        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2297
2298    let dict = PyDict::new(py);
2299
2300    dict.set_item("values", flat.reshape((rows4, cols))?)?;
2301    dict.set_item(
2302        "lookbacks",
2303        combos
2304            .iter()
2305            .map(|p| p.unmitigated_fvg_lookback.unwrap() as u64)
2306            .collect::<Vec<_>>()
2307            .into_pyarray(py),
2308    )?;
2309    dict.set_item(
2310        "smoothings",
2311        combos
2312            .iter()
2313            .map(|p| p.smoothing_length.unwrap() as u64)
2314            .collect::<Vec<_>>()
2315            .into_pyarray(py),
2316    )?;
2317    dict.set_item(
2318        "resets",
2319        combos
2320            .iter()
2321            .map(|p| p.reset_on_cross.unwrap_or(false))
2322            .collect::<Vec<_>>()
2323            .into_pyarray(py),
2324    )?;
2325    Ok(dict)
2326}
2327
2328#[cfg(feature = "python")]
2329#[pyclass]
2330pub struct FvgTrailingStopStreamPy {
2331    stream: FvgTrailingStopStream,
2332}
2333
2334#[cfg(feature = "python")]
2335#[pymethods]
2336impl FvgTrailingStopStreamPy {
2337    #[new]
2338    fn new(
2339        unmitigated_fvg_lookback: usize,
2340        smoothing_length: usize,
2341        reset_on_cross: bool,
2342    ) -> PyResult<Self> {
2343        let params = FvgTrailingStopParams {
2344            unmitigated_fvg_lookback: Some(unmitigated_fvg_lookback),
2345            smoothing_length: Some(smoothing_length),
2346            reset_on_cross: Some(reset_on_cross),
2347        };
2348        let stream = FvgTrailingStopStream::try_new(params)
2349            .map_err(|e| PyValueError::new_err(e.to_string()))?;
2350        Ok(FvgTrailingStopStreamPy { stream })
2351    }
2352
2353    fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64, f64, f64)> {
2354        self.stream.update(high, low, close)
2355    }
2356}
2357
2358#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2359#[wasm_bindgen]
2360pub fn fvg_ts_alloc(len: usize) -> *mut f64 {
2361    let mut v = Vec::<f64>::with_capacity(len);
2362    let p = v.as_mut_ptr();
2363    std::mem::forget(v);
2364    p
2365}
2366
2367#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2368#[wasm_bindgen]
2369pub fn fvg_ts_free(ptr: *mut f64, len: usize) {
2370    unsafe {
2371        let _ = Vec::from_raw_parts(ptr, len, len);
2372    }
2373}
2374
2375#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2376#[derive(Serialize, Deserialize)]
2377pub struct FvgTsJsOutput {
2378    pub values: Vec<f64>,
2379    pub rows: usize,
2380    pub cols: usize,
2381}
2382
2383#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2384#[derive(Serialize, Deserialize)]
2385pub struct FvgTsBatchJsOutput {
2386    pub values: Vec<f64>,
2387    pub combos: Vec<FvgTrailingStopParams>,
2388    pub rows: usize,
2389    pub cols: usize,
2390}
2391
2392#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2393#[wasm_bindgen(js_name = "fvgTrailingStop")]
2394pub fn fvg_trailing_stop_js(
2395    high: &[f64],
2396    low: &[f64],
2397    close: &[f64],
2398    unmitigated_fvg_lookback: usize,
2399    smoothing_length: usize,
2400    reset_on_cross: bool,
2401) -> Result<JsValue, JsValue> {
2402    if high.is_empty() || low.is_empty() || close.is_empty() {
2403        return Err(JsValue::from_str(
2404            "fvg_trailing_stop: Input data slice is empty.",
2405        ));
2406    }
2407
2408    let params = FvgTrailingStopParams {
2409        unmitigated_fvg_lookback: Some(unmitigated_fvg_lookback),
2410        smoothing_length: Some(smoothing_length),
2411        reset_on_cross: Some(reset_on_cross),
2412    };
2413    let input = FvgTrailingStopInput::from_slices(high, low, close, params);
2414
2415    let (h, low_in, c, lookback, smoothing_len, reset, first) =
2416        fvg_ts_prepare(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
2417    let len = h.len();
2418    let warm = (first + 2 + smoothing_len.saturating_sub(1)).min(len);
2419
2420    let mut buf_mu = make_uninit_matrix(4, len);
2421    init_matrix_prefixes(&mut buf_mu, len, &[warm, warm, warm, warm]);
2422    let out: &mut [f64] =
2423        unsafe { core::slice::from_raw_parts_mut(buf_mu.as_mut_ptr() as *mut f64, buf_mu.len()) };
2424    let (first_half, second_half) = out.split_at_mut(2 * len);
2425    let (u, l) = first_half.split_at_mut(len);
2426    let (uts, lts) = second_half.split_at_mut(len);
2427
2428    let chosen = Kernel::Scalar;
2429    fvg_ts_compute_into(
2430        h,
2431        low_in,
2432        c,
2433        lookback,
2434        smoothing_len,
2435        reset,
2436        u,
2437        l,
2438        uts,
2439        lts,
2440        chosen,
2441    );
2442    for v in &mut u[..warm] {
2443        *v = f64::NAN;
2444    }
2445    for v in &mut l[..warm] {
2446        *v = f64::NAN;
2447    }
2448    for v in &mut uts[..warm] {
2449        *v = f64::NAN;
2450    }
2451    for v in &mut lts[..warm] {
2452        *v = f64::NAN;
2453    }
2454
2455    let obj = js_sys::Object::new();
2456    let upper_arr = js_sys::Array::from_iter(u.iter().map(|&v| JsValue::from_f64(v)));
2457    let lower_arr = js_sys::Array::from_iter(l.iter().map(|&v| JsValue::from_f64(v)));
2458    let upper_ts_arr = js_sys::Array::from_iter(uts.iter().map(|&v| JsValue::from_f64(v)));
2459    let lower_ts_arr = js_sys::Array::from_iter(lts.iter().map(|&v| JsValue::from_f64(v)));
2460
2461    js_sys::Reflect::set(&obj, &JsValue::from_str("upper"), &upper_arr)?;
2462    js_sys::Reflect::set(&obj, &JsValue::from_str("lower"), &lower_arr)?;
2463    js_sys::Reflect::set(&obj, &JsValue::from_str("upperTs"), &upper_ts_arr)?;
2464    js_sys::Reflect::set(&obj, &JsValue::from_str("lowerTs"), &lower_ts_arr)?;
2465
2466    Ok(obj.into())
2467}
2468
2469#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2470#[wasm_bindgen]
2471pub fn fvg_trailing_stop_into_flat(
2472    high_ptr: *const f64,
2473    low_ptr: *const f64,
2474    close_ptr: *const f64,
2475    out_ptr: *mut f64,
2476    len: usize,
2477    unmitigated_fvg_lookback: usize,
2478    smoothing_length: usize,
2479    reset_on_cross: bool,
2480) -> Result<(), JsValue> {
2481    if [
2482        high_ptr as usize,
2483        low_ptr as usize,
2484        close_ptr as usize,
2485        out_ptr as usize,
2486    ]
2487    .iter()
2488    .any(|&p| p == 0)
2489    {
2490        return Err(JsValue::from_str("null pointer"));
2491    }
2492    unsafe {
2493        let h = core::slice::from_raw_parts(high_ptr, len);
2494        let l = core::slice::from_raw_parts(low_ptr, len);
2495        let c = core::slice::from_raw_parts(close_ptr, len);
2496        let out = core::slice::from_raw_parts_mut(out_ptr, 4 * len);
2497        let (first_half, second_half) = out.split_at_mut(2 * len);
2498        let (u, lw) = first_half.split_at_mut(len);
2499        let (uts, lts) = second_half.split_at_mut(len);
2500        let params = FvgTrailingStopParams {
2501            unmitigated_fvg_lookback: Some(unmitigated_fvg_lookback),
2502            smoothing_length: Some(smoothing_length),
2503            reset_on_cross: Some(reset_on_cross),
2504        };
2505        let input = FvgTrailingStopInput::from_slices(h, l, c, params);
2506        fvg_trailing_stop_into_slices(u, lw, uts, lts, &input, Kernel::Auto)
2507            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2508    }
2509    Ok(())
2510}
2511
2512#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2513#[wasm_bindgen(js_name = "fvgTrailingStopBatch")]
2514pub fn fvg_trailing_stop_batch_js(
2515    high: &[f64],
2516    low: &[f64],
2517    close: &[f64],
2518    lookback_start: usize,
2519    lookback_end: usize,
2520    lookback_step: usize,
2521    smoothing_start: usize,
2522    smoothing_end: usize,
2523    smoothing_step: usize,
2524    reset_include_false: bool,
2525    reset_include_true: bool,
2526) -> Result<JsValue, JsValue> {
2527    if high.is_empty() || low.is_empty() || close.is_empty() {
2528        return Err(JsValue::from_str(
2529            "fvg_trailing_stop: Input data slice is empty.",
2530        ));
2531    }
2532    let cols = high.len();
2533    if cols != low.len() || cols != close.len() {
2534        let e = FvgTrailingStopError::InvalidPeriod {
2535            period: cols,
2536            data_len: cols,
2537        };
2538        return Err(JsValue::from_str(&e.to_string()));
2539    }
2540    let sweep = FvgTsBatchRange {
2541        lookback: (lookback_start, lookback_end, lookback_step),
2542        smoothing: (smoothing_start, smoothing_end, smoothing_step),
2543        reset_on_cross: (reset_include_false, reset_include_true),
2544    };
2545    let combos = expand_grid_ts(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2546    let rows = combos.len();
2547
2548    let first = first_valid_ohlc(high, low, close);
2549    if first == usize::MAX {
2550        let e = FvgTrailingStopError::AllValuesNaN;
2551        return Err(JsValue::from_str(&e.to_string()));
2552    }
2553    let mut max_sm = 0usize;
2554    let rows4 = rows
2555        .checked_mul(4)
2556        .ok_or_else(|| JsValue::from_str("rows*4 overflow"))?;
2557    let mut buf_mu = make_uninit_matrix(rows4, cols);
2558    let mut warms = Vec::with_capacity(rows4);
2559    for prm in &combos {
2560        let look = prm.unmitigated_fvg_lookback.unwrap_or(5);
2561        if look == 0 {
2562            let e = FvgTrailingStopError::InvalidLookback { lookback: look };
2563            return Err(JsValue::from_str(&e.to_string()));
2564        }
2565        let sm = prm.smoothing_length.unwrap_or(9);
2566        if sm == 0 {
2567            let e = FvgTrailingStopError::InvalidSmoothingLength { smoothing: sm };
2568            return Err(JsValue::from_str(&e.to_string()));
2569        }
2570        if sm > max_sm {
2571            max_sm = sm;
2572        }
2573        let w = (first + 2 + sm.saturating_sub(1)).min(cols);
2574        warms.extend_from_slice(&[w, w, w, w]);
2575    }
2576    let need = 2 + max_sm.saturating_sub(1);
2577    if cols - first < need {
2578        let e = FvgTrailingStopError::NotEnoughValidData {
2579            needed: need,
2580            valid: cols - first,
2581        };
2582        return Err(JsValue::from_str(&e.to_string()));
2583    }
2584    init_matrix_prefixes(&mut buf_mu, cols, &warms);
2585
2586    let flat: &mut [f64] =
2587        unsafe { core::slice::from_raw_parts_mut(buf_mu.as_mut_ptr() as *mut f64, buf_mu.len()) };
2588    fvg_ts_batch_inner_into(
2589        high,
2590        low,
2591        close,
2592        &sweep,
2593        detect_best_batch_kernel(),
2594        false,
2595        flat,
2596    )
2597    .map_err(|e| JsValue::from_str(&e.to_string()))?;
2598
2599    let values = unsafe {
2600        Vec::from_raw_parts(
2601            buf_mu.as_mut_ptr() as *mut f64,
2602            buf_mu.len(),
2603            buf_mu.capacity(),
2604        )
2605    };
2606    core::mem::forget(buf_mu);
2607
2608    let out = FvgTsBatchJsOutput {
2609        values,
2610        combos,
2611        rows,
2612        cols,
2613    };
2614    serde_wasm_bindgen::to_value(&out)
2615        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2616}
2617
2618#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2619#[wasm_bindgen(js_name = "fvgTrailingStopAlloc")]
2620pub fn fvg_trailing_stop_alloc_js(size: usize) -> *mut f64 {
2621    let mut vec = Vec::<f64>::with_capacity(size * 4);
2622    let ptr = vec.as_mut_ptr();
2623    std::mem::forget(vec);
2624    ptr
2625}
2626
2627#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2628#[wasm_bindgen(js_name = "fvgTrailingStopFree")]
2629pub fn fvg_trailing_stop_free_js(ptr: *mut f64, size: usize) {
2630    unsafe {
2631        let _ = Vec::from_raw_parts(ptr, size * 4, size * 4);
2632    }
2633}
2634
2635#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2636#[wasm_bindgen(js_name = "fvgTrailingStopZeroCopy")]
2637pub fn fvg_trailing_stop_zero_copy_js(
2638    high: &[f64],
2639    low: &[f64],
2640    close: &[f64],
2641    unmitigated_fvg_lookback: usize,
2642    smoothing_length: usize,
2643    reset_on_cross: bool,
2644    ptr: *mut f64,
2645) -> Result<JsValue, JsValue> {
2646    if ptr.is_null() {
2647        return Err(JsValue::from_str("null pointer"));
2648    }
2649    let len = high.len();
2650
2651    let (upper, lower, upper_ts, lower_ts) = unsafe {
2652        (
2653            std::slice::from_raw_parts_mut(ptr, len),
2654            std::slice::from_raw_parts_mut(ptr.add(len), len),
2655            std::slice::from_raw_parts_mut(ptr.add(len * 2), len),
2656            std::slice::from_raw_parts_mut(ptr.add(len * 3), len),
2657        )
2658    };
2659
2660    for i in 0..len {
2661        upper[i] = f64::NAN;
2662        lower[i] = f64::NAN;
2663        upper_ts[i] = f64::NAN;
2664        lower_ts[i] = f64::NAN;
2665    }
2666
2667    let params = FvgTrailingStopParams {
2668        unmitigated_fvg_lookback: Some(unmitigated_fvg_lookback),
2669        smoothing_length: Some(smoothing_length),
2670        reset_on_cross: Some(reset_on_cross),
2671    };
2672
2673    let input = FvgTrailingStopInput {
2674        data: FvgTrailingStopData::Slices { high, low, close },
2675        params,
2676    };
2677
2678    fvg_trailing_stop_into_slices(upper, lower, upper_ts, lower_ts, &input, Kernel::Auto)
2679        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2680
2681    let obj = js_sys::Object::new();
2682    let upper_arr = unsafe { js_sys::Float64Array::view(upper) };
2683    let lower_arr = unsafe { js_sys::Float64Array::view(lower) };
2684    let upper_ts_arr = unsafe { js_sys::Float64Array::view(upper_ts) };
2685    let lower_ts_arr = unsafe { js_sys::Float64Array::view(lower_ts) };
2686
2687    js_sys::Reflect::set(&obj, &JsValue::from_str("upper"), &upper_arr)?;
2688    js_sys::Reflect::set(&obj, &JsValue::from_str("lower"), &lower_arr)?;
2689    js_sys::Reflect::set(&obj, &JsValue::from_str("upperTs"), &upper_ts_arr)?;
2690    js_sys::Reflect::set(&obj, &JsValue::from_str("lowerTs"), &lower_ts_arr)?;
2691
2692    Ok(obj.into())
2693}
2694
2695#[derive(Copy, Clone, Debug)]
2696pub struct FvgTrailingStopBuilder {
2697    unmitigated_fvg_lookback: Option<usize>,
2698    smoothing_length: Option<usize>,
2699    reset_on_cross: Option<bool>,
2700    kernel: Kernel,
2701}
2702
2703impl Default for FvgTrailingStopBuilder {
2704    fn default() -> Self {
2705        Self {
2706            unmitigated_fvg_lookback: None,
2707            smoothing_length: None,
2708            reset_on_cross: None,
2709            kernel: Kernel::Auto,
2710        }
2711    }
2712}
2713
2714impl FvgTrailingStopBuilder {
2715    pub fn new() -> Self {
2716        Self::default()
2717    }
2718
2719    pub fn lookback(mut self, n: usize) -> Self {
2720        self.unmitigated_fvg_lookback = Some(n);
2721        self
2722    }
2723
2724    pub fn smoothing(mut self, n: usize) -> Self {
2725        self.smoothing_length = Some(n);
2726        self
2727    }
2728
2729    pub fn reset_on_cross(mut self, reset: bool) -> Self {
2730        self.reset_on_cross = Some(reset);
2731        self
2732    }
2733
2734    pub fn kernel(mut self, k: Kernel) -> Self {
2735        self.kernel = k;
2736        self
2737    }
2738
2739    pub fn apply(&self, candles: &Candles) -> Result<FvgTrailingStopOutput, FvgTrailingStopError> {
2740        let params = FvgTrailingStopParams {
2741            unmitigated_fvg_lookback: self.unmitigated_fvg_lookback,
2742            smoothing_length: self.smoothing_length,
2743            reset_on_cross: self.reset_on_cross,
2744        };
2745        let input = FvgTrailingStopInput::from_candles(candles, params);
2746        fvg_trailing_stop_with_kernel(&input, self.kernel)
2747    }
2748
2749    pub fn apply_slice(
2750        &self,
2751        high: &[f64],
2752        low: &[f64],
2753        close: &[f64],
2754    ) -> Result<FvgTrailingStopOutput, FvgTrailingStopError> {
2755        let params = FvgTrailingStopParams {
2756            unmitigated_fvg_lookback: self.unmitigated_fvg_lookback,
2757            smoothing_length: self.smoothing_length,
2758            reset_on_cross: self.reset_on_cross,
2759        };
2760        let input = FvgTrailingStopInput::from_slices(high, low, close, params);
2761        fvg_trailing_stop_with_kernel(&input, self.kernel)
2762    }
2763
2764    pub fn into_stream(self) -> Result<FvgTrailingStopStream, FvgTrailingStopError> {
2765        let params = FvgTrailingStopParams {
2766            unmitigated_fvg_lookback: self.unmitigated_fvg_lookback,
2767            smoothing_length: self.smoothing_length,
2768            reset_on_cross: self.reset_on_cross,
2769        };
2770        FvgTrailingStopStream::try_new(params)
2771    }
2772}
2773
2774#[cfg(test)]
2775mod tests {
2776    use super::*;
2777    use crate::utilities::data_loader::read_candles_from_csv;
2778
2779    macro_rules! skip_if_unsupported {
2780        ($kernel:expr, $test_name:expr) => {
2781            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
2782            if matches!(
2783                $kernel,
2784                Kernel::Avx2 | Kernel::Avx512 | Kernel::Avx2Batch | Kernel::Avx512Batch
2785            ) {
2786                eprintln!("Skipping {} - AVX not available", $test_name);
2787                return Ok(());
2788            }
2789        };
2790    }
2791
2792    fn check_fvg_ts_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2793        skip_if_unsupported!(kernel, test_name);
2794        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2795        let candles = read_candles_from_csv(file_path)?;
2796
2797        let params = FvgTrailingStopParams {
2798            unmitigated_fvg_lookback: Some(5),
2799            smoothing_length: Some(9),
2800            reset_on_cross: Some(false),
2801        };
2802        let input = FvgTrailingStopInput::from_candles(&candles, params);
2803        let result = fvg_trailing_stop_with_kernel(&input, kernel)?;
2804
2805        let expected_lower = 55643.00;
2806        let expected_lower_ts = 60223.33333333;
2807        let tolerance = 0.01;
2808
2809        let n = result.lower.len();
2810        if n >= 5 {
2811            for i in (n - 5)..n {
2812                if !result.lower[i].is_nan() {
2813                    let diff = (result.lower[i] - expected_lower).abs();
2814                    assert!(
2815                        diff < tolerance,
2816                        "[{}] Lower value mismatch at {}: expected {}, got {}, diff {}",
2817                        test_name,
2818                        i,
2819                        expected_lower,
2820                        result.lower[i],
2821                        diff
2822                    );
2823                }
2824                if !result.lower_ts[i].is_nan() {
2825                    let diff = (result.lower_ts[i] - expected_lower_ts).abs();
2826                    assert!(
2827                        diff < tolerance,
2828                        "[{}] Lower TS value mismatch at {}: expected {}, got {}, diff {}",
2829                        test_name,
2830                        i,
2831                        expected_lower_ts,
2832                        result.lower_ts[i],
2833                        diff
2834                    );
2835                }
2836            }
2837        }
2838        Ok(())
2839    }
2840
2841    fn check_fvg_ts_default_candles(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2842        skip_if_unsupported!(kernel, test_name);
2843        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2844        let candles = read_candles_from_csv(file_path)?;
2845
2846        let input = FvgTrailingStopInput::with_default_candles(&candles);
2847        let output = fvg_trailing_stop_with_kernel(&input, kernel)?;
2848        assert_eq!(output.upper.len(), candles.close.len());
2849        assert_eq!(output.lower.len(), candles.close.len());
2850        assert_eq!(output.upper_ts.len(), candles.close.len());
2851        assert_eq!(output.lower_ts.len(), candles.close.len());
2852
2853        Ok(())
2854    }
2855
2856    fn check_fvg_ts_empty_input(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2857        skip_if_unsupported!(kernel, test_name);
2858        let empty: [f64; 0] = [];
2859        let params = FvgTrailingStopParams::default();
2860        let input = FvgTrailingStopInput::from_slices(&empty, &empty, &empty, params);
2861        let res = fvg_trailing_stop_with_kernel(&input, kernel);
2862        assert!(
2863            matches!(res, Err(FvgTrailingStopError::EmptyInputData)),
2864            "[{}] Should fail with empty input",
2865            test_name
2866        );
2867        Ok(())
2868    }
2869
2870    fn check_fvg_ts_all_nan(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2871        skip_if_unsupported!(kernel, test_name);
2872        let nan_data = vec![f64::NAN; 100];
2873        let params = FvgTrailingStopParams::default();
2874        let input = FvgTrailingStopInput::from_slices(&nan_data, &nan_data, &nan_data, params);
2875        let res = fvg_trailing_stop_with_kernel(&input, kernel);
2876        assert!(
2877            matches!(res, Err(FvgTrailingStopError::AllValuesNaN)),
2878            "[{}] Should fail with all NaN",
2879            test_name
2880        );
2881        Ok(())
2882    }
2883
2884    fn check_fvg_ts_partial_nan(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2885        skip_if_unsupported!(kernel, test_name);
2886        let mut high = vec![100.0; 50];
2887        let mut low = vec![95.0; 50];
2888        let mut close = vec![97.0; 50];
2889
2890        for i in 10..20 {
2891            high[i] = f64::NAN;
2892            low[i] = f64::NAN;
2893            close[i] = f64::NAN;
2894        }
2895
2896        let params = FvgTrailingStopParams::default();
2897        let input = FvgTrailingStopInput::from_slices(&high, &low, &close, params);
2898        let result = fvg_trailing_stop_with_kernel(&input, kernel)?;
2899
2900        assert_eq!(result.upper.len(), 50);
2901        assert_eq!(result.lower.len(), 50);
2902        Ok(())
2903    }
2904
2905    fn check_fvg_ts_streaming(test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2906        let params = FvgTrailingStopParams::default();
2907        let mut stream = FvgTrailingStopStream::try_new(params)?;
2908
2909        let test_data = vec![
2910            (100.0, 95.0, 97.0),
2911            (101.0, 96.0, 98.0),
2912            (102.0, 97.0, 99.0),
2913            (103.0, 98.0, 100.0),
2914            (104.0, 99.0, 101.0),
2915            (105.0, 100.0, 102.0),
2916            (106.0, 101.0, 103.0),
2917            (107.0, 102.0, 104.0),
2918            (108.0, 103.0, 105.0),
2919            (109.0, 104.0, 106.0),
2920            (110.0, 105.0, 107.0),
2921            (111.0, 106.0, 108.0),
2922        ];
2923
2924        for (h, l, c) in test_data {
2925            stream.update(h, l, c);
2926        }
2927
2928        Ok(())
2929    }
2930
2931    fn check_fvg_ts_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2932        skip_if_unsupported!(kernel, test_name);
2933        #[cfg(debug_assertions)]
2934        {
2935            let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2936            let candles = read_candles_from_csv(file_path)?;
2937
2938            let params = FvgTrailingStopParams::default();
2939            let input = FvgTrailingStopInput::from_candles(&candles, params);
2940            let out = fvg_trailing_stop_with_kernel(&input, kernel)?;
2941
2942            for (name, row) in [
2943                ("upper", &out.upper),
2944                ("lower", &out.lower),
2945                ("upper_ts", &out.upper_ts),
2946                ("lower_ts", &out.lower_ts),
2947            ] {
2948                for (i, &v) in row.iter().enumerate() {
2949                    if v.is_nan() {
2950                        continue;
2951                    }
2952                    let b = v.to_bits();
2953                    assert_ne!(
2954                        b, 0x1111_1111_1111_1111,
2955                        "[{}] alloc poison in {} at {}",
2956                        test_name, name, i
2957                    );
2958                    assert_ne!(
2959                        b, 0x2222_2222_2222_2222,
2960                        "[{}] matrix poison in {} at {}",
2961                        test_name, name, i
2962                    );
2963                    assert_ne!(
2964                        b, 0x3333_3333_3333_3333,
2965                        "[{}] uninit poison in {} at {}",
2966                        test_name, name, i
2967                    );
2968                }
2969            }
2970        }
2971        Ok(())
2972    }
2973
2974    fn check_fvg_ts_batch_default(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2975        skip_if_unsupported!(kernel, test_name);
2976        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2977        let candles = read_candles_from_csv(file_path)?;
2978
2979        let output = FvgTsBatchBuilder::new()
2980            .kernel(kernel)
2981            .apply_candles(&candles)?;
2982
2983        assert_eq!(output.combos.len(), 1);
2984        assert_eq!(output.rows, 1);
2985        assert_eq!(output.cols, candles.close.len());
2986
2987        Ok(())
2988    }
2989
2990    fn check_fvg_ts_batch_sweep(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2991        skip_if_unsupported!(kernel, test_name);
2992        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2993        let candles = read_candles_from_csv(file_path)?;
2994
2995        let output = FvgTsBatchBuilder::new()
2996            .kernel(kernel)
2997            .lookback_range(3, 7, 2)
2998            .smoothing_range(5, 10, 5)
2999            .reset_toggle(true, true)
3000            .apply_candles(&candles)?;
3001
3002        assert_eq!(output.combos.len(), 12);
3003        assert_eq!(output.rows, 12);
3004        assert_eq!(output.cols, candles.close.len());
3005
3006        Ok(())
3007    }
3008
3009    fn check_fvg_ts_builder_apply_slice(
3010        test_name: &str,
3011        kernel: Kernel,
3012    ) -> Result<(), Box<dyn Error>> {
3013        skip_if_unsupported!(kernel, test_name);
3014        let high = vec![100.0, 102.0, 103.0, 105.0, 104.0];
3015        let low = vec![98.0, 99.0, 101.0, 102.0, 103.0];
3016        let close = vec![99.0, 101.0, 102.0, 104.0, 103.5];
3017
3018        let result = FvgTrailingStopBuilder::new()
3019            .lookback(3)
3020            .smoothing(5)
3021            .kernel(kernel)
3022            .apply_slice(&high, &low, &close)?;
3023
3024        assert_eq!(result.upper.len(), 5);
3025        assert_eq!(result.lower.len(), 5);
3026
3027        Ok(())
3028    }
3029
3030    fn check_fvg_ts_into_slices_warm_nan(
3031        test_name: &str,
3032        kernel: Kernel,
3033    ) -> Result<(), Box<dyn Error>> {
3034        skip_if_unsupported!(kernel, test_name);
3035        let h = vec![
3036            100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, 109.0,
3037        ];
3038        let l = vec![
3039            99.0, 99.5, 100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0,
3040        ];
3041        let c = vec![
3042            99.5, 100.5, 101.5, 102.5, 103.5, 104.5, 105.5, 106.5, 107.5, 108.5,
3043        ];
3044        let params = FvgTrailingStopParams::default();
3045        let input = FvgTrailingStopInput::from_slices(&h, &l, &c, params);
3046
3047        let mut u = vec![0.0; h.len()];
3048        let mut d = u.clone();
3049        let mut uts = u.clone();
3050        let mut lts = u.clone();
3051
3052        let smoothing_len = input.get_smoothing();
3053
3054        fvg_trailing_stop_into_slices(&mut u, &mut d, &mut uts, &mut lts, &input, kernel)?;
3055
3056        let expected_warm = 2 + smoothing_len - 1;
3057        for v in [&u, &d, &uts, &lts] {
3058            for i in 0..expected_warm.min(h.len()) {
3059                assert!(
3060                    v[i].is_nan(),
3061                    "[{}] Expected NaN at index {} but got {}",
3062                    test_name,
3063                    i,
3064                    v[i]
3065                );
3066            }
3067        }
3068        Ok(())
3069    }
3070
3071    fn check_fvg_ts_invalid_smoothing(
3072        test_name: &str,
3073        kernel: Kernel,
3074    ) -> Result<(), Box<dyn Error>> {
3075        skip_if_unsupported!(kernel, test_name);
3076        let h = vec![1.0; 20];
3077        let l = vec![0.0; 20];
3078        let c = vec![0.5; 20];
3079        let params = FvgTrailingStopParams {
3080            unmitigated_fvg_lookback: Some(5),
3081            smoothing_length: Some(0),
3082            reset_on_cross: Some(false),
3083        };
3084        let input = FvgTrailingStopInput::from_slices(&h, &l, &c, params);
3085        let res = fvg_trailing_stop_with_kernel(&input, kernel);
3086        assert!(
3087            matches!(
3088                res,
3089                Err(FvgTrailingStopError::InvalidSmoothingLength { .. })
3090            ),
3091            "[{}] expected InvalidSmoothingLength",
3092            test_name
3093        );
3094        Ok(())
3095    }
3096
3097    fn check_fvg_ts_invalid_lookback(
3098        test_name: &str,
3099        kernel: Kernel,
3100    ) -> Result<(), Box<dyn Error>> {
3101        skip_if_unsupported!(kernel, test_name);
3102        let h = vec![1.0; 20];
3103        let l = vec![0.0; 20];
3104        let c = vec![0.5; 20];
3105        let params = FvgTrailingStopParams {
3106            unmitigated_fvg_lookback: Some(0),
3107            smoothing_length: Some(9),
3108            reset_on_cross: Some(false),
3109        };
3110        let input = FvgTrailingStopInput::from_slices(&h, &l, &c, params);
3111        let res = fvg_trailing_stop_with_kernel(&input, kernel);
3112        assert!(
3113            matches!(res, Err(FvgTrailingStopError::InvalidLookback { .. })),
3114            "[{}] expected InvalidLookback",
3115            test_name
3116        );
3117        Ok(())
3118    }
3119
3120    fn check_fvg_ts_batch_values_for(
3121        test_name: &str,
3122        kernel: Kernel,
3123    ) -> Result<(), Box<dyn Error>> {
3124        skip_if_unsupported!(kernel, test_name);
3125        let h = vec![100.0; 64];
3126        let l = vec![90.0; 64];
3127        let c = vec![95.0; 64];
3128        let sweep = FvgTsBatchRange::default();
3129        let out = fvg_trailing_stop_batch_with_kernel(&h, &l, &c, &sweep, kernel)?;
3130        let p = FvgTrailingStopParams::default();
3131        let (u, d, uts, lts) = out.values_for(&p).expect("missing row");
3132        assert_eq!(u.len(), out.cols);
3133        assert_eq!(d.len(), out.cols);
3134        assert_eq!(uts.len(), out.cols);
3135        assert_eq!(lts.len(), out.cols);
3136        Ok(())
3137    }
3138
3139    macro_rules! generate_all_fvg_ts_tests {
3140        ($($test_fn:ident),*) => {
3141            paste::paste! {
3142                $(
3143                    #[test]
3144                    fn [<$test_fn _scalar>]() {
3145                        let _ = $test_fn(stringify!([<$test_fn _scalar>]), Kernel::Scalar);
3146                    }
3147                )*
3148                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3149                $(
3150                    #[test]
3151                    fn [<$test_fn _avx2>]() {
3152                        let _ = $test_fn(stringify!([<$test_fn _avx2>]), Kernel::Avx2);
3153                    }
3154                    #[test]
3155                    fn [<$test_fn _avx512>]() {
3156                        let _ = $test_fn(stringify!([<$test_fn _avx512>]), Kernel::Avx512);
3157                    }
3158                )*
3159                #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
3160                $(
3161                    #[test]
3162                    fn [<$test_fn _simd128>]() {
3163                        let _ = $test_fn(stringify!([<$test_fn _simd128>]), Kernel::Scalar);
3164                    }
3165                )*
3166            }
3167        }
3168    }
3169
3170    generate_all_fvg_ts_tests!(
3171        check_fvg_ts_accuracy,
3172        check_fvg_ts_default_candles,
3173        check_fvg_ts_empty_input,
3174        check_fvg_ts_all_nan,
3175        check_fvg_ts_partial_nan,
3176        check_fvg_ts_streaming,
3177        check_fvg_ts_no_poison,
3178        check_fvg_ts_batch_default,
3179        check_fvg_ts_batch_sweep,
3180        check_fvg_ts_builder_apply_slice,
3181        check_fvg_ts_into_slices_warm_nan,
3182        check_fvg_ts_invalid_smoothing,
3183        check_fvg_ts_invalid_lookback,
3184        check_fvg_ts_batch_values_for
3185    );
3186
3187    #[test]
3188    fn test_fvg_trailing_stop_into_matches_api() -> Result<(), Box<dyn Error>> {
3189        let n = 128usize;
3190        let mut high = Vec::with_capacity(n);
3191        let mut low = Vec::with_capacity(n);
3192        let mut close = Vec::with_capacity(n);
3193        for i in 0..n {
3194            let base = 100.0 + i as f64 * 0.5;
3195            high.push(base + 1.0 + (i % 3) as f64 * 0.1);
3196            low.push(base - 1.0 - (i % 2) as f64 * 0.1);
3197            close.push(base + ((i % 5) as f64 - 2.0) * 0.05);
3198        }
3199
3200        let params = FvgTrailingStopParams::default();
3201        let input = FvgTrailingStopInput::from_slices(&high, &low, &close, params);
3202
3203        let base = fvg_trailing_stop(&input)?;
3204
3205        let mut u = vec![0.0; n];
3206        let mut d = vec![0.0; n];
3207        let mut uts = vec![0.0; n];
3208        let mut lts = vec![0.0; n];
3209        fvg_trailing_stop_into(&input, &mut u, &mut d, &mut uts, &mut lts)?;
3210
3211        fn eq_or_both_nan(a: f64, b: f64) -> bool {
3212            (a.is_nan() && b.is_nan()) || (a == b) || ((a - b).abs() <= 1e-12)
3213        }
3214
3215        assert_eq!(u.len(), base.upper.len());
3216        assert_eq!(d.len(), base.lower.len());
3217        assert_eq!(uts.len(), base.upper_ts.len());
3218        assert_eq!(lts.len(), base.lower_ts.len());
3219
3220        for i in 0..n {
3221            assert!(
3222                eq_or_both_nan(u[i], base.upper[i]),
3223                "upper mismatch at {}: {} vs {}",
3224                i,
3225                u[i],
3226                base.upper[i]
3227            );
3228            assert!(
3229                eq_or_both_nan(d[i], base.lower[i]),
3230                "lower mismatch at {}: {} vs {}",
3231                i,
3232                d[i],
3233                base.lower[i]
3234            );
3235            assert!(
3236                eq_or_both_nan(uts[i], base.upper_ts[i]),
3237                "upper_ts mismatch at {}: {} vs {}",
3238                i,
3239                uts[i],
3240                base.upper_ts[i]
3241            );
3242            assert!(
3243                eq_or_both_nan(lts[i], base.lower_ts[i]),
3244                "lower_ts mismatch at {}: {} vs {}",
3245                i,
3246                lts[i],
3247                base.lower_ts[i]
3248            );
3249        }
3250
3251        Ok(())
3252    }
3253}