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(¶ms)?,
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}