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}