Skip to main content

vector_ta/indicators/
exponential_trend.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 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, make_uninit_matrix,
19};
20#[cfg(feature = "python")]
21use crate::utilities::kernel_validation::validate_kernel;
22#[cfg(not(target_arch = "wasm32"))]
23use rayon::prelude::*;
24#[cfg(test)]
25use std::error::Error as StdError;
26use std::mem::ManuallyDrop;
27use thiserror::Error;
28
29const DEFAULT_EXP_RATE: f64 = 0.00003;
30const DEFAULT_INITIAL_DISTANCE: f64 = 4.0;
31const DEFAULT_WIDTH_MULTIPLIER: f64 = 1.0;
32const ATR_LENGTH: usize = 14;
33const SEED_BAR_INDEX: usize = 100;
34const MAX_EXP_RATE: f64 = 0.5;
35const MIN_DISTANCE: f64 = 0.1;
36const MIN_WIDTH_MULTIPLIER: f64 = 0.1;
37const MAX_EXP_MULTIPLIER: f64 = 900.0;
38
39#[derive(Debug, Clone)]
40pub enum ExponentialTrendData<'a> {
41    Candles {
42        candles: &'a Candles,
43    },
44    Slices {
45        high: &'a [f64],
46        low: &'a [f64],
47        close: &'a [f64],
48    },
49}
50
51#[derive(Debug, Clone)]
52#[cfg_attr(
53    all(target_arch = "wasm32", feature = "wasm"),
54    derive(Serialize, Deserialize)
55)]
56pub struct ExponentialTrendOutput {
57    pub uptrend_base: Vec<f64>,
58    pub downtrend_base: Vec<f64>,
59    pub uptrend_extension: Vec<f64>,
60    pub downtrend_extension: Vec<f64>,
61    pub bullish_change: Vec<f64>,
62    pub bearish_change: Vec<f64>,
63}
64
65#[derive(Debug, Clone, PartialEq)]
66#[cfg_attr(
67    all(target_arch = "wasm32", feature = "wasm"),
68    derive(Serialize, Deserialize)
69)]
70pub struct ExponentialTrendParams {
71    pub exp_rate: Option<f64>,
72    pub initial_distance: Option<f64>,
73    pub width_multiplier: Option<f64>,
74}
75
76impl Default for ExponentialTrendParams {
77    fn default() -> Self {
78        Self {
79            exp_rate: Some(DEFAULT_EXP_RATE),
80            initial_distance: Some(DEFAULT_INITIAL_DISTANCE),
81            width_multiplier: Some(DEFAULT_WIDTH_MULTIPLIER),
82        }
83    }
84}
85
86#[derive(Debug, Clone)]
87pub struct ExponentialTrendInput<'a> {
88    pub data: ExponentialTrendData<'a>,
89    pub params: ExponentialTrendParams,
90}
91
92impl<'a> ExponentialTrendInput<'a> {
93    #[inline(always)]
94    pub fn from_candles(candles: &'a Candles, params: ExponentialTrendParams) -> Self {
95        Self {
96            data: ExponentialTrendData::Candles { candles },
97            params,
98        }
99    }
100
101    #[inline(always)]
102    pub fn from_slices(
103        high: &'a [f64],
104        low: &'a [f64],
105        close: &'a [f64],
106        params: ExponentialTrendParams,
107    ) -> Self {
108        Self {
109            data: ExponentialTrendData::Slices { high, low, close },
110            params,
111        }
112    }
113
114    #[inline(always)]
115    pub fn with_default_candles(candles: &'a Candles) -> Self {
116        Self::from_candles(candles, ExponentialTrendParams::default())
117    }
118
119    #[inline(always)]
120    pub fn get_exp_rate(&self) -> f64 {
121        self.params.exp_rate.unwrap_or(DEFAULT_EXP_RATE)
122    }
123
124    #[inline(always)]
125    pub fn get_initial_distance(&self) -> f64 {
126        self.params
127            .initial_distance
128            .unwrap_or(DEFAULT_INITIAL_DISTANCE)
129    }
130
131    #[inline(always)]
132    pub fn get_width_multiplier(&self) -> f64 {
133        self.params
134            .width_multiplier
135            .unwrap_or(DEFAULT_WIDTH_MULTIPLIER)
136    }
137
138    #[inline(always)]
139    fn as_hlc(&self) -> (&'a [f64], &'a [f64], &'a [f64]) {
140        match &self.data {
141            ExponentialTrendData::Candles { candles } => (
142                source_type(candles, "high"),
143                source_type(candles, "low"),
144                source_type(candles, "close"),
145            ),
146            ExponentialTrendData::Slices { high, low, close } => (*high, *low, *close),
147        }
148    }
149}
150
151impl<'a> AsRef<[f64]> for ExponentialTrendInput<'a> {
152    #[inline(always)]
153    fn as_ref(&self) -> &[f64] {
154        self.as_hlc().2
155    }
156}
157
158#[derive(Clone, Copy, Debug)]
159pub struct ExponentialTrendBuilder {
160    exp_rate: Option<f64>,
161    initial_distance: Option<f64>,
162    width_multiplier: Option<f64>,
163    kernel: Kernel,
164}
165
166impl Default for ExponentialTrendBuilder {
167    fn default() -> Self {
168        Self {
169            exp_rate: None,
170            initial_distance: None,
171            width_multiplier: None,
172            kernel: Kernel::Auto,
173        }
174    }
175}
176
177impl ExponentialTrendBuilder {
178    #[inline(always)]
179    pub fn new() -> Self {
180        Self::default()
181    }
182
183    #[inline(always)]
184    pub fn exp_rate(mut self, value: f64) -> Self {
185        self.exp_rate = Some(value);
186        self
187    }
188
189    #[inline(always)]
190    pub fn initial_distance(mut self, value: f64) -> Self {
191        self.initial_distance = Some(value);
192        self
193    }
194
195    #[inline(always)]
196    pub fn width_multiplier(mut self, value: f64) -> Self {
197        self.width_multiplier = Some(value);
198        self
199    }
200
201    #[inline(always)]
202    pub fn kernel(mut self, kernel: Kernel) -> Self {
203        self.kernel = kernel;
204        self
205    }
206
207    #[inline(always)]
208    fn params(self) -> ExponentialTrendParams {
209        ExponentialTrendParams {
210            exp_rate: self.exp_rate,
211            initial_distance: self.initial_distance,
212            width_multiplier: self.width_multiplier,
213        }
214    }
215
216    #[inline(always)]
217    pub fn apply(self, candles: &Candles) -> Result<ExponentialTrendOutput, ExponentialTrendError> {
218        let kernel = self.kernel;
219        let input = ExponentialTrendInput::from_candles(candles, self.params());
220        exponential_trend_with_kernel(&input, kernel)
221    }
222
223    #[inline(always)]
224    pub fn apply_slices(
225        self,
226        high: &[f64],
227        low: &[f64],
228        close: &[f64],
229    ) -> Result<ExponentialTrendOutput, ExponentialTrendError> {
230        let kernel = self.kernel;
231        let input = ExponentialTrendInput::from_slices(high, low, close, self.params());
232        exponential_trend_with_kernel(&input, kernel)
233    }
234
235    #[inline(always)]
236    pub fn into_stream(self) -> Result<ExponentialTrendStream, ExponentialTrendError> {
237        ExponentialTrendStream::try_new(self.params())
238    }
239}
240
241#[derive(Debug, Error)]
242pub enum ExponentialTrendError {
243    #[error("exponential_trend: input data slice is empty.")]
244    EmptyInputData,
245    #[error("exponential_trend: all values are NaN.")]
246    AllValuesNaN,
247    #[error(
248        "exponential_trend: inconsistent slice lengths: high={high_len}, low={low_len}, close={close_len}"
249    )]
250    InconsistentSliceLengths {
251        high_len: usize,
252        low_len: usize,
253        close_len: usize,
254    },
255    #[error("exponential_trend: invalid exp_rate: {exp_rate}")]
256    InvalidExpRate { exp_rate: f64 },
257    #[error("exponential_trend: invalid initial_distance: {initial_distance}")]
258    InvalidInitialDistance { initial_distance: f64 },
259    #[error("exponential_trend: invalid width_multiplier: {width_multiplier}")]
260    InvalidWidthMultiplier { width_multiplier: f64 },
261    #[error("exponential_trend: not enough valid data: needed = {needed}, valid = {valid}")]
262    NotEnoughValidData { needed: usize, valid: usize },
263    #[error("exponential_trend: output length mismatch: expected = {expected}, got = {got}")]
264    OutputLengthMismatch { expected: usize, got: usize },
265    #[error(
266        "exponential_trend: invalid range for {axis}: start = {start}, end = {end}, step = {step}"
267    )]
268    InvalidRange {
269        axis: &'static str,
270        start: String,
271        end: String,
272        step: String,
273    },
274    #[error("exponential_trend: invalid kernel for batch: {0:?}")]
275    InvalidKernelForBatch(Kernel),
276}
277
278#[derive(Clone, Copy, Debug)]
279struct PreparedExponentialTrend<'a> {
280    high: &'a [f64],
281    low: &'a [f64],
282    close: &'a [f64],
283    exp_rate: f64,
284    initial_distance: f64,
285    width_multiplier: f64,
286    warmup: usize,
287}
288
289#[derive(Clone, Debug)]
290struct AtrState {
291    len: usize,
292    count: usize,
293    sum: f64,
294    value: f64,
295    prev_close: f64,
296    have_prev_close: bool,
297}
298
299impl AtrState {
300    #[inline(always)]
301    fn new(len: usize) -> Self {
302        Self {
303            len,
304            count: 0,
305            sum: 0.0,
306            value: f64::NAN,
307            prev_close: f64::NAN,
308            have_prev_close: false,
309        }
310    }
311
312    #[inline(always)]
313    fn reset(&mut self) {
314        self.count = 0;
315        self.sum = 0.0;
316        self.value = f64::NAN;
317        self.prev_close = f64::NAN;
318        self.have_prev_close = false;
319    }
320
321    #[inline(always)]
322    fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
323        let prev_close = if self.have_prev_close {
324            self.prev_close
325        } else {
326            close
327        };
328        let tr = (high - low)
329            .max((high - prev_close).abs())
330            .max((low - prev_close).abs());
331        self.prev_close = close;
332        self.have_prev_close = true;
333
334        if self.count < self.len {
335            self.count += 1;
336            self.sum += tr;
337            if self.count < self.len {
338                return None;
339            }
340            self.value = self.sum / self.len as f64;
341            return Some(self.value);
342        }
343
344        self.value = ((self.value * (self.len - 1) as f64) + tr) / self.len as f64;
345        Some(self.value)
346    }
347}
348
349#[derive(Clone, Debug)]
350struct ExponentialTrendState {
351    atr_state: AtrState,
352    prev_upper_band: f64,
353    prev_lower_band: f64,
354    prev_close: f64,
355    prev_atr_ready: bool,
356    initial_line: f64,
357    prev_initial_line: f64,
358    trend: i32,
359    bars_since_change: usize,
360    segment_index: usize,
361}
362
363impl ExponentialTrendState {
364    #[inline(always)]
365    fn new() -> Self {
366        Self {
367            atr_state: AtrState::new(ATR_LENGTH),
368            prev_upper_band: f64::NAN,
369            prev_lower_band: f64::NAN,
370            prev_close: f64::NAN,
371            prev_atr_ready: false,
372            initial_line: 0.0,
373            prev_initial_line: 0.0,
374            trend: 0,
375            bars_since_change: 0,
376            segment_index: 0,
377        }
378    }
379
380    #[inline(always)]
381    fn reset(&mut self) {
382        self.atr_state.reset();
383        self.prev_upper_band = f64::NAN;
384        self.prev_lower_band = f64::NAN;
385        self.prev_close = f64::NAN;
386        self.prev_atr_ready = false;
387        self.initial_line = 0.0;
388        self.prev_initial_line = 0.0;
389        self.trend = 0;
390        self.bars_since_change = 0;
391        self.segment_index = 0;
392    }
393
394    #[inline(always)]
395    fn update(
396        &mut self,
397        high: f64,
398        low: f64,
399        close: f64,
400        exp_rate: f64,
401        initial_distance: f64,
402        width_multiplier: f64,
403    ) -> Option<(f64, f64, f64, f64, f64, f64)> {
404        if !high.is_finite() || !low.is_finite() || !close.is_finite() {
405            self.reset();
406            return None;
407        }
408
409        let atr = self.atr_state.update(high, low, close);
410        let atr_ready = atr.is_some();
411        let mut upper = f64::NAN;
412        let mut lower = f64::NAN;
413        let mut direction = 1i32;
414
415        if let Some(atr_value) = atr {
416            let src = (high + low) * 0.5;
417            let raw_upper = src + initial_distance * atr_value;
418            let raw_lower = src - initial_distance * atr_value;
419            let prev_lower = if self.prev_lower_band.is_finite() {
420                self.prev_lower_band
421            } else {
422                0.0
423            };
424            let prev_upper = if self.prev_upper_band.is_finite() {
425                self.prev_upper_band
426            } else {
427                0.0
428            };
429            let prev_close = if self.prev_close.is_finite() {
430                self.prev_close
431            } else {
432                close
433            };
434
435            lower = if raw_lower > prev_lower || prev_close < prev_lower {
436                raw_lower
437            } else {
438                prev_lower
439            };
440            upper = if raw_upper < prev_upper || prev_close > prev_upper {
441                raw_upper
442            } else {
443                prev_upper
444            };
445
446            direction = if !self.prev_atr_ready {
447                1
448            } else if close < lower {
449                1
450            } else {
451                -1
452            };
453        }
454
455        let prev_trend = self.trend;
456        let prev_initial = self.prev_initial_line;
457        let prev_close = self.prev_close;
458
459        if self.segment_index == SEED_BAR_INDEX && upper.is_finite() && lower.is_finite() {
460            if direction < 0 {
461                self.initial_line = lower;
462                self.trend = 1;
463            } else {
464                self.initial_line = upper;
465                self.trend = -1;
466            }
467        }
468
469        let crossover = self.initial_line.is_finite()
470            && prev_close.is_finite()
471            && prev_initial.is_finite()
472            && close > self.initial_line
473            && prev_close <= prev_initial;
474        let crossunder = self.initial_line.is_finite()
475            && prev_close.is_finite()
476            && prev_initial.is_finite()
477            && close < self.initial_line
478            && prev_close >= prev_initial;
479
480        if crossover && lower.is_finite() {
481            self.initial_line = lower;
482            self.trend = 1;
483        } else if crossunder && upper.is_finite() {
484            self.initial_line = upper;
485            self.trend = -1;
486        }
487
488        if self.trend != prev_trend {
489            self.bars_since_change = 0;
490        } else if self.trend != 0 {
491            self.bars_since_change = self.bars_since_change.saturating_add(1);
492        }
493
494        if self.trend != 0 {
495            let exp_multiplier = (1.0
496                + (self.trend as f64) * (1.0 - (-exp_rate * self.bars_since_change as f64).exp()))
497            .min(MAX_EXP_MULTIPLIER);
498            self.initial_line *= exp_multiplier;
499        }
500
501        let mut uptrend_base = f64::NAN;
502        let mut downtrend_base = f64::NAN;
503        let mut uptrend_extension = f64::NAN;
504        let mut downtrend_extension = f64::NAN;
505        let mut bullish_change = f64::NAN;
506        let mut bearish_change = f64::NAN;
507
508        if let Some(atr_value) = atr {
509            let extension = self.initial_line
510                + if self.trend > 0 {
511                    atr_value * width_multiplier
512                } else {
513                    -atr_value * width_multiplier
514                };
515
516            if self.trend == 1 {
517                uptrend_base = self.initial_line;
518                uptrend_extension = extension;
519            } else if self.trend == -1 {
520                downtrend_base = self.initial_line;
521                downtrend_extension = extension;
522            }
523
524            if crossover {
525                bullish_change = self.initial_line - atr_value;
526            }
527            if crossunder {
528                bearish_change = self.initial_line + atr_value;
529            }
530        }
531
532        self.prev_upper_band = upper;
533        self.prev_lower_band = lower;
534        self.prev_close = close;
535        self.prev_initial_line = self.initial_line;
536        self.prev_atr_ready = atr_ready;
537        self.segment_index = self.segment_index.saturating_add(1);
538
539        Some((
540            uptrend_base,
541            downtrend_base,
542            uptrend_extension,
543            downtrend_extension,
544            bullish_change,
545            bearish_change,
546        ))
547    }
548}
549
550#[derive(Clone, Debug)]
551pub struct ExponentialTrendStream {
552    params: ExponentialTrendParams,
553    state: ExponentialTrendState,
554}
555
556impl ExponentialTrendStream {
557    #[inline(always)]
558    pub fn try_new(params: ExponentialTrendParams) -> Result<Self, ExponentialTrendError> {
559        validate_params(
560            params.exp_rate.unwrap_or(DEFAULT_EXP_RATE),
561            params.initial_distance.unwrap_or(DEFAULT_INITIAL_DISTANCE),
562            params.width_multiplier.unwrap_or(DEFAULT_WIDTH_MULTIPLIER),
563            usize::MAX,
564        )?;
565        Ok(Self {
566            params,
567            state: ExponentialTrendState::new(),
568        })
569    }
570
571    #[inline(always)]
572    pub fn update(
573        &mut self,
574        high: f64,
575        low: f64,
576        close: f64,
577    ) -> Option<(f64, f64, f64, f64, f64, f64)> {
578        self.state.update(
579            high,
580            low,
581            close,
582            self.params.exp_rate.unwrap_or(DEFAULT_EXP_RATE),
583            self.params
584                .initial_distance
585                .unwrap_or(DEFAULT_INITIAL_DISTANCE),
586            self.params
587                .width_multiplier
588                .unwrap_or(DEFAULT_WIDTH_MULTIPLIER),
589        )
590    }
591
592    #[inline(always)]
593    pub fn reset(&mut self) {
594        self.state.reset();
595    }
596}
597
598#[inline(always)]
599fn required_valid_bars() -> usize {
600    SEED_BAR_INDEX + 1
601}
602
603#[inline(always)]
604fn validate_params(
605    exp_rate: f64,
606    initial_distance: f64,
607    width_multiplier: f64,
608    data_len: usize,
609) -> Result<(), ExponentialTrendError> {
610    if !exp_rate.is_finite() || !(0.0..=MAX_EXP_RATE).contains(&exp_rate) {
611        return Err(ExponentialTrendError::InvalidExpRate { exp_rate });
612    }
613    if !initial_distance.is_finite() || initial_distance < MIN_DISTANCE {
614        return Err(ExponentialTrendError::InvalidInitialDistance { initial_distance });
615    }
616    if !width_multiplier.is_finite() || width_multiplier < MIN_WIDTH_MULTIPLIER {
617        return Err(ExponentialTrendError::InvalidWidthMultiplier { width_multiplier });
618    }
619    if data_len != usize::MAX && data_len < required_valid_bars() {
620        return Err(ExponentialTrendError::NotEnoughValidData {
621            needed: required_valid_bars(),
622            valid: data_len,
623        });
624    }
625    Ok(())
626}
627
628fn analyze_valid_segments(
629    high: &[f64],
630    low: &[f64],
631    close: &[f64],
632) -> Result<(usize, usize), ExponentialTrendError> {
633    if high.is_empty() {
634        return Err(ExponentialTrendError::EmptyInputData);
635    }
636    if high.len() != low.len() || high.len() != close.len() {
637        return Err(ExponentialTrendError::InconsistentSliceLengths {
638            high_len: high.len(),
639            low_len: low.len(),
640            close_len: close.len(),
641        });
642    }
643
644    let mut valid = 0usize;
645    let mut run = 0usize;
646    let mut max_run = 0usize;
647
648    for i in 0..high.len() {
649        if high[i].is_finite() && low[i].is_finite() && close[i].is_finite() {
650            valid += 1;
651            run += 1;
652            max_run = max_run.max(run);
653        } else {
654            run = 0;
655        }
656    }
657
658    if valid == 0 {
659        return Err(ExponentialTrendError::AllValuesNaN);
660    }
661
662    Ok((valid, max_run))
663}
664
665fn prepare_input<'a>(
666    input: &'a ExponentialTrendInput<'a>,
667    kernel: Kernel,
668) -> Result<PreparedExponentialTrend<'a>, ExponentialTrendError> {
669    if matches!(kernel, Kernel::Auto) {
670        let _ = detect_best_kernel();
671    }
672
673    let (high, low, close) = input.as_hlc();
674    let exp_rate = input.get_exp_rate();
675    let initial_distance = input.get_initial_distance();
676    let width_multiplier = input.get_width_multiplier();
677    validate_params(exp_rate, initial_distance, width_multiplier, close.len())?;
678
679    let (_, max_run) = analyze_valid_segments(high, low, close)?;
680    if max_run < required_valid_bars() {
681        return Err(ExponentialTrendError::NotEnoughValidData {
682            needed: required_valid_bars(),
683            valid: max_run,
684        });
685    }
686
687    Ok(PreparedExponentialTrend {
688        high,
689        low,
690        close,
691        exp_rate,
692        initial_distance,
693        width_multiplier,
694        warmup: SEED_BAR_INDEX,
695    })
696}
697
698fn compute_row(
699    high: &[f64],
700    low: &[f64],
701    close: &[f64],
702    exp_rate: f64,
703    initial_distance: f64,
704    width_multiplier: f64,
705    uptrend_base_out: &mut [f64],
706    downtrend_base_out: &mut [f64],
707    uptrend_extension_out: &mut [f64],
708    downtrend_extension_out: &mut [f64],
709    bullish_change_out: &mut [f64],
710    bearish_change_out: &mut [f64],
711) -> Result<(), ExponentialTrendError> {
712    let expected = close.len();
713    for out in [
714        &mut *uptrend_base_out,
715        &mut *downtrend_base_out,
716        &mut *uptrend_extension_out,
717        &mut *downtrend_extension_out,
718        &mut *bullish_change_out,
719        &mut *bearish_change_out,
720    ] {
721        if out.len() != expected {
722            return Err(ExponentialTrendError::OutputLengthMismatch {
723                expected,
724                got: out.len(),
725            });
726        }
727    }
728
729    let mut state = ExponentialTrendState::new();
730    for i in 0..expected {
731        if let Some((ub, db, ue, de, bc, brc)) = state.update(
732            high[i],
733            low[i],
734            close[i],
735            exp_rate,
736            initial_distance,
737            width_multiplier,
738        ) {
739            uptrend_base_out[i] = ub;
740            downtrend_base_out[i] = db;
741            uptrend_extension_out[i] = ue;
742            downtrend_extension_out[i] = de;
743            bullish_change_out[i] = bc;
744            bearish_change_out[i] = brc;
745        } else {
746            uptrend_base_out[i] = f64::NAN;
747            downtrend_base_out[i] = f64::NAN;
748            uptrend_extension_out[i] = f64::NAN;
749            downtrend_extension_out[i] = f64::NAN;
750            bullish_change_out[i] = f64::NAN;
751            bearish_change_out[i] = f64::NAN;
752        }
753    }
754
755    Ok(())
756}
757
758#[inline]
759pub fn exponential_trend(
760    input: &ExponentialTrendInput,
761) -> Result<ExponentialTrendOutput, ExponentialTrendError> {
762    exponential_trend_with_kernel(input, Kernel::Auto)
763}
764
765pub fn exponential_trend_with_kernel(
766    input: &ExponentialTrendInput,
767    kernel: Kernel,
768) -> Result<ExponentialTrendOutput, ExponentialTrendError> {
769    let prepared = prepare_input(input, kernel)?;
770    let len = prepared.close.len();
771    let warmup = prepared.warmup;
772    let mut uptrend_base = alloc_with_nan_prefix(len, warmup);
773    let mut downtrend_base = alloc_with_nan_prefix(len, warmup);
774    let mut uptrend_extension = alloc_with_nan_prefix(len, warmup);
775    let mut downtrend_extension = alloc_with_nan_prefix(len, warmup);
776    let mut bullish_change = alloc_with_nan_prefix(len, warmup);
777    let mut bearish_change = alloc_with_nan_prefix(len, warmup);
778
779    compute_row(
780        prepared.high,
781        prepared.low,
782        prepared.close,
783        prepared.exp_rate,
784        prepared.initial_distance,
785        prepared.width_multiplier,
786        &mut uptrend_base,
787        &mut downtrend_base,
788        &mut uptrend_extension,
789        &mut downtrend_extension,
790        &mut bullish_change,
791        &mut bearish_change,
792    )?;
793
794    Ok(ExponentialTrendOutput {
795        uptrend_base,
796        downtrend_base,
797        uptrend_extension,
798        downtrend_extension,
799        bullish_change,
800        bearish_change,
801    })
802}
803
804#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
805pub fn exponential_trend_into(
806    uptrend_base_out: &mut [f64],
807    downtrend_base_out: &mut [f64],
808    uptrend_extension_out: &mut [f64],
809    downtrend_extension_out: &mut [f64],
810    bullish_change_out: &mut [f64],
811    bearish_change_out: &mut [f64],
812    input: &ExponentialTrendInput,
813) -> Result<(), ExponentialTrendError> {
814    exponential_trend_into_slice(
815        uptrend_base_out,
816        downtrend_base_out,
817        uptrend_extension_out,
818        downtrend_extension_out,
819        bullish_change_out,
820        bearish_change_out,
821        input,
822        Kernel::Auto,
823    )
824}
825
826pub fn exponential_trend_into_slice(
827    uptrend_base_out: &mut [f64],
828    downtrend_base_out: &mut [f64],
829    uptrend_extension_out: &mut [f64],
830    downtrend_extension_out: &mut [f64],
831    bullish_change_out: &mut [f64],
832    bearish_change_out: &mut [f64],
833    input: &ExponentialTrendInput,
834    kernel: Kernel,
835) -> Result<(), ExponentialTrendError> {
836    let prepared = prepare_input(input, kernel)?;
837    compute_row(
838        prepared.high,
839        prepared.low,
840        prepared.close,
841        prepared.exp_rate,
842        prepared.initial_distance,
843        prepared.width_multiplier,
844        uptrend_base_out,
845        downtrend_base_out,
846        uptrend_extension_out,
847        downtrend_extension_out,
848        bullish_change_out,
849        bearish_change_out,
850    )
851}
852
853#[derive(Clone, Debug)]
854pub struct ExponentialTrendBatchRange {
855    pub exp_rate: (f64, f64, f64),
856    pub initial_distance: (f64, f64, f64),
857    pub width_multiplier: (f64, f64, f64),
858}
859
860impl Default for ExponentialTrendBatchRange {
861    fn default() -> Self {
862        Self {
863            exp_rate: (DEFAULT_EXP_RATE, DEFAULT_EXP_RATE, 0.0),
864            initial_distance: (DEFAULT_INITIAL_DISTANCE, DEFAULT_INITIAL_DISTANCE, 0.0),
865            width_multiplier: (DEFAULT_WIDTH_MULTIPLIER, DEFAULT_WIDTH_MULTIPLIER, 0.0),
866        }
867    }
868}
869
870#[derive(Clone, Debug, Default)]
871pub struct ExponentialTrendBatchBuilder {
872    range: ExponentialTrendBatchRange,
873    kernel: Kernel,
874}
875
876impl ExponentialTrendBatchBuilder {
877    #[inline(always)]
878    pub fn new() -> Self {
879        Self::default()
880    }
881
882    #[inline(always)]
883    pub fn kernel(mut self, kernel: Kernel) -> Self {
884        self.kernel = kernel;
885        self
886    }
887
888    #[inline(always)]
889    pub fn exp_rate_range(mut self, start: f64, end: f64, step: f64) -> Self {
890        self.range.exp_rate = (start, end, step);
891        self
892    }
893
894    #[inline(always)]
895    pub fn initial_distance_range(mut self, start: f64, end: f64, step: f64) -> Self {
896        self.range.initial_distance = (start, end, step);
897        self
898    }
899
900    #[inline(always)]
901    pub fn width_multiplier_range(mut self, start: f64, end: f64, step: f64) -> Self {
902        self.range.width_multiplier = (start, end, step);
903        self
904    }
905
906    #[inline(always)]
907    pub fn apply(
908        self,
909        candles: &Candles,
910    ) -> Result<ExponentialTrendBatchOutput, ExponentialTrendError> {
911        exponential_trend_batch_with_kernel(
912            source_type(candles, "high"),
913            source_type(candles, "low"),
914            source_type(candles, "close"),
915            &self.range,
916            self.kernel,
917        )
918    }
919
920    #[inline(always)]
921    pub fn apply_slices(
922        self,
923        high: &[f64],
924        low: &[f64],
925        close: &[f64],
926    ) -> Result<ExponentialTrendBatchOutput, ExponentialTrendError> {
927        exponential_trend_batch_with_kernel(high, low, close, &self.range, self.kernel)
928    }
929}
930
931#[derive(Debug, Clone)]
932#[cfg_attr(
933    all(target_arch = "wasm32", feature = "wasm"),
934    derive(Serialize, Deserialize)
935)]
936pub struct ExponentialTrendBatchOutput {
937    pub uptrend_base: Vec<f64>,
938    pub downtrend_base: Vec<f64>,
939    pub uptrend_extension: Vec<f64>,
940    pub downtrend_extension: Vec<f64>,
941    pub bullish_change: Vec<f64>,
942    pub bearish_change: Vec<f64>,
943    pub combos: Vec<ExponentialTrendParams>,
944    pub rows: usize,
945    pub cols: usize,
946}
947
948#[inline(always)]
949fn axis_f64(
950    axis: &'static str,
951    (start, end, step): (f64, f64, f64),
952) -> Result<Vec<f64>, ExponentialTrendError> {
953    if !start.is_finite() || !end.is_finite() || !step.is_finite() {
954        return Err(ExponentialTrendError::InvalidRange {
955            axis,
956            start: start.to_string(),
957            end: end.to_string(),
958            step: step.to_string(),
959        });
960    }
961    if (start - end).abs() <= f64::EPSILON || step == 0.0 {
962        return Ok(vec![start]);
963    }
964    if step < 0.0 {
965        return Err(ExponentialTrendError::InvalidRange {
966            axis,
967            start: start.to_string(),
968            end: end.to_string(),
969            step: step.to_string(),
970        });
971    }
972
973    let mut out = Vec::new();
974    let eps = step.abs() * 1e-9 + 1e-12;
975    if start < end {
976        let mut value = start;
977        while value <= end + eps {
978            out.push(value.min(end));
979            value += step;
980        }
981    } else {
982        let mut value = start;
983        while value >= end - eps {
984            out.push(value.max(end));
985            value -= step;
986        }
987    }
988
989    if out.is_empty() || (out.last().copied().unwrap_or(start) - end).abs() > eps {
990        return Err(ExponentialTrendError::InvalidRange {
991            axis,
992            start: start.to_string(),
993            end: end.to_string(),
994            step: step.to_string(),
995        });
996    }
997
998    Ok(out)
999}
1000
1001pub fn expand_grid_exponential_trend(
1002    sweep: &ExponentialTrendBatchRange,
1003) -> Result<Vec<ExponentialTrendParams>, ExponentialTrendError> {
1004    let exp_rates = axis_f64("exp_rate", sweep.exp_rate)?;
1005    let initial_distances = axis_f64("initial_distance", sweep.initial_distance)?;
1006    let width_multipliers = axis_f64("width_multiplier", sweep.width_multiplier)?;
1007    let mut out =
1008        Vec::with_capacity(exp_rates.len() * initial_distances.len() * width_multipliers.len());
1009    for &exp_rate in &exp_rates {
1010        for &initial_distance in &initial_distances {
1011            for &width_multiplier in &width_multipliers {
1012                out.push(ExponentialTrendParams {
1013                    exp_rate: Some(exp_rate),
1014                    initial_distance: Some(initial_distance),
1015                    width_multiplier: Some(width_multiplier),
1016                });
1017            }
1018        }
1019    }
1020    Ok(out)
1021}
1022
1023fn batch_inner_into(
1024    high: &[f64],
1025    low: &[f64],
1026    close: &[f64],
1027    sweep: &ExponentialTrendBatchRange,
1028    parallel: bool,
1029    uptrend_base_out: &mut [f64],
1030    downtrend_base_out: &mut [f64],
1031    uptrend_extension_out: &mut [f64],
1032    downtrend_extension_out: &mut [f64],
1033    bullish_change_out: &mut [f64],
1034    bearish_change_out: &mut [f64],
1035) -> Result<Vec<ExponentialTrendParams>, ExponentialTrendError> {
1036    let (_, max_run) = analyze_valid_segments(high, low, close)?;
1037    if max_run < required_valid_bars() {
1038        return Err(ExponentialTrendError::NotEnoughValidData {
1039            needed: required_valid_bars(),
1040            valid: max_run,
1041        });
1042    }
1043
1044    let combos = expand_grid_exponential_trend(sweep)?;
1045    let rows = combos.len();
1046    let cols = close.len();
1047    let expected = rows * cols;
1048
1049    for out in [
1050        &mut *uptrend_base_out,
1051        &mut *downtrend_base_out,
1052        &mut *uptrend_extension_out,
1053        &mut *downtrend_extension_out,
1054        &mut *bullish_change_out,
1055        &mut *bearish_change_out,
1056    ] {
1057        if out.len() != expected {
1058            return Err(ExponentialTrendError::OutputLengthMismatch {
1059                expected,
1060                got: out.len(),
1061            });
1062        }
1063    }
1064
1065    for params in &combos {
1066        validate_params(
1067            params.exp_rate.unwrap_or(DEFAULT_EXP_RATE),
1068            params.initial_distance.unwrap_or(DEFAULT_INITIAL_DISTANCE),
1069            params.width_multiplier.unwrap_or(DEFAULT_WIDTH_MULTIPLIER),
1070            usize::MAX,
1071        )?;
1072    }
1073
1074    let do_row = |row: usize,
1075                  uptrend_base_row: &mut [f64],
1076                  downtrend_base_row: &mut [f64],
1077                  uptrend_extension_row: &mut [f64],
1078                  downtrend_extension_row: &mut [f64],
1079                  bullish_change_row: &mut [f64],
1080                  bearish_change_row: &mut [f64]| {
1081        let params = &combos[row];
1082        compute_row(
1083            high,
1084            low,
1085            close,
1086            params.exp_rate.unwrap_or(DEFAULT_EXP_RATE),
1087            params.initial_distance.unwrap_or(DEFAULT_INITIAL_DISTANCE),
1088            params.width_multiplier.unwrap_or(DEFAULT_WIDTH_MULTIPLIER),
1089            uptrend_base_row,
1090            downtrend_base_row,
1091            uptrend_extension_row,
1092            downtrend_extension_row,
1093            bullish_change_row,
1094            bearish_change_row,
1095        )
1096    };
1097
1098    if parallel {
1099        #[cfg(not(target_arch = "wasm32"))]
1100        {
1101            uptrend_base_out
1102                .par_chunks_mut(cols)
1103                .zip(downtrend_base_out.par_chunks_mut(cols))
1104                .zip(uptrend_extension_out.par_chunks_mut(cols))
1105                .zip(downtrend_extension_out.par_chunks_mut(cols))
1106                .zip(bullish_change_out.par_chunks_mut(cols))
1107                .zip(bearish_change_out.par_chunks_mut(cols))
1108                .enumerate()
1109                .try_for_each(
1110                    |(
1111                        row,
1112                        (
1113                            (
1114                                (
1115                                    ((uptrend_base_row, downtrend_base_row), uptrend_extension_row),
1116                                    downtrend_extension_row,
1117                                ),
1118                                bullish_change_row,
1119                            ),
1120                            bearish_change_row,
1121                        ),
1122                    )| {
1123                        do_row(
1124                            row,
1125                            uptrend_base_row,
1126                            downtrend_base_row,
1127                            uptrend_extension_row,
1128                            downtrend_extension_row,
1129                            bullish_change_row,
1130                            bearish_change_row,
1131                        )
1132                    },
1133                )?;
1134        }
1135        #[cfg(target_arch = "wasm32")]
1136        {
1137            for row in 0..rows {
1138                let start = row * cols;
1139                let end = start + cols;
1140                do_row(
1141                    row,
1142                    &mut uptrend_base_out[start..end],
1143                    &mut downtrend_base_out[start..end],
1144                    &mut uptrend_extension_out[start..end],
1145                    &mut downtrend_extension_out[start..end],
1146                    &mut bullish_change_out[start..end],
1147                    &mut bearish_change_out[start..end],
1148                )?;
1149            }
1150        }
1151    } else {
1152        for row in 0..rows {
1153            let start = row * cols;
1154            let end = start + cols;
1155            do_row(
1156                row,
1157                &mut uptrend_base_out[start..end],
1158                &mut downtrend_base_out[start..end],
1159                &mut uptrend_extension_out[start..end],
1160                &mut downtrend_extension_out[start..end],
1161                &mut bullish_change_out[start..end],
1162                &mut bearish_change_out[start..end],
1163            )?;
1164        }
1165    }
1166
1167    Ok(combos)
1168}
1169
1170pub fn exponential_trend_batch_with_kernel(
1171    high: &[f64],
1172    low: &[f64],
1173    close: &[f64],
1174    sweep: &ExponentialTrendBatchRange,
1175    kernel: Kernel,
1176) -> Result<ExponentialTrendBatchOutput, ExponentialTrendError> {
1177    match kernel {
1178        Kernel::Auto => {
1179            let _ = detect_best_batch_kernel();
1180        }
1181        k if !k.is_batch() => return Err(ExponentialTrendError::InvalidKernelForBatch(k)),
1182        _ => {}
1183    }
1184
1185    let rows = expand_grid_exponential_trend(sweep)?.len();
1186    let cols = close.len();
1187    let mut uptrend_base_guard = ManuallyDrop::new(make_uninit_matrix(rows, cols));
1188    let mut downtrend_base_guard = ManuallyDrop::new(make_uninit_matrix(rows, cols));
1189    let mut uptrend_extension_guard = ManuallyDrop::new(make_uninit_matrix(rows, cols));
1190    let mut downtrend_extension_guard = ManuallyDrop::new(make_uninit_matrix(rows, cols));
1191    let mut bullish_change_guard = ManuallyDrop::new(make_uninit_matrix(rows, cols));
1192    let mut bearish_change_guard = ManuallyDrop::new(make_uninit_matrix(rows, cols));
1193
1194    let uptrend_base: &mut [f64] = unsafe {
1195        core::slice::from_raw_parts_mut(
1196            uptrend_base_guard.as_mut_ptr() as *mut f64,
1197            uptrend_base_guard.len(),
1198        )
1199    };
1200    let downtrend_base: &mut [f64] = unsafe {
1201        core::slice::from_raw_parts_mut(
1202            downtrend_base_guard.as_mut_ptr() as *mut f64,
1203            downtrend_base_guard.len(),
1204        )
1205    };
1206    let uptrend_extension: &mut [f64] = unsafe {
1207        core::slice::from_raw_parts_mut(
1208            uptrend_extension_guard.as_mut_ptr() as *mut f64,
1209            uptrend_extension_guard.len(),
1210        )
1211    };
1212    let downtrend_extension: &mut [f64] = unsafe {
1213        core::slice::from_raw_parts_mut(
1214            downtrend_extension_guard.as_mut_ptr() as *mut f64,
1215            downtrend_extension_guard.len(),
1216        )
1217    };
1218    let bullish_change: &mut [f64] = unsafe {
1219        core::slice::from_raw_parts_mut(
1220            bullish_change_guard.as_mut_ptr() as *mut f64,
1221            bullish_change_guard.len(),
1222        )
1223    };
1224    let bearish_change: &mut [f64] = unsafe {
1225        core::slice::from_raw_parts_mut(
1226            bearish_change_guard.as_mut_ptr() as *mut f64,
1227            bearish_change_guard.len(),
1228        )
1229    };
1230
1231    let combos = batch_inner_into(
1232        high,
1233        low,
1234        close,
1235        sweep,
1236        !cfg!(target_arch = "wasm32"),
1237        uptrend_base,
1238        downtrend_base,
1239        uptrend_extension,
1240        downtrend_extension,
1241        bullish_change,
1242        bearish_change,
1243    )?;
1244
1245    Ok(ExponentialTrendBatchOutput {
1246        uptrend_base: unsafe {
1247            Vec::from_raw_parts(
1248                uptrend_base_guard.as_mut_ptr() as *mut f64,
1249                uptrend_base_guard.len(),
1250                uptrend_base_guard.capacity(),
1251            )
1252        },
1253        downtrend_base: unsafe {
1254            Vec::from_raw_parts(
1255                downtrend_base_guard.as_mut_ptr() as *mut f64,
1256                downtrend_base_guard.len(),
1257                downtrend_base_guard.capacity(),
1258            )
1259        },
1260        uptrend_extension: unsafe {
1261            Vec::from_raw_parts(
1262                uptrend_extension_guard.as_mut_ptr() as *mut f64,
1263                uptrend_extension_guard.len(),
1264                uptrend_extension_guard.capacity(),
1265            )
1266        },
1267        downtrend_extension: unsafe {
1268            Vec::from_raw_parts(
1269                downtrend_extension_guard.as_mut_ptr() as *mut f64,
1270                downtrend_extension_guard.len(),
1271                downtrend_extension_guard.capacity(),
1272            )
1273        },
1274        bullish_change: unsafe {
1275            Vec::from_raw_parts(
1276                bullish_change_guard.as_mut_ptr() as *mut f64,
1277                bullish_change_guard.len(),
1278                bullish_change_guard.capacity(),
1279            )
1280        },
1281        bearish_change: unsafe {
1282            Vec::from_raw_parts(
1283                bearish_change_guard.as_mut_ptr() as *mut f64,
1284                bearish_change_guard.len(),
1285                bearish_change_guard.capacity(),
1286            )
1287        },
1288        combos,
1289        rows,
1290        cols,
1291    })
1292}
1293
1294pub fn exponential_trend_batch_slice(
1295    high: &[f64],
1296    low: &[f64],
1297    close: &[f64],
1298    sweep: &ExponentialTrendBatchRange,
1299    kernel: Kernel,
1300) -> Result<ExponentialTrendBatchOutput, ExponentialTrendError> {
1301    exponential_trend_batch_with_kernel(high, low, close, sweep, kernel)
1302}
1303
1304pub fn exponential_trend_batch_par_slice(
1305    high: &[f64],
1306    low: &[f64],
1307    close: &[f64],
1308    sweep: &ExponentialTrendBatchRange,
1309    kernel: Kernel,
1310) -> Result<ExponentialTrendBatchOutput, ExponentialTrendError> {
1311    exponential_trend_batch_with_kernel(high, low, close, sweep, kernel)
1312}
1313
1314#[cfg(feature = "python")]
1315#[pyfunction(name = "exponential_trend")]
1316#[pyo3(signature = (high, low, close, exp_rate=DEFAULT_EXP_RATE, initial_distance=DEFAULT_INITIAL_DISTANCE, width_multiplier=DEFAULT_WIDTH_MULTIPLIER, kernel=None))]
1317pub fn exponential_trend_py<'py>(
1318    py: Python<'py>,
1319    high: PyReadonlyArray1<'py, f64>,
1320    low: PyReadonlyArray1<'py, f64>,
1321    close: PyReadonlyArray1<'py, f64>,
1322    exp_rate: f64,
1323    initial_distance: f64,
1324    width_multiplier: f64,
1325    kernel: Option<&str>,
1326) -> PyResult<Bound<'py, PyDict>> {
1327    let kernel = validate_kernel(kernel, false)?;
1328    let input = ExponentialTrendInput::from_slices(
1329        high.as_slice()?,
1330        low.as_slice()?,
1331        close.as_slice()?,
1332        ExponentialTrendParams {
1333            exp_rate: Some(exp_rate),
1334            initial_distance: Some(initial_distance),
1335            width_multiplier: Some(width_multiplier),
1336        },
1337    );
1338    let out = py
1339        .allow_threads(|| exponential_trend_with_kernel(&input, kernel))
1340        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1341    let dict = PyDict::new(py);
1342    dict.set_item("uptrend_base", out.uptrend_base.into_pyarray(py))?;
1343    dict.set_item("downtrend_base", out.downtrend_base.into_pyarray(py))?;
1344    dict.set_item("uptrend_extension", out.uptrend_extension.into_pyarray(py))?;
1345    dict.set_item(
1346        "downtrend_extension",
1347        out.downtrend_extension.into_pyarray(py),
1348    )?;
1349    dict.set_item("bullish_change", out.bullish_change.into_pyarray(py))?;
1350    dict.set_item("bearish_change", out.bearish_change.into_pyarray(py))?;
1351    Ok(dict)
1352}
1353
1354#[cfg(feature = "python")]
1355#[pyfunction(name = "exponential_trend_batch")]
1356#[pyo3(signature = (high, low, close, exp_rate_range=(DEFAULT_EXP_RATE, DEFAULT_EXP_RATE, 0.0), initial_distance_range=(DEFAULT_INITIAL_DISTANCE, DEFAULT_INITIAL_DISTANCE, 0.0), width_multiplier_range=(DEFAULT_WIDTH_MULTIPLIER, DEFAULT_WIDTH_MULTIPLIER, 0.0), kernel=None))]
1357pub fn exponential_trend_batch_py<'py>(
1358    py: Python<'py>,
1359    high: PyReadonlyArray1<'py, f64>,
1360    low: PyReadonlyArray1<'py, f64>,
1361    close: PyReadonlyArray1<'py, f64>,
1362    exp_rate_range: (f64, f64, f64),
1363    initial_distance_range: (f64, f64, f64),
1364    width_multiplier_range: (f64, f64, f64),
1365    kernel: Option<&str>,
1366) -> PyResult<Bound<'py, PyDict>> {
1367    let kernel = validate_kernel(kernel, true)?;
1368    let out = exponential_trend_batch_with_kernel(
1369        high.as_slice()?,
1370        low.as_slice()?,
1371        close.as_slice()?,
1372        &ExponentialTrendBatchRange {
1373            exp_rate: exp_rate_range,
1374            initial_distance: initial_distance_range,
1375            width_multiplier: width_multiplier_range,
1376        },
1377        kernel,
1378    )
1379    .map_err(|e| PyValueError::new_err(e.to_string()))?;
1380    let dict = PyDict::new(py);
1381    dict.set_item(
1382        "uptrend_base",
1383        out.uptrend_base
1384            .into_pyarray(py)
1385            .reshape((out.rows, out.cols))?,
1386    )?;
1387    dict.set_item(
1388        "downtrend_base",
1389        out.downtrend_base
1390            .into_pyarray(py)
1391            .reshape((out.rows, out.cols))?,
1392    )?;
1393    dict.set_item(
1394        "uptrend_extension",
1395        out.uptrend_extension
1396            .into_pyarray(py)
1397            .reshape((out.rows, out.cols))?,
1398    )?;
1399    dict.set_item(
1400        "downtrend_extension",
1401        out.downtrend_extension
1402            .into_pyarray(py)
1403            .reshape((out.rows, out.cols))?,
1404    )?;
1405    dict.set_item(
1406        "bullish_change",
1407        out.bullish_change
1408            .into_pyarray(py)
1409            .reshape((out.rows, out.cols))?,
1410    )?;
1411    dict.set_item(
1412        "bearish_change",
1413        out.bearish_change
1414            .into_pyarray(py)
1415            .reshape((out.rows, out.cols))?,
1416    )?;
1417    dict.set_item(
1418        "exp_rates",
1419        out.combos
1420            .iter()
1421            .map(|combo| combo.exp_rate.unwrap_or(DEFAULT_EXP_RATE))
1422            .collect::<Vec<_>>()
1423            .into_pyarray(py),
1424    )?;
1425    dict.set_item(
1426        "initial_distances",
1427        out.combos
1428            .iter()
1429            .map(|combo| combo.initial_distance.unwrap_or(DEFAULT_INITIAL_DISTANCE))
1430            .collect::<Vec<_>>()
1431            .into_pyarray(py),
1432    )?;
1433    dict.set_item(
1434        "width_multipliers",
1435        out.combos
1436            .iter()
1437            .map(|combo| combo.width_multiplier.unwrap_or(DEFAULT_WIDTH_MULTIPLIER))
1438            .collect::<Vec<_>>()
1439            .into_pyarray(py),
1440    )?;
1441    dict.set_item("rows", out.rows)?;
1442    dict.set_item("cols", out.cols)?;
1443    Ok(dict)
1444}
1445
1446#[cfg(feature = "python")]
1447#[pyclass(name = "ExponentialTrendStream")]
1448pub struct ExponentialTrendStreamPy {
1449    inner: ExponentialTrendStream,
1450}
1451
1452#[cfg(feature = "python")]
1453#[pymethods]
1454impl ExponentialTrendStreamPy {
1455    #[new]
1456    #[pyo3(signature = (exp_rate=None, initial_distance=None, width_multiplier=None))]
1457    pub fn new(
1458        exp_rate: Option<f64>,
1459        initial_distance: Option<f64>,
1460        width_multiplier: Option<f64>,
1461    ) -> PyResult<Self> {
1462        let inner = ExponentialTrendStream::try_new(ExponentialTrendParams {
1463            exp_rate,
1464            initial_distance,
1465            width_multiplier,
1466        })
1467        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1468        Ok(Self { inner })
1469    }
1470
1471    pub fn update(
1472        &mut self,
1473        high: f64,
1474        low: f64,
1475        close: f64,
1476    ) -> Option<(f64, f64, f64, f64, f64, f64)> {
1477        self.inner.update(high, low, close)
1478    }
1479}
1480
1481#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1482#[derive(Serialize, Deserialize)]
1483pub struct ExponentialTrendBatchConfig {
1484    pub exp_rate_range: (f64, f64, f64),
1485    pub initial_distance_range: (f64, f64, f64),
1486    pub width_multiplier_range: (f64, f64, f64),
1487}
1488
1489#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1490#[wasm_bindgen]
1491pub fn exponential_trend_js(
1492    high: &[f64],
1493    low: &[f64],
1494    close: &[f64],
1495    exp_rate: f64,
1496    initial_distance: f64,
1497    width_multiplier: f64,
1498) -> Result<JsValue, JsValue> {
1499    let input = ExponentialTrendInput::from_slices(
1500        high,
1501        low,
1502        close,
1503        ExponentialTrendParams {
1504            exp_rate: Some(exp_rate),
1505            initial_distance: Some(initial_distance),
1506            width_multiplier: Some(width_multiplier),
1507        },
1508    );
1509    let out = exponential_trend_with_kernel(&input, Kernel::Auto)
1510        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1511    serde_wasm_bindgen::to_value(&out).map_err(|e| JsValue::from_str(&e.to_string()))
1512}
1513
1514#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1515#[wasm_bindgen]
1516pub fn exponential_trend_alloc(len: usize) -> *mut f64 {
1517    let mut values = Vec::<f64>::with_capacity(len);
1518    let ptr = values.as_mut_ptr();
1519    std::mem::forget(values);
1520    ptr
1521}
1522
1523#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1524#[wasm_bindgen]
1525pub fn exponential_trend_free(ptr: *mut f64, len: usize) {
1526    if !ptr.is_null() {
1527        unsafe {
1528            let _ = Vec::from_raw_parts(ptr, len, len);
1529        }
1530    }
1531}
1532
1533#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1534#[wasm_bindgen]
1535pub fn exponential_trend_into(
1536    high_ptr: *const f64,
1537    low_ptr: *const f64,
1538    close_ptr: *const f64,
1539    uptrend_base_ptr: *mut f64,
1540    downtrend_base_ptr: *mut f64,
1541    uptrend_extension_ptr: *mut f64,
1542    downtrend_extension_ptr: *mut f64,
1543    bullish_change_ptr: *mut f64,
1544    bearish_change_ptr: *mut f64,
1545    len: usize,
1546    exp_rate: f64,
1547    initial_distance: f64,
1548    width_multiplier: f64,
1549) -> Result<(), JsValue> {
1550    if high_ptr.is_null()
1551        || low_ptr.is_null()
1552        || close_ptr.is_null()
1553        || uptrend_base_ptr.is_null()
1554        || downtrend_base_ptr.is_null()
1555        || uptrend_extension_ptr.is_null()
1556        || downtrend_extension_ptr.is_null()
1557        || bullish_change_ptr.is_null()
1558        || bearish_change_ptr.is_null()
1559    {
1560        return Err(JsValue::from_str("Null pointer provided"));
1561    }
1562
1563    unsafe {
1564        let input = ExponentialTrendInput::from_slices(
1565            std::slice::from_raw_parts(high_ptr, len),
1566            std::slice::from_raw_parts(low_ptr, len),
1567            std::slice::from_raw_parts(close_ptr, len),
1568            ExponentialTrendParams {
1569                exp_rate: Some(exp_rate),
1570                initial_distance: Some(initial_distance),
1571                width_multiplier: Some(width_multiplier),
1572            },
1573        );
1574        let out = exponential_trend_with_kernel(&input, Kernel::Auto)
1575            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1576        std::slice::from_raw_parts_mut(uptrend_base_ptr, len).copy_from_slice(&out.uptrend_base);
1577        std::slice::from_raw_parts_mut(downtrend_base_ptr, len)
1578            .copy_from_slice(&out.downtrend_base);
1579        std::slice::from_raw_parts_mut(uptrend_extension_ptr, len)
1580            .copy_from_slice(&out.uptrend_extension);
1581        std::slice::from_raw_parts_mut(downtrend_extension_ptr, len)
1582            .copy_from_slice(&out.downtrend_extension);
1583        std::slice::from_raw_parts_mut(bullish_change_ptr, len)
1584            .copy_from_slice(&out.bullish_change);
1585        std::slice::from_raw_parts_mut(bearish_change_ptr, len)
1586            .copy_from_slice(&out.bearish_change);
1587    }
1588
1589    Ok(())
1590}
1591
1592#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1593#[wasm_bindgen(js_name = exponential_trend_batch)]
1594pub fn exponential_trend_batch_unified_js(
1595    high: &[f64],
1596    low: &[f64],
1597    close: &[f64],
1598    config: JsValue,
1599) -> Result<JsValue, JsValue> {
1600    let config: ExponentialTrendBatchConfig =
1601        serde_wasm_bindgen::from_value(config).map_err(|e| JsValue::from_str(&e.to_string()))?;
1602    let out = exponential_trend_batch_with_kernel(
1603        high,
1604        low,
1605        close,
1606        &ExponentialTrendBatchRange {
1607            exp_rate: config.exp_rate_range,
1608            initial_distance: config.initial_distance_range,
1609            width_multiplier: config.width_multiplier_range,
1610        },
1611        Kernel::Auto,
1612    )
1613    .map_err(|e| JsValue::from_str(&e.to_string()))?;
1614    serde_wasm_bindgen::to_value(&out).map_err(|e| JsValue::from_str(&e.to_string()))
1615}
1616
1617#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1618#[wasm_bindgen(js_name = exponential_trend_batch_into)]
1619pub fn exponential_trend_batch_into(
1620    high_ptr: *const f64,
1621    low_ptr: *const f64,
1622    close_ptr: *const f64,
1623    uptrend_base_ptr: *mut f64,
1624    downtrend_base_ptr: *mut f64,
1625    uptrend_extension_ptr: *mut f64,
1626    downtrend_extension_ptr: *mut f64,
1627    bullish_change_ptr: *mut f64,
1628    bearish_change_ptr: *mut f64,
1629    len: usize,
1630    exp_rate_start: f64,
1631    exp_rate_end: f64,
1632    exp_rate_step: f64,
1633    initial_distance_start: f64,
1634    initial_distance_end: f64,
1635    initial_distance_step: f64,
1636    width_multiplier_start: f64,
1637    width_multiplier_end: f64,
1638    width_multiplier_step: f64,
1639) -> Result<usize, JsValue> {
1640    let sweep = ExponentialTrendBatchRange {
1641        exp_rate: (exp_rate_start, exp_rate_end, exp_rate_step),
1642        initial_distance: (
1643            initial_distance_start,
1644            initial_distance_end,
1645            initial_distance_step,
1646        ),
1647        width_multiplier: (
1648            width_multiplier_start,
1649            width_multiplier_end,
1650            width_multiplier_step,
1651        ),
1652    };
1653    let rows = expand_grid_exponential_trend(&sweep)
1654        .map_err(|e| JsValue::from_str(&e.to_string()))?
1655        .len();
1656    let total = rows
1657        .checked_mul(len)
1658        .ok_or_else(|| JsValue::from_str("rows * cols overflow"))?;
1659
1660    unsafe {
1661        let out = exponential_trend_batch_with_kernel(
1662            std::slice::from_raw_parts(high_ptr, len),
1663            std::slice::from_raw_parts(low_ptr, len),
1664            std::slice::from_raw_parts(close_ptr, len),
1665            &sweep,
1666            Kernel::Auto,
1667        )
1668        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1669        std::slice::from_raw_parts_mut(uptrend_base_ptr, total).copy_from_slice(&out.uptrend_base);
1670        std::slice::from_raw_parts_mut(downtrend_base_ptr, total)
1671            .copy_from_slice(&out.downtrend_base);
1672        std::slice::from_raw_parts_mut(uptrend_extension_ptr, total)
1673            .copy_from_slice(&out.uptrend_extension);
1674        std::slice::from_raw_parts_mut(downtrend_extension_ptr, total)
1675            .copy_from_slice(&out.downtrend_extension);
1676        std::slice::from_raw_parts_mut(bullish_change_ptr, total)
1677            .copy_from_slice(&out.bullish_change);
1678        std::slice::from_raw_parts_mut(bearish_change_ptr, total)
1679            .copy_from_slice(&out.bearish_change);
1680    }
1681
1682    Ok(rows)
1683}
1684
1685#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1686#[wasm_bindgen]
1687pub struct ExponentialTrendStreamWasm {
1688    inner: ExponentialTrendStream,
1689}
1690
1691#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1692#[wasm_bindgen]
1693impl ExponentialTrendStreamWasm {
1694    #[wasm_bindgen(constructor)]
1695    pub fn new(
1696        exp_rate: Option<f64>,
1697        initial_distance: Option<f64>,
1698        width_multiplier: Option<f64>,
1699    ) -> Result<ExponentialTrendStreamWasm, JsValue> {
1700        let inner = ExponentialTrendStream::try_new(ExponentialTrendParams {
1701            exp_rate,
1702            initial_distance,
1703            width_multiplier,
1704        })
1705        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1706        Ok(Self { inner })
1707    }
1708
1709    pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsValue> {
1710        serde_wasm_bindgen::to_value(&self.inner.update(high, low, close))
1711            .map_err(|e| JsValue::from_str(&e.to_string()))
1712    }
1713}
1714
1715#[cfg(test)]
1716mod tests {
1717    use super::*;
1718
1719    fn assert_series_eq(left: &[f64], right: &[f64]) {
1720        assert_eq!(left.len(), right.len());
1721        for (lhs, rhs) in left.iter().zip(right.iter()) {
1722            assert!(lhs == rhs || (lhs.is_nan() && rhs.is_nan()));
1723        }
1724    }
1725
1726    fn sample_hlc() -> (Vec<f64>, Vec<f64>, Vec<f64>) {
1727        let mut high = Vec::with_capacity(160);
1728        let mut low = Vec::with_capacity(160);
1729        let mut close = Vec::with_capacity(160);
1730
1731        for i in 0..60 {
1732            let c = 100.0 + (i as f64) * 0.65 + ((i % 5) as f64 - 2.0) * 0.08;
1733            close.push(c);
1734        }
1735        for i in 60..100 {
1736            let x = (i - 60) as f64;
1737            let c = 139.0 + x * 0.05 - ((i % 4) as f64) * 0.12;
1738            close.push(c);
1739        }
1740        for i in 100..120 {
1741            let x = (i - 100) as f64;
1742            let c = 140.0 + x * 0.55;
1743            close.push(c);
1744        }
1745        for i in 120..140 {
1746            let x = (i - 120) as f64;
1747            let c = 150.0 - x * 2.4;
1748            close.push(c);
1749        }
1750        for i in 140..160 {
1751            let x = (i - 140) as f64;
1752            let c = 102.0 + x * 1.8;
1753            close.push(c);
1754        }
1755
1756        for (i, &c) in close.iter().enumerate() {
1757            let wiggle = ((i % 3) as f64) * 0.15;
1758            high.push(c + 1.6 + wiggle);
1759            low.push(c - 1.5 - wiggle * 0.8);
1760        }
1761
1762        (high, low, close)
1763    }
1764
1765    #[test]
1766    fn exponential_trend_outputs_present() -> Result<(), Box<dyn StdError>> {
1767        let (high, low, close) = sample_hlc();
1768        let input = ExponentialTrendInput::from_slices(
1769            &high,
1770            &low,
1771            &close,
1772            ExponentialTrendParams::default(),
1773        );
1774        let out = exponential_trend(&input)?;
1775        assert!(out.uptrend_base.iter().any(|v| v.is_finite()));
1776        assert!(out.downtrend_base.iter().any(|v| v.is_finite()));
1777        Ok(())
1778    }
1779
1780    #[test]
1781    fn exponential_trend_into_matches_api() -> Result<(), Box<dyn StdError>> {
1782        let (high, low, close) = sample_hlc();
1783        let input = ExponentialTrendInput::from_slices(
1784            &high,
1785            &low,
1786            &close,
1787            ExponentialTrendParams::default(),
1788        );
1789        let baseline = exponential_trend(&input)?;
1790        let len = close.len();
1791
1792        let mut uptrend_base = vec![f64::NAN; len];
1793        let mut downtrend_base = vec![f64::NAN; len];
1794        let mut uptrend_extension = vec![f64::NAN; len];
1795        let mut downtrend_extension = vec![f64::NAN; len];
1796        let mut bullish_change = vec![f64::NAN; len];
1797        let mut bearish_change = vec![f64::NAN; len];
1798
1799        exponential_trend_into(
1800            &mut uptrend_base,
1801            &mut downtrend_base,
1802            &mut uptrend_extension,
1803            &mut downtrend_extension,
1804            &mut bullish_change,
1805            &mut bearish_change,
1806            &input,
1807        )?;
1808
1809        assert_series_eq(&uptrend_base, &baseline.uptrend_base);
1810        assert_series_eq(&downtrend_base, &baseline.downtrend_base);
1811        assert_series_eq(&uptrend_extension, &baseline.uptrend_extension);
1812        assert_series_eq(&downtrend_extension, &baseline.downtrend_extension);
1813        assert_series_eq(&bullish_change, &baseline.bullish_change);
1814        assert_series_eq(&bearish_change, &baseline.bearish_change);
1815        Ok(())
1816    }
1817
1818    #[test]
1819    fn exponential_trend_stream_matches_api() -> Result<(), Box<dyn StdError>> {
1820        let (high, low, close) = sample_hlc();
1821        let params = ExponentialTrendParams::default();
1822        let input = ExponentialTrendInput::from_slices(&high, &low, &close, params.clone());
1823        let batch = exponential_trend(&input)?;
1824        let mut stream = ExponentialTrendStream::try_new(params)?;
1825
1826        let mut uptrend_base = vec![f64::NAN; close.len()];
1827        let mut downtrend_base = vec![f64::NAN; close.len()];
1828        let mut uptrend_extension = vec![f64::NAN; close.len()];
1829        let mut downtrend_extension = vec![f64::NAN; close.len()];
1830        let mut bullish_change = vec![f64::NAN; close.len()];
1831        let mut bearish_change = vec![f64::NAN; close.len()];
1832
1833        for i in 0..close.len() {
1834            if let Some((ub, db, ue, de, bc, brc)) = stream.update(high[i], low[i], close[i]) {
1835                uptrend_base[i] = ub;
1836                downtrend_base[i] = db;
1837                uptrend_extension[i] = ue;
1838                downtrend_extension[i] = de;
1839                bullish_change[i] = bc;
1840                bearish_change[i] = brc;
1841            }
1842        }
1843
1844        assert_series_eq(&uptrend_base, &batch.uptrend_base);
1845        assert_series_eq(&downtrend_base, &batch.downtrend_base);
1846        assert_series_eq(&uptrend_extension, &batch.uptrend_extension);
1847        assert_series_eq(&downtrend_extension, &batch.downtrend_extension);
1848        assert_series_eq(&bullish_change, &batch.bullish_change);
1849        assert_series_eq(&bearish_change, &batch.bearish_change);
1850        Ok(())
1851    }
1852
1853    #[test]
1854    fn exponential_trend_batch_matches_single() -> Result<(), Box<dyn StdError>> {
1855        let (high, low, close) = sample_hlc();
1856        let batch = exponential_trend_batch_with_kernel(
1857            &high,
1858            &low,
1859            &close,
1860            &ExponentialTrendBatchRange {
1861                exp_rate: (DEFAULT_EXP_RATE, DEFAULT_EXP_RATE, 0.0),
1862                initial_distance: (DEFAULT_INITIAL_DISTANCE, DEFAULT_INITIAL_DISTANCE, 0.0),
1863                width_multiplier: (DEFAULT_WIDTH_MULTIPLIER, DEFAULT_WIDTH_MULTIPLIER, 0.0),
1864            },
1865            Kernel::ScalarBatch,
1866        )?;
1867        let input = ExponentialTrendInput::from_slices(
1868            &high,
1869            &low,
1870            &close,
1871            ExponentialTrendParams::default(),
1872        );
1873        let single = exponential_trend(&input)?;
1874        assert_eq!(batch.rows, 1);
1875        assert_eq!(batch.cols, close.len());
1876        assert_series_eq(&batch.uptrend_base[..close.len()], &single.uptrend_base);
1877        Ok(())
1878    }
1879
1880    #[test]
1881    fn exponential_trend_invalid_exp_rate() {
1882        let (high, low, close) = sample_hlc();
1883        let input = ExponentialTrendInput::from_slices(
1884            &high,
1885            &low,
1886            &close,
1887            ExponentialTrendParams {
1888                exp_rate: Some(0.8),
1889                initial_distance: Some(DEFAULT_INITIAL_DISTANCE),
1890                width_multiplier: Some(DEFAULT_WIDTH_MULTIPLIER),
1891            },
1892        );
1893        let err = exponential_trend(&input).unwrap_err();
1894        assert!(matches!(err, ExponentialTrendError::InvalidExpRate { .. }));
1895    }
1896}