Skip to main content

vector_ta/indicators/
adaptive_macd.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9#[cfg(feature = "python")]
10use pyo3::wrap_pyfunction;
11
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use serde::{Deserialize, Serialize};
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use wasm_bindgen::prelude::*;
16
17use crate::utilities::data_loader::{source_type, Candles};
18use crate::utilities::enums::Kernel;
19use crate::utilities::helpers::{
20    alloc_with_nan_prefix, detect_best_batch_kernel, make_uninit_matrix,
21};
22#[cfg(feature = "python")]
23use crate::utilities::kernel_validation::validate_kernel;
24#[cfg(not(target_arch = "wasm32"))]
25use rayon::prelude::*;
26use std::convert::AsRef;
27#[cfg(test)]
28use std::error::Error as StdError;
29use std::mem::ManuallyDrop;
30use thiserror::Error;
31
32const DEFAULT_LENGTH: usize = 20;
33const DEFAULT_FAST_PERIOD: usize = 10;
34const DEFAULT_SLOW_PERIOD: usize = 20;
35const DEFAULT_SIGNAL_PERIOD: usize = 9;
36const MIN_PERIOD: usize = 2;
37const CORR_EPSILON: f64 = 1e-12;
38
39impl<'a> AsRef<[f64]> for AdaptiveMacdInput<'a> {
40    #[inline(always)]
41    fn as_ref(&self) -> &[f64] {
42        match &self.data {
43            AdaptiveMacdData::Slice(slice) => slice,
44            AdaptiveMacdData::Candles { candles, source } => source_type(candles, source),
45        }
46    }
47}
48
49#[derive(Debug, Clone)]
50pub enum AdaptiveMacdData<'a> {
51    Candles {
52        candles: &'a Candles,
53        source: &'a str,
54    },
55    Slice(&'a [f64]),
56}
57
58#[derive(Debug, Clone)]
59#[cfg_attr(
60    all(target_arch = "wasm32", feature = "wasm"),
61    derive(Serialize, Deserialize)
62)]
63pub struct AdaptiveMacdOutput {
64    pub macd: Vec<f64>,
65    pub signal: Vec<f64>,
66    pub hist: Vec<f64>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70#[cfg_attr(
71    all(target_arch = "wasm32", feature = "wasm"),
72    derive(Serialize, Deserialize)
73)]
74pub struct AdaptiveMacdParams {
75    pub length: Option<usize>,
76    pub fast_period: Option<usize>,
77    pub slow_period: Option<usize>,
78    pub signal_period: Option<usize>,
79}
80
81impl Default for AdaptiveMacdParams {
82    fn default() -> Self {
83        Self {
84            length: Some(DEFAULT_LENGTH),
85            fast_period: Some(DEFAULT_FAST_PERIOD),
86            slow_period: Some(DEFAULT_SLOW_PERIOD),
87            signal_period: Some(DEFAULT_SIGNAL_PERIOD),
88        }
89    }
90}
91
92#[derive(Debug, Clone)]
93pub struct AdaptiveMacdInput<'a> {
94    pub data: AdaptiveMacdData<'a>,
95    pub params: AdaptiveMacdParams,
96}
97
98impl<'a> AdaptiveMacdInput<'a> {
99    #[inline]
100    pub fn from_candles(candles: &'a Candles, source: &'a str, params: AdaptiveMacdParams) -> Self {
101        Self {
102            data: AdaptiveMacdData::Candles { candles, source },
103            params,
104        }
105    }
106
107    #[inline]
108    pub fn from_slice(slice: &'a [f64], params: AdaptiveMacdParams) -> Self {
109        Self {
110            data: AdaptiveMacdData::Slice(slice),
111            params,
112        }
113    }
114
115    #[inline]
116    pub fn with_default_candles(candles: &'a Candles) -> Self {
117        Self::from_candles(candles, "close", AdaptiveMacdParams::default())
118    }
119
120    #[inline(always)]
121    pub fn get_length(&self) -> usize {
122        self.params.length.unwrap_or(DEFAULT_LENGTH)
123    }
124
125    #[inline(always)]
126    pub fn get_fast_period(&self) -> usize {
127        self.params.fast_period.unwrap_or(DEFAULT_FAST_PERIOD)
128    }
129
130    #[inline(always)]
131    pub fn get_slow_period(&self) -> usize {
132        self.params.slow_period.unwrap_or(DEFAULT_SLOW_PERIOD)
133    }
134
135    #[inline(always)]
136    pub fn get_signal_period(&self) -> usize {
137        self.params.signal_period.unwrap_or(DEFAULT_SIGNAL_PERIOD)
138    }
139}
140
141#[derive(Copy, Clone, Debug)]
142pub struct AdaptiveMacdBuilder {
143    length: Option<usize>,
144    fast_period: Option<usize>,
145    slow_period: Option<usize>,
146    signal_period: Option<usize>,
147    kernel: Kernel,
148}
149
150impl Default for AdaptiveMacdBuilder {
151    fn default() -> Self {
152        Self {
153            length: None,
154            fast_period: None,
155            slow_period: None,
156            signal_period: None,
157            kernel: Kernel::Auto,
158        }
159    }
160}
161
162impl AdaptiveMacdBuilder {
163    #[inline(always)]
164    pub fn new() -> Self {
165        Self::default()
166    }
167
168    #[inline(always)]
169    pub fn length(mut self, length: usize) -> Self {
170        self.length = Some(length);
171        self
172    }
173
174    #[inline(always)]
175    pub fn fast_period(mut self, fast_period: usize) -> Self {
176        self.fast_period = Some(fast_period);
177        self
178    }
179
180    #[inline(always)]
181    pub fn slow_period(mut self, slow_period: usize) -> Self {
182        self.slow_period = Some(slow_period);
183        self
184    }
185
186    #[inline(always)]
187    pub fn signal_period(mut self, signal_period: usize) -> Self {
188        self.signal_period = Some(signal_period);
189        self
190    }
191
192    #[inline(always)]
193    pub fn kernel(mut self, kernel: Kernel) -> Self {
194        self.kernel = kernel;
195        self
196    }
197
198    #[inline(always)]
199    fn params(self) -> AdaptiveMacdParams {
200        AdaptiveMacdParams {
201            length: self.length,
202            fast_period: self.fast_period,
203            slow_period: self.slow_period,
204            signal_period: self.signal_period,
205        }
206    }
207
208    #[inline(always)]
209    pub fn apply(self, candles: &Candles) -> Result<AdaptiveMacdOutput, AdaptiveMacdError> {
210        adaptive_macd_with_kernel(
211            &AdaptiveMacdInput::from_candles(candles, "close", self.params()),
212            self.kernel,
213        )
214    }
215
216    #[inline(always)]
217    pub fn apply_slice(self, data: &[f64]) -> Result<AdaptiveMacdOutput, AdaptiveMacdError> {
218        adaptive_macd_with_kernel(
219            &AdaptiveMacdInput::from_slice(data, self.params()),
220            self.kernel,
221        )
222    }
223
224    #[inline(always)]
225    pub fn into_stream(self) -> Result<AdaptiveMacdStream, AdaptiveMacdError> {
226        AdaptiveMacdStream::try_new(self.params())
227    }
228}
229
230#[derive(Debug, Error)]
231pub enum AdaptiveMacdError {
232    #[error("adaptive_macd: input data slice is empty.")]
233    EmptyInputData,
234    #[error("adaptive_macd: all values are NaN.")]
235    AllValuesNaN,
236    #[error(
237        "adaptive_macd: invalid period: length = {length}, fast = {fast}, slow = {slow}, signal = {signal}, data length = {data_len}"
238    )]
239    InvalidPeriod {
240        length: usize,
241        fast: usize,
242        slow: usize,
243        signal: usize,
244        data_len: usize,
245    },
246    #[error("adaptive_macd: not enough valid data: needed = {needed}, valid = {valid}")]
247    NotEnoughValidData { needed: usize, valid: usize },
248    #[error("adaptive_macd: output length mismatch: expected = {expected}, got = {got}")]
249    OutputLengthMismatch { expected: usize, got: usize },
250    #[error(
251        "adaptive_macd: invalid range for {axis}: start = {start}, end = {end}, step = {step}"
252    )]
253    InvalidRange {
254        axis: &'static str,
255        start: usize,
256        end: usize,
257        step: usize,
258    },
259    #[error("adaptive_macd: invalid kernel for batch: {0:?}")]
260    InvalidKernelForBatch(Kernel),
261}
262
263#[derive(Clone, Copy, Debug)]
264struct PreparedInput<'a> {
265    data: &'a [f64],
266    first_valid: usize,
267    length: usize,
268    fast_period: usize,
269    slow_period: usize,
270    signal_period: usize,
271    warmup: usize,
272    kernel: Kernel,
273}
274
275#[derive(Clone, Copy, Debug)]
276struct AdaptiveMacdSpec {
277    delta_coeff: f64,
278    recur_coeff: f64,
279    trend_coeff: f64,
280    cycle_coeff: f64,
281}
282
283#[derive(Clone, Debug)]
284pub struct AdaptiveMacdStream {
285    params: AdaptiveMacdParams,
286    state: AdaptiveMacdState,
287}
288
289#[derive(Clone, Debug)]
290struct AdaptiveMacdState {
291    corr: RollingCorrelationState,
292    signal: EmaLikeState,
293    prev_close: f64,
294    prev_macd1: f64,
295    prev_macd2: f64,
296    spec: AdaptiveMacdSpec,
297}
298
299#[derive(Clone, Debug)]
300struct RollingCorrelationState {
301    length: usize,
302    ring: Vec<f64>,
303    head: usize,
304    count: usize,
305    sum_y: f64,
306    sum_y2: f64,
307    sum_xy: f64,
308    sum_x: f64,
309    denom_x: f64,
310}
311
312#[derive(Clone, Debug)]
313struct EmaLikeState {
314    period: usize,
315    alpha: f64,
316    beta: f64,
317    count: usize,
318    sum: f64,
319    value: f64,
320    started: bool,
321}
322
323#[derive(Clone, Debug)]
324pub struct AdaptiveMacdBatchRange {
325    pub length: (usize, usize, usize),
326    pub fast_period: (usize, usize, usize),
327    pub slow_period: (usize, usize, usize),
328    pub signal_period: (usize, usize, usize),
329}
330
331impl Default for AdaptiveMacdBatchRange {
332    fn default() -> Self {
333        Self {
334            length: (DEFAULT_LENGTH, DEFAULT_LENGTH, 0),
335            fast_period: (DEFAULT_FAST_PERIOD, DEFAULT_FAST_PERIOD, 0),
336            slow_period: (DEFAULT_SLOW_PERIOD, DEFAULT_SLOW_PERIOD, 0),
337            signal_period: (DEFAULT_SIGNAL_PERIOD, DEFAULT_SIGNAL_PERIOD, 0),
338        }
339    }
340}
341
342#[derive(Clone, Debug, Default)]
343pub struct AdaptiveMacdBatchBuilder {
344    range: AdaptiveMacdBatchRange,
345    kernel: Kernel,
346}
347
348#[derive(Clone, Debug)]
349pub struct AdaptiveMacdBatchOutput {
350    pub macd: Vec<f64>,
351    pub signal: Vec<f64>,
352    pub hist: Vec<f64>,
353    pub combos: Vec<AdaptiveMacdParams>,
354    pub rows: usize,
355    pub cols: usize,
356}
357
358impl AdaptiveMacdBatchBuilder {
359    #[inline(always)]
360    pub fn new() -> Self {
361        Self::default()
362    }
363
364    #[inline(always)]
365    pub fn kernel(mut self, kernel: Kernel) -> Self {
366        self.kernel = kernel;
367        self
368    }
369
370    #[inline(always)]
371    pub fn length_range(mut self, start: usize, end: usize, step: usize) -> Self {
372        self.range.length = (start, end, step);
373        self
374    }
375
376    #[inline(always)]
377    pub fn fast_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
378        self.range.fast_period = (start, end, step);
379        self
380    }
381
382    #[inline(always)]
383    pub fn slow_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
384        self.range.slow_period = (start, end, step);
385        self
386    }
387
388    #[inline(always)]
389    pub fn signal_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
390        self.range.signal_period = (start, end, step);
391        self
392    }
393
394    #[inline(always)]
395    pub fn apply_slice(self, data: &[f64]) -> Result<AdaptiveMacdBatchOutput, AdaptiveMacdError> {
396        adaptive_macd_batch_with_kernel(data, &self.range, self.kernel)
397    }
398
399    #[inline(always)]
400    pub fn with_default_slice(
401        data: &[f64],
402        kernel: Kernel,
403    ) -> Result<AdaptiveMacdBatchOutput, AdaptiveMacdError> {
404        AdaptiveMacdBatchBuilder::new()
405            .kernel(kernel)
406            .apply_slice(data)
407    }
408
409    #[inline(always)]
410    pub fn apply_candles(
411        self,
412        candles: &Candles,
413        source: &str,
414    ) -> Result<AdaptiveMacdBatchOutput, AdaptiveMacdError> {
415        self.apply_slice(source_type(candles, source))
416    }
417
418    #[inline(always)]
419    pub fn with_default_candles(
420        candles: &Candles,
421    ) -> Result<AdaptiveMacdBatchOutput, AdaptiveMacdError> {
422        AdaptiveMacdBatchBuilder::new()
423            .kernel(Kernel::Auto)
424            .apply_candles(candles, "close")
425    }
426}
427
428#[inline(always)]
429fn normalize_single_kernel_to_scalar(_kernel: Kernel) -> Kernel {
430    Kernel::Scalar
431}
432
433#[inline(always)]
434fn validate_periods(
435    length: usize,
436    fast_period: usize,
437    slow_period: usize,
438    signal_period: usize,
439    data_len: usize,
440) -> Result<(), AdaptiveMacdError> {
441    if length < MIN_PERIOD
442        || fast_period < MIN_PERIOD
443        || slow_period < MIN_PERIOD
444        || signal_period < MIN_PERIOD
445        || length > data_len
446        || fast_period > data_len
447        || slow_period > data_len
448        || signal_period > data_len
449    {
450        return Err(AdaptiveMacdError::InvalidPeriod {
451            length,
452            fast: fast_period,
453            slow: slow_period,
454            signal: signal_period,
455            data_len,
456        });
457    }
458    Ok(())
459}
460
461#[inline(always)]
462fn build_spec(fast_period: usize, slow_period: usize) -> AdaptiveMacdSpec {
463    let a1 = 2.0 / (fast_period as f64 + 1.0);
464    let a2 = 2.0 / (slow_period as f64 + 1.0);
465    AdaptiveMacdSpec {
466        delta_coeff: a1 - a2,
467        recur_coeff: 2.0 - a1 - a2,
468        trend_coeff: (1.0 - a1) * (1.0 - a2),
469        cycle_coeff: (1.0 - a1) / (1.0 - a2),
470    }
471}
472
473#[inline(always)]
474fn prepare_input<'a>(
475    input: &'a AdaptiveMacdInput<'a>,
476    kernel: Kernel,
477) -> Result<PreparedInput<'a>, AdaptiveMacdError> {
478    let data = input.as_ref();
479    if data.is_empty() {
480        return Err(AdaptiveMacdError::EmptyInputData);
481    }
482
483    let first_valid = data
484        .iter()
485        .position(|value| !value.is_nan())
486        .ok_or(AdaptiveMacdError::AllValuesNaN)?;
487
488    let length = input.get_length();
489    let fast_period = input.get_fast_period();
490    let slow_period = input.get_slow_period();
491    let signal_period = input.get_signal_period();
492
493    validate_periods(length, fast_period, slow_period, signal_period, data.len())?;
494
495    let valid = data.len() - first_valid;
496    if valid < length {
497        return Err(AdaptiveMacdError::NotEnoughValidData {
498            needed: length,
499            valid,
500        });
501    }
502
503    Ok(PreparedInput {
504        data,
505        first_valid,
506        length,
507        fast_period,
508        slow_period,
509        signal_period,
510        warmup: first_valid + length - 1,
511        kernel: normalize_single_kernel_to_scalar(kernel),
512    })
513}
514
515impl RollingCorrelationState {
516    #[inline(always)]
517    fn new(length: usize) -> Self {
518        let sum_x = (length.saturating_sub(1) * length) as f64 * 0.5;
519        let sum_x2 = (length.saturating_sub(1) * length * (2 * length - 1)) as f64 / 6.0;
520        let n = length as f64;
521        Self {
522            length,
523            ring: vec![0.0; length],
524            head: 0,
525            count: 0,
526            sum_y: 0.0,
527            sum_y2: 0.0,
528            sum_xy: 0.0,
529            sum_x,
530            denom_x: n.mul_add(sum_x2, -(sum_x * sum_x)),
531        }
532    }
533
534    #[inline(always)]
535    fn reset(&mut self) {
536        self.head = 0;
537        self.count = 0;
538        self.sum_y = 0.0;
539        self.sum_y2 = 0.0;
540        self.sum_xy = 0.0;
541    }
542
543    #[inline(always)]
544    fn corr_sq(&self) -> f64 {
545        let n = self.length as f64;
546        let denom_y = n.mul_add(self.sum_y2, -(self.sum_y * self.sum_y));
547        if denom_y <= CORR_EPSILON {
548            return 0.0;
549        }
550        let num = n.mul_add(self.sum_xy, -(self.sum_x * self.sum_y));
551        ((num * num) / (self.denom_x * denom_y)).clamp(0.0, 1.0)
552    }
553
554    #[inline(always)]
555    fn push(&mut self, value: f64) -> Option<f64> {
556        if !value.is_finite() {
557            self.reset();
558            return None;
559        }
560
561        if self.count < self.length {
562            let idx = self.count;
563            self.ring[self.head] = value;
564            self.head += 1;
565            if self.head == self.length {
566                self.head = 0;
567            }
568            self.count += 1;
569            self.sum_y += value;
570            self.sum_y2 += value * value;
571            self.sum_xy += (idx as f64) * value;
572            return if self.count == self.length {
573                Some(self.corr_sq())
574            } else {
575                None
576            };
577        }
578
579        let old = self.ring[self.head];
580        let prev_sum_y = self.sum_y;
581        let prev_sum_xy = self.sum_xy;
582
583        self.ring[self.head] = value;
584        self.head += 1;
585        if self.head == self.length {
586            self.head = 0;
587        }
588
589        self.sum_y = prev_sum_y - old + value;
590        self.sum_y2 = self.sum_y2 - old * old + value * value;
591        self.sum_xy = prev_sum_xy - (prev_sum_y - old) + (self.length as f64 - 1.0) * value;
592
593        Some(self.corr_sq())
594    }
595}
596
597impl EmaLikeState {
598    #[inline(always)]
599    fn new(period: usize) -> Self {
600        let alpha = 2.0 / (period as f64 + 1.0);
601        Self {
602            period,
603            alpha,
604            beta: 1.0 - alpha,
605            count: 0,
606            sum: 0.0,
607            value: f64::NAN,
608            started: false,
609        }
610    }
611
612    #[inline(always)]
613    fn update(&mut self, value: f64) -> Option<f64> {
614        if !value.is_finite() {
615            return if self.started { Some(self.value) } else { None };
616        }
617        if !self.started {
618            self.started = true;
619            self.count = 1;
620            self.sum = value;
621            self.value = value;
622            return Some(value);
623        }
624        if self.count < self.period {
625            self.count += 1;
626            self.sum += value;
627            self.value = self.sum / self.count as f64;
628            return Some(self.value);
629        }
630        self.value = self.beta.mul_add(self.value, self.alpha * value);
631        Some(self.value)
632    }
633}
634
635impl AdaptiveMacdState {
636    #[inline(always)]
637    fn new(params: &AdaptiveMacdParams) -> Result<Self, AdaptiveMacdError> {
638        let length = params.length.unwrap_or(DEFAULT_LENGTH);
639        let fast_period = params.fast_period.unwrap_or(DEFAULT_FAST_PERIOD);
640        let slow_period = params.slow_period.unwrap_or(DEFAULT_SLOW_PERIOD);
641        let signal_period = params.signal_period.unwrap_or(DEFAULT_SIGNAL_PERIOD);
642        validate_periods(length, fast_period, slow_period, signal_period, usize::MAX)?;
643        Ok(Self {
644            corr: RollingCorrelationState::new(length),
645            signal: EmaLikeState::new(signal_period),
646            prev_close: f64::NAN,
647            prev_macd1: f64::NAN,
648            prev_macd2: f64::NAN,
649            spec: build_spec(fast_period, slow_period),
650        })
651    }
652
653    #[inline(always)]
654    fn update(&mut self, value: f64) -> (f64, f64, f64) {
655        let current_macd = if value.is_finite() {
656            let corr_sq = self.corr.push(value);
657            if self.prev_close.is_finite() {
658                if let Some(corr_sq) = corr_sq {
659                    let r2 = 0.5 * corr_sq + 0.5;
660                    let k = r2 * self.spec.trend_coeff + (1.0 - r2) * self.spec.cycle_coeff;
661                    let prev1 = if self.prev_macd1.is_finite() {
662                        self.prev_macd1
663                    } else {
664                        0.0
665                    };
666                    let prev2 = if self.prev_macd2.is_finite() {
667                        self.prev_macd2
668                    } else {
669                        0.0
670                    };
671                    (value - self.prev_close) * self.spec.delta_coeff
672                        + self.spec.recur_coeff * prev1
673                        - k * prev2
674                } else {
675                    f64::NAN
676                }
677            } else {
678                f64::NAN
679            }
680        } else {
681            self.corr.reset();
682            f64::NAN
683        };
684
685        self.prev_close = value;
686        self.prev_macd2 = self.prev_macd1;
687        self.prev_macd1 = current_macd;
688
689        let signal = self.signal.update(current_macd).unwrap_or(f64::NAN);
690        let hist = if current_macd.is_finite() && signal.is_finite() {
691            current_macd - signal
692        } else {
693            f64::NAN
694        };
695        (current_macd, signal, hist)
696    }
697}
698
699impl AdaptiveMacdStream {
700    #[inline(always)]
701    pub fn try_new(params: AdaptiveMacdParams) -> Result<Self, AdaptiveMacdError> {
702        Ok(Self {
703            state: AdaptiveMacdState::new(&params)?,
704            params,
705        })
706    }
707
708    #[inline(always)]
709    pub fn update(&mut self, value: f64) -> Option<(f64, f64, f64)> {
710        let (macd, signal, hist) = self.state.update(value);
711        if macd.is_finite() {
712            Some((macd, signal, hist))
713        } else {
714            None
715        }
716    }
717
718    #[inline(always)]
719    pub fn params(&self) -> &AdaptiveMacdParams {
720        &self.params
721    }
722}
723
724#[inline(always)]
725fn compute_row(
726    data: &[f64],
727    params: &AdaptiveMacdParams,
728    macd_out: &mut [f64],
729    signal_out: &mut [f64],
730    hist_out: &mut [f64],
731) -> Result<(), AdaptiveMacdError> {
732    if macd_out.len() != data.len()
733        || signal_out.len() != data.len()
734        || hist_out.len() != data.len()
735    {
736        return Err(AdaptiveMacdError::OutputLengthMismatch {
737            expected: data.len(),
738            got: macd_out.len().max(signal_out.len()).max(hist_out.len()),
739        });
740    }
741
742    let mut state = AdaptiveMacdState::new(params)?;
743    for i in 0..data.len() {
744        let (macd, signal, hist) = state.update(data[i]);
745        macd_out[i] = macd;
746        signal_out[i] = signal;
747        hist_out[i] = hist;
748    }
749    Ok(())
750}
751
752#[inline]
753pub fn adaptive_macd(input: &AdaptiveMacdInput) -> Result<AdaptiveMacdOutput, AdaptiveMacdError> {
754    adaptive_macd_with_kernel(input, Kernel::Auto)
755}
756
757pub fn adaptive_macd_with_kernel(
758    input: &AdaptiveMacdInput,
759    kernel: Kernel,
760) -> Result<AdaptiveMacdOutput, AdaptiveMacdError> {
761    let prepared = prepare_input(input, kernel)?;
762    let _ = prepared.kernel;
763    let mut macd = alloc_with_nan_prefix(prepared.data.len(), prepared.warmup);
764    let mut signal = alloc_with_nan_prefix(prepared.data.len(), prepared.warmup);
765    let mut hist = alloc_with_nan_prefix(prepared.data.len(), prepared.warmup);
766    compute_row(
767        prepared.data,
768        &AdaptiveMacdParams {
769            length: Some(prepared.length),
770            fast_period: Some(prepared.fast_period),
771            slow_period: Some(prepared.slow_period),
772            signal_period: Some(prepared.signal_period),
773        },
774        &mut macd,
775        &mut signal,
776        &mut hist,
777    )?;
778    Ok(AdaptiveMacdOutput { macd, signal, hist })
779}
780
781#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
782pub fn adaptive_macd_into(
783    input: &AdaptiveMacdInput,
784    macd_out: &mut [f64],
785    signal_out: &mut [f64],
786    hist_out: &mut [f64],
787) -> Result<(), AdaptiveMacdError> {
788    adaptive_macd_into_slice(macd_out, signal_out, hist_out, input, Kernel::Auto)
789}
790
791pub fn adaptive_macd_into_slice(
792    macd_out: &mut [f64],
793    signal_out: &mut [f64],
794    hist_out: &mut [f64],
795    input: &AdaptiveMacdInput,
796    kernel: Kernel,
797) -> Result<(), AdaptiveMacdError> {
798    let prepared = prepare_input(input, kernel)?;
799    let _ = prepared.kernel;
800    compute_row(
801        prepared.data,
802        &AdaptiveMacdParams {
803            length: Some(prepared.length),
804            fast_period: Some(prepared.fast_period),
805            slow_period: Some(prepared.slow_period),
806            signal_period: Some(prepared.signal_period),
807        },
808        macd_out,
809        signal_out,
810        hist_out,
811    )
812}
813
814#[inline(always)]
815fn axis_values(
816    axis: &'static str,
817    start: usize,
818    end: usize,
819    step: usize,
820) -> Result<Vec<usize>, AdaptiveMacdError> {
821    if step == 0 || start == end {
822        return Ok(vec![start]);
823    }
824    if start < end {
825        let mut out = Vec::new();
826        let mut current = start;
827        loop {
828            out.push(current);
829            match current.checked_add(step) {
830                Some(next) if next <= end => current = next,
831                Some(_) | None => break,
832            }
833        }
834        if out.is_empty() {
835            return Err(AdaptiveMacdError::InvalidRange {
836                axis,
837                start,
838                end,
839                step,
840            });
841        }
842        return Ok(out);
843    }
844
845    let mut out = Vec::new();
846    let mut current = start;
847    loop {
848        out.push(current);
849        if current <= end || current < step {
850            break;
851        }
852        current -= step;
853        if current < end {
854            break;
855        }
856    }
857    if out.is_empty() {
858        return Err(AdaptiveMacdError::InvalidRange {
859            axis,
860            start,
861            end,
862            step,
863        });
864    }
865    Ok(out)
866}
867
868#[inline]
869pub fn expand_grid(
870    sweep: &AdaptiveMacdBatchRange,
871) -> Result<Vec<AdaptiveMacdParams>, AdaptiveMacdError> {
872    let lengths = axis_values("length", sweep.length.0, sweep.length.1, sweep.length.2)?;
873    let fasts = axis_values(
874        "fast_period",
875        sweep.fast_period.0,
876        sweep.fast_period.1,
877        sweep.fast_period.2,
878    )?;
879    let slows = axis_values(
880        "slow_period",
881        sweep.slow_period.0,
882        sweep.slow_period.1,
883        sweep.slow_period.2,
884    )?;
885    let signals = axis_values(
886        "signal_period",
887        sweep.signal_period.0,
888        sweep.signal_period.1,
889        sweep.signal_period.2,
890    )?;
891
892    let mut out = Vec::new();
893    for &length in &lengths {
894        for &fast_period in &fasts {
895            for &slow_period in &slows {
896                for &signal_period in &signals {
897                    out.push(AdaptiveMacdParams {
898                        length: Some(length),
899                        fast_period: Some(fast_period),
900                        slow_period: Some(slow_period),
901                        signal_period: Some(signal_period),
902                    });
903                }
904            }
905        }
906    }
907    Ok(out)
908}
909
910fn adaptive_macd_batch_inner_into(
911    data: &[f64],
912    sweep: &AdaptiveMacdBatchRange,
913    parallel: bool,
914    macd_out: &mut [f64],
915    signal_out: &mut [f64],
916    hist_out: &mut [f64],
917) -> Result<Vec<AdaptiveMacdParams>, AdaptiveMacdError> {
918    if data.is_empty() {
919        return Err(AdaptiveMacdError::EmptyInputData);
920    }
921    if data.iter().all(|value| value.is_nan()) {
922        return Err(AdaptiveMacdError::AllValuesNaN);
923    }
924
925    let combos = expand_grid(sweep)?;
926    let rows = combos.len();
927    let cols = data.len();
928    let expected = rows
929        .checked_mul(cols)
930        .ok_or(AdaptiveMacdError::OutputLengthMismatch {
931            expected: usize::MAX,
932            got: macd_out.len(),
933        })?;
934    if macd_out.len() != expected || signal_out.len() != expected || hist_out.len() != expected {
935        return Err(AdaptiveMacdError::OutputLengthMismatch {
936            expected,
937            got: macd_out.len().max(signal_out.len()).max(hist_out.len()),
938        });
939    }
940
941    for params in &combos {
942        validate_periods(
943            params.length.unwrap_or(DEFAULT_LENGTH),
944            params.fast_period.unwrap_or(DEFAULT_FAST_PERIOD),
945            params.slow_period.unwrap_or(DEFAULT_SLOW_PERIOD),
946            params.signal_period.unwrap_or(DEFAULT_SIGNAL_PERIOD),
947            cols,
948        )?;
949    }
950
951    let do_row =
952        |row: usize, macd_row: &mut [f64], signal_row: &mut [f64], hist_row: &mut [f64]| {
953            compute_row(data, &combos[row], macd_row, signal_row, hist_row)
954        };
955
956    if parallel {
957        #[cfg(not(target_arch = "wasm32"))]
958        {
959            macd_out
960                .par_chunks_mut(cols)
961                .zip(signal_out.par_chunks_mut(cols))
962                .zip(hist_out.par_chunks_mut(cols))
963                .enumerate()
964                .try_for_each(|(row, ((macd_row, signal_row), hist_row))| {
965                    do_row(row, macd_row, signal_row, hist_row)
966                })?;
967        }
968        #[cfg(target_arch = "wasm32")]
969        {
970            for (row, ((macd_row, signal_row), hist_row)) in macd_out
971                .chunks_mut(cols)
972                .zip(signal_out.chunks_mut(cols))
973                .zip(hist_out.chunks_mut(cols))
974                .enumerate()
975            {
976                do_row(row, macd_row, signal_row, hist_row)?;
977            }
978        }
979    } else {
980        for (row, ((macd_row, signal_row), hist_row)) in macd_out
981            .chunks_mut(cols)
982            .zip(signal_out.chunks_mut(cols))
983            .zip(hist_out.chunks_mut(cols))
984            .enumerate()
985        {
986            do_row(row, macd_row, signal_row, hist_row)?;
987        }
988    }
989
990    Ok(combos)
991}
992
993pub fn adaptive_macd_batch_with_kernel(
994    data: &[f64],
995    sweep: &AdaptiveMacdBatchRange,
996    kernel: Kernel,
997) -> Result<AdaptiveMacdBatchOutput, AdaptiveMacdError> {
998    let batch_kernel = match kernel {
999        Kernel::Auto => detect_best_batch_kernel(),
1000        other if other.is_batch() => other,
1001        _ => return Err(AdaptiveMacdError::InvalidKernelForBatch(kernel)),
1002    };
1003    let _ = batch_kernel;
1004    adaptive_macd_batch_par_slice(data, sweep, Kernel::Scalar)
1005}
1006
1007pub fn adaptive_macd_batch_slice(
1008    data: &[f64],
1009    sweep: &AdaptiveMacdBatchRange,
1010    _kernel: Kernel,
1011) -> Result<AdaptiveMacdBatchOutput, AdaptiveMacdError> {
1012    adaptive_macd_batch_impl(data, sweep, false)
1013}
1014
1015pub fn adaptive_macd_batch_par_slice(
1016    data: &[f64],
1017    sweep: &AdaptiveMacdBatchRange,
1018    _kernel: Kernel,
1019) -> Result<AdaptiveMacdBatchOutput, AdaptiveMacdError> {
1020    adaptive_macd_batch_impl(data, sweep, true)
1021}
1022
1023fn adaptive_macd_batch_impl(
1024    data: &[f64],
1025    sweep: &AdaptiveMacdBatchRange,
1026    parallel: bool,
1027) -> Result<AdaptiveMacdBatchOutput, AdaptiveMacdError> {
1028    let combos = expand_grid(sweep)?;
1029    let rows = combos.len();
1030    let cols = data.len();
1031
1032    let mut macd_mu = make_uninit_matrix(rows, cols);
1033    let mut signal_mu = make_uninit_matrix(rows, cols);
1034    let mut hist_mu = make_uninit_matrix(rows, cols);
1035
1036    let mut macd_guard = ManuallyDrop::new(macd_mu);
1037    let mut signal_guard = ManuallyDrop::new(signal_mu);
1038    let mut hist_guard = ManuallyDrop::new(hist_mu);
1039
1040    let macd_out: &mut [f64] = unsafe {
1041        core::slice::from_raw_parts_mut(macd_guard.as_mut_ptr() as *mut f64, macd_guard.len())
1042    };
1043    let signal_out: &mut [f64] = unsafe {
1044        core::slice::from_raw_parts_mut(signal_guard.as_mut_ptr() as *mut f64, signal_guard.len())
1045    };
1046    let hist_out: &mut [f64] = unsafe {
1047        core::slice::from_raw_parts_mut(hist_guard.as_mut_ptr() as *mut f64, hist_guard.len())
1048    };
1049
1050    let combos =
1051        adaptive_macd_batch_inner_into(data, sweep, parallel, macd_out, signal_out, hist_out)?;
1052
1053    let macd = unsafe {
1054        Vec::from_raw_parts(
1055            macd_guard.as_mut_ptr() as *mut f64,
1056            macd_guard.len(),
1057            macd_guard.capacity(),
1058        )
1059    };
1060    let signal = unsafe {
1061        Vec::from_raw_parts(
1062            signal_guard.as_mut_ptr() as *mut f64,
1063            signal_guard.len(),
1064            signal_guard.capacity(),
1065        )
1066    };
1067    let hist = unsafe {
1068        Vec::from_raw_parts(
1069            hist_guard.as_mut_ptr() as *mut f64,
1070            hist_guard.len(),
1071            hist_guard.capacity(),
1072        )
1073    };
1074
1075    Ok(AdaptiveMacdBatchOutput {
1076        macd,
1077        signal,
1078        hist,
1079        combos,
1080        rows,
1081        cols,
1082    })
1083}
1084
1085#[cfg(feature = "python")]
1086#[pyfunction(name = "adaptive_macd")]
1087#[pyo3(signature = (data, length=DEFAULT_LENGTH, fast_period=DEFAULT_FAST_PERIOD, slow_period=DEFAULT_SLOW_PERIOD, signal_period=DEFAULT_SIGNAL_PERIOD, kernel=None))]
1088pub fn adaptive_macd_py<'py>(
1089    py: Python<'py>,
1090    data: PyReadonlyArray1<'py, f64>,
1091    length: usize,
1092    fast_period: usize,
1093    slow_period: usize,
1094    signal_period: usize,
1095    kernel: Option<&str>,
1096) -> PyResult<(
1097    Bound<'py, PyArray1<f64>>,
1098    Bound<'py, PyArray1<f64>>,
1099    Bound<'py, PyArray1<f64>>,
1100)> {
1101    let slice_in = data.as_slice()?;
1102    let kern = validate_kernel(kernel, false)?;
1103    let input = AdaptiveMacdInput::from_slice(
1104        slice_in,
1105        AdaptiveMacdParams {
1106            length: Some(length),
1107            fast_period: Some(fast_period),
1108            slow_period: Some(slow_period),
1109            signal_period: Some(signal_period),
1110        },
1111    );
1112    let result = py
1113        .allow_threads(|| adaptive_macd_with_kernel(&input, kern))
1114        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1115    Ok((
1116        result.macd.into_pyarray(py),
1117        result.signal.into_pyarray(py),
1118        result.hist.into_pyarray(py),
1119    ))
1120}
1121
1122#[cfg(feature = "python")]
1123#[pyfunction(name = "adaptive_macd_batch")]
1124#[pyo3(signature = (data, length_range=(DEFAULT_LENGTH, DEFAULT_LENGTH, 0), fast_period_range=(DEFAULT_FAST_PERIOD, DEFAULT_FAST_PERIOD, 0), slow_period_range=(DEFAULT_SLOW_PERIOD, DEFAULT_SLOW_PERIOD, 0), signal_period_range=(DEFAULT_SIGNAL_PERIOD, DEFAULT_SIGNAL_PERIOD, 0), kernel=None))]
1125pub fn adaptive_macd_batch_py<'py>(
1126    py: Python<'py>,
1127    data: PyReadonlyArray1<'py, f64>,
1128    length_range: (usize, usize, usize),
1129    fast_period_range: (usize, usize, usize),
1130    slow_period_range: (usize, usize, usize),
1131    signal_period_range: (usize, usize, usize),
1132    kernel: Option<&str>,
1133) -> PyResult<Bound<'py, PyDict>> {
1134    let slice_in = data.as_slice()?;
1135    let _ = validate_kernel(kernel, true)?;
1136    let sweep = AdaptiveMacdBatchRange {
1137        length: length_range,
1138        fast_period: fast_period_range,
1139        slow_period: slow_period_range,
1140        signal_period: signal_period_range,
1141    };
1142    let rows = expand_grid(&sweep)
1143        .map_err(|e| PyValueError::new_err(e.to_string()))?
1144        .len();
1145    let cols = slice_in.len();
1146    let total = rows
1147        .checked_mul(cols)
1148        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1149
1150    let macd_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1151    let signal_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1152    let hist_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1153
1154    let macd_slice = unsafe { macd_arr.as_slice_mut()? };
1155    let signal_slice = unsafe { signal_arr.as_slice_mut()? };
1156    let hist_slice = unsafe { hist_arr.as_slice_mut()? };
1157
1158    let combos = py
1159        .allow_threads(|| {
1160            adaptive_macd_batch_inner_into(
1161                slice_in,
1162                &sweep,
1163                true,
1164                macd_slice,
1165                signal_slice,
1166                hist_slice,
1167            )
1168        })
1169        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1170
1171    let dict = PyDict::new(py);
1172    dict.set_item("macd", macd_arr.reshape((rows, cols))?)?;
1173    dict.set_item("signal", signal_arr.reshape((rows, cols))?)?;
1174    dict.set_item("hist", hist_arr.reshape((rows, cols))?)?;
1175    dict.set_item(
1176        "lengths",
1177        combos
1178            .iter()
1179            .map(|params| params.length.unwrap_or(DEFAULT_LENGTH) as u64)
1180            .collect::<Vec<_>>()
1181            .into_pyarray(py),
1182    )?;
1183    dict.set_item(
1184        "fast_periods",
1185        combos
1186            .iter()
1187            .map(|params| params.fast_period.unwrap_or(DEFAULT_FAST_PERIOD) as u64)
1188            .collect::<Vec<_>>()
1189            .into_pyarray(py),
1190    )?;
1191    dict.set_item(
1192        "slow_periods",
1193        combos
1194            .iter()
1195            .map(|params| params.slow_period.unwrap_or(DEFAULT_SLOW_PERIOD) as u64)
1196            .collect::<Vec<_>>()
1197            .into_pyarray(py),
1198    )?;
1199    dict.set_item(
1200        "signal_periods",
1201        combos
1202            .iter()
1203            .map(|params| params.signal_period.unwrap_or(DEFAULT_SIGNAL_PERIOD) as u64)
1204            .collect::<Vec<_>>()
1205            .into_pyarray(py),
1206    )?;
1207    dict.set_item("rows", rows)?;
1208    dict.set_item("cols", cols)?;
1209    Ok(dict)
1210}
1211
1212#[cfg(feature = "python")]
1213#[pyclass(name = "AdaptiveMacdStream")]
1214pub struct AdaptiveMacdStreamPy {
1215    inner: AdaptiveMacdStream,
1216}
1217
1218#[cfg(feature = "python")]
1219#[pymethods]
1220impl AdaptiveMacdStreamPy {
1221    #[new]
1222    #[pyo3(signature = (length=DEFAULT_LENGTH, fast_period=DEFAULT_FAST_PERIOD, slow_period=DEFAULT_SLOW_PERIOD, signal_period=DEFAULT_SIGNAL_PERIOD))]
1223    pub fn new(
1224        length: usize,
1225        fast_period: usize,
1226        slow_period: usize,
1227        signal_period: usize,
1228    ) -> PyResult<Self> {
1229        let inner = AdaptiveMacdStream::try_new(AdaptiveMacdParams {
1230            length: Some(length),
1231            fast_period: Some(fast_period),
1232            slow_period: Some(slow_period),
1233            signal_period: Some(signal_period),
1234        })
1235        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1236        Ok(Self { inner })
1237    }
1238
1239    pub fn update(&mut self, value: f64) -> Option<(f64, f64, f64)> {
1240        self.inner.update(value)
1241    }
1242}
1243
1244#[cfg(feature = "python")]
1245pub fn register_adaptive_macd_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
1246    m.add_function(wrap_pyfunction!(adaptive_macd_py, m)?)?;
1247    m.add_function(wrap_pyfunction!(adaptive_macd_batch_py, m)?)?;
1248    m.add_class::<AdaptiveMacdStreamPy>()?;
1249    Ok(())
1250}
1251
1252#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1253#[derive(Serialize, Deserialize)]
1254pub struct AdaptiveMacdBatchConfig {
1255    pub length_range: (usize, usize, usize),
1256    pub fast_period_range: (usize, usize, usize),
1257    pub slow_period_range: (usize, usize, usize),
1258    pub signal_period_range: (usize, usize, usize),
1259}
1260
1261#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1262#[derive(Serialize, Deserialize)]
1263pub struct AdaptiveMacdBatchJsOutput {
1264    pub macd: Vec<f64>,
1265    pub signal: Vec<f64>,
1266    pub hist: Vec<f64>,
1267    pub combos: Vec<AdaptiveMacdParams>,
1268    pub rows: usize,
1269    pub cols: usize,
1270}
1271
1272#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1273#[wasm_bindgen]
1274pub fn adaptive_macd_js(
1275    data: &[f64],
1276    length: usize,
1277    fast_period: usize,
1278    slow_period: usize,
1279    signal_period: usize,
1280) -> Result<JsValue, JsValue> {
1281    let input = AdaptiveMacdInput::from_slice(
1282        data,
1283        AdaptiveMacdParams {
1284            length: Some(length),
1285            fast_period: Some(fast_period),
1286            slow_period: Some(slow_period),
1287            signal_period: Some(signal_period),
1288        },
1289    );
1290    let output = adaptive_macd_with_kernel(&input, Kernel::Auto)
1291        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1292    serde_wasm_bindgen::to_value(&output)
1293        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1294}
1295
1296#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1297#[wasm_bindgen]
1298pub fn adaptive_macd_alloc(len: usize) -> *mut f64 {
1299    let mut vec = Vec::<f64>::with_capacity(len);
1300    let ptr = vec.as_mut_ptr();
1301    std::mem::forget(vec);
1302    ptr
1303}
1304
1305#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1306#[wasm_bindgen]
1307pub fn adaptive_macd_free(ptr: *mut f64, len: usize) {
1308    if !ptr.is_null() {
1309        unsafe {
1310            let _ = Vec::from_raw_parts(ptr, len, len);
1311        }
1312    }
1313}
1314
1315#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1316#[wasm_bindgen]
1317pub fn adaptive_macd_into(
1318    in_ptr: *const f64,
1319    macd_ptr: *mut f64,
1320    signal_ptr: *mut f64,
1321    hist_ptr: *mut f64,
1322    len: usize,
1323    length: usize,
1324    fast_period: usize,
1325    slow_period: usize,
1326    signal_period: usize,
1327) -> Result<(), JsValue> {
1328    if in_ptr.is_null() || macd_ptr.is_null() || signal_ptr.is_null() || hist_ptr.is_null() {
1329        return Err(JsValue::from_str("Null pointer provided"));
1330    }
1331
1332    unsafe {
1333        let data = std::slice::from_raw_parts(in_ptr, len);
1334        let input = AdaptiveMacdInput::from_slice(
1335            data,
1336            AdaptiveMacdParams {
1337                length: Some(length),
1338                fast_period: Some(fast_period),
1339                slow_period: Some(slow_period),
1340                signal_period: Some(signal_period),
1341            },
1342        );
1343
1344        let aliased = in_ptr == macd_ptr
1345            || in_ptr == signal_ptr
1346            || in_ptr == hist_ptr
1347            || macd_ptr == signal_ptr
1348            || macd_ptr == hist_ptr
1349            || signal_ptr == hist_ptr;
1350
1351        if aliased {
1352            let out = adaptive_macd_with_kernel(&input, Kernel::Auto)
1353                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1354            std::slice::from_raw_parts_mut(macd_ptr, len).copy_from_slice(&out.macd);
1355            std::slice::from_raw_parts_mut(signal_ptr, len).copy_from_slice(&out.signal);
1356            std::slice::from_raw_parts_mut(hist_ptr, len).copy_from_slice(&out.hist);
1357        } else {
1358            let macd_out = std::slice::from_raw_parts_mut(macd_ptr, len);
1359            let signal_out = std::slice::from_raw_parts_mut(signal_ptr, len);
1360            let hist_out = std::slice::from_raw_parts_mut(hist_ptr, len);
1361            adaptive_macd_into_slice(macd_out, signal_out, hist_out, &input, Kernel::Auto)
1362                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1363        }
1364    }
1365
1366    Ok(())
1367}
1368
1369#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1370#[wasm_bindgen(js_name = adaptive_macd_batch)]
1371pub fn adaptive_macd_batch_unified_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
1372    let config: AdaptiveMacdBatchConfig = serde_wasm_bindgen::from_value(config)
1373        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
1374    let sweep = AdaptiveMacdBatchRange {
1375        length: config.length_range,
1376        fast_period: config.fast_period_range,
1377        slow_period: config.slow_period_range,
1378        signal_period: config.signal_period_range,
1379    };
1380    let output = adaptive_macd_batch_with_kernel(data, &sweep, Kernel::Auto)
1381        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1382    let js_output = AdaptiveMacdBatchJsOutput {
1383        macd: output.macd,
1384        signal: output.signal,
1385        hist: output.hist,
1386        combos: output.combos,
1387        rows: output.rows,
1388        cols: output.cols,
1389    };
1390    serde_wasm_bindgen::to_value(&js_output)
1391        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1392}
1393
1394#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1395#[wasm_bindgen]
1396pub fn adaptive_macd_batch_into(
1397    in_ptr: *const f64,
1398    macd_ptr: *mut f64,
1399    signal_ptr: *mut f64,
1400    hist_ptr: *mut f64,
1401    len: usize,
1402    length_start: usize,
1403    length_end: usize,
1404    length_step: usize,
1405    fast_period_start: usize,
1406    fast_period_end: usize,
1407    fast_period_step: usize,
1408    slow_period_start: usize,
1409    slow_period_end: usize,
1410    slow_period_step: usize,
1411    signal_period_start: usize,
1412    signal_period_end: usize,
1413    signal_period_step: usize,
1414) -> Result<usize, JsValue> {
1415    if in_ptr.is_null() || macd_ptr.is_null() || signal_ptr.is_null() || hist_ptr.is_null() {
1416        return Err(JsValue::from_str("Null pointer provided"));
1417    }
1418
1419    let sweep = AdaptiveMacdBatchRange {
1420        length: (length_start, length_end, length_step),
1421        fast_period: (fast_period_start, fast_period_end, fast_period_step),
1422        slow_period: (slow_period_start, slow_period_end, slow_period_step),
1423        signal_period: (signal_period_start, signal_period_end, signal_period_step),
1424    };
1425    let rows = expand_grid(&sweep)
1426        .map_err(|e| JsValue::from_str(&e.to_string()))?
1427        .len();
1428    let total = rows
1429        .checked_mul(len)
1430        .ok_or_else(|| JsValue::from_str("rows*len overflow"))?;
1431
1432    unsafe {
1433        let data = std::slice::from_raw_parts(in_ptr, len);
1434        let macd_out = std::slice::from_raw_parts_mut(macd_ptr, total);
1435        let signal_out = std::slice::from_raw_parts_mut(signal_ptr, total);
1436        let hist_out = std::slice::from_raw_parts_mut(hist_ptr, total);
1437        adaptive_macd_batch_inner_into(data, &sweep, false, macd_out, signal_out, hist_out)
1438            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1439    }
1440
1441    Ok(rows)
1442}
1443
1444#[cfg(test)]
1445mod tests {
1446    use super::*;
1447    use crate::utilities::data_loader::read_candles_from_csv;
1448
1449    fn linear_data(size: usize) -> Vec<f64> {
1450        (0..size).map(|i| i as f64).collect()
1451    }
1452
1453    fn constant_data(size: usize, value: f64) -> Vec<f64> {
1454        vec![value; size]
1455    }
1456
1457    fn linear_reference(
1458        size: usize,
1459        length: usize,
1460        fast_period: usize,
1461        slow_period: usize,
1462        signal_period: usize,
1463    ) -> AdaptiveMacdOutput {
1464        let spec = build_spec(fast_period, slow_period);
1465        let mut macd = vec![f64::NAN; size];
1466        let mut signal = vec![f64::NAN; size];
1467        let mut hist = vec![f64::NAN; size];
1468        let mut signal_state = EmaLikeState::new(signal_period);
1469        let k = spec.trend_coeff;
1470        for i in (length - 1)..size {
1471            let prev1 = if i >= 1 && macd[i - 1].is_finite() {
1472                macd[i - 1]
1473            } else {
1474                0.0
1475            };
1476            let prev2 = if i >= 2 && macd[i - 2].is_finite() {
1477                macd[i - 2]
1478            } else {
1479                0.0
1480            };
1481            macd[i] = spec.delta_coeff + spec.recur_coeff * prev1 - k * prev2;
1482            signal[i] = signal_state.update(macd[i]).unwrap_or(f64::NAN);
1483            hist[i] = macd[i] - signal[i];
1484        }
1485        AdaptiveMacdOutput { macd, signal, hist }
1486    }
1487
1488    fn assert_close(actual: &[f64], expected: &[f64], tol: f64) {
1489        assert_eq!(actual.len(), expected.len());
1490        for (idx, (&a, &e)) in actual.iter().zip(expected.iter()).enumerate() {
1491            if a.is_nan() || e.is_nan() {
1492                assert!(
1493                    a.is_nan() && e.is_nan(),
1494                    "NaN mismatch at idx {}: actual={} expected={}",
1495                    idx,
1496                    a,
1497                    e
1498                );
1499            } else {
1500                assert!(
1501                    (a - e).abs() <= tol,
1502                    "value mismatch at idx {}: actual={} expected={} tol={}",
1503                    idx,
1504                    a,
1505                    e,
1506                    tol
1507                );
1508            }
1509        }
1510    }
1511
1512    #[test]
1513    fn adaptive_macd_linear_trend_matches_reference() -> Result<(), Box<dyn StdError>> {
1514        let data = linear_data(32);
1515        let params = AdaptiveMacdParams {
1516            length: Some(5),
1517            fast_period: Some(4),
1518            slow_period: Some(9),
1519            signal_period: Some(3),
1520        };
1521        let input = AdaptiveMacdInput::from_slice(&data, params.clone());
1522        let output = adaptive_macd(&input)?;
1523        let expected = linear_reference(32, 5, 4, 9, 3);
1524        assert_close(&output.macd, &expected.macd, 1e-12);
1525        assert_close(&output.signal, &expected.signal, 1e-12);
1526        assert_close(&output.hist, &expected.hist, 1e-12);
1527        Ok(())
1528    }
1529
1530    #[test]
1531    fn adaptive_macd_constant_series_flattens_to_zero() -> Result<(), Box<dyn StdError>> {
1532        let data = constant_data(24, 100.0);
1533        let input = AdaptiveMacdInput::from_slice(
1534            &data,
1535            AdaptiveMacdParams {
1536                length: Some(6),
1537                fast_period: Some(5),
1538                slow_period: Some(10),
1539                signal_period: Some(4),
1540            },
1541        );
1542        let output = adaptive_macd(&input)?;
1543        for i in 0..5 {
1544            assert!(output.macd[i].is_nan());
1545            assert!(output.signal[i].is_nan());
1546            assert!(output.hist[i].is_nan());
1547        }
1548        for i in 5..data.len() {
1549            assert!(output.macd[i].abs() <= 1e-12);
1550            assert!(output.signal[i].abs() <= 1e-12);
1551            assert!(output.hist[i].abs() <= 1e-12);
1552        }
1553        Ok(())
1554    }
1555
1556    #[test]
1557    fn adaptive_macd_nan_gap_restarts_macd() -> Result<(), Box<dyn StdError>> {
1558        let data = vec![
1559            1.0,
1560            2.0,
1561            3.0,
1562            4.0,
1563            5.0,
1564            6.0,
1565            f64::NAN,
1566            8.0,
1567            9.0,
1568            10.0,
1569            11.0,
1570            12.0,
1571            13.0,
1572        ];
1573        let input = AdaptiveMacdInput::from_slice(
1574            &data,
1575            AdaptiveMacdParams {
1576                length: Some(4),
1577                fast_period: Some(3),
1578                slow_period: Some(6),
1579                signal_period: Some(3),
1580            },
1581        );
1582        let output = adaptive_macd(&input)?;
1583        assert!(output.macd[..3].iter().all(|v| v.is_nan()));
1584        assert!(output.macd[3].is_finite());
1585        assert!(output.macd[6].is_nan());
1586        assert!(output.macd[7].is_nan());
1587        assert!(output.macd[8].is_nan());
1588        assert!(output.macd[9].is_nan());
1589        assert!(output.macd[10].is_finite());
1590        assert!(output.hist[6].is_nan());
1591        Ok(())
1592    }
1593
1594    #[test]
1595    fn adaptive_macd_into_matches_single() -> Result<(), Box<dyn StdError>> {
1596        let data = linear_data(28);
1597        let input = AdaptiveMacdInput::from_slice(
1598            &data,
1599            AdaptiveMacdParams {
1600                length: Some(5),
1601                fast_period: Some(4),
1602                slow_period: Some(8),
1603                signal_period: Some(3),
1604            },
1605        );
1606        let output = adaptive_macd(&input)?;
1607        let mut macd = vec![0.0; data.len()];
1608        let mut signal = vec![0.0; data.len()];
1609        let mut hist = vec![0.0; data.len()];
1610        adaptive_macd_into_slice(&mut macd, &mut signal, &mut hist, &input, Kernel::Auto)?;
1611        assert_close(&macd, &output.macd, 1e-12);
1612        assert_close(&signal, &output.signal, 1e-12);
1613        assert_close(&hist, &output.hist, 1e-12);
1614        Ok(())
1615    }
1616
1617    #[test]
1618    fn adaptive_macd_stream_matches_batch() -> Result<(), Box<dyn StdError>> {
1619        let data = linear_data(28);
1620        let params = AdaptiveMacdParams {
1621            length: Some(5),
1622            fast_period: Some(4),
1623            slow_period: Some(8),
1624            signal_period: Some(3),
1625        };
1626        let input = AdaptiveMacdInput::from_slice(&data, params.clone());
1627        let batch = adaptive_macd(&input)?;
1628        let mut stream = AdaptiveMacdStream::try_new(params)?;
1629        let mut macd = Vec::with_capacity(data.len());
1630        let mut signal = Vec::with_capacity(data.len());
1631        let mut hist = Vec::with_capacity(data.len());
1632        for value in data {
1633            match stream.update(value) {
1634                Some((m, s, h)) => {
1635                    macd.push(m);
1636                    signal.push(s);
1637                    hist.push(h);
1638                }
1639                None => {
1640                    macd.push(f64::NAN);
1641                    signal.push(f64::NAN);
1642                    hist.push(f64::NAN);
1643                }
1644            }
1645        }
1646        assert_close(&macd, &batch.macd, 1e-12);
1647        assert_close(&signal, &batch.signal, 1e-12);
1648        assert_close(&hist, &batch.hist, 1e-12);
1649        Ok(())
1650    }
1651
1652    #[test]
1653    fn adaptive_macd_batch_matches_single() -> Result<(), Box<dyn StdError>> {
1654        let data = linear_data(26);
1655        let sweep = AdaptiveMacdBatchRange {
1656            length: (4, 5, 1),
1657            fast_period: (3, 4, 1),
1658            slow_period: (6, 7, 1),
1659            signal_period: (3, 3, 0),
1660        };
1661        let batch = adaptive_macd_batch_with_kernel(&data, &sweep, Kernel::ScalarBatch)?;
1662        assert_eq!(batch.rows, 8);
1663        assert_eq!(batch.cols, data.len());
1664        for (row, params) in batch.combos.iter().enumerate() {
1665            let input = AdaptiveMacdInput::from_slice(&data, params.clone());
1666            let single = adaptive_macd(&input)?;
1667            let start = row * batch.cols;
1668            let end = start + batch.cols;
1669            assert_close(&batch.macd[start..end], &single.macd, 1e-12);
1670            assert_close(&batch.signal[start..end], &single.signal, 1e-12);
1671            assert_close(&batch.hist[start..end], &single.hist, 1e-12);
1672        }
1673        Ok(())
1674    }
1675
1676    #[test]
1677    fn adaptive_macd_invalid_period_errors() {
1678        let data = linear_data(10);
1679        let input = AdaptiveMacdInput::from_slice(
1680            &data,
1681            AdaptiveMacdParams {
1682                length: Some(1),
1683                fast_period: Some(3),
1684                slow_period: Some(6),
1685                signal_period: Some(3),
1686            },
1687        );
1688        assert!(matches!(
1689            adaptive_macd(&input),
1690            Err(AdaptiveMacdError::InvalidPeriod { .. })
1691        ));
1692    }
1693
1694    #[test]
1695    fn adaptive_macd_all_nan_errors() {
1696        let data = vec![f64::NAN; 12];
1697        let input = AdaptiveMacdInput::from_slice(&data, AdaptiveMacdParams::default());
1698        assert!(matches!(
1699            adaptive_macd(&input),
1700            Err(AdaptiveMacdError::AllValuesNaN)
1701        ));
1702    }
1703
1704    #[test]
1705    fn adaptive_macd_default_candles_smoke() -> Result<(), Box<dyn StdError>> {
1706        let candles = read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv")?;
1707        let input = AdaptiveMacdInput::with_default_candles(&candles);
1708        let output = adaptive_macd(&input)?;
1709        assert_eq!(output.macd.len(), candles.close.len());
1710        assert_eq!(output.signal.len(), candles.close.len());
1711        assert_eq!(output.hist.len(), candles.close.len());
1712        Ok(())
1713    }
1714}