Skip to main content

vector_ta/indicators/
trend_flow_trail.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, 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 serde_wasm_bindgen;
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use wasm_bindgen::prelude::*;
16
17use crate::indicators::moving_averages::ema::{EmaParams, EmaStream};
18use crate::indicators::moving_averages::hma::{HmaParams, HmaStream};
19use crate::utilities::data_loader::{source_type, Candles};
20use crate::utilities::enums::Kernel;
21use crate::utilities::helpers::{
22    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
23    make_uninit_matrix,
24};
25#[cfg(feature = "python")]
26use crate::utilities::kernel_validation::validate_kernel;
27use std::mem::{ManuallyDrop, MaybeUninit};
28use thiserror::Error;
29
30const DEFAULT_ALPHA_LENGTH: usize = 33;
31const DEFAULT_ALPHA_MULTIPLIER: f64 = 3.3;
32const DEFAULT_MFI_LENGTH: usize = 14;
33const MFI_HMA_LENGTH: usize = 7;
34const MFI_HMA_SQRT: usize = 2;
35
36type TrendFlowTrailRow = (
37    f64,
38    f64,
39    f64,
40    f64,
41    f64,
42    f64,
43    f64,
44    f64,
45    f64,
46    f64,
47    f64,
48    f64,
49    f64,
50    f64,
51    f64,
52    f64,
53    f64,
54);
55
56#[derive(Debug, Clone)]
57pub enum TrendFlowTrailData<'a> {
58    Candles {
59        candles: &'a Candles,
60    },
61    Slices {
62        open: &'a [f64],
63        high: &'a [f64],
64        low: &'a [f64],
65        close: &'a [f64],
66        volume: &'a [f64],
67    },
68}
69
70#[derive(Debug, Clone)]
71#[cfg_attr(
72    all(target_arch = "wasm32", feature = "wasm"),
73    derive(Serialize, Deserialize)
74)]
75pub struct TrendFlowTrailOutput {
76    pub alpha_trail: Vec<f64>,
77    pub alpha_trail_bullish: Vec<f64>,
78    pub alpha_trail_bearish: Vec<f64>,
79    pub alpha_dir: Vec<f64>,
80    pub mfi: Vec<f64>,
81    pub tp_upper: Vec<f64>,
82    pub tp_lower: Vec<f64>,
83    pub alpha_trail_bullish_switch: Vec<f64>,
84    pub alpha_trail_bearish_switch: Vec<f64>,
85    pub mfi_overbought: Vec<f64>,
86    pub mfi_oversold: Vec<f64>,
87    pub mfi_cross_up_mid: Vec<f64>,
88    pub mfi_cross_down_mid: Vec<f64>,
89    pub price_cross_alpha_trail_up: Vec<f64>,
90    pub price_cross_alpha_trail_down: Vec<f64>,
91    pub mfi_above_90: Vec<f64>,
92    pub mfi_below_10: Vec<f64>,
93}
94
95#[derive(Debug, Clone, PartialEq)]
96#[cfg_attr(
97    all(target_arch = "wasm32", feature = "wasm"),
98    derive(Serialize, Deserialize)
99)]
100pub struct TrendFlowTrailParams {
101    pub alpha_length: Option<usize>,
102    pub alpha_multiplier: Option<f64>,
103    pub mfi_length: Option<usize>,
104}
105
106impl Default for TrendFlowTrailParams {
107    fn default() -> Self {
108        Self {
109            alpha_length: Some(DEFAULT_ALPHA_LENGTH),
110            alpha_multiplier: Some(DEFAULT_ALPHA_MULTIPLIER),
111            mfi_length: Some(DEFAULT_MFI_LENGTH),
112        }
113    }
114}
115
116#[derive(Debug, Clone)]
117pub struct TrendFlowTrailInput<'a> {
118    pub data: TrendFlowTrailData<'a>,
119    pub params: TrendFlowTrailParams,
120}
121
122impl<'a> TrendFlowTrailInput<'a> {
123    #[inline(always)]
124    pub fn from_candles(candles: &'a Candles, params: TrendFlowTrailParams) -> Self {
125        Self {
126            data: TrendFlowTrailData::Candles { candles },
127            params,
128        }
129    }
130
131    #[inline(always)]
132    pub fn from_slices(
133        open: &'a [f64],
134        high: &'a [f64],
135        low: &'a [f64],
136        close: &'a [f64],
137        volume: &'a [f64],
138        params: TrendFlowTrailParams,
139    ) -> Self {
140        Self {
141            data: TrendFlowTrailData::Slices {
142                open,
143                high,
144                low,
145                close,
146                volume,
147            },
148            params,
149        }
150    }
151
152    #[inline(always)]
153    pub fn with_default_candles(candles: &'a Candles) -> Self {
154        Self::from_candles(candles, TrendFlowTrailParams::default())
155    }
156
157    #[inline(always)]
158    pub fn get_alpha_length(&self) -> usize {
159        self.params.alpha_length.unwrap_or(DEFAULT_ALPHA_LENGTH)
160    }
161
162    #[inline(always)]
163    pub fn get_alpha_multiplier(&self) -> f64 {
164        self.params
165            .alpha_multiplier
166            .unwrap_or(DEFAULT_ALPHA_MULTIPLIER)
167    }
168
169    #[inline(always)]
170    pub fn get_mfi_length(&self) -> usize {
171        self.params.mfi_length.unwrap_or(DEFAULT_MFI_LENGTH)
172    }
173
174    #[inline(always)]
175    fn as_ohlcv(&self) -> (&'a [f64], &'a [f64], &'a [f64], &'a [f64], &'a [f64]) {
176        match &self.data {
177            TrendFlowTrailData::Candles { candles } => (
178                source_type(candles, "open"),
179                source_type(candles, "high"),
180                source_type(candles, "low"),
181                source_type(candles, "close"),
182                source_type(candles, "volume"),
183            ),
184            TrendFlowTrailData::Slices {
185                open,
186                high,
187                low,
188                close,
189                volume,
190            } => (*open, *high, *low, *close, *volume),
191        }
192    }
193}
194
195impl<'a> AsRef<[f64]> for TrendFlowTrailInput<'a> {
196    #[inline(always)]
197    fn as_ref(&self) -> &[f64] {
198        self.as_ohlcv().3
199    }
200}
201
202#[derive(Clone, Copy, Debug)]
203pub struct TrendFlowTrailBuilder {
204    alpha_length: Option<usize>,
205    alpha_multiplier: Option<f64>,
206    mfi_length: Option<usize>,
207    kernel: Kernel,
208}
209
210impl Default for TrendFlowTrailBuilder {
211    fn default() -> Self {
212        Self {
213            alpha_length: None,
214            alpha_multiplier: None,
215            mfi_length: None,
216            kernel: Kernel::Auto,
217        }
218    }
219}
220
221impl TrendFlowTrailBuilder {
222    #[inline(always)]
223    pub fn new() -> Self {
224        Self::default()
225    }
226
227    #[inline(always)]
228    pub fn alpha_length(mut self, value: usize) -> Self {
229        self.alpha_length = Some(value);
230        self
231    }
232
233    #[inline(always)]
234    pub fn alpha_multiplier(mut self, value: f64) -> Self {
235        self.alpha_multiplier = Some(value);
236        self
237    }
238
239    #[inline(always)]
240    pub fn mfi_length(mut self, value: usize) -> Self {
241        self.mfi_length = Some(value);
242        self
243    }
244
245    #[inline(always)]
246    pub fn kernel(mut self, kernel: Kernel) -> Self {
247        self.kernel = kernel;
248        self
249    }
250
251    #[inline(always)]
252    fn params(self) -> TrendFlowTrailParams {
253        TrendFlowTrailParams {
254            alpha_length: self.alpha_length,
255            alpha_multiplier: self.alpha_multiplier,
256            mfi_length: self.mfi_length,
257        }
258    }
259
260    #[inline(always)]
261    pub fn apply(self, candles: &Candles) -> Result<TrendFlowTrailOutput, TrendFlowTrailError> {
262        let kernel = self.kernel;
263        trend_flow_trail_with_kernel(
264            &TrendFlowTrailInput::from_candles(candles, self.params()),
265            kernel,
266        )
267    }
268
269    #[inline(always)]
270    pub fn apply_slices(
271        self,
272        open: &[f64],
273        high: &[f64],
274        low: &[f64],
275        close: &[f64],
276        volume: &[f64],
277    ) -> Result<TrendFlowTrailOutput, TrendFlowTrailError> {
278        let kernel = self.kernel;
279        trend_flow_trail_with_kernel(
280            &TrendFlowTrailInput::from_slices(open, high, low, close, volume, self.params()),
281            kernel,
282        )
283    }
284
285    #[inline(always)]
286    pub fn into_stream(self) -> Result<TrendFlowTrailStream, TrendFlowTrailError> {
287        TrendFlowTrailStream::try_new(self.params())
288    }
289}
290
291#[derive(Debug, Error)]
292pub enum TrendFlowTrailError {
293    #[error("trend_flow_trail: input data slice is empty.")]
294    EmptyInputData,
295    #[error("trend_flow_trail: all values are NaN.")]
296    AllValuesNaN,
297    #[error("trend_flow_trail: inconsistent slice lengths: open={open_len}, high={high_len}, low={low_len}, close={close_len}, volume={volume_len}")]
298    InconsistentSliceLengths {
299        open_len: usize,
300        high_len: usize,
301        low_len: usize,
302        close_len: usize,
303        volume_len: usize,
304    },
305    #[error("trend_flow_trail: invalid alpha_length: {alpha_length}")]
306    InvalidAlphaLength { alpha_length: usize },
307    #[error("trend_flow_trail: invalid alpha_multiplier: {alpha_multiplier}")]
308    InvalidAlphaMultiplier { alpha_multiplier: f64 },
309    #[error("trend_flow_trail: invalid mfi_length: {mfi_length}")]
310    InvalidMfiLength { mfi_length: usize },
311    #[error("trend_flow_trail: not enough valid data: needed = {needed}, valid = {valid}")]
312    NotEnoughValidData { needed: usize, valid: usize },
313    #[error("trend_flow_trail: output length mismatch: expected = {expected}, got = {got}")]
314    OutputLengthMismatch { expected: usize, got: usize },
315    #[error(
316        "trend_flow_trail: invalid range for {axis}: start = {start}, end = {end}, step = {step}"
317    )]
318    InvalidRange {
319        axis: &'static str,
320        start: String,
321        end: String,
322        step: String,
323    },
324    #[error("trend_flow_trail: invalid kernel for batch: {0:?}")]
325    InvalidKernelForBatch(Kernel),
326}
327
328#[derive(Clone, Copy, Debug)]
329struct PreparedTrendFlowTrail<'a> {
330    open: &'a [f64],
331    high: &'a [f64],
332    low: &'a [f64],
333    close: &'a [f64],
334    volume: &'a [f64],
335    alpha_length: usize,
336    alpha_multiplier: f64,
337    mfi_length: usize,
338    warmup: usize,
339}
340
341#[derive(Clone, Debug)]
342struct HmaLikeStream {
343    period: usize,
344    inner: Option<HmaStream>,
345}
346
347impl HmaLikeStream {
348    fn try_new_alpha(period: usize) -> Result<Self, TrendFlowTrailError> {
349        if period == 0 {
350            return Err(TrendFlowTrailError::InvalidAlphaLength {
351                alpha_length: period,
352            });
353        }
354        Ok(Self {
355            period,
356            inner: if period == 1 {
357                None
358            } else {
359                Some(
360                    HmaStream::try_new(HmaParams {
361                        period: Some(period),
362                    })
363                    .map_err(|_| TrendFlowTrailError::InvalidAlphaLength {
364                        alpha_length: period,
365                    })?,
366                )
367            },
368        })
369    }
370
371    fn try_new_fixed(period: usize) -> Result<Self, TrendFlowTrailError> {
372        Ok(Self {
373            period,
374            inner: Some(
375                HmaStream::try_new(HmaParams {
376                    period: Some(period),
377                })
378                .map_err(|_| TrendFlowTrailError::InvalidMfiLength { mfi_length: period })?,
379            ),
380        })
381    }
382
383    #[inline(always)]
384    fn update(&mut self, value: f64) -> Option<f64> {
385        if self.period == 1 {
386            Some(value)
387        } else {
388            self.inner.as_mut().and_then(|inner| inner.update(value))
389        }
390    }
391}
392
393#[derive(Clone, Debug)]
394struct MoneyFlowRawState {
395    len: usize,
396    pos: Vec<f64>,
397    neg: Vec<f64>,
398    head: usize,
399    count: usize,
400    pos_sum: f64,
401    neg_sum: f64,
402    prev_src: Option<f64>,
403}
404
405impl MoneyFlowRawState {
406    fn new(len: usize) -> Result<Self, TrendFlowTrailError> {
407        if len == 0 {
408            return Err(TrendFlowTrailError::InvalidMfiLength { mfi_length: len });
409        }
410        Ok(Self {
411            len,
412            pos: vec![0.0; len],
413            neg: vec![0.0; len],
414            head: 0,
415            count: 0,
416            pos_sum: 0.0,
417            neg_sum: 0.0,
418            prev_src: None,
419        })
420    }
421
422    #[inline(always)]
423    fn update(&mut self, src: f64, volume: f64) -> Option<f64> {
424        let delta = self.prev_src.map(|prev| src - prev).unwrap_or(0.0);
425        self.prev_src = Some(src);
426        let pos_flow = if delta > 0.0 { volume * src } else { 0.0 };
427        let neg_flow = if delta < 0.0 { volume * src } else { 0.0 };
428        if self.count == self.len {
429            self.pos_sum -= self.pos[self.head];
430            self.neg_sum -= self.neg[self.head];
431        } else {
432            self.count += 1;
433        }
434        self.pos[self.head] = pos_flow;
435        self.neg[self.head] = neg_flow;
436        self.pos_sum += pos_flow;
437        self.neg_sum += neg_flow;
438        self.head = (self.head + 1) % self.len;
439        if self.count < self.len {
440            return None;
441        }
442        let ratio = self.pos_sum / self.neg_sum;
443        Some(100.0 - (100.0 / (1.0 + ratio)))
444    }
445}
446
447#[derive(Clone, Debug)]
448struct TrendFlowTrailState {
449    alpha_length: usize,
450    alpha_multiplier: f64,
451    mfi_length: usize,
452    basis_stream: HmaLikeStream,
453    spread_stream: EmaStream,
454    money_flow_raw: MoneyFlowRawState,
455    mfi_stream: HmaLikeStream,
456    prev_upper: Option<f64>,
457    prev_lower: Option<f64>,
458    prev_trail: Option<f64>,
459    prev_alpha_dir: Option<f64>,
460    prev_close: Option<f64>,
461    prev_mfi: Option<f64>,
462}
463
464impl TrendFlowTrailState {
465    fn try_new(
466        alpha_length: usize,
467        alpha_multiplier: f64,
468        mfi_length: usize,
469    ) -> Result<Self, TrendFlowTrailError> {
470        validate_params(alpha_length, alpha_multiplier, mfi_length, usize::MAX)?;
471        Ok(Self {
472            alpha_length,
473            alpha_multiplier,
474            mfi_length,
475            basis_stream: HmaLikeStream::try_new_alpha(alpha_length)?,
476            spread_stream: EmaStream::try_new(EmaParams {
477                period: Some(alpha_length.max(1)),
478            })
479            .map_err(|_| TrendFlowTrailError::InvalidAlphaLength { alpha_length })?,
480            money_flow_raw: MoneyFlowRawState::new(mfi_length)?,
481            mfi_stream: HmaLikeStream::try_new_fixed(MFI_HMA_LENGTH)?,
482            prev_upper: None,
483            prev_lower: None,
484            prev_trail: None,
485            prev_alpha_dir: None,
486            prev_close: None,
487            prev_mfi: None,
488        })
489    }
490
491    #[inline(always)]
492    fn reset(&mut self) {
493        *self = Self::try_new(self.alpha_length, self.alpha_multiplier, self.mfi_length)
494            .expect("trend_flow_trail params already validated");
495    }
496
497    fn update(
498        &mut self,
499        open: f64,
500        high: f64,
501        low: f64,
502        close: f64,
503        volume: f64,
504    ) -> Option<TrendFlowTrailRow> {
505        if !(open.is_finite()
506            && high.is_finite()
507            && low.is_finite()
508            && close.is_finite()
509            && volume.is_finite())
510        {
511            self.reset();
512            return None;
513        }
514
515        let basis = self.basis_stream.update(close);
516        let spread = self
517            .spread_stream
518            .update((high - low).abs())
519            .map(|x| x * self.alpha_multiplier);
520        let hlc3 = (high + low + close) / 3.0;
521        let mfi = self
522            .money_flow_raw
523            .update(hlc3, volume)
524            .and_then(|raw| self.mfi_stream.update(raw));
525
526        let prev_close = self.prev_close;
527        self.prev_close = Some(close);
528        let prev_alpha_dir = self.prev_alpha_dir;
529        let prev_trail = self.prev_trail;
530        let prev_mfi = self.prev_mfi;
531
532        let (basis, spread, mfi) = match (basis, spread, mfi) {
533            (Some(basis), Some(spread), Some(mfi)) if mfi.is_finite() => (basis, spread, mfi),
534            _ => return None,
535        };
536
537        let mut upper = basis + spread;
538        let mut lower = basis - spread;
539        let prev_upper = self.prev_upper.unwrap_or(0.0);
540        let prev_lower = self.prev_lower.unwrap_or(0.0);
541        let prev_close_value = prev_close.unwrap_or(0.0);
542
543        lower = if lower > prev_lower || prev_close_value < prev_lower {
544            lower
545        } else {
546            prev_lower
547        };
548        upper = if upper < prev_upper || prev_close_value > prev_upper {
549            upper
550        } else {
551            prev_upper
552        };
553
554        let alpha_dir = if prev_trail.is_none() {
555            1.0
556        } else if prev_trail == Some(prev_upper) {
557            if close > upper {
558                -1.0
559            } else {
560                1.0
561            }
562        } else if close < lower {
563            1.0
564        } else {
565            -1.0
566        };
567
568        let alpha_trail = if alpha_dir < 0.0 { lower } else { upper };
569        self.prev_upper = Some(upper);
570        self.prev_lower = Some(lower);
571        self.prev_trail = Some(alpha_trail);
572        self.prev_alpha_dir = Some(alpha_dir);
573        self.prev_mfi = Some(mfi);
574
575        Some((
576            alpha_trail,
577            if alpha_dir < 0.0 {
578                alpha_trail
579            } else {
580                f64::NAN
581            },
582            if alpha_dir > 0.0 {
583                alpha_trail
584            } else {
585                f64::NAN
586            },
587            alpha_dir,
588            mfi,
589            if crossover(prev_mfi, mfi, 80.0) && alpha_dir == -1.0 {
590                1.0
591            } else {
592                f64::NAN
593            },
594            if crossunder(prev_mfi, mfi, 20.0) && alpha_dir == 1.0 {
595                1.0
596            } else {
597                f64::NAN
598            },
599            if crossover(prev_alpha_dir, alpha_dir, 0.0) {
600                1.0
601            } else {
602                f64::NAN
603            },
604            if crossunder(prev_alpha_dir, alpha_dir, 0.0) {
605                1.0
606            } else {
607                f64::NAN
608            },
609            if crossover(prev_mfi, mfi, 80.0) {
610                1.0
611            } else {
612                f64::NAN
613            },
614            if crossunder(prev_mfi, mfi, 20.0) {
615                1.0
616            } else {
617                f64::NAN
618            },
619            if crossover(prev_mfi, mfi, 50.0) {
620                1.0
621            } else {
622                f64::NAN
623            },
624            if crossunder(prev_mfi, mfi, 50.0) {
625                1.0
626            } else {
627                f64::NAN
628            },
629            if cross_pair(prev_close, close, prev_trail, alpha_trail) {
630                1.0
631            } else {
632                f64::NAN
633            },
634            if crossunder_pair(prev_close, close, prev_trail, alpha_trail) {
635                1.0
636            } else {
637                f64::NAN
638            },
639            if crossover(prev_mfi, mfi, 90.0) {
640                1.0
641            } else {
642                f64::NAN
643            },
644            if crossunder(prev_mfi, mfi, 10.0) {
645                1.0
646            } else {
647                f64::NAN
648            },
649        ))
650    }
651}
652
653#[derive(Clone, Debug)]
654pub struct TrendFlowTrailStream {
655    state: TrendFlowTrailState,
656}
657
658impl TrendFlowTrailStream {
659    #[inline(always)]
660    pub fn try_new(params: TrendFlowTrailParams) -> Result<Self, TrendFlowTrailError> {
661        Ok(Self {
662            state: TrendFlowTrailState::try_new(
663                params.alpha_length.unwrap_or(DEFAULT_ALPHA_LENGTH),
664                params.alpha_multiplier.unwrap_or(DEFAULT_ALPHA_MULTIPLIER),
665                params.mfi_length.unwrap_or(DEFAULT_MFI_LENGTH),
666            )?,
667        })
668    }
669
670    #[inline(always)]
671    pub fn update(
672        &mut self,
673        open: f64,
674        high: f64,
675        low: f64,
676        close: f64,
677        volume: f64,
678    ) -> Option<TrendFlowTrailRow> {
679        self.state.update(open, high, low, close, volume)
680    }
681}
682
683#[inline(always)]
684fn crossover(prev_left: Option<f64>, left: f64, right: f64) -> bool {
685    prev_left
686        .map(|p| p.is_finite() && p <= right && left.is_finite() && left > right)
687        .unwrap_or(false)
688}
689
690#[inline(always)]
691fn crossunder(prev_left: Option<f64>, left: f64, right: f64) -> bool {
692    prev_left
693        .map(|p| p.is_finite() && p >= right && left.is_finite() && left < right)
694        .unwrap_or(false)
695}
696
697#[inline(always)]
698fn cross_pair(prev_left: Option<f64>, left: f64, prev_right: Option<f64>, right: f64) -> bool {
699    match (prev_left, prev_right) {
700        (Some(pl), Some(pr))
701            if pl.is_finite() && pr.is_finite() && left.is_finite() && right.is_finite() =>
702        {
703            pl <= pr && left > right
704        }
705        _ => false,
706    }
707}
708
709#[inline(always)]
710fn crossunder_pair(prev_left: Option<f64>, left: f64, prev_right: Option<f64>, right: f64) -> bool {
711    match (prev_left, prev_right) {
712        (Some(pl), Some(pr))
713            if pl.is_finite() && pr.is_finite() && left.is_finite() && right.is_finite() =>
714        {
715            pl >= pr && left < right
716        }
717        _ => false,
718    }
719}
720
721#[inline(always)]
722fn alpha_required_bars(alpha_length: usize) -> usize {
723    if alpha_length <= 1 {
724        1
725    } else {
726        alpha_length + (alpha_length as f64).sqrt().floor() as usize - 1
727    }
728}
729
730#[inline(always)]
731fn required_valid_bars(alpha_length: usize, mfi_length: usize) -> usize {
732    alpha_required_bars(alpha_length).max(mfi_length + MFI_HMA_LENGTH + MFI_HMA_SQRT - 1)
733}
734
735fn validate_params(
736    alpha_length: usize,
737    alpha_multiplier: f64,
738    mfi_length: usize,
739    data_len: usize,
740) -> Result<(), TrendFlowTrailError> {
741    if alpha_length == 0 {
742        return Err(TrendFlowTrailError::InvalidAlphaLength { alpha_length });
743    }
744    if !alpha_multiplier.is_finite() || alpha_multiplier < 0.1 {
745        return Err(TrendFlowTrailError::InvalidAlphaMultiplier { alpha_multiplier });
746    }
747    if mfi_length == 0 {
748        return Err(TrendFlowTrailError::InvalidMfiLength { mfi_length });
749    }
750    if data_len != usize::MAX {
751        let needed = required_valid_bars(alpha_length, mfi_length);
752        if data_len < needed {
753            return Err(TrendFlowTrailError::NotEnoughValidData {
754                needed,
755                valid: data_len,
756            });
757        }
758    }
759    Ok(())
760}
761
762fn analyze_valid_segments(
763    open: &[f64],
764    high: &[f64],
765    low: &[f64],
766    close: &[f64],
767    volume: &[f64],
768) -> Result<(usize, usize), TrendFlowTrailError> {
769    if open.is_empty() {
770        return Err(TrendFlowTrailError::EmptyInputData);
771    }
772    if open.len() != high.len()
773        || open.len() != low.len()
774        || open.len() != close.len()
775        || open.len() != volume.len()
776    {
777        return Err(TrendFlowTrailError::InconsistentSliceLengths {
778            open_len: open.len(),
779            high_len: high.len(),
780            low_len: low.len(),
781            close_len: close.len(),
782            volume_len: volume.len(),
783        });
784    }
785    let mut valid = 0usize;
786    let mut run = 0usize;
787    let mut max_run = 0usize;
788    for i in 0..open.len() {
789        if open[i].is_finite()
790            && high[i].is_finite()
791            && low[i].is_finite()
792            && close[i].is_finite()
793            && volume[i].is_finite()
794        {
795            valid += 1;
796            run += 1;
797            max_run = max_run.max(run);
798        } else {
799            run = 0;
800        }
801    }
802    if valid == 0 {
803        return Err(TrendFlowTrailError::AllValuesNaN);
804    }
805    Ok((valid, max_run))
806}
807
808fn prepare_input<'a>(
809    input: &'a TrendFlowTrailInput<'a>,
810    kernel: Kernel,
811) -> Result<PreparedTrendFlowTrail<'a>, TrendFlowTrailError> {
812    if matches!(kernel, Kernel::Auto) {
813        let _ = detect_best_kernel();
814    }
815    let (open, high, low, close, volume) = input.as_ohlcv();
816    let alpha_length = input.get_alpha_length();
817    let alpha_multiplier = input.get_alpha_multiplier();
818    let mfi_length = input.get_mfi_length();
819    validate_params(alpha_length, alpha_multiplier, mfi_length, close.len())?;
820    let (_, max_run) = analyze_valid_segments(open, high, low, close, volume)?;
821    let needed = required_valid_bars(alpha_length, mfi_length);
822    if max_run < needed {
823        return Err(TrendFlowTrailError::NotEnoughValidData {
824            needed,
825            valid: max_run,
826        });
827    }
828    Ok(PreparedTrendFlowTrail {
829        open,
830        high,
831        low,
832        close,
833        volume,
834        alpha_length,
835        alpha_multiplier,
836        mfi_length,
837        warmup: needed - 1,
838    })
839}
840
841#[allow(clippy::too_many_arguments)]
842fn compute_row(
843    open: &[f64],
844    high: &[f64],
845    low: &[f64],
846    close: &[f64],
847    volume: &[f64],
848    alpha_length: usize,
849    alpha_multiplier: f64,
850    mfi_length: usize,
851    alpha_trail_out: &mut [f64],
852    alpha_trail_bullish_out: &mut [f64],
853    alpha_trail_bearish_out: &mut [f64],
854    alpha_dir_out: &mut [f64],
855    mfi_out: &mut [f64],
856    tp_upper_out: &mut [f64],
857    tp_lower_out: &mut [f64],
858    alpha_trail_bullish_switch_out: &mut [f64],
859    alpha_trail_bearish_switch_out: &mut [f64],
860    mfi_overbought_out: &mut [f64],
861    mfi_oversold_out: &mut [f64],
862    mfi_cross_up_mid_out: &mut [f64],
863    mfi_cross_down_mid_out: &mut [f64],
864    price_cross_alpha_trail_up_out: &mut [f64],
865    price_cross_alpha_trail_down_out: &mut [f64],
866    mfi_above_90_out: &mut [f64],
867    mfi_below_10_out: &mut [f64],
868) -> Result<(), TrendFlowTrailError> {
869    let expected = close.len();
870    for out in [
871        &*alpha_trail_out,
872        &*alpha_trail_bullish_out,
873        &*alpha_trail_bearish_out,
874        &*alpha_dir_out,
875        &*mfi_out,
876        &*tp_upper_out,
877        &*tp_lower_out,
878        &*alpha_trail_bullish_switch_out,
879        &*alpha_trail_bearish_switch_out,
880        &*mfi_overbought_out,
881        &*mfi_oversold_out,
882        &*mfi_cross_up_mid_out,
883        &*mfi_cross_down_mid_out,
884        &*price_cross_alpha_trail_up_out,
885        &*price_cross_alpha_trail_down_out,
886        &*mfi_above_90_out,
887        &*mfi_below_10_out,
888    ] {
889        if out.len() != expected {
890            return Err(TrendFlowTrailError::OutputLengthMismatch {
891                expected,
892                got: out.len(),
893            });
894        }
895    }
896    let mut state = TrendFlowTrailState::try_new(alpha_length, alpha_multiplier, mfi_length)?;
897    for i in 0..expected {
898        match state.update(open[i], high[i], low[i], close[i], volume[i]) {
899            Some((a, ab, ar, d, m, tu, tl, bs, rs, ob, os, mu, md, pu, pd, a90, b10)) => {
900                alpha_trail_out[i] = a;
901                alpha_trail_bullish_out[i] = ab;
902                alpha_trail_bearish_out[i] = ar;
903                alpha_dir_out[i] = d;
904                mfi_out[i] = m;
905                tp_upper_out[i] = tu;
906                tp_lower_out[i] = tl;
907                alpha_trail_bullish_switch_out[i] = bs;
908                alpha_trail_bearish_switch_out[i] = rs;
909                mfi_overbought_out[i] = ob;
910                mfi_oversold_out[i] = os;
911                mfi_cross_up_mid_out[i] = mu;
912                mfi_cross_down_mid_out[i] = md;
913                price_cross_alpha_trail_up_out[i] = pu;
914                price_cross_alpha_trail_down_out[i] = pd;
915                mfi_above_90_out[i] = a90;
916                mfi_below_10_out[i] = b10;
917            }
918            None => {
919                alpha_trail_out[i] = f64::NAN;
920                alpha_trail_bullish_out[i] = f64::NAN;
921                alpha_trail_bearish_out[i] = f64::NAN;
922                alpha_dir_out[i] = f64::NAN;
923                mfi_out[i] = f64::NAN;
924                tp_upper_out[i] = f64::NAN;
925                tp_lower_out[i] = f64::NAN;
926                alpha_trail_bullish_switch_out[i] = f64::NAN;
927                alpha_trail_bearish_switch_out[i] = f64::NAN;
928                mfi_overbought_out[i] = f64::NAN;
929                mfi_oversold_out[i] = f64::NAN;
930                mfi_cross_up_mid_out[i] = f64::NAN;
931                mfi_cross_down_mid_out[i] = f64::NAN;
932                price_cross_alpha_trail_up_out[i] = f64::NAN;
933                price_cross_alpha_trail_down_out[i] = f64::NAN;
934                mfi_above_90_out[i] = f64::NAN;
935                mfi_below_10_out[i] = f64::NAN;
936            }
937        }
938    }
939    Ok(())
940}
941
942#[inline]
943pub fn trend_flow_trail(
944    input: &TrendFlowTrailInput,
945) -> Result<TrendFlowTrailOutput, TrendFlowTrailError> {
946    trend_flow_trail_with_kernel(input, Kernel::Auto)
947}
948
949pub fn trend_flow_trail_with_kernel(
950    input: &TrendFlowTrailInput,
951    kernel: Kernel,
952) -> Result<TrendFlowTrailOutput, TrendFlowTrailError> {
953    let prepared = prepare_input(input, kernel)?;
954    let len = prepared.close.len();
955    let warmup = prepared.warmup;
956    let mut alpha_trail = alloc_with_nan_prefix(len, warmup);
957    let mut alpha_trail_bullish = alloc_with_nan_prefix(len, warmup);
958    let mut alpha_trail_bearish = alloc_with_nan_prefix(len, warmup);
959    let mut alpha_dir = alloc_with_nan_prefix(len, warmup);
960    let mut mfi = alloc_with_nan_prefix(len, warmup);
961    let mut tp_upper = alloc_with_nan_prefix(len, warmup);
962    let mut tp_lower = alloc_with_nan_prefix(len, warmup);
963    let mut alpha_trail_bullish_switch = alloc_with_nan_prefix(len, warmup);
964    let mut alpha_trail_bearish_switch = alloc_with_nan_prefix(len, warmup);
965    let mut mfi_overbought = alloc_with_nan_prefix(len, warmup);
966    let mut mfi_oversold = alloc_with_nan_prefix(len, warmup);
967    let mut mfi_cross_up_mid = alloc_with_nan_prefix(len, warmup);
968    let mut mfi_cross_down_mid = alloc_with_nan_prefix(len, warmup);
969    let mut price_cross_alpha_trail_up = alloc_with_nan_prefix(len, warmup);
970    let mut price_cross_alpha_trail_down = alloc_with_nan_prefix(len, warmup);
971    let mut mfi_above_90 = alloc_with_nan_prefix(len, warmup);
972    let mut mfi_below_10 = alloc_with_nan_prefix(len, warmup);
973    compute_row(
974        prepared.open,
975        prepared.high,
976        prepared.low,
977        prepared.close,
978        prepared.volume,
979        prepared.alpha_length,
980        prepared.alpha_multiplier,
981        prepared.mfi_length,
982        &mut alpha_trail,
983        &mut alpha_trail_bullish,
984        &mut alpha_trail_bearish,
985        &mut alpha_dir,
986        &mut mfi,
987        &mut tp_upper,
988        &mut tp_lower,
989        &mut alpha_trail_bullish_switch,
990        &mut alpha_trail_bearish_switch,
991        &mut mfi_overbought,
992        &mut mfi_oversold,
993        &mut mfi_cross_up_mid,
994        &mut mfi_cross_down_mid,
995        &mut price_cross_alpha_trail_up,
996        &mut price_cross_alpha_trail_down,
997        &mut mfi_above_90,
998        &mut mfi_below_10,
999    )?;
1000    Ok(TrendFlowTrailOutput {
1001        alpha_trail,
1002        alpha_trail_bullish,
1003        alpha_trail_bearish,
1004        alpha_dir,
1005        mfi,
1006        tp_upper,
1007        tp_lower,
1008        alpha_trail_bullish_switch,
1009        alpha_trail_bearish_switch,
1010        mfi_overbought,
1011        mfi_oversold,
1012        mfi_cross_up_mid,
1013        mfi_cross_down_mid,
1014        price_cross_alpha_trail_up,
1015        price_cross_alpha_trail_down,
1016        mfi_above_90,
1017        mfi_below_10,
1018    })
1019}
1020
1021#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1022#[allow(clippy::too_many_arguments)]
1023pub fn trend_flow_trail_into(
1024    alpha_trail_out: &mut [f64],
1025    alpha_trail_bullish_out: &mut [f64],
1026    alpha_trail_bearish_out: &mut [f64],
1027    alpha_dir_out: &mut [f64],
1028    mfi_out: &mut [f64],
1029    tp_upper_out: &mut [f64],
1030    tp_lower_out: &mut [f64],
1031    alpha_trail_bullish_switch_out: &mut [f64],
1032    alpha_trail_bearish_switch_out: &mut [f64],
1033    mfi_overbought_out: &mut [f64],
1034    mfi_oversold_out: &mut [f64],
1035    mfi_cross_up_mid_out: &mut [f64],
1036    mfi_cross_down_mid_out: &mut [f64],
1037    price_cross_alpha_trail_up_out: &mut [f64],
1038    price_cross_alpha_trail_down_out: &mut [f64],
1039    mfi_above_90_out: &mut [f64],
1040    mfi_below_10_out: &mut [f64],
1041    input: &TrendFlowTrailInput,
1042) -> Result<(), TrendFlowTrailError> {
1043    trend_flow_trail_into_slice(
1044        alpha_trail_out,
1045        alpha_trail_bullish_out,
1046        alpha_trail_bearish_out,
1047        alpha_dir_out,
1048        mfi_out,
1049        tp_upper_out,
1050        tp_lower_out,
1051        alpha_trail_bullish_switch_out,
1052        alpha_trail_bearish_switch_out,
1053        mfi_overbought_out,
1054        mfi_oversold_out,
1055        mfi_cross_up_mid_out,
1056        mfi_cross_down_mid_out,
1057        price_cross_alpha_trail_up_out,
1058        price_cross_alpha_trail_down_out,
1059        mfi_above_90_out,
1060        mfi_below_10_out,
1061        input,
1062        Kernel::Auto,
1063    )
1064}
1065
1066#[allow(clippy::too_many_arguments)]
1067pub fn trend_flow_trail_into_slice(
1068    alpha_trail_out: &mut [f64],
1069    alpha_trail_bullish_out: &mut [f64],
1070    alpha_trail_bearish_out: &mut [f64],
1071    alpha_dir_out: &mut [f64],
1072    mfi_out: &mut [f64],
1073    tp_upper_out: &mut [f64],
1074    tp_lower_out: &mut [f64],
1075    alpha_trail_bullish_switch_out: &mut [f64],
1076    alpha_trail_bearish_switch_out: &mut [f64],
1077    mfi_overbought_out: &mut [f64],
1078    mfi_oversold_out: &mut [f64],
1079    mfi_cross_up_mid_out: &mut [f64],
1080    mfi_cross_down_mid_out: &mut [f64],
1081    price_cross_alpha_trail_up_out: &mut [f64],
1082    price_cross_alpha_trail_down_out: &mut [f64],
1083    mfi_above_90_out: &mut [f64],
1084    mfi_below_10_out: &mut [f64],
1085    input: &TrendFlowTrailInput,
1086    kernel: Kernel,
1087) -> Result<(), TrendFlowTrailError> {
1088    let prepared = prepare_input(input, kernel)?;
1089    compute_row(
1090        prepared.open,
1091        prepared.high,
1092        prepared.low,
1093        prepared.close,
1094        prepared.volume,
1095        prepared.alpha_length,
1096        prepared.alpha_multiplier,
1097        prepared.mfi_length,
1098        alpha_trail_out,
1099        alpha_trail_bullish_out,
1100        alpha_trail_bearish_out,
1101        alpha_dir_out,
1102        mfi_out,
1103        tp_upper_out,
1104        tp_lower_out,
1105        alpha_trail_bullish_switch_out,
1106        alpha_trail_bearish_switch_out,
1107        mfi_overbought_out,
1108        mfi_oversold_out,
1109        mfi_cross_up_mid_out,
1110        mfi_cross_down_mid_out,
1111        price_cross_alpha_trail_up_out,
1112        price_cross_alpha_trail_down_out,
1113        mfi_above_90_out,
1114        mfi_below_10_out,
1115    )
1116}
1117
1118#[derive(Clone, Debug)]
1119pub struct TrendFlowTrailBatchRange {
1120    pub alpha_length: (usize, usize, usize),
1121    pub alpha_multiplier: (f64, f64, f64),
1122    pub mfi_length: (usize, usize, usize),
1123}
1124
1125impl Default for TrendFlowTrailBatchRange {
1126    fn default() -> Self {
1127        Self {
1128            alpha_length: (DEFAULT_ALPHA_LENGTH, DEFAULT_ALPHA_LENGTH, 0),
1129            alpha_multiplier: (DEFAULT_ALPHA_MULTIPLIER, DEFAULT_ALPHA_MULTIPLIER, 0.0),
1130            mfi_length: (DEFAULT_MFI_LENGTH, DEFAULT_MFI_LENGTH, 0),
1131        }
1132    }
1133}
1134
1135#[derive(Clone, Debug, Default)]
1136pub struct TrendFlowTrailBatchBuilder {
1137    pub range: TrendFlowTrailBatchRange,
1138    pub kernel: Kernel,
1139}
1140
1141impl TrendFlowTrailBatchBuilder {
1142    pub fn new() -> Self {
1143        Self::default()
1144    }
1145    pub fn alpha_length(mut self, start: usize, end: usize, step: usize) -> Self {
1146        self.range.alpha_length = (start, end, step);
1147        self
1148    }
1149    pub fn alpha_multiplier(mut self, start: f64, end: f64, step: f64) -> Self {
1150        self.range.alpha_multiplier = (start, end, step);
1151        self
1152    }
1153    pub fn mfi_length(mut self, start: usize, end: usize, step: usize) -> Self {
1154        self.range.mfi_length = (start, end, step);
1155        self
1156    }
1157    pub fn kernel(mut self, kernel: Kernel) -> Self {
1158        self.kernel = kernel;
1159        self
1160    }
1161    pub fn apply_slices(
1162        self,
1163        open: &[f64],
1164        high: &[f64],
1165        low: &[f64],
1166        close: &[f64],
1167        volume: &[f64],
1168    ) -> Result<TrendFlowTrailBatchOutput, TrendFlowTrailError> {
1169        trend_flow_trail_batch_with_kernel(open, high, low, close, volume, &self.range, self.kernel)
1170    }
1171}
1172
1173#[derive(Debug, Clone)]
1174#[cfg_attr(
1175    all(target_arch = "wasm32", feature = "wasm"),
1176    derive(Serialize, Deserialize)
1177)]
1178pub struct TrendFlowTrailBatchOutput {
1179    pub rows: usize,
1180    pub cols: usize,
1181    pub alpha_trail: Vec<f64>,
1182    pub alpha_trail_bullish: Vec<f64>,
1183    pub alpha_trail_bearish: Vec<f64>,
1184    pub alpha_dir: Vec<f64>,
1185    pub mfi: Vec<f64>,
1186    pub tp_upper: Vec<f64>,
1187    pub tp_lower: Vec<f64>,
1188    pub alpha_trail_bullish_switch: Vec<f64>,
1189    pub alpha_trail_bearish_switch: Vec<f64>,
1190    pub mfi_overbought: Vec<f64>,
1191    pub mfi_oversold: Vec<f64>,
1192    pub mfi_cross_up_mid: Vec<f64>,
1193    pub mfi_cross_down_mid: Vec<f64>,
1194    pub price_cross_alpha_trail_up: Vec<f64>,
1195    pub price_cross_alpha_trail_down: Vec<f64>,
1196    pub mfi_above_90: Vec<f64>,
1197    pub mfi_below_10: Vec<f64>,
1198}
1199
1200fn axis_usize(
1201    axis: &'static str,
1202    (start, end, step): (usize, usize, usize),
1203) -> Result<Vec<usize>, TrendFlowTrailError> {
1204    if start == end || step == 0 {
1205        return Ok(vec![start]);
1206    }
1207    let mut out = Vec::new();
1208    if start < end {
1209        let mut value = start;
1210        while value <= end {
1211            out.push(value);
1212            value = value.saturating_add(step);
1213            if step == 0 {
1214                break;
1215            }
1216        }
1217    } else {
1218        let mut value = start;
1219        while value >= end {
1220            out.push(value);
1221            if value < step {
1222                break;
1223            }
1224            value -= step;
1225            if step == 0 {
1226                break;
1227            }
1228        }
1229    }
1230    if out.is_empty() || out.last().copied() != Some(end) {
1231        return Err(TrendFlowTrailError::InvalidRange {
1232            axis,
1233            start: start.to_string(),
1234            end: end.to_string(),
1235            step: step.to_string(),
1236        });
1237    }
1238    Ok(out)
1239}
1240
1241fn axis_f64(
1242    axis: &'static str,
1243    (start, end, step): (f64, f64, f64),
1244) -> Result<Vec<f64>, TrendFlowTrailError> {
1245    if !start.is_finite() || !end.is_finite() || !step.is_finite() || step < 0.0 {
1246        return Err(TrendFlowTrailError::InvalidRange {
1247            axis,
1248            start: start.to_string(),
1249            end: end.to_string(),
1250            step: step.to_string(),
1251        });
1252    }
1253    if (start - end).abs() <= f64::EPSILON || step == 0.0 {
1254        return Ok(vec![start]);
1255    }
1256    let mut out = Vec::new();
1257    let eps = step.abs() * 1e-9 + 1e-12;
1258    if start < end {
1259        let mut value = start;
1260        while value <= end + eps {
1261            out.push(value.min(end));
1262            value += step;
1263        }
1264    } else {
1265        let mut value = start;
1266        while value >= end - eps {
1267            out.push(value.max(end));
1268            value -= step;
1269        }
1270    }
1271    if out.is_empty() || (out.last().copied().unwrap_or(start) - end).abs() > eps {
1272        return Err(TrendFlowTrailError::InvalidRange {
1273            axis,
1274            start: start.to_string(),
1275            end: end.to_string(),
1276            step: step.to_string(),
1277        });
1278    }
1279    Ok(out)
1280}
1281
1282pub fn expand_grid_trend_flow_trail(
1283    sweep: &TrendFlowTrailBatchRange,
1284) -> Result<Vec<TrendFlowTrailParams>, TrendFlowTrailError> {
1285    let alpha_lengths = axis_usize("alpha_length", sweep.alpha_length)?;
1286    let alpha_multipliers = axis_f64("alpha_multiplier", sweep.alpha_multiplier)?;
1287    let mfi_lengths = axis_usize("mfi_length", sweep.mfi_length)?;
1288    let mut out =
1289        Vec::with_capacity(alpha_lengths.len() * alpha_multipliers.len() * mfi_lengths.len());
1290    for &alpha_length in &alpha_lengths {
1291        for &alpha_multiplier in &alpha_multipliers {
1292            for &mfi_length in &mfi_lengths {
1293                out.push(TrendFlowTrailParams {
1294                    alpha_length: Some(alpha_length),
1295                    alpha_multiplier: Some(alpha_multiplier),
1296                    mfi_length: Some(mfi_length),
1297                });
1298            }
1299        }
1300    }
1301    Ok(out)
1302}
1303
1304#[allow(clippy::too_many_arguments)]
1305pub fn trend_flow_trail_batch_with_kernel(
1306    open: &[f64],
1307    high: &[f64],
1308    low: &[f64],
1309    close: &[f64],
1310    volume: &[f64],
1311    sweep: &TrendFlowTrailBatchRange,
1312    kernel: Kernel,
1313) -> Result<TrendFlowTrailBatchOutput, TrendFlowTrailError> {
1314    match kernel {
1315        Kernel::Auto => {
1316            let _ = detect_best_batch_kernel();
1317        }
1318        k if !k.is_batch() => return Err(TrendFlowTrailError::InvalidKernelForBatch(k)),
1319        _ => {}
1320    }
1321    let (_, max_run) = analyze_valid_segments(open, high, low, close, volume)?;
1322    let combos = expand_grid_trend_flow_trail(sweep)?;
1323    for params in &combos {
1324        let needed = required_valid_bars(
1325            params.alpha_length.unwrap_or(DEFAULT_ALPHA_LENGTH),
1326            params.mfi_length.unwrap_or(DEFAULT_MFI_LENGTH),
1327        );
1328        if max_run < needed {
1329            return Err(TrendFlowTrailError::NotEnoughValidData {
1330                needed,
1331                valid: max_run,
1332            });
1333        }
1334    }
1335    let rows = combos.len();
1336    let cols = close.len();
1337    let total = rows * cols;
1338    let warmups: Vec<usize> = combos
1339        .iter()
1340        .map(|p| {
1341            required_valid_bars(
1342                p.alpha_length.unwrap_or(DEFAULT_ALPHA_LENGTH),
1343                p.mfi_length.unwrap_or(DEFAULT_MFI_LENGTH),
1344            ) - 1
1345        })
1346        .collect();
1347
1348    let mut alpha_trail_mu = make_uninit_matrix(rows, cols);
1349    let mut alpha_trail_bullish_mu = make_uninit_matrix(rows, cols);
1350    let mut alpha_trail_bearish_mu = make_uninit_matrix(rows, cols);
1351    let mut alpha_dir_mu = make_uninit_matrix(rows, cols);
1352    let mut mfi_mu = make_uninit_matrix(rows, cols);
1353    let mut tp_upper_mu = make_uninit_matrix(rows, cols);
1354    let mut tp_lower_mu = make_uninit_matrix(rows, cols);
1355    let mut alpha_trail_bullish_switch_mu = make_uninit_matrix(rows, cols);
1356    let mut alpha_trail_bearish_switch_mu = make_uninit_matrix(rows, cols);
1357    let mut mfi_overbought_mu = make_uninit_matrix(rows, cols);
1358    let mut mfi_oversold_mu = make_uninit_matrix(rows, cols);
1359    let mut mfi_cross_up_mid_mu = make_uninit_matrix(rows, cols);
1360    let mut mfi_cross_down_mid_mu = make_uninit_matrix(rows, cols);
1361    let mut price_cross_alpha_trail_up_mu = make_uninit_matrix(rows, cols);
1362    let mut price_cross_alpha_trail_down_mu = make_uninit_matrix(rows, cols);
1363    let mut mfi_above_90_mu = make_uninit_matrix(rows, cols);
1364    let mut mfi_below_10_mu = make_uninit_matrix(rows, cols);
1365
1366    for buf in [
1367        &mut alpha_trail_mu,
1368        &mut alpha_trail_bullish_mu,
1369        &mut alpha_trail_bearish_mu,
1370        &mut alpha_dir_mu,
1371        &mut mfi_mu,
1372        &mut tp_upper_mu,
1373        &mut tp_lower_mu,
1374        &mut alpha_trail_bullish_switch_mu,
1375        &mut alpha_trail_bearish_switch_mu,
1376        &mut mfi_overbought_mu,
1377        &mut mfi_oversold_mu,
1378        &mut mfi_cross_up_mid_mu,
1379        &mut mfi_cross_down_mid_mu,
1380        &mut price_cross_alpha_trail_up_mu,
1381        &mut price_cross_alpha_trail_down_mu,
1382        &mut mfi_above_90_mu,
1383        &mut mfi_below_10_mu,
1384    ] {
1385        init_matrix_prefixes(buf, cols, &warmups);
1386    }
1387
1388    let alpha_trail =
1389        unsafe { std::slice::from_raw_parts_mut(alpha_trail_mu.as_mut_ptr() as *mut f64, total) };
1390    let alpha_trail_bullish = unsafe {
1391        std::slice::from_raw_parts_mut(alpha_trail_bullish_mu.as_mut_ptr() as *mut f64, total)
1392    };
1393    let alpha_trail_bearish = unsafe {
1394        std::slice::from_raw_parts_mut(alpha_trail_bearish_mu.as_mut_ptr() as *mut f64, total)
1395    };
1396    let alpha_dir =
1397        unsafe { std::slice::from_raw_parts_mut(alpha_dir_mu.as_mut_ptr() as *mut f64, total) };
1398    let mfi = unsafe { std::slice::from_raw_parts_mut(mfi_mu.as_mut_ptr() as *mut f64, total) };
1399    let tp_upper =
1400        unsafe { std::slice::from_raw_parts_mut(tp_upper_mu.as_mut_ptr() as *mut f64, total) };
1401    let tp_lower =
1402        unsafe { std::slice::from_raw_parts_mut(tp_lower_mu.as_mut_ptr() as *mut f64, total) };
1403    let alpha_trail_bullish_switch = unsafe {
1404        std::slice::from_raw_parts_mut(
1405            alpha_trail_bullish_switch_mu.as_mut_ptr() as *mut f64,
1406            total,
1407        )
1408    };
1409    let alpha_trail_bearish_switch = unsafe {
1410        std::slice::from_raw_parts_mut(
1411            alpha_trail_bearish_switch_mu.as_mut_ptr() as *mut f64,
1412            total,
1413        )
1414    };
1415    let mfi_overbought = unsafe {
1416        std::slice::from_raw_parts_mut(mfi_overbought_mu.as_mut_ptr() as *mut f64, total)
1417    };
1418    let mfi_oversold =
1419        unsafe { std::slice::from_raw_parts_mut(mfi_oversold_mu.as_mut_ptr() as *mut f64, total) };
1420    let mfi_cross_up_mid = unsafe {
1421        std::slice::from_raw_parts_mut(mfi_cross_up_mid_mu.as_mut_ptr() as *mut f64, total)
1422    };
1423    let mfi_cross_down_mid = unsafe {
1424        std::slice::from_raw_parts_mut(mfi_cross_down_mid_mu.as_mut_ptr() as *mut f64, total)
1425    };
1426    let price_cross_alpha_trail_up = unsafe {
1427        std::slice::from_raw_parts_mut(
1428            price_cross_alpha_trail_up_mu.as_mut_ptr() as *mut f64,
1429            total,
1430        )
1431    };
1432    let price_cross_alpha_trail_down = unsafe {
1433        std::slice::from_raw_parts_mut(
1434            price_cross_alpha_trail_down_mu.as_mut_ptr() as *mut f64,
1435            total,
1436        )
1437    };
1438    let mfi_above_90 =
1439        unsafe { std::slice::from_raw_parts_mut(mfi_above_90_mu.as_mut_ptr() as *mut f64, total) };
1440    let mfi_below_10 =
1441        unsafe { std::slice::from_raw_parts_mut(mfi_below_10_mu.as_mut_ptr() as *mut f64, total) };
1442
1443    for (row, params) in combos.iter().enumerate() {
1444        let offset = row * cols;
1445        compute_row(
1446            open,
1447            high,
1448            low,
1449            close,
1450            volume,
1451            params.alpha_length.unwrap_or(DEFAULT_ALPHA_LENGTH),
1452            params.alpha_multiplier.unwrap_or(DEFAULT_ALPHA_MULTIPLIER),
1453            params.mfi_length.unwrap_or(DEFAULT_MFI_LENGTH),
1454            &mut alpha_trail[offset..offset + cols],
1455            &mut alpha_trail_bullish[offset..offset + cols],
1456            &mut alpha_trail_bearish[offset..offset + cols],
1457            &mut alpha_dir[offset..offset + cols],
1458            &mut mfi[offset..offset + cols],
1459            &mut tp_upper[offset..offset + cols],
1460            &mut tp_lower[offset..offset + cols],
1461            &mut alpha_trail_bullish_switch[offset..offset + cols],
1462            &mut alpha_trail_bearish_switch[offset..offset + cols],
1463            &mut mfi_overbought[offset..offset + cols],
1464            &mut mfi_oversold[offset..offset + cols],
1465            &mut mfi_cross_up_mid[offset..offset + cols],
1466            &mut mfi_cross_down_mid[offset..offset + cols],
1467            &mut price_cross_alpha_trail_up[offset..offset + cols],
1468            &mut price_cross_alpha_trail_down[offset..offset + cols],
1469            &mut mfi_above_90[offset..offset + cols],
1470            &mut mfi_below_10[offset..offset + cols],
1471        )?;
1472    }
1473
1474    Ok(TrendFlowTrailBatchOutput {
1475        rows,
1476        cols,
1477        alpha_trail: alpha_trail.to_vec(),
1478        alpha_trail_bullish: alpha_trail_bullish.to_vec(),
1479        alpha_trail_bearish: alpha_trail_bearish.to_vec(),
1480        alpha_dir: alpha_dir.to_vec(),
1481        mfi: mfi.to_vec(),
1482        tp_upper: tp_upper.to_vec(),
1483        tp_lower: tp_lower.to_vec(),
1484        alpha_trail_bullish_switch: alpha_trail_bullish_switch.to_vec(),
1485        alpha_trail_bearish_switch: alpha_trail_bearish_switch.to_vec(),
1486        mfi_overbought: mfi_overbought.to_vec(),
1487        mfi_oversold: mfi_oversold.to_vec(),
1488        mfi_cross_up_mid: mfi_cross_up_mid.to_vec(),
1489        mfi_cross_down_mid: mfi_cross_down_mid.to_vec(),
1490        price_cross_alpha_trail_up: price_cross_alpha_trail_up.to_vec(),
1491        price_cross_alpha_trail_down: price_cross_alpha_trail_down.to_vec(),
1492        mfi_above_90: mfi_above_90.to_vec(),
1493        mfi_below_10: mfi_below_10.to_vec(),
1494    })
1495}
1496
1497#[allow(clippy::too_many_arguments)]
1498pub fn trend_flow_trail_batch_slice(
1499    open: &[f64],
1500    high: &[f64],
1501    low: &[f64],
1502    close: &[f64],
1503    volume: &[f64],
1504    sweep: &TrendFlowTrailBatchRange,
1505    kernel: Kernel,
1506) -> Result<TrendFlowTrailBatchOutput, TrendFlowTrailError> {
1507    trend_flow_trail_batch_with_kernel(open, high, low, close, volume, sweep, kernel)
1508}
1509
1510#[allow(clippy::too_many_arguments)]
1511pub fn trend_flow_trail_batch_par_slice(
1512    open: &[f64],
1513    high: &[f64],
1514    low: &[f64],
1515    close: &[f64],
1516    volume: &[f64],
1517    sweep: &TrendFlowTrailBatchRange,
1518    kernel: Kernel,
1519) -> Result<TrendFlowTrailBatchOutput, TrendFlowTrailError> {
1520    trend_flow_trail_batch_with_kernel(open, high, low, close, volume, sweep, kernel)
1521}
1522
1523#[cfg(feature = "python")]
1524#[pyfunction(name = "trend_flow_trail")]
1525#[pyo3(signature = (open, high, low, close, volume, alpha_length=DEFAULT_ALPHA_LENGTH, alpha_multiplier=DEFAULT_ALPHA_MULTIPLIER, mfi_length=DEFAULT_MFI_LENGTH, kernel=None))]
1526pub fn trend_flow_trail_py<'py>(
1527    py: Python<'py>,
1528    open: PyReadonlyArray1<'py, f64>,
1529    high: PyReadonlyArray1<'py, f64>,
1530    low: PyReadonlyArray1<'py, f64>,
1531    close: PyReadonlyArray1<'py, f64>,
1532    volume: PyReadonlyArray1<'py, f64>,
1533    alpha_length: usize,
1534    alpha_multiplier: f64,
1535    mfi_length: usize,
1536    kernel: Option<&str>,
1537) -> PyResult<Bound<'py, PyDict>> {
1538    let kernel = validate_kernel(kernel, false)?;
1539    let open = open.as_slice()?;
1540    let high = high.as_slice()?;
1541    let low = low.as_slice()?;
1542    let close = close.as_slice()?;
1543    let volume = volume.as_slice()?;
1544    let input = TrendFlowTrailInput::from_slices(
1545        open,
1546        high,
1547        low,
1548        close,
1549        volume,
1550        TrendFlowTrailParams {
1551            alpha_length: Some(alpha_length),
1552            alpha_multiplier: Some(alpha_multiplier),
1553            mfi_length: Some(mfi_length),
1554        },
1555    );
1556    let out = py
1557        .allow_threads(|| trend_flow_trail_with_kernel(&input, kernel))
1558        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1559    let dict = PyDict::new(py);
1560    dict.set_item("alpha_trail", out.alpha_trail.into_pyarray(py))?;
1561    dict.set_item(
1562        "alpha_trail_bullish",
1563        out.alpha_trail_bullish.into_pyarray(py),
1564    )?;
1565    dict.set_item(
1566        "alpha_trail_bearish",
1567        out.alpha_trail_bearish.into_pyarray(py),
1568    )?;
1569    dict.set_item("alpha_dir", out.alpha_dir.into_pyarray(py))?;
1570    dict.set_item("mfi", out.mfi.into_pyarray(py))?;
1571    dict.set_item("tp_upper", out.tp_upper.into_pyarray(py))?;
1572    dict.set_item("tp_lower", out.tp_lower.into_pyarray(py))?;
1573    dict.set_item(
1574        "alpha_trail_bullish_switch",
1575        out.alpha_trail_bullish_switch.into_pyarray(py),
1576    )?;
1577    dict.set_item(
1578        "alpha_trail_bearish_switch",
1579        out.alpha_trail_bearish_switch.into_pyarray(py),
1580    )?;
1581    dict.set_item("mfi_overbought", out.mfi_overbought.into_pyarray(py))?;
1582    dict.set_item("mfi_oversold", out.mfi_oversold.into_pyarray(py))?;
1583    dict.set_item("mfi_cross_up_mid", out.mfi_cross_up_mid.into_pyarray(py))?;
1584    dict.set_item(
1585        "mfi_cross_down_mid",
1586        out.mfi_cross_down_mid.into_pyarray(py),
1587    )?;
1588    dict.set_item(
1589        "price_cross_alpha_trail_up",
1590        out.price_cross_alpha_trail_up.into_pyarray(py),
1591    )?;
1592    dict.set_item(
1593        "price_cross_alpha_trail_down",
1594        out.price_cross_alpha_trail_down.into_pyarray(py),
1595    )?;
1596    dict.set_item("mfi_above_90", out.mfi_above_90.into_pyarray(py))?;
1597    dict.set_item("mfi_below_10", out.mfi_below_10.into_pyarray(py))?;
1598    Ok(dict)
1599}
1600
1601#[cfg(feature = "python")]
1602#[pyfunction(name = "trend_flow_trail_batch")]
1603#[pyo3(signature = (open, high, low, close, volume, alpha_length_range=(DEFAULT_ALPHA_LENGTH, DEFAULT_ALPHA_LENGTH, 0), alpha_multiplier_range=(DEFAULT_ALPHA_MULTIPLIER, DEFAULT_ALPHA_MULTIPLIER, 0.0), mfi_length_range=(DEFAULT_MFI_LENGTH, DEFAULT_MFI_LENGTH, 0), kernel=None))]
1604pub fn trend_flow_trail_batch_py<'py>(
1605    py: Python<'py>,
1606    open: PyReadonlyArray1<'py, f64>,
1607    high: PyReadonlyArray1<'py, f64>,
1608    low: PyReadonlyArray1<'py, f64>,
1609    close: PyReadonlyArray1<'py, f64>,
1610    volume: PyReadonlyArray1<'py, f64>,
1611    alpha_length_range: (usize, usize, usize),
1612    alpha_multiplier_range: (f64, f64, f64),
1613    mfi_length_range: (usize, usize, usize),
1614    kernel: Option<&str>,
1615) -> PyResult<Bound<'py, PyDict>> {
1616    let kernel = validate_kernel(kernel, true)?;
1617    let out = trend_flow_trail_batch_with_kernel(
1618        open.as_slice()?,
1619        high.as_slice()?,
1620        low.as_slice()?,
1621        close.as_slice()?,
1622        volume.as_slice()?,
1623        &TrendFlowTrailBatchRange {
1624            alpha_length: alpha_length_range,
1625            alpha_multiplier: alpha_multiplier_range,
1626            mfi_length: mfi_length_range,
1627        },
1628        kernel,
1629    )
1630    .map_err(|e| PyValueError::new_err(e.to_string()))?;
1631    let dict = PyDict::new(py);
1632    dict.set_item("rows", out.rows)?;
1633    dict.set_item("cols", out.cols)?;
1634    dict.set_item(
1635        "alpha_trail",
1636        out.alpha_trail
1637            .into_pyarray(py)
1638            .reshape((out.rows, out.cols))?,
1639    )?;
1640    dict.set_item(
1641        "alpha_trail_bullish",
1642        out.alpha_trail_bullish
1643            .into_pyarray(py)
1644            .reshape((out.rows, out.cols))?,
1645    )?;
1646    dict.set_item(
1647        "alpha_trail_bearish",
1648        out.alpha_trail_bearish
1649            .into_pyarray(py)
1650            .reshape((out.rows, out.cols))?,
1651    )?;
1652    dict.set_item(
1653        "alpha_dir",
1654        out.alpha_dir
1655            .into_pyarray(py)
1656            .reshape((out.rows, out.cols))?,
1657    )?;
1658    dict.set_item(
1659        "mfi",
1660        out.mfi.into_pyarray(py).reshape((out.rows, out.cols))?,
1661    )?;
1662    dict.set_item(
1663        "tp_upper",
1664        out.tp_upper
1665            .into_pyarray(py)
1666            .reshape((out.rows, out.cols))?,
1667    )?;
1668    dict.set_item(
1669        "tp_lower",
1670        out.tp_lower
1671            .into_pyarray(py)
1672            .reshape((out.rows, out.cols))?,
1673    )?;
1674    dict.set_item(
1675        "alpha_trail_bullish_switch",
1676        out.alpha_trail_bullish_switch
1677            .into_pyarray(py)
1678            .reshape((out.rows, out.cols))?,
1679    )?;
1680    dict.set_item(
1681        "alpha_trail_bearish_switch",
1682        out.alpha_trail_bearish_switch
1683            .into_pyarray(py)
1684            .reshape((out.rows, out.cols))?,
1685    )?;
1686    dict.set_item(
1687        "mfi_overbought",
1688        out.mfi_overbought
1689            .into_pyarray(py)
1690            .reshape((out.rows, out.cols))?,
1691    )?;
1692    dict.set_item(
1693        "mfi_oversold",
1694        out.mfi_oversold
1695            .into_pyarray(py)
1696            .reshape((out.rows, out.cols))?,
1697    )?;
1698    dict.set_item(
1699        "mfi_cross_up_mid",
1700        out.mfi_cross_up_mid
1701            .into_pyarray(py)
1702            .reshape((out.rows, out.cols))?,
1703    )?;
1704    dict.set_item(
1705        "mfi_cross_down_mid",
1706        out.mfi_cross_down_mid
1707            .into_pyarray(py)
1708            .reshape((out.rows, out.cols))?,
1709    )?;
1710    dict.set_item(
1711        "price_cross_alpha_trail_up",
1712        out.price_cross_alpha_trail_up
1713            .into_pyarray(py)
1714            .reshape((out.rows, out.cols))?,
1715    )?;
1716    dict.set_item(
1717        "price_cross_alpha_trail_down",
1718        out.price_cross_alpha_trail_down
1719            .into_pyarray(py)
1720            .reshape((out.rows, out.cols))?,
1721    )?;
1722    dict.set_item(
1723        "mfi_above_90",
1724        out.mfi_above_90
1725            .into_pyarray(py)
1726            .reshape((out.rows, out.cols))?,
1727    )?;
1728    dict.set_item(
1729        "mfi_below_10",
1730        out.mfi_below_10
1731            .into_pyarray(py)
1732            .reshape((out.rows, out.cols))?,
1733    )?;
1734    Ok(dict)
1735}
1736
1737#[cfg(feature = "python")]
1738#[pyclass(name = "TrendFlowTrailStream")]
1739pub struct TrendFlowTrailStreamPy {
1740    inner: TrendFlowTrailStream,
1741}
1742
1743#[cfg(feature = "python")]
1744#[pymethods]
1745impl TrendFlowTrailStreamPy {
1746    #[new]
1747    #[pyo3(signature = (alpha_length=None, alpha_multiplier=None, mfi_length=None))]
1748    pub fn new(
1749        alpha_length: Option<usize>,
1750        alpha_multiplier: Option<f64>,
1751        mfi_length: Option<usize>,
1752    ) -> PyResult<Self> {
1753        Ok(Self {
1754            inner: TrendFlowTrailStream::try_new(TrendFlowTrailParams {
1755                alpha_length,
1756                alpha_multiplier,
1757                mfi_length,
1758            })
1759            .map_err(|e| PyValueError::new_err(e.to_string()))?,
1760        })
1761    }
1762
1763    pub fn update(
1764        &mut self,
1765        open: f64,
1766        high: f64,
1767        low: f64,
1768        close: f64,
1769        volume: f64,
1770    ) -> Option<Vec<f64>> {
1771        self.inner
1772            .update(open, high, low, close, volume)
1773            .map(|row| {
1774                vec![
1775                    row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.8, row.9, row.10,
1776                    row.11, row.12, row.13, row.14, row.15, row.16,
1777                ]
1778            })
1779    }
1780}
1781
1782#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1783#[wasm_bindgen]
1784pub fn trend_flow_trail_js(
1785    open: &[f64],
1786    high: &[f64],
1787    low: &[f64],
1788    close: &[f64],
1789    volume: &[f64],
1790    alpha_length: usize,
1791    alpha_multiplier: f64,
1792    mfi_length: usize,
1793) -> Result<JsValue, JsValue> {
1794    let out = trend_flow_trail_with_kernel(
1795        &TrendFlowTrailInput::from_slices(
1796            open,
1797            high,
1798            low,
1799            close,
1800            volume,
1801            TrendFlowTrailParams {
1802                alpha_length: Some(alpha_length),
1803                alpha_multiplier: Some(alpha_multiplier),
1804                mfi_length: Some(mfi_length),
1805            },
1806        ),
1807        Kernel::Auto,
1808    )
1809    .map_err(|e| JsValue::from_str(&e.to_string()))?;
1810    serde_wasm_bindgen::to_value(&out).map_err(|e| JsValue::from_str(&e.to_string()))
1811}
1812
1813#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1814#[wasm_bindgen]
1815pub fn trend_flow_trail_alloc(len: usize) -> *mut f64 {
1816    let mut values = Vec::<f64>::with_capacity(len);
1817    let ptr = values.as_mut_ptr();
1818    std::mem::forget(values);
1819    ptr
1820}
1821
1822#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1823#[wasm_bindgen]
1824pub fn trend_flow_trail_free(ptr: *mut f64, len: usize) {
1825    if !ptr.is_null() {
1826        unsafe {
1827            let _ = Vec::from_raw_parts(ptr, len, len);
1828        }
1829    }
1830}
1831
1832#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1833#[wasm_bindgen]
1834#[allow(clippy::too_many_arguments)]
1835pub fn trend_flow_trail_into(
1836    open_ptr: *const f64,
1837    high_ptr: *const f64,
1838    low_ptr: *const f64,
1839    close_ptr: *const f64,
1840    volume_ptr: *const f64,
1841    alpha_trail_ptr: *mut f64,
1842    alpha_trail_bullish_ptr: *mut f64,
1843    alpha_trail_bearish_ptr: *mut f64,
1844    alpha_dir_ptr: *mut f64,
1845    mfi_ptr: *mut f64,
1846    tp_upper_ptr: *mut f64,
1847    tp_lower_ptr: *mut f64,
1848    alpha_trail_bullish_switch_ptr: *mut f64,
1849    alpha_trail_bearish_switch_ptr: *mut f64,
1850    mfi_overbought_ptr: *mut f64,
1851    mfi_oversold_ptr: *mut f64,
1852    mfi_cross_up_mid_ptr: *mut f64,
1853    mfi_cross_down_mid_ptr: *mut f64,
1854    price_cross_alpha_trail_up_ptr: *mut f64,
1855    price_cross_alpha_trail_down_ptr: *mut f64,
1856    mfi_above_90_ptr: *mut f64,
1857    mfi_below_10_ptr: *mut f64,
1858    len: usize,
1859    alpha_length: usize,
1860    alpha_multiplier: f64,
1861    mfi_length: usize,
1862) -> Result<(), JsValue> {
1863    unsafe {
1864        trend_flow_trail_into_slice(
1865            std::slice::from_raw_parts_mut(alpha_trail_ptr, len),
1866            std::slice::from_raw_parts_mut(alpha_trail_bullish_ptr, len),
1867            std::slice::from_raw_parts_mut(alpha_trail_bearish_ptr, len),
1868            std::slice::from_raw_parts_mut(alpha_dir_ptr, len),
1869            std::slice::from_raw_parts_mut(mfi_ptr, len),
1870            std::slice::from_raw_parts_mut(tp_upper_ptr, len),
1871            std::slice::from_raw_parts_mut(tp_lower_ptr, len),
1872            std::slice::from_raw_parts_mut(alpha_trail_bullish_switch_ptr, len),
1873            std::slice::from_raw_parts_mut(alpha_trail_bearish_switch_ptr, len),
1874            std::slice::from_raw_parts_mut(mfi_overbought_ptr, len),
1875            std::slice::from_raw_parts_mut(mfi_oversold_ptr, len),
1876            std::slice::from_raw_parts_mut(mfi_cross_up_mid_ptr, len),
1877            std::slice::from_raw_parts_mut(mfi_cross_down_mid_ptr, len),
1878            std::slice::from_raw_parts_mut(price_cross_alpha_trail_up_ptr, len),
1879            std::slice::from_raw_parts_mut(price_cross_alpha_trail_down_ptr, len),
1880            std::slice::from_raw_parts_mut(mfi_above_90_ptr, len),
1881            std::slice::from_raw_parts_mut(mfi_below_10_ptr, len),
1882            &TrendFlowTrailInput::from_slices(
1883                std::slice::from_raw_parts(open_ptr, len),
1884                std::slice::from_raw_parts(high_ptr, len),
1885                std::slice::from_raw_parts(low_ptr, len),
1886                std::slice::from_raw_parts(close_ptr, len),
1887                std::slice::from_raw_parts(volume_ptr, len),
1888                TrendFlowTrailParams {
1889                    alpha_length: Some(alpha_length),
1890                    alpha_multiplier: Some(alpha_multiplier),
1891                    mfi_length: Some(mfi_length),
1892                },
1893            ),
1894            Kernel::Auto,
1895        )
1896        .map_err(|e| JsValue::from_str(&e.to_string()))
1897    }
1898}
1899
1900#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1901#[derive(Serialize, Deserialize)]
1902pub struct TrendFlowTrailBatchConfig {
1903    pub alpha_length_range: (usize, usize, usize),
1904    pub alpha_multiplier_range: (f64, f64, f64),
1905    pub mfi_length_range: (usize, usize, usize),
1906}
1907
1908#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1909#[wasm_bindgen(js_name = trend_flow_trail_batch)]
1910pub fn trend_flow_trail_batch_unified_js(
1911    open: &[f64],
1912    high: &[f64],
1913    low: &[f64],
1914    close: &[f64],
1915    volume: &[f64],
1916    config: JsValue,
1917) -> Result<JsValue, JsValue> {
1918    let config: TrendFlowTrailBatchConfig =
1919        serde_wasm_bindgen::from_value(config).map_err(|e| JsValue::from_str(&e.to_string()))?;
1920    let out = trend_flow_trail_batch_with_kernel(
1921        open,
1922        high,
1923        low,
1924        close,
1925        volume,
1926        &TrendFlowTrailBatchRange {
1927            alpha_length: config.alpha_length_range,
1928            alpha_multiplier: config.alpha_multiplier_range,
1929            mfi_length: config.mfi_length_range,
1930        },
1931        Kernel::Auto,
1932    )
1933    .map_err(|e| JsValue::from_str(&e.to_string()))?;
1934    serde_wasm_bindgen::to_value(&out).map_err(|e| JsValue::from_str(&e.to_string()))
1935}
1936
1937#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1938#[wasm_bindgen]
1939pub struct TrendFlowTrailStreamWasm {
1940    inner: TrendFlowTrailStream,
1941}
1942
1943#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1944#[wasm_bindgen]
1945impl TrendFlowTrailStreamWasm {
1946    #[wasm_bindgen(constructor)]
1947    pub fn new(
1948        alpha_length: Option<usize>,
1949        alpha_multiplier: Option<f64>,
1950        mfi_length: Option<usize>,
1951    ) -> Result<Self, JsValue> {
1952        Ok(Self {
1953            inner: TrendFlowTrailStream::try_new(TrendFlowTrailParams {
1954                alpha_length,
1955                alpha_multiplier,
1956                mfi_length,
1957            })
1958            .map_err(|e| JsValue::from_str(&e.to_string()))?,
1959        })
1960    }
1961
1962    pub fn update(
1963        &mut self,
1964        open: f64,
1965        high: f64,
1966        low: f64,
1967        close: f64,
1968        volume: f64,
1969    ) -> Result<JsValue, JsValue> {
1970        let value = self
1971            .inner
1972            .update(open, high, low, close, volume)
1973            .map(|row| {
1974                vec![
1975                    row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.8, row.9, row.10,
1976                    row.11, row.12, row.13, row.14, row.15, row.16,
1977                ]
1978            });
1979        serde_wasm_bindgen::to_value(&value).map_err(|e| JsValue::from_str(&e.to_string()))
1980    }
1981}
1982
1983#[cfg(test)]
1984mod tests {
1985    use super::*;
1986
1987    fn sample_ohlcv() -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
1988        let mut open = Vec::with_capacity(180);
1989        let mut high = Vec::with_capacity(180);
1990        let mut low = Vec::with_capacity(180);
1991        let mut close = Vec::with_capacity(180);
1992        let mut volume = Vec::with_capacity(180);
1993        for i in 0..180 {
1994            let base = if i < 60 {
1995                100.0 + i as f64 * 0.35
1996            } else if i < 120 {
1997                121.0 - (i - 60) as f64 * 0.22
1998            } else {
1999                108.0 + (i - 120) as f64 * 0.42
2000            };
2001            let wiggle = ((i % 7) as f64 - 3.0) * 0.18;
2002            let c = base + wiggle;
2003            let o = c - ((i % 5) as f64 - 2.0) * 0.11;
2004            open.push(o);
2005            close.push(c);
2006            high.push(c.max(o) + 1.2 + (i % 3) as f64 * 0.07);
2007            low.push(c.min(o) - 1.1 - (i % 4) as f64 * 0.05);
2008            volume.push(1000.0 + (i % 9) as f64 * 37.0 + i as f64 * 4.0);
2009        }
2010        (open, high, low, close, volume)
2011    }
2012
2013    fn assert_series_eq(left: &[f64], right: &[f64]) {
2014        assert_eq!(left.len(), right.len());
2015        for (lhs, rhs) in left.iter().zip(right.iter()) {
2016            assert!(lhs == rhs || (lhs.is_nan() && rhs.is_nan()));
2017        }
2018    }
2019
2020    #[test]
2021    fn trend_flow_trail_outputs_present() -> Result<(), TrendFlowTrailError> {
2022        let (open, high, low, close, volume) = sample_ohlcv();
2023        let out = trend_flow_trail(&TrendFlowTrailInput::from_slices(
2024            &open,
2025            &high,
2026            &low,
2027            &close,
2028            &volume,
2029            TrendFlowTrailParams::default(),
2030        ))?;
2031        assert!(out.alpha_trail.iter().any(|v| v.is_finite()));
2032        assert!(out.mfi.iter().any(|v| v.is_finite()));
2033        Ok(())
2034    }
2035
2036    #[test]
2037    fn trend_flow_trail_stream_matches_api() -> Result<(), TrendFlowTrailError> {
2038        let (open, high, low, close, volume) = sample_ohlcv();
2039        let params = TrendFlowTrailParams::default();
2040        let out = trend_flow_trail(&TrendFlowTrailInput::from_slices(
2041            &open,
2042            &high,
2043            &low,
2044            &close,
2045            &volume,
2046            params.clone(),
2047        ))?;
2048        let mut stream = TrendFlowTrailStream::try_new(params)?;
2049        let mut alpha_trail = vec![f64::NAN; close.len()];
2050        for i in 0..close.len() {
2051            if let Some((trail, ..)) = stream.update(open[i], high[i], low[i], close[i], volume[i])
2052            {
2053                alpha_trail[i] = trail;
2054            }
2055        }
2056        assert_series_eq(&alpha_trail, &out.alpha_trail);
2057        Ok(())
2058    }
2059}