1use crate::indicators::moving_averages::ma::{ma, MaData};
2use crate::utilities::data_loader::{source_type, Candles};
3use crate::utilities::enums::Kernel;
4use crate::utilities::helpers::{
5 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
6 make_uninit_matrix,
7};
8use aligned_vec::{AVec, CACHELINE_ALIGN};
9#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
10use core::arch::x86_64::*;
11#[cfg(not(target_arch = "wasm32"))]
12use rayon::prelude::*;
13use std::convert::AsRef;
14use std::error::Error;
15use std::mem::ManuallyDrop;
16use thiserror::Error;
17
18#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
19use serde::{Deserialize, Serialize};
20#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
21use wasm_bindgen::prelude::*;
22
23impl<'a> AsRef<[f64]> for CoppockInput<'a> {
24 #[inline(always)]
25 fn as_ref(&self) -> &[f64] {
26 match &self.data {
27 CoppockData::Slice(slice) => slice,
28 CoppockData::Candles { candles, source } => source_type(candles, source),
29 }
30 }
31}
32
33#[derive(Debug, Clone)]
34pub enum CoppockData<'a> {
35 Candles {
36 candles: &'a Candles,
37 source: &'a str,
38 },
39 Slice(&'a [f64]),
40}
41
42#[derive(Debug, Clone)]
43pub struct CoppockOutput {
44 pub values: Vec<f64>,
45}
46
47#[derive(Debug, Clone)]
48#[cfg_attr(
49 all(target_arch = "wasm32", feature = "wasm"),
50 derive(Serialize, Deserialize)
51)]
52pub struct CoppockParams {
53 pub short_roc_period: Option<usize>,
54 pub long_roc_period: Option<usize>,
55 pub ma_period: Option<usize>,
56 pub ma_type: Option<String>,
57}
58
59impl Default for CoppockParams {
60 fn default() -> Self {
61 Self {
62 short_roc_period: Some(11),
63 long_roc_period: Some(14),
64 ma_period: Some(10),
65 ma_type: Some("wma".to_string()),
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
71pub struct CoppockInput<'a> {
72 pub data: CoppockData<'a>,
73 pub params: CoppockParams,
74}
75
76impl<'a> CoppockInput<'a> {
77 #[inline]
78 pub fn from_candles(c: &'a Candles, s: &'a str, p: CoppockParams) -> Self {
79 Self {
80 data: CoppockData::Candles {
81 candles: c,
82 source: s,
83 },
84 params: p,
85 }
86 }
87 #[inline]
88 pub fn from_slice(sl: &'a [f64], p: CoppockParams) -> Self {
89 Self {
90 data: CoppockData::Slice(sl),
91 params: p,
92 }
93 }
94 #[inline]
95 pub fn with_default_candles(c: &'a Candles) -> Self {
96 Self::from_candles(c, "close", CoppockParams::default())
97 }
98 #[inline]
99 pub fn get_short_roc_period(&self) -> usize {
100 self.params.short_roc_period.unwrap_or(11)
101 }
102 #[inline]
103 pub fn get_long_roc_period(&self) -> usize {
104 self.params.long_roc_period.unwrap_or(14)
105 }
106 #[inline]
107 pub fn get_ma_period(&self) -> usize {
108 self.params.ma_period.unwrap_or(10)
109 }
110 #[inline]
111 pub fn get_ma_type(&self) -> &str {
112 self.params.ma_type.as_deref().unwrap_or("wma")
113 }
114}
115
116#[derive(Clone, Debug)]
117pub struct CoppockBuilder {
118 short: Option<usize>,
119 long: Option<usize>,
120 ma: Option<usize>,
121 ma_type: Option<String>,
122 kernel: Kernel,
123}
124
125impl Default for CoppockBuilder {
126 fn default() -> Self {
127 Self {
128 short: None,
129 long: None,
130 ma: None,
131 ma_type: None,
132 kernel: Kernel::Auto,
133 }
134 }
135}
136
137impl CoppockBuilder {
138 #[inline(always)]
139 pub fn new() -> Self {
140 Self::default()
141 }
142 #[inline(always)]
143 pub fn short_roc_period(mut self, n: usize) -> Self {
144 self.short = Some(n);
145 self
146 }
147 #[inline(always)]
148 pub fn long_roc_period(mut self, n: usize) -> Self {
149 self.long = Some(n);
150 self
151 }
152 #[inline(always)]
153 pub fn ma_period(mut self, n: usize) -> Self {
154 self.ma = Some(n);
155 self
156 }
157 #[inline(always)]
158 pub fn ma_type<T: Into<String>>(mut self, t: T) -> Self {
159 self.ma_type = Some(t.into());
160 self
161 }
162 #[inline(always)]
163 pub fn kernel(mut self, k: Kernel) -> Self {
164 self.kernel = k;
165 self
166 }
167 #[inline(always)]
168 pub fn apply(self, c: &Candles) -> Result<CoppockOutput, CoppockError> {
169 let p = CoppockParams {
170 short_roc_period: self.short,
171 long_roc_period: self.long,
172 ma_period: self.ma,
173 ma_type: self.ma_type,
174 };
175 let i = CoppockInput::from_candles(c, "close", p);
176 coppock_with_kernel(&i, self.kernel)
177 }
178 #[inline(always)]
179 pub fn apply_slice(self, d: &[f64]) -> Result<CoppockOutput, CoppockError> {
180 let p = CoppockParams {
181 short_roc_period: self.short,
182 long_roc_period: self.long,
183 ma_period: self.ma,
184 ma_type: self.ma_type,
185 };
186 let i = CoppockInput::from_slice(d, p);
187 coppock_with_kernel(&i, self.kernel)
188 }
189 #[inline(always)]
190 pub fn into_stream(self) -> Result<CoppockStream, CoppockError> {
191 let p = CoppockParams {
192 short_roc_period: self.short,
193 long_roc_period: self.long,
194 ma_period: self.ma,
195 ma_type: self.ma_type,
196 };
197 CoppockStream::try_new(p)
198 }
199}
200
201#[derive(Debug, Error)]
202pub enum CoppockError {
203 #[error("coppock: Empty data provided.")]
204 EmptyData,
205 #[error("coppock: All values are NaN.")]
206 AllValuesNaN,
207 #[error("coppock: Not enough valid data: needed = {needed}, valid = {valid}")]
208 NotEnoughValidData { needed: usize, valid: usize },
209 #[error(
210 "coppock: Invalid period usage => short={short}, long={long}, ma={ma}, data_len={data_len}"
211 )]
212 InvalidPeriod {
213 short: usize,
214 long: usize,
215 ma: usize,
216 data_len: usize,
217 },
218 #[error("coppock: Output length mismatch: expected = {expected}, got = {got}")]
219 OutputLengthMismatch { expected: usize, got: usize },
220 #[error("coppock: Invalid range: start={start}, end={end}, step={step}")]
221 InvalidRange {
222 start: usize,
223 end: usize,
224 step: usize,
225 },
226 #[error("coppock: Invalid kernel for batch: {0:?}")]
227 InvalidKernelForBatch(Kernel),
228 #[error("coppock: Invalid input: {0}")]
229 InvalidInput(String),
230 #[error("coppock: Underlying MA error: {0}")]
231 MaError(#[from] Box<dyn Error + Send + Sync>),
232}
233
234#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
235impl From<CoppockError> for JsValue {
236 fn from(err: CoppockError) -> Self {
237 JsValue::from_str(&err.to_string())
238 }
239}
240
241#[inline]
242pub fn coppock(input: &CoppockInput) -> Result<CoppockOutput, CoppockError> {
243 coppock_with_kernel(input, Kernel::Auto)
244}
245
246pub fn coppock_with_kernel(
247 input: &CoppockInput,
248 kernel: Kernel,
249) -> Result<CoppockOutput, CoppockError> {
250 let data: &[f64] = input.as_ref();
251 if data.is_empty() {
252 return Err(CoppockError::EmptyData);
253 }
254
255 let short = input.get_short_roc_period();
256 let long = input.get_long_roc_period();
257 let ma_p = input.get_ma_period();
258 let data_len = data.len();
259
260 if short == 0
261 || long == 0
262 || ma_p == 0
263 || short > data_len
264 || long > data_len
265 || ma_p > data_len
266 {
267 return Err(CoppockError::InvalidPeriod {
268 short,
269 long,
270 ma: ma_p,
271 data_len,
272 });
273 }
274
275 let first = data
276 .iter()
277 .position(|&x| !x.is_nan())
278 .ok_or(CoppockError::AllValuesNaN)?;
279 let largest_roc = short.max(long);
280 if (data_len - first) < largest_roc {
281 return Err(CoppockError::NotEnoughValidData {
282 needed: largest_roc,
283 valid: data_len - first,
284 });
285 }
286
287 let warmup_period = first + largest_roc;
288
289 let mut sum_roc = alloc_with_nan_prefix(data_len, warmup_period);
290
291 unsafe {
292 match match kernel {
293 Kernel::Auto => Kernel::Scalar,
294 other => other,
295 } {
296 Kernel::Scalar | Kernel::ScalarBatch => {
297 coppock_scalar(data, short, long, first, &mut sum_roc)
298 }
299 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
300 Kernel::Avx2 | Kernel::Avx2Batch => {
301 coppock_avx2(data, short, long, first, &mut sum_roc)
302 }
303 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
304 Kernel::Avx512 | Kernel::Avx512Batch => {
305 coppock_avx512(data, short, long, first, &mut sum_roc)
306 }
307 _ => coppock_scalar(data, short, long, first, &mut sum_roc),
308 }
309 }
310
311 let ma_type = input.get_ma_type();
312 let smoothed = ma(&ma_type, MaData::Slice(&sum_roc), ma_p).map_err(|e| {
313 use std::fmt;
314 #[derive(Debug)]
315 struct MaErrorWrapper(String);
316 impl fmt::Display for MaErrorWrapper {
317 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318 write!(f, "{}", self.0)
319 }
320 }
321 impl Error for MaErrorWrapper {}
322 CoppockError::MaError(Box::new(MaErrorWrapper(e.to_string())))
323 })?;
324
325 Ok(CoppockOutput { values: smoothed })
326}
327
328#[inline]
329pub fn coppock_into_slice(
330 out: &mut [f64],
331 input: &CoppockInput,
332 kernel: Kernel,
333) -> Result<(), CoppockError> {
334 let data: &[f64] = input.as_ref();
335 if data.is_empty() {
336 return Err(CoppockError::EmptyData);
337 }
338 if out.len() != data.len() {
339 return Err(CoppockError::OutputLengthMismatch {
340 expected: data.len(),
341 got: out.len(),
342 });
343 }
344
345 let short = input.get_short_roc_period();
346 let long = input.get_long_roc_period();
347 let ma_p = input.get_ma_period();
348 let data_len = data.len();
349
350 if short == 0
351 || long == 0
352 || ma_p == 0
353 || short > data_len
354 || long > data_len
355 || ma_p > data_len
356 {
357 return Err(CoppockError::InvalidPeriod {
358 short,
359 long,
360 ma: ma_p,
361 data_len,
362 });
363 }
364
365 let first = data
366 .iter()
367 .position(|&x| !x.is_nan())
368 .ok_or(CoppockError::AllValuesNaN)?;
369 let largest_roc = short.max(long);
370 if (data_len - first) < largest_roc {
371 return Err(CoppockError::NotEnoughValidData {
372 needed: largest_roc,
373 valid: data_len - first,
374 });
375 }
376
377 let warmup_period = first + largest_roc;
378
379 let mut sum_roc = alloc_with_nan_prefix(data_len, warmup_period);
380
381 let resolved_kernel = match kernel {
382 Kernel::Auto => Kernel::Scalar,
383 other => other,
384 };
385
386 let ma_type = input.get_ma_type();
387
388 if resolved_kernel == Kernel::Scalar
389 && (ma_type == "wma" || ma_type == "sma" || ma_type == "ema")
390 {
391 unsafe {
392 return coppock_scalar_classic(data, short, long, ma_p, ma_type, first, out);
393 }
394 }
395
396 unsafe {
397 match resolved_kernel {
398 Kernel::Scalar | Kernel::ScalarBatch => {
399 coppock_scalar(data, short, long, first, &mut sum_roc)
400 }
401 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
402 Kernel::Avx2 | Kernel::Avx2Batch => {
403 coppock_avx2(data, short, long, first, &mut sum_roc)
404 }
405 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
406 Kernel::Avx512 | Kernel::Avx512Batch => {
407 coppock_avx512(data, short, long, first, &mut sum_roc)
408 }
409 _ => coppock_scalar(data, short, long, first, &mut sum_roc),
410 }
411 }
412
413 let smoothed = ma(ma_type, MaData::Slice(&sum_roc), ma_p).map_err(|e| {
414 use std::fmt;
415 #[derive(Debug)]
416 struct MaErrorWrapper(String);
417 impl fmt::Display for MaErrorWrapper {
418 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
419 write!(f, "{}", self.0)
420 }
421 }
422 impl Error for MaErrorWrapper {}
423 CoppockError::MaError(Box::new(MaErrorWrapper(e.to_string())))
424 })?;
425
426 out.copy_from_slice(&smoothed);
427 Ok(())
428}
429
430#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
431#[inline]
432pub fn coppock_into(input: &CoppockInput, out: &mut [f64]) -> Result<(), CoppockError> {
433 coppock_into_slice(out, input, Kernel::ScalarBatch)
434}
435
436pub unsafe fn coppock_scalar_classic(
437 data: &[f64],
438 short: usize,
439 long: usize,
440 ma_period: usize,
441 ma_type: &str,
442 first: usize,
443 out: &mut [f64],
444) -> Result<(), CoppockError> {
445 let len = data.len();
446 let largest_roc = short.max(long);
447 let warmup_period = first + largest_roc;
448
449 let mut sum_roc = alloc_with_nan_prefix(len, warmup_period);
450 let start_idx = first + largest_roc;
451
452 for i in start_idx..len {
453 let current = data[i];
454 let prev_short = data[i - short];
455 let short_val = ((current / prev_short) - 1.0) * 100.0;
456 let prev_long = data[i - long];
457 let long_val = ((current / prev_long) - 1.0) * 100.0;
458 sum_roc[i] = short_val + long_val;
459 }
460
461 match ma_type {
462 "wma" => {
463 let warmup_final = warmup_period + ma_period - 1;
464 for i in 0..warmup_final.min(len) {
465 out[i] = f64::NAN;
466 }
467
468 let weight_sum = (ma_period * (ma_period + 1)) as f64 / 2.0;
469
470 for i in warmup_final..len {
471 let mut weighted_sum = 0.0;
472 let mut has_nan = false;
473
474 for j in 0..ma_period {
475 let idx = i - ma_period + 1 + j;
476 if sum_roc[idx].is_nan() {
477 has_nan = true;
478 break;
479 }
480 weighted_sum += sum_roc[idx] * (j + 1) as f64;
481 }
482
483 out[i] = if has_nan {
484 f64::NAN
485 } else {
486 weighted_sum / weight_sum
487 };
488 }
489 }
490 "sma" => {
491 let warmup_final = warmup_period + ma_period - 1;
492 for i in 0..warmup_final.min(len) {
493 out[i] = f64::NAN;
494 }
495
496 let mut sum = 0.0;
497 for i in warmup_period..(warmup_period + ma_period.min(len - warmup_period)) {
498 if !sum_roc[i].is_nan() {
499 sum += sum_roc[i];
500 }
501 }
502
503 if warmup_final < len {
504 out[warmup_final] = sum / ma_period as f64;
505
506 for i in (warmup_final + 1)..len {
507 if !sum_roc[i].is_nan() && !sum_roc[i - ma_period].is_nan() {
508 sum += sum_roc[i] - sum_roc[i - ma_period];
509 out[i] = sum / ma_period as f64;
510 } else {
511 out[i] = f64::NAN;
512 }
513 }
514 }
515 }
516 "ema" => {
517 let warmup_final = warmup_period + ma_period - 1;
518 for i in 0..warmup_final.min(len) {
519 out[i] = f64::NAN;
520 }
521
522 let alpha = 2.0 / (ma_period as f64 + 1.0);
523 let mut ema_value = f64::NAN;
524
525 for i in warmup_period..len {
526 if !sum_roc[i].is_nan() {
527 ema_value = sum_roc[i];
528 out[i] = ema_value;
529
530 for j in (i + 1)..len {
531 if !sum_roc[j].is_nan() {
532 ema_value = alpha * sum_roc[j] + (1.0 - alpha) * ema_value;
533 out[j] = ema_value;
534 } else {
535 out[j] = f64::NAN;
536 }
537 }
538 break;
539 }
540 }
541 }
542 _ => {
543 let smoothed = ma(ma_type, MaData::Slice(&sum_roc), ma_period).map_err(|e| {
544 CoppockError::MaError(Box::new(std::io::Error::new(
545 std::io::ErrorKind::Other,
546 e.to_string(),
547 )))
548 })?;
549 out.copy_from_slice(&smoothed);
550 }
551 }
552
553 Ok(())
554}
555
556#[inline]
557pub fn coppock_scalar(data: &[f64], short: usize, long: usize, first: usize, out: &mut [f64]) {
558 let largest = short.max(long);
559 let start_idx = first + largest;
560 for i in start_idx..data.len() {
561 let current = data[i];
562 let prev_short = data[i - short];
563 let short_val = ((current / prev_short) - 1.0) * 100.0;
564 let prev_long = data[i - long];
565 let long_val = ((current / prev_long) - 1.0) * 100.0;
566 out[i] = short_val + long_val;
567 }
568}
569
570#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
571#[target_feature(enable = "avx2")]
572pub unsafe fn coppock_avx2(data: &[f64], short: usize, long: usize, first: usize, out: &mut [f64]) {
573 use core::arch::x86_64::*;
574
575 let largest = short.max(long);
576 let start = first + largest;
577 let n = data.len();
578 if start >= n {
579 return;
580 }
581
582 let mut p_cur = data.as_ptr().add(start);
583 let mut p_ps = data.as_ptr().add(start - short);
584 let mut p_pl = data.as_ptr().add(start - long);
585 let mut p_out = out.as_mut_ptr().add(start);
586
587 let remaining = n - start;
588 let step = 4usize;
589 let vec_len = remaining / step * step;
590
591 let v_one = _mm256_set1_pd(1.0);
592 let v_scale = _mm256_set1_pd(100.0);
593
594 let end_vec = p_cur.add(vec_len);
595 while p_cur < end_vec {
596 let vc = _mm256_loadu_pd(p_cur);
597 let vs = _mm256_loadu_pd(p_ps);
598 let vl = _mm256_loadu_pd(p_pl);
599
600 let r_s = _mm256_div_pd(vc, vs);
601 let r_l = _mm256_div_pd(vc, vl);
602
603 let t0 = _mm256_mul_pd(_mm256_sub_pd(r_s, v_one), v_scale);
604 let t1 = _mm256_mul_pd(_mm256_sub_pd(r_l, v_one), v_scale);
605 let res = _mm256_add_pd(t0, t1);
606
607 _mm256_storeu_pd(p_out, res);
608
609 p_cur = p_cur.add(step);
610 p_ps = p_ps.add(step);
611 p_pl = p_pl.add(step);
612 p_out = p_out.add(step);
613 }
614
615 let tail = remaining - vec_len;
616 for _ in 0..tail {
617 let c = *p_cur;
618 let s = *p_ps;
619 let l = *p_pl;
620 let rs = (c / s - 1.0) * 100.0;
621 let rl = (c / l - 1.0) * 100.0;
622 *p_out = rs + rl;
623
624 p_cur = p_cur.add(1);
625 p_ps = p_ps.add(1);
626 p_pl = p_pl.add(1);
627 p_out = p_out.add(1);
628 }
629}
630
631#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
632#[target_feature(enable = "avx512f")]
633pub unsafe fn coppock_avx512(
634 data: &[f64],
635 short: usize,
636 long: usize,
637 first: usize,
638 out: &mut [f64],
639) {
640 if short.max(long) <= 32 {
641 coppock_avx512_short(data, short, long, first, out)
642 } else {
643 coppock_avx512_long(data, short, long, first, out)
644 }
645}
646
647#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
648#[target_feature(enable = "avx512f")]
649pub unsafe fn coppock_avx512_short(
650 data: &[f64],
651 short: usize,
652 long: usize,
653 first: usize,
654 out: &mut [f64],
655) {
656 use core::arch::x86_64::*;
657
658 let largest = short.max(long);
659 let start = first + largest;
660 let n = data.len();
661 if start >= n {
662 return;
663 }
664
665 let mut p_cur = data.as_ptr().add(start);
666 let mut p_ps = data.as_ptr().add(start - short);
667 let mut p_pl = data.as_ptr().add(start - long);
668 let mut p_out = out.as_mut_ptr().add(start);
669
670 let remaining = n - start;
671 let step = 8usize;
672 let vec_len = remaining / step * step;
673
674 let v_one = _mm512_set1_pd(1.0);
675 let v_scale = _mm512_set1_pd(100.0);
676
677 let end_vec = p_cur.add(vec_len);
678 while p_cur < end_vec {
679 let vc = _mm512_loadu_pd(p_cur);
680 let vs = _mm512_loadu_pd(p_ps);
681 let vl = _mm512_loadu_pd(p_pl);
682
683 let r_s = _mm512_div_pd(vc, vs);
684 let r_l = _mm512_div_pd(vc, vl);
685
686 let t0 = _mm512_mul_pd(_mm512_sub_pd(r_s, v_one), v_scale);
687 let t1 = _mm512_mul_pd(_mm512_sub_pd(r_l, v_one), v_scale);
688 let res = _mm512_add_pd(t0, t1);
689
690 _mm512_storeu_pd(p_out, res);
691
692 p_cur = p_cur.add(step);
693 p_ps = p_ps.add(step);
694 p_pl = p_pl.add(step);
695 p_out = p_out.add(step);
696 }
697
698 let tail = remaining - vec_len;
699 for _ in 0..tail {
700 let c = *p_cur;
701 let s = *p_ps;
702 let l = *p_pl;
703 let rs = (c / s - 1.0) * 100.0;
704 let rl = (c / l - 1.0) * 100.0;
705 *p_out = rs + rl;
706
707 p_cur = p_cur.add(1);
708 p_ps = p_ps.add(1);
709 p_pl = p_pl.add(1);
710 p_out = p_out.add(1);
711 }
712}
713
714#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
715#[target_feature(enable = "avx512f")]
716pub unsafe fn coppock_avx512_long(
717 data: &[f64],
718 short: usize,
719 long: usize,
720 first: usize,
721 out: &mut [f64],
722) {
723 coppock_avx512_short(data, short, long, first, out)
724}
725
726#[derive(Debug, Clone, Copy, PartialEq, Eq)]
727enum MaMode {
728 Wma,
729 Sma,
730 Ema,
731 Unsupported,
732}
733
734#[derive(Debug, Clone)]
735pub struct CoppockStream {
736 short: usize,
737 long: usize,
738 ma_period: usize,
739 ma_type: String,
740 mode: MaMode,
741
742 price: Vec<f64>,
743 inv_price: Vec<f64>,
744 p_head: usize,
745 p_filled: bool,
746
747 roc: Vec<f64>,
748 r_head: usize,
749 r_filled: bool,
750
751 ma_sum: f64,
752 wma_num: f64,
753 wma_denom: f64,
754
755 ema_alpha: f64,
756 ema_val: f64,
757 ema_init: bool,
758}
759
760#[inline(always)]
761fn parse_mode(s: &str) -> MaMode {
762 match s {
763 "wma" => MaMode::Wma,
764 "sma" => MaMode::Sma,
765 "ema" => MaMode::Ema,
766 _ => MaMode::Unsupported,
767 }
768}
769
770#[inline(always)]
771fn bump(i: &mut usize, n: usize) {
772 *i += 1;
773 if *i == n {
774 *i = 0;
775 }
776}
777
778#[inline(always)]
779fn wrap_sub(idx: usize, offset: usize, n: usize) -> usize {
780 let j = idx + n - offset;
781 if j >= n {
782 j - n
783 } else {
784 j
785 }
786}
787
788#[inline(always)]
789fn safe_inv(x: f64) -> f64 {
790 if x.is_finite() && x != 0.0 {
791 1.0 / x
792 } else {
793 f64::NAN
794 }
795}
796
797impl CoppockStream {
798 pub fn try_new(params: CoppockParams) -> Result<Self, CoppockError> {
799 let short = params.short_roc_period.unwrap_or(11);
800 let long = params.long_roc_period.unwrap_or(14);
801 let ma_period = params.ma_period.unwrap_or(10);
802 let ma_type = params.ma_type.unwrap_or_else(|| "wma".to_string());
803 if short == 0 || long == 0 || ma_period == 0 {
804 return Err(CoppockError::InvalidPeriod {
805 short,
806 long,
807 ma: ma_period,
808 data_len: 0,
809 });
810 }
811
812 let mode = parse_mode(&ma_type);
813
814 let price_cap = long.max(short) + 1;
815 let ma_cap = ma_period;
816
817 Ok(Self {
818 short,
819 long,
820 ma_period,
821 ma_type,
822 mode,
823
824 price: vec![f64::NAN; price_cap],
825 inv_price: vec![f64::NAN; price_cap],
826 p_head: 0,
827 p_filled: false,
828
829 roc: vec![f64::NAN; ma_cap],
830 r_head: 0,
831 r_filled: false,
832
833 ma_sum: 0.0,
834 wma_num: 0.0,
835 wma_denom: (ma_period * (ma_period + 1)) as f64 * 0.5,
836
837 ema_alpha: 2.0 / (ma_period as f64 + 1.0),
838 ema_val: f64::NAN,
839 ema_init: false,
840 })
841 }
842
843 #[inline(always)]
844 pub fn update(&mut self, value: f64) -> Option<f64> {
845 let p_n = self.price.len();
846 let write_p = self.p_head;
847
848 self.price[write_p] = value;
849 self.inv_price[write_p] = safe_inv(value);
850
851 bump(&mut self.p_head, p_n);
852 if !self.p_filled && self.p_head == 0 {
853 self.p_filled = true;
854 }
855 if !self.p_filled {
856 return None;
857 }
858
859 let cur_idx = write_p;
860 let prev_s_idx = wrap_sub(cur_idx, self.short, p_n);
861 let prev_l_idx = wrap_sub(cur_idx, self.long, p_n);
862
863 let cur = self.price[cur_idx];
864 let invs = self.inv_price[prev_s_idx];
865 let invl = self.inv_price[prev_l_idx];
866
867 if !(cur.is_finite() && invs.is_finite() && invl.is_finite()) {
868 return None;
869 }
870
871 let mut sum_roc = (cur * (invs + invl) - 2.0) * 100.0;
872
873 let n = self.ma_period;
874 let write_r = self.r_head;
875 let old = self.roc[write_r];
876
877 if !self.r_filled {
878 self.ma_sum += sum_roc;
879 self.wma_num += (write_r as f64 + 1.0) * sum_roc;
880
881 self.roc[write_r] = sum_roc;
882 bump(&mut self.r_head, n);
883
884 if !self.r_filled && self.r_head == 0 {
885 self.r_filled = true;
886 }
887
888 if !self.r_filled {
889 return None;
890 }
891
892 return Some(match self.mode {
893 MaMode::Wma => self.wma_num / self.wma_denom,
894 MaMode::Sma => self.ma_sum / n as f64,
895 MaMode::Ema => {
896 self.ema_val = sum_roc;
897 self.ema_init = true;
898 self.ema_val
899 }
900 MaMode::Unsupported => return None,
901 });
902 }
903
904 let prev_sum = self.ma_sum;
905 self.ma_sum = prev_sum - old + sum_roc;
906
907 self.wma_num = self.wma_num + (n as f64) * sum_roc - prev_sum;
908
909 self.roc[write_r] = sum_roc;
910 bump(&mut self.r_head, n);
911
912 Some(match self.mode {
913 MaMode::Wma => self.wma_num / self.wma_denom,
914 MaMode::Sma => self.ma_sum / n as f64,
915 MaMode::Ema => {
916 if !self.ema_init {
917 self.ema_val = sum_roc;
918 self.ema_init = true;
919 } else {
920 self.ema_val = self.ema_alpha * sum_roc + (1.0 - self.ema_alpha) * self.ema_val;
921 }
922 self.ema_val
923 }
924 MaMode::Unsupported => return None,
925 })
926 }
927}
928#[derive(Clone, Debug)]
929pub struct CoppockBatchRange {
930 pub short: (usize, usize, usize),
931 pub long: (usize, usize, usize),
932 pub ma: (usize, usize, usize),
933}
934
935impl Default for CoppockBatchRange {
936 fn default() -> Self {
937 Self {
938 short: (11, 11, 0),
939 long: (14, 14, 0),
940 ma: (10, 259, 1),
941 }
942 }
943}
944
945#[derive(Clone, Debug, Default)]
946pub struct CoppockBatchBuilder {
947 range: CoppockBatchRange,
948 kernel: Kernel,
949}
950
951impl CoppockBatchBuilder {
952 pub fn new() -> Self {
953 Self::default()
954 }
955 pub fn kernel(mut self, k: Kernel) -> Self {
956 self.kernel = k;
957 self
958 }
959 #[inline]
960 pub fn short_range(mut self, start: usize, end: usize, step: usize) -> Self {
961 self.range.short = (start, end, step);
962 self
963 }
964 #[inline]
965 pub fn short_static(mut self, n: usize) -> Self {
966 self.range.short = (n, n, 0);
967 self
968 }
969 #[inline]
970 pub fn long_range(mut self, start: usize, end: usize, step: usize) -> Self {
971 self.range.long = (start, end, step);
972 self
973 }
974 #[inline]
975 pub fn long_static(mut self, n: usize) -> Self {
976 self.range.long = (n, n, 0);
977 self
978 }
979 #[inline]
980 pub fn ma_range(mut self, start: usize, end: usize, step: usize) -> Self {
981 self.range.ma = (start, end, step);
982 self
983 }
984 #[inline]
985 pub fn ma_static(mut self, n: usize) -> Self {
986 self.range.ma = (n, n, 0);
987 self
988 }
989 pub fn apply_slice(self, data: &[f64]) -> Result<CoppockBatchOutput, CoppockError> {
990 coppock_batch_with_kernel(data, &self.range, self.kernel)
991 }
992 pub fn with_default_slice(data: &[f64], k: Kernel) -> Result<CoppockBatchOutput, CoppockError> {
993 CoppockBatchBuilder::new().kernel(k).apply_slice(data)
994 }
995 pub fn apply_candles(self, c: &Candles, src: &str) -> Result<CoppockBatchOutput, CoppockError> {
996 let slice = source_type(c, src);
997 self.apply_slice(slice)
998 }
999 pub fn with_default_candles(c: &Candles) -> Result<CoppockBatchOutput, CoppockError> {
1000 CoppockBatchBuilder::new()
1001 .kernel(Kernel::Auto)
1002 .apply_candles(c, "close")
1003 }
1004}
1005
1006pub fn coppock_batch_with_kernel(
1007 data: &[f64],
1008 sweep: &CoppockBatchRange,
1009 k: Kernel,
1010) -> Result<CoppockBatchOutput, CoppockError> {
1011 let kernel = match k {
1012 Kernel::Auto => Kernel::ScalarBatch,
1013 other if other.is_batch() => other,
1014 _ => return Err(CoppockError::InvalidKernelForBatch(k)),
1015 };
1016 let simd = match kernel {
1017 Kernel::Avx512Batch => Kernel::Avx512,
1018 Kernel::Avx2Batch => Kernel::Avx2,
1019 Kernel::ScalarBatch => Kernel::Scalar,
1020 _ => unreachable!(),
1021 };
1022 coppock_batch_par_slice(data, sweep, simd)
1023}
1024
1025#[derive(Clone, Debug)]
1026pub struct CoppockBatchOutput {
1027 pub values: Vec<f64>,
1028 pub combos: Vec<CoppockParams>,
1029 pub rows: usize,
1030 pub cols: usize,
1031}
1032
1033impl CoppockBatchOutput {
1034 pub fn row_for_params(&self, p: &CoppockParams) -> Option<usize> {
1035 self.combos.iter().position(|c| {
1036 c.short_roc_period.unwrap_or(11) == p.short_roc_period.unwrap_or(11)
1037 && c.long_roc_period.unwrap_or(14) == p.long_roc_period.unwrap_or(14)
1038 && c.ma_period.unwrap_or(10) == p.ma_period.unwrap_or(10)
1039 })
1040 }
1041 pub fn values_for(&self, p: &CoppockParams) -> Option<&[f64]> {
1042 self.row_for_params(p).map(|row| {
1043 let start = row * self.cols;
1044 &self.values[start..start + self.cols]
1045 })
1046 }
1047}
1048
1049#[inline(always)]
1050fn expand_grid(r: &CoppockBatchRange) -> Result<Vec<CoppockParams>, CoppockError> {
1051 fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, CoppockError> {
1052 if step == 0 || start == end {
1053 return Ok(vec![start]);
1054 }
1055 let mut v = Vec::new();
1056 if start < end {
1057 let mut cur = start;
1058 loop {
1059 v.push(cur);
1060 if cur == end {
1061 break;
1062 }
1063 cur =
1064 cur.checked_add(step)
1065 .ok_or(CoppockError::InvalidRange { start, end, step })?;
1066 if cur > end {
1067 break;
1068 }
1069 }
1070 } else {
1071 let mut cur = start;
1072 loop {
1073 v.push(cur);
1074 if cur == end {
1075 break;
1076 }
1077 cur =
1078 cur.checked_sub(step)
1079 .ok_or(CoppockError::InvalidRange { start, end, step })?;
1080 if cur < end {
1081 break;
1082 }
1083 }
1084 }
1085 if v.is_empty() {
1086 return Err(CoppockError::InvalidRange { start, end, step });
1087 }
1088 Ok(v)
1089 }
1090 let shorts = axis_usize(r.short)?;
1091 let longs = axis_usize(r.long)?;
1092 let mas = axis_usize(r.ma)?;
1093 if shorts.is_empty() || longs.is_empty() || mas.is_empty() {
1094 return Err(CoppockError::InvalidRange {
1095 start: 0,
1096 end: 0,
1097 step: 0,
1098 });
1099 }
1100 let cap = shorts
1101 .len()
1102 .checked_mul(longs.len())
1103 .and_then(|x| x.checked_mul(mas.len()))
1104 .ok_or_else(|| CoppockError::InvalidInput("coppock: parameter grid too large".into()))?;
1105 let mut out = Vec::with_capacity(cap);
1106 for &s in &shorts {
1107 for &l in &longs {
1108 for &m in &mas {
1109 out.push(CoppockParams {
1110 short_roc_period: Some(s),
1111 long_roc_period: Some(l),
1112 ma_period: Some(m),
1113 ma_type: Some("wma".to_string()),
1114 });
1115 }
1116 }
1117 }
1118 Ok(out)
1119}
1120
1121#[inline(always)]
1122pub fn coppock_batch_slice(
1123 data: &[f64],
1124 sweep: &CoppockBatchRange,
1125 kern: Kernel,
1126) -> Result<CoppockBatchOutput, CoppockError> {
1127 coppock_batch_inner(data, sweep, kern, false)
1128}
1129
1130#[inline(always)]
1131pub fn coppock_batch_par_slice(
1132 data: &[f64],
1133 sweep: &CoppockBatchRange,
1134 kern: Kernel,
1135) -> Result<CoppockBatchOutput, CoppockError> {
1136 coppock_batch_inner(data, sweep, kern, true)
1137}
1138
1139#[inline(always)]
1140fn coppock_batch_inner(
1141 data: &[f64],
1142 sweep: &CoppockBatchRange,
1143 kern: Kernel,
1144 parallel: bool,
1145) -> Result<CoppockBatchOutput, CoppockError> {
1146 let combos = expand_grid(sweep)?;
1147 if combos.is_empty() {
1148 return Err(CoppockError::InvalidRange {
1149 start: 0,
1150 end: 0,
1151 step: 0,
1152 });
1153 }
1154 let first = data
1155 .iter()
1156 .position(|x| !x.is_nan())
1157 .ok_or(CoppockError::AllValuesNaN)?;
1158 let max_roc = combos
1159 .iter()
1160 .map(|c| c.short_roc_period.unwrap().max(c.long_roc_period.unwrap()))
1161 .max()
1162 .unwrap();
1163 let _ = combos.len().checked_mul(max_roc).ok_or_else(|| {
1164 CoppockError::InvalidInput("coppock: n_combos*max_period overflow".into())
1165 })?;
1166 if data.len() - first < max_roc {
1167 return Err(CoppockError::NotEnoughValidData {
1168 needed: max_roc,
1169 valid: data.len() - first,
1170 });
1171 }
1172
1173 let rows = combos.len();
1174 let cols = data.len();
1175 let _total = rows
1176 .checked_mul(cols)
1177 .ok_or_else(|| CoppockError::InvalidInput("rows*cols overflow".into()))?;
1178
1179 let mut buf_mu = make_uninit_matrix(rows, cols);
1180
1181 let warm: Vec<usize> = combos
1182 .iter()
1183 .map(|c| {
1184 let short = c.short_roc_period.unwrap();
1185 let long = c.long_roc_period.unwrap();
1186 let ma_p = c.ma_period.unwrap();
1187 let largest = short.max(long);
1188
1189 first + largest + (ma_p - 1)
1190 })
1191 .collect();
1192
1193 init_matrix_prefixes(&mut buf_mu, cols, &warm);
1194
1195 let mut buf_guard = ManuallyDrop::new(buf_mu);
1196 let values: &mut [f64] = unsafe {
1197 core::slice::from_raw_parts_mut(buf_guard.as_mut_ptr() as *mut f64, buf_guard.len())
1198 };
1199
1200 let inv: Vec<f64> = data.iter().map(|&x| 1.0f64 / x).collect();
1201
1202 let do_row = |row: usize, out_row: &mut [f64]| {
1203 let c = &combos[row];
1204 let short = c.short_roc_period.unwrap();
1205 let long = c.long_roc_period.unwrap();
1206 let ma_p = c.ma_period.unwrap();
1207 let ma_type = c.ma_type.as_deref().unwrap_or("wma");
1208 let largest = short.max(long);
1209
1210 let sum_roc_warmup = first + largest;
1211
1212 let mut sum_roc = alloc_with_nan_prefix(cols, sum_roc_warmup);
1213 coppock_row_scalar_with_inv(data, first, short, long, &inv, &mut sum_roc);
1214
1215 let smoothed = ma(&ma_type, MaData::Slice(&sum_roc), ma_p).expect("MA error inside batch");
1216
1217 out_row.copy_from_slice(&smoothed);
1218 };
1219
1220 if parallel {
1221 #[cfg(not(target_arch = "wasm32"))]
1222 {
1223 values
1224 .par_chunks_mut(cols)
1225 .enumerate()
1226 .for_each(|(row, slice)| do_row(row, slice));
1227 }
1228 #[cfg(target_arch = "wasm32")]
1229 {
1230 for (row, slice) in values.chunks_mut(cols).enumerate() {
1231 do_row(row, slice);
1232 }
1233 }
1234 } else {
1235 for (row, slice) in values.chunks_mut(cols).enumerate() {
1236 do_row(row, slice);
1237 }
1238 }
1239
1240 let values = unsafe {
1241 Vec::from_raw_parts(
1242 buf_guard.as_mut_ptr() as *mut f64,
1243 buf_guard.len(),
1244 buf_guard.capacity(),
1245 )
1246 };
1247
1248 Ok(CoppockBatchOutput {
1249 values,
1250 combos,
1251 rows,
1252 cols,
1253 })
1254}
1255
1256#[inline(always)]
1257pub fn coppock_batch_inner_into(
1258 data: &[f64],
1259 sweep: &CoppockBatchRange,
1260 kern: Kernel,
1261 parallel: bool,
1262 out: &mut [f64],
1263) -> Result<Vec<CoppockParams>, CoppockError> {
1264 let combos = expand_grid(sweep)?;
1265 if combos.is_empty() {
1266 return Err(CoppockError::InvalidRange {
1267 start: 0,
1268 end: 0,
1269 step: 0,
1270 });
1271 }
1272 let first = data
1273 .iter()
1274 .position(|x| !x.is_nan())
1275 .ok_or(CoppockError::AllValuesNaN)?;
1276 let max_roc = combos
1277 .iter()
1278 .map(|c| c.short_roc_period.unwrap().max(c.long_roc_period.unwrap()))
1279 .max()
1280 .unwrap();
1281 let _ = combos.len().checked_mul(max_roc).ok_or_else(|| {
1282 CoppockError::InvalidInput("coppock: n_combos*max_period overflow".into())
1283 })?;
1284 if data.len() - first < max_roc {
1285 return Err(CoppockError::NotEnoughValidData {
1286 needed: max_roc,
1287 valid: data.len() - first,
1288 });
1289 }
1290
1291 let rows = combos.len();
1292 let cols = data.len();
1293 let expected = rows
1294 .checked_mul(cols)
1295 .ok_or_else(|| CoppockError::InvalidInput("rows*cols overflow".into()))?;
1296 if out.len() != expected {
1297 return Err(CoppockError::OutputLengthMismatch {
1298 expected,
1299 got: out.len(),
1300 });
1301 }
1302
1303 let warm: Vec<usize> = combos
1304 .iter()
1305 .map(|c| {
1306 let short = c.short_roc_period.unwrap();
1307 let long = c.long_roc_period.unwrap();
1308 let ma_p = c.ma_period.unwrap();
1309 let largest = short.max(long);
1310 first + largest + (ma_p - 1)
1311 })
1312 .collect();
1313
1314 let out_mu: &mut [std::mem::MaybeUninit<f64>] = unsafe {
1315 std::slice::from_raw_parts_mut(
1316 out.as_mut_ptr() as *mut std::mem::MaybeUninit<f64>,
1317 out.len(),
1318 )
1319 };
1320 init_matrix_prefixes(out_mu, cols, &warm);
1321
1322 let inv: Vec<f64> = data.iter().map(|&x| 1.0f64 / x).collect();
1323
1324 let do_row = |row: usize, out_row: &mut [f64]| {
1325 let c = &combos[row];
1326 let short = c.short_roc_period.unwrap();
1327 let long = c.long_roc_period.unwrap();
1328 let ma_p = c.ma_period.unwrap();
1329 let ma_type = c.ma_type.as_deref().unwrap_or("wma");
1330 let largest = short.max(long);
1331 let sum_roc_warmup = first + largest;
1332
1333 let mut sum_roc = alloc_with_nan_prefix(cols, sum_roc_warmup);
1334
1335 coppock_row_scalar_with_inv(data, first, short, long, &inv, &mut sum_roc);
1336
1337 let smoothed = ma(&ma_type, MaData::Slice(&sum_roc), ma_p).expect("MA error inside batch");
1338
1339 out_row.copy_from_slice(&smoothed);
1340 };
1341
1342 if parallel {
1343 #[cfg(not(target_arch = "wasm32"))]
1344 {
1345 out.par_chunks_mut(cols)
1346 .enumerate()
1347 .for_each(|(row, slice)| do_row(row, slice));
1348 }
1349 #[cfg(target_arch = "wasm32")]
1350 {
1351 for (row, slice) in out.chunks_mut(cols).enumerate() {
1352 do_row(row, slice);
1353 }
1354 }
1355 } else {
1356 for (row, slice) in out.chunks_mut(cols).enumerate() {
1357 do_row(row, slice);
1358 }
1359 }
1360
1361 Ok(combos)
1362}
1363
1364#[inline(always)]
1365pub fn coppock_row_scalar(
1366 data: &[f64],
1367 first: usize,
1368 short: usize,
1369 long: usize,
1370 _stride: usize,
1371 _w_ptr: *const f64,
1372 _inv_n: f64,
1373 out: &mut [f64],
1374) {
1375 let largest = short.max(long);
1376 for i in (first + largest)..data.len() {
1377 let current = data[i];
1378 let prev_short = data[i - short];
1379 let short_val = ((current / prev_short) - 1.0) * 100.0;
1380 let prev_long = data[i - long];
1381 let long_val = ((current / prev_long) - 1.0) * 100.0;
1382 out[i] = short_val + long_val;
1383 }
1384}
1385
1386#[inline(always)]
1387fn coppock_row_scalar_with_inv(
1388 data: &[f64],
1389 first: usize,
1390 short: usize,
1391 long: usize,
1392 inv: &[f64],
1393 out: &mut [f64],
1394) {
1395 let largest = short.max(long);
1396 for i in (first + largest)..data.len() {
1397 let c = data[i];
1398 let is = inv[i - short];
1399 let il = inv[i - long];
1400 out[i] = (c * is + c * il - 2.0) * 100.0;
1401 }
1402}
1403
1404#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1405#[target_feature(enable = "avx2")]
1406pub unsafe fn coppock_row_avx2(
1407 data: &[f64],
1408 first: usize,
1409 short: usize,
1410 long: usize,
1411 stride: usize,
1412 w_ptr: *const f64,
1413 inv_n: f64,
1414 out: &mut [f64],
1415) {
1416 let _ = (stride, w_ptr, inv_n);
1417 coppock_avx2(data, short, long, first, out)
1418}
1419
1420#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1421#[target_feature(enable = "avx512f")]
1422pub unsafe fn coppock_row_avx512(
1423 data: &[f64],
1424 first: usize,
1425 short: usize,
1426 long: usize,
1427 stride: usize,
1428 w_ptr: *const f64,
1429 inv_n: f64,
1430 out: &mut [f64],
1431) {
1432 if short.max(long) <= 32 {
1433 coppock_row_avx512_short(data, first, short, long, stride, w_ptr, inv_n, out)
1434 } else {
1435 coppock_row_avx512_long(data, first, short, long, stride, w_ptr, inv_n, out)
1436 }
1437}
1438
1439#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1440#[target_feature(enable = "avx512f")]
1441pub unsafe fn coppock_row_avx512_short(
1442 data: &[f64],
1443 first: usize,
1444 short: usize,
1445 long: usize,
1446 stride: usize,
1447 w_ptr: *const f64,
1448 inv_n: f64,
1449 out: &mut [f64],
1450) {
1451 let _ = (stride, w_ptr, inv_n);
1452 coppock_avx512_short(data, short, long, first, out)
1453}
1454
1455#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1456#[target_feature(enable = "avx512f")]
1457pub unsafe fn coppock_row_avx512_long(
1458 data: &[f64],
1459 first: usize,
1460 short: usize,
1461 long: usize,
1462 stride: usize,
1463 w_ptr: *const f64,
1464 inv_n: f64,
1465 out: &mut [f64],
1466) {
1467 let _ = (stride, w_ptr, inv_n);
1468 coppock_avx512_long(data, short, long, first, out)
1469}
1470
1471#[inline(always)]
1472fn expand_grid_coppock(_r: &CoppockBatchRange) -> Vec<CoppockParams> {
1473 vec![CoppockParams::default()]
1474}
1475
1476#[cfg(test)]
1477mod tests {
1478 use super::*;
1479 use crate::skip_if_unsupported;
1480 use crate::utilities::data_loader::read_candles_from_csv;
1481
1482 fn check_coppock_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1483 skip_if_unsupported!(kernel, test_name);
1484 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1485 let candles = read_candles_from_csv(file_path)?;
1486 let default_params = CoppockParams::default();
1487 let input = CoppockInput::from_candles(&candles, "close", default_params);
1488 let output = coppock_with_kernel(&input, kernel)?;
1489 assert_eq!(output.values.len(), candles.close.len());
1490 Ok(())
1491 }
1492
1493 #[test]
1494 fn test_coppock_into_matches_api() -> Result<(), Box<dyn Error>> {
1495 let n = 256usize;
1496 let data: Vec<f64> = (0..n).map(|i| 100.0 + (i as f64) * 0.25).collect();
1497
1498 let input = CoppockInput::from_slice(&data, CoppockParams::default());
1499
1500 let baseline = coppock(&input)?.values;
1501
1502 let mut out = vec![0.0; n];
1503 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1504 {
1505 coppock_into(&input, &mut out)?;
1506 }
1507 #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1508 {
1509 out.copy_from_slice(&baseline);
1510 }
1511
1512 assert_eq!(baseline.len(), out.len());
1513
1514 for i in 0..n {
1515 let a = baseline[i];
1516 let b = out[i];
1517 let eq = (a.is_nan() && b.is_nan()) || (a == b);
1518 assert!(eq, "mismatch at index {i}: baseline={a}, into={b}");
1519 }
1520
1521 Ok(())
1522 }
1523 fn check_coppock_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1524 skip_if_unsupported!(kernel, test_name);
1525 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1526 let candles = read_candles_from_csv(file_path)?;
1527 let input = CoppockInput::with_default_candles(&candles);
1528 let result = coppock_with_kernel(&input, kernel)?;
1529 let expected_last_five = [
1530 -1.4542764618985533,
1531 -1.3795224034983653,
1532 -1.614331648987457,
1533 -1.9179048338714915,
1534 -2.1096548435774625,
1535 ];
1536 let start = result.values.len().saturating_sub(5);
1537 for (i, &val) in result.values[start..].iter().enumerate() {
1538 let diff = (val - expected_last_five[i]).abs();
1539 assert!(
1540 diff < 1e-7,
1541 "[{}] Coppock {:?} mismatch at idx {}: got {}, expected {}",
1542 test_name,
1543 kernel,
1544 i,
1545 val,
1546 expected_last_five[i]
1547 );
1548 }
1549 Ok(())
1550 }
1551 fn check_coppock_default_candles(
1552 test_name: &str,
1553 kernel: Kernel,
1554 ) -> Result<(), Box<dyn Error>> {
1555 skip_if_unsupported!(kernel, test_name);
1556 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1557 let candles = read_candles_from_csv(file_path)?;
1558 let input = CoppockInput::with_default_candles(&candles);
1559 match input.data {
1560 CoppockData::Candles { source, .. } => assert_eq!(source, "close"),
1561 _ => panic!("Expected CoppockData::Candles"),
1562 }
1563 let output = coppock_with_kernel(&input, kernel)?;
1564 assert_eq!(output.values.len(), candles.close.len());
1565 Ok(())
1566 }
1567 fn check_coppock_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1568 skip_if_unsupported!(kernel, test_name);
1569 let input_data = [10.0, 20.0, 30.0];
1570 let params = CoppockParams {
1571 short_roc_period: Some(0),
1572 long_roc_period: Some(14),
1573 ma_period: Some(10),
1574 ma_type: Some("wma".to_string()),
1575 };
1576 let input = CoppockInput::from_slice(&input_data, params);
1577 let res = coppock_with_kernel(&input, kernel);
1578 assert!(
1579 res.is_err(),
1580 "[{}] Coppock should fail with zero short period",
1581 test_name
1582 );
1583 Ok(())
1584 }
1585 fn check_coppock_period_exceeds_length(
1586 test_name: &str,
1587 kernel: Kernel,
1588 ) -> Result<(), Box<dyn Error>> {
1589 skip_if_unsupported!(kernel, test_name);
1590 let data_small = [10.0, 20.0, 30.0];
1591 let params = CoppockParams {
1592 short_roc_period: Some(14),
1593 long_roc_period: Some(20),
1594 ma_period: Some(10),
1595 ma_type: Some("wma".to_string()),
1596 };
1597 let input = CoppockInput::from_slice(&data_small, params);
1598 let res = coppock_with_kernel(&input, kernel);
1599 assert!(
1600 res.is_err(),
1601 "[{}] Coppock should fail with short/long>data.len()",
1602 test_name
1603 );
1604 Ok(())
1605 }
1606 fn check_coppock_very_small_dataset(
1607 test_name: &str,
1608 kernel: Kernel,
1609 ) -> Result<(), Box<dyn Error>> {
1610 skip_if_unsupported!(kernel, test_name);
1611 let single_point = [42.0];
1612 let params = CoppockParams {
1613 short_roc_period: Some(11),
1614 long_roc_period: Some(14),
1615 ma_period: Some(10),
1616 ma_type: Some("wma".to_string()),
1617 };
1618 let input = CoppockInput::from_slice(&single_point, params);
1619 let res = coppock_with_kernel(&input, kernel);
1620 assert!(
1621 res.is_err(),
1622 "[{}] Coppock should fail with insufficient data",
1623 test_name
1624 );
1625 Ok(())
1626 }
1627 fn check_coppock_reinput(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1628 skip_if_unsupported!(kernel, test_name);
1629
1630 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1631 let candles = read_candles_from_csv(file_path)?;
1632 let default_params = CoppockParams::default();
1633 let first_input = CoppockInput::from_candles(&candles, "close", default_params.clone());
1634 let first_result = coppock_with_kernel(&first_input, kernel)?;
1635
1636 let second_params = CoppockParams {
1637 short_roc_period: Some(5),
1638 long_roc_period: Some(8),
1639 ma_period: Some(3),
1640 ma_type: Some("sma".to_string()),
1641 };
1642 let second_input = CoppockInput::from_slice(&first_result.values, second_params.clone());
1643 let second_result = coppock_with_kernel(&second_input, kernel)?;
1644
1645 assert_eq!(second_result.values.len(), first_result.values.len());
1646
1647 let short1 = default_params.short_roc_period.unwrap();
1648 let long1 = default_params.long_roc_period.unwrap();
1649 let ma1 = default_params.ma_period.unwrap();
1650 let largest1 = short1.max(long1);
1651 let first_valid1 = largest1 + (ma1 - 1);
1652
1653 let short2 = second_params.short_roc_period.unwrap();
1654 let long2 = second_params.long_roc_period.unwrap();
1655 let ma2 = second_params.ma_period.unwrap();
1656 let largest2 = short2.max(long2);
1657 let first_valid2 = first_valid1 + largest2 + (ma2 - 1);
1658
1659 for i in first_valid2..second_result.values.len() {
1660 assert!(
1661 !second_result.values[i].is_nan(),
1662 "[{}] Expected no NaN after index {}, found NaN at {}",
1663 test_name,
1664 first_valid2,
1665 i
1666 );
1667 }
1668
1669 Ok(())
1670 }
1671 fn check_coppock_nan_handling(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1672 skip_if_unsupported!(kernel, test_name);
1673 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1674 let candles = read_candles_from_csv(file_path)?;
1675 let input = CoppockInput::from_candles(
1676 &candles,
1677 "close",
1678 CoppockParams {
1679 short_roc_period: Some(11),
1680 long_roc_period: Some(14),
1681 ma_period: Some(10),
1682 ma_type: Some("wma".to_string()),
1683 },
1684 );
1685 let res = coppock_with_kernel(&input, kernel)?;
1686 assert_eq!(res.values.len(), candles.close.len());
1687 if res.values.len() > 30 {
1688 for (i, &val) in res.values[30..].iter().enumerate() {
1689 assert!(
1690 !val.is_nan(),
1691 "[{}] Found unexpected NaN at out-index {}",
1692 test_name,
1693 30 + i
1694 );
1695 }
1696 }
1697 Ok(())
1698 }
1699 fn check_coppock_streaming(test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
1700 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1701 let candles = read_candles_from_csv(file_path)?;
1702 let short = 11;
1703 let long = 14;
1704 let ma_period = 10;
1705 let ma_type = "wma".to_string();
1706 let input = CoppockInput::from_candles(
1707 &candles,
1708 "close",
1709 CoppockParams {
1710 short_roc_period: Some(short),
1711 long_roc_period: Some(long),
1712 ma_period: Some(ma_period),
1713 ma_type: Some(ma_type.clone()),
1714 },
1715 );
1716 let batch_output = coppock_with_kernel(&input, Kernel::Scalar)?.values;
1717 let mut stream = CoppockStream::try_new(CoppockParams {
1718 short_roc_period: Some(short),
1719 long_roc_period: Some(long),
1720 ma_period: Some(ma_period),
1721 ma_type: Some(ma_type),
1722 })?;
1723 let mut stream_values = Vec::with_capacity(candles.close.len());
1724 for &price in &candles.close {
1725 match stream.update(price) {
1726 Some(v) => stream_values.push(v),
1727 None => stream_values.push(f64::NAN),
1728 }
1729 }
1730 assert_eq!(batch_output.len(), stream_values.len());
1731 for (i, (&b, &s)) in batch_output.iter().zip(stream_values.iter()).enumerate() {
1732 if b.is_nan() && s.is_nan() {
1733 continue;
1734 }
1735 let diff = (b - s).abs();
1736 assert!(
1737 diff < 1e-8,
1738 "[{}] Coppock streaming f64 mismatch at idx {}: batch={}, stream={}, diff={}",
1739 test_name,
1740 i,
1741 b,
1742 s,
1743 diff
1744 );
1745 }
1746 Ok(())
1747 }
1748
1749 #[cfg(debug_assertions)]
1750 fn check_coppock_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1751 skip_if_unsupported!(kernel, test_name);
1752
1753 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1754 let candles = read_candles_from_csv(file_path)?;
1755
1756 let param_combos = vec![
1757 CoppockParams {
1758 short_roc_period: Some(11),
1759 long_roc_period: Some(14),
1760 ma_period: Some(10),
1761 ma_type: Some("wma".to_string()),
1762 },
1763 CoppockParams {
1764 short_roc_period: Some(5),
1765 long_roc_period: Some(8),
1766 ma_period: Some(3),
1767 ma_type: Some("sma".to_string()),
1768 },
1769 CoppockParams {
1770 short_roc_period: Some(20),
1771 long_roc_period: Some(25),
1772 ma_period: Some(15),
1773 ma_type: Some("ema".to_string()),
1774 },
1775 ];
1776
1777 for params in param_combos {
1778 let input = CoppockInput::from_candles(&candles, "close", params);
1779 let output = coppock_with_kernel(&input, kernel)?;
1780
1781 for (i, &val) in output.values.iter().enumerate() {
1782 if val.is_nan() {
1783 continue;
1784 }
1785
1786 let bits = val.to_bits();
1787
1788 if bits == 0x11111111_11111111 {
1789 panic!(
1790 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {}",
1791 test_name, val, bits, i
1792 );
1793 }
1794
1795 if bits == 0x22222222_22222222 {
1796 panic!(
1797 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {}",
1798 test_name, val, bits, i
1799 );
1800 }
1801
1802 if bits == 0x33333333_33333333 {
1803 panic!(
1804 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {}",
1805 test_name, val, bits, i
1806 );
1807 }
1808 }
1809 }
1810
1811 Ok(())
1812 }
1813
1814 #[cfg(not(debug_assertions))]
1815 fn check_coppock_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
1816 Ok(())
1817 }
1818
1819 macro_rules! generate_all_coppock_tests {
1820 ($($test_fn:ident),*) => {
1821 paste::paste! {
1822 $(
1823 #[test]
1824 fn [<$test_fn _scalar_f64>]() {
1825 let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
1826 }
1827 )*
1828 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1829 $(
1830 #[test]
1831 fn [<$test_fn _avx2_f64>]() {
1832 let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
1833 }
1834 #[test]
1835 fn [<$test_fn _avx512_f64>]() {
1836 let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
1837 }
1838 )*
1839 }
1840 }
1841 }
1842 #[cfg(feature = "proptest")]
1843 #[allow(clippy::float_cmp)]
1844 fn check_coppock_property(
1845 test_name: &str,
1846 kernel: Kernel,
1847 ) -> Result<(), Box<dyn std::error::Error>> {
1848 use proptest::prelude::*;
1849 skip_if_unsupported!(kernel, test_name);
1850
1851 let random_data_strat =
1852 (2usize..=20, 5usize..=30, 2usize..=15).prop_flat_map(|(short, long, ma_period)| {
1853 let data_len = long.max(short) + ma_period + 50;
1854 (
1855 prop::collection::vec(
1856 (10.0f64..10000.0f64)
1857 .prop_filter("positive finite", |x| x.is_finite() && *x > 0.0),
1858 data_len..data_len + 100,
1859 ),
1860 Just(short),
1861 Just(long),
1862 Just(ma_period),
1863 prop::sample::select(vec!["wma", "sma", "ema"]),
1864 )
1865 });
1866
1867 let constant_data_strat =
1868 (2usize..=15, 5usize..=20, 2usize..=10).prop_flat_map(|(short, long, ma_period)| {
1869 let data_len = long.max(short) + ma_period + 30;
1870 (
1871 (100.0f64..1000.0f64).prop_map(move |val| vec![val; data_len]),
1872 Just(short),
1873 Just(long),
1874 Just(ma_period),
1875 Just("wma"),
1876 )
1877 });
1878
1879 let trending_data_strat =
1880 (2usize..=15, 5usize..=25, 2usize..=12).prop_flat_map(|(short, long, ma_period)| {
1881 let data_len = long.max(short) + ma_period + 40;
1882 (
1883 prop::bool::ANY.prop_flat_map(move |increasing| {
1884 if increasing {
1885 Just(
1886 (0..data_len)
1887 .map(|i| 100.0 + i as f64 * 2.0)
1888 .collect::<Vec<_>>(),
1889 )
1890 } else {
1891 Just(
1892 (0..data_len)
1893 .map(|i| 1000.0 - i as f64 * 2.0)
1894 .collect::<Vec<_>>(),
1895 )
1896 }
1897 }),
1898 Just(short),
1899 Just(long),
1900 Just(ma_period),
1901 Just("sma"),
1902 )
1903 });
1904
1905 let edge_case_strat =
1906 (2usize..=3, 3usize..=5, 2usize..=3).prop_flat_map(|(short, long, ma_period)| {
1907 let data_len = 20;
1908 (
1909 prop::collection::vec(
1910 (50.0f64..150.0f64).prop_filter("positive", |x| *x > 0.0),
1911 data_len..data_len + 10,
1912 ),
1913 Just(short),
1914 Just(long),
1915 Just(ma_period),
1916 Just("wma"),
1917 )
1918 });
1919
1920 let equal_periods_strat =
1921 (5usize..=15, 2usize..=10).prop_flat_map(|(period, ma_period)| {
1922 let data_len = period + ma_period + 30;
1923 (
1924 prop::collection::vec(
1925 (50.0f64..500.0f64).prop_filter("positive", |x| *x > 0.0),
1926 data_len..data_len + 20,
1927 ),
1928 Just(period),
1929 Just(period),
1930 Just(ma_period),
1931 Just("wma"),
1932 )
1933 });
1934
1935 let nan_prefix_strat = (2usize..=10, 5usize..=15, 2usize..=8, 1usize..=5).prop_flat_map(
1936 |(short, long, ma_period, nan_count)| {
1937 let data_len = nan_count + long.max(short) + ma_period + 20;
1938 (
1939 prop::collection::vec(
1940 (100.0f64..1000.0f64),
1941 data_len - nan_count..data_len - nan_count + 10,
1942 )
1943 .prop_map(move |mut vals| {
1944 let mut result = vec![f64::NAN; nan_count];
1945 result.append(&mut vals);
1946 result
1947 }),
1948 Just(short),
1949 Just(long),
1950 Just(ma_period),
1951 Just("sma"),
1952 )
1953 },
1954 );
1955
1956 let combined_strat = prop::strategy::Union::new(vec![
1957 random_data_strat.boxed(),
1958 constant_data_strat.boxed(),
1959 trending_data_strat.boxed(),
1960 edge_case_strat.boxed(),
1961 equal_periods_strat.boxed(),
1962 nan_prefix_strat.boxed(),
1963 ]);
1964
1965 proptest::test_runner::TestRunner::default()
1966 .run(
1967 &combined_strat,
1968 |(data, short, long, ma_period, ma_type)| {
1969 let params = CoppockParams {
1970 short_roc_period: Some(short),
1971 long_roc_period: Some(long),
1972 ma_period: Some(ma_period),
1973 ma_type: Some(ma_type.to_string()),
1974 };
1975 let input = CoppockInput::from_slice(&data, params.clone());
1976
1977 let result = coppock_with_kernel(&input, kernel);
1978 prop_assert!(
1979 result.is_ok(),
1980 "Coppock computation failed: {:?}",
1981 result.err()
1982 );
1983 let out = result.unwrap().values;
1984
1985 let ref_result = coppock_with_kernel(&input, Kernel::Scalar);
1986 prop_assert!(ref_result.is_ok(), "Reference computation failed");
1987 let ref_out = ref_result.unwrap().values;
1988
1989 let first = data.iter().position(|&x| !x.is_nan()).unwrap_or(0);
1990 let largest_roc = short.max(long);
1991 let warmup = first + largest_roc + (ma_period - 1);
1992
1993 for i in warmup..data.len() {
1994 let y = out[i];
1995 let r = ref_out[i];
1996
1997 if y.is_nan() != r.is_nan() {
1998 prop_assert!(
1999 false,
2000 "NaN mismatch at index {}: kernel={:?}, ref={:?}",
2001 i,
2002 y,
2003 r
2004 );
2005 }
2006
2007 if y.is_finite() && r.is_finite() {
2008 let y_bits = y.to_bits();
2009 let r_bits = r.to_bits();
2010 let ulp_diff = y_bits.abs_diff(r_bits);
2011
2012 prop_assert!(
2013 (y - r).abs() <= 1e-9 || ulp_diff <= 10,
2014 "Value mismatch at index {}: kernel={}, ref={}, diff={}, ULP={}",
2015 i,
2016 y,
2017 r,
2018 (y - r).abs(),
2019 ulp_diff
2020 );
2021 }
2022 }
2023
2024 let is_constant = data.windows(2).all(|w| (w[0] - w[1]).abs() < f64::EPSILON);
2025 if is_constant && data.len() > warmup + 5 {
2026 for i in (warmup + 5)..data.len() {
2027 let val = out[i];
2028 if val.is_finite() {
2029 prop_assert!(
2030 val.abs() <= 1e-6,
2031 "Constant data should produce ~0, got {} at index {}",
2032 val,
2033 i
2034 );
2035 }
2036 }
2037 }
2038
2039 let is_increasing = data.windows(2).all(|w| w[1] >= w[0]);
2040 let is_decreasing = data.windows(2).all(|w| w[1] <= w[0]);
2041
2042 if (is_increasing || is_decreasing) && data.len() > warmup + 10 {
2043 let expected_positive = is_increasing;
2044
2045 for i in (warmup + 10)..(warmup + 15).min(data.len()) {
2046 let val = out[i];
2047 if val.is_finite() && val.abs() > 1e-10 {
2048 if expected_positive {
2049 prop_assert!(
2050 val >= -1e-6,
2051 "Expected positive Coppock for increasing data, got {} at index {}",
2052 val, i
2053 );
2054 } else {
2055 prop_assert!(
2056 val <= 1e-6,
2057 "Expected negative Coppock for decreasing data, got {} at index {}",
2058 val, i
2059 );
2060 }
2061 }
2062 }
2063 }
2064
2065 for i in warmup..data.len() {
2066 let val = out[i];
2067 prop_assert!(
2068 val.is_finite() || val.is_nan(),
2069 "Found non-finite value {} at index {}",
2070 val,
2071 i
2072 );
2073 }
2074
2075 for i in warmup..data.len() {
2076 let val = out[i];
2077 if val.is_finite() {
2078 prop_assert!(
2079 val.abs() <= 100_000.0,
2080 "Unreasonably large Coppock value {} at index {} (exceeds 100,000%)",
2081 val, i
2082 );
2083 }
2084 }
2085
2086 for i in warmup..data.len() {
2087 let val = out[i];
2088 prop_assert!(
2089 val.is_finite() || val.is_nan(),
2090 "Found infinity at index {}: {}",
2091 i,
2092 val
2093 );
2094
2095 if i >= largest_roc {
2096 let current = data[i];
2097 let prev_short = data[i - short];
2098 let prev_long = data[i - long];
2099
2100 if current.is_finite()
2101 && prev_short.is_finite()
2102 && prev_long.is_finite()
2103 && prev_short != 0.0
2104 && prev_long != 0.0
2105 {
2106 if i >= warmup {
2107 prop_assert!(
2108 val.is_finite(),
2109 "Expected finite value but got {} at index {} with valid inputs",
2110 val, i
2111 );
2112 }
2113 }
2114 }
2115 }
2116
2117 if short == long && data.len() > warmup + 5 {
2118 for i in (warmup + 1)..data.len().min(warmup + 6) {
2119 let val = out[i];
2120 if val.is_finite() {
2121 prop_assert!(
2122 val.abs() <= 100_000.0,
2123 "Equal periods produced unreasonable value {} at index {}",
2124 val,
2125 i
2126 );
2127 }
2128 }
2129 }
2130
2131 Ok(())
2132 },
2133 )
2134 .unwrap();
2135
2136 Ok(())
2137 }
2138
2139 generate_all_coppock_tests!(
2140 check_coppock_partial_params,
2141 check_coppock_accuracy,
2142 check_coppock_default_candles,
2143 check_coppock_zero_period,
2144 check_coppock_period_exceeds_length,
2145 check_coppock_very_small_dataset,
2146 check_coppock_reinput,
2147 check_coppock_nan_handling,
2148 check_coppock_streaming,
2149 check_coppock_no_poison
2150 );
2151
2152 #[cfg(feature = "proptest")]
2153 generate_all_coppock_tests!(check_coppock_property);
2154 fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2155 skip_if_unsupported!(kernel, test);
2156 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2157 let c = read_candles_from_csv(file)?;
2158 let output = CoppockBatchBuilder::new()
2159 .kernel(kernel)
2160 .apply_candles(&c, "close")?;
2161 let def = CoppockParams::default();
2162 let row = output.values_for(&def).expect("default row missing");
2163 assert_eq!(row.len(), c.close.len());
2164
2165 let expected = [
2166 -1.4542764618985533,
2167 -1.3795224034983653,
2168 -1.614331648987457,
2169 -1.9179048338714915,
2170 -2.1096548435774625,
2171 ];
2172 let start = row.len() - 5;
2173 for (i, &v) in row[start..].iter().enumerate() {
2174 assert!(
2175 (v - expected[i]).abs() < 1e-7,
2176 "[{test}] default-row mismatch at idx {i}: {v} vs {expected:?}"
2177 );
2178 }
2179 Ok(())
2180 }
2181
2182 #[cfg(debug_assertions)]
2183 fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2184 skip_if_unsupported!(kernel, test);
2185
2186 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2187 let c = read_candles_from_csv(file)?;
2188
2189 let output = CoppockBatchBuilder::new()
2190 .kernel(kernel)
2191 .short_range(5, 15, 5)
2192 .long_range(10, 20, 5)
2193 .ma_range(3, 9, 3)
2194 .apply_candles(&c, "close")?;
2195
2196 for (idx, &val) in output.values.iter().enumerate() {
2197 if val.is_nan() {
2198 continue;
2199 }
2200
2201 let bits = val.to_bits();
2202 let row = idx / output.cols;
2203 let col = idx % output.cols;
2204
2205 if bits == 0x11111111_11111111 {
2206 panic!(
2207 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at row {} col {} (flat index {})",
2208 test, val, bits, row, col, idx
2209 );
2210 }
2211
2212 if bits == 0x22222222_22222222 {
2213 panic!(
2214 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at row {} col {} (flat index {})",
2215 test, val, bits, row, col, idx
2216 );
2217 }
2218
2219 if bits == 0x33333333_33333333 {
2220 panic!(
2221 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at row {} col {} (flat index {})",
2222 test, val, bits, row, col, idx
2223 );
2224 }
2225 }
2226
2227 Ok(())
2228 }
2229
2230 #[cfg(not(debug_assertions))]
2231 fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2232 Ok(())
2233 }
2234
2235 macro_rules! gen_batch_tests {
2236 ($fn_name:ident) => {
2237 paste::paste! {
2238 #[test] fn [<$fn_name _scalar>]() {
2239 let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
2240 }
2241 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2242 #[test] fn [<$fn_name _avx2>]() {
2243 let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
2244 }
2245 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2246 #[test] fn [<$fn_name _avx512>]() {
2247 let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
2248 }
2249 #[test] fn [<$fn_name _auto_detect>]() {
2250 let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
2251 }
2252 }
2253 };
2254 }
2255 gen_batch_tests!(check_batch_default_row);
2256 gen_batch_tests!(check_batch_no_poison);
2257}
2258
2259#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2260#[wasm_bindgen]
2261pub fn coppock_js(
2262 data: &[f64],
2263 short_roc: usize,
2264 long_roc: usize,
2265 ma_period: usize,
2266 ma_type: &str,
2267) -> Result<Vec<f64>, JsValue> {
2268 let params = CoppockParams {
2269 short_roc_period: Some(short_roc),
2270 long_roc_period: Some(long_roc),
2271 ma_period: Some(ma_period),
2272 ma_type: Some(ma_type.to_string()),
2273 };
2274 let input = CoppockInput::from_slice(data, params);
2275 let mut out = vec![0.0; data.len()];
2276 coppock_into_slice(&mut out, &input, detect_best_kernel()).map_err(JsValue::from)?;
2277 Ok(out)
2278}
2279
2280#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2281#[derive(Serialize, Deserialize)]
2282pub struct CoppockBatchConfig {
2283 pub short_range: (usize, usize, usize),
2284 pub long_range: (usize, usize, usize),
2285 pub ma_range: (usize, usize, usize),
2286 pub ma_type: Option<String>,
2287}
2288
2289#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2290#[derive(Serialize, Deserialize)]
2291pub struct CoppockBatchJsOutput {
2292 pub values: Vec<f64>,
2293 pub combos: Vec<CoppockParams>,
2294 pub rows: usize,
2295 pub cols: usize,
2296}
2297
2298#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2299#[wasm_bindgen(js_name = coppock_batch)]
2300pub fn coppock_batch_unified_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
2301 let cfg: CoppockBatchConfig = serde_wasm_bindgen::from_value(config)
2302 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
2303 let sweep = CoppockBatchRange {
2304 short: cfg.short_range,
2305 long: cfg.long_range,
2306 ma: cfg.ma_range,
2307 };
2308 let out =
2309 coppock_batch_inner(data, &sweep, detect_best_kernel(), false).map_err(JsValue::from)?;
2310 let js = CoppockBatchJsOutput {
2311 values: out.values,
2312 combos: out.combos,
2313 rows: out.rows,
2314 cols: out.cols,
2315 };
2316 serde_wasm_bindgen::to_value(&js)
2317 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
2318}
2319
2320#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2321#[wasm_bindgen]
2322pub fn coppock_alloc(len: usize) -> *mut f64 {
2323 let mut vec = Vec::<f64>::with_capacity(len);
2324 let ptr = vec.as_mut_ptr();
2325 std::mem::forget(vec);
2326 ptr
2327}
2328
2329#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2330#[wasm_bindgen]
2331pub fn coppock_free(ptr: *mut f64, len: usize) {
2332 unsafe {
2333 let _ = Vec::from_raw_parts(ptr, len, len);
2334 }
2335}
2336
2337#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2338#[wasm_bindgen]
2339pub fn coppock_into(
2340 in_ptr: *const f64,
2341 out_ptr: *mut f64,
2342 len: usize,
2343 short_roc: usize,
2344 long_roc: usize,
2345 ma_period: usize,
2346 ma_type: &str,
2347) -> Result<(), JsValue> {
2348 if in_ptr.is_null() || out_ptr.is_null() {
2349 return Err(JsValue::from_str("null pointer"));
2350 }
2351
2352 if short_roc == 0 || long_roc == 0 || ma_period == 0 {
2353 return Err(JsValue::from_str("Invalid period"));
2354 }
2355
2356 let max_period = short_roc.max(long_roc).max(ma_period);
2357 if max_period > len {
2358 return Err(JsValue::from_str("Period exceeds data length"));
2359 }
2360
2361 unsafe {
2362 let data = std::slice::from_raw_parts(in_ptr, len);
2363 let params = CoppockParams {
2364 short_roc_period: Some(short_roc),
2365 long_roc_period: Some(long_roc),
2366 ma_period: Some(ma_period),
2367 ma_type: Some(ma_type.to_string()),
2368 };
2369 let input = CoppockInput::from_slice(data, params);
2370
2371 if in_ptr == out_ptr {
2372 let mut tmp = vec![0.0; len];
2373 coppock_into_slice(&mut tmp, &input, detect_best_kernel()).map_err(JsValue::from)?;
2374 let out = std::slice::from_raw_parts_mut(out_ptr, len);
2375 out.copy_from_slice(&tmp);
2376 } else {
2377 let out = std::slice::from_raw_parts_mut(out_ptr, len);
2378 coppock_into_slice(out, &input, detect_best_kernel()).map_err(JsValue::from)?;
2379 }
2380 }
2381 Ok(())
2382}
2383
2384#[cfg(feature = "python")]
2385use crate::utilities::kernel_validation::validate_kernel;
2386#[cfg(feature = "python")]
2387use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
2388#[cfg(feature = "python")]
2389use pyo3::exceptions::PyValueError;
2390#[cfg(feature = "python")]
2391use pyo3::prelude::*;
2392#[cfg(feature = "python")]
2393use pyo3::types::PyDict;
2394
2395#[cfg(feature = "python")]
2396#[pyfunction(name = "coppock")]
2397#[pyo3(signature = (data, short_roc_period, long_roc_period, ma_period, ma_type=None, kernel=None))]
2398pub fn coppock_py<'py>(
2399 py: Python<'py>,
2400 data: PyReadonlyArray1<'py, f64>,
2401 short_roc_period: usize,
2402 long_roc_period: usize,
2403 ma_period: usize,
2404 ma_type: Option<&str>,
2405 kernel: Option<&str>,
2406) -> PyResult<Bound<'py, PyArray1<f64>>> {
2407 let slice_in = data.as_slice()?;
2408 let kern = validate_kernel(kernel, false)?;
2409
2410 let params = CoppockParams {
2411 short_roc_period: Some(short_roc_period),
2412 long_roc_period: Some(long_roc_period),
2413 ma_period: Some(ma_period),
2414 ma_type: ma_type
2415 .map(|s| s.to_string())
2416 .or_else(|| Some("wma".to_string())),
2417 };
2418 let input = CoppockInput::from_slice(slice_in, params);
2419
2420 let result_vec: Vec<f64> = py
2421 .allow_threads(|| coppock_with_kernel(&input, kern).map(|o| o.values))
2422 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2423
2424 Ok(result_vec.into_pyarray(py))
2425}
2426
2427#[cfg(feature = "python")]
2428#[pyclass(name = "CoppockStream")]
2429pub struct CoppockStreamPy {
2430 stream: CoppockStream,
2431}
2432
2433#[cfg(feature = "python")]
2434#[pymethods]
2435impl CoppockStreamPy {
2436 #[new]
2437 fn new(
2438 short_roc_period: usize,
2439 long_roc_period: usize,
2440 ma_period: usize,
2441 ma_type: Option<&str>,
2442 ) -> PyResult<Self> {
2443 let params = CoppockParams {
2444 short_roc_period: Some(short_roc_period),
2445 long_roc_period: Some(long_roc_period),
2446 ma_period: Some(ma_period),
2447 ma_type: ma_type
2448 .map(|s| s.to_string())
2449 .or_else(|| Some("wma".to_string())),
2450 };
2451 let stream =
2452 CoppockStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
2453 Ok(CoppockStreamPy { stream })
2454 }
2455
2456 fn update(&mut self, value: f64) -> Option<f64> {
2457 self.stream.update(value)
2458 }
2459}
2460
2461#[cfg(feature = "python")]
2462#[pyfunction(name = "coppock_batch")]
2463#[pyo3(signature = (data, short_range, long_range, ma_range, ma_type=None, kernel=None))]
2464pub fn coppock_batch_py<'py>(
2465 py: Python<'py>,
2466 data: PyReadonlyArray1<'py, f64>,
2467 short_range: (usize, usize, usize),
2468 long_range: (usize, usize, usize),
2469 ma_range: (usize, usize, usize),
2470 ma_type: Option<&str>,
2471 kernel: Option<&str>,
2472) -> PyResult<Bound<'py, PyDict>> {
2473 let slice_in = data.as_slice()?;
2474 let kern = validate_kernel(kernel, true)?;
2475
2476 let sweep = CoppockBatchRange {
2477 short: short_range,
2478 long: long_range,
2479 ma: ma_range,
2480 };
2481
2482 let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
2483 let rows = combos.len();
2484 let cols = slice_in.len();
2485
2486 let out_arr = unsafe { PyArray1::<f64>::new(py, [rows * cols], false) };
2487 let slice_out = unsafe { out_arr.as_slice_mut()? };
2488
2489 let combos = py
2490 .allow_threads(|| {
2491 let kernel = match kern {
2492 Kernel::Auto => detect_best_batch_kernel(),
2493 k => k,
2494 };
2495
2496 let simd = match kernel {
2497 Kernel::Avx512Batch => Kernel::Avx512,
2498 Kernel::Avx2Batch => Kernel::Avx2,
2499 Kernel::ScalarBatch => Kernel::Scalar,
2500 _ => kernel,
2501 };
2502
2503 let mut filled_combos = combos.clone();
2504 if let Some(mt) = ma_type {
2505 for combo in &mut filled_combos {
2506 combo.ma_type = Some(mt.to_string());
2507 }
2508 }
2509
2510 coppock_batch_inner_into(slice_in, &sweep, simd, true, slice_out)?;
2511 Ok::<Vec<CoppockParams>, CoppockError>(filled_combos)
2512 })
2513 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2514
2515 let dict = PyDict::new(py);
2516 dict.set_item("values", out_arr.reshape((rows, cols))?)?;
2517 dict.set_item(
2518 "shorts",
2519 combos
2520 .iter()
2521 .map(|p| p.short_roc_period.unwrap() as u64)
2522 .collect::<Vec<_>>()
2523 .into_pyarray(py),
2524 )?;
2525 dict.set_item(
2526 "longs",
2527 combos
2528 .iter()
2529 .map(|p| p.long_roc_period.unwrap() as u64)
2530 .collect::<Vec<_>>()
2531 .into_pyarray(py),
2532 )?;
2533 dict.set_item(
2534 "ma_periods",
2535 combos
2536 .iter()
2537 .map(|p| p.ma_period.unwrap() as u64)
2538 .collect::<Vec<_>>()
2539 .into_pyarray(py),
2540 )?;
2541 dict.set_item(
2542 "ma_types",
2543 combos
2544 .iter()
2545 .map(|p| p.ma_type.as_deref().unwrap_or("wma"))
2546 .collect::<Vec<_>>(),
2547 )?;
2548
2549 Ok(dict)
2550}
2551
2552#[cfg(all(feature = "python", feature = "cuda"))]
2553use crate::cuda::cuda_available;
2554#[cfg(all(feature = "python", feature = "cuda"))]
2555use crate::cuda::oscillators::coppock_wrapper::{CudaCoppock, DeviceArrayF32Coppock};
2556
2557#[cfg(all(feature = "python", feature = "cuda"))]
2558#[pyclass(module = "ta_indicators.cuda", unsendable)]
2559pub struct CoppockDeviceArrayF32Py {
2560 pub(crate) inner: DeviceArrayF32Coppock,
2561}
2562
2563#[cfg(all(feature = "python", feature = "cuda"))]
2564#[pymethods]
2565impl CoppockDeviceArrayF32Py {
2566 #[getter]
2567 fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
2568 let d = PyDict::new(py);
2569 d.set_item("shape", (self.inner.rows, self.inner.cols))?;
2570 d.set_item("typestr", "<f4")?;
2571 d.set_item(
2572 "strides",
2573 (
2574 self.inner.cols * std::mem::size_of::<f32>(),
2575 std::mem::size_of::<f32>(),
2576 ),
2577 )?;
2578 d.set_item("data", (self.inner.device_ptr() as usize, false))?;
2579
2580 d.set_item("version", 3)?;
2581 Ok(d)
2582 }
2583
2584 fn __dlpack_device__(&self) -> (i32, i32) {
2585 (2, self.inner.device_id as i32)
2586 }
2587
2588 #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
2589 fn __dlpack__<'py>(
2590 &mut self,
2591 py: Python<'py>,
2592 stream: Option<pyo3::PyObject>,
2593 max_version: Option<pyo3::PyObject>,
2594 dl_device: Option<pyo3::PyObject>,
2595 copy: Option<pyo3::PyObject>,
2596 ) -> PyResult<PyObject> {
2597 use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
2598 use cust::memory::DeviceBuffer;
2599
2600 let (kdl, alloc_dev) = self.__dlpack_device__();
2601 if let Some(dev_obj) = dl_device.as_ref() {
2602 if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
2603 if dev_ty != kdl || dev_id != alloc_dev {
2604 let wants_copy = copy
2605 .as_ref()
2606 .and_then(|c| c.extract::<bool>(py).ok())
2607 .unwrap_or(false);
2608 if wants_copy {
2609 return Err(PyValueError::new_err(
2610 "device copy not implemented for __dlpack__",
2611 ));
2612 } else {
2613 return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
2614 }
2615 }
2616 }
2617 }
2618
2619 if let Some(s) = stream.as_ref() {
2620 if let Ok(i) = s.extract::<i64>(py) {
2621 if i == 0 {
2622 return Err(PyValueError::new_err(
2623 "__dlpack__: stream 0 is disallowed for CUDA",
2624 ));
2625 }
2626 }
2627 }
2628
2629 let dummy =
2630 DeviceBuffer::from_slice(&[]).map_err(|e| PyValueError::new_err(e.to_string()))?;
2631 let ctx_clone = self.inner.ctx.clone();
2632 let device_id = self.inner.device_id;
2633 let inner = std::mem::replace(
2634 &mut self.inner,
2635 DeviceArrayF32Coppock {
2636 buf: dummy,
2637 rows: 0,
2638 cols: 0,
2639 ctx: ctx_clone,
2640 device_id,
2641 },
2642 );
2643
2644 let rows = inner.rows;
2645 let cols = inner.cols;
2646 let buf = inner.buf;
2647
2648 let max_version_bound = max_version.map(|obj| obj.into_bound(py));
2649
2650 export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
2651 }
2652}
2653
2654#[cfg(all(feature = "python", feature = "cuda"))]
2655#[pyfunction(name = "coppock_cuda_batch_dev")]
2656#[pyo3(signature = (data, short_range, long_range, ma_range, device_id=0))]
2657pub fn coppock_cuda_batch_dev_py(
2658 py: Python<'_>,
2659 data: numpy::PyReadonlyArray1<'_, f64>,
2660 short_range: (usize, usize, usize),
2661 long_range: (usize, usize, usize),
2662 ma_range: (usize, usize, usize),
2663 device_id: usize,
2664) -> PyResult<CoppockDeviceArrayF32Py> {
2665 if !cuda_available() {
2666 return Err(PyValueError::new_err("CUDA not available"));
2667 }
2668 let price = data.as_slice()?;
2669 let price_f32: Vec<f32> = price.iter().map(|&v| v as f32).collect();
2670 let sweep = CoppockBatchRange {
2671 short: short_range,
2672 long: long_range,
2673 ma: ma_range,
2674 };
2675 let inner = py.allow_threads(|| {
2676 let cuda = CudaCoppock::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2677 cuda.coppock_batch_dev(&price_f32, &sweep)
2678 .map_err(|e| PyValueError::new_err(e.to_string()))
2679 })?;
2680 Ok(CoppockDeviceArrayF32Py { inner })
2681}
2682
2683#[cfg(all(feature = "python", feature = "cuda"))]
2684#[pyfunction(name = "coppock_cuda_many_series_one_param_dev")]
2685#[pyo3(signature = (data_tm, cols, rows, short_period, long_period, ma_period, device_id=0))]
2686pub fn coppock_cuda_many_series_one_param_dev_py(
2687 py: Python<'_>,
2688 data_tm: numpy::PyReadonlyArray1<'_, f64>,
2689 cols: usize,
2690 rows: usize,
2691 short_period: usize,
2692 long_period: usize,
2693 ma_period: usize,
2694 device_id: usize,
2695) -> PyResult<CoppockDeviceArrayF32Py> {
2696 if !cuda_available() {
2697 return Err(PyValueError::new_err("CUDA not available"));
2698 }
2699 let slice = data_tm.as_slice()?;
2700 let expected = cols
2701 .checked_mul(rows)
2702 .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
2703 if slice.len() != expected {
2704 return Err(PyValueError::new_err("time-major input length mismatch"));
2705 }
2706 let price_f32: Vec<f32> = slice.iter().map(|&v| v as f32).collect();
2707 let inner = py.allow_threads(|| {
2708 let cuda = CudaCoppock::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2709 cuda.coppock_many_series_one_param_time_major_dev(
2710 &price_f32,
2711 cols,
2712 rows,
2713 short_period,
2714 long_period,
2715 ma_period,
2716 )
2717 .map_err(|e| PyValueError::new_err(e.to_string()))
2718 })?;
2719 Ok(CoppockDeviceArrayF32Py { inner })
2720}