1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::{source_type, Candles};
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
19 make_uninit_matrix,
20};
21#[cfg(feature = "python")]
22use crate::utilities::kernel_validation::validate_kernel;
23
24use std::convert::AsRef;
25use std::error::Error;
26use thiserror::Error;
27
28#[derive(Debug, Clone)]
29pub struct FvgTrailingStopOutput {
30 pub upper: Vec<f64>,
31 pub lower: Vec<f64>,
32 pub upper_ts: Vec<f64>,
33 pub lower_ts: Vec<f64>,
34}
35
36#[derive(Debug, Clone)]
37#[cfg_attr(
38 all(target_arch = "wasm32", feature = "wasm"),
39 derive(Serialize, Deserialize)
40)]
41pub struct FvgTrailingStopParams {
42 pub unmitigated_fvg_lookback: Option<usize>,
43 pub smoothing_length: Option<usize>,
44 pub reset_on_cross: Option<bool>,
45}
46
47impl Default for FvgTrailingStopParams {
48 fn default() -> Self {
49 Self {
50 unmitigated_fvg_lookback: Some(5),
51 smoothing_length: Some(9),
52 reset_on_cross: Some(false),
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
58pub enum FvgTrailingStopData<'a> {
59 Candles(&'a Candles),
60 Slices {
61 high: &'a [f64],
62 low: &'a [f64],
63 close: &'a [f64],
64 },
65}
66
67#[inline]
68fn first_valid_ohlc(high: &[f64], low: &[f64], close: &[f64]) -> usize {
69 for i in 0..high.len() {
70 if !high[i].is_nan() && !low[i].is_nan() && !close[i].is_nan() {
71 return i;
72 }
73 }
74 usize::MAX
75}
76
77#[derive(Debug, Clone)]
78pub struct FvgTrailingStopInput<'a> {
79 pub data: FvgTrailingStopData<'a>,
80 pub params: FvgTrailingStopParams,
81}
82
83impl<'a> FvgTrailingStopInput<'a> {
84 pub fn from_candles(candles: &'a Candles, params: FvgTrailingStopParams) -> Self {
85 Self {
86 data: FvgTrailingStopData::Candles(candles),
87 params,
88 }
89 }
90
91 pub fn from_slices(
92 high: &'a [f64],
93 low: &'a [f64],
94 close: &'a [f64],
95 params: FvgTrailingStopParams,
96 ) -> Self {
97 Self {
98 data: FvgTrailingStopData::Slices { high, low, close },
99 params,
100 }
101 }
102
103 pub fn with_default_candles(candles: &'a Candles) -> Self {
104 Self::from_candles(candles, FvgTrailingStopParams::default())
105 }
106
107 pub fn get_lookback(&self) -> usize {
108 self.params.unmitigated_fvg_lookback.unwrap_or(5)
109 }
110
111 pub fn get_smoothing(&self) -> usize {
112 self.params.smoothing_length.unwrap_or(9)
113 }
114
115 pub fn get_reset_on_cross(&self) -> bool {
116 self.params.reset_on_cross.unwrap_or(false)
117 }
118
119 pub fn as_slices(&self) -> (&'a [f64], &'a [f64], &'a [f64]) {
120 match &self.data {
121 FvgTrailingStopData::Candles(c) => (&c.high, &c.low, &c.close),
122 FvgTrailingStopData::Slices { high, low, close } => (high, low, close),
123 }
124 }
125}
126
127#[derive(Debug, Error)]
128pub enum FvgTrailingStopError {
129 #[error("fvg_trailing_stop: Input data slice is empty.")]
130 EmptyInputData,
131
132 #[error("fvg_trailing_stop: All values are NaN.")]
133 AllValuesNaN,
134
135 #[error("fvg_trailing_stop: Invalid period: period = {period}, data length = {data_len}")]
136 InvalidPeriod { period: usize, data_len: usize },
137
138 #[error("fvg_trailing_stop: Not enough valid data: needed = {needed}, valid = {valid}")]
139 NotEnoughValidData { needed: usize, valid: usize },
140
141 #[error("fvg_trailing_stop: Invalid smoothing_length: {smoothing}")]
142 InvalidSmoothingLength { smoothing: usize },
143
144 #[error("fvg_trailing_stop: Invalid unmitigated_fvg_lookback: {lookback}")]
145 InvalidLookback { lookback: usize },
146
147 #[error("fvg_trailing_stop: Output length mismatch: expected {expected}, got {got}")]
148 OutputLengthMismatch { expected: usize, got: usize },
149
150 #[error("fvg_trailing_stop: Invalid range: start={start}, end={end}, step={step}")]
151 InvalidRange {
152 start: usize,
153 end: usize,
154 step: usize,
155 },
156
157 #[error("fvg_trailing_stop: Invalid kernel for batch path: {0:?}")]
158 InvalidKernelForBatch(Kernel),
159}
160
161#[inline]
162fn fvg_ts_scalar(
163 high: &[f64],
164 low: &[f64],
165 close: &[f64],
166 lookback: usize,
167 smoothing_len: usize,
168 reset_on_cross: bool,
169 upper: &mut [f64],
170 lower: &mut [f64],
171 upper_ts: &mut [f64],
172 lower_ts: &mut [f64],
173) {
174 let len = high.len();
175 debug_assert_eq!(len, low.len());
176 debug_assert_eq!(len, close.len());
177 debug_assert_eq!(len, upper.len());
178 debug_assert_eq!(len, lower.len());
179 debug_assert_eq!(len, upper_ts.len());
180 debug_assert_eq!(len, lower_ts.len());
181
182 let mut bull_buf = vec![0.0f64; lookback];
183 let mut bear_buf = vec![0.0f64; lookback];
184 let mut bull_len: usize = 0;
185 let mut bear_len: usize = 0;
186
187 let mut last_bull_non_na: Option<usize> = None;
188 let mut last_bear_non_na: Option<usize> = None;
189
190 let w = smoothing_len;
191 let mut bull_ring_vals = vec![0.0f64; w];
192 let mut bull_ring_nan = vec![false; w];
193 let mut bear_ring_vals = vec![0.0f64; w];
194 let mut bear_ring_nan = vec![false; w];
195
196 let mut bull_sum = 0.0f64;
197 let mut bear_sum = 0.0f64;
198 let mut bull_nan_cnt = 0usize;
199 let mut bear_nan_cnt = 0usize;
200 let mut bull_ring_count = 0usize;
201 let mut bear_ring_count = 0usize;
202 let mut bull_ring_idx = 0usize;
203 let mut bear_ring_idx = 0usize;
204
205 let mut os: Option<i8> = None;
206 let mut ts: Option<f64> = None;
207 let mut ts_prev: Option<f64> = None;
208
209 for i in 0..len {
210 if i >= 2 && !high[i - 2].is_nan() && !low[i - 2].is_nan() && !close[i - 1].is_nan() {
211 if low[i] > high[i - 2] && close[i - 1] > high[i - 2] {
212 if bull_len < lookback {
213 bull_buf[bull_len] = high[i - 2];
214 bull_len += 1;
215 } else {
216 for k in 1..lookback {
217 bull_buf[k - 1] = bull_buf[k];
218 }
219 bull_buf[lookback - 1] = high[i - 2];
220 }
221 }
222 if high[i] < low[i - 2] && close[i - 1] < low[i - 2] {
223 if bear_len < lookback {
224 bear_buf[bear_len] = low[i - 2];
225 bear_len += 1;
226 } else {
227 for k in 1..lookback {
228 bear_buf[k - 1] = bear_buf[k];
229 }
230 bear_buf[lookback - 1] = low[i - 2];
231 }
232 }
233 }
234
235 let c = close[i];
236
237 let mut new_bull_len = 0usize;
238 let mut bull_acc = 0.0f64;
239 for k in 0..bull_len {
240 let v = bull_buf[k];
241 if c >= v {
242 bull_buf[new_bull_len] = v;
243 new_bull_len += 1;
244 bull_acc += v;
245 }
246 }
247 bull_len = new_bull_len;
248
249 let mut new_bear_len = 0usize;
250 let mut bear_acc = 0.0f64;
251 for k in 0..bear_len {
252 let v = bear_buf[k];
253 if c <= v {
254 bear_buf[new_bear_len] = v;
255 new_bear_len += 1;
256 bear_acc += v;
257 }
258 }
259 bear_len = new_bear_len;
260
261 let bull_avg = if bull_len > 0 {
262 bull_acc / (bull_len as f64)
263 } else {
264 f64::NAN
265 };
266 let bear_avg = if bear_len > 0 {
267 bear_acc / (bear_len as f64)
268 } else {
269 f64::NAN
270 };
271
272 if !bull_avg.is_nan() {
273 last_bull_non_na = Some(i);
274 }
275 if !bear_avg.is_nan() {
276 last_bear_non_na = Some(i);
277 }
278
279 let bull_bs = if bull_avg.is_nan() {
280 match last_bull_non_na {
281 Some(last) => ((i - last).max(1)).min(w),
282 None => 1,
283 }
284 } else {
285 1
286 };
287 let bear_bs = if bear_avg.is_nan() {
288 match last_bear_non_na {
289 Some(last) => ((i - last).max(1)).min(w),
290 None => 1,
291 }
292 } else {
293 1
294 };
295
296 let bull_sma = if bull_avg.is_nan() && (i + 1) >= bull_bs {
297 let mut s = 0.0f64;
298 let start = i + 1 - bull_bs;
299 for j in start..=i {
300 s += close[j];
301 }
302 s / (bull_bs as f64)
303 } else {
304 f64::NAN
305 };
306 let bear_sma = if bear_avg.is_nan() && (i + 1) >= bear_bs {
307 let mut s = 0.0f64;
308 let start = i + 1 - bear_bs;
309 for j in start..=i {
310 s += close[j];
311 }
312 s / (bear_bs as f64)
313 } else {
314 f64::NAN
315 };
316
317 let x_bull = if !bull_avg.is_nan() {
318 bull_avg
319 } else {
320 bull_sma
321 };
322 let x_bear = if !bear_avg.is_nan() {
323 bear_avg
324 } else {
325 bear_sma
326 };
327
328 if bull_ring_count < w {
329 let is_nan = x_bull.is_nan();
330 bull_ring_nan[bull_ring_count] = is_nan;
331 bull_ring_vals[bull_ring_count] = if is_nan { 0.0 } else { x_bull };
332 if is_nan {
333 bull_nan_cnt += 1;
334 } else {
335 bull_sum += x_bull;
336 }
337 bull_ring_count += 1;
338 } else {
339 let idx = bull_ring_idx;
340 if bull_ring_nan[idx] {
341 bull_nan_cnt -= 1;
342 } else {
343 bull_sum -= bull_ring_vals[idx];
344 }
345 let is_nan = x_bull.is_nan();
346 bull_ring_nan[idx] = is_nan;
347 if is_nan {
348 bull_ring_vals[idx] = 0.0;
349 bull_nan_cnt += 1;
350 } else {
351 bull_ring_vals[idx] = x_bull;
352 bull_sum += x_bull;
353 }
354 bull_ring_idx = if idx + 1 == w { 0 } else { idx + 1 };
355 }
356
357 if bear_ring_count < w {
358 let is_nan = x_bear.is_nan();
359 bear_ring_nan[bear_ring_count] = is_nan;
360 bear_ring_vals[bear_ring_count] = if is_nan { 0.0 } else { x_bear };
361 if is_nan {
362 bear_nan_cnt += 1;
363 } else {
364 bear_sum += x_bear;
365 }
366 bear_ring_count += 1;
367 } else {
368 let idx = bear_ring_idx;
369 if bear_ring_nan[idx] {
370 bear_nan_cnt -= 1;
371 } else {
372 bear_sum -= bear_ring_vals[idx];
373 }
374 let is_nan = x_bear.is_nan();
375 bear_ring_nan[idx] = is_nan;
376 if is_nan {
377 bear_ring_vals[idx] = 0.0;
378 bear_nan_cnt += 1;
379 } else {
380 bear_ring_vals[idx] = x_bear;
381 bear_sum += x_bear;
382 }
383 bear_ring_idx = if idx + 1 == w { 0 } else { idx + 1 };
384 }
385
386 let bull_disp = if bull_ring_count >= w && bull_nan_cnt == 0 {
387 bull_sum / (w as f64)
388 } else {
389 f64::NAN
390 };
391 let bear_disp = if bear_ring_count >= w && bear_nan_cnt == 0 {
392 bear_sum / (w as f64)
393 } else {
394 f64::NAN
395 };
396
397 let prev_os = os;
398 let next_os = if !bear_disp.is_nan() && c > bear_disp {
399 Some(1)
400 } else if !bull_disp.is_nan() && c < bull_disp {
401 Some(-1)
402 } else {
403 os
404 };
405 os = next_os;
406
407 if let (Some(cur), Some(prev)) = (os, prev_os) {
408 if cur == 1 && prev != 1 {
409 ts = Some(bull_disp);
410 } else if cur == -1 && prev != -1 {
411 ts = Some(bear_disp);
412 } else if cur == 1 {
413 if let Some(t) = ts {
414 ts = Some(bull_disp.max(t));
415 }
416 } else if cur == -1 {
417 if let Some(t) = ts {
418 ts = Some(bear_disp.min(t));
419 }
420 }
421 } else {
422 if os == Some(1) {
423 if let Some(t) = ts {
424 ts = Some(bull_disp.max(t));
425 }
426 }
427 if os == Some(-1) {
428 if let Some(t) = ts {
429 ts = Some(bear_disp.min(t));
430 }
431 }
432 }
433
434 if reset_on_cross {
435 if os == Some(1) {
436 if let Some(t) = ts {
437 if c < t {
438 ts = None;
439 }
440 } else if !bear_disp.is_nan() && c > bear_disp {
441 ts = Some(bull_disp);
442 }
443 } else if os == Some(-1) {
444 if let Some(t) = ts {
445 if c > t {
446 ts = None;
447 }
448 } else if !bull_disp.is_nan() && c < bull_disp {
449 ts = Some(bear_disp);
450 }
451 }
452 }
453
454 let show = ts.is_some() || ts_prev.is_some();
455 let ts_nz = if ts.is_some() { ts } else { ts_prev };
456
457 if os == Some(1) && show {
458 upper[i] = f64::NAN;
459 lower[i] = bull_disp;
460 upper_ts[i] = f64::NAN;
461 lower_ts[i] = ts_nz.unwrap_or(f64::NAN);
462 } else if os == Some(-1) && show {
463 upper[i] = bear_disp;
464 lower[i] = f64::NAN;
465 upper_ts[i] = ts_nz.unwrap_or(f64::NAN);
466 lower_ts[i] = f64::NAN;
467 } else {
468 upper[i] = f64::NAN;
469 lower[i] = f64::NAN;
470 upper_ts[i] = f64::NAN;
471 lower_ts[i] = f64::NAN;
472 }
473
474 ts_prev = ts;
475 }
476}
477
478#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
479#[target_feature(enable = "avx2,fma")]
480unsafe fn fvg_ts_avx2(
481 high: &[f64],
482 low: &[f64],
483 close: &[f64],
484 lookback: usize,
485 smoothing_len: usize,
486 reset_on_cross: bool,
487 upper: &mut [f64],
488 lower: &mut [f64],
489 upper_ts: &mut [f64],
490 lower_ts: &mut [f64],
491) {
492 fvg_ts_scalar(
493 high,
494 low,
495 close,
496 lookback,
497 smoothing_len,
498 reset_on_cross,
499 upper,
500 lower,
501 upper_ts,
502 lower_ts,
503 );
504}
505
506#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
507#[target_feature(enable = "avx512f")]
508unsafe fn fvg_ts_avx512(
509 high: &[f64],
510 low: &[f64],
511 close: &[f64],
512 lookback: usize,
513 smoothing_len: usize,
514 reset_on_cross: bool,
515 upper: &mut [f64],
516 lower: &mut [f64],
517 upper_ts: &mut [f64],
518 lower_ts: &mut [f64],
519) {
520 fvg_ts_scalar(
521 high,
522 low,
523 close,
524 lookback,
525 smoothing_len,
526 reset_on_cross,
527 upper,
528 lower,
529 upper_ts,
530 lower_ts,
531 );
532}
533
534#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
535#[inline]
536unsafe fn fvg_ts_simd128(
537 high: &[f64],
538 low: &[f64],
539 close: &[f64],
540 lookback: usize,
541 smoothing_len: usize,
542 reset_on_cross: bool,
543 upper: &mut [f64],
544 lower: &mut [f64],
545 upper_ts: &mut [f64],
546 lower_ts: &mut [f64],
547) {
548 fvg_ts_scalar(
549 high,
550 low,
551 close,
552 lookback,
553 smoothing_len,
554 reset_on_cross,
555 upper,
556 lower,
557 upper_ts,
558 lower_ts,
559 );
560}
561
562#[inline]
563fn fvg_ts_prepare<'a>(
564 input: &'a FvgTrailingStopInput,
565) -> Result<(&'a [f64], &'a [f64], &'a [f64], usize, usize, bool, usize), FvgTrailingStopError> {
566 let (h, l, c) = input.as_slices();
567 if h.is_empty() || l.is_empty() || c.is_empty() {
568 return Err(FvgTrailingStopError::EmptyInputData);
569 }
570 let len = h.len();
571 if len != l.len() || len != c.len() {
572 return Err(FvgTrailingStopError::InvalidPeriod {
573 period: len,
574 data_len: len,
575 });
576 }
577 let first = first_valid_ohlc(h, l, c);
578 if first == usize::MAX {
579 return Err(FvgTrailingStopError::AllValuesNaN);
580 }
581 let lookback = input.get_lookback();
582 let smoothing_len = input.get_smoothing();
583
584 if lookback == 0 {
585 return Err(FvgTrailingStopError::InvalidLookback { lookback });
586 }
587 if smoothing_len == 0 {
588 return Err(FvgTrailingStopError::InvalidSmoothingLength {
589 smoothing: smoothing_len,
590 });
591 }
592
593 let need = 2 + smoothing_len.saturating_sub(1);
594 if len - first < need {
595 return Err(FvgTrailingStopError::NotEnoughValidData {
596 needed: need,
597 valid: len - first,
598 });
599 }
600 let reset_on_cross = input.get_reset_on_cross();
601 Ok((h, l, c, lookback, smoothing_len, reset_on_cross, first))
602}
603
604#[inline]
605fn fvg_ts_compute_into(
606 high: &[f64],
607 low: &[f64],
608 close: &[f64],
609 lookback: usize,
610 smoothing_len: usize,
611 reset_on_cross: bool,
612 upper: &mut [f64],
613 lower: &mut [f64],
614 upper_ts: &mut [f64],
615 lower_ts: &mut [f64],
616 kernel: Kernel,
617) {
618 unsafe {
619 #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
620 {
621 if matches!(kernel, Kernel::Scalar | Kernel::ScalarBatch) {
622 fvg_ts_simd128(
623 high,
624 low,
625 close,
626 lookback,
627 smoothing_len,
628 reset_on_cross,
629 upper,
630 lower,
631 upper_ts,
632 lower_ts,
633 );
634 return;
635 }
636 }
637
638 match kernel {
639 Kernel::Scalar | Kernel::ScalarBatch => fvg_ts_scalar(
640 high,
641 low,
642 close,
643 lookback,
644 smoothing_len,
645 reset_on_cross,
646 upper,
647 lower,
648 upper_ts,
649 lower_ts,
650 ),
651 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
652 Kernel::Avx2 | Kernel::Avx2Batch => fvg_ts_avx2(
653 high,
654 low,
655 close,
656 lookback,
657 smoothing_len,
658 reset_on_cross,
659 upper,
660 lower,
661 upper_ts,
662 lower_ts,
663 ),
664 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
665 Kernel::Avx512 | Kernel::Avx512Batch => fvg_ts_avx512(
666 high,
667 low,
668 close,
669 lookback,
670 smoothing_len,
671 reset_on_cross,
672 upper,
673 lower,
674 upper_ts,
675 lower_ts,
676 ),
677 #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
678 Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
679 fvg_ts_scalar(
680 high,
681 low,
682 close,
683 lookback,
684 smoothing_len,
685 reset_on_cross,
686 upper,
687 lower,
688 upper_ts,
689 lower_ts,
690 )
691 }
692 _ => unreachable!(),
693 }
694 }
695}
696
697#[inline]
698pub fn fvg_trailing_stop(
699 input: &FvgTrailingStopInput,
700) -> Result<FvgTrailingStopOutput, FvgTrailingStopError> {
701 fvg_trailing_stop_with_kernel(input, Kernel::Auto)
702}
703
704pub fn fvg_trailing_stop_with_kernel(
705 input: &FvgTrailingStopInput,
706 kernel: Kernel,
707) -> Result<FvgTrailingStopOutput, FvgTrailingStopError> {
708 let (h, l, c, lookback, smoothing_len, reset_on_cross, first) = fvg_ts_prepare(input)?;
709 let len = h.len();
710 let warm = (first + 2 + smoothing_len.saturating_sub(1)).min(len);
711
712 let mut upper = alloc_with_nan_prefix(len, warm);
713 let mut lower = alloc_with_nan_prefix(len, warm);
714 let mut upper_ts = alloc_with_nan_prefix(len, warm);
715 let mut lower_ts = alloc_with_nan_prefix(len, warm);
716
717 let chosen = match kernel {
718 Kernel::Auto => Kernel::Scalar,
719 k => k,
720 };
721
722 fvg_ts_compute_into(
723 h,
724 l,
725 c,
726 lookback,
727 smoothing_len,
728 reset_on_cross,
729 &mut upper,
730 &mut lower,
731 &mut upper_ts,
732 &mut lower_ts,
733 chosen,
734 );
735
736 Ok(FvgTrailingStopOutput {
737 upper,
738 lower,
739 upper_ts,
740 lower_ts,
741 })
742}
743
744#[inline]
745pub fn fvg_trailing_stop_into(
746 input: &FvgTrailingStopInput,
747 upper: &mut [f64],
748 lower: &mut [f64],
749 upper_ts: &mut [f64],
750 lower_ts: &mut [f64],
751) -> Result<(), FvgTrailingStopError> {
752 fvg_trailing_stop_into_slices(upper, lower, upper_ts, lower_ts, input, Kernel::Auto)
753}
754
755#[inline]
756pub fn fvg_trailing_stop_into_slices(
757 upper: &mut [f64],
758 lower: &mut [f64],
759 upper_ts: &mut [f64],
760 lower_ts: &mut [f64],
761 input: &FvgTrailingStopInput,
762 kernel: Kernel,
763) -> Result<(), FvgTrailingStopError> {
764 let (h, l, c, lookback, smoothing_len, reset_on_cross, first) = fvg_ts_prepare(input)?;
765 let len = h.len();
766 if [upper.len(), lower.len(), upper_ts.len(), lower_ts.len()]
767 .iter()
768 .any(|&n| n != len)
769 {
770 return Err(FvgTrailingStopError::OutputLengthMismatch {
771 expected: len,
772 got: upper
773 .len()
774 .min(lower.len())
775 .min(upper_ts.len())
776 .min(lower_ts.len()),
777 });
778 }
779 let chosen = match kernel {
780 Kernel::Auto => Kernel::Scalar,
781 k => k,
782 };
783
784 fvg_ts_compute_into(
785 h,
786 l,
787 c,
788 lookback,
789 smoothing_len,
790 reset_on_cross,
791 upper,
792 lower,
793 upper_ts,
794 lower_ts,
795 chosen,
796 );
797
798 let warm = (first + 2 + smoothing_len.saturating_sub(1)).min(len);
799 for dst in [upper, lower, upper_ts, lower_ts] {
800 for v in &mut dst[..warm] {
801 *v = f64::NAN;
802 }
803 }
804 Ok(())
805}
806
807#[derive(Clone, Debug)]
808pub struct FvgTsBatchRange {
809 pub lookback: (usize, usize, usize),
810 pub smoothing: (usize, usize, usize),
811 pub reset_on_cross: (bool, bool),
812}
813
814impl Default for FvgTsBatchRange {
815 fn default() -> Self {
816 Self {
817 lookback: (5, 254, 1),
818 smoothing: (9, 9, 0),
819 reset_on_cross: (false, false),
820 }
821 }
822}
823
824#[derive(Clone, Debug)]
825pub struct FvgTsBatchOutput {
826 pub values: Vec<f64>,
827 pub combos: Vec<FvgTrailingStopParams>,
828 pub rows: usize,
829 pub cols: usize,
830}
831
832impl FvgTsBatchOutput {
833 pub fn row_for_params(&self, p: &FvgTrailingStopParams) -> Option<usize> {
834 self.combos.iter().position(|c| {
835 c.unmitigated_fvg_lookback.unwrap_or(5) == p.unmitigated_fvg_lookback.unwrap_or(5)
836 && c.smoothing_length.unwrap_or(9) == p.smoothing_length.unwrap_or(9)
837 && c.reset_on_cross.unwrap_or(false) == p.reset_on_cross.unwrap_or(false)
838 })
839 }
840
841 pub fn values_for(
842 &self,
843 p: &FvgTrailingStopParams,
844 ) -> Option<(&[f64], &[f64], &[f64], &[f64])> {
845 let r = self.row_for_params(p)?;
846 let cols = self.cols;
847 let base = r * 4 * cols;
848 Some((
849 &self.values[base..base + cols],
850 &self.values[base + cols..base + 2 * cols],
851 &self.values[base + 2 * cols..base + 3 * cols],
852 &self.values[base + 3 * cols..base + 4 * cols],
853 ))
854 }
855}
856
857#[inline]
858fn expand_axis_usize(
859 (start, end, step): (usize, usize, usize),
860) -> Result<Vec<usize>, FvgTrailingStopError> {
861 if step == 0 {
862 return Ok(vec![start]);
863 }
864 let mut out = Vec::new();
865 if start <= end {
866 let mut v = start;
867 while v <= end {
868 out.push(v);
869 match v.checked_add(step) {
870 Some(nv) => v = nv,
871 None => break,
872 }
873 }
874 } else {
875 let mut v = start;
876 loop {
877 if v < end {
878 break;
879 }
880 out.push(v);
881 match v.checked_sub(step) {
882 Some(next) => v = next,
883 None => break,
884 }
885 }
886 }
887 if out.is_empty() {
888 return Err(FvgTrailingStopError::InvalidRange { start, end, step });
889 }
890 Ok(out)
891}
892
893#[inline]
894fn expand_grid_ts(r: &FvgTsBatchRange) -> Result<Vec<FvgTrailingStopParams>, FvgTrailingStopError> {
895 let looks = expand_axis_usize(r.lookback)?;
896 let smooths = expand_axis_usize(r.smoothing)?;
897 let mut resets = Vec::new();
898 if r.reset_on_cross.0 {
899 resets.push(false);
900 }
901 if r.reset_on_cross.1 {
902 resets.push(true);
903 }
904 if resets.is_empty() {
905 resets.push(false);
906 }
907
908 let mut v = Vec::with_capacity(
909 looks
910 .len()
911 .saturating_mul(smooths.len())
912 .saturating_mul(resets.len()),
913 );
914 for &lb in &looks {
915 for &sm in &smooths {
916 for &rs in &resets {
917 v.push(FvgTrailingStopParams {
918 unmitigated_fvg_lookback: Some(lb),
919 smoothing_length: Some(sm),
920 reset_on_cross: Some(rs),
921 });
922 }
923 }
924 }
925 if v.is_empty() {
926 return Err(FvgTrailingStopError::InvalidRange {
927 start: r.lookback.0,
928 end: r.lookback.1,
929 step: r.lookback.2,
930 });
931 }
932 Ok(v)
933}
934
935#[inline(always)]
936pub fn fvg_ts_batch_inner_into(
937 h: &[f64],
938 l: &[f64],
939 c: &[f64],
940 sweep: &FvgTsBatchRange,
941 kern: Kernel,
942 parallel: bool,
943 out: &mut [f64],
944) -> Result<Vec<FvgTrailingStopParams>, FvgTrailingStopError> {
945 if !matches!(
946 kern,
947 Kernel::Auto | Kernel::ScalarBatch | Kernel::Avx2Batch | Kernel::Avx512Batch
948 ) {
949 return Err(FvgTrailingStopError::InvalidKernelForBatch(kern));
950 }
951
952 if h.is_empty() || l.is_empty() || c.is_empty() {
953 return Err(FvgTrailingStopError::EmptyInputData);
954 }
955 let len = h.len();
956 if len != l.len() || len != c.len() {
957 return Err(FvgTrailingStopError::InvalidPeriod {
958 period: len,
959 data_len: len,
960 });
961 }
962
963 let combos = expand_grid_ts(sweep)?;
964 let rows = combos.len();
965 let cols = len;
966 let expected = rows
967 .checked_mul(4)
968 .and_then(|x| x.checked_mul(cols))
969 .ok_or_else(|| FvgTrailingStopError::InvalidRange {
970 start: rows,
971 end: cols,
972 step: 4,
973 })?;
974 if out.len() != expected {
975 return Err(FvgTrailingStopError::OutputLengthMismatch {
976 expected,
977 got: out.len(),
978 });
979 }
980
981 let first = first_valid_ohlc(h, l, c);
982 if first == usize::MAX {
983 return Err(FvgTrailingStopError::AllValuesNaN);
984 }
985
986 let mut max_sm = 0usize;
987 for prm in &combos {
988 let look = prm.unmitigated_fvg_lookback.unwrap_or(5);
989 if look == 0 {
990 return Err(FvgTrailingStopError::InvalidLookback { lookback: look });
991 }
992 let sm = prm.smoothing_length.unwrap_or(9);
993 if sm == 0 {
994 return Err(FvgTrailingStopError::InvalidSmoothingLength { smoothing: sm });
995 }
996 if sm > max_sm {
997 max_sm = sm;
998 }
999 }
1000 let need = 2 + max_sm.saturating_sub(1);
1001 if len - first < need {
1002 return Err(FvgTrailingStopError::NotEnoughValidData {
1003 needed: need,
1004 valid: len - first,
1005 });
1006 }
1007
1008 let _chosen = match kern {
1009 Kernel::Auto => detect_best_batch_kernel(),
1010 k => k,
1011 };
1012
1013 let mut bull_cand = vec![f64::NAN; len];
1014 let mut bear_cand = vec![f64::NAN; len];
1015 if len >= 3 {
1016 for i in 2..len {
1017 let hi2 = h[i - 2];
1018 let lo2 = l[i - 2];
1019 let cm1 = c[i - 1];
1020 let hi = h[i];
1021 let lo = l[i];
1022 if !(hi2.is_nan() || lo2.is_nan() || cm1.is_nan()) {
1023 if lo > hi2 && cm1 > hi2 {
1024 bull_cand[i] = hi2;
1025 }
1026 if hi < lo2 && cm1 < lo2 {
1027 bear_cand[i] = lo2;
1028 }
1029 }
1030 }
1031 }
1032
1033 let mut pref_sum_close = vec![0.0f64; len + 1];
1034 let mut pref_nan_count = vec![0usize; len + 1];
1035 for i in 0..len {
1036 let is_nan = c[i].is_nan();
1037 pref_sum_close[i + 1] = pref_sum_close[i] + if is_nan { 0.0 } else { c[i] };
1038 pref_nan_count[i + 1] = pref_nan_count[i] + if is_nan { 1 } else { 0 };
1039 }
1040
1041 let do_one = |row: usize, dst: &mut [f64]| {
1042 let look = combos[row].unmitigated_fvg_lookback.unwrap();
1043 let sm = combos[row].smoothing_length.unwrap_or(9);
1044 let rst = combos[row].reset_on_cross.unwrap_or(false);
1045 let warm = (first + 2 + sm.saturating_sub(1)).min(cols);
1046 let (u_block, rest) = dst.split_at_mut(cols);
1047 let (l_block, rest) = rest.split_at_mut(cols);
1048 let (uts_block, lts_block) = rest.split_at_mut(cols);
1049
1050 let mut bull_buf = vec![0.0f64; look];
1051 let mut bear_buf = vec![0.0f64; look];
1052 let mut bull_len = 0usize;
1053 let mut bear_len = 0usize;
1054 let mut last_bull_non_na: Option<usize> = None;
1055 let mut last_bear_non_na: Option<usize> = None;
1056
1057 let mut bull_ring_vals = vec![0.0f64; sm];
1058 let mut bull_ring_nan = vec![false; sm];
1059 let mut bear_ring_vals = vec![0.0f64; sm];
1060 let mut bear_ring_nan = vec![false; sm];
1061 let mut bull_sum = 0.0f64;
1062 let mut bear_sum = 0.0f64;
1063 let mut bull_nan_cnt = 0usize;
1064 let mut bear_nan_cnt = 0usize;
1065 let mut bull_ring_count = 0usize;
1066 let mut bear_ring_count = 0usize;
1067 let mut bull_ring_idx = 0usize;
1068 let mut bear_ring_idx = 0usize;
1069
1070 let mut os: Option<i8> = None;
1071 let mut ts: Option<f64> = None;
1072 let mut ts_prev: Option<f64> = None;
1073
1074 for i in 0..cols {
1075 let bc = bull_cand[i];
1076 if !bc.is_nan() {
1077 if bull_len < look {
1078 bull_buf[bull_len] = bc;
1079 bull_len += 1;
1080 } else {
1081 for k in 1..look {
1082 bull_buf[k - 1] = bull_buf[k];
1083 }
1084 bull_buf[look - 1] = bc;
1085 }
1086 }
1087 let ec = bear_cand[i];
1088 if !ec.is_nan() {
1089 if bear_len < look {
1090 bear_buf[bear_len] = ec;
1091 bear_len += 1;
1092 } else {
1093 for k in 1..look {
1094 bear_buf[k - 1] = bear_buf[k];
1095 }
1096 bear_buf[look - 1] = ec;
1097 }
1098 }
1099
1100 let price = c[i];
1101 let mut new_bull_len = 0usize;
1102 let mut bull_acc = 0.0f64;
1103 for k in 0..bull_len {
1104 let v = bull_buf[k];
1105 if price >= v {
1106 bull_buf[new_bull_len] = v;
1107 new_bull_len += 1;
1108 bull_acc += v;
1109 }
1110 }
1111 bull_len = new_bull_len;
1112
1113 let mut new_bear_len = 0usize;
1114 let mut bear_acc = 0.0f64;
1115 for k in 0..bear_len {
1116 let v = bear_buf[k];
1117 if price <= v {
1118 bear_buf[new_bear_len] = v;
1119 new_bear_len += 1;
1120 bear_acc += v;
1121 }
1122 }
1123 bear_len = new_bear_len;
1124
1125 let bull_avg = if bull_len > 0 {
1126 bull_acc / (bull_len as f64)
1127 } else {
1128 f64::NAN
1129 };
1130 let bear_avg = if bear_len > 0 {
1131 bear_acc / (bear_len as f64)
1132 } else {
1133 f64::NAN
1134 };
1135 if !bull_avg.is_nan() {
1136 last_bull_non_na = Some(i);
1137 }
1138 if !bear_avg.is_nan() {
1139 last_bear_non_na = Some(i);
1140 }
1141
1142 let bull_bs = if bull_avg.is_nan() {
1143 match last_bull_non_na {
1144 Some(last) => ((i - last).max(1)).min(sm),
1145 None => 1,
1146 }
1147 } else {
1148 1
1149 };
1150 let bear_bs = if bear_avg.is_nan() {
1151 match last_bear_non_na {
1152 Some(last) => ((i - last).max(1)).min(sm),
1153 None => 1,
1154 }
1155 } else {
1156 1
1157 };
1158
1159 let bull_sma = if bull_avg.is_nan() && (i + 1) >= bull_bs {
1160 let s = pref_sum_close[i + 1] - pref_sum_close[i + 1 - bull_bs];
1161 let nans = pref_nan_count[i + 1] - pref_nan_count[i + 1 - bull_bs];
1162 if nans == 0 {
1163 s / (bull_bs as f64)
1164 } else {
1165 f64::NAN
1166 }
1167 } else {
1168 f64::NAN
1169 };
1170 let bear_sma = if bear_avg.is_nan() && (i + 1) >= bear_bs {
1171 let s = pref_sum_close[i + 1] - pref_sum_close[i + 1 - bear_bs];
1172 let nans = pref_nan_count[i + 1] - pref_nan_count[i + 1 - bear_bs];
1173 if nans == 0 {
1174 s / (bear_bs as f64)
1175 } else {
1176 f64::NAN
1177 }
1178 } else {
1179 f64::NAN
1180 };
1181
1182 let x_bull = if !bull_avg.is_nan() {
1183 bull_avg
1184 } else {
1185 bull_sma
1186 };
1187 let x_bear = if !bear_avg.is_nan() {
1188 bear_avg
1189 } else {
1190 bear_sma
1191 };
1192
1193 if bull_ring_count < sm {
1194 let is_nan = x_bull.is_nan();
1195 bull_ring_nan[bull_ring_count] = is_nan;
1196 bull_ring_vals[bull_ring_count] = if is_nan { 0.0 } else { x_bull };
1197 if is_nan {
1198 bull_nan_cnt += 1
1199 } else {
1200 bull_sum += x_bull
1201 }
1202 bull_ring_count += 1;
1203 } else {
1204 let idx = bull_ring_idx;
1205 if bull_ring_nan[idx] {
1206 bull_nan_cnt -= 1
1207 } else {
1208 bull_sum -= bull_ring_vals[idx]
1209 }
1210 let is_nan = x_bull.is_nan();
1211 bull_ring_nan[idx] = is_nan;
1212 if is_nan {
1213 bull_ring_vals[idx] = 0.0;
1214 bull_nan_cnt += 1
1215 } else {
1216 bull_ring_vals[idx] = x_bull;
1217 bull_sum += x_bull
1218 }
1219 bull_ring_idx = if idx + 1 == sm { 0 } else { idx + 1 };
1220 }
1221
1222 if bear_ring_count < sm {
1223 let is_nan = x_bear.is_nan();
1224 bear_ring_nan[bear_ring_count] = is_nan;
1225 bear_ring_vals[bear_ring_count] = if is_nan { 0.0 } else { x_bear };
1226 if is_nan {
1227 bear_nan_cnt += 1
1228 } else {
1229 bear_sum += x_bear
1230 }
1231 bear_ring_count += 1;
1232 } else {
1233 let idx = bear_ring_idx;
1234 if bear_ring_nan[idx] {
1235 bear_nan_cnt -= 1
1236 } else {
1237 bear_sum -= bear_ring_vals[idx]
1238 }
1239 let is_nan = x_bear.is_nan();
1240 bear_ring_nan[idx] = is_nan;
1241 if is_nan {
1242 bear_ring_vals[idx] = 0.0;
1243 bear_nan_cnt += 1
1244 } else {
1245 bear_ring_vals[idx] = x_bear;
1246 bear_sum += x_bear
1247 }
1248 bear_ring_idx = if idx + 1 == sm { 0 } else { idx + 1 };
1249 }
1250
1251 let bull_disp = if bull_ring_count >= sm && bull_nan_cnt == 0 {
1252 bull_sum / (sm as f64)
1253 } else {
1254 f64::NAN
1255 };
1256 let bear_disp = if bear_ring_count >= sm && bear_nan_cnt == 0 {
1257 bear_sum / (sm as f64)
1258 } else {
1259 f64::NAN
1260 };
1261
1262 let prev_os = os;
1263 let next_os = if !bear_disp.is_nan() && price > bear_disp {
1264 Some(1)
1265 } else if !bull_disp.is_nan() && price < bull_disp {
1266 Some(-1)
1267 } else {
1268 os
1269 };
1270 os = next_os;
1271
1272 if let (Some(cur), Some(prev)) = (os, prev_os) {
1273 if cur == 1 && prev != 1 {
1274 ts = Some(bull_disp);
1275 } else if cur == -1 && prev != -1 {
1276 ts = Some(bear_disp);
1277 } else if cur == 1 {
1278 if let Some(t) = ts {
1279 ts = Some(bull_disp.max(t));
1280 }
1281 } else if cur == -1 {
1282 if let Some(t) = ts {
1283 ts = Some(bear_disp.min(t));
1284 }
1285 }
1286 } else {
1287 if os == Some(1) {
1288 if let Some(t) = ts {
1289 ts = Some(bull_disp.max(t));
1290 }
1291 }
1292 if os == Some(-1) {
1293 if let Some(t) = ts {
1294 ts = Some(bear_disp.min(t));
1295 }
1296 }
1297 }
1298
1299 if rst {
1300 if os == Some(1) {
1301 if let Some(t) = ts {
1302 if price < t {
1303 ts = None;
1304 }
1305 } else if !bear_disp.is_nan() && price > bear_disp {
1306 ts = Some(bull_disp);
1307 }
1308 } else if os == Some(-1) {
1309 if let Some(t) = ts {
1310 if price > t {
1311 ts = None;
1312 }
1313 } else if !bull_disp.is_nan() && price < bull_disp {
1314 ts = Some(bear_disp);
1315 }
1316 }
1317 }
1318
1319 let show = ts.is_some() || ts_prev.is_some();
1320 let ts_nz = if ts.is_some() { ts } else { ts_prev };
1321 if os == Some(1) && show {
1322 u_block[i] = f64::NAN;
1323 l_block[i] = bull_disp;
1324 uts_block[i] = f64::NAN;
1325 lts_block[i] = ts_nz.unwrap_or(f64::NAN);
1326 } else if os == Some(-1) && show {
1327 u_block[i] = bear_disp;
1328 l_block[i] = f64::NAN;
1329 uts_block[i] = ts_nz.unwrap_or(f64::NAN);
1330 lts_block[i] = f64::NAN;
1331 } else {
1332 u_block[i] = f64::NAN;
1333 l_block[i] = f64::NAN;
1334 uts_block[i] = f64::NAN;
1335 lts_block[i] = f64::NAN;
1336 }
1337 ts_prev = ts;
1338 }
1339
1340 for buf in [u_block, l_block, uts_block, lts_block] {
1341 for v in &mut buf[..warm] {
1342 *v = f64::NAN;
1343 }
1344 }
1345 };
1346
1347 #[cfg(not(target_arch = "wasm32"))]
1348 if parallel {
1349 use rayon::prelude::*;
1350 out.par_chunks_mut(4 * cols)
1351 .enumerate()
1352 .for_each(|(row, dst)| do_one(row, dst));
1353 } else {
1354 out.chunks_mut(4 * cols)
1355 .enumerate()
1356 .for_each(|(row, dst)| do_one(row, dst));
1357 }
1358
1359 #[cfg(target_arch = "wasm32")]
1360 out.chunks_mut(4 * cols)
1361 .enumerate()
1362 .for_each(|(row, dst)| do_one(row, dst));
1363
1364 Ok(combos)
1365}
1366
1367pub fn fvg_trailing_stop_batch_with_kernel(
1368 high: &[f64],
1369 low: &[f64],
1370 close: &[f64],
1371 sweep: &FvgTsBatchRange,
1372 kernel: Kernel,
1373) -> Result<FvgTsBatchOutput, FvgTrailingStopError> {
1374 if high.is_empty() || low.is_empty() || close.is_empty() {
1375 return Err(FvgTrailingStopError::EmptyInputData);
1376 }
1377 let len = high.len();
1378 if len != low.len() || len != close.len() {
1379 return Err(FvgTrailingStopError::InvalidPeriod {
1380 period: len,
1381 data_len: len,
1382 });
1383 }
1384 if !matches!(
1385 kernel,
1386 Kernel::Auto | Kernel::ScalarBatch | Kernel::Avx2Batch | Kernel::Avx512Batch
1387 ) {
1388 return Err(FvgTrailingStopError::InvalidKernelForBatch(kernel));
1389 }
1390
1391 let combos = expand_grid_ts(sweep)?;
1392 let rows = combos.len();
1393 let cols = len;
1394
1395 let first = first_valid_ohlc(high, low, close);
1396 if first == usize::MAX {
1397 return Err(FvgTrailingStopError::AllValuesNaN);
1398 }
1399 let mut max_sm = 0usize;
1400 let mut warms = Vec::with_capacity(4 * rows);
1401 for prm in &combos {
1402 let look = prm.unmitigated_fvg_lookback.unwrap_or(5);
1403 if look == 0 {
1404 return Err(FvgTrailingStopError::InvalidLookback { lookback: look });
1405 }
1406 let sm = prm.smoothing_length.unwrap_or(9);
1407 if sm == 0 {
1408 return Err(FvgTrailingStopError::InvalidSmoothingLength { smoothing: sm });
1409 }
1410 if sm > max_sm {
1411 max_sm = sm;
1412 }
1413 let w = (first + 2 + sm.saturating_sub(1)).min(cols);
1414 warms.extend_from_slice(&[w, w, w, w]);
1415 }
1416 let need = 2 + max_sm.saturating_sub(1);
1417 if cols - first < need {
1418 return Err(FvgTrailingStopError::NotEnoughValidData {
1419 needed: need,
1420 valid: cols - first,
1421 });
1422 }
1423
1424 let rows4 = rows
1425 .checked_mul(4)
1426 .ok_or_else(|| FvgTrailingStopError::InvalidRange {
1427 start: rows,
1428 end: 4,
1429 step: 1,
1430 })?;
1431 let mut buf_mu = make_uninit_matrix(rows4, cols);
1432 init_matrix_prefixes(&mut buf_mu, cols, &warms);
1433
1434 let flat: &mut [f64] =
1435 unsafe { core::slice::from_raw_parts_mut(buf_mu.as_mut_ptr() as *mut f64, buf_mu.len()) };
1436 let used = fvg_ts_batch_inner_into(high, low, close, sweep, kernel, true, flat)?;
1437
1438 let values = unsafe {
1439 Vec::from_raw_parts(
1440 buf_mu.as_mut_ptr() as *mut f64,
1441 buf_mu.len(),
1442 buf_mu.capacity(),
1443 )
1444 };
1445 core::mem::forget(buf_mu);
1446
1447 Ok(FvgTsBatchOutput {
1448 values,
1449 combos: used,
1450 rows,
1451 cols,
1452 })
1453}
1454
1455#[derive(Clone, Debug, Default)]
1456pub struct FvgTsBatchBuilder {
1457 range: FvgTsBatchRange,
1458 kernel: Kernel,
1459}
1460
1461impl FvgTsBatchBuilder {
1462 pub fn new() -> Self {
1463 Self::default()
1464 }
1465
1466 pub fn lookback_range(mut self, start: usize, end: usize, step: usize) -> Self {
1467 self.range.lookback = (start, end, step);
1468 self
1469 }
1470
1471 pub fn smoothing_range(mut self, start: usize, end: usize, step: usize) -> Self {
1472 self.range.smoothing = (start, end, step);
1473 self
1474 }
1475
1476 pub fn reset_toggle(mut self, include_false: bool, include_true: bool) -> Self {
1477 self.range.reset_on_cross = (include_false, include_true);
1478 self
1479 }
1480
1481 pub fn kernel(mut self, k: Kernel) -> Self {
1482 self.kernel = k;
1483 self
1484 }
1485
1486 pub fn apply_candles(self, c: &Candles) -> Result<FvgTsBatchOutput, FvgTrailingStopError> {
1487 fvg_trailing_stop_batch_with_kernel(&c.high, &c.low, &c.close, &self.range, self.kernel)
1488 }
1489
1490 pub fn apply_slices(
1491 self,
1492 high: &[f64],
1493 low: &[f64],
1494 close: &[f64],
1495 ) -> Result<FvgTsBatchOutput, FvgTrailingStopError> {
1496 fvg_trailing_stop_batch_with_kernel(high, low, close, &self.range, self.kernel)
1497 }
1498
1499 pub fn with_default_candles(c: &Candles) -> Result<FvgTsBatchOutput, FvgTrailingStopError> {
1500 FvgTsBatchBuilder::new()
1501 .kernel(Kernel::Auto)
1502 .apply_candles(c)
1503 }
1504
1505 pub fn with_default_slices(
1506 high: &[f64],
1507 low: &[f64],
1508 close: &[f64],
1509 ) -> Result<FvgTsBatchOutput, FvgTrailingStopError> {
1510 FvgTsBatchBuilder::new()
1511 .kernel(Kernel::Auto)
1512 .apply_slices(high, low, close)
1513 }
1514}
1515
1516use core::cmp::Ordering;
1517use std::cmp::Reverse;
1518use std::collections::BinaryHeap;
1519
1520#[inline]
1521fn f64_to_bits_pos(v: f64) -> u64 {
1522 debug_assert!(v.is_finite() && v >= 0.0);
1523 v.to_bits()
1524}
1525
1526#[derive(Copy, Clone, Debug)]
1527struct Slot {
1528 val: f64,
1529 alive: bool,
1530 stamp: u32,
1531}
1532
1533#[derive(Copy, Clone, Eq, PartialEq, Debug)]
1534struct HeapItem {
1535 bits: u64,
1536 slot: u32,
1537 stamp: u32,
1538 seq: u32,
1539}
1540impl Ord for HeapItem {
1541 #[inline]
1542 fn cmp(&self, other: &Self) -> Ordering {
1543 self.bits
1544 .cmp(&other.bits)
1545 .then(self.seq.cmp(&other.seq))
1546 .then(self.slot.cmp(&other.slot))
1547 }
1548}
1549impl PartialOrd for HeapItem {
1550 #[inline]
1551 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1552 Some(self.cmp(other))
1553 }
1554}
1555
1556pub struct FvgTrailingStopStream {
1557 lookback: usize,
1558 smoothing_len: usize,
1559 reset_on_cross: bool,
1560
1561 bull_slots: Vec<Slot>,
1562 bull_head: usize,
1563 bull_occ: usize,
1564 bull_sum: f64,
1565 bull_cnt: u32,
1566 bull_heap: BinaryHeap<HeapItem>,
1567 bull_seq: u32,
1568
1569 bear_slots: Vec<Slot>,
1570 bear_head: usize,
1571 bear_occ: usize,
1572 bear_sum: f64,
1573 bear_cnt: u32,
1574 bear_heap: BinaryHeap<Reverse<HeapItem>>,
1575 bear_seq: u32,
1576
1577 last_bull_non_na: Option<usize>,
1578 last_bear_non_na: Option<usize>,
1579
1580 xbull_vals: Vec<f64>,
1581 xbull_idx: usize,
1582 xbull_filled: usize,
1583 xbull_sum: f64,
1584 xbull_nan: u32,
1585
1586 xbear_vals: Vec<f64>,
1587 xbear_idx: usize,
1588 xbear_filled: usize,
1589 xbear_sum: f64,
1590 xbear_nan: u32,
1591
1592 pref_sum_ring: Vec<f64>,
1593 pref_nan_ring: Vec<u32>,
1594 pref_idx: usize,
1595 pref_sum_total: f64,
1596 pref_nan_total: u32,
1597
1598 os: Option<i8>,
1599 ts: Option<f64>,
1600 ts_prev: Option<f64>,
1601 bar_count: usize,
1602
1603 hi_m2: f64,
1604 hi_m1: f64,
1605 lo_m2: f64,
1606 lo_m1: f64,
1607 cl_m1: f64,
1608
1609 inv_w: f64,
1610}
1611
1612impl FvgTrailingStopStream {
1613 pub fn try_new(params: FvgTrailingStopParams) -> Result<Self, FvgTrailingStopError> {
1614 let lookback = params.unmitigated_fvg_lookback.unwrap_or(5);
1615 let smoothing_len = params.smoothing_length.unwrap_or(9);
1616 if lookback == 0 {
1617 return Err(FvgTrailingStopError::InvalidLookback { lookback });
1618 }
1619 if smoothing_len == 0 {
1620 return Err(FvgTrailingStopError::InvalidSmoothingLength {
1621 smoothing: smoothing_len,
1622 });
1623 }
1624
1625 let mut bull_slots = Vec::with_capacity(lookback);
1626 bull_slots.resize(
1627 lookback,
1628 Slot {
1629 val: f64::NAN,
1630 alive: false,
1631 stamp: 0,
1632 },
1633 );
1634 let mut bear_slots = Vec::with_capacity(lookback);
1635 bear_slots.resize(
1636 lookback,
1637 Slot {
1638 val: f64::NAN,
1639 alive: false,
1640 stamp: 0,
1641 },
1642 );
1643
1644 let mut xbull_vals = Vec::with_capacity(smoothing_len);
1645 xbull_vals.resize(smoothing_len, f64::NAN);
1646 let mut xbear_vals = Vec::with_capacity(smoothing_len);
1647 xbear_vals.resize(smoothing_len, f64::NAN);
1648
1649 let mut pref_sum_ring = Vec::with_capacity(smoothing_len + 1);
1650 pref_sum_ring.resize(smoothing_len + 1, 0.0);
1651 let mut pref_nan_ring = Vec::with_capacity(smoothing_len + 1);
1652 pref_nan_ring.resize(smoothing_len + 1, 0);
1653
1654 Ok(Self {
1655 lookback,
1656 smoothing_len,
1657 reset_on_cross: params.reset_on_cross.unwrap_or(false),
1658
1659 bull_slots,
1660 bull_head: 0,
1661 bull_occ: 0,
1662 bull_sum: 0.0,
1663 bull_cnt: 0,
1664 bull_heap: BinaryHeap::new(),
1665 bull_seq: 0,
1666
1667 bear_slots,
1668 bear_head: 0,
1669 bear_occ: 0,
1670 bear_sum: 0.0,
1671 bear_cnt: 0,
1672 bear_heap: BinaryHeap::new(),
1673 bear_seq: 0,
1674
1675 last_bull_non_na: None,
1676 last_bear_non_na: None,
1677
1678 xbull_vals,
1679 xbull_idx: 0,
1680 xbull_filled: 0,
1681 xbull_sum: 0.0,
1682 xbull_nan: 0,
1683
1684 xbear_vals,
1685 xbear_idx: 0,
1686 xbear_filled: 0,
1687 xbear_sum: 0.0,
1688 xbear_nan: 0,
1689
1690 pref_sum_ring,
1691 pref_nan_ring,
1692 pref_idx: 0,
1693 pref_sum_total: 0.0,
1694 pref_nan_total: 0,
1695
1696 os: None,
1697 ts: None,
1698 ts_prev: None,
1699 bar_count: 0,
1700
1701 hi_m2: f64::NAN,
1702 hi_m1: f64::NAN,
1703 lo_m2: f64::NAN,
1704 lo_m1: f64::NAN,
1705 cl_m1: f64::NAN,
1706
1707 inv_w: 1.0 / (smoothing_len as f64),
1708 })
1709 }
1710
1711 #[inline(always)]
1712 fn bull_push(&mut self, v: f64) {
1713 if v.is_nan() {
1714 return;
1715 }
1716 let idx = self.bull_head;
1717 if self.bull_occ == self.lookback {
1718 let s = &mut self.bull_slots[idx];
1719 if s.alive {
1720 self.bull_sum -= s.val;
1721 self.bull_cnt -= 1;
1722 s.alive = false;
1723 }
1724 } else {
1725 self.bull_occ += 1;
1726 }
1727 self.bull_seq = self.bull_seq.wrapping_add(1);
1728 let stamp = self.bull_seq;
1729
1730 self.bull_slots[idx] = Slot {
1731 val: v,
1732 alive: true,
1733 stamp,
1734 };
1735 self.bull_sum += v;
1736 self.bull_cnt += 1;
1737
1738 self.bull_heap.push(HeapItem {
1739 bits: f64_to_bits_pos(v),
1740 slot: idx as u32,
1741 stamp,
1742 seq: stamp,
1743 });
1744
1745 self.bull_head = if idx + 1 == self.lookback { 0 } else { idx + 1 };
1746 }
1747
1748 #[inline(always)]
1749 fn bear_push(&mut self, v: f64) {
1750 if v.is_nan() {
1751 return;
1752 }
1753 let idx = self.bear_head;
1754 if self.bear_occ == self.lookback {
1755 let s = &mut self.bear_slots[idx];
1756 if s.alive {
1757 self.bear_sum -= s.val;
1758 self.bear_cnt -= 1;
1759 s.alive = false;
1760 }
1761 } else {
1762 self.bear_occ += 1;
1763 }
1764 self.bear_seq = self.bear_seq.wrapping_add(1);
1765 let stamp = self.bear_seq;
1766
1767 self.bear_slots[idx] = Slot {
1768 val: v,
1769 alive: true,
1770 stamp,
1771 };
1772 self.bear_sum += v;
1773 self.bear_cnt += 1;
1774
1775 let item = HeapItem {
1776 bits: f64_to_bits_pos(v),
1777 slot: idx as u32,
1778 stamp,
1779 seq: stamp,
1780 };
1781 self.bear_heap.push(Reverse(item));
1782
1783 self.bear_head = if idx + 1 == self.lookback { 0 } else { idx + 1 };
1784 }
1785
1786 #[inline(always)]
1787 fn bull_sweep(&mut self, close: f64) {
1788 while let Some(top) = self.bull_heap.peek().copied() {
1789 let v = f64::from_bits(top.bits);
1790 if !(v > close) {
1791 break;
1792 }
1793 self.bull_heap.pop();
1794 let idx = top.slot as usize;
1795 if idx < self.bull_slots.len() {
1796 let s = &mut self.bull_slots[idx];
1797 if s.alive && s.stamp == top.stamp {
1798 s.alive = false;
1799 self.bull_sum -= s.val;
1800 self.bull_cnt -= 1;
1801 }
1802 }
1803 }
1804 }
1805
1806 #[inline(always)]
1807 fn bear_sweep(&mut self, close: f64) {
1808 while let Some(Reverse(top)) = self.bear_heap.peek().copied() {
1809 let v = f64::from_bits(top.bits);
1810 if !(v < close) {
1811 break;
1812 }
1813 self.bear_heap.pop();
1814 let idx = top.slot as usize;
1815 if idx < self.bear_slots.len() {
1816 let s = &mut self.bear_slots[idx];
1817 if s.alive && s.stamp == top.stamp {
1818 s.alive = false;
1819 self.bear_sum -= s.val;
1820 self.bear_cnt -= 1;
1821 }
1822 }
1823 }
1824 }
1825
1826 #[inline(always)]
1827 fn push_x_and_smooth(
1828 vals: &mut [f64],
1829 idx: &mut usize,
1830 filled: &mut usize,
1831 sum: &mut f64,
1832 nan: &mut u32,
1833 w: usize,
1834 inv_w: f64,
1835 x: f64,
1836 ) -> f64 {
1837 let pos = *idx;
1838
1839 if *filled == w {
1840 let old = vals[pos];
1841 if old.is_nan() {
1842 *nan -= 1;
1843 } else {
1844 *sum -= old;
1845 }
1846 } else {
1847 *filled += 1;
1848 }
1849
1850 vals[pos] = x;
1851 if x.is_nan() {
1852 *nan += 1;
1853 } else {
1854 *sum += x;
1855 }
1856
1857 *idx = if pos + 1 == w { 0 } else { pos + 1 };
1858
1859 if *filled == w && *nan == 0 {
1860 *sum * inv_w
1861 } else {
1862 f64::NAN
1863 }
1864 }
1865
1866 #[inline(always)]
1867 fn prefix_add_close(
1868 pref_sum_ring: &mut [f64],
1869 pref_nan_ring: &mut [u32],
1870 pref_idx: &mut usize,
1871 pref_sum_total: &mut f64,
1872 pref_nan_total: &mut u32,
1873 w: usize,
1874 close: f64,
1875 ) {
1876 let add = if close.is_nan() { 0.0 } else { close };
1877 let add_nan = if close.is_nan() { 1 } else { 0 };
1878 *pref_sum_total += add;
1879 *pref_nan_total += add_nan;
1880
1881 let ring_len = w + 1;
1882 let next = if *pref_idx + 1 == ring_len {
1883 0
1884 } else {
1885 *pref_idx + 1
1886 };
1887 pref_sum_ring[next] = *pref_sum_total;
1888 pref_nan_ring[next] = *pref_nan_total;
1889 *pref_idx = next;
1890 }
1891
1892 #[inline(always)]
1893 fn prefix_last_bs(
1894 pref_sum_ring: &[f64],
1895 pref_nan_ring: &[u32],
1896 pref_idx: usize,
1897 w: usize,
1898 bs: usize,
1899 ) -> (f64, u32) {
1900 debug_assert!(bs >= 1 && bs <= w);
1901 let ring_len = w + 1;
1902 let prev = (pref_idx + ring_len - bs) % ring_len;
1903 let s = pref_sum_ring[pref_idx] - pref_sum_ring[prev];
1904 let nans = pref_nan_ring[pref_idx] - pref_nan_ring[prev];
1905 (s, nans)
1906 }
1907
1908 pub fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64, f64, f64)> {
1909 Self::prefix_add_close(
1910 &mut self.pref_sum_ring,
1911 &mut self.pref_nan_ring,
1912 &mut self.pref_idx,
1913 &mut self.pref_sum_total,
1914 &mut self.pref_nan_total,
1915 self.smoothing_len,
1916 close,
1917 );
1918
1919 if self.bar_count >= 2
1920 && self.hi_m2.is_finite()
1921 && self.lo_m2.is_finite()
1922 && self.cl_m1.is_finite()
1923 {
1924 if low > self.hi_m2 && self.cl_m1 > self.hi_m2 {
1925 self.bull_push(self.hi_m2);
1926 }
1927 if high < self.lo_m2 && self.cl_m1 < self.lo_m2 {
1928 self.bear_push(self.lo_m2);
1929 }
1930 }
1931
1932 self.bull_sweep(close);
1933 self.bear_sweep(close);
1934
1935 let bull_avg = if self.bull_cnt > 0 {
1936 self.bull_sum / (self.bull_cnt as f64)
1937 } else {
1938 f64::NAN
1939 };
1940 let bear_avg = if self.bear_cnt > 0 {
1941 self.bear_sum / (self.bear_cnt as f64)
1942 } else {
1943 f64::NAN
1944 };
1945 if !bull_avg.is_nan() {
1946 self.last_bull_non_na = Some(self.bar_count);
1947 }
1948 if !bear_avg.is_nan() {
1949 self.last_bear_non_na = Some(self.bar_count);
1950 }
1951
1952 let bull_bs = if bull_avg.is_nan() {
1953 match self.last_bull_non_na {
1954 Some(last) => ((self.bar_count - last).max(1)).min(self.smoothing_len),
1955 None => 1,
1956 }
1957 } else {
1958 1
1959 };
1960 let bear_bs = if bear_avg.is_nan() {
1961 match self.last_bear_non_na {
1962 Some(last) => ((self.bar_count - last).max(1)).min(self.smoothing_len),
1963 None => 1,
1964 }
1965 } else {
1966 1
1967 };
1968
1969 let bull_sma = if bull_avg.is_nan() {
1970 let (s, nans) = Self::prefix_last_bs(
1971 &self.pref_sum_ring,
1972 &self.pref_nan_ring,
1973 self.pref_idx,
1974 self.smoothing_len,
1975 bull_bs,
1976 );
1977 if nans == 0 {
1978 s / (bull_bs as f64)
1979 } else {
1980 f64::NAN
1981 }
1982 } else {
1983 f64::NAN
1984 };
1985 let bear_sma = if bear_avg.is_nan() {
1986 let (s, nans) = Self::prefix_last_bs(
1987 &self.pref_sum_ring,
1988 &self.pref_nan_ring,
1989 self.pref_idx,
1990 self.smoothing_len,
1991 bear_bs,
1992 );
1993 if nans == 0 {
1994 s / (bear_bs as f64)
1995 } else {
1996 f64::NAN
1997 }
1998 } else {
1999 f64::NAN
2000 };
2001
2002 let x_bull = if !bull_avg.is_nan() {
2003 bull_avg
2004 } else {
2005 bull_sma
2006 };
2007 let x_bear = if !bear_avg.is_nan() {
2008 bear_avg
2009 } else {
2010 bear_sma
2011 };
2012
2013 let bull_disp = Self::push_x_and_smooth(
2014 &mut self.xbull_vals,
2015 &mut self.xbull_idx,
2016 &mut self.xbull_filled,
2017 &mut self.xbull_sum,
2018 &mut self.xbull_nan,
2019 self.smoothing_len,
2020 self.inv_w,
2021 x_bull,
2022 );
2023 let bear_disp = Self::push_x_and_smooth(
2024 &mut self.xbear_vals,
2025 &mut self.xbear_idx,
2026 &mut self.xbear_filled,
2027 &mut self.xbear_sum,
2028 &mut self.xbear_nan,
2029 self.smoothing_len,
2030 self.inv_w,
2031 x_bear,
2032 );
2033
2034 let prev_os = self.os;
2035 let next_os = if !bear_disp.is_nan() && close > bear_disp {
2036 Some(1)
2037 } else if !bull_disp.is_nan() && close < bull_disp {
2038 Some(-1)
2039 } else {
2040 self.os
2041 };
2042 self.os = next_os;
2043
2044 if let (Some(cur), Some(prev)) = (self.os, prev_os) {
2045 if cur == 1 && prev != 1 {
2046 self.ts = Some(bull_disp);
2047 } else if cur == -1 && prev != -1 {
2048 self.ts = Some(bear_disp);
2049 } else if cur == 1 {
2050 if let Some(t) = self.ts {
2051 self.ts = Some(bull_disp.max(t));
2052 }
2053 } else if cur == -1 {
2054 if let Some(t) = self.ts {
2055 self.ts = Some(bear_disp.min(t));
2056 }
2057 }
2058 } else {
2059 if self.os == Some(1) {
2060 if let Some(t) = self.ts {
2061 self.ts = Some(bull_disp.max(t));
2062 }
2063 }
2064 if self.os == Some(-1) {
2065 if let Some(t) = self.ts {
2066 self.ts = Some(bear_disp.min(t));
2067 }
2068 }
2069 }
2070
2071 if self.reset_on_cross {
2072 if self.os == Some(1) {
2073 if let Some(t) = self.ts {
2074 if close < t {
2075 self.ts = None;
2076 }
2077 } else if !bear_disp.is_nan() && close > bear_disp {
2078 self.ts = Some(bull_disp);
2079 }
2080 } else if self.os == Some(-1) {
2081 if let Some(t) = self.ts {
2082 if close > t {
2083 self.ts = None;
2084 }
2085 } else if !bull_disp.is_nan() && close < bull_disp {
2086 self.ts = Some(bear_disp);
2087 }
2088 }
2089 }
2090
2091 let show = self.ts.is_some() || self.ts_prev.is_some();
2092 let ts_nz = self.ts.or(self.ts_prev);
2093
2094 let (mut upper, mut lower, mut upper_ts, mut lower_ts) =
2095 (f64::NAN, f64::NAN, f64::NAN, f64::NAN);
2096
2097 if self.os == Some(1) && show {
2098 lower = bull_disp;
2099 lower_ts = ts_nz.unwrap_or(f64::NAN);
2100 } else if self.os == Some(-1) && show {
2101 upper = bear_disp;
2102 upper_ts = ts_nz.unwrap_or(f64::NAN);
2103 }
2104
2105 self.ts_prev = self.ts;
2106
2107 self.hi_m2 = self.hi_m1;
2108 self.hi_m1 = high;
2109 self.lo_m2 = self.lo_m1;
2110 self.lo_m1 = low;
2111 self.cl_m1 = close;
2112
2113 self.bar_count += 1;
2114
2115 if self.bar_count >= self.smoothing_len + 2 {
2116 Some((upper, lower, upper_ts, lower_ts))
2117 } else {
2118 None
2119 }
2120 }
2121}
2122
2123#[cfg(feature = "python")]
2124#[pyfunction(name = "fvg_trailing_stop")]
2125#[pyo3(signature = (high, low, close, unmitigated_fvg_lookback, smoothing_length, reset_on_cross, kernel=None))]
2126pub fn fvg_trailing_stop_py<'py>(
2127 py: Python<'py>,
2128 high: PyReadonlyArray1<'py, f64>,
2129 low: PyReadonlyArray1<'py, f64>,
2130 close: PyReadonlyArray1<'py, f64>,
2131 unmitigated_fvg_lookback: usize,
2132 smoothing_length: usize,
2133 reset_on_cross: bool,
2134 kernel: Option<&str>,
2135) -> PyResult<(
2136 Bound<'py, PyArray1<f64>>,
2137 Bound<'py, PyArray1<f64>>,
2138 Bound<'py, PyArray1<f64>>,
2139 Bound<'py, PyArray1<f64>>,
2140)> {
2141 use numpy::IntoPyArray;
2142 let (h, l, c) = (high.as_slice()?, low.as_slice()?, close.as_slice()?);
2143 let kern = validate_kernel(kernel, false)?;
2144 let params = FvgTrailingStopParams {
2145 unmitigated_fvg_lookback: Some(unmitigated_fvg_lookback),
2146 smoothing_length: Some(smoothing_length),
2147 reset_on_cross: Some(reset_on_cross),
2148 };
2149 let input = FvgTrailingStopInput::from_slices(h, l, c, params);
2150 let out = py
2151 .allow_threads(|| fvg_trailing_stop_with_kernel(&input, kern))
2152 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2153 Ok((
2154 out.upper.into_pyarray(py),
2155 out.lower.into_pyarray(py),
2156 out.upper_ts.into_pyarray(py),
2157 out.lower_ts.into_pyarray(py),
2158 ))
2159}
2160
2161#[cfg(all(feature = "python", feature = "cuda"))]
2162use crate::utilities::dlpack_cuda::{make_device_array_py, DeviceArrayF32Py};
2163#[cfg(all(feature = "python", feature = "cuda"))]
2164#[cfg(all(feature = "python", feature = "cuda"))]
2165#[pyfunction(name = "fvg_trailing_stop_cuda_batch_dev")]
2166#[pyo3(signature = (high, low, close, lookback_range, smoothing_range, reset_toggle, device_id=0))]
2167pub fn fvg_trailing_stop_cuda_batch_dev_py(
2168 py: Python<'_>,
2169 high: PyReadonlyArray1<'_, f32>,
2170 low: PyReadonlyArray1<'_, f32>,
2171 close: PyReadonlyArray1<'_, f32>,
2172 lookback_range: (usize, usize, usize),
2173 smoothing_range: (usize, usize, usize),
2174 reset_toggle: (bool, bool),
2175 device_id: usize,
2176) -> PyResult<(
2177 DeviceArrayF32Py,
2178 DeviceArrayF32Py,
2179 DeviceArrayF32Py,
2180 DeviceArrayF32Py,
2181)> {
2182 use crate::cuda::cuda_available;
2183 if !cuda_available() {
2184 return Err(PyValueError::new_err("CUDA not available"));
2185 }
2186 let (h, l, c) = (high.as_slice()?, low.as_slice()?, close.as_slice()?);
2187 let sweep = FvgTsBatchRange {
2188 lookback: lookback_range,
2189 smoothing: smoothing_range,
2190 reset_on_cross: reset_toggle,
2191 };
2192 let (u, lwr, uts, lts) = py.allow_threads(|| {
2193 let cuda = crate::cuda::fvg_trailing_stop_wrapper::CudaFvgTs::new(device_id)
2194 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2195 let batch = cuda
2196 .fvg_ts_batch_dev(h, l, c, &sweep)
2197 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2198 Ok::<_, PyErr>((batch.upper, batch.lower, batch.upper_ts, batch.lower_ts))
2199 })?;
2200 let upper_dev = make_device_array_py(device_id, u)?;
2201 let lower_dev = make_device_array_py(device_id, lwr)?;
2202 let upper_ts_dev = make_device_array_py(device_id, uts)?;
2203 let lower_ts_dev = make_device_array_py(device_id, lts)?;
2204 Ok((upper_dev, lower_dev, upper_ts_dev, lower_ts_dev))
2205}
2206
2207#[cfg(all(feature = "python", feature = "cuda"))]
2208#[pyfunction(name = "fvg_trailing_stop_cuda_many_series_one_param_dev")]
2209#[pyo3(signature = (high_tm, low_tm, close_tm, cols, rows, unmitigated_fvg_lookback, smoothing_length, reset_on_cross, device_id=0))]
2210pub fn fvg_trailing_stop_cuda_many_series_one_param_dev_py(
2211 py: Python<'_>,
2212 high_tm: PyReadonlyArray1<'_, f32>,
2213 low_tm: PyReadonlyArray1<'_, f32>,
2214 close_tm: PyReadonlyArray1<'_, f32>,
2215 cols: usize,
2216 rows: usize,
2217 unmitigated_fvg_lookback: usize,
2218 smoothing_length: usize,
2219 reset_on_cross: bool,
2220 device_id: usize,
2221) -> PyResult<(
2222 DeviceArrayF32Py,
2223 DeviceArrayF32Py,
2224 DeviceArrayF32Py,
2225 DeviceArrayF32Py,
2226)> {
2227 use crate::cuda::cuda_available;
2228 if !cuda_available() {
2229 return Err(PyValueError::new_err("CUDA not available"));
2230 }
2231 let (h, l, c) = (
2232 high_tm.as_slice()?,
2233 low_tm.as_slice()?,
2234 close_tm.as_slice()?,
2235 );
2236 if h.len() != l.len() || h.len() != c.len() || h.len() != cols * rows {
2237 return Err(PyValueError::new_err(
2238 "time-major arrays must match cols*rows",
2239 ));
2240 }
2241 let params = FvgTrailingStopParams {
2242 unmitigated_fvg_lookback: Some(unmitigated_fvg_lookback),
2243 smoothing_length: Some(smoothing_length),
2244 reset_on_cross: Some(reset_on_cross),
2245 };
2246 let (u, lw, uts, lts) = py.allow_threads(|| {
2247 let cuda = crate::cuda::fvg_trailing_stop_wrapper::CudaFvgTs::new(device_id)
2248 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2249 cuda.fvg_ts_many_series_one_param_time_major_dev(h, l, c, cols, rows, ¶ms)
2250 .map_err(|e| PyValueError::new_err(e.to_string()))
2251 })?;
2252 let upper_dev = make_device_array_py(device_id, u)?;
2253 let lower_dev = make_device_array_py(device_id, lw)?;
2254 let upper_ts_dev = make_device_array_py(device_id, uts)?;
2255 let lower_ts_dev = make_device_array_py(device_id, lts)?;
2256 Ok((upper_dev, lower_dev, upper_ts_dev, lower_ts_dev))
2257}
2258
2259#[cfg(feature = "python")]
2260#[pyfunction(name = "fvg_trailing_stop_batch")]
2261#[pyo3(signature = (high, low, close, lookback_range, smoothing_range, reset_toggle, kernel=None))]
2262pub fn fvg_trailing_stop_batch_py<'py>(
2263 py: Python<'py>,
2264 high: PyReadonlyArray1<'py, f64>,
2265 low: PyReadonlyArray1<'py, f64>,
2266 close: PyReadonlyArray1<'py, f64>,
2267 lookback_range: (usize, usize, usize),
2268 smoothing_range: (usize, usize, usize),
2269 reset_toggle: (bool, bool),
2270 kernel: Option<&str>,
2271) -> PyResult<Bound<'py, PyDict>> {
2272 use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
2273
2274 let (h, l, c) = (high.as_slice()?, low.as_slice()?, close.as_slice()?);
2275 let sweep = FvgTsBatchRange {
2276 lookback: lookback_range,
2277 smoothing: smoothing_range,
2278 reset_on_cross: reset_toggle,
2279 };
2280 let kern = validate_kernel(kernel, true)?;
2281
2282 let combos = expand_grid_ts(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
2283 let rows = combos.len();
2284 let cols = h.len();
2285 let rows4 = rows
2286 .checked_mul(4)
2287 .ok_or_else(|| PyValueError::new_err("rows*4 overflow"))?;
2288 let total = rows4
2289 .checked_mul(cols)
2290 .ok_or_else(|| PyValueError::new_err("rows*4*cols overflow"))?;
2291
2292 let flat = unsafe { PyArray1::<f64>::new(py, [total], false) };
2293 let flat_mut = unsafe { flat.as_slice_mut()? };
2294
2295 py.allow_threads(|| fvg_ts_batch_inner_into(h, l, c, &sweep, kern, true, flat_mut))
2296 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2297
2298 let dict = PyDict::new(py);
2299
2300 dict.set_item("values", flat.reshape((rows4, cols))?)?;
2301 dict.set_item(
2302 "lookbacks",
2303 combos
2304 .iter()
2305 .map(|p| p.unmitigated_fvg_lookback.unwrap() as u64)
2306 .collect::<Vec<_>>()
2307 .into_pyarray(py),
2308 )?;
2309 dict.set_item(
2310 "smoothings",
2311 combos
2312 .iter()
2313 .map(|p| p.smoothing_length.unwrap() as u64)
2314 .collect::<Vec<_>>()
2315 .into_pyarray(py),
2316 )?;
2317 dict.set_item(
2318 "resets",
2319 combos
2320 .iter()
2321 .map(|p| p.reset_on_cross.unwrap_or(false))
2322 .collect::<Vec<_>>()
2323 .into_pyarray(py),
2324 )?;
2325 Ok(dict)
2326}
2327
2328#[cfg(feature = "python")]
2329#[pyclass]
2330pub struct FvgTrailingStopStreamPy {
2331 stream: FvgTrailingStopStream,
2332}
2333
2334#[cfg(feature = "python")]
2335#[pymethods]
2336impl FvgTrailingStopStreamPy {
2337 #[new]
2338 fn new(
2339 unmitigated_fvg_lookback: usize,
2340 smoothing_length: usize,
2341 reset_on_cross: bool,
2342 ) -> PyResult<Self> {
2343 let params = FvgTrailingStopParams {
2344 unmitigated_fvg_lookback: Some(unmitigated_fvg_lookback),
2345 smoothing_length: Some(smoothing_length),
2346 reset_on_cross: Some(reset_on_cross),
2347 };
2348 let stream = FvgTrailingStopStream::try_new(params)
2349 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2350 Ok(FvgTrailingStopStreamPy { stream })
2351 }
2352
2353 fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64, f64, f64)> {
2354 self.stream.update(high, low, close)
2355 }
2356}
2357
2358#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2359#[wasm_bindgen]
2360pub fn fvg_ts_alloc(len: usize) -> *mut f64 {
2361 let mut v = Vec::<f64>::with_capacity(len);
2362 let p = v.as_mut_ptr();
2363 std::mem::forget(v);
2364 p
2365}
2366
2367#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2368#[wasm_bindgen]
2369pub fn fvg_ts_free(ptr: *mut f64, len: usize) {
2370 unsafe {
2371 let _ = Vec::from_raw_parts(ptr, len, len);
2372 }
2373}
2374
2375#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2376#[derive(Serialize, Deserialize)]
2377pub struct FvgTsJsOutput {
2378 pub values: Vec<f64>,
2379 pub rows: usize,
2380 pub cols: usize,
2381}
2382
2383#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2384#[derive(Serialize, Deserialize)]
2385pub struct FvgTsBatchJsOutput {
2386 pub values: Vec<f64>,
2387 pub combos: Vec<FvgTrailingStopParams>,
2388 pub rows: usize,
2389 pub cols: usize,
2390}
2391
2392#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2393#[wasm_bindgen(js_name = "fvgTrailingStop")]
2394pub fn fvg_trailing_stop_js(
2395 high: &[f64],
2396 low: &[f64],
2397 close: &[f64],
2398 unmitigated_fvg_lookback: usize,
2399 smoothing_length: usize,
2400 reset_on_cross: bool,
2401) -> Result<JsValue, JsValue> {
2402 if high.is_empty() || low.is_empty() || close.is_empty() {
2403 return Err(JsValue::from_str(
2404 "fvg_trailing_stop: Input data slice is empty.",
2405 ));
2406 }
2407
2408 let params = FvgTrailingStopParams {
2409 unmitigated_fvg_lookback: Some(unmitigated_fvg_lookback),
2410 smoothing_length: Some(smoothing_length),
2411 reset_on_cross: Some(reset_on_cross),
2412 };
2413 let input = FvgTrailingStopInput::from_slices(high, low, close, params);
2414
2415 let (h, low_in, c, lookback, smoothing_len, reset, first) =
2416 fvg_ts_prepare(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
2417 let len = h.len();
2418 let warm = (first + 2 + smoothing_len.saturating_sub(1)).min(len);
2419
2420 let mut buf_mu = make_uninit_matrix(4, len);
2421 init_matrix_prefixes(&mut buf_mu, len, &[warm, warm, warm, warm]);
2422 let out: &mut [f64] =
2423 unsafe { core::slice::from_raw_parts_mut(buf_mu.as_mut_ptr() as *mut f64, buf_mu.len()) };
2424 let (first_half, second_half) = out.split_at_mut(2 * len);
2425 let (u, l) = first_half.split_at_mut(len);
2426 let (uts, lts) = second_half.split_at_mut(len);
2427
2428 let chosen = Kernel::Scalar;
2429 fvg_ts_compute_into(
2430 h,
2431 low_in,
2432 c,
2433 lookback,
2434 smoothing_len,
2435 reset,
2436 u,
2437 l,
2438 uts,
2439 lts,
2440 chosen,
2441 );
2442 for v in &mut u[..warm] {
2443 *v = f64::NAN;
2444 }
2445 for v in &mut l[..warm] {
2446 *v = f64::NAN;
2447 }
2448 for v in &mut uts[..warm] {
2449 *v = f64::NAN;
2450 }
2451 for v in &mut lts[..warm] {
2452 *v = f64::NAN;
2453 }
2454
2455 let obj = js_sys::Object::new();
2456 let upper_arr = js_sys::Array::from_iter(u.iter().map(|&v| JsValue::from_f64(v)));
2457 let lower_arr = js_sys::Array::from_iter(l.iter().map(|&v| JsValue::from_f64(v)));
2458 let upper_ts_arr = js_sys::Array::from_iter(uts.iter().map(|&v| JsValue::from_f64(v)));
2459 let lower_ts_arr = js_sys::Array::from_iter(lts.iter().map(|&v| JsValue::from_f64(v)));
2460
2461 js_sys::Reflect::set(&obj, &JsValue::from_str("upper"), &upper_arr)?;
2462 js_sys::Reflect::set(&obj, &JsValue::from_str("lower"), &lower_arr)?;
2463 js_sys::Reflect::set(&obj, &JsValue::from_str("upperTs"), &upper_ts_arr)?;
2464 js_sys::Reflect::set(&obj, &JsValue::from_str("lowerTs"), &lower_ts_arr)?;
2465
2466 Ok(obj.into())
2467}
2468
2469#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2470#[wasm_bindgen]
2471pub fn fvg_trailing_stop_into_flat(
2472 high_ptr: *const f64,
2473 low_ptr: *const f64,
2474 close_ptr: *const f64,
2475 out_ptr: *mut f64,
2476 len: usize,
2477 unmitigated_fvg_lookback: usize,
2478 smoothing_length: usize,
2479 reset_on_cross: bool,
2480) -> Result<(), JsValue> {
2481 if [
2482 high_ptr as usize,
2483 low_ptr as usize,
2484 close_ptr as usize,
2485 out_ptr as usize,
2486 ]
2487 .iter()
2488 .any(|&p| p == 0)
2489 {
2490 return Err(JsValue::from_str("null pointer"));
2491 }
2492 unsafe {
2493 let h = core::slice::from_raw_parts(high_ptr, len);
2494 let l = core::slice::from_raw_parts(low_ptr, len);
2495 let c = core::slice::from_raw_parts(close_ptr, len);
2496 let out = core::slice::from_raw_parts_mut(out_ptr, 4 * len);
2497 let (first_half, second_half) = out.split_at_mut(2 * len);
2498 let (u, lw) = first_half.split_at_mut(len);
2499 let (uts, lts) = second_half.split_at_mut(len);
2500 let params = FvgTrailingStopParams {
2501 unmitigated_fvg_lookback: Some(unmitigated_fvg_lookback),
2502 smoothing_length: Some(smoothing_length),
2503 reset_on_cross: Some(reset_on_cross),
2504 };
2505 let input = FvgTrailingStopInput::from_slices(h, l, c, params);
2506 fvg_trailing_stop_into_slices(u, lw, uts, lts, &input, Kernel::Auto)
2507 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2508 }
2509 Ok(())
2510}
2511
2512#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2513#[wasm_bindgen(js_name = "fvgTrailingStopBatch")]
2514pub fn fvg_trailing_stop_batch_js(
2515 high: &[f64],
2516 low: &[f64],
2517 close: &[f64],
2518 lookback_start: usize,
2519 lookback_end: usize,
2520 lookback_step: usize,
2521 smoothing_start: usize,
2522 smoothing_end: usize,
2523 smoothing_step: usize,
2524 reset_include_false: bool,
2525 reset_include_true: bool,
2526) -> Result<JsValue, JsValue> {
2527 if high.is_empty() || low.is_empty() || close.is_empty() {
2528 return Err(JsValue::from_str(
2529 "fvg_trailing_stop: Input data slice is empty.",
2530 ));
2531 }
2532 let cols = high.len();
2533 if cols != low.len() || cols != close.len() {
2534 let e = FvgTrailingStopError::InvalidPeriod {
2535 period: cols,
2536 data_len: cols,
2537 };
2538 return Err(JsValue::from_str(&e.to_string()));
2539 }
2540 let sweep = FvgTsBatchRange {
2541 lookback: (lookback_start, lookback_end, lookback_step),
2542 smoothing: (smoothing_start, smoothing_end, smoothing_step),
2543 reset_on_cross: (reset_include_false, reset_include_true),
2544 };
2545 let combos = expand_grid_ts(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2546 let rows = combos.len();
2547
2548 let first = first_valid_ohlc(high, low, close);
2549 if first == usize::MAX {
2550 let e = FvgTrailingStopError::AllValuesNaN;
2551 return Err(JsValue::from_str(&e.to_string()));
2552 }
2553 let mut max_sm = 0usize;
2554 let rows4 = rows
2555 .checked_mul(4)
2556 .ok_or_else(|| JsValue::from_str("rows*4 overflow"))?;
2557 let mut buf_mu = make_uninit_matrix(rows4, cols);
2558 let mut warms = Vec::with_capacity(rows4);
2559 for prm in &combos {
2560 let look = prm.unmitigated_fvg_lookback.unwrap_or(5);
2561 if look == 0 {
2562 let e = FvgTrailingStopError::InvalidLookback { lookback: look };
2563 return Err(JsValue::from_str(&e.to_string()));
2564 }
2565 let sm = prm.smoothing_length.unwrap_or(9);
2566 if sm == 0 {
2567 let e = FvgTrailingStopError::InvalidSmoothingLength { smoothing: sm };
2568 return Err(JsValue::from_str(&e.to_string()));
2569 }
2570 if sm > max_sm {
2571 max_sm = sm;
2572 }
2573 let w = (first + 2 + sm.saturating_sub(1)).min(cols);
2574 warms.extend_from_slice(&[w, w, w, w]);
2575 }
2576 let need = 2 + max_sm.saturating_sub(1);
2577 if cols - first < need {
2578 let e = FvgTrailingStopError::NotEnoughValidData {
2579 needed: need,
2580 valid: cols - first,
2581 };
2582 return Err(JsValue::from_str(&e.to_string()));
2583 }
2584 init_matrix_prefixes(&mut buf_mu, cols, &warms);
2585
2586 let flat: &mut [f64] =
2587 unsafe { core::slice::from_raw_parts_mut(buf_mu.as_mut_ptr() as *mut f64, buf_mu.len()) };
2588 fvg_ts_batch_inner_into(
2589 high,
2590 low,
2591 close,
2592 &sweep,
2593 detect_best_batch_kernel(),
2594 false,
2595 flat,
2596 )
2597 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2598
2599 let values = unsafe {
2600 Vec::from_raw_parts(
2601 buf_mu.as_mut_ptr() as *mut f64,
2602 buf_mu.len(),
2603 buf_mu.capacity(),
2604 )
2605 };
2606 core::mem::forget(buf_mu);
2607
2608 let out = FvgTsBatchJsOutput {
2609 values,
2610 combos,
2611 rows,
2612 cols,
2613 };
2614 serde_wasm_bindgen::to_value(&out)
2615 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2616}
2617
2618#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2619#[wasm_bindgen(js_name = "fvgTrailingStopAlloc")]
2620pub fn fvg_trailing_stop_alloc_js(size: usize) -> *mut f64 {
2621 let mut vec = Vec::<f64>::with_capacity(size * 4);
2622 let ptr = vec.as_mut_ptr();
2623 std::mem::forget(vec);
2624 ptr
2625}
2626
2627#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2628#[wasm_bindgen(js_name = "fvgTrailingStopFree")]
2629pub fn fvg_trailing_stop_free_js(ptr: *mut f64, size: usize) {
2630 unsafe {
2631 let _ = Vec::from_raw_parts(ptr, size * 4, size * 4);
2632 }
2633}
2634
2635#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2636#[wasm_bindgen(js_name = "fvgTrailingStopZeroCopy")]
2637pub fn fvg_trailing_stop_zero_copy_js(
2638 high: &[f64],
2639 low: &[f64],
2640 close: &[f64],
2641 unmitigated_fvg_lookback: usize,
2642 smoothing_length: usize,
2643 reset_on_cross: bool,
2644 ptr: *mut f64,
2645) -> Result<JsValue, JsValue> {
2646 if ptr.is_null() {
2647 return Err(JsValue::from_str("null pointer"));
2648 }
2649 let len = high.len();
2650
2651 let (upper, lower, upper_ts, lower_ts) = unsafe {
2652 (
2653 std::slice::from_raw_parts_mut(ptr, len),
2654 std::slice::from_raw_parts_mut(ptr.add(len), len),
2655 std::slice::from_raw_parts_mut(ptr.add(len * 2), len),
2656 std::slice::from_raw_parts_mut(ptr.add(len * 3), len),
2657 )
2658 };
2659
2660 for i in 0..len {
2661 upper[i] = f64::NAN;
2662 lower[i] = f64::NAN;
2663 upper_ts[i] = f64::NAN;
2664 lower_ts[i] = f64::NAN;
2665 }
2666
2667 let params = FvgTrailingStopParams {
2668 unmitigated_fvg_lookback: Some(unmitigated_fvg_lookback),
2669 smoothing_length: Some(smoothing_length),
2670 reset_on_cross: Some(reset_on_cross),
2671 };
2672
2673 let input = FvgTrailingStopInput {
2674 data: FvgTrailingStopData::Slices { high, low, close },
2675 params,
2676 };
2677
2678 fvg_trailing_stop_into_slices(upper, lower, upper_ts, lower_ts, &input, Kernel::Auto)
2679 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2680
2681 let obj = js_sys::Object::new();
2682 let upper_arr = unsafe { js_sys::Float64Array::view(upper) };
2683 let lower_arr = unsafe { js_sys::Float64Array::view(lower) };
2684 let upper_ts_arr = unsafe { js_sys::Float64Array::view(upper_ts) };
2685 let lower_ts_arr = unsafe { js_sys::Float64Array::view(lower_ts) };
2686
2687 js_sys::Reflect::set(&obj, &JsValue::from_str("upper"), &upper_arr)?;
2688 js_sys::Reflect::set(&obj, &JsValue::from_str("lower"), &lower_arr)?;
2689 js_sys::Reflect::set(&obj, &JsValue::from_str("upperTs"), &upper_ts_arr)?;
2690 js_sys::Reflect::set(&obj, &JsValue::from_str("lowerTs"), &lower_ts_arr)?;
2691
2692 Ok(obj.into())
2693}
2694
2695#[derive(Copy, Clone, Debug)]
2696pub struct FvgTrailingStopBuilder {
2697 unmitigated_fvg_lookback: Option<usize>,
2698 smoothing_length: Option<usize>,
2699 reset_on_cross: Option<bool>,
2700 kernel: Kernel,
2701}
2702
2703impl Default for FvgTrailingStopBuilder {
2704 fn default() -> Self {
2705 Self {
2706 unmitigated_fvg_lookback: None,
2707 smoothing_length: None,
2708 reset_on_cross: None,
2709 kernel: Kernel::Auto,
2710 }
2711 }
2712}
2713
2714impl FvgTrailingStopBuilder {
2715 pub fn new() -> Self {
2716 Self::default()
2717 }
2718
2719 pub fn lookback(mut self, n: usize) -> Self {
2720 self.unmitigated_fvg_lookback = Some(n);
2721 self
2722 }
2723
2724 pub fn smoothing(mut self, n: usize) -> Self {
2725 self.smoothing_length = Some(n);
2726 self
2727 }
2728
2729 pub fn reset_on_cross(mut self, reset: bool) -> Self {
2730 self.reset_on_cross = Some(reset);
2731 self
2732 }
2733
2734 pub fn kernel(mut self, k: Kernel) -> Self {
2735 self.kernel = k;
2736 self
2737 }
2738
2739 pub fn apply(&self, candles: &Candles) -> Result<FvgTrailingStopOutput, FvgTrailingStopError> {
2740 let params = FvgTrailingStopParams {
2741 unmitigated_fvg_lookback: self.unmitigated_fvg_lookback,
2742 smoothing_length: self.smoothing_length,
2743 reset_on_cross: self.reset_on_cross,
2744 };
2745 let input = FvgTrailingStopInput::from_candles(candles, params);
2746 fvg_trailing_stop_with_kernel(&input, self.kernel)
2747 }
2748
2749 pub fn apply_slice(
2750 &self,
2751 high: &[f64],
2752 low: &[f64],
2753 close: &[f64],
2754 ) -> Result<FvgTrailingStopOutput, FvgTrailingStopError> {
2755 let params = FvgTrailingStopParams {
2756 unmitigated_fvg_lookback: self.unmitigated_fvg_lookback,
2757 smoothing_length: self.smoothing_length,
2758 reset_on_cross: self.reset_on_cross,
2759 };
2760 let input = FvgTrailingStopInput::from_slices(high, low, close, params);
2761 fvg_trailing_stop_with_kernel(&input, self.kernel)
2762 }
2763
2764 pub fn into_stream(self) -> Result<FvgTrailingStopStream, FvgTrailingStopError> {
2765 let params = FvgTrailingStopParams {
2766 unmitigated_fvg_lookback: self.unmitigated_fvg_lookback,
2767 smoothing_length: self.smoothing_length,
2768 reset_on_cross: self.reset_on_cross,
2769 };
2770 FvgTrailingStopStream::try_new(params)
2771 }
2772}
2773
2774#[cfg(test)]
2775mod tests {
2776 use super::*;
2777 use crate::utilities::data_loader::read_candles_from_csv;
2778
2779 macro_rules! skip_if_unsupported {
2780 ($kernel:expr, $test_name:expr) => {
2781 #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
2782 if matches!(
2783 $kernel,
2784 Kernel::Avx2 | Kernel::Avx512 | Kernel::Avx2Batch | Kernel::Avx512Batch
2785 ) {
2786 eprintln!("Skipping {} - AVX not available", $test_name);
2787 return Ok(());
2788 }
2789 };
2790 }
2791
2792 fn check_fvg_ts_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2793 skip_if_unsupported!(kernel, test_name);
2794 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2795 let candles = read_candles_from_csv(file_path)?;
2796
2797 let params = FvgTrailingStopParams {
2798 unmitigated_fvg_lookback: Some(5),
2799 smoothing_length: Some(9),
2800 reset_on_cross: Some(false),
2801 };
2802 let input = FvgTrailingStopInput::from_candles(&candles, params);
2803 let result = fvg_trailing_stop_with_kernel(&input, kernel)?;
2804
2805 let expected_lower = 55643.00;
2806 let expected_lower_ts = 60223.33333333;
2807 let tolerance = 0.01;
2808
2809 let n = result.lower.len();
2810 if n >= 5 {
2811 for i in (n - 5)..n {
2812 if !result.lower[i].is_nan() {
2813 let diff = (result.lower[i] - expected_lower).abs();
2814 assert!(
2815 diff < tolerance,
2816 "[{}] Lower value mismatch at {}: expected {}, got {}, diff {}",
2817 test_name,
2818 i,
2819 expected_lower,
2820 result.lower[i],
2821 diff
2822 );
2823 }
2824 if !result.lower_ts[i].is_nan() {
2825 let diff = (result.lower_ts[i] - expected_lower_ts).abs();
2826 assert!(
2827 diff < tolerance,
2828 "[{}] Lower TS value mismatch at {}: expected {}, got {}, diff {}",
2829 test_name,
2830 i,
2831 expected_lower_ts,
2832 result.lower_ts[i],
2833 diff
2834 );
2835 }
2836 }
2837 }
2838 Ok(())
2839 }
2840
2841 fn check_fvg_ts_default_candles(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2842 skip_if_unsupported!(kernel, test_name);
2843 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2844 let candles = read_candles_from_csv(file_path)?;
2845
2846 let input = FvgTrailingStopInput::with_default_candles(&candles);
2847 let output = fvg_trailing_stop_with_kernel(&input, kernel)?;
2848 assert_eq!(output.upper.len(), candles.close.len());
2849 assert_eq!(output.lower.len(), candles.close.len());
2850 assert_eq!(output.upper_ts.len(), candles.close.len());
2851 assert_eq!(output.lower_ts.len(), candles.close.len());
2852
2853 Ok(())
2854 }
2855
2856 fn check_fvg_ts_empty_input(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2857 skip_if_unsupported!(kernel, test_name);
2858 let empty: [f64; 0] = [];
2859 let params = FvgTrailingStopParams::default();
2860 let input = FvgTrailingStopInput::from_slices(&empty, &empty, &empty, params);
2861 let res = fvg_trailing_stop_with_kernel(&input, kernel);
2862 assert!(
2863 matches!(res, Err(FvgTrailingStopError::EmptyInputData)),
2864 "[{}] Should fail with empty input",
2865 test_name
2866 );
2867 Ok(())
2868 }
2869
2870 fn check_fvg_ts_all_nan(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2871 skip_if_unsupported!(kernel, test_name);
2872 let nan_data = vec![f64::NAN; 100];
2873 let params = FvgTrailingStopParams::default();
2874 let input = FvgTrailingStopInput::from_slices(&nan_data, &nan_data, &nan_data, params);
2875 let res = fvg_trailing_stop_with_kernel(&input, kernel);
2876 assert!(
2877 matches!(res, Err(FvgTrailingStopError::AllValuesNaN)),
2878 "[{}] Should fail with all NaN",
2879 test_name
2880 );
2881 Ok(())
2882 }
2883
2884 fn check_fvg_ts_partial_nan(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2885 skip_if_unsupported!(kernel, test_name);
2886 let mut high = vec![100.0; 50];
2887 let mut low = vec![95.0; 50];
2888 let mut close = vec![97.0; 50];
2889
2890 for i in 10..20 {
2891 high[i] = f64::NAN;
2892 low[i] = f64::NAN;
2893 close[i] = f64::NAN;
2894 }
2895
2896 let params = FvgTrailingStopParams::default();
2897 let input = FvgTrailingStopInput::from_slices(&high, &low, &close, params);
2898 let result = fvg_trailing_stop_with_kernel(&input, kernel)?;
2899
2900 assert_eq!(result.upper.len(), 50);
2901 assert_eq!(result.lower.len(), 50);
2902 Ok(())
2903 }
2904
2905 fn check_fvg_ts_streaming(test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2906 let params = FvgTrailingStopParams::default();
2907 let mut stream = FvgTrailingStopStream::try_new(params)?;
2908
2909 let test_data = vec![
2910 (100.0, 95.0, 97.0),
2911 (101.0, 96.0, 98.0),
2912 (102.0, 97.0, 99.0),
2913 (103.0, 98.0, 100.0),
2914 (104.0, 99.0, 101.0),
2915 (105.0, 100.0, 102.0),
2916 (106.0, 101.0, 103.0),
2917 (107.0, 102.0, 104.0),
2918 (108.0, 103.0, 105.0),
2919 (109.0, 104.0, 106.0),
2920 (110.0, 105.0, 107.0),
2921 (111.0, 106.0, 108.0),
2922 ];
2923
2924 for (h, l, c) in test_data {
2925 stream.update(h, l, c);
2926 }
2927
2928 Ok(())
2929 }
2930
2931 fn check_fvg_ts_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2932 skip_if_unsupported!(kernel, test_name);
2933 #[cfg(debug_assertions)]
2934 {
2935 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2936 let candles = read_candles_from_csv(file_path)?;
2937
2938 let params = FvgTrailingStopParams::default();
2939 let input = FvgTrailingStopInput::from_candles(&candles, params);
2940 let out = fvg_trailing_stop_with_kernel(&input, kernel)?;
2941
2942 for (name, row) in [
2943 ("upper", &out.upper),
2944 ("lower", &out.lower),
2945 ("upper_ts", &out.upper_ts),
2946 ("lower_ts", &out.lower_ts),
2947 ] {
2948 for (i, &v) in row.iter().enumerate() {
2949 if v.is_nan() {
2950 continue;
2951 }
2952 let b = v.to_bits();
2953 assert_ne!(
2954 b, 0x1111_1111_1111_1111,
2955 "[{}] alloc poison in {} at {}",
2956 test_name, name, i
2957 );
2958 assert_ne!(
2959 b, 0x2222_2222_2222_2222,
2960 "[{}] matrix poison in {} at {}",
2961 test_name, name, i
2962 );
2963 assert_ne!(
2964 b, 0x3333_3333_3333_3333,
2965 "[{}] uninit poison in {} at {}",
2966 test_name, name, i
2967 );
2968 }
2969 }
2970 }
2971 Ok(())
2972 }
2973
2974 fn check_fvg_ts_batch_default(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2975 skip_if_unsupported!(kernel, test_name);
2976 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2977 let candles = read_candles_from_csv(file_path)?;
2978
2979 let output = FvgTsBatchBuilder::new()
2980 .kernel(kernel)
2981 .apply_candles(&candles)?;
2982
2983 assert_eq!(output.combos.len(), 1);
2984 assert_eq!(output.rows, 1);
2985 assert_eq!(output.cols, candles.close.len());
2986
2987 Ok(())
2988 }
2989
2990 fn check_fvg_ts_batch_sweep(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2991 skip_if_unsupported!(kernel, test_name);
2992 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2993 let candles = read_candles_from_csv(file_path)?;
2994
2995 let output = FvgTsBatchBuilder::new()
2996 .kernel(kernel)
2997 .lookback_range(3, 7, 2)
2998 .smoothing_range(5, 10, 5)
2999 .reset_toggle(true, true)
3000 .apply_candles(&candles)?;
3001
3002 assert_eq!(output.combos.len(), 12);
3003 assert_eq!(output.rows, 12);
3004 assert_eq!(output.cols, candles.close.len());
3005
3006 Ok(())
3007 }
3008
3009 fn check_fvg_ts_builder_apply_slice(
3010 test_name: &str,
3011 kernel: Kernel,
3012 ) -> Result<(), Box<dyn Error>> {
3013 skip_if_unsupported!(kernel, test_name);
3014 let high = vec![100.0, 102.0, 103.0, 105.0, 104.0];
3015 let low = vec![98.0, 99.0, 101.0, 102.0, 103.0];
3016 let close = vec![99.0, 101.0, 102.0, 104.0, 103.5];
3017
3018 let result = FvgTrailingStopBuilder::new()
3019 .lookback(3)
3020 .smoothing(5)
3021 .kernel(kernel)
3022 .apply_slice(&high, &low, &close)?;
3023
3024 assert_eq!(result.upper.len(), 5);
3025 assert_eq!(result.lower.len(), 5);
3026
3027 Ok(())
3028 }
3029
3030 fn check_fvg_ts_into_slices_warm_nan(
3031 test_name: &str,
3032 kernel: Kernel,
3033 ) -> Result<(), Box<dyn Error>> {
3034 skip_if_unsupported!(kernel, test_name);
3035 let h = vec![
3036 100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, 109.0,
3037 ];
3038 let l = vec![
3039 99.0, 99.5, 100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0,
3040 ];
3041 let c = vec![
3042 99.5, 100.5, 101.5, 102.5, 103.5, 104.5, 105.5, 106.5, 107.5, 108.5,
3043 ];
3044 let params = FvgTrailingStopParams::default();
3045 let input = FvgTrailingStopInput::from_slices(&h, &l, &c, params);
3046
3047 let mut u = vec![0.0; h.len()];
3048 let mut d = u.clone();
3049 let mut uts = u.clone();
3050 let mut lts = u.clone();
3051
3052 let smoothing_len = input.get_smoothing();
3053
3054 fvg_trailing_stop_into_slices(&mut u, &mut d, &mut uts, &mut lts, &input, kernel)?;
3055
3056 let expected_warm = 2 + smoothing_len - 1;
3057 for v in [&u, &d, &uts, <s] {
3058 for i in 0..expected_warm.min(h.len()) {
3059 assert!(
3060 v[i].is_nan(),
3061 "[{}] Expected NaN at index {} but got {}",
3062 test_name,
3063 i,
3064 v[i]
3065 );
3066 }
3067 }
3068 Ok(())
3069 }
3070
3071 fn check_fvg_ts_invalid_smoothing(
3072 test_name: &str,
3073 kernel: Kernel,
3074 ) -> Result<(), Box<dyn Error>> {
3075 skip_if_unsupported!(kernel, test_name);
3076 let h = vec![1.0; 20];
3077 let l = vec![0.0; 20];
3078 let c = vec![0.5; 20];
3079 let params = FvgTrailingStopParams {
3080 unmitigated_fvg_lookback: Some(5),
3081 smoothing_length: Some(0),
3082 reset_on_cross: Some(false),
3083 };
3084 let input = FvgTrailingStopInput::from_slices(&h, &l, &c, params);
3085 let res = fvg_trailing_stop_with_kernel(&input, kernel);
3086 assert!(
3087 matches!(
3088 res,
3089 Err(FvgTrailingStopError::InvalidSmoothingLength { .. })
3090 ),
3091 "[{}] expected InvalidSmoothingLength",
3092 test_name
3093 );
3094 Ok(())
3095 }
3096
3097 fn check_fvg_ts_invalid_lookback(
3098 test_name: &str,
3099 kernel: Kernel,
3100 ) -> Result<(), Box<dyn Error>> {
3101 skip_if_unsupported!(kernel, test_name);
3102 let h = vec![1.0; 20];
3103 let l = vec![0.0; 20];
3104 let c = vec![0.5; 20];
3105 let params = FvgTrailingStopParams {
3106 unmitigated_fvg_lookback: Some(0),
3107 smoothing_length: Some(9),
3108 reset_on_cross: Some(false),
3109 };
3110 let input = FvgTrailingStopInput::from_slices(&h, &l, &c, params);
3111 let res = fvg_trailing_stop_with_kernel(&input, kernel);
3112 assert!(
3113 matches!(res, Err(FvgTrailingStopError::InvalidLookback { .. })),
3114 "[{}] expected InvalidLookback",
3115 test_name
3116 );
3117 Ok(())
3118 }
3119
3120 fn check_fvg_ts_batch_values_for(
3121 test_name: &str,
3122 kernel: Kernel,
3123 ) -> Result<(), Box<dyn Error>> {
3124 skip_if_unsupported!(kernel, test_name);
3125 let h = vec![100.0; 64];
3126 let l = vec![90.0; 64];
3127 let c = vec![95.0; 64];
3128 let sweep = FvgTsBatchRange::default();
3129 let out = fvg_trailing_stop_batch_with_kernel(&h, &l, &c, &sweep, kernel)?;
3130 let p = FvgTrailingStopParams::default();
3131 let (u, d, uts, lts) = out.values_for(&p).expect("missing row");
3132 assert_eq!(u.len(), out.cols);
3133 assert_eq!(d.len(), out.cols);
3134 assert_eq!(uts.len(), out.cols);
3135 assert_eq!(lts.len(), out.cols);
3136 Ok(())
3137 }
3138
3139 macro_rules! generate_all_fvg_ts_tests {
3140 ($($test_fn:ident),*) => {
3141 paste::paste! {
3142 $(
3143 #[test]
3144 fn [<$test_fn _scalar>]() {
3145 let _ = $test_fn(stringify!([<$test_fn _scalar>]), Kernel::Scalar);
3146 }
3147 )*
3148 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3149 $(
3150 #[test]
3151 fn [<$test_fn _avx2>]() {
3152 let _ = $test_fn(stringify!([<$test_fn _avx2>]), Kernel::Avx2);
3153 }
3154 #[test]
3155 fn [<$test_fn _avx512>]() {
3156 let _ = $test_fn(stringify!([<$test_fn _avx512>]), Kernel::Avx512);
3157 }
3158 )*
3159 #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
3160 $(
3161 #[test]
3162 fn [<$test_fn _simd128>]() {
3163 let _ = $test_fn(stringify!([<$test_fn _simd128>]), Kernel::Scalar);
3164 }
3165 )*
3166 }
3167 }
3168 }
3169
3170 generate_all_fvg_ts_tests!(
3171 check_fvg_ts_accuracy,
3172 check_fvg_ts_default_candles,
3173 check_fvg_ts_empty_input,
3174 check_fvg_ts_all_nan,
3175 check_fvg_ts_partial_nan,
3176 check_fvg_ts_streaming,
3177 check_fvg_ts_no_poison,
3178 check_fvg_ts_batch_default,
3179 check_fvg_ts_batch_sweep,
3180 check_fvg_ts_builder_apply_slice,
3181 check_fvg_ts_into_slices_warm_nan,
3182 check_fvg_ts_invalid_smoothing,
3183 check_fvg_ts_invalid_lookback,
3184 check_fvg_ts_batch_values_for
3185 );
3186
3187 #[test]
3188 fn test_fvg_trailing_stop_into_matches_api() -> Result<(), Box<dyn Error>> {
3189 let n = 128usize;
3190 let mut high = Vec::with_capacity(n);
3191 let mut low = Vec::with_capacity(n);
3192 let mut close = Vec::with_capacity(n);
3193 for i in 0..n {
3194 let base = 100.0 + i as f64 * 0.5;
3195 high.push(base + 1.0 + (i % 3) as f64 * 0.1);
3196 low.push(base - 1.0 - (i % 2) as f64 * 0.1);
3197 close.push(base + ((i % 5) as f64 - 2.0) * 0.05);
3198 }
3199
3200 let params = FvgTrailingStopParams::default();
3201 let input = FvgTrailingStopInput::from_slices(&high, &low, &close, params);
3202
3203 let base = fvg_trailing_stop(&input)?;
3204
3205 let mut u = vec![0.0; n];
3206 let mut d = vec![0.0; n];
3207 let mut uts = vec![0.0; n];
3208 let mut lts = vec![0.0; n];
3209 fvg_trailing_stop_into(&input, &mut u, &mut d, &mut uts, &mut lts)?;
3210
3211 fn eq_or_both_nan(a: f64, b: f64) -> bool {
3212 (a.is_nan() && b.is_nan()) || (a == b) || ((a - b).abs() <= 1e-12)
3213 }
3214
3215 assert_eq!(u.len(), base.upper.len());
3216 assert_eq!(d.len(), base.lower.len());
3217 assert_eq!(uts.len(), base.upper_ts.len());
3218 assert_eq!(lts.len(), base.lower_ts.len());
3219
3220 for i in 0..n {
3221 assert!(
3222 eq_or_both_nan(u[i], base.upper[i]),
3223 "upper mismatch at {}: {} vs {}",
3224 i,
3225 u[i],
3226 base.upper[i]
3227 );
3228 assert!(
3229 eq_or_both_nan(d[i], base.lower[i]),
3230 "lower mismatch at {}: {} vs {}",
3231 i,
3232 d[i],
3233 base.lower[i]
3234 );
3235 assert!(
3236 eq_or_both_nan(uts[i], base.upper_ts[i]),
3237 "upper_ts mismatch at {}: {} vs {}",
3238 i,
3239 uts[i],
3240 base.upper_ts[i]
3241 );
3242 assert!(
3243 eq_or_both_nan(lts[i], base.lower_ts[i]),
3244 "lower_ts mismatch at {}: {} vs {}",
3245 i,
3246 lts[i],
3247 base.lower_ts[i]
3248 );
3249 }
3250
3251 Ok(())
3252 }
3253}