Skip to main content

vector_ta/indicators/
marketefi.rs

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}