1use crate::utilities::data_loader::{source_type, Candles};
2use crate::utilities::enums::Kernel;
3use crate::utilities::helpers::{
4 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
5 make_uninit_matrix,
6};
7#[cfg(feature = "python")]
8use crate::utilities::kernel_validation::validate_kernel;
9#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
10use core::arch::x86_64::*;
11#[cfg(feature = "python")]
12use numpy::{IntoPyArray, PyArray1};
13#[cfg(feature = "python")]
14use pyo3::exceptions::PyValueError;
15#[cfg(feature = "python")]
16use pyo3::prelude::*;
17#[cfg(feature = "python")]
18use pyo3::types::PyDict;
19#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
20use serde::{Deserialize, Serialize};
21use std::convert::AsRef;
22use std::error::Error;
23use std::mem::MaybeUninit;
24use thiserror::Error;
25#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
26use wasm_bindgen::prelude::*;
27
28#[cfg(all(feature = "python", feature = "cuda"))]
29use crate::cuda::moving_averages::DeviceArrayF32;
30#[cfg(all(feature = "python", feature = "cuda"))]
31use crate::cuda::{cuda_available, CudaMarketefi};
32#[cfg(all(feature = "python", feature = "cuda"))]
33use crate::utilities::dlpack_cuda::DeviceArrayF32Py as SharedDeviceArrayF32Py;
34#[cfg(all(feature = "python", feature = "cuda"))]
35use cust::context::Context;
36#[cfg(all(feature = "python", feature = "cuda"))]
37use std::sync::Arc;
38
39#[derive(Debug, Clone)]
40pub enum MarketefiData<'a> {
41 Candles {
42 candles: &'a Candles,
43 source_high: &'a str,
44 source_low: &'a str,
45 source_volume: &'a str,
46 },
47 Slices {
48 high: &'a [f64],
49 low: &'a [f64],
50 volume: &'a [f64],
51 },
52}
53
54#[derive(Debug, Clone)]
55#[cfg_attr(
56 all(target_arch = "wasm32", feature = "wasm"),
57 derive(Serialize, Deserialize)
58)]
59pub struct MarketefiParams;
60
61impl Default for MarketefiParams {
62 fn default() -> Self {
63 Self
64 }
65}
66
67#[derive(Debug, Clone)]
68pub struct MarketefiInput<'a> {
69 pub data: MarketefiData<'a>,
70 pub params: MarketefiParams,
71}
72
73impl<'a> MarketefiInput<'a> {
74 #[inline]
75 pub fn from_candles(
76 candles: &'a Candles,
77 source_high: &'a str,
78 source_low: &'a str,
79 source_volume: &'a str,
80 params: MarketefiParams,
81 ) -> Self {
82 Self {
83 data: MarketefiData::Candles {
84 candles,
85 source_high,
86 source_low,
87 source_volume,
88 },
89 params,
90 }
91 }
92 #[inline]
93 pub fn from_slices(
94 high: &'a [f64],
95 low: &'a [f64],
96 volume: &'a [f64],
97 params: MarketefiParams,
98 ) -> Self {
99 Self {
100 data: MarketefiData::Slices { high, low, volume },
101 params,
102 }
103 }
104 #[inline]
105 pub fn with_default_candles(candles: &'a Candles) -> Self {
106 Self::from_candles(candles, "high", "low", "volume", MarketefiParams::default())
107 }
108}
109
110#[derive(Debug, Clone)]
111pub struct MarketefiOutput {
112 pub values: Vec<f64>,
113}
114
115#[derive(Copy, Clone, Debug, Default)]
116pub struct MarketefiBuilder {
117 kernel: Kernel,
118}
119
120impl MarketefiBuilder {
121 #[inline(always)]
122 pub fn new() -> Self {
123 Self::default()
124 }
125 #[inline(always)]
126 pub fn kernel(mut self, k: Kernel) -> Self {
127 self.kernel = k;
128 self
129 }
130 #[inline(always)]
131 pub fn apply(self, c: &Candles) -> Result<MarketefiOutput, MarketefiError> {
132 let i = MarketefiInput::with_default_candles(c);
133 marketefi_with_kernel(&i, self.kernel)
134 }
135 #[inline(always)]
136 pub fn apply_slices(
137 self,
138 high: &[f64],
139 low: &[f64],
140 volume: &[f64],
141 ) -> Result<MarketefiOutput, MarketefiError> {
142 let i = MarketefiInput::from_slices(high, low, volume, MarketefiParams::default());
143 marketefi_with_kernel(&i, self.kernel)
144 }
145 #[inline(always)]
146 pub fn into_stream(self) -> MarketefiStream {
147 MarketefiStream::new()
148 }
149}
150
151#[derive(Debug, Error)]
152pub enum MarketefiError {
153 #[error("marketefi: Input data slice is empty.")]
154 EmptyInputData,
155 #[error("marketefi: All values are NaN.")]
156 AllValuesNaN,
157 #[error("marketefi: Invalid period: period = {period}, data length = {data_len}")]
158 InvalidPeriod { period: usize, data_len: usize },
159 #[error("marketefi: Not enough valid data: needed = {needed}, valid = {valid}")]
160 NotEnoughValidData { needed: usize, valid: usize },
161 #[error("marketefi: Mismatched data length among high, low, and volume.")]
162 MismatchedDataLength,
163 #[error("marketefi: Output length mismatch: expected {expected}, got {got}")]
164 OutputLengthMismatch { expected: usize, got: usize },
165 #[error("marketefi: Invalid range: start={start}, end={end}, step={step}")]
166 InvalidRange {
167 start: String,
168 end: String,
169 step: String,
170 },
171 #[error("marketefi: Invalid kernel for batch: {0:?}")]
172 InvalidKernelForBatch(Kernel),
173 #[error("marketefi: Zero or NaN volume at a valid index.")]
174 ZeroOrNaNVolume,
175}
176
177#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
178impl From<MarketefiError> for JsValue {
179 fn from(err: MarketefiError) -> Self {
180 JsValue::from_str(&err.to_string())
181 }
182}
183
184#[inline]
185pub fn marketefi(input: &MarketefiInput) -> Result<MarketefiOutput, MarketefiError> {
186 marketefi_with_kernel(input, Kernel::Auto)
187}
188
189#[inline(always)]
190fn marketefi_prepare<'a>(
191 input: &'a MarketefiInput<'a>,
192 kernel: Kernel,
193) -> Result<(&'a [f64], &'a [f64], &'a [f64], usize, Kernel), MarketefiError> {
194 let (high, low, volume) = match &input.data {
195 MarketefiData::Candles {
196 candles,
197 source_high,
198 source_low,
199 source_volume,
200 } => (
201 source_type(candles, source_high),
202 source_type(candles, source_low),
203 source_type(candles, source_volume),
204 ),
205 MarketefiData::Slices { high, low, volume } => (*high, *low, *volume),
206 };
207
208 if high.is_empty() || low.is_empty() || volume.is_empty() {
209 return Err(MarketefiError::EmptyInputData);
210 }
211 if high.len() != low.len() || low.len() != volume.len() {
212 return Err(MarketefiError::MismatchedDataLength);
213 }
214
215 let len = high.len();
216 let first = (0..len)
217 .find(|&i| {
218 let h = high[i];
219 let l = low[i];
220 let v = volume[i];
221 !(h.is_nan() || l.is_nan() || v.is_nan())
222 })
223 .ok_or(MarketefiError::AllValuesNaN)?;
224
225 let chosen = match kernel {
226 Kernel::Auto => match detect_best_kernel() {
227 Kernel::Avx512 => Kernel::Avx2,
228 other => other,
229 },
230 k => k,
231 };
232 Ok((high, low, volume, first, chosen))
233}
234
235#[inline(always)]
236fn marketefi_compute_into(
237 high: &[f64],
238 low: &[f64],
239 volume: &[f64],
240 first: usize,
241 kernel: Kernel,
242 out: &mut [f64],
243) {
244 unsafe {
245 match kernel {
246 Kernel::Scalar | Kernel::ScalarBatch => marketefi_scalar(high, low, volume, first, out),
247 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
248 Kernel::Avx2 | Kernel::Avx2Batch => marketefi_avx2(high, low, volume, first, out),
249 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
250 Kernel::Avx512 | Kernel::Avx512Batch => marketefi_avx512(high, low, volume, first, out),
251 _ => marketefi_scalar(high, low, volume, first, out),
252 }
253 }
254}
255
256#[inline(always)]
257fn marketefi_compute_into_any_valid(
258 high: &[f64],
259 low: &[f64],
260 volume: &[f64],
261 first: usize,
262 kernel: Kernel,
263 out: &mut [f64],
264) -> bool {
265 unsafe {
266 match kernel {
267 Kernel::Scalar | Kernel::ScalarBatch => {
268 marketefi_scalar_any_valid(high, low, volume, first, out)
269 }
270 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
271 Kernel::Avx2 | Kernel::Avx2Batch => {
272 marketefi_avx2_any_valid(high, low, volume, first, out)
273 }
274 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
275 Kernel::Avx512 | Kernel::Avx512Batch => {
276 marketefi_avx512_any_valid(high, low, volume, first, out)
277 }
278 _ => marketefi_scalar_any_valid(high, low, volume, first, out),
279 }
280 }
281}
282
283#[inline]
284pub fn marketefi_into_slice(
285 dst: &mut [f64],
286 input: &MarketefiInput,
287 kern: Kernel,
288) -> Result<(), MarketefiError> {
289 let (h, l, v, first, chosen) = marketefi_prepare(input, kern)?;
290 if dst.len() != h.len() {
291 return Err(MarketefiError::OutputLengthMismatch {
292 expected: h.len(),
293 got: dst.len(),
294 });
295 }
296
297 let any_valid = marketefi_compute_into_any_valid(h, l, v, first, chosen, dst);
298 for x in &mut dst[..first] {
299 *x = f64::NAN;
300 }
301
302 if !any_valid {
303 return Err(MarketefiError::NotEnoughValidData {
304 needed: 1,
305 valid: 0,
306 });
307 }
308 Ok(())
309}
310
311pub fn marketefi_with_kernel(
312 input: &MarketefiInput,
313 kernel: Kernel,
314) -> Result<MarketefiOutput, MarketefiError> {
315 let (h, l, v, first, chosen) = marketefi_prepare(input, kernel)?;
316 let mut out = alloc_with_nan_prefix(h.len(), first);
317 let any_valid = marketefi_compute_into_any_valid(h, l, v, first, chosen, &mut out);
318
319 if !any_valid {
320 return Err(MarketefiError::NotEnoughValidData {
321 needed: 1,
322 valid: 0,
323 });
324 }
325
326 Ok(MarketefiOutput { values: out })
327}
328
329#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
330#[inline]
331pub fn marketefi_into(input: &MarketefiInput, out: &mut [f64]) -> Result<(), MarketefiError> {
332 let (h, l, v, first, chosen) = marketefi_prepare(input, Kernel::Auto)?;
333 if out.len() != h.len() {
334 return Err(MarketefiError::OutputLengthMismatch {
335 expected: h.len(),
336 got: out.len(),
337 });
338 }
339
340 for x in &mut out[..first] {
341 *x = f64::from_bits(0x7ff8_0000_0000_0000);
342 }
343
344 let any_valid = marketefi_compute_into_any_valid(h, l, v, first, chosen, out);
345
346 if !any_valid {
347 return Err(MarketefiError::NotEnoughValidData {
348 needed: 1,
349 valid: 0,
350 });
351 }
352 Ok(())
353}
354
355#[inline]
356fn marketefi_scalar_any_valid(
357 high: &[f64],
358 low: &[f64],
359 volume: &[f64],
360 first_valid: usize,
361 out: &mut [f64],
362) -> bool {
363 let n = high.len();
364 if first_valid >= n {
365 return false;
366 }
367
368 let mut any_valid = false;
369 let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
370
371 unsafe {
372 let hp = high.as_ptr();
373 let lp = low.as_ptr();
374 let vp = volume.as_ptr();
375 let op = out.as_mut_ptr();
376
377 let mut i = first_valid;
378 while i + 4 <= n {
379 let v0 = *vp.add(i);
380 if v0 == 0.0 {
381 *op.add(i) = qnan;
382 } else {
383 let res0 = (*hp.add(i) - *lp.add(i)) / v0;
384 if res0.is_nan() {
385 *op.add(i) = qnan;
386 } else {
387 *op.add(i) = res0;
388 any_valid = true;
389 }
390 }
391
392 let v1 = *vp.add(i + 1);
393 if v1 == 0.0 {
394 *op.add(i + 1) = qnan;
395 } else {
396 let res1 = (*hp.add(i + 1) - *lp.add(i + 1)) / v1;
397 if res1.is_nan() {
398 *op.add(i + 1) = qnan;
399 } else {
400 *op.add(i + 1) = res1;
401 any_valid = true;
402 }
403 }
404
405 let v2 = *vp.add(i + 2);
406 if v2 == 0.0 {
407 *op.add(i + 2) = qnan;
408 } else {
409 let res2 = (*hp.add(i + 2) - *lp.add(i + 2)) / v2;
410 if res2.is_nan() {
411 *op.add(i + 2) = qnan;
412 } else {
413 *op.add(i + 2) = res2;
414 any_valid = true;
415 }
416 }
417
418 let v3 = *vp.add(i + 3);
419 if v3 == 0.0 {
420 *op.add(i + 3) = qnan;
421 } else {
422 let res3 = (*hp.add(i + 3) - *lp.add(i + 3)) / v3;
423 if res3.is_nan() {
424 *op.add(i + 3) = qnan;
425 } else {
426 *op.add(i + 3) = res3;
427 any_valid = true;
428 }
429 }
430
431 i += 4;
432 }
433
434 while i < n {
435 let v = *vp.add(i);
436 if v == 0.0 {
437 *op.add(i) = qnan;
438 } else {
439 let res = (*hp.add(i) - *lp.add(i)) / v;
440 if res.is_nan() {
441 *op.add(i) = qnan;
442 } else {
443 *op.add(i) = res;
444 any_valid = true;
445 }
446 }
447 i += 1;
448 }
449 }
450
451 any_valid
452}
453
454#[inline]
455pub fn marketefi_scalar(
456 high: &[f64],
457 low: &[f64],
458 volume: &[f64],
459 first_valid: usize,
460 out: &mut [f64],
461) {
462 let _ = marketefi_scalar_any_valid(high, low, volume, first_valid, out);
463}
464
465#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
466#[inline]
467pub fn marketefi_avx512(
468 high: &[f64],
469 low: &[f64],
470 volume: &[f64],
471 first_valid: usize,
472 out: &mut [f64],
473) {
474 #[target_feature(enable = "avx512f")]
475 unsafe fn avx512_body(
476 high: &[f64],
477 low: &[f64],
478 volume: &[f64],
479 first: usize,
480 out: &mut [f64],
481 ) {
482 let n = high.len();
483 let hp = high.as_ptr();
484 let lp = low.as_ptr();
485 let vp = volume.as_ptr();
486 let op = out.as_mut_ptr();
487
488 let mut i = first;
489 let vnan = _mm512_set1_pd(f64::NAN);
490 let vzero = _mm512_set1_pd(0.0);
491
492 while i + 8 <= n {
493 let h = _mm512_loadu_pd(hp.add(i));
494 let l = _mm512_loadu_pd(lp.add(i));
495 let v = _mm512_loadu_pd(vp.add(i));
496
497 let mh = _mm512_cmp_pd_mask(h, h, _CMP_ORD_Q);
498 let ml = _mm512_cmp_pd_mask(l, l, _CMP_ORD_Q);
499 let mv = _mm512_cmp_pd_mask(v, v, _CMP_ORD_Q);
500 let mnz = _mm512_cmp_pd_mask(v, vzero, _CMP_NEQ_OQ);
501 let mvalid = mh & ml & mv & mnz;
502
503 let diff = _mm512_sub_pd(h, l);
504
505 let mut y = _mm512_rcp14_pd(v);
506
507 let two = _mm512_set1_pd(2.0);
508 let t1 = _mm512_mul_pd(v, y);
509 let t2 = _mm512_sub_pd(two, t1);
510 y = _mm512_mul_pd(y, t2);
511
512 let t1b = _mm512_mul_pd(v, y);
513 let t2b = _mm512_sub_pd(two, t1b);
514 y = _mm512_mul_pd(y, t2b);
515
516 let res = _mm512_mul_pd(diff, y);
517
518 let outv = _mm512_mask_mov_pd(vnan, mvalid, res);
519 _mm512_storeu_pd(op.add(i), outv);
520 i += 8;
521 }
522
523 while i < n {
524 let h = *hp.add(i);
525 let l = *lp.add(i);
526 let v = *vp.add(i);
527 *op.add(i) = if v != 0.0 && !(h.is_nan() | l.is_nan() | v.is_nan()) {
528 (h - l) / v
529 } else {
530 f64::NAN
531 };
532 i += 1;
533 }
534 }
535
536 unsafe { avx512_body(high, low, volume, first_valid, out) }
537}
538
539#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
540#[inline]
541fn marketefi_avx512_any_valid(
542 high: &[f64],
543 low: &[f64],
544 volume: &[f64],
545 first_valid: usize,
546 out: &mut [f64],
547) -> bool {
548 #[target_feature(enable = "avx512f")]
549 unsafe fn avx512_body_any(
550 high: &[f64],
551 low: &[f64],
552 volume: &[f64],
553 first: usize,
554 out: &mut [f64],
555 ) -> bool {
556 let n = high.len();
557 let hp = high.as_ptr();
558 let lp = low.as_ptr();
559 let vp = volume.as_ptr();
560 let op = out.as_mut_ptr();
561
562 let mut i = first;
563 let mut any_valid = false;
564 let vnan = _mm512_set1_pd(f64::NAN);
565 let vzero = _mm512_set1_pd(0.0);
566
567 while i + 8 <= n {
568 let h = _mm512_loadu_pd(hp.add(i));
569 let l = _mm512_loadu_pd(lp.add(i));
570 let v = _mm512_loadu_pd(vp.add(i));
571
572 let mh = _mm512_cmp_pd_mask(h, h, _CMP_ORD_Q);
573 let ml = _mm512_cmp_pd_mask(l, l, _CMP_ORD_Q);
574 let mv = _mm512_cmp_pd_mask(v, v, _CMP_ORD_Q);
575 let mnz = _mm512_cmp_pd_mask(v, vzero, _CMP_NEQ_OQ);
576 let mvalid = mh & ml & mv & mnz;
577 if mvalid != 0 {
578 any_valid = true;
579 }
580
581 let diff = _mm512_sub_pd(h, l);
582
583 let mut y = _mm512_rcp14_pd(v);
584 let two = _mm512_set1_pd(2.0);
585 let t1 = _mm512_mul_pd(v, y);
586 let t2 = _mm512_sub_pd(two, t1);
587 y = _mm512_mul_pd(y, t2);
588 let t1b = _mm512_mul_pd(v, y);
589 let t2b = _mm512_sub_pd(two, t1b);
590 y = _mm512_mul_pd(y, t2b);
591
592 let res = _mm512_mul_pd(diff, y);
593 let outv = _mm512_mask_mov_pd(vnan, mvalid, res);
594 _mm512_storeu_pd(op.add(i), outv);
595 i += 8;
596 }
597
598 while i < n {
599 let h = *hp.add(i);
600 let l = *lp.add(i);
601 let v = *vp.add(i);
602 if v != 0.0 && !(h.is_nan() | l.is_nan() | v.is_nan()) {
603 *op.add(i) = (h - l) / v;
604 any_valid = true;
605 } else {
606 *op.add(i) = f64::NAN;
607 }
608 i += 1;
609 }
610
611 any_valid
612 }
613
614 unsafe { avx512_body_any(high, low, volume, first_valid, out) }
615}
616
617#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
618#[inline]
619pub fn marketefi_avx2(
620 high: &[f64],
621 low: &[f64],
622 volume: &[f64],
623 first_valid: usize,
624 out: &mut [f64],
625) {
626 #[target_feature(enable = "avx2")]
627 unsafe fn avx2_body(high: &[f64], low: &[f64], volume: &[f64], first: usize, out: &mut [f64]) {
628 let n = high.len();
629 let hp = high.as_ptr();
630 let lp = low.as_ptr();
631 let vp = volume.as_ptr();
632 let op = out.as_mut_ptr();
633
634 let mut i = first;
635 let vzero = _mm256_set1_pd(0.0);
636 let vnan = _mm256_set1_pd(f64::NAN);
637
638 while i + 4 <= n {
639 let h = _mm256_loadu_pd(hp.add(i));
640 let l = _mm256_loadu_pd(lp.add(i));
641 let v = _mm256_loadu_pd(vp.add(i));
642
643 let ord_h = _mm256_cmp_pd(h, h, _CMP_ORD_Q);
644 let ord_l = _mm256_cmp_pd(l, l, _CMP_ORD_Q);
645 let ord_v = _mm256_cmp_pd(v, v, _CMP_ORD_Q);
646 let nz_v = _mm256_cmp_pd(v, vzero, _CMP_NEQ_OQ);
647 let valid = _mm256_and_pd(_mm256_and_pd(ord_h, ord_l), _mm256_and_pd(ord_v, nz_v));
648
649 let diff = _mm256_sub_pd(h, l);
650 let res = _mm256_div_pd(diff, v);
651 let outv = _mm256_blendv_pd(vnan, res, valid);
652
653 _mm256_storeu_pd(op.add(i), outv);
654 i += 4;
655 }
656
657 while i < n {
658 let h = *hp.add(i);
659 let l = *lp.add(i);
660 let v = *vp.add(i);
661 *op.add(i) = if v != 0.0 && !(h.is_nan() | l.is_nan() | v.is_nan()) {
662 (h - l) / v
663 } else {
664 f64::NAN
665 };
666 i += 1;
667 }
668 }
669
670 unsafe { avx2_body(high, low, volume, first_valid, out) }
671}
672
673#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
674#[inline]
675fn marketefi_avx2_any_valid(
676 high: &[f64],
677 low: &[f64],
678 volume: &[f64],
679 first_valid: usize,
680 out: &mut [f64],
681) -> bool {
682 #[target_feature(enable = "avx2")]
683 unsafe fn avx2_body_any(
684 high: &[f64],
685 low: &[f64],
686 volume: &[f64],
687 first: usize,
688 out: &mut [f64],
689 ) -> bool {
690 let n = high.len();
691 let hp = high.as_ptr();
692 let lp = low.as_ptr();
693 let vp = volume.as_ptr();
694 let op = out.as_mut_ptr();
695
696 let mut i = first;
697 let mut any_valid = false;
698 let vzero = _mm256_set1_pd(0.0);
699 let vnan = _mm256_set1_pd(f64::NAN);
700
701 while i + 4 <= n {
702 let h = _mm256_loadu_pd(hp.add(i));
703 let l = _mm256_loadu_pd(lp.add(i));
704 let v = _mm256_loadu_pd(vp.add(i));
705
706 let ord_h = _mm256_cmp_pd(h, h, _CMP_ORD_Q);
707 let ord_l = _mm256_cmp_pd(l, l, _CMP_ORD_Q);
708 let ord_v = _mm256_cmp_pd(v, v, _CMP_ORD_Q);
709 let nz_v = _mm256_cmp_pd(v, vzero, _CMP_NEQ_OQ);
710 let valid = _mm256_and_pd(_mm256_and_pd(ord_h, ord_l), _mm256_and_pd(ord_v, nz_v));
711
712 if _mm256_movemask_pd(valid) != 0 {
713 any_valid = true;
714 }
715
716 let diff = _mm256_sub_pd(h, l);
717 let res = _mm256_div_pd(diff, v);
718 let outv = _mm256_blendv_pd(vnan, res, valid);
719
720 _mm256_storeu_pd(op.add(i), outv);
721 i += 4;
722 }
723
724 while i < n {
725 let h = *hp.add(i);
726 let l = *lp.add(i);
727 let v = *vp.add(i);
728 if v != 0.0 && !(h.is_nan() | l.is_nan() | v.is_nan()) {
729 *op.add(i) = (h - l) / v;
730 any_valid = true;
731 } else {
732 *op.add(i) = f64::NAN;
733 }
734 i += 1;
735 }
736
737 any_valid
738 }
739
740 unsafe { avx2_body_any(high, low, volume, first_valid, out) }
741}
742
743#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
744#[inline]
745pub fn marketefi_avx512_short(
746 high: &[f64],
747 low: &[f64],
748 volume: &[f64],
749 first_valid: usize,
750 out: &mut [f64],
751) {
752 marketefi_avx512(high, low, volume, first_valid, out)
753}
754
755#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
756#[inline]
757pub fn marketefi_avx512_long(
758 high: &[f64],
759 low: &[f64],
760 volume: &[f64],
761 first_valid: usize,
762 out: &mut [f64],
763) {
764 marketefi_avx512(high, low, volume, first_valid, out)
765}
766
767#[inline(always)]
768pub fn marketefi_row_scalar(
769 high: &[f64],
770 low: &[f64],
771 volume: &[f64],
772 first: usize,
773 out: &mut [f64],
774) {
775 marketefi_scalar(high, low, volume, first, out)
776}
777
778#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
779#[inline(always)]
780pub fn marketefi_row_avx2(
781 high: &[f64],
782 low: &[f64],
783 volume: &[f64],
784 first: usize,
785 out: &mut [f64],
786) {
787 marketefi_scalar(high, low, volume, first, out)
788}
789
790#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
791#[inline(always)]
792pub fn marketefi_row_avx512(
793 high: &[f64],
794 low: &[f64],
795 volume: &[f64],
796 first: usize,
797 out: &mut [f64],
798) {
799 marketefi_scalar(high, low, volume, first, out)
800}
801
802#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
803#[inline(always)]
804pub fn marketefi_row_avx512_short(
805 high: &[f64],
806 low: &[f64],
807 volume: &[f64],
808 first: usize,
809 out: &mut [f64],
810) {
811 marketefi_scalar(high, low, volume, first, out)
812}
813
814#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
815#[inline(always)]
816pub fn marketefi_row_avx512_long(
817 high: &[f64],
818 low: &[f64],
819 volume: &[f64],
820 first: usize,
821 out: &mut [f64],
822) {
823 marketefi_scalar(high, low, volume, first, out)
824}
825
826#[derive(Clone, Debug)]
827pub struct MarketefiBatchRange;
828
829impl Default for MarketefiBatchRange {
830 fn default() -> Self {
831 Self
832 }
833}
834
835#[derive(Clone, Debug, Default)]
836pub struct MarketefiBatchBuilder {
837 kernel: Kernel,
838}
839
840impl MarketefiBatchBuilder {
841 pub fn new() -> Self {
842 Self::default()
843 }
844 pub fn kernel(mut self, k: Kernel) -> Self {
845 self.kernel = k;
846 self
847 }
848 pub fn apply_slices(
849 self,
850 high: &[f64],
851 low: &[f64],
852 volume: &[f64],
853 ) -> Result<MarketefiBatchOutput, MarketefiError> {
854 marketefi_batch_with_kernel(high, low, volume, self.kernel)
855 }
856 pub fn with_default_candles(c: &Candles) -> Result<MarketefiBatchOutput, MarketefiError> {
857 let high = source_type(c, "high");
858 let low = source_type(c, "low");
859 let volume = source_type(c, "volume");
860 MarketefiBatchBuilder::new()
861 .kernel(Kernel::Auto)
862 .apply_slices(high, low, volume)
863 }
864}
865
866pub fn marketefi_batch_with_kernel(
867 high: &[f64],
868 low: &[f64],
869 volume: &[f64],
870 kernel: Kernel,
871) -> Result<MarketefiBatchOutput, MarketefiError> {
872 let k = match kernel {
873 Kernel::Auto => detect_best_batch_kernel(),
874 x if x.is_batch() => x,
875 other => return Err(MarketefiError::InvalidKernelForBatch(other)),
876 };
877 marketefi_batch_par_slice(high, low, volume, k)
878}
879
880#[derive(Clone, Debug)]
881pub struct MarketefiBatchOutput {
882 pub values: Vec<f64>,
883 pub combos: Vec<MarketefiParams>,
884 pub rows: usize,
885 pub cols: usize,
886}
887
888#[inline(always)]
889pub fn marketefi_batch_slice(
890 high: &[f64],
891 low: &[f64],
892 volume: &[f64],
893 kernel: Kernel,
894) -> Result<MarketefiBatchOutput, MarketefiError> {
895 marketefi_batch_inner(high, low, volume, kernel, false)
896}
897
898#[inline(always)]
899pub fn marketefi_batch_par_slice(
900 high: &[f64],
901 low: &[f64],
902 volume: &[f64],
903 kernel: Kernel,
904) -> Result<MarketefiBatchOutput, MarketefiError> {
905 marketefi_batch_inner(high, low, volume, kernel, true)
906}
907
908#[inline(always)]
909fn marketefi_batch_inner_into(
910 high: &[f64],
911 low: &[f64],
912 volume: &[f64],
913 kernel: Kernel,
914 _parallel: bool,
915 out: &mut [f64],
916) -> Result<(), MarketefiError> {
917 if high.is_empty() || low.is_empty() || volume.is_empty() {
918 return Err(MarketefiError::EmptyInputData);
919 }
920 if high.len() != low.len() || low.len() != volume.len() {
921 return Err(MarketefiError::MismatchedDataLength);
922 }
923
924 let cols = high.len();
925 let first = (0..cols)
926 .find(|&i| !(high[i].is_nan() || low[i].is_nan() || volume[i].is_nan()))
927 .ok_or(MarketefiError::AllValuesNaN)?;
928
929 let out_mu = unsafe {
930 core::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut MaybeUninit<f64>, out.len())
931 };
932 init_matrix_prefixes(out_mu, cols, &[first]);
933
934 let chosen = match kernel {
935 Kernel::Auto => detect_best_batch_kernel(),
936 k => k,
937 };
938
939 let row = unsafe { core::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut f64, cols) };
940 marketefi_compute_into(
941 high,
942 low,
943 volume,
944 first,
945 match chosen {
946 Kernel::Avx512Batch => Kernel::Avx512,
947 Kernel::Avx2Batch => Kernel::Avx2,
948 Kernel::ScalarBatch => Kernel::Scalar,
949 _ => chosen,
950 },
951 row,
952 );
953
954 Ok(())
955}
956
957#[inline(always)]
958fn marketefi_batch_inner(
959 high: &[f64],
960 low: &[f64],
961 volume: &[f64],
962 kernel: Kernel,
963 parallel: bool,
964) -> Result<MarketefiBatchOutput, MarketefiError> {
965 let cols = high.len();
966
967 let combos = expand_grid(&MarketefiBatchRange::default());
968 let rows = combos.len();
969
970 rows.checked_mul(cols)
971 .ok_or_else(|| MarketefiError::InvalidRange {
972 start: rows.to_string(),
973 end: cols.to_string(),
974 step: "rows*cols overflow".to_string(),
975 })?;
976
977 let mut buf_mu = make_uninit_matrix(rows, cols);
978
979 let first = (0..cols)
980 .find(|&i| !(high[i].is_nan() || low[i].is_nan() || volume[i].is_nan()))
981 .ok_or(MarketefiError::AllValuesNaN)?;
982 init_matrix_prefixes(&mut buf_mu, cols, &[first]);
983
984 let mut guard = core::mem::ManuallyDrop::new(buf_mu);
985 let out_f64: &mut [f64] =
986 unsafe { core::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };
987 marketefi_batch_inner_into(high, low, volume, kernel, parallel, out_f64)?;
988
989 let values = unsafe {
990 Vec::from_raw_parts(
991 guard.as_mut_ptr() as *mut f64,
992 guard.len(),
993 guard.capacity(),
994 )
995 };
996 Ok(MarketefiBatchOutput {
997 values,
998 combos,
999 rows,
1000 cols,
1001 })
1002}
1003
1004#[inline(always)]
1005pub fn expand_grid(_: &MarketefiBatchRange) -> Vec<MarketefiParams> {
1006 vec![MarketefiParams]
1007}
1008
1009#[derive(Debug, Clone, Default)]
1010pub struct MarketefiStream;
1011
1012impl MarketefiStream {
1013 #[inline(always)]
1014 pub fn new() -> Self {
1015 Self
1016 }
1017
1018 #[inline(always)]
1019 pub fn update(&mut self, high: f64, low: f64, volume: f64) -> Option<f64> {
1020 if high.is_nan() || low.is_nan() || volume.is_nan() || volume == 0.0 {
1021 None
1022 } else {
1023 let diff = high - low;
1024 Some(diff / volume)
1025 }
1026 }
1027
1028 #[inline(always)]
1029 pub fn update_fast(&mut self, high: f64, low: f64, volume: f64) -> Option<f64> {
1030 if high.is_nan() || low.is_nan() || volume.is_nan() || volume == 0.0 {
1031 None
1032 } else {
1033 let diff = high - low;
1034 Some(diff * approx_recip_nr2_f64(volume))
1035 }
1036 }
1037
1038 #[inline(always)]
1039 pub fn update_unchecked(&mut self, high: f64, low: f64, volume: f64) -> f64 {
1040 debug_assert!(!high.is_nan() && !low.is_nan() && !volume.is_nan() && volume != 0.0);
1041 (high - low) / volume
1042 }
1043}
1044
1045#[inline(always)]
1046fn approx_recip_nr2_f64(x: f64) -> f64 {
1047 const F32_MIN_NORM: f64 = f32::MIN_POSITIVE as f64;
1048 if x.abs() < F32_MIN_NORM {
1049 return 1.0 / x;
1050 }
1051
1052 let mut y = (1.0f32 / (x as f32)) as f64;
1053
1054 y *= (-x).mul_add(y, 2.0);
1055 y *= (-x).mul_add(y, 2.0);
1056 y
1057}
1058
1059#[cfg(feature = "python")]
1060#[pyfunction(name = "marketefi")]
1061#[pyo3(signature = (high, low, volume, kernel=None))]
1062pub fn marketefi_py<'py>(
1063 py: Python<'py>,
1064 high: numpy::PyReadonlyArray1<'py, f64>,
1065 low: numpy::PyReadonlyArray1<'py, f64>,
1066 volume: numpy::PyReadonlyArray1<'py, f64>,
1067 kernel: Option<&str>,
1068) -> PyResult<Bound<'py, numpy::PyArray1<f64>>> {
1069 use numpy::{IntoPyArray, PyArrayMethods};
1070
1071 let high_slice = high.as_slice()?;
1072 let low_slice = low.as_slice()?;
1073 let volume_slice = volume.as_slice()?;
1074 let kern = validate_kernel(kernel, false)?;
1075
1076 let input = MarketefiInput::from_slices(
1077 high_slice,
1078 low_slice,
1079 volume_slice,
1080 MarketefiParams::default(),
1081 );
1082
1083 let result_vec: Vec<f64> = py
1084 .allow_threads(|| marketefi_with_kernel(&input, kern).map(|o| o.values))
1085 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1086
1087 Ok(result_vec.into_pyarray(py))
1088}
1089
1090#[cfg(feature = "python")]
1091#[pyclass(name = "MarketefiStream")]
1092pub struct MarketefiStreamPy {
1093 stream: MarketefiStream,
1094}
1095
1096#[cfg(feature = "python")]
1097#[pymethods]
1098impl MarketefiStreamPy {
1099 #[new]
1100 fn new() -> PyResult<Self> {
1101 Ok(MarketefiStreamPy {
1102 stream: MarketefiStream::new(),
1103 })
1104 }
1105
1106 fn update(&mut self, high: f64, low: f64, volume: f64) -> Option<f64> {
1107 self.stream.update(high, low, volume)
1108 }
1109}
1110
1111#[cfg(all(feature = "python", feature = "cuda"))]
1112#[pyclass(
1113 module = "ta_indicators.cuda",
1114 name = "MarketefiDeviceArrayF32",
1115 unsendable
1116)]
1117pub struct MarketefiDeviceArrayF32Py {
1118 pub(crate) inner: SharedDeviceArrayF32Py,
1119}
1120
1121#[cfg(all(feature = "python", feature = "cuda"))]
1122#[pymethods]
1123impl MarketefiDeviceArrayF32Py {
1124 #[getter]
1125 fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
1126 self.inner.__cuda_array_interface__(py)
1127 }
1128
1129 fn __dlpack_device__(&self) -> PyResult<(i32, i32)> {
1130 self.inner.__dlpack_device__()
1131 }
1132
1133 #[pyo3(signature=(stream=None, max_version=None, dl_device=None, copy=None))]
1134 fn __dlpack__<'py>(
1135 &mut self,
1136 py: Python<'py>,
1137 stream: Option<PyObject>,
1138 max_version: Option<PyObject>,
1139 dl_device: Option<PyObject>,
1140 copy: Option<PyObject>,
1141 ) -> PyResult<PyObject> {
1142 self.inner
1143 .__dlpack__(py, stream, max_version, dl_device, copy)
1144 }
1145}
1146
1147#[cfg(all(feature = "python", feature = "cuda"))]
1148impl MarketefiDeviceArrayF32Py {
1149 fn new_from_rust(inner: DeviceArrayF32, ctx_guard: Arc<Context>, device_id: u32) -> Self {
1150 let shared = SharedDeviceArrayF32Py {
1151 inner,
1152 _ctx: Some(ctx_guard),
1153 device_id: Some(device_id),
1154 };
1155 Self { inner: shared }
1156 }
1157}
1158
1159#[cfg(feature = "python")]
1160#[pyfunction(name = "marketefi_batch")]
1161#[pyo3(signature = (high, low, volume, kernel=None))]
1162pub fn marketefi_batch_py<'py>(
1163 py: Python<'py>,
1164 high: numpy::PyReadonlyArray1<'py, f64>,
1165 low: numpy::PyReadonlyArray1<'py, f64>,
1166 volume: numpy::PyReadonlyArray1<'py, f64>,
1167 kernel: Option<&str>,
1168) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
1169 use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
1170 use pyo3::types::PyDict;
1171
1172 let h = high.as_slice()?;
1173 let l = low.as_slice()?;
1174 let v = volume.as_slice()?;
1175 let k = validate_kernel(kernel, true)?;
1176
1177 let rows = 1usize;
1178 let cols = h.len();
1179 let out_arr = unsafe { PyArray1::<f64>::new(py, [rows * cols], false) };
1180 let out_slice = unsafe { out_arr.as_slice_mut()? };
1181
1182 py.allow_threads(|| marketefi_batch_inner_into(h, l, v, k, true, out_slice))
1183 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1184
1185 let dict = PyDict::new(py);
1186 dict.set_item("values", out_arr.reshape((rows, cols))?)?;
1187 Ok(dict)
1188}
1189
1190#[cfg(all(feature = "python", feature = "cuda"))]
1191#[pyfunction(name = "marketefi_cuda_batch_dev")]
1192#[pyo3(signature = (high_f32, low_f32, volume_f32, device_id=0))]
1193pub fn marketefi_cuda_batch_dev_py(
1194 py: Python<'_>,
1195 high_f32: numpy::PyReadonlyArray1<'_, f32>,
1196 low_f32: numpy::PyReadonlyArray1<'_, f32>,
1197 volume_f32: numpy::PyReadonlyArray1<'_, f32>,
1198 device_id: usize,
1199) -> PyResult<MarketefiDeviceArrayF32Py> {
1200 use numpy::PyArrayMethods;
1201 if !cuda_available() {
1202 return Err(PyValueError::new_err("CUDA not available"));
1203 }
1204 let h = high_f32.as_slice()?;
1205 let l = low_f32.as_slice()?;
1206 let v = volume_f32.as_slice()?;
1207 if h.len() != l.len() || l.len() != v.len() {
1208 return Err(PyValueError::new_err(
1209 "high, low, volume must have same length",
1210 ));
1211 }
1212 let (inner, ctx_guard, dev_id) = py.allow_threads(|| -> PyResult<_> {
1213 let cuda =
1214 CudaMarketefi::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1215 let ctx = cuda.context_arc();
1216 let dev = cuda.device_id();
1217 let inner = cuda
1218 .marketefi_batch_dev(h, l, v)
1219 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1220 Ok((inner, ctx, dev))
1221 })?;
1222 Ok(MarketefiDeviceArrayF32Py::new_from_rust(
1223 inner, ctx_guard, dev_id,
1224 ))
1225}
1226
1227#[cfg(all(feature = "python", feature = "cuda"))]
1228#[pyfunction(name = "marketefi_cuda_many_series_one_param_dev")]
1229#[pyo3(signature = (high_tm_f32, low_tm_f32, volume_tm_f32, device_id=0))]
1230pub fn marketefi_cuda_many_series_one_param_dev_py(
1231 py: Python<'_>,
1232 high_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
1233 low_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
1234 volume_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
1235 device_id: usize,
1236) -> PyResult<MarketefiDeviceArrayF32Py> {
1237 use numpy::{PyArrayMethods, PyUntypedArrayMethods};
1238 if !cuda_available() {
1239 return Err(PyValueError::new_err("CUDA not available"));
1240 }
1241 let h = high_tm_f32.as_slice()?;
1242 let l = low_tm_f32.as_slice()?;
1243 let v = volume_tm_f32.as_slice()?;
1244 let shp_h = high_tm_f32.shape();
1245 let shp_l = low_tm_f32.shape();
1246 let shp_v = volume_tm_f32.shape();
1247 if shp_h.len() != 2 || shp_h != shp_l || shp_h != shp_v {
1248 return Err(PyValueError::new_err(
1249 "high_tm, low_tm, volume_tm must have same 2D shape",
1250 ));
1251 }
1252 let rows = shp_h[0];
1253 let cols = shp_h[1];
1254 let (inner, ctx_guard, dev_id) = py.allow_threads(|| -> PyResult<_> {
1255 let cuda =
1256 CudaMarketefi::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1257 let ctx = cuda.context_arc();
1258 let dev = cuda.device_id();
1259 let inner = cuda
1260 .marketefi_many_series_one_param_time_major_dev(h, l, v, cols, rows)
1261 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1262 Ok((inner, ctx, dev))
1263 })?;
1264 Ok(MarketefiDeviceArrayF32Py::new_from_rust(
1265 inner, ctx_guard, dev_id,
1266 ))
1267}
1268
1269#[cfg(test)]
1270mod tests {
1271 use super::*;
1272 use crate::skip_if_unsupported;
1273 use crate::utilities::data_loader::read_candles_from_csv;
1274 use paste::paste;
1275 #[cfg(feature = "proptest")]
1276 use proptest::prelude::*;
1277
1278 #[test]
1279 fn test_marketefi_into_matches_api() -> Result<(), Box<dyn Error>> {
1280 let len = 256;
1281 let mut high = vec![f64::NAN; len];
1282 let mut low = vec![f64::NAN; len];
1283 let mut volume = vec![f64::NAN; len];
1284
1285 for i in 10..len {
1286 let base = 100.0 + (i as f64) * 0.1;
1287 let spread = 0.5 + (i % 7) as f64 * 0.05;
1288 high[i] = base + spread;
1289 low[i] = base - spread * 0.3;
1290 volume[i] = if i % 53 == 0 {
1291 0.0
1292 } else {
1293 1000.0 + (i as f64)
1294 };
1295 }
1296
1297 let input = MarketefiInput::from_slices(&high, &low, &volume, MarketefiParams::default());
1298
1299 let baseline = marketefi_with_kernel(&input, Kernel::Auto)?;
1300
1301 let mut out = vec![0.0; len];
1302 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1303 {
1304 marketefi_into(&input, &mut out)?;
1305 }
1306 #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1307 {
1308 marketefi_into_slice(&mut out, &input, Kernel::Auto)?;
1309 }
1310
1311 assert_eq!(out.len(), baseline.values.len());
1312 for (a, b) in out.iter().zip(baseline.values.iter()) {
1313 let eq = (a.is_nan() && b.is_nan()) || (a == b);
1314 assert!(eq, "mismatch: got {a:?}, expected {b:?}");
1315 }
1316 Ok(())
1317 }
1318
1319 fn check_marketefi_accuracy(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1320 skip_if_unsupported!(kernel, test);
1321 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1322 let candles = read_candles_from_csv(file_path)?;
1323 let input = MarketefiInput::with_default_candles(&candles);
1324 let res = marketefi_with_kernel(&input, kernel)?;
1325 assert_eq!(res.values.len(), candles.close.len());
1326 let expected_last_five = [
1327 2.8460112192104607,
1328 3.020938522420525,
1329 3.0474861329079292,
1330 3.691017115591989,
1331 2.247810963176202,
1332 ];
1333 let start = res.values.len() - 5;
1334 for (i, &v) in res.values[start..].iter().enumerate() {
1335 let exp = expected_last_five[i];
1336 assert!(
1337 (v - exp).abs() < 1e-6,
1338 "[{}] marketefi mismatch at {}: got {}, exp {}",
1339 test,
1340 start + i,
1341 v,
1342 exp
1343 );
1344 }
1345 Ok(())
1346 }
1347
1348 fn check_marketefi_nan_handling(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1349 skip_if_unsupported!(kernel, test);
1350 let high = [f64::NAN, 2.0, 3.0];
1351 let low = [f64::NAN, 1.0, 2.0];
1352 let vol = [f64::NAN, 1.0, 1.0];
1353 let input = MarketefiInput::from_slices(&high, &low, &vol, MarketefiParams::default());
1354 let res = marketefi_with_kernel(&input, kernel)?;
1355 assert!(res.values[0].is_nan());
1356 assert_eq!(res.values[1], 1.0 / 1.0);
1357 Ok(())
1358 }
1359
1360 fn check_marketefi_empty_data(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1361 skip_if_unsupported!(kernel, test);
1362 let input = MarketefiInput::from_slices(&[], &[], &[], MarketefiParams::default());
1363 let res = marketefi_with_kernel(&input, kernel);
1364 assert!(res.is_err());
1365 Ok(())
1366 }
1367
1368 fn check_marketefi_streaming(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1369 skip_if_unsupported!(kernel, test);
1370 let high = [3.0, 4.0, 5.0];
1371 let low = [2.0, 3.0, 3.0];
1372 let vol = [1.0, 2.0, 2.0];
1373 let mut stream = MarketefiStream::new();
1374 let mut vals = Vec::new();
1375 for i in 0..high.len() {
1376 vals.push(stream.update(high[i], low[i], vol[i]).unwrap_or(f64::NAN));
1377 }
1378 let input = MarketefiInput::from_slices(&high, &low, &vol, MarketefiParams::default());
1379 let res = marketefi_with_kernel(&input, kernel)?;
1380 for (a, b) in vals.iter().zip(res.values.iter()) {
1381 if a.is_nan() && b.is_nan() {
1382 continue;
1383 }
1384 assert!((a - b).abs() < 1e-8);
1385 }
1386 Ok(())
1387 }
1388
1389 #[cfg(feature = "proptest")]
1390 #[allow(clippy::float_cmp)]
1391 fn check_marketefi_property(
1392 test_name: &str,
1393 kernel: Kernel,
1394 ) -> Result<(), Box<dyn std::error::Error>> {
1395 skip_if_unsupported!(kernel, test_name);
1396
1397 let strat = (50usize..400, 0usize..7, any::<u64>()).prop_map(|(len, scenario, seed)| {
1398 let mut rng_state = seed.wrapping_mul(1664525).wrapping_add(1013904223);
1399 let mut next_f64 = || {
1400 rng_state = rng_state.wrapping_mul(1664525).wrapping_add(1013904223);
1401 (rng_state as f64) / (u64::MAX as f64)
1402 };
1403
1404 let mut high = Vec::with_capacity(len);
1405 let mut low = Vec::with_capacity(len);
1406 let mut volume = Vec::with_capacity(len);
1407
1408 match scenario {
1409 0 => {
1410 for _ in 0..len {
1411 let base = 50.0 + next_f64() * 450.0;
1412 let spread = 0.1 + next_f64() * 10.0;
1413 high.push(base + spread);
1414 low.push(base);
1415 volume.push(100.0 + next_f64() * 10000.0);
1416 }
1417 }
1418 1 => {
1419 let price = 100.0 + next_f64() * 200.0;
1420 for _ in 0..len {
1421 high.push(price);
1422 low.push(price);
1423 volume.push(1000.0 + next_f64() * 1000.0);
1424 }
1425 }
1426 2 => {
1427 let mut base = 100.0;
1428 for i in 0..len {
1429 let trend = 0.5 * (i as f64 / len as f64);
1430 base += trend;
1431 let volatility = 0.5 + (i as f64 / len as f64) * 5.0;
1432 high.push(base + volatility);
1433 low.push(base - volatility * 0.5);
1434 volume.push(500.0 + next_f64() * 5000.0 + i as f64 * 10.0);
1435 }
1436 }
1437 3 => {
1438 for _ in 0..len {
1439 let base = 50.0 + next_f64() * 100.0;
1440 let spread = 0.1 + next_f64() * 5.0;
1441 high.push(base + spread);
1442 low.push(base);
1443 volume.push(0.001 + next_f64() * 1.0);
1444 }
1445 }
1446 4 => {
1447 for _ in 0..len {
1448 let base = 1000.0 + next_f64() * 9000.0;
1449 let spread = 10.0 + next_f64() * 100.0;
1450 high.push(base + spread);
1451 low.push(base);
1452 volume.push(1e6 + next_f64() * 1e7);
1453 }
1454 }
1455 5 => {
1456 for i in 0..len {
1457 let base = 100.0 + next_f64() * 100.0;
1458 let spread = 1.0 + next_f64() * 5.0;
1459 high.push(base + spread);
1460 low.push(base);
1461
1462 if i % 5 == 0 {
1463 volume.push(0.0);
1464 } else {
1465 volume.push(100.0 + next_f64() * 1000.0);
1466 }
1467 }
1468 }
1469 _ => {
1470 for _ in 0..len {
1471 let base = 100.0 + next_f64() * 200.0;
1472 let spread = 1.0 + next_f64() * 10.0;
1473
1474 if next_f64() < 0.3 {
1475 high.push(base - spread);
1476 low.push(base);
1477 } else {
1478 high.push(base + spread);
1479 low.push(base);
1480 }
1481 volume.push(500.0 + next_f64() * 5000.0);
1482 }
1483 }
1484 }
1485
1486 (high, low, volume, scenario)
1487 });
1488
1489 proptest::test_runner::TestRunner::default().run(
1490 &strat,
1491 |(high, low, volume, scenario)| {
1492 let input =
1493 MarketefiInput::from_slices(&high, &low, &volume, MarketefiParams::default());
1494
1495 let output = marketefi_with_kernel(&input, kernel)?;
1496
1497 let ref_output = marketefi_with_kernel(&input, Kernel::Scalar)?;
1498
1499 prop_assert_eq!(
1500 output.values.len(),
1501 high.len(),
1502 "Output length mismatch: got {}, expected {}",
1503 output.values.len(),
1504 high.len()
1505 );
1506
1507 let first_valid = (0..high.len())
1508 .find(|&i| !high[i].is_nan() && !low[i].is_nan() && !volume[i].is_nan());
1509
1510 if let Some(first) = first_valid {
1511 for i in 0..first {
1512 prop_assert!(
1513 output.values[i].is_nan(),
1514 "Expected NaN before first valid index {} but got {} at index {}",
1515 first,
1516 output.values[i],
1517 i
1518 );
1519 }
1520 }
1521
1522 for i in 0..high.len() {
1523 let expected = if high[i].is_nan()
1524 || low[i].is_nan()
1525 || volume[i].is_nan()
1526 || volume[i] == 0.0
1527 {
1528 f64::NAN
1529 } else {
1530 (high[i] - low[i]) / volume[i]
1531 };
1532
1533 let actual = output.values[i];
1534
1535 if expected.is_nan() {
1536 prop_assert!(
1537 actual.is_nan(),
1538 "Expected NaN at index {} but got {}",
1539 i,
1540 actual
1541 );
1542 } else {
1543 prop_assert!(
1544 (actual - expected).abs() < 1e-10,
1545 "Calculation mismatch at index {}: expected {}, got {}",
1546 i,
1547 expected,
1548 actual
1549 );
1550 }
1551 }
1552
1553 for i in 0..output.values.len() {
1554 let out_val = output.values[i];
1555 let ref_val = ref_output.values[i];
1556
1557 if out_val.is_nan() && ref_val.is_nan() {
1558 continue;
1559 }
1560
1561 prop_assert!(
1562 (out_val - ref_val).abs() < 1e-10,
1563 "Kernel mismatch at index {}: kernel={}, reference={}",
1564 i,
1565 out_val,
1566 ref_val
1567 );
1568 }
1569
1570 if scenario == 1 {
1571 for i in 0..output.values.len() {
1572 if !high[i].is_nan()
1573 && !low[i].is_nan()
1574 && !volume[i].is_nan()
1575 && volume[i] != 0.0
1576 {
1577 prop_assert!(
1578 (output.values[i] - 0.0).abs() < 1e-10,
1579 "When high=low, expected 0.0 but got {} at index {}",
1580 output.values[i],
1581 i
1582 );
1583 }
1584 }
1585 }
1586
1587 if scenario == 5 {
1588 for i in 0..output.values.len() {
1589 if volume[i] == 0.0 {
1590 prop_assert!(
1591 output.values[i].is_nan(),
1592 "Expected NaN for zero volume at index {} but got {}",
1593 i,
1594 output.values[i]
1595 );
1596 }
1597 }
1598 }
1599
1600 if scenario == 6 {
1601 for i in 0..output.values.len() {
1602 if !high[i].is_nan()
1603 && !low[i].is_nan()
1604 && !volume[i].is_nan()
1605 && volume[i] != 0.0
1606 {
1607 if high[i] < low[i] {
1608 prop_assert!(output.values[i] < 0.0,
1609 "Expected negative value when high < low at index {}, but got {}", i, output.values[i]);
1610 }
1611 }
1612 }
1613 }
1614
1615 for (i, &val) in output.values.iter().enumerate() {
1616 if val.is_nan() {
1617 continue;
1618 }
1619
1620 let bits = val.to_bits();
1621
1622 prop_assert!(
1623 bits != 0x11111111_11111111,
1624 "Found alloc_with_nan_prefix poison value at index {}",
1625 i
1626 );
1627 prop_assert!(
1628 bits != 0x22222222_22222222,
1629 "Found init_matrix_prefixes poison value at index {}",
1630 i
1631 );
1632 prop_assert!(
1633 bits != 0x33333333_33333333,
1634 "Found make_uninit_matrix poison value at index {}",
1635 i
1636 );
1637 }
1638
1639 Ok(())
1640 },
1641 )?;
1642
1643 Ok(())
1644 }
1645
1646 macro_rules! generate_all_marketefi_tests {
1647 ($($test_fn:ident),*) => {
1648 paste! {
1649 $(
1650 #[test]
1651 fn [<$test_fn _scalar_f64>]() {
1652 let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
1653 }
1654 )*
1655 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1656 $(
1657 #[test]
1658 fn [<$test_fn _avx2_f64>]() {
1659 let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
1660 }
1661 #[test]
1662 fn [<$test_fn _avx512_f64>]() {
1663 let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
1664 }
1665 )*
1666 }
1667 }
1668 }
1669 generate_all_marketefi_tests!(
1670 check_marketefi_accuracy,
1671 check_marketefi_nan_handling,
1672 check_marketefi_empty_data,
1673 check_marketefi_streaming,
1674 check_marketefi_no_poison
1675 );
1676
1677 #[cfg(feature = "proptest")]
1678 generate_all_marketefi_tests!(check_marketefi_property);
1679
1680 #[test]
1681 fn test_marketefi_into_slice() -> Result<(), Box<dyn Error>> {
1682 let high = vec![100.0, 105.0, 110.0, 108.0, 112.0];
1683 let low = vec![95.0, 98.0, 102.0, 104.0, 106.0];
1684 let volume = vec![1000.0, 1500.0, 2000.0, 1200.0, 1800.0];
1685
1686 let input = MarketefiInput::from_slices(&high, &low, &volume, MarketefiParams::default());
1687
1688 let mut dst = vec![0.0; high.len()];
1689 marketefi_into_slice(&mut dst, &input, Kernel::Scalar)?;
1690
1691 let output = marketefi(&input)?;
1692
1693 assert_eq!(dst.len(), output.values.len());
1694 for i in 0..dst.len() {
1695 if dst[i].is_nan() && output.values[i].is_nan() {
1696 continue;
1697 }
1698 assert!(
1699 (dst[i] - output.values[i]).abs() < 1e-10,
1700 "Mismatch at index {}: into_slice={}, regular={}",
1701 i,
1702 dst[i],
1703 output.values[i]
1704 );
1705 }
1706
1707 for i in 0..high.len() {
1708 let expected = (high[i] - low[i]) / volume[i];
1709 assert!(
1710 (dst[i] - expected).abs() < 1e-10,
1711 "Incorrect calculation at index {}: got={}, expected={}",
1712 i,
1713 dst[i],
1714 expected
1715 );
1716 }
1717
1718 Ok(())
1719 }
1720
1721 #[cfg(debug_assertions)]
1722 fn check_marketefi_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1723 skip_if_unsupported!(kernel, test_name);
1724
1725 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1726 let candles = read_candles_from_csv(file_path)?;
1727
1728 let params = MarketefiParams::default();
1729 let input = MarketefiInput::from_candles(&candles, "high", "low", "volume", params.clone());
1730 let output = marketefi_with_kernel(&input, kernel)?;
1731
1732 for (i, &val) in output.values.iter().enumerate() {
1733 if val.is_nan() {
1734 continue;
1735 }
1736
1737 let bits = val.to_bits();
1738
1739 if bits == 0x11111111_11111111 {
1740 panic!(
1741 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {}",
1742 test_name, val, bits, i
1743 );
1744 }
1745
1746 if bits == 0x22222222_22222222 {
1747 panic!(
1748 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {}",
1749 test_name, val, bits, i
1750 );
1751 }
1752
1753 if bits == 0x33333333_33333333 {
1754 panic!(
1755 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {}",
1756 test_name, val, bits, i
1757 );
1758 }
1759 }
1760
1761 Ok(())
1762 }
1763
1764 #[cfg(not(debug_assertions))]
1765 fn check_marketefi_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
1766 Ok(())
1767 }
1768
1769 fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1770 skip_if_unsupported!(kernel, test);
1771 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1772 let candles = read_candles_from_csv(file_path)?;
1773 let out = MarketefiBatchBuilder::new().kernel(kernel).apply_slices(
1774 source_type(&candles, "high"),
1775 source_type(&candles, "low"),
1776 source_type(&candles, "volume"),
1777 )?;
1778 let expected_last_five = [
1779 2.8460112192104607,
1780 3.020938522420525,
1781 3.0474861329079292,
1782 3.691017115591989,
1783 2.247810963176202,
1784 ];
1785 let row = &out.values;
1786 let start = row.len() - 5;
1787 for (i, &v) in row[start..].iter().enumerate() {
1788 let exp = expected_last_five[i];
1789 assert!(
1790 (v - exp).abs() < 1e-8,
1791 "[{test}] batch row mismatch at {i}: {v} vs {exp}"
1792 );
1793 }
1794 Ok(())
1795 }
1796
1797 #[cfg(debug_assertions)]
1798 fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1799 skip_if_unsupported!(kernel, test);
1800
1801 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1802 let c = read_candles_from_csv(file)?;
1803
1804 let output = MarketefiBatchBuilder::new().kernel(kernel).apply_slices(
1805 source_type(&c, "high"),
1806 source_type(&c, "low"),
1807 source_type(&c, "volume"),
1808 )?;
1809
1810 for (idx, &val) in output.values.iter().enumerate() {
1811 if val.is_nan() {
1812 continue;
1813 }
1814
1815 let bits = val.to_bits();
1816 let row = idx / output.cols;
1817 let col = idx % output.cols;
1818
1819 if bits == 0x11111111_11111111 {
1820 panic!(
1821 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
1822 at row {} col {} (flat index {})",
1823 test, val, bits, row, col, idx
1824 );
1825 }
1826
1827 if bits == 0x22222222_22222222 {
1828 panic!(
1829 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) \
1830 at row {} col {} (flat index {})",
1831 test, val, bits, row, col, idx
1832 );
1833 }
1834
1835 if bits == 0x33333333_33333333 {
1836 panic!(
1837 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) \
1838 at row {} col {} (flat index {})",
1839 test, val, bits, row, col, idx
1840 );
1841 }
1842 }
1843
1844 Ok(())
1845 }
1846
1847 #[cfg(not(debug_assertions))]
1848 fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
1849 Ok(())
1850 }
1851
1852 macro_rules! gen_batch_tests {
1853 ($fn_name:ident) => {
1854 paste! {
1855 #[test] fn [<$fn_name _scalar>]() {
1856 let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
1857 }
1858 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1859 #[test] fn [<$fn_name _avx2>]() {
1860 let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
1861 }
1862 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1863 #[test] fn [<$fn_name _avx512>]() {
1864 let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
1865 }
1866 #[test] fn [<$fn_name _auto_detect>]() {
1867 let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
1868 }
1869 }
1870 };
1871 }
1872 gen_batch_tests!(check_batch_default_row);
1873 gen_batch_tests!(check_batch_no_poison);
1874}
1875
1876#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1877#[wasm_bindgen]
1878pub fn marketefi_js(high: &[f64], low: &[f64], volume: &[f64]) -> Result<Vec<f64>, JsValue> {
1879 let input = MarketefiInput::from_slices(high, low, volume, MarketefiParams::default());
1880
1881 let mut output = vec![0.0; high.len()];
1882
1883 marketefi_into_slice(&mut output, &input, Kernel::Auto)
1884 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1885
1886 Ok(output)
1887}
1888
1889#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1890#[wasm_bindgen]
1891pub fn marketefi_alloc(len: usize) -> *mut f64 {
1892 let mut vec = Vec::<f64>::with_capacity(len);
1893 let ptr = vec.as_mut_ptr();
1894 std::mem::forget(vec);
1895 ptr
1896}
1897
1898#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1899#[wasm_bindgen]
1900pub fn marketefi_free(ptr: *mut f64, len: usize) {
1901 if !ptr.is_null() {
1902 unsafe {
1903 let _ = Vec::from_raw_parts(ptr, len, len);
1904 }
1905 }
1906}
1907
1908#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1909#[wasm_bindgen]
1910pub fn marketefi_into(
1911 high_ptr: *const f64,
1912 low_ptr: *const f64,
1913 volume_ptr: *const f64,
1914 out_ptr: *mut f64,
1915 len: usize,
1916) -> Result<(), JsValue> {
1917 if high_ptr.is_null() || low_ptr.is_null() || volume_ptr.is_null() || out_ptr.is_null() {
1918 return Err(JsValue::from_str("null pointer passed to marketefi_into"));
1919 }
1920
1921 unsafe {
1922 let high = std::slice::from_raw_parts(high_ptr, len);
1923 let low = std::slice::from_raw_parts(low_ptr, len);
1924 let volume = std::slice::from_raw_parts(volume_ptr, len);
1925
1926 let input = MarketefiInput::from_slices(high, low, volume, MarketefiParams::default());
1927
1928 if high_ptr == out_ptr || low_ptr == out_ptr || volume_ptr == out_ptr {
1929 let mut temp = vec![0.0; len];
1930 marketefi_into_slice(&mut temp, &input, Kernel::Auto)
1931 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1932 let out = std::slice::from_raw_parts_mut(out_ptr, len);
1933 out.copy_from_slice(&temp);
1934 } else {
1935 let out = std::slice::from_raw_parts_mut(out_ptr, len);
1936 marketefi_into_slice(out, &input, Kernel::Auto)
1937 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1938 }
1939
1940 Ok(())
1941 }
1942}
1943
1944#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1945#[derive(Serialize, Deserialize)]
1946pub struct MarketefiBatchConfig {}
1947
1948#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1949#[derive(Serialize, Deserialize)]
1950pub struct MarketefiBatchJsOutput {
1951 pub values: Vec<f64>,
1952 pub combos: Vec<MarketefiParams>,
1953 pub rows: usize,
1954 pub cols: usize,
1955}
1956
1957#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1958#[wasm_bindgen(js_name = marketefi_batch)]
1959pub fn marketefi_batch_js(
1960 high: &[f64],
1961 low: &[f64],
1962 volume: &[f64],
1963 _config: JsValue,
1964) -> Result<JsValue, JsValue> {
1965 let result = marketefi_batch_with_kernel(high, low, volume, Kernel::Auto)
1966 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1967
1968 let output = MarketefiBatchJsOutput {
1969 values: result.values,
1970 combos: result.combos,
1971 rows: result.rows,
1972 cols: result.cols,
1973 };
1974
1975 serde_wasm_bindgen::to_value(&output)
1976 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1977}
1978
1979#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1980#[wasm_bindgen]
1981pub fn marketefi_batch_into(
1982 high_ptr: *const f64,
1983 low_ptr: *const f64,
1984 volume_ptr: *const f64,
1985 out_ptr: *mut f64,
1986 len: usize,
1987) -> Result<usize, JsValue> {
1988 if high_ptr.is_null() || low_ptr.is_null() || volume_ptr.is_null() || out_ptr.is_null() {
1989 return Err(JsValue::from_str(
1990 "null pointer passed to marketefi_batch_into",
1991 ));
1992 }
1993 unsafe {
1994 let h = core::slice::from_raw_parts(high_ptr, len);
1995 let l = core::slice::from_raw_parts(low_ptr, len);
1996 let v = core::slice::from_raw_parts(volume_ptr, len);
1997 let out = core::slice::from_raw_parts_mut(out_ptr, len);
1998
1999 marketefi_batch_inner_into(h, l, v, Kernel::Auto, false, out)
2000 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2001 Ok(1)
2002 }
2003}