Skip to main content

vector_ta/indicators/moving_averages/
frama.rs

1#[cfg(all(feature = "python", feature = "cuda"))]
2use crate::cuda::moving_averages::CudaFrama;
3use crate::utilities::data_loader::{source_type, Candles};
4use crate::utilities::enums::Kernel;
5use crate::utilities::helpers::{
6    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
7    make_uninit_matrix,
8};
9#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
10use core::arch::x86_64::*;
11#[cfg(not(target_arch = "wasm32"))]
12use rayon::prelude::*;
13use std::collections::VecDeque;
14use std::convert::AsRef;
15use std::error::Error;
16#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
17use std::hint::unlikely;
18use std::mem::{swap, MaybeUninit};
19use thiserror::Error;
20
21impl<'a> AsRef<[f64]> for FramaInput<'a> {
22    #[inline(always)]
23    fn as_ref(&self) -> &[f64] {
24        match &self.data {
25            FramaData::Candles { candles } => candles.select_candle_field("close").unwrap(),
26            FramaData::Slices { close, .. } => close,
27        }
28    }
29}
30
31#[inline(always)]
32unsafe fn seed_sma(close: &[f64], first: usize, win: usize, out: &mut [f64]) {
33    let mut sum = 0.0;
34    for k in 0..win {
35        sum += *close.get_unchecked(first + k);
36    }
37    *out.get_unchecked_mut(first + win - 1) = sum / win as f64;
38}
39
40#[derive(Debug, Clone)]
41pub enum FramaData<'a> {
42    Candles {
43        candles: &'a Candles,
44    },
45    Slices {
46        high: &'a [f64],
47        low: &'a [f64],
48        close: &'a [f64],
49    },
50}
51
52#[derive(Debug, Clone)]
53pub struct FramaOutput {
54    pub values: Vec<f64>,
55}
56
57#[derive(Debug, Clone)]
58#[cfg_attr(
59    all(target_arch = "wasm32", feature = "wasm"),
60    derive(Serialize, Deserialize)
61)]
62pub struct FramaParams {
63    pub window: Option<usize>,
64    pub sc: Option<usize>,
65    pub fc: Option<usize>,
66}
67
68impl Default for FramaParams {
69    fn default() -> Self {
70        Self {
71            window: Some(10),
72            sc: Some(300),
73            fc: Some(1),
74        }
75    }
76}
77
78#[derive(Debug, Clone)]
79pub struct FramaInput<'a> {
80    pub data: FramaData<'a>,
81    pub params: FramaParams,
82}
83
84impl<'a> FramaInput<'a> {
85    #[inline]
86    pub fn from_candles(candles: &'a Candles, params: FramaParams) -> Self {
87        Self {
88            data: FramaData::Candles { candles },
89            params,
90        }
91    }
92    #[inline]
93    pub fn from_slices(
94        high: &'a [f64],
95        low: &'a [f64],
96        close: &'a [f64],
97        params: FramaParams,
98    ) -> Self {
99        Self {
100            data: FramaData::Slices { high, low, close },
101            params,
102        }
103    }
104    #[inline]
105    pub fn with_default_candles(candles: &'a Candles) -> Self {
106        Self::from_candles(candles, FramaParams::default())
107    }
108    #[inline]
109    pub fn get_window(&self) -> usize {
110        self.params.window.unwrap_or(10)
111    }
112    #[inline]
113    pub fn get_sc(&self) -> usize {
114        self.params.sc.unwrap_or(300)
115    }
116    #[inline]
117    pub fn get_fc(&self) -> usize {
118        self.params.fc.unwrap_or(1)
119    }
120
121    #[inline]
122    pub fn slices(&self) -> (&'a [f64], &'a [f64], &'a [f64]) {
123        match &self.data {
124            FramaData::Candles { candles } => (
125                candles.select_candle_field("high").unwrap(),
126                candles.select_candle_field("low").unwrap(),
127                candles.select_candle_field("close").unwrap(),
128            ),
129            FramaData::Slices { high, low, close } => (*high, *low, *close),
130        }
131    }
132}
133
134#[derive(Copy, Clone, Debug)]
135pub struct FramaBuilder {
136    window: Option<usize>,
137    sc: Option<usize>,
138    fc: Option<usize>,
139    kernel: Kernel,
140}
141
142impl Default for FramaBuilder {
143    fn default() -> Self {
144        Self {
145            window: None,
146            sc: None,
147            fc: None,
148            kernel: Kernel::Auto,
149        }
150    }
151}
152impl FramaBuilder {
153    #[inline(always)]
154    pub fn new() -> Self {
155        Self::default()
156    }
157    #[inline(always)]
158    pub fn window(mut self, n: usize) -> Self {
159        self.window = Some(n);
160        self
161    }
162    #[inline(always)]
163    pub fn sc(mut self, x: usize) -> Self {
164        self.sc = Some(x);
165        self
166    }
167    #[inline(always)]
168    pub fn fc(mut self, x: usize) -> Self {
169        self.fc = Some(x);
170        self
171    }
172    #[inline(always)]
173    pub fn kernel(mut self, k: Kernel) -> Self {
174        self.kernel = k;
175        self
176    }
177    #[inline(always)]
178    pub fn apply(self, c: &Candles) -> Result<FramaOutput, FramaError> {
179        let p = FramaParams {
180            window: self.window,
181            sc: self.sc,
182            fc: self.fc,
183        };
184        let i = FramaInput::from_candles(c, p);
185        frama_with_kernel(&i, self.kernel)
186    }
187    #[inline(always)]
188    pub fn apply_slices(
189        self,
190        high: &[f64],
191        low: &[f64],
192        close: &[f64],
193    ) -> Result<FramaOutput, FramaError> {
194        let p = FramaParams {
195            window: self.window,
196            sc: self.sc,
197            fc: self.fc,
198        };
199        let i = FramaInput::from_slices(high, low, close, p);
200        frama_with_kernel(&i, self.kernel)
201    }
202    #[inline(always)]
203    pub fn into_stream(self) -> Result<FramaStream, FramaError> {
204        let p = FramaParams {
205            window: self.window,
206            sc: self.sc,
207            fc: self.fc,
208        };
209        FramaStream::try_new(p)
210    }
211}
212
213#[derive(Debug, Error)]
214pub enum FramaError {
215    #[error("frama: Input data slice is empty.")]
216    EmptyInputData,
217
218    #[error("frama: Mismatched slice lengths: high={high}, low={low}, close={close}")]
219    MismatchedInputLength {
220        high: usize,
221        low: usize,
222        close: usize,
223    },
224    #[error("frama: All values are NaN.")]
225    AllValuesNaN,
226    #[error("frama: Invalid window: window = {window}, data length = {data_len}")]
227    InvalidWindow { window: usize, data_len: usize },
228    #[error("frama: Not enough valid data: needed = {needed}, valid = {valid}")]
229    NotEnoughValidData { needed: usize, valid: usize },
230
231    #[error("frama: Output slice length mismatch: expected = {expected}, got = {got}")]
232    OutputLengthMismatch { expected: usize, got: usize },
233
234    #[error("frama: Invalid range: start={start}, end={end}, step={step}")]
235    InvalidRange {
236        start: usize,
237        end: usize,
238        step: usize,
239    },
240
241    #[error("frama: Invalid kernel for batch API: {0:?}")]
242    InvalidKernelForBatch(Kernel),
243
244    #[error("frama: Invalid smoothing constants: sc={sc}, fc={fc}")]
245    InvalidSmoothing { sc: usize, fc: usize },
246
247    #[error("frama: arithmetic overflow while computing {context}")]
248    ArithmeticOverflow { context: &'static str },
249}
250
251#[inline]
252pub fn frama(input: &FramaInput) -> Result<FramaOutput, FramaError> {
253    frama_with_kernel(input, Kernel::Auto)
254}
255
256#[inline(always)]
257fn frama_prepare<'a>(
258    input: &'a FramaInput,
259    kernel: Kernel,
260) -> Result<
261    (
262        (&'a [f64], &'a [f64], &'a [f64]),
263        usize,
264        usize,
265        usize,
266        usize,
267        usize,
268        usize,
269        Kernel,
270    ),
271    FramaError,
272> {
273    let (high, low, close) = input.slices();
274    let len = high.len();
275    if len == 0 {
276        return Err(FramaError::EmptyInputData);
277    }
278    if low.len() != len || close.len() != len {
279        return Err(FramaError::MismatchedInputLength {
280            high: len,
281            low: low.len(),
282            close: close.len(),
283        });
284    }
285    let window = input.get_window();
286    let sc = input.get_sc();
287    let fc = input.get_fc();
288    if sc == 0 || fc == 0 {
289        return Err(FramaError::InvalidSmoothing { sc, fc });
290    }
291    let first = (0..len)
292        .find(|&i| !high[i].is_nan() && !low[i].is_nan() && !close[i].is_nan())
293        .ok_or(FramaError::AllValuesNaN)?;
294    if window == 0 || window > len {
295        return Err(FramaError::InvalidWindow {
296            window,
297            data_len: len,
298        });
299    }
300
301    let mut win = window;
302    if win & 1 == 1 {
303        win += 1;
304    }
305
306    if (len - first) < win {
307        return Err(FramaError::NotEnoughValidData {
308            needed: win,
309            valid: len - first,
310        });
311    }
312
313    let chosen = match kernel {
314        Kernel::Auto => match detect_best_kernel() {
315            Kernel::Avx512 => Kernel::Avx2,
316            k => k,
317        },
318        other => other,
319    };
320
321    let warm = first + win - 1;
322
323    Ok(((high, low, close), window, sc, fc, first, len, warm, chosen))
324}
325
326#[inline(always)]
327fn frama_compute_into(
328    high: &[f64],
329    low: &[f64],
330    close: &[f64],
331    window: usize,
332    sc: usize,
333    fc: usize,
334    first: usize,
335    len: usize,
336    warm: usize,
337    chosen: Kernel,
338    out: &mut [f64],
339) -> Result<(), FramaError> {
340    let mut win = window;
341    if win & 1 == 1 {
342        win += 1;
343    }
344    let seed = close[first..first + win].iter().sum::<f64>() / win as f64;
345    out[first + win - 1] = seed;
346
347    match chosen {
348        Kernel::Scalar | Kernel::ScalarBatch => {
349            if win <= 32 {
350                unsafe {
351                    frama_small_scan(high, low, close, win, sc, fc, first, len, out)?;
352                }
353            } else {
354                frama_scalar_deque(high, low, close, win, sc, fc, first, len, out)?;
355            }
356        }
357
358        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
359        Kernel::Avx2 | Kernel::Avx2Batch => unsafe {
360            frama_avx2_into(high, low, close, win, sc, fc, first, len, out)?;
361        },
362
363        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
364        Kernel::Avx512 | Kernel::Avx512Batch => unsafe {
365            frama_avx512_into(high, low, close, win, sc, fc, first, len, out)?;
366        },
367
368        _ => unreachable!("`Auto` must be resolved above"),
369    }
370
371    Ok(())
372}
373
374pub fn frama_with_kernel(input: &FramaInput, kernel: Kernel) -> Result<FramaOutput, FramaError> {
375    let ((high, low, close), window, sc, fc, first, len, warm, chosen) =
376        frama_prepare(input, kernel)?;
377    let mut out = alloc_with_nan_prefix(len, warm);
378    frama_compute_into(
379        high, low, close, window, sc, fc, first, len, warm, chosen, &mut out,
380    )?;
381    Ok(FramaOutput { values: out })
382}
383
384#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
385#[inline]
386pub fn frama_into(input: &FramaInput, out: &mut [f64]) -> Result<(), FramaError> {
387    frama_into_slice(out, input, Kernel::Auto)
388}
389
390#[derive(Copy, Clone)]
391struct MonoDeque<const CAP: usize> {
392    buf: [usize; CAP],
393    head: usize,
394    tail: usize,
395}
396impl<const CAP: usize> MonoDeque<CAP> {
397    #[inline(always)]
398    const fn new() -> Self {
399        Self {
400            buf: [0; CAP],
401            head: 0,
402            tail: 0,
403        }
404    }
405    #[inline(always)]
406    fn clear(&mut self) {
407        self.head = 0;
408        self.tail = 0;
409    }
410    #[inline(always)]
411    fn is_empty(&self) -> bool {
412        self.head == self.tail
413    }
414
415    #[inline(always)]
416    unsafe fn front(&self) -> usize {
417        *self.buf.get_unchecked(self.head)
418    }
419
420    #[inline(always)]
421    fn expire(&mut self, idx_out: usize) {
422        if !self.is_empty() && unsafe { self.front() } == idx_out {
423            self.head = (self.head + 1) % CAP;
424        }
425    }
426
427    #[inline(always)]
428    unsafe fn push_max(&mut self, idx: usize, data: &[f64]) {
429        while !self.is_empty() {
430            let last = self.buf[(self.tail + CAP - 1) % CAP];
431            if *data.get_unchecked(last) >= *data.get_unchecked(idx) {
432                break;
433            }
434            self.tail = (self.tail + CAP - 1) % CAP;
435        }
436        self.buf[self.tail] = idx;
437        self.tail = (self.tail + 1) % CAP;
438    }
439
440    #[inline(always)]
441    unsafe fn push_min(&mut self, idx: usize, data: &[f64]) {
442        while !self.is_empty() {
443            let last = self.buf[(self.tail + CAP - 1) % CAP];
444            if *data.get_unchecked(last) <= *data.get_unchecked(idx) {
445                break;
446            }
447            self.tail = (self.tail + CAP - 1) % CAP;
448        }
449        self.buf[self.tail] = idx;
450        self.tail = (self.tail + 1) % CAP;
451    }
452}
453
454#[inline(always)]
455fn frama_scalar_deque(
456    high: &[f64],
457    low: &[f64],
458    close: &[f64],
459    mut window: usize,
460    sc: usize,
461    fc: usize,
462    first: usize,
463    len: usize,
464    out: &mut [f64],
465) -> Result<(), FramaError> {
466    if window & 1 == 1 {
467        window += 1;
468    }
469    let half = window / 2;
470    const MAX_W: usize = 1024;
471    assert!(window <= MAX_W, "window bigger than CAP");
472
473    let mut d_full_max: MonoDeque<MAX_W> = MonoDeque::new();
474    let mut d_full_min: MonoDeque<MAX_W> = MonoDeque::new();
475    let mut d_left_max: MonoDeque<MAX_W> = MonoDeque::new();
476    let mut d_left_min: MonoDeque<MAX_W> = MonoDeque::new();
477    let mut d_right_max: MonoDeque<MAX_W> = MonoDeque::new();
478    let mut d_right_min: MonoDeque<MAX_W> = MonoDeque::new();
479
480    unsafe {
481        for idx in first..(first + window) {
482            if !high[idx].is_nan() && !low[idx].is_nan() {
483                d_full_max.push_max(idx, high);
484                d_full_min.push_min(idx, low);
485                if idx < first + half {
486                    d_left_max.push_max(idx, high);
487                    d_left_min.push_min(idx, low);
488                } else {
489                    d_right_max.push_max(idx, high);
490                    d_right_min.push_min(idx, low);
491                }
492            }
493        }
494    }
495
496    let w_ln = (2.0 / (sc as f64 + 1.0)).ln();
497    let sc_lim = 2.0 / (sc as f64 + 1.0);
498    let mut d_prev = 1.0;
499
500    let mut pm1 = f64::NAN;
501    let mut pm2 = f64::NAN;
502    let mut pm3 = f64::NAN;
503    let mut pn1 = f64::NAN;
504    let mut pn2 = f64::NAN;
505    let mut pn3 = f64::NAN;
506
507    let mut half_progress = 0usize;
508
509    for i in (first + window)..len {
510        let idx_out = i - window;
511        d_full_max.expire(idx_out);
512        d_full_min.expire(idx_out);
513        d_left_max.expire(idx_out);
514        d_left_min.expire(idx_out);
515        d_right_max.expire(idx_out + half);
516        d_right_min.expire(idx_out + half);
517
518        let newest = i - 1;
519        if !high[newest].is_nan() && !low[newest].is_nan() {
520            unsafe {
521                d_full_max.push_max(newest, high);
522                d_full_min.push_min(newest, low);
523
524                if newest < (idx_out + half) {
525                    d_left_max.push_max(newest, high);
526                    d_left_min.push_min(newest, low);
527                } else {
528                    d_right_max.push_max(newest, high);
529                    d_right_min.push_min(newest, low);
530                }
531            }
532        }
533        fn front_or(
534            dq_max: &MonoDeque<MAX_W>,
535            dq_min: &MonoDeque<MAX_W>,
536            prev_max: &mut f64,
537            prev_min: &mut f64,
538            high: &[f64],
539            low: &[f64],
540        ) -> (f64, f64) {
541            let maxv = if !dq_max.is_empty() {
542                high[unsafe { dq_max.front() }]
543            } else {
544                *prev_max
545            };
546            let minv = if !dq_min.is_empty() {
547                low[unsafe { dq_min.front() }]
548            } else {
549                *prev_min
550            };
551            *prev_max = maxv;
552            *prev_min = minv;
553            (maxv, minv)
554        }
555        let (max1, min1) = front_or(&d_right_max, &d_right_min, &mut pm1, &mut pn1, high, low);
556        let (max2, min2) = front_or(&d_left_max, &d_left_min, &mut pm2, &mut pn2, high, low);
557        let (max3, min3) = front_or(&d_full_max, &d_full_min, &mut pm3, &mut pn3, high, low);
558
559        if !(high[i].is_nan() || low[i].is_nan() || close[i].is_nan()) {
560            let n1 = (max1 - min1) / (half as f64);
561            let n2 = (max2 - min2) / (half as f64);
562            let n3 = (max3 - min3) / (window as f64);
563
564            let d_cur = if n1 > 0.0 && n2 > 0.0 && n3 > 0.0 {
565                ((n1 + n2).ln() - n3.ln()) / std::f64::consts::LN_2
566            } else {
567                d_prev
568            };
569            d_prev = d_cur;
570
571            let mut alpha0 = (w_ln * (d_cur - 1.0)).exp();
572            if alpha0 < 0.1 {
573                alpha0 = 0.1;
574            }
575            if alpha0 > 1.0 {
576                alpha0 = 1.0;
577            }
578            let old_n = (2.0 - alpha0) / alpha0;
579            let new_n = (sc - fc) as f64 * ((old_n - 1.0) / (sc as f64 - 1.0)) + fc as f64;
580            let mut alpha = 2.0 / (new_n + 1.0);
581            if alpha < sc_lim {
582                alpha = sc_lim;
583            }
584            if alpha > 1.0 {
585                alpha = 1.0;
586            }
587
588            out[i] = alpha * close[i] + (1.0 - alpha) * out[i - 1];
589        } else {
590            out[i] = out[i - 1];
591        }
592
593        half_progress += 1;
594        if half_progress == half {
595            swap(&mut d_left_max, &mut d_right_max);
596            swap(&mut d_left_min, &mut d_right_min);
597            d_right_max.clear();
598            d_right_min.clear();
599            half_progress = 0;
600        }
601    }
602
603    Ok(())
604}
605
606#[inline(always)]
607pub fn frama_scalar(
608    high: &[f64],
609    low: &[f64],
610    close: &[f64],
611    window: usize,
612    sc: usize,
613    fc: usize,
614    first: usize,
615    len: usize,
616) -> Result<FramaOutput, FramaError> {
617    let mut win = window;
618    if win & 1 == 1 {
619        win += 1;
620    }
621    let warm = first + win - 1;
622
623    let mut out = alloc_with_nan_prefix(len, warm);
624    frama_compute_into(
625        high,
626        low,
627        close,
628        window,
629        sc,
630        fc,
631        first,
632        len,
633        warm,
634        Kernel::Scalar,
635        &mut out,
636    )?;
637    Ok(FramaOutput { values: out })
638}
639
640#[inline(always)]
641unsafe fn frama_small_scan(
642    high: &[f64],
643    low: &[f64],
644    close: &[f64],
645    win: usize,
646    sc: usize,
647    fc: usize,
648    first: usize,
649    len: usize,
650    out: &mut [f64],
651) -> Result<(), FramaError> {
652    let half = win >> 1;
653    let win_f64 = win as f64;
654    let half_f64 = half as f64;
655    let w_ln = (2.0 / (sc as f64 + 1.0)).ln();
656    let sc_floor = 2.0 / (sc as f64 + 1.0);
657    let mut d_prev = 1.0_f64;
658
659    for i in (first + win)..len {
660        let seg_start = i - win;
661        let mid = seg_start + half;
662
663        let mut max1 = f64::MIN;
664        let mut min1 = f64::MAX;
665        let mut max2 = f64::MIN;
666        let mut min2 = f64::MAX;
667
668        let mut j = seg_start;
669        while j + 1 < mid {
670            let h0 = *high.get_unchecked(j);
671            let h1 = *high.get_unchecked(j + 1);
672            let l0 = *low.get_unchecked(j);
673            let l1 = *low.get_unchecked(j + 1);
674            max2 = f64::max(max2, f64::max(h0, h1));
675            min2 = f64::min(min2, f64::min(l0, l1));
676            j += 2;
677        }
678        if j < mid {
679            max2 = f64::max(max2, *high.get_unchecked(j));
680            min2 = f64::min(min2, *low.get_unchecked(j));
681        }
682
683        j = mid;
684        while j + 1 < i {
685            let h0 = *high.get_unchecked(j);
686            let h1 = *high.get_unchecked(j + 1);
687            let l0 = *low.get_unchecked(j);
688            let l1 = *low.get_unchecked(j + 1);
689            max1 = f64::max(max1, f64::max(h0, h1));
690            min1 = f64::min(min1, f64::min(l0, l1));
691            j += 2;
692        }
693        if j < i {
694            max1 = f64::max(max1, *high.get_unchecked(j));
695            min1 = f64::min(min1, *low.get_unchecked(j));
696        }
697
698        let max3 = f64::max(max1, max2);
699        let min3 = f64::min(min1, min2);
700
701        let n1 = (max1 - min1) / half_f64;
702        let n2 = (max2 - min2) / half_f64;
703        let n3 = (max3 - min3) / win_f64;
704
705        let d_cur = if n1 > 0.0 && n2 > 0.0 && n3 > 0.0 {
706            ((n1 + n2).ln() - n3.ln()) / std::f64::consts::LN_2
707        } else {
708            d_prev
709        };
710        d_prev = d_cur;
711
712        let mut alpha0 = (w_ln * (d_cur - 1.0)).exp().clamp(0.1, 1.0);
713        let old_n = (2.0 - alpha0) / alpha0;
714        let new_n = (sc - fc) as f64 * ((old_n - 1.0) / (sc as f64 - 1.0)) + fc as f64;
715        let alpha = (2.0 / (new_n + 1.0)).clamp(sc_floor, 1.0);
716
717        out[i] = (*close.get_unchecked(i)).mul_add(alpha, (1.0 - alpha) * out[i - 1]);
718    }
719    Ok(())
720}
721
722#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
723#[inline(always)]
724unsafe fn hmax_pd256(v: __m256d) -> f64 {
725    let hi = _mm256_extractf128_pd::<1>(v);
726    let lo = _mm256_castpd256_pd128(v);
727    let m = _mm_max_pd(hi, lo);
728    let m = _mm_max_pd(m, _mm_permute_pd::<0b01>(m));
729    _mm_cvtsd_f64(m)
730}
731
732#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
733#[inline(always)]
734unsafe fn hmin_pd256(v: __m256d) -> f64 {
735    let hi = _mm256_extractf128_pd::<1>(v);
736    let lo = _mm256_castpd256_pd128(v);
737    let m = _mm_min_pd(hi, lo);
738    let m = _mm_min_pd(m, _mm_permute_pd::<0b01>(m));
739    _mm_cvtsd_f64(m)
740}
741
742#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
743#[target_feature(enable = "avx2")]
744unsafe fn frama_avx2_small<const WIN: usize>(
745    high: &[f64],
746    low: &[f64],
747    close: &[f64],
748    sc: usize,
749    fc: usize,
750    first: usize,
751    len: usize,
752    out: &mut [f64],
753) {
754    const LANES: usize = 4;
755    const LN2: f64 = std::f64::consts::LN_2;
756
757    let half = WIN / 2;
758    let win_f64 = WIN as f64;
759    let half_f64 = half as f64;
760    let w_ln = (2.0 / (sc as f64 + 1.0)).ln();
761    let sc_floor = 2.0 / (sc as f64 + 1.0);
762    let mut d_prev = 1.0;
763
764    for i in (first + WIN)..len {
765        if i + 1 < len {
766            _mm_prefetch(high.as_ptr().add(i + 1 - WIN) as *const i8, _MM_HINT_T0);
767            _mm_prefetch(low.as_ptr().add(i + 1 - WIN) as *const i8, _MM_HINT_T0);
768            _mm_prefetch(close.as_ptr().add(i + 1) as *const i8, _MM_HINT_T0);
769        }
770
771        if unlikely(
772            (*high.get_unchecked(i)).is_nan()
773                || (*low.get_unchecked(i)).is_nan()
774                || (*close.get_unchecked(i)).is_nan(),
775        ) {
776            *out.get_unchecked_mut(i) = *out.get_unchecked(i - 1);
777            continue;
778        }
779
780        let mut v_max_l = _mm256_set1_pd(f64::MIN);
781        let mut v_min_l = _mm256_set1_pd(f64::MAX);
782        let mut idx_l = i - WIN;
783
784        for _ in 0..(half / LANES) {
785            let h = _mm256_loadu_pd(high.as_ptr().add(idx_l));
786            let l = _mm256_loadu_pd(low.as_ptr().add(idx_l));
787            v_max_l = _mm256_max_pd(v_max_l, h);
788            v_min_l = _mm256_min_pd(v_min_l, l);
789            idx_l += LANES;
790        }
791
792        let mut max_l = hmax_pd256(v_max_l);
793        let mut min_l = hmin_pd256(v_min_l);
794
795        for j in idx_l..(i - half) {
796            let h = *high.get_unchecked(j);
797            let l = *low.get_unchecked(j);
798            max_l = max_l.max(h);
799            min_l = min_l.min(l);
800        }
801
802        let mut v_max_r = _mm256_set1_pd(f64::MIN);
803        let mut v_min_r = _mm256_set1_pd(f64::MAX);
804        let mut idx_r = i - half;
805
806        for _ in 0..(half / LANES) {
807            let h = _mm256_loadu_pd(high.as_ptr().add(idx_r));
808            let l = _mm256_loadu_pd(low.as_ptr().add(idx_r));
809            v_max_r = _mm256_max_pd(v_max_r, h);
810            v_min_r = _mm256_min_pd(v_min_r, l);
811            idx_r += LANES;
812        }
813
814        let mut max_r = hmax_pd256(v_max_r);
815        let mut min_r = hmin_pd256(v_min_r);
816
817        for j in idx_r..i {
818            let h = *high.get_unchecked(j);
819            let l = *low.get_unchecked(j);
820            max_r = max_r.max(h);
821            min_r = min_r.min(l);
822        }
823
824        let max_w = max_l.max(max_r);
825        let min_w = min_l.min(min_r);
826
827        let n1 = (max_r - min_r) / half_f64;
828        let n2 = (max_l - min_l) / half_f64;
829        let n3 = (max_w - min_w) / win_f64;
830
831        let d = if n1 > 0.0 && n2 > 0.0 && n3 > 0.0 {
832            ((n1 + n2).ln() - n3.ln()) / LN2
833        } else {
834            d_prev
835        };
836        d_prev = d;
837
838        let mut a0 = (w_ln * (d - 1.0)).exp().clamp(0.1, 1.0);
839        let old_n = (2.0 - a0) / a0;
840        let new_n = (sc - fc) as f64 * ((old_n - 1.0) / (sc as f64 - 1.0)) + fc as f64;
841        let alpha = (2.0 / (new_n + 1.0)).clamp(sc_floor, 1.0);
842
843        *out.get_unchecked_mut(i) =
844            (*close.get_unchecked(i)).mul_add(alpha, (1.0 - alpha) * *out.get_unchecked(i - 1));
845    }
846}
847
848#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
849#[target_feature(enable = "avx512f")]
850unsafe fn frama_avx512_small<const WIN: usize>(
851    high: &[f64],
852    low: &[f64],
853    close: &[f64],
854    sc: usize,
855    fc: usize,
856    first: usize,
857    len: usize,
858    out: &mut [f64],
859) {
860    const LANES: usize = 8;
861    const LN2: f64 = std::f64::consts::LN_2;
862
863    let half = WIN / 2;
864    let vec_cnt = half / LANES;
865    let tail = (half & (LANES - 1)) as i32;
866    let mask = (1u8 << tail) - 1;
867
868    let w_ln = (2.0 / (sc as f64 + 1.0)).ln();
869    let sc_floor = 2.0 / (sc as f64 + 1.0);
870    let win_f64 = WIN as f64;
871    let half_f64 = half as f64;
872
873    let v_min_init = _mm512_set1_pd(f64::MIN);
874    let v_max_init = _mm512_set1_pd(f64::MAX);
875
876    let mut d_prev = 1.0;
877
878    for i in (first + WIN)..len {
879        if i + 1 < len {
880            _mm_prefetch(high.as_ptr().add(i + 1 - WIN) as *const i8, _MM_HINT_T0);
881            _mm_prefetch(low.as_ptr().add(i + 1 - WIN) as *const i8, _MM_HINT_T0);
882            _mm_prefetch(close.as_ptr().add(i + 1) as *const i8, _MM_HINT_T0);
883        }
884
885        if unlikely(
886            (*high.get_unchecked(i)).is_nan()
887                || (*low.get_unchecked(i)).is_nan()
888                || (*close.get_unchecked(i)).is_nan(),
889        ) {
890            *out.get_unchecked_mut(i) = *out.get_unchecked(i - 1);
891            continue;
892        }
893
894        let mut v_max_l = v_min_init;
895        let mut v_min_l = v_max_init;
896        let base_l = i - WIN;
897
898        for k in 0..vec_cnt {
899            let off = base_l + k * LANES;
900            let h = _mm512_loadu_pd(high.as_ptr().add(off));
901            let l = _mm512_loadu_pd(low.as_ptr().add(off));
902            v_max_l = _mm512_max_pd(v_max_l, h);
903            v_min_l = _mm512_min_pd(v_min_l, l);
904        }
905
906        if tail != 0 {
907            let off = base_l + vec_cnt * LANES;
908            let h_tail =
909                _mm512_mask_loadu_pd(_mm512_set1_pd(f64::MIN), mask, high.as_ptr().add(off));
910            let l_tail =
911                _mm512_mask_loadu_pd(_mm512_set1_pd(f64::MAX), mask, low.as_ptr().add(off));
912            v_max_l = _mm512_max_pd(v_max_l, h_tail);
913            v_min_l = _mm512_min_pd(v_min_l, l_tail);
914        }
915
916        let max_l = _mm512_reduce_max_pd(v_max_l);
917        let min_l = _mm512_reduce_min_pd(v_min_l);
918
919        let mut v_max_r = v_min_init;
920        let mut v_min_r = v_max_init;
921        let base_r = i - half;
922
923        for k in 0..vec_cnt {
924            let off = base_r + k * LANES;
925            let h = _mm512_loadu_pd(high.as_ptr().add(off));
926            let l = _mm512_loadu_pd(low.as_ptr().add(off));
927            v_max_r = _mm512_max_pd(v_max_r, h);
928            v_min_r = _mm512_min_pd(v_min_r, l);
929        }
930
931        if tail != 0 {
932            let off = base_r + vec_cnt * LANES;
933            let h_tail =
934                _mm512_mask_loadu_pd(_mm512_set1_pd(f64::MIN), mask, high.as_ptr().add(off));
935            let l_tail =
936                _mm512_mask_loadu_pd(_mm512_set1_pd(f64::MAX), mask, low.as_ptr().add(off));
937            v_max_r = _mm512_max_pd(v_max_r, h_tail);
938            v_min_r = _mm512_min_pd(v_min_r, l_tail);
939        }
940
941        let max_r = _mm512_reduce_max_pd(v_max_r);
942        let min_r = _mm512_reduce_min_pd(v_min_r);
943
944        let max_w = max_l.max(max_r);
945        let min_w = min_l.min(min_r);
946
947        let n1 = (max_r - min_r) / half_f64;
948        let n2 = (max_l - min_l) / half_f64;
949        let n3 = (max_w - min_w) / win_f64;
950
951        let d = if n1 > 0.0 && n2 > 0.0 && n3 > 0.0 {
952            ((n1 + n2).ln() - n3.ln()) / LN2
953        } else {
954            d_prev
955        };
956        d_prev = d;
957
958        let mut a0 = (w_ln * (d - 1.0)).exp().clamp(0.1, 1.0);
959        let old_n = (2.0 - a0) / a0;
960        let new_n = (sc - fc) as f64 * ((old_n - 1.0) / (sc as f64 - 1.0)) + fc as f64;
961        let alpha = (2.0 / (new_n + 1.0)).clamp(sc_floor, 1.0);
962
963        *out.get_unchecked_mut(i) =
964            (*close.get_unchecked(i)).mul_add(alpha, (1.0 - alpha) * *out.get_unchecked(i - 1));
965    }
966}
967
968#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
969#[inline(always)]
970unsafe fn frama_avx2_into(
971    high: &[f64],
972    low: &[f64],
973    close: &[f64],
974    window: usize,
975    sc: usize,
976    fc: usize,
977    first: usize,
978    len: usize,
979    out: &mut [f64],
980) -> Result<(), FramaError> {
981    let mut win = window;
982    if win & 1 == 1 {
983        win += 1;
984    }
985
986    if win <= 32 {
987        match win {
988            10 => unsafe { frama_avx2_small::<10>(high, low, close, sc, fc, first, len, out) },
989            14 => unsafe { frama_avx2_small::<14>(high, low, close, sc, fc, first, len, out) },
990            20 => unsafe { frama_avx2_small::<20>(high, low, close, sc, fc, first, len, out) },
991            32 => unsafe { frama_avx2_small::<32>(high, low, close, sc, fc, first, len, out) },
992            _ => unsafe { frama_small_scan(high, low, close, win, sc, fc, first, len, out)? },
993        }
994    } else {
995        frama_scalar_deque(high, low, close, win, sc, fc, first, len, out)?;
996    }
997    Ok(())
998}
999
1000#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1001#[inline(always)]
1002unsafe fn frama_avx512_into(
1003    high: &[f64],
1004    low: &[f64],
1005    close: &[f64],
1006    window: usize,
1007    sc: usize,
1008    fc: usize,
1009    first: usize,
1010    len: usize,
1011    out: &mut [f64],
1012) -> Result<(), FramaError> {
1013    let mut win = window;
1014    if win & 1 == 1 {
1015        win += 1;
1016    }
1017
1018    if win <= 32 {
1019        match win {
1020            10 => unsafe { frama_avx512_small::<10>(high, low, close, sc, fc, first, len, out) },
1021            14 => unsafe { frama_avx512_small::<14>(high, low, close, sc, fc, first, len, out) },
1022            20 => unsafe { frama_avx512_small::<20>(high, low, close, sc, fc, first, len, out) },
1023            32 => unsafe { frama_avx512_small::<32>(high, low, close, sc, fc, first, len, out) },
1024            _ => unsafe { frama_small_scan(high, low, close, win, sc, fc, first, len, out)? },
1025        }
1026    } else {
1027        frama_scalar_deque(high, low, close, win, sc, fc, first, len, out)?;
1028    }
1029    Ok(())
1030}
1031
1032#[derive(Clone, Debug)]
1033pub struct FramaBatchRange {
1034    pub window: (usize, usize, usize),
1035    pub sc: (usize, usize, usize),
1036    pub fc: (usize, usize, usize),
1037}
1038impl Default for FramaBatchRange {
1039    fn default() -> Self {
1040        Self {
1041            window: (10, 259, 1),
1042            sc: (300, 300, 0),
1043            fc: (1, 1, 0),
1044        }
1045    }
1046}
1047#[derive(Clone, Debug, Default)]
1048pub struct FramaBatchBuilder {
1049    range: FramaBatchRange,
1050    kernel: Kernel,
1051}
1052impl FramaBatchBuilder {
1053    pub fn new() -> Self {
1054        Self::default()
1055    }
1056    pub fn kernel(mut self, k: Kernel) -> Self {
1057        self.kernel = k;
1058        self
1059    }
1060    #[inline]
1061    pub fn window_range(mut self, start: usize, end: usize, step: usize) -> Self {
1062        self.range.window = (start, end, step);
1063        self
1064    }
1065    #[inline]
1066    pub fn sc_range(mut self, start: usize, end: usize, step: usize) -> Self {
1067        self.range.sc = (start, end, step);
1068        self
1069    }
1070    #[inline]
1071    pub fn fc_range(mut self, start: usize, end: usize, step: usize) -> Self {
1072        self.range.fc = (start, end, step);
1073        self
1074    }
1075    pub fn apply_slices(
1076        self,
1077        high: &[f64],
1078        low: &[f64],
1079        close: &[f64],
1080    ) -> Result<FramaBatchOutput, FramaError> {
1081        frama_batch_with_kernel(high, low, close, &self.range, self.kernel)
1082    }
1083    pub fn apply_slice(self, slice: &[f64]) -> Result<FramaBatchOutput, FramaError> {
1084        self.apply_slices(slice, slice, slice)
1085    }
1086    pub fn with_default_slices(
1087        high: &[f64],
1088        low: &[f64],
1089        close: &[f64],
1090        k: Kernel,
1091    ) -> Result<FramaBatchOutput, FramaError> {
1092        FramaBatchBuilder::new()
1093            .kernel(k)
1094            .apply_slices(high, low, close)
1095    }
1096    pub fn apply_candles(self, c: &Candles) -> Result<FramaBatchOutput, FramaError> {
1097        let h = c.select_candle_field("high").unwrap();
1098        let l = c.select_candle_field("low").unwrap();
1099        let o = c.select_candle_field("close").unwrap();
1100        self.apply_slices(h, l, o)
1101    }
1102    pub fn with_default_candles(c: &Candles) -> Result<FramaBatchOutput, FramaError> {
1103        FramaBatchBuilder::new()
1104            .kernel(Kernel::Auto)
1105            .apply_candles(c)
1106    }
1107}
1108#[derive(Clone, Debug)]
1109pub struct FramaBatchOutput {
1110    pub values: Vec<f64>,
1111    pub combos: Vec<FramaParams>,
1112    pub rows: usize,
1113    pub cols: usize,
1114}
1115impl FramaBatchOutput {
1116    pub fn row_for_params(&self, p: &FramaParams) -> Option<usize> {
1117        self.combos.iter().position(|c| {
1118            c.window.unwrap_or(10) == p.window.unwrap_or(10)
1119                && c.sc.unwrap_or(300) == p.sc.unwrap_or(300)
1120                && c.fc.unwrap_or(1) == p.fc.unwrap_or(1)
1121        })
1122    }
1123    pub fn values_for(&self, p: &FramaParams) -> Option<&[f64]> {
1124        self.row_for_params(p).map(|row| {
1125            let start = row * self.cols;
1126            &self.values[start..start + self.cols]
1127        })
1128    }
1129}
1130#[inline(always)]
1131fn expand_grid(r: &FramaBatchRange) -> Vec<FramaParams> {
1132    fn axis_usize((start, end, step): (usize, usize, usize)) -> Vec<usize> {
1133        if step == 0 || start == end {
1134            return vec![start];
1135        }
1136
1137        let (lo, hi) = if start <= end {
1138            (start, end)
1139        } else {
1140            (end, start)
1141        };
1142        let mut v = Vec::new();
1143        let mut x = lo;
1144        loop {
1145            v.push(x);
1146            match x.checked_add(step) {
1147                Some(nx) if nx <= hi => x = nx,
1148                _ => break,
1149            }
1150        }
1151        if start > end {
1152            v.reverse();
1153        }
1154        v
1155    }
1156    let windows = axis_usize(r.window);
1157    let scs = axis_usize(r.sc);
1158    let fcs = axis_usize(r.fc);
1159
1160    let cap = windows
1161        .len()
1162        .checked_mul(scs.len())
1163        .and_then(|x| x.checked_mul(fcs.len()))
1164        .unwrap_or(0);
1165    let mut out = Vec::with_capacity(cap);
1166    for &w in &windows {
1167        for &s in &scs {
1168            for &f in &fcs {
1169                out.push(FramaParams {
1170                    window: Some(w),
1171                    sc: Some(s),
1172                    fc: Some(f),
1173                });
1174            }
1175        }
1176    }
1177    out
1178}
1179
1180pub fn frama_batch_with_kernel(
1181    high: &[f64],
1182    low: &[f64],
1183    close: &[f64],
1184    sweep: &FramaBatchRange,
1185    k: Kernel,
1186) -> Result<FramaBatchOutput, FramaError> {
1187    let kernel = match k {
1188        Kernel::Auto => match detect_best_batch_kernel() {
1189            Kernel::Avx512Batch => Kernel::Avx2Batch,
1190            other => other,
1191        },
1192        other if other.is_batch() => other,
1193        other => return Err(FramaError::InvalidKernelForBatch(other)),
1194    };
1195    let simd = match kernel {
1196        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1197        Kernel::Avx512Batch => Kernel::Avx512,
1198        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1199        Kernel::Avx2Batch => Kernel::Avx2,
1200        Kernel::ScalarBatch => Kernel::Scalar,
1201        _ => unreachable!(),
1202    };
1203    frama_batch_inner(high, low, close, sweep, simd, true)
1204}
1205
1206#[inline(always)]
1207pub fn frama_batch_slice(
1208    high: &[f64],
1209    low: &[f64],
1210    close: &[f64],
1211    sweep: &FramaBatchRange,
1212    kern: Kernel,
1213) -> Result<FramaBatchOutput, FramaError> {
1214    frama_batch_inner(high, low, close, sweep, kern, false)
1215}
1216#[inline(always)]
1217pub fn frama_batch_par_slice(
1218    high: &[f64],
1219    low: &[f64],
1220    close: &[f64],
1221    sweep: &FramaBatchRange,
1222    kern: Kernel,
1223) -> Result<FramaBatchOutput, FramaError> {
1224    frama_batch_inner(high, low, close, sweep, kern, true)
1225}
1226
1227#[inline(always)]
1228fn frama_batch_inner(
1229    high: &[f64],
1230    low: &[f64],
1231    close: &[f64],
1232    sweep: &FramaBatchRange,
1233    kern: Kernel,
1234    parallel: bool,
1235) -> Result<FramaBatchOutput, FramaError> {
1236    if high.is_empty() || low.is_empty() || close.is_empty() {
1237        return Err(FramaError::EmptyInputData);
1238    }
1239
1240    let combos = expand_grid(sweep);
1241    if combos.is_empty() {
1242        return Err(FramaError::InvalidRange {
1243            start: sweep.window.0,
1244            end: sweep.window.1,
1245            step: sweep.window.2,
1246        });
1247    }
1248
1249    let rows = combos.len();
1250    let cols = close.len();
1251
1252    let _ = rows
1253        .checked_mul(cols)
1254        .ok_or(FramaError::ArithmeticOverflow {
1255            context: "rows*cols",
1256        })?;
1257
1258    let mut buf_mu = make_uninit_matrix(rows, cols);
1259
1260    let first = (0..cols)
1261        .find(|&i| !high[i].is_nan() && !low[i].is_nan() && !close[i].is_nan())
1262        .unwrap_or(0);
1263
1264    let warm: Vec<usize> = combos
1265        .iter()
1266        .map(|p| {
1267            let mut win = p.window.unwrap();
1268
1269            if win & 1 == 1 {
1270                win += 1;
1271            }
1272            first + win - 1
1273        })
1274        .collect();
1275
1276    init_matrix_prefixes(&mut buf_mu, cols, &warm);
1277
1278    let mut buf_guard = core::mem::ManuallyDrop::new(buf_mu);
1279    let out: &mut [f64] = unsafe {
1280        core::slice::from_raw_parts_mut(buf_guard.as_mut_ptr() as *mut f64, buf_guard.len())
1281    };
1282
1283    let combos_ret = frama_batch_inner_into(high, low, close, sweep, kern, parallel, out)?;
1284
1285    let values = unsafe {
1286        Vec::from_raw_parts(
1287            buf_guard.as_mut_ptr() as *mut f64,
1288            buf_guard.len(),
1289            buf_guard.capacity(),
1290        )
1291    };
1292
1293    Ok(FramaBatchOutput {
1294        values,
1295        combos: combos_ret,
1296        rows,
1297        cols,
1298    })
1299}
1300
1301#[inline(always)]
1302fn frama_batch_inner_into(
1303    high: &[f64],
1304    low: &[f64],
1305    close: &[f64],
1306    sweep: &FramaBatchRange,
1307    kern: Kernel,
1308    parallel: bool,
1309    out: &mut [f64],
1310) -> Result<Vec<FramaParams>, FramaError> {
1311    let combos = expand_grid(sweep);
1312    if combos.is_empty() {
1313        return Err(FramaError::InvalidRange {
1314            start: sweep.window.0,
1315            end: sweep.window.1,
1316            step: sweep.window.2,
1317        });
1318    }
1319
1320    if high.is_empty() {
1321        return Err(FramaError::EmptyInputData);
1322    }
1323    if low.len() != high.len() || close.len() != high.len() {
1324        return Err(FramaError::MismatchedInputLength {
1325            high: high.len(),
1326            low: low.len(),
1327            close: close.len(),
1328        });
1329    }
1330
1331    let len = high.len();
1332    let first = (0..len)
1333        .find(|&i| !high[i].is_nan() && !low[i].is_nan() && !close[i].is_nan())
1334        .ok_or(FramaError::AllValuesNaN)?;
1335
1336    let max_even_w = combos
1337        .iter()
1338        .map(|c| {
1339            let w = c.window.unwrap();
1340            if w & 1 == 1 {
1341                w + 1
1342            } else {
1343                w
1344            }
1345        })
1346        .max()
1347        .unwrap();
1348
1349    if len - first < max_even_w {
1350        return Err(FramaError::NotEnoughValidData {
1351            needed: max_even_w,
1352            valid: len - first,
1353        });
1354    }
1355
1356    let rows = combos.len();
1357    let cols = len;
1358
1359    let do_row = |row: usize, dst: &mut [f64]| unsafe {
1360        let p = &combos[row];
1361        let window = p.window.unwrap();
1362        let sc = p.sc.unwrap();
1363        let fc = p.fc.unwrap();
1364
1365        match kern {
1366            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1367            Kernel::Avx512 => frama_row_avx512(high, low, close, first, window, dst, sc, fc),
1368            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1369            Kernel::Avx2 => frama_row_avx2(high, low, close, first, window, dst, sc, fc),
1370            _ => frama_row_scalar(high, low, close, first, window, dst, sc, fc),
1371        }
1372    };
1373
1374    if parallel {
1375        #[cfg(not(target_arch = "wasm32"))]
1376        {
1377            out.par_chunks_mut(cols)
1378                .enumerate()
1379                .for_each(|(row, slice)| do_row(row, slice));
1380        }
1381        #[cfg(target_arch = "wasm32")]
1382        {
1383            for (row, slice) in out.chunks_mut(cols).enumerate() {
1384                do_row(row, slice);
1385            }
1386        }
1387    } else {
1388        for (row, slice) in out.chunks_mut(cols).enumerate() {
1389            do_row(row, slice);
1390        }
1391    }
1392
1393    Ok(combos)
1394}
1395
1396#[derive(Debug, Clone)]
1397pub struct FramaStream {
1398    window: usize,
1399    sc: usize,
1400    fc: usize,
1401    n: usize,
1402    w: f64,
1403    buffer: Vec<(f64, f64, f64)>,
1404    head: usize,
1405    filled: bool,
1406    last_val: f64,
1407    d_prev: f64,
1408    alpha_prev: f64,
1409
1410    half: usize,
1411    idx: usize,
1412
1413    dq_r_max: DqMax,
1414    dq_r_min: DqMin,
1415    dq_l_max: DqMax,
1416    dq_l_min: DqMin,
1417    dq_w_max: DqMax,
1418    dq_w_min: DqMin,
1419
1420    pm_right: f64,
1421    pn_right: f64,
1422    pm_left: f64,
1423    pn_left: f64,
1424    pm_full: f64,
1425    pn_full: f64,
1426
1427    sc_floor: f64,
1428}
1429impl FramaStream {
1430    pub fn try_new(params: FramaParams) -> Result<Self, FramaError> {
1431        let window = params.window.unwrap_or(10);
1432        let sc = params.sc.unwrap_or(300);
1433        let fc = params.fc.unwrap_or(1);
1434        if window == 0 {
1435            return Err(FramaError::InvalidWindow {
1436                window,
1437                data_len: 0,
1438            });
1439        }
1440        let mut n = window;
1441        if n % 2 == 1 {
1442            n += 1;
1443        }
1444        Ok(Self {
1445            window,
1446            sc,
1447            fc,
1448            n,
1449            w: (2.0 / (sc as f64 + 1.0)).ln(),
1450            buffer: vec![(f64::NAN, f64::NAN, f64::NAN); n],
1451            head: 0,
1452            filled: false,
1453            last_val: f64::NAN,
1454            d_prev: 1.0,
1455            alpha_prev: 2.0 / (sc as f64 + 1.0),
1456
1457            half: n / 2,
1458            idx: 0,
1459            dq_r_max: DqMax::default(),
1460            dq_r_min: DqMin::default(),
1461            dq_l_max: DqMax::default(),
1462            dq_l_min: DqMin::default(),
1463            dq_w_max: DqMax::default(),
1464            dq_w_min: DqMin::default(),
1465            pm_right: f64::NAN,
1466            pn_right: f64::NAN,
1467            pm_left: f64::NAN,
1468            pn_left: f64::NAN,
1469            pm_full: f64::NAN,
1470            pn_full: f64::NAN,
1471            sc_floor: 2.0 / (sc as f64 + 1.0),
1472        })
1473    }
1474    #[inline(always)]
1475    pub fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
1476        if !self.filled {
1477            self.buffer[self.head] = (high, low, close);
1478            self.head += 1;
1479
1480            if self.head == self.n {
1481                self.head = 0;
1482                self.filled = true;
1483
1484                let sum: f64 = self.buffer.iter().map(|&(_, _, c)| c).sum();
1485                self.last_val = sum / self.n as f64;
1486
1487                self.dq_r_max.clear();
1488                self.dq_r_min.clear();
1489                self.dq_l_max.clear();
1490                self.dq_l_min.clear();
1491                self.dq_w_max.clear();
1492                self.dq_w_min.clear();
1493
1494                for j in 0..self.n {
1495                    let (h, l, _) = self.buffer[j];
1496                    if !(h.is_nan() || l.is_nan()) {
1497                        self.dq_w_max.push(j, h);
1498                        self.dq_w_min.push(j, l);
1499                        if j < self.half {
1500                            self.dq_l_max.push(j, h);
1501                            self.dq_l_min.push(j, l);
1502                        } else {
1503                            self.dq_r_max.push(j, h);
1504                            self.dq_r_min.push(j, l);
1505                        }
1506                    }
1507                }
1508
1509                self.pm_right = self.dq_r_max.front_val().unwrap_or(f64::NAN);
1510                self.pn_right = self.dq_r_min.front_val().unwrap_or(f64::NAN);
1511                self.pm_left = self.dq_l_max.front_val().unwrap_or(f64::NAN);
1512                self.pn_left = self.dq_l_min.front_val().unwrap_or(f64::NAN);
1513                self.pm_full = self.dq_w_max.front_val().unwrap_or(f64::NAN);
1514                self.pn_full = self.dq_w_min.front_val().unwrap_or(f64::NAN);
1515
1516                self.idx = self.n;
1517
1518                return Some(self.last_val);
1519            }
1520
1521            return None;
1522        }
1523
1524        let i = self.idx;
1525
1526        let right_lb = i.saturating_sub(self.half);
1527        let left_lb = i.saturating_sub(self.n);
1528        self.dq_r_max.expire_lt(right_lb);
1529        self.dq_r_min.expire_lt(right_lb);
1530        self.dq_l_max.expire_lt(left_lb);
1531        self.dq_l_min.expire_lt(left_lb);
1532        self.dq_w_max.expire_lt(left_lb);
1533        self.dq_w_min.expire_lt(left_lb);
1534
1535        let (max_r, min_r) = {
1536            let mr = self.dq_r_max.front_val().unwrap_or(self.pm_right);
1537            let nr = self.dq_r_min.front_val().unwrap_or(self.pn_right);
1538            (mr, nr)
1539        };
1540        let (max_l, min_l) = {
1541            let ml = self.dq_l_max.front_val().unwrap_or(self.pm_left);
1542            let nl = self.dq_l_min.front_val().unwrap_or(self.pn_left);
1543            (ml, nl)
1544        };
1545        let (max_w, min_w) = {
1546            let mw = self.dq_w_max.front_val().unwrap_or(self.pm_full);
1547            let nw = self.dq_w_min.front_val().unwrap_or(self.pn_full);
1548            (mw, nw)
1549        };
1550
1551        self.pm_right = max_r;
1552        self.pn_right = min_r;
1553        self.pm_left = max_l;
1554        self.pn_left = min_l;
1555        self.pm_full = max_w;
1556        self.pn_full = min_w;
1557
1558        let half_f = self.half as f64;
1559        let win_f = self.n as f64;
1560
1561        let output = if !(high.is_nan() || low.is_nan() || close.is_nan()) {
1562            let n1 = (max_r - min_r) / half_f;
1563            let n2 = (max_l - min_l) / half_f;
1564            let n3 = (max_w - min_w) / win_f;
1565
1566            let d = if n1 > 0.0 && n2 > 0.0 && n3 > 0.0 {
1567                ((n1 + n2).ln() - n3.ln()) / std::f64::consts::LN_2
1568            } else {
1569                self.d_prev
1570            };
1571            self.d_prev = d;
1572
1573            let mut a0 = (self.w * (d - 1.0)).exp();
1574            if a0 < 0.1 {
1575                a0 = 0.1;
1576            }
1577            if a0 > 1.0 {
1578                a0 = 1.0;
1579            }
1580
1581            let old_n = (2.0 - a0) / a0;
1582            let new_n = (self.sc - self.fc) as f64 * ((old_n - 1.0) / (self.sc as f64 - 1.0))
1583                + self.fc as f64;
1584
1585            let mut alpha = 2.0 / (new_n + 1.0);
1586            if alpha < self.sc_floor {
1587                alpha = self.sc_floor;
1588            }
1589            if alpha > 1.0 {
1590                alpha = 1.0;
1591            }
1592            self.alpha_prev = alpha;
1593
1594            close.mul_add(alpha, (1.0 - alpha) * self.last_val)
1595        } else {
1596            self.last_val
1597        };
1598
1599        if !(high.is_nan() || low.is_nan()) {
1600            self.dq_r_max.push(i, high);
1601            self.dq_r_min.push(i, low);
1602            self.dq_w_max.push(i, high);
1603            self.dq_w_min.push(i, low);
1604        }
1605
1606        if i >= self.half {
1607            let j = i - self.half;
1608            let (h_l, l_l, _) = self.buffer[j % self.n];
1609            if !(h_l.is_nan() || l_l.is_nan()) {
1610                self.dq_l_max.push(j, h_l);
1611                self.dq_l_min.push(j, l_l);
1612            }
1613        }
1614
1615        self.buffer[self.head] = (high, low, close);
1616        self.head = (self.head + 1) % self.n;
1617
1618        self.idx += 1;
1619        self.last_val = output;
1620        Some(output)
1621    }
1622}
1623
1624#[derive(Default, Debug, Clone)]
1625struct DqMax {
1626    q: VecDeque<(usize, f64)>,
1627}
1628#[derive(Default, Debug, Clone)]
1629struct DqMin {
1630    q: VecDeque<(usize, f64)>,
1631}
1632
1633impl DqMax {
1634    #[inline(always)]
1635    fn clear(&mut self) {
1636        self.q.clear();
1637    }
1638    #[inline(always)]
1639    fn expire_lt(&mut self, bound: usize) {
1640        while let Some(&(i, _)) = self.q.front() {
1641            if i < bound {
1642                self.q.pop_front();
1643            } else {
1644                break;
1645            }
1646        }
1647    }
1648    #[inline(always)]
1649    fn push(&mut self, idx: usize, val: f64) {
1650        while let Some(&(_, v)) = self.q.back() {
1651            if v >= val {
1652                break;
1653            }
1654            self.q.pop_back();
1655        }
1656        self.q.push_back((idx, val));
1657    }
1658    #[inline(always)]
1659    fn front_val(&self) -> Option<f64> {
1660        self.q.front().map(|&(_, v)| v)
1661    }
1662}
1663impl DqMin {
1664    #[inline(always)]
1665    fn clear(&mut self) {
1666        self.q.clear();
1667    }
1668    #[inline(always)]
1669    fn expire_lt(&mut self, bound: usize) {
1670        while let Some(&(i, _)) = self.q.front() {
1671            if i < bound {
1672                self.q.pop_front();
1673            } else {
1674                break;
1675            }
1676        }
1677    }
1678    #[inline(always)]
1679    fn push(&mut self, idx: usize, val: f64) {
1680        while let Some(&(_, v)) = self.q.back() {
1681            if v <= val {
1682                break;
1683            }
1684            self.q.pop_back();
1685        }
1686        self.q.push_back((idx, val));
1687    }
1688    #[inline(always)]
1689    fn front_val(&self) -> Option<f64> {
1690        self.q.front().map(|&(_, v)| v)
1691    }
1692}
1693
1694#[inline(always)]
1695pub unsafe fn frama_row_scalar(
1696    high: &[f64],
1697    low: &[f64],
1698    close: &[f64],
1699    first: usize,
1700    window: usize,
1701    out: &mut [f64],
1702    sc: usize,
1703    fc: usize,
1704) {
1705    let len = high.len();
1706
1707    let mut win = window;
1708    if win & 1 == 1 {
1709        win += 1;
1710    }
1711    seed_sma(close, first, win, out);
1712
1713    if win <= 32 {
1714        frama_small_scan(high, low, close, win, sc, fc, first, len, out).unwrap();
1715    } else {
1716        frama_scalar_deque(high, low, close, win, sc, fc, first, len, out).unwrap();
1717    }
1718}
1719
1720#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1721#[inline(always)]
1722pub unsafe fn frama_row_avx2(
1723    high: &[f64],
1724    low: &[f64],
1725    close: &[f64],
1726    first: usize,
1727    window: usize,
1728    out: &mut [f64],
1729    sc: usize,
1730    fc: usize,
1731) {
1732    let mut win = window;
1733    if win & 1 == 1 {
1734        win += 1;
1735    }
1736
1737    seed_sma(close, first, win, out);
1738
1739    if win <= 32 {
1740        match win {
1741            10 => frama_avx2_small::<10>(high, low, close, sc, fc, first, high.len(), out),
1742            14 => frama_avx2_small::<14>(high, low, close, sc, fc, first, high.len(), out),
1743            20 => frama_avx2_small::<20>(high, low, close, sc, fc, first, high.len(), out),
1744            32 => frama_avx2_small::<32>(high, low, close, sc, fc, first, high.len(), out),
1745            _ => frama_small_scan(high, low, close, win, sc, fc, first, high.len(), out).unwrap(),
1746        }
1747    } else {
1748        frama_scalar_deque(high, low, close, win, sc, fc, first, high.len(), out).unwrap();
1749    }
1750}
1751
1752#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1753#[inline(always)]
1754pub unsafe fn frama_row_avx512(
1755    high: &[f64],
1756    low: &[f64],
1757    close: &[f64],
1758    first: usize,
1759    window: usize,
1760    out: &mut [f64],
1761    sc: usize,
1762    fc: usize,
1763) {
1764    let mut win = window;
1765    if win & 1 == 1 {
1766        win += 1;
1767    }
1768
1769    seed_sma(close, first, win, out);
1770
1771    if win <= 32 {
1772        match win {
1773            10 => frama_avx512_small::<10>(high, low, close, sc, fc, first, high.len(), out),
1774            14 => frama_avx512_small::<14>(high, low, close, sc, fc, first, high.len(), out),
1775            20 => frama_avx512_small::<20>(high, low, close, sc, fc, first, high.len(), out),
1776            32 => frama_avx512_small::<32>(high, low, close, sc, fc, first, high.len(), out),
1777            _ => frama_small_scan(high, low, close, win, sc, fc, first, high.len(), out).unwrap(),
1778        }
1779    } else {
1780        frama_scalar_deque(high, low, close, win, sc, fc, first, high.len(), out).unwrap();
1781    }
1782}
1783
1784#[cfg(test)]
1785mod tests {
1786    use super::*;
1787    use crate::skip_if_unsupported;
1788    use crate::utilities::data_loader::read_candles_from_csv;
1789    use crate::utilities::enums::Kernel;
1790    use paste::paste;
1791    use proptest::prelude::*;
1792
1793    fn check_frama_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1794        skip_if_unsupported!(kernel, test_name);
1795        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1796        let candles = read_candles_from_csv(file_path)?;
1797        let default_params = FramaParams {
1798            window: None,
1799            sc: None,
1800            fc: None,
1801        };
1802        let input = FramaInput::from_candles(&candles, default_params);
1803        let output = frama_with_kernel(&input, kernel)?;
1804        assert_eq!(output.values.len(), candles.close.len());
1805        Ok(())
1806    }
1807    fn check_frama_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1808        skip_if_unsupported!(kernel, test_name);
1809        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1810        let candles = read_candles_from_csv(file_path)?;
1811        let input = FramaInput::from_candles(&candles, FramaParams::default());
1812        let result = frama_with_kernel(&input, kernel)?;
1813        let expected_last_five = [
1814            59337.23056930512,
1815            59321.607512374605,
1816            59286.677929994796,
1817            59268.00202402624,
1818            59160.03888720062,
1819        ];
1820        let start = result.values.len().saturating_sub(5);
1821        for (i, &val) in result.values[start..].iter().enumerate() {
1822            let diff = (val - expected_last_five[i]).abs();
1823            assert!(
1824                diff < 1e-1,
1825                "[{}] FRAMA {:?} mismatch at idx {}: got {}, expected {}",
1826                test_name,
1827                kernel,
1828                i,
1829                val,
1830                expected_last_five[i]
1831            );
1832        }
1833        Ok(())
1834    }
1835    fn check_frama_zero_window(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1836        skip_if_unsupported!(kernel, test_name);
1837        let input_data = [10.0, 20.0, 30.0];
1838        let params = FramaParams {
1839            window: Some(0),
1840            sc: None,
1841            fc: None,
1842        };
1843        let input = FramaInput::from_slices(&input_data, &input_data, &input_data, params);
1844        let res = frama_with_kernel(&input, kernel);
1845        assert!(
1846            res.is_err(),
1847            "[{}] FRAMA should fail with zero window",
1848            test_name
1849        );
1850        Ok(())
1851    }
1852    fn check_frama_window_exceeds_length(
1853        test_name: &str,
1854        kernel: Kernel,
1855    ) -> Result<(), Box<dyn Error>> {
1856        skip_if_unsupported!(kernel, test_name);
1857        let data_small = [10.0, 20.0, 30.0];
1858        let params = FramaParams {
1859            window: Some(10),
1860            sc: None,
1861            fc: None,
1862        };
1863        let input = FramaInput::from_slices(&data_small, &data_small, &data_small, params);
1864        let res = frama_with_kernel(&input, kernel);
1865        assert!(
1866            res.is_err(),
1867            "[{}] FRAMA should fail with window exceeding length",
1868            test_name
1869        );
1870        Ok(())
1871    }
1872    fn check_frama_very_small_dataset(
1873        test_name: &str,
1874        kernel: Kernel,
1875    ) -> Result<(), Box<dyn Error>> {
1876        skip_if_unsupported!(kernel, test_name);
1877        let single_point = [42.0];
1878        let params = FramaParams {
1879            window: Some(9),
1880            sc: None,
1881            fc: None,
1882        };
1883        let input = FramaInput::from_slices(&single_point, &single_point, &single_point, params);
1884        let res = frama_with_kernel(&input, kernel);
1885        assert!(
1886            res.is_err(),
1887            "[{}] FRAMA should fail with insufficient data",
1888            test_name
1889        );
1890        Ok(())
1891    }
1892    fn check_frama_all_nan(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1893        skip_if_unsupported!(kernel, test_name);
1894        let nan_data = [f64::NAN, f64::NAN, f64::NAN];
1895        let params = FramaParams::default();
1896        let input = FramaInput::from_slices(&nan_data, &nan_data, &nan_data, params);
1897        let res = frama_with_kernel(&input, kernel);
1898        assert!(res.is_err());
1899        Ok(())
1900    }
1901    fn check_frama_empty_input(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1902        skip_if_unsupported!(kernel, test_name);
1903        let empty: [f64; 0] = [];
1904        let params = FramaParams::default();
1905        let input = FramaInput::from_slices(&empty, &empty, &empty, params);
1906        let res = frama_with_kernel(&input, kernel);
1907        assert!(matches!(res, Err(FramaError::EmptyInputData)));
1908        Ok(())
1909    }
1910
1911    fn check_frama_mismatched_len(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1912        skip_if_unsupported!(kernel, test_name);
1913        let h = [1.0, 2.0, 3.0];
1914        let l = [1.0, 2.0];
1915        let c = [1.0, 2.0, 3.0];
1916        let params = FramaParams::default();
1917        let input = FramaInput::from_slices(&h, &l, &c, params);
1918        let res = frama_with_kernel(&input, kernel);
1919        assert!(matches!(res, Err(FramaError::MismatchedInputLength { .. })));
1920        Ok(())
1921    }
1922
1923    fn check_frama_reinput(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1924        skip_if_unsupported!(kernel, test_name);
1925        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1926        let candles = read_candles_from_csv(file_path)?;
1927
1928        let params = FramaParams::default();
1929        let first_input = FramaInput::from_candles(&candles, params.clone());
1930        let first_res = frama_with_kernel(&first_input, kernel)?;
1931
1932        let second_input = FramaInput::from_slices(
1933            &first_res.values,
1934            &first_res.values,
1935            &first_res.values,
1936            params,
1937        );
1938        let second_res = frama_with_kernel(&second_input, kernel)?;
1939        assert_eq!(first_res.values.len(), second_res.values.len());
1940        Ok(())
1941    }
1942
1943    fn check_frama_nan_handling(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1944        skip_if_unsupported!(kernel, test_name);
1945        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1946        let candles = read_candles_from_csv(file_path)?;
1947        let input = FramaInput::from_candles(&candles, FramaParams::default());
1948        let res = frama_with_kernel(&input, kernel)?;
1949        if res.values.len() > 240 {
1950            for (i, &v) in res.values[240..].iter().enumerate() {
1951                assert!(
1952                    !v.is_nan(),
1953                    "[{}] Found unexpected NaN at out-index {}",
1954                    test_name,
1955                    240 + i
1956                );
1957            }
1958        }
1959        Ok(())
1960    }
1961
1962    fn check_frama_property(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1963        skip_if_unsupported!(kernel, test_name);
1964
1965        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1966        let candles = read_candles_from_csv(file_path)?;
1967        let high = candles.select_candle_field("high").unwrap();
1968        let low = candles.select_candle_field("low").unwrap();
1969        let close = candles.select_candle_field("close").unwrap();
1970
1971        let data_len = high.len();
1972        let strat = (
1973            4usize..=64,
1974            50usize..500,
1975            1usize..50,
1976            0usize..data_len.saturating_sub(200),
1977            100usize..=200,
1978        );
1979
1980        proptest::test_runner::TestRunner::default()
1981            .run(&strat, |(window, sc, fc, start_idx, slice_len)| {
1982                let end_idx = (start_idx + slice_len).min(data_len);
1983                let actual_len = end_idx - start_idx;
1984
1985                if actual_len < window * 2 {
1986                    return Ok(());
1987                }
1988
1989                let high_slice = &high[start_idx..end_idx];
1990                let low_slice = &low[start_idx..end_idx];
1991                let close_slice = &close[start_idx..end_idx];
1992
1993                let params = FramaParams {
1994                    window: Some(window),
1995                    sc: Some(sc),
1996                    fc: Some(fc),
1997                };
1998
1999                let input = FramaInput::from_slices(high_slice, low_slice, close_slice, params);
2000                let result = frama_with_kernel(&input, kernel);
2001
2002                prop_assert!(result.is_ok(), "FRAMA failed: {:?}", result.err());
2003                let FramaOutput { values: out } = result.unwrap();
2004
2005                let FramaOutput { values: ref_out } =
2006                    frama_with_kernel(&input, Kernel::Scalar).unwrap();
2007
2008                let actual_window = if window & 1 == 1 { window + 1 } else { window };
2009
2010                let first_output_idx = actual_window - 1;
2011
2012                for i in 0..first_output_idx.min(out.len()) {
2013                    prop_assert!(
2014                        out[i].is_nan(),
2015                        "Expected NaN during warmup at index {}, got {}",
2016                        i,
2017                        out[i]
2018                    );
2019                }
2020
2021                for i in first_output_idx..out.len() {
2022                    let y = out[i];
2023                    let r = ref_out[i];
2024
2025                    let all_high_max = high_slice
2026                        .iter()
2027                        .filter(|x| x.is_finite())
2028                        .cloned()
2029                        .fold(f64::NEG_INFINITY, f64::max);
2030                    let all_low_min = low_slice
2031                        .iter()
2032                        .filter(|x| x.is_finite())
2033                        .cloned()
2034                        .fold(f64::INFINITY, f64::min);
2035
2036                    if all_high_max.is_finite() && all_low_min.is_finite() {
2037                        let tolerance = (all_high_max - all_low_min) * 0.01;
2038                        prop_assert!(
2039                            y.is_nan()
2040                                || (y >= all_low_min - tolerance && y <= all_high_max + tolerance),
2041                            "idx {}: {} not in overall range [{}, {}] with tolerance {}",
2042                            i,
2043                            y,
2044                            all_low_min,
2045                            all_high_max,
2046                            tolerance
2047                        );
2048                    }
2049
2050                    if !y.is_finite() || !r.is_finite() {
2051                        prop_assert!(
2052                            y.to_bits() == r.to_bits(),
2053                            "NaN mismatch at idx {}: {} vs {}",
2054                            i,
2055                            y,
2056                            r
2057                        );
2058                    } else {
2059                        let ulp_diff = y.to_bits().abs_diff(r.to_bits());
2060                        prop_assert!(
2061                            (y - r).abs() <= 1e-9 || ulp_diff <= 4,
2062                            "mismatch at idx {}: {} vs {} (ULP={})",
2063                            i,
2064                            y,
2065                            r,
2066                            ulp_diff
2067                        );
2068                    }
2069
2070                    if fc >= sc && i > first_output_idx {
2071                        let change = (y - out[i - 1]).abs();
2072                        let price_change = (close_slice[i] - close_slice[i - 1]).abs();
2073                        prop_assert!(
2074							change <= price_change + 1e-6,
2075							"Unexpected large change at idx {} with fc >= sc: {} vs price change {}",
2076							i,
2077							change,
2078							price_change
2079						);
2080                    }
2081                }
2082
2083                Ok(())
2084            })
2085            .unwrap();
2086
2087        Ok(())
2088    }
2089    fn check_frama_streaming(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2090        skip_if_unsupported!(kernel, test_name);
2091        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2092        let candles = read_candles_from_csv(file_path)?;
2093        let high = candles.select_candle_field("high").unwrap();
2094        let low = candles.select_candle_field("low").unwrap();
2095        let close = candles.select_candle_field("close").unwrap();
2096        let period = 10;
2097        let sc = 300;
2098        let fc = 1;
2099        let input = FramaInput::from_slices(
2100            high,
2101            low,
2102            close,
2103            FramaParams {
2104                window: Some(period),
2105                sc: Some(sc),
2106                fc: Some(fc),
2107            },
2108        );
2109        let batch_output = frama_with_kernel(&input, kernel)?.values;
2110        let mut stream = FramaStream::try_new(FramaParams {
2111            window: Some(period),
2112            sc: Some(sc),
2113            fc: Some(fc),
2114        })?;
2115        let mut stream_values = Vec::with_capacity(close.len());
2116        for ((&h, &l), &c) in high.iter().zip(low.iter()).zip(close.iter()) {
2117            match stream.update(h, l, c) {
2118                Some(val) => stream_values.push(val),
2119                None => stream_values.push(f64::NAN),
2120            }
2121        }
2122        assert_eq!(batch_output.len(), stream_values.len());
2123        for (i, (&b, &s)) in batch_output.iter().zip(stream_values.iter()).enumerate() {
2124            if b.is_nan() && s.is_nan() {
2125                continue;
2126            }
2127            let diff = (b - s).abs();
2128            assert!(
2129                diff < 1e-7,
2130                "[{}] FRAMA streaming mismatch at idx {}: batch={}, stream={}",
2131                test_name,
2132                i,
2133                b,
2134                s
2135            );
2136        }
2137        Ok(())
2138    }
2139    fn check_frama_default_candles(test: &str, k: Kernel) -> Result<(), Box<dyn Error>> {
2140        skip_if_unsupported!(k, test);
2141        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2142        let c = read_candles_from_csv(file)?;
2143        let input = FramaInput::with_default_candles(&c);
2144        match input.data {
2145            FramaData::Candles { .. } => {}
2146            _ => panic!("Expected FramaData::Candles"),
2147        }
2148        let out = frama_with_kernel(&input, k)?;
2149        assert_eq!(out.values.len(), c.close.len());
2150        Ok(())
2151    }
2152
2153    #[cfg(debug_assertions)]
2154    fn check_frama_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2155        skip_if_unsupported!(kernel, test_name);
2156
2157        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2158        let candles = read_candles_from_csv(file_path)?;
2159
2160        let test_cases = vec![
2161            FramaParams::default(),
2162            FramaParams {
2163                window: Some(4),
2164                sc: Some(300),
2165                fc: Some(1),
2166            },
2167            FramaParams {
2168                window: Some(8),
2169                sc: Some(150),
2170                fc: Some(1),
2171            },
2172            FramaParams {
2173                window: Some(10),
2174                sc: Some(200),
2175                fc: Some(2),
2176            },
2177            FramaParams {
2178                window: Some(12),
2179                sc: Some(400),
2180                fc: Some(1),
2181            },
2182            FramaParams {
2183                window: Some(20),
2184                sc: Some(300),
2185                fc: Some(1),
2186            },
2187            FramaParams {
2188                window: Some(30),
2189                sc: Some(500),
2190                fc: Some(3),
2191            },
2192            FramaParams {
2193                window: Some(16),
2194                sc: Some(100),
2195                fc: Some(1),
2196            },
2197            FramaParams {
2198                window: Some(14),
2199                sc: Some(600),
2200                fc: Some(4),
2201            },
2202        ];
2203
2204        for params in test_cases {
2205            let input = FramaInput::from_candles(&candles, params.clone());
2206            let output = frama_with_kernel(&input, kernel)?;
2207
2208            for (i, &val) in output.values.iter().enumerate() {
2209                if val.is_nan() {
2210                    continue;
2211                }
2212
2213                let bits = val.to_bits();
2214
2215                if bits == 0x11111111_11111111 {
2216                    panic!(
2217                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} with params window={:?}, sc={:?}, fc={:?}",
2218                        test_name, val, bits, i, params.window, params.sc, params.fc
2219                    );
2220                }
2221
2222                if bits == 0x22222222_22222222 {
2223                    panic!(
2224                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} with params window={:?}, sc={:?}, fc={:?}",
2225                        test_name, val, bits, i, params.window, params.sc, params.fc
2226                    );
2227                }
2228
2229                if bits == 0x33333333_33333333 {
2230                    panic!(
2231                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} with params window={:?}, sc={:?}, fc={:?}",
2232                        test_name, val, bits, i, params.window, params.sc, params.fc
2233                    );
2234                }
2235            }
2236        }
2237
2238        Ok(())
2239    }
2240
2241    #[cfg(not(debug_assertions))]
2242    fn check_frama_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2243        Ok(())
2244    }
2245
2246    macro_rules! generate_all_frama_tests {
2247        ($($test_fn:ident),*) => {
2248            paste! {
2249                $(
2250                    #[test]
2251                    fn [<$test_fn _scalar_f64>]() {
2252                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
2253                    }
2254                )*
2255                $(
2256                    #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2257                    #[test]
2258                    fn [<$test_fn _avx2_f64>]() {
2259                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
2260                    }
2261                )*
2262                $(
2263                    #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2264                    #[test]
2265                    fn [<$test_fn _avx512_f64>]() {
2266                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
2267                    }
2268                )*
2269            }
2270        }
2271    }
2272    generate_all_frama_tests!(
2273        check_frama_partial_params,
2274        check_frama_accuracy,
2275        check_frama_zero_window,
2276        check_frama_window_exceeds_length,
2277        check_frama_very_small_dataset,
2278        check_frama_all_nan,
2279        check_frama_empty_input,
2280        check_frama_mismatched_len,
2281        check_frama_reinput,
2282        check_frama_nan_handling,
2283        check_frama_property,
2284        check_frama_streaming,
2285        check_frama_default_candles,
2286        check_frama_no_poison
2287    );
2288    fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2289        skip_if_unsupported!(kernel, test);
2290        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2291        let c = read_candles_from_csv(file)?;
2292        let output = FramaBatchBuilder::new().kernel(kernel).apply_candles(&c)?;
2293        let def = FramaParams::default();
2294        let row = output.values_for(&def).expect("default row missing");
2295        assert_eq!(row.len(), c.close.len());
2296        let expected = [
2297            59337.23056930512,
2298            59321.607512374605,
2299            59286.677929994796,
2300            59268.00202402624,
2301            59160.03888720062,
2302        ];
2303        let start = row.len() - 5;
2304        for (i, &v) in row[start..].iter().enumerate() {
2305            assert!(
2306                (v - expected[i]).abs() < 1e-1,
2307                "[{test}] default-row mismatch at idx {i}: {v} vs {expected:?}"
2308            );
2309        }
2310        Ok(())
2311    }
2312
2313    #[cfg(debug_assertions)]
2314    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2315        skip_if_unsupported!(kernel, test);
2316
2317        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2318        let c = read_candles_from_csv(file)?;
2319
2320        let test_configs = vec![
2321            ((4, 8, 2), (100, 300, 100), (1, 2, 1)),
2322            ((10, 20, 5), (200, 400, 100), (1, 3, 1)),
2323            ((20, 30, 5), (300, 600, 150), (1, 4, 1)),
2324            ((6, 12, 2), (150, 450, 50), (1, 2, 1)),
2325            ((8, 16, 2), (100, 500, 100), (1, 5, 1)),
2326        ];
2327
2328        for (window_range, sc_range, fc_range) in test_configs {
2329            let output = FramaBatchBuilder::new()
2330                .kernel(kernel)
2331                .window_range(window_range.0, window_range.1, window_range.2)
2332                .sc_range(sc_range.0, sc_range.1, sc_range.2)
2333                .fc_range(fc_range.0, fc_range.1, fc_range.2)
2334                .apply_candles(&c)?;
2335
2336            for (idx, &val) in output.values.iter().enumerate() {
2337                if val.is_nan() {
2338                    continue;
2339                }
2340
2341                let bits = val.to_bits();
2342                let row = idx / output.cols;
2343                let col = idx % output.cols;
2344                let params = &output.combos[row];
2345
2346                if bits == 0x11111111_11111111 {
2347                    panic!(
2348                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at row {} col {} (params: window={:?}, sc={:?}, fc={:?})",
2349                        test, val, bits, row, col, params.window, params.sc, params.fc
2350                    );
2351                }
2352
2353                if bits == 0x22222222_22222222 {
2354                    panic!(
2355                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at row {} col {} (params: window={:?}, sc={:?}, fc={:?})",
2356                        test, val, bits, row, col, params.window, params.sc, params.fc
2357                    );
2358                }
2359
2360                if bits == 0x33333333_33333333 {
2361                    panic!(
2362                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at row {} col {} (params: window={:?}, sc={:?}, fc={:?})",
2363                        test, val, bits, row, col, params.window, params.sc, params.fc
2364                    );
2365                }
2366            }
2367        }
2368
2369        Ok(())
2370    }
2371
2372    #[cfg(not(debug_assertions))]
2373    fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2374        Ok(())
2375    }
2376    macro_rules! gen_batch_tests {
2377        ($fn_name:ident) => {
2378            paste! {
2379                #[test]
2380                fn [<$fn_name _scalar>]() {
2381                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
2382                }
2383                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2384                #[test]
2385                fn [<$fn_name _avx2>]() {
2386                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
2387                }
2388                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2389                #[test]
2390                fn [<$fn_name _avx512>]() {
2391                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
2392                }
2393                #[test]
2394                fn [<$fn_name _auto_detect>]() {
2395                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
2396                }
2397            }
2398        };
2399    }
2400    gen_batch_tests!(check_batch_default_row);
2401    gen_batch_tests!(check_batch_no_poison);
2402
2403    #[test]
2404    fn test_frama_into_matches_api() -> Result<(), Box<dyn Error>> {
2405        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2406        let candles = read_candles_from_csv(file_path)?;
2407
2408        let input = FramaInput::with_default_candles(&candles);
2409        let baseline = frama(&input)?.values;
2410
2411        let mut out = vec![0.0; candles.close.len()];
2412
2413        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2414        {
2415            frama_into(&input, &mut out)?;
2416        }
2417        #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2418        {
2419            frama_into_slice(&mut out, &input, Kernel::Auto)?;
2420        }
2421
2422        assert_eq!(out.len(), baseline.len());
2423        for i in 0..out.len() {
2424            let a = out[i];
2425            let b = baseline[i];
2426            if a.is_nan() || b.is_nan() {
2427                assert!(a.is_nan() && b.is_nan(), "NaN mismatch at index {}", i);
2428            } else {
2429                assert!(a == b, "Value mismatch at index {}: {} != {}", i, a, b);
2430            }
2431        }
2432        Ok(())
2433    }
2434}
2435
2436#[cfg(feature = "python")]
2437use crate::utilities::kernel_validation::validate_kernel;
2438#[cfg(feature = "python")]
2439use numpy::{IntoPyArray, PyArrayMethods, PyReadonlyArray1};
2440#[cfg(feature = "python")]
2441use pyo3::exceptions::PyValueError;
2442#[cfg(feature = "python")]
2443use pyo3::prelude::*;
2444#[cfg(feature = "python")]
2445use pyo3::types::PyDict;
2446
2447#[cfg(all(feature = "python", feature = "cuda"))]
2448use crate::cuda::moving_averages::DeviceArrayF32;
2449#[cfg(all(feature = "python", feature = "cuda"))]
2450use cust::context::Context;
2451#[cfg(all(feature = "python", feature = "cuda"))]
2452use cust::memory::DeviceBuffer;
2453#[cfg(all(feature = "python", feature = "cuda"))]
2454use std::sync::Arc;
2455
2456#[cfg(all(feature = "python", feature = "cuda"))]
2457#[pyclass(
2458    module = "ta_indicators.cuda",
2459    name = "DeviceArrayF32Frama",
2460    unsendable
2461)]
2462pub struct DeviceArrayF32FramaPy {
2463    pub(crate) inner: DeviceArrayF32,
2464    _ctx_guard: Arc<Context>,
2465    _device_id: u32,
2466}
2467
2468#[cfg(all(feature = "python", feature = "cuda"))]
2469#[pymethods]
2470impl DeviceArrayF32FramaPy {
2471    #[new]
2472    fn py_new() -> PyResult<Self> {
2473        Err(pyo3::exceptions::PyTypeError::new_err(
2474            "use CUDA FRAMA factory functions to create this object",
2475        ))
2476    }
2477
2478    #[getter]
2479    fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
2480        let d = PyDict::new(py);
2481        let item = std::mem::size_of::<f32>();
2482        d.set_item("shape", (self.inner.rows, self.inner.cols))?;
2483        d.set_item("typestr", "<f4")?;
2484        d.set_item("strides", (self.inner.cols * item, item))?;
2485        let size = self.inner.rows.saturating_mul(self.inner.cols);
2486        let ptr_val: usize = if size == 0 {
2487            0
2488        } else {
2489            self.inner.buf.as_device_ptr().as_raw() as usize
2490        };
2491        d.set_item("data", (ptr_val, false))?;
2492
2493        d.set_item("version", 3)?;
2494        Ok(d)
2495    }
2496
2497    fn __dlpack_device__(&self) -> (i32, i32) {
2498        (2, self._device_id as i32)
2499    }
2500
2501    #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
2502    fn __dlpack__<'py>(
2503        &mut self,
2504        py: Python<'py>,
2505        stream: Option<PyObject>,
2506        max_version: Option<PyObject>,
2507        dl_device: Option<PyObject>,
2508        copy: Option<PyObject>,
2509    ) -> PyResult<PyObject> {
2510        use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
2511
2512        let (kdl, alloc_dev) = self.__dlpack_device__();
2513        if let Some(dev_obj) = dl_device.as_ref() {
2514            if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
2515                if dev_ty != kdl || dev_id != alloc_dev {
2516                    let wants_copy = copy
2517                        .as_ref()
2518                        .and_then(|c| c.extract::<bool>(py).ok())
2519                        .unwrap_or(false);
2520                    if wants_copy {
2521                        return Err(PyValueError::new_err(
2522                            "device copy not implemented for __dlpack__",
2523                        ));
2524                    } else {
2525                        return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
2526                    }
2527                }
2528            }
2529        }
2530        let _ = stream;
2531
2532        let dummy =
2533            DeviceBuffer::from_slice(&[]).map_err(|e| PyValueError::new_err(e.to_string()))?;
2534        let inner = std::mem::replace(
2535            &mut self.inner,
2536            DeviceArrayF32 {
2537                buf: dummy,
2538                rows: 0,
2539                cols: 0,
2540            },
2541        );
2542
2543        let rows = inner.rows;
2544        let cols = inner.cols;
2545        let buf = inner.buf;
2546
2547        let max_version_bound = max_version.map(|obj| obj.into_bound(py));
2548
2549        export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
2550    }
2551}
2552
2553#[cfg(all(feature = "python", feature = "cuda"))]
2554impl DeviceArrayF32FramaPy {
2555    pub fn new(inner: DeviceArrayF32, ctx_guard: Arc<Context>, device_id: u32) -> Self {
2556        Self {
2557            inner,
2558            _ctx_guard: ctx_guard,
2559            _device_id: device_id,
2560        }
2561    }
2562}
2563
2564#[cfg(feature = "python")]
2565#[pyfunction(name = "frama")]
2566#[pyo3(signature = (high, low, close, window, sc=300, fc=1, kernel=None))]
2567pub fn frama_py<'py>(
2568    py: Python<'py>,
2569    high: PyReadonlyArray1<'py, f64>,
2570    low: PyReadonlyArray1<'py, f64>,
2571    close: PyReadonlyArray1<'py, f64>,
2572    window: usize,
2573    sc: usize,
2574    fc: usize,
2575    kernel: Option<&str>,
2576) -> PyResult<Bound<'py, numpy::PyArray1<f64>>> {
2577    let h = high.as_slice()?;
2578    let l = low.as_slice()?;
2579    let c = close.as_slice()?;
2580
2581    let params = FramaParams {
2582        window: Some(window),
2583        sc: Some(sc),
2584        fc: Some(fc),
2585    };
2586    let input = FramaInput::from_slices(h, l, c, params);
2587    let kern = validate_kernel(kernel, false)?;
2588
2589    let out: Vec<f64> = py
2590        .allow_threads(|| frama_with_kernel(&input, kern).map(|o| o.values))
2591        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2592
2593    Ok(out.into_pyarray(py))
2594}
2595
2596#[cfg(feature = "python")]
2597#[pyfunction(name = "frama_batch")]
2598#[pyo3(signature = (high, low, close, window_range, sc_range, fc_range, kernel=None))]
2599pub fn frama_batch_py<'py>(
2600    py: Python<'py>,
2601    high: PyReadonlyArray1<'py, f64>,
2602    low: PyReadonlyArray1<'py, f64>,
2603    close: PyReadonlyArray1<'py, f64>,
2604    window_range: (usize, usize, usize),
2605    sc_range: (usize, usize, usize),
2606    fc_range: (usize, usize, usize),
2607    kernel: Option<&str>,
2608) -> PyResult<Bound<'py, PyDict>> {
2609    use numpy::{PyArray1, PyArrayMethods};
2610
2611    let high_slice = high.as_slice()?;
2612    let low_slice = low.as_slice()?;
2613    let close_slice = close.as_slice()?;
2614    let kern = validate_kernel(kernel, true)?;
2615
2616    let range = FramaBatchRange {
2617        window: window_range,
2618        sc: sc_range,
2619        fc: fc_range,
2620    };
2621
2622    let combos = expand_grid(&range);
2623    let rows = combos.len();
2624    let cols = close_slice.len();
2625
2626    let out_arr = unsafe { PyArray1::<f64>::new(py, [rows * cols], false) };
2627    let slice_out = unsafe { out_arr.as_slice_mut()? };
2628
2629    let combos_result = py
2630        .allow_threads(|| -> Result<Vec<FramaParams>, FramaError> {
2631            let kernel = match kern {
2632                Kernel::Auto => match detect_best_batch_kernel() {
2633                    Kernel::Avx512Batch => Kernel::Avx2Batch,
2634                    other => other,
2635                },
2636                k => k,
2637            };
2638
2639            let single_kernel = match kernel {
2640                Kernel::ScalarBatch => Kernel::Scalar,
2641                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2642                Kernel::Avx2Batch => Kernel::Avx2,
2643                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2644                Kernel::Avx512Batch => Kernel::Avx512,
2645                _ => Kernel::Scalar,
2646            };
2647
2648            let first = close_slice
2649                .iter()
2650                .enumerate()
2651                .find(|(i, &v)| !v.is_nan() && !high_slice[*i].is_nan() && !low_slice[*i].is_nan())
2652                .map(|(i, _)| i)
2653                .unwrap_or(0);
2654
2655            for (row_idx, combo) in combos.iter().enumerate() {
2656                let window = combo.window.unwrap_or(10);
2657                let warmup_period = first + window - 1;
2658                let row_start = row_idx * cols;
2659                for col_idx in 0..warmup_period.min(cols) {
2660                    slice_out[row_start + col_idx] = f64::NAN;
2661                }
2662            }
2663
2664            frama_batch_inner_into(
2665                high_slice,
2666                low_slice,
2667                close_slice,
2668                &range,
2669                single_kernel,
2670                true,
2671                slice_out,
2672            )
2673        })
2674        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2675
2676    let dict = PyDict::new(py);
2677    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
2678
2679    let windows: Vec<u64> = combos_result
2680        .iter()
2681        .map(|c| c.window.unwrap_or(10) as u64)
2682        .collect();
2683    let scs: Vec<u64> = combos_result
2684        .iter()
2685        .map(|c| c.sc.unwrap_or(300) as u64)
2686        .collect();
2687    let fcs: Vec<u64> = combos_result
2688        .iter()
2689        .map(|c| c.fc.unwrap_or(1) as u64)
2690        .collect();
2691
2692    dict.set_item("windows", windows.into_pyarray(py))?;
2693    dict.set_item("scs", scs.into_pyarray(py))?;
2694    dict.set_item("fcs", fcs.into_pyarray(py))?;
2695    Ok(dict)
2696}
2697
2698#[cfg(all(feature = "python", feature = "cuda"))]
2699#[pyfunction(name = "frama_cuda_batch_dev")]
2700#[pyo3(signature = (high_f32, low_f32, close_f32, window_range, sc_range, fc_range, device_id=0))]
2701pub fn frama_cuda_batch_dev_py<'py>(
2702    py: Python<'py>,
2703    high_f32: numpy::PyReadonlyArray1<'py, f32>,
2704    low_f32: numpy::PyReadonlyArray1<'py, f32>,
2705    close_f32: numpy::PyReadonlyArray1<'py, f32>,
2706    window_range: (usize, usize, usize),
2707    sc_range: (usize, usize, usize),
2708    fc_range: (usize, usize, usize),
2709    device_id: usize,
2710) -> PyResult<(DeviceArrayF32FramaPy, Bound<'py, PyDict>)> {
2711    use crate::cuda::cuda_available;
2712    use numpy::IntoPyArray;
2713    use pyo3::types::PyDict;
2714
2715    if !cuda_available() {
2716        return Err(PyValueError::new_err("CUDA not available"));
2717    }
2718
2719    let high_slice = high_f32.as_slice()?;
2720    let low_slice = low_f32.as_slice()?;
2721    let close_slice = close_f32.as_slice()?;
2722    if high_slice.len() != low_slice.len() || high_slice.len() != close_slice.len() {
2723        return Err(PyValueError::new_err("mismatched slice lengths"));
2724    }
2725
2726    let sweep = FramaBatchRange {
2727        window: window_range,
2728        sc: sc_range,
2729        fc: fc_range,
2730    };
2731
2732    let (inner, combos, ctx, dev_id) = py.allow_threads(|| {
2733        let cuda = CudaFrama::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2734        let ctx = cuda.ctx();
2735        let dev_id = cuda.device_id();
2736        cuda.frama_batch_dev(high_slice, low_slice, close_slice, &sweep)
2737            .map_err(|e| PyValueError::new_err(e.to_string()))
2738            .map(|(d, c)| (d, c, ctx, dev_id))
2739    })?;
2740
2741    let dict = PyDict::new(py);
2742    let windows: Vec<u64> = combos.iter().map(|c| c.window.unwrap() as u64).collect();
2743    let scs: Vec<u64> = combos.iter().map(|c| c.sc.unwrap() as u64).collect();
2744    let fcs: Vec<u64> = combos.iter().map(|c| c.fc.unwrap() as u64).collect();
2745    dict.set_item("windows", windows.into_pyarray(py))?;
2746    dict.set_item("scs", scs.into_pyarray(py))?;
2747    dict.set_item("fcs", fcs.into_pyarray(py))?;
2748
2749    Ok((DeviceArrayF32FramaPy::new(inner, ctx, dev_id), dict))
2750}
2751
2752#[cfg(all(feature = "python", feature = "cuda"))]
2753#[pyfunction(name = "frama_cuda_many_series_one_param_dev")]
2754#[pyo3(signature = (high_tm_f32, low_tm_f32, close_tm_f32, window, sc, fc, device_id=0))]
2755pub fn frama_cuda_many_series_one_param_dev_py(
2756    py: Python<'_>,
2757    high_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
2758    low_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
2759    close_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
2760    window: usize,
2761    sc: usize,
2762    fc: usize,
2763    device_id: usize,
2764) -> PyResult<DeviceArrayF32FramaPy> {
2765    use crate::cuda::cuda_available;
2766    use numpy::PyUntypedArrayMethods;
2767
2768    if !cuda_available() {
2769        return Err(PyValueError::new_err("CUDA not available"));
2770    }
2771
2772    let high_shape = high_tm_f32.shape();
2773    let low_shape = low_tm_f32.shape();
2774    let close_shape = close_tm_f32.shape();
2775    if low_shape != high_shape || close_shape != high_shape {
2776        return Err(PyValueError::new_err(
2777            "high, low, and close arrays must share the same shape",
2778        ));
2779    }
2780
2781    let rows = high_shape[0];
2782    let cols = high_shape[1];
2783    let high_slice = high_tm_f32.as_slice()?;
2784    let low_slice = low_tm_f32.as_slice()?;
2785    let close_slice = close_tm_f32.as_slice()?;
2786
2787    let params = FramaParams {
2788        window: Some(window),
2789        sc: Some(sc),
2790        fc: Some(fc),
2791    };
2792
2793    let (inner, ctx, dev_id) = py.allow_threads(|| {
2794        let cuda = CudaFrama::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2795        let ctx = cuda.ctx();
2796        let dev_id = cuda.device_id();
2797        cuda.frama_many_series_one_param_time_major_dev(
2798            high_slice,
2799            low_slice,
2800            close_slice,
2801            cols,
2802            rows,
2803            &params,
2804        )
2805        .map_err(|e| PyValueError::new_err(e.to_string()))
2806        .map(|d| (d, ctx, dev_id))
2807    })?;
2808
2809    Ok(DeviceArrayF32FramaPy::new(inner, ctx, dev_id))
2810}
2811
2812#[cfg(feature = "python")]
2813#[pyclass(name = "FramaStream")]
2814pub struct FramaStreamPy {
2815    inner: FramaStream,
2816}
2817
2818#[cfg(feature = "python")]
2819#[pymethods]
2820impl FramaStreamPy {
2821    #[new]
2822    fn new(window: usize, sc: usize, fc: usize) -> PyResult<Self> {
2823        Ok(Self {
2824            inner: FramaStream::try_new(FramaParams {
2825                window: Some(window),
2826                sc: Some(sc),
2827                fc: Some(fc),
2828            })
2829            .map_err(|e| PyValueError::new_err(e.to_string()))?,
2830        })
2831    }
2832
2833    fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
2834        self.inner.update(high, low, close)
2835    }
2836}
2837
2838#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2839use serde::{Deserialize, Serialize};
2840#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2841use wasm_bindgen::prelude::*;
2842
2843#[inline]
2844pub fn frama_into_slice(
2845    dst: &mut [f64],
2846    input: &FramaInput,
2847    kern: Kernel,
2848) -> Result<(), FramaError> {
2849    let ((high, low, close), window, sc, fc, first, len, _warm_from_prepare, chosen) =
2850        frama_prepare(input, kern)?;
2851
2852    if dst.len() != len {
2853        return Err(FramaError::OutputLengthMismatch {
2854            expected: len,
2855            got: dst.len(),
2856        });
2857    }
2858
2859    let mut win = window;
2860    if win & 1 == 1 {
2861        win += 1;
2862    }
2863    let warm = first + win - 1;
2864
2865    for v in &mut dst[..warm] {
2866        *v = f64::NAN;
2867    }
2868
2869    frama_compute_into(
2870        high, low, close, window, sc, fc, first, len, warm, chosen, dst,
2871    )?;
2872
2873    Ok(())
2874}
2875
2876#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2877#[wasm_bindgen]
2878pub fn frama_js(
2879    high: &[f64],
2880    low: &[f64],
2881    close: &[f64],
2882    window: usize,
2883    sc: usize,
2884    fc: usize,
2885) -> Result<Vec<f64>, JsValue> {
2886    let input = FramaInput::from_slices(
2887        high,
2888        low,
2889        close,
2890        FramaParams {
2891            window: Some(window),
2892            sc: Some(sc),
2893            fc: Some(fc),
2894        },
2895    );
2896
2897    let ((h, l, c), window, sc, fc, first, len, _warm, _chosen) =
2898        frama_prepare(&input, Kernel::Scalar).map_err(|e| JsValue::from_str(&e.to_string()))?;
2899
2900    let mut out = vec![f64::NAN; len];
2901
2902    let mut stream = FramaStream::try_new(FramaParams {
2903        window: Some(window),
2904        sc: Some(sc),
2905        fc: Some(fc),
2906    })
2907    .map_err(|e| JsValue::from_str(&e.to_string()))?;
2908
2909    for i in first..len {
2910        if let Some(v) = stream.update(h[i], l[i], c[i]) {
2911            out[i] = v;
2912        }
2913    }
2914
2915    Ok(out)
2916}
2917
2918#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2919#[wasm_bindgen]
2920pub fn frama_batch_js(
2921    high: &[f64],
2922    low: &[f64],
2923    close: &[f64],
2924    window_start: usize,
2925    window_end: usize,
2926    window_step: usize,
2927    sc_start: usize,
2928    sc_end: usize,
2929    sc_step: usize,
2930    fc_start: usize,
2931    fc_end: usize,
2932    fc_step: usize,
2933) -> Result<Vec<f64>, JsValue> {
2934    let range = FramaBatchRange {
2935        window: (window_start, window_end, window_step),
2936        sc: (sc_start, sc_end, sc_step),
2937        fc: (fc_start, fc_end, fc_step),
2938    };
2939
2940    let combos = expand_grid(&range);
2941    if combos.is_empty() {
2942        return Err(JsValue::from_str(
2943            &FramaError::InvalidRange {
2944                start: window_start,
2945                end: window_end,
2946                step: window_step,
2947            }
2948            .to_string(),
2949        ));
2950    }
2951
2952    let cols = close.len();
2953    let rows = combos.len();
2954    let total = rows.checked_mul(cols).ok_or_else(|| {
2955        JsValue::from_str(
2956            &FramaError::ArithmeticOverflow {
2957                context: "rows*cols",
2958            }
2959            .to_string(),
2960        )
2961    })?;
2962    let mut out = vec![f64::NAN; total];
2963
2964    for (row, p) in combos.iter().enumerate() {
2965        let window = p.window.unwrap_or(10);
2966        let sc = p.sc.unwrap_or(300);
2967        let fc = p.fc.unwrap_or(1);
2968
2969        let row_out = &mut out[row * cols..(row + 1) * cols];
2970        let input = FramaInput::from_slices(
2971            high,
2972            low,
2973            close,
2974            FramaParams {
2975                window: Some(window),
2976                sc: Some(sc),
2977                fc: Some(fc),
2978            },
2979        );
2980        let ((h, l, c), window, sc, fc, first, len, _warm, _chosen) =
2981            frama_prepare(&input, Kernel::Scalar).map_err(|e| JsValue::from_str(&e.to_string()))?;
2982
2983        if row_out.len() != len {
2984            return Err(JsValue::from_str(
2985                &FramaError::OutputLengthMismatch {
2986                    expected: len,
2987                    got: row_out.len(),
2988                }
2989                .to_string(),
2990            ));
2991        }
2992
2993        let mut stream = FramaStream::try_new(FramaParams {
2994            window: Some(window),
2995            sc: Some(sc),
2996            fc: Some(fc),
2997        })
2998        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2999
3000        for i in first..len {
3001            if let Some(v) = stream.update(h[i], l[i], c[i]) {
3002                row_out[i] = v;
3003            }
3004        }
3005    }
3006
3007    Ok(out)
3008}
3009
3010#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3011#[wasm_bindgen]
3012pub fn frama_batch_metadata_js(
3013    window_start: usize,
3014    window_end: usize,
3015    window_step: usize,
3016    sc_start: usize,
3017    sc_end: usize,
3018    sc_step: usize,
3019    fc_start: usize,
3020    fc_end: usize,
3021    fc_step: usize,
3022) -> Vec<usize> {
3023    let range = FramaBatchRange {
3024        window: (window_start, window_end, window_step),
3025        sc: (sc_start, sc_end, sc_step),
3026        fc: (fc_start, fc_end, fc_step),
3027    };
3028
3029    let combos = expand_grid(&range);
3030    let mut metadata = Vec::with_capacity(combos.len() * 3);
3031
3032    for combo in combos {
3033        metadata.push(combo.window.unwrap_or(10));
3034        metadata.push(combo.sc.unwrap_or(300));
3035        metadata.push(combo.fc.unwrap_or(1));
3036    }
3037
3038    metadata
3039}
3040
3041#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3042#[wasm_bindgen]
3043pub fn frama_alloc(len: usize) -> *mut f64 {
3044    let mut vec = Vec::<f64>::with_capacity(len);
3045    let ptr = vec.as_mut_ptr();
3046    std::mem::forget(vec);
3047    ptr
3048}
3049
3050#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3051#[wasm_bindgen]
3052pub fn frama_free(ptr: *mut f64, len: usize) {
3053    if !ptr.is_null() {
3054        unsafe {
3055            let _ = Vec::from_raw_parts(ptr, len, len);
3056        }
3057    }
3058}
3059
3060#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3061#[wasm_bindgen(js_name = frama_into)]
3062pub fn frama_into_js(
3063    high_ptr: *const f64,
3064    low_ptr: *const f64,
3065    close_ptr: *const f64,
3066    out_ptr: *mut f64,
3067    len: usize,
3068    window: usize,
3069    sc: usize,
3070    fc: usize,
3071) -> Result<(), JsValue> {
3072    if high_ptr.is_null() || low_ptr.is_null() || close_ptr.is_null() || out_ptr.is_null() {
3073        return Err(JsValue::from_str("Null pointer provided"));
3074    }
3075
3076    unsafe {
3077        let h = std::slice::from_raw_parts(high_ptr, len);
3078        let l = std::slice::from_raw_parts(low_ptr, len);
3079        let c = std::slice::from_raw_parts(close_ptr, len);
3080        let out = std::slice::from_raw_parts_mut(out_ptr, len);
3081
3082        let input = FramaInput::from_slices(
3083            h,
3084            l,
3085            c,
3086            FramaParams {
3087                window: Some(window),
3088                sc: Some(sc),
3089                fc: Some(fc),
3090            },
3091        );
3092
3093        if out_ptr as *const f64 == high_ptr
3094            || out_ptr as *const f64 == low_ptr
3095            || out_ptr as *const f64 == close_ptr
3096        {
3097            let mut tmp = vec![0.0; len];
3098            frama_into_slice(&mut tmp, &input, detect_best_kernel())
3099                .map_err(|e| JsValue::from_str(&e.to_string()))?;
3100            out.copy_from_slice(&tmp);
3101        } else {
3102            frama_into_slice(out, &input, detect_best_kernel())
3103                .map_err(|e| JsValue::from_str(&e.to_string()))?;
3104        }
3105    }
3106    Ok(())
3107}
3108
3109#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3110#[derive(Serialize, Deserialize)]
3111pub struct FramaBatchConfig {
3112    pub window_range: (usize, usize, usize),
3113    pub sc_range: (usize, usize, usize),
3114    pub fc_range: (usize, usize, usize),
3115}
3116
3117#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3118#[derive(Serialize, Deserialize)]
3119pub struct FramaBatchJsOutput {
3120    pub values: Vec<f64>,
3121    pub combos: Vec<FramaParams>,
3122    pub rows: usize,
3123    pub cols: usize,
3124}
3125
3126#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3127#[wasm_bindgen(js_name = frama_batch)]
3128pub fn frama_batch_unified_js(
3129    high: &[f64],
3130    low: &[f64],
3131    close: &[f64],
3132    config: JsValue,
3133) -> Result<JsValue, JsValue> {
3134    let config: FramaBatchConfig = serde_wasm_bindgen::from_value(config)
3135        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
3136
3137    let sweep = FramaBatchRange {
3138        window: config.window_range,
3139        sc: config.sc_range,
3140        fc: config.fc_range,
3141    };
3142
3143    let output = frama_batch_inner(high, low, close, &sweep, Kernel::Auto, false)
3144        .map_err(|e| JsValue::from_str(&e.to_string()))?;
3145
3146    let result = FramaBatchJsOutput {
3147        values: output.values,
3148        combos: output.combos,
3149        rows: output.rows,
3150        cols: output.cols,
3151    };
3152
3153    serde_wasm_bindgen::to_value(&result)
3154        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
3155}
3156
3157#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3158#[wasm_bindgen(js_name = frama_batch_into)]
3159pub fn frama_batch_into_js(
3160    high_ptr: *const f64,
3161    low_ptr: *const f64,
3162    close_ptr: *const f64,
3163    out_ptr: *mut f64,
3164    len: usize,
3165    w0: usize,
3166    w1: usize,
3167    ws: usize,
3168    s0: usize,
3169    s1: usize,
3170    ss: usize,
3171    f0: usize,
3172    f1: usize,
3173    fs: usize,
3174) -> Result<usize, JsValue> {
3175    if high_ptr.is_null() || low_ptr.is_null() || close_ptr.is_null() || out_ptr.is_null() {
3176        return Err(JsValue::from_str("null pointer passed to frama_batch_into"));
3177    }
3178
3179    unsafe {
3180        let h = std::slice::from_raw_parts(high_ptr, len);
3181        let l = std::slice::from_raw_parts(low_ptr, len);
3182        let c = std::slice::from_raw_parts(close_ptr, len);
3183        let sweep = FramaBatchRange {
3184            window: (w0, w1, ws),
3185            sc: (s0, s1, ss),
3186            fc: (f0, f1, fs),
3187        };
3188
3189        let combos = expand_grid(&sweep);
3190        let rows = combos.len();
3191        let cols = len;
3192        let out = std::slice::from_raw_parts_mut(out_ptr, rows * cols);
3193        frama_batch_inner_into(h, l, c, &sweep, detect_best_kernel(), false, out)
3194            .map_err(|e| JsValue::from_str(&e.to_string()))?;
3195        Ok(rows)
3196    }
3197}