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