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#[cfg(feature = "python")]
10use pyo3::wrap_pyfunction;
11
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use serde::{Deserialize, Serialize};
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use wasm_bindgen::prelude::*;
16
17use crate::utilities::data_loader::Candles;
18use crate::utilities::enums::Kernel;
19use crate::utilities::helpers::{
20 detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes, make_uninit_matrix,
21};
22#[cfg(feature = "python")]
23use crate::utilities::kernel_validation::validate_kernel;
24#[cfg(not(target_arch = "wasm32"))]
25use rayon::prelude::*;
26use std::collections::VecDeque;
27use std::mem::ManuallyDrop;
28use thiserror::Error;
29
30const DEFAULT_LEFT_BARS: usize = 20;
31const DEFAULT_RIGHT_BARS: usize = 1;
32const DEFAULT_LEVEL: f64 = -0.382;
33const DEFAULT_TRIGGER: &str = "close";
34const FLOAT_TOL: f64 = 1e-12;
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37enum TriggerMode {
38 Close,
39 Wick,
40}
41
42impl TriggerMode {
43 #[inline(always)]
44 fn parse(value: &str) -> Option<Self> {
45 if value.eq_ignore_ascii_case("close") {
46 Some(Self::Close)
47 } else if value.eq_ignore_ascii_case("wick") {
48 Some(Self::Wick)
49 } else {
50 None
51 }
52 }
53
54 #[inline(always)]
55 fn as_str(self) -> &'static str {
56 match self {
57 Self::Close => "close",
58 Self::Wick => "wick",
59 }
60 }
61}
62
63#[derive(Debug, Clone)]
64pub enum FibonacciTrailingStopData<'a> {
65 Candles(&'a Candles),
66 Slices {
67 high: &'a [f64],
68 low: &'a [f64],
69 close: &'a [f64],
70 },
71}
72
73#[derive(Debug, Clone)]
74pub struct FibonacciTrailingStopOutput {
75 pub trailing_stop: Vec<f64>,
76 pub long_stop: Vec<f64>,
77 pub short_stop: Vec<f64>,
78 pub direction: Vec<f64>,
79}
80
81#[derive(Debug, Clone, Copy)]
82pub struct FibonacciTrailingStopPoint {
83 pub trailing_stop: f64,
84 pub long_stop: f64,
85 pub short_stop: f64,
86 pub direction: f64,
87}
88
89impl FibonacciTrailingStopPoint {
90 #[inline(always)]
91 fn nan() -> Self {
92 Self {
93 trailing_stop: f64::NAN,
94 long_stop: f64::NAN,
95 short_stop: f64::NAN,
96 direction: f64::NAN,
97 }
98 }
99}
100
101#[derive(Debug, Clone, PartialEq)]
102#[cfg_attr(
103 all(target_arch = "wasm32", feature = "wasm"),
104 derive(Serialize, Deserialize)
105)]
106pub struct FibonacciTrailingStopParams {
107 pub left_bars: Option<usize>,
108 pub right_bars: Option<usize>,
109 pub level: Option<f64>,
110 pub trigger: Option<String>,
111}
112
113impl Default for FibonacciTrailingStopParams {
114 fn default() -> Self {
115 Self {
116 left_bars: Some(DEFAULT_LEFT_BARS),
117 right_bars: Some(DEFAULT_RIGHT_BARS),
118 level: Some(DEFAULT_LEVEL),
119 trigger: Some(DEFAULT_TRIGGER.to_string()),
120 }
121 }
122}
123
124#[derive(Debug, Clone)]
125pub struct FibonacciTrailingStopInput<'a> {
126 pub data: FibonacciTrailingStopData<'a>,
127 pub params: FibonacciTrailingStopParams,
128}
129
130impl<'a> FibonacciTrailingStopInput<'a> {
131 #[inline]
132 pub fn from_candles(candles: &'a Candles, params: FibonacciTrailingStopParams) -> Self {
133 Self {
134 data: FibonacciTrailingStopData::Candles(candles),
135 params,
136 }
137 }
138
139 #[inline]
140 pub fn from_slices(
141 high: &'a [f64],
142 low: &'a [f64],
143 close: &'a [f64],
144 params: FibonacciTrailingStopParams,
145 ) -> Self {
146 Self {
147 data: FibonacciTrailingStopData::Slices { high, low, close },
148 params,
149 }
150 }
151
152 #[inline]
153 pub fn with_default_candles(candles: &'a Candles) -> Self {
154 Self::from_candles(candles, FibonacciTrailingStopParams::default())
155 }
156
157 #[inline]
158 pub fn as_slices(&self) -> (&'a [f64], &'a [f64], &'a [f64]) {
159 match &self.data {
160 FibonacciTrailingStopData::Candles(candles) => {
161 (&candles.high, &candles.low, &candles.close)
162 }
163 FibonacciTrailingStopData::Slices { high, low, close } => (high, low, close),
164 }
165 }
166}
167
168#[derive(Clone, Copy, Debug, Default)]
169pub struct FibonacciTrailingStopBuilder {
170 left_bars: Option<usize>,
171 right_bars: Option<usize>,
172 level: Option<f64>,
173 trigger: Option<TriggerMode>,
174 kernel: Kernel,
175}
176
177impl FibonacciTrailingStopBuilder {
178 #[inline]
179 pub fn new() -> Self {
180 Self::default()
181 }
182
183 #[inline]
184 pub fn left_bars(mut self, value: usize) -> Self {
185 self.left_bars = Some(value);
186 self
187 }
188
189 #[inline]
190 pub fn right_bars(mut self, value: usize) -> Self {
191 self.right_bars = Some(value);
192 self
193 }
194
195 #[inline]
196 pub fn level(mut self, value: f64) -> Self {
197 self.level = Some(value);
198 self
199 }
200
201 #[inline]
202 pub fn trigger(mut self, value: &str) -> Result<Self, FibonacciTrailingStopError> {
203 self.trigger = Some(TriggerMode::parse(value).ok_or_else(|| {
204 FibonacciTrailingStopError::InvalidTrigger {
205 trigger: value.to_string(),
206 }
207 })?);
208 Ok(self)
209 }
210
211 #[inline]
212 pub fn kernel(mut self, kernel: Kernel) -> Self {
213 self.kernel = kernel;
214 self
215 }
216
217 #[inline]
218 pub fn apply(
219 self,
220 candles: &Candles,
221 ) -> Result<FibonacciTrailingStopOutput, FibonacciTrailingStopError> {
222 let input = FibonacciTrailingStopInput::from_candles(
223 candles,
224 FibonacciTrailingStopParams {
225 left_bars: self.left_bars,
226 right_bars: self.right_bars,
227 level: self.level,
228 trigger: Some(
229 self.trigger
230 .unwrap_or(TriggerMode::Close)
231 .as_str()
232 .to_string(),
233 ),
234 },
235 );
236 fibonacci_trailing_stop_with_kernel(&input, self.kernel)
237 }
238
239 #[inline]
240 pub fn apply_slices(
241 self,
242 high: &[f64],
243 low: &[f64],
244 close: &[f64],
245 ) -> Result<FibonacciTrailingStopOutput, FibonacciTrailingStopError> {
246 let input = FibonacciTrailingStopInput::from_slices(
247 high,
248 low,
249 close,
250 FibonacciTrailingStopParams {
251 left_bars: self.left_bars,
252 right_bars: self.right_bars,
253 level: self.level,
254 trigger: Some(
255 self.trigger
256 .unwrap_or(TriggerMode::Close)
257 .as_str()
258 .to_string(),
259 ),
260 },
261 );
262 fibonacci_trailing_stop_with_kernel(&input, self.kernel)
263 }
264
265 #[inline]
266 pub fn into_stream(self) -> Result<FibonacciTrailingStopStream, FibonacciTrailingStopError> {
267 FibonacciTrailingStopStream::try_new(FibonacciTrailingStopParams {
268 left_bars: self.left_bars,
269 right_bars: self.right_bars,
270 level: self.level,
271 trigger: Some(
272 self.trigger
273 .unwrap_or(TriggerMode::Close)
274 .as_str()
275 .to_string(),
276 ),
277 })
278 }
279}
280
281#[derive(Debug, Error)]
282pub enum FibonacciTrailingStopError {
283 #[error("fibonacci_trailing_stop: Input data slice is empty.")]
284 EmptyInputData,
285 #[error("fibonacci_trailing_stop: Input slice lengths differ: high={high_len}, low={low_len}, close={close_len}.")]
286 MismatchedInputLengths {
287 high_len: usize,
288 low_len: usize,
289 close_len: usize,
290 },
291 #[error("fibonacci_trailing_stop: All values are NaN.")]
292 AllValuesNaN,
293 #[error(
294 "fibonacci_trailing_stop: Invalid left_bars: left_bars = {left_bars}, data length = {data_len}"
295 )]
296 InvalidLeftBars { left_bars: usize, data_len: usize },
297 #[error(
298 "fibonacci_trailing_stop: Invalid right_bars: right_bars = {right_bars}, data length = {data_len}"
299 )]
300 InvalidRightBars { right_bars: usize, data_len: usize },
301 #[error("fibonacci_trailing_stop: Invalid level: {level}")]
302 InvalidLevel { level: f64 },
303 #[error("fibonacci_trailing_stop: Invalid trigger: {trigger}")]
304 InvalidTrigger { trigger: String },
305 #[error("fibonacci_trailing_stop: Not enough valid data: needed = {needed}, valid = {valid}")]
306 NotEnoughValidData { needed: usize, valid: usize },
307 #[error("fibonacci_trailing_stop: Output length mismatch: expected = {expected}")]
308 OutputLengthMismatch { expected: usize },
309 #[error("fibonacci_trailing_stop: Invalid range: start={start}, end={end}, step={step}")]
310 InvalidRange {
311 start: String,
312 end: String,
313 step: String,
314 },
315 #[error("fibonacci_trailing_stop: Invalid kernel for batch: {0:?}")]
316 InvalidKernelForBatch(Kernel),
317}
318
319#[derive(Clone, Copy, Debug)]
320struct ResolvedParams {
321 left_bars: usize,
322 right_bars: usize,
323 left_small: usize,
324 right_small: usize,
325 level: f64,
326 trigger: TriggerMode,
327}
328
329#[derive(Clone, Copy, Debug)]
330struct PivotPoint {
331 price: f64,
332 dir: i8,
333}
334
335#[derive(Clone, Debug)]
336struct CoreState {
337 trigger: TriggerMode,
338 level: f64,
339 dir: i8,
340 st: f64,
341 max_level: f64,
342 min_level: f64,
343 pivots: Vec<PivotPoint>,
344}
345
346impl CoreState {
347 #[inline]
348 fn new(high: f64, low: f64, close: f64, params: ResolvedParams) -> Self {
349 Self {
350 trigger: params.trigger,
351 level: params.level,
352 dir: 0,
353 st: close,
354 max_level: high,
355 min_level: low,
356 pivots: Vec::with_capacity(3),
357 }
358 }
359
360 #[inline]
361 fn update_pivots(&mut self, ph: Option<f64>, pl: Option<f64>) {
362 if let Some(value) = ph {
363 if let Some(first) = self.pivots.first_mut() {
364 if first.dir > 0 && value > first.price {
365 first.price = value;
366 } else if first.dir < 0 && value > first.price {
367 self.pivots.insert(
368 0,
369 PivotPoint {
370 price: value,
371 dir: 1,
372 },
373 );
374 }
375 } else {
376 self.pivots.push(PivotPoint {
377 price: value,
378 dir: 1,
379 });
380 }
381 }
382
383 if let Some(value) = pl {
384 if let Some(first) = self.pivots.first_mut() {
385 if first.dir < 0 && value < first.price {
386 first.price = value;
387 } else if first.dir > 0 && value < first.price {
388 self.pivots.insert(
389 0,
390 PivotPoint {
391 price: value,
392 dir: -1,
393 },
394 );
395 }
396 } else {
397 self.pivots.push(PivotPoint {
398 price: value,
399 dir: -1,
400 });
401 }
402 }
403
404 if self.pivots.len() > 3 {
405 self.pivots.truncate(3);
406 }
407 }
408
409 #[inline]
410 fn apply_bar(
411 &mut self,
412 high: f64,
413 low: f64,
414 close: f64,
415 ph: Option<f64>,
416 pl: Option<f64>,
417 ) -> FibonacciTrailingStopPoint {
418 self.update_pivots(ph, pl);
419
420 if self.pivots.len() >= 2 {
421 let p0 = self.pivots[0].price;
422 let p1 = self.pivots[1].price;
423 let mut max_value = p0.max(p1);
424 let mut min_value = p0.min(p1);
425 if self.pivots.len() == 2 {
426 self.st = (max_value + min_value) * 0.5;
427 }
428 let dif = max_value - min_value;
429 max_value += dif * self.level;
430 min_value -= dif * self.level;
431 self.max_level = max_value;
432 self.min_level = min_value;
433 }
434
435 let price = match self.trigger {
436 TriggerMode::Close => close,
437 TriggerMode::Wick => {
438 if self.dir < 1 {
439 high
440 } else {
441 low
442 }
443 }
444 };
445
446 if self.dir < 1 {
447 if price > self.st {
448 self.st = self.min_level;
449 self.dir = 1;
450 } else {
451 self.st = self.st.min(self.max_level);
452 }
453 }
454
455 if self.dir > -1 {
456 if price < self.st {
457 self.st = self.max_level;
458 self.dir = -1;
459 } else {
460 self.st = self.st.max(self.min_level);
461 }
462 }
463
464 FibonacciTrailingStopPoint {
465 trailing_stop: self.st,
466 long_stop: if self.dir == 1 { self.st } else { f64::NAN },
467 short_stop: if self.dir == -1 { self.st } else { f64::NAN },
468 direction: self.dir as f64,
469 }
470 }
471}
472
473#[inline(always)]
474fn first_valid_ohlc(high: &[f64], low: &[f64], close: &[f64]) -> usize {
475 for i in 0..high.len() {
476 if high[i].is_finite() && low[i].is_finite() && close[i].is_finite() {
477 return i;
478 }
479 }
480 high.len()
481}
482
483#[inline(always)]
484fn max_consecutive_valid_ohlc(high: &[f64], low: &[f64], close: &[f64]) -> usize {
485 let mut best = 0usize;
486 let mut run = 0usize;
487 for i in 0..high.len() {
488 if high[i].is_finite() && low[i].is_finite() && close[i].is_finite() {
489 run += 1;
490 if run > best {
491 best = run;
492 }
493 } else {
494 run = 0;
495 }
496 }
497 best
498}
499
500#[inline(always)]
501fn canonical_trigger_name(trigger: Option<&str>) -> String {
502 trigger.unwrap_or(DEFAULT_TRIGGER).to_ascii_lowercase()
503}
504
505#[inline]
506fn resolve_params(
507 params: &FibonacciTrailingStopParams,
508 data_len: Option<usize>,
509) -> Result<ResolvedParams, FibonacciTrailingStopError> {
510 let left_bars = params.left_bars.unwrap_or(DEFAULT_LEFT_BARS);
511 let right_bars = params.right_bars.unwrap_or(DEFAULT_RIGHT_BARS);
512 let level = params.level.unwrap_or(DEFAULT_LEVEL);
513 let trigger_name = canonical_trigger_name(params.trigger.as_deref());
514 let trigger = TriggerMode::parse(&trigger_name).ok_or_else(|| {
515 FibonacciTrailingStopError::InvalidTrigger {
516 trigger: trigger_name.clone(),
517 }
518 })?;
519
520 if left_bars == 0 {
521 return Err(FibonacciTrailingStopError::InvalidLeftBars {
522 left_bars,
523 data_len: data_len.unwrap_or(0),
524 });
525 }
526 if right_bars == 0 {
527 return Err(FibonacciTrailingStopError::InvalidRightBars {
528 right_bars,
529 data_len: data_len.unwrap_or(0),
530 });
531 }
532 if !level.is_finite() {
533 return Err(FibonacciTrailingStopError::InvalidLevel { level });
534 }
535 if let Some(len) = data_len {
536 let needed = left_bars + right_bars + 1;
537 if needed > len {
538 return Err(FibonacciTrailingStopError::NotEnoughValidData { needed, valid: len });
539 }
540 }
541
542 Ok(ResolvedParams {
543 left_bars,
544 right_bars,
545 left_small: ((left_bars + 1) / 2).max(1),
546 right_small: ((right_bars + 1) / 2).max(1),
547 level,
548 trigger,
549 })
550}
551
552#[inline(always)]
553fn confirmed_pivot_high_at(data: &[f64], idx: usize, left: usize, right: usize) -> Option<f64> {
554 if idx < right {
555 return None;
556 }
557 let center = idx - right;
558 if center < left || center + right >= data.len() {
559 return None;
560 }
561 let candidate = data[center];
562 if !candidate.is_finite() {
563 return None;
564 }
565 for &value in &data[(center - left)..=(center + right)] {
566 if !value.is_finite() || value > candidate {
567 return None;
568 }
569 }
570 Some(candidate)
571}
572
573#[inline(always)]
574fn confirmed_pivot_low_at(data: &[f64], idx: usize, left: usize, right: usize) -> Option<f64> {
575 if idx < right {
576 return None;
577 }
578 let center = idx - right;
579 if center < left || center + right >= data.len() {
580 return None;
581 }
582 let candidate = data[center];
583 if !candidate.is_finite() {
584 return None;
585 }
586 for &value in &data[(center - left)..=(center + right)] {
587 if !value.is_finite() || value < candidate {
588 return None;
589 }
590 }
591 Some(candidate)
592}
593
594#[inline(always)]
595fn buffer_pivot_high(data: &VecDeque<f64>, left: usize, right: usize) -> Option<f64> {
596 if data.len() < left + right + 1 {
597 return None;
598 }
599 let center = data.len() - 1 - right;
600 let candidate = data[center];
601 if !candidate.is_finite() {
602 return None;
603 }
604 for i in (center - left)..=(center + right) {
605 let value = data[i];
606 if !value.is_finite() || value > candidate {
607 return None;
608 }
609 }
610 Some(candidate)
611}
612
613#[inline(always)]
614fn buffer_pivot_low(data: &VecDeque<f64>, left: usize, right: usize) -> Option<f64> {
615 if data.len() < left + right + 1 {
616 return None;
617 }
618 let center = data.len() - 1 - right;
619 let candidate = data[center];
620 if !candidate.is_finite() {
621 return None;
622 }
623 for i in (center - left)..=(center + right) {
624 let value = data[i];
625 if !value.is_finite() || value < candidate {
626 return None;
627 }
628 }
629 Some(candidate)
630}
631
632#[derive(Clone, Debug)]
633pub struct FibonacciTrailingStopStream {
634 params: ResolvedParams,
635 state: Option<CoreState>,
636 high_buf: VecDeque<f64>,
637 low_buf: VecDeque<f64>,
638 max_window: usize,
639}
640
641impl FibonacciTrailingStopStream {
642 #[inline]
643 pub fn try_new(
644 params: FibonacciTrailingStopParams,
645 ) -> Result<Self, FibonacciTrailingStopError> {
646 let params = resolve_params(¶ms, None)?;
647 let max_window = (params.left_bars + params.right_bars + 1)
648 .max(params.left_small + params.right_small + 1);
649 Ok(Self {
650 params,
651 state: None,
652 high_buf: VecDeque::with_capacity(max_window),
653 low_buf: VecDeque::with_capacity(max_window),
654 max_window,
655 })
656 }
657
658 #[inline]
659 pub fn reset(&mut self) {
660 self.state = None;
661 self.high_buf.clear();
662 self.low_buf.clear();
663 }
664
665 #[inline]
666 pub fn update(
667 &mut self,
668 high: f64,
669 low: f64,
670 close: f64,
671 ) -> Option<FibonacciTrailingStopPoint> {
672 if !(high.is_finite() && low.is_finite() && close.is_finite()) {
673 self.reset();
674 return None;
675 }
676
677 self.high_buf.push_back(high);
678 self.low_buf.push_back(low);
679 if self.high_buf.len() > self.max_window {
680 self.high_buf.pop_front();
681 self.low_buf.pop_front();
682 }
683
684 let ph = buffer_pivot_high(
685 &self.high_buf,
686 self.params.left_bars,
687 self.params.right_bars,
688 );
689 let pl = buffer_pivot_low(&self.low_buf, self.params.left_bars, self.params.right_bars);
690
691 let state = self
692 .state
693 .get_or_insert_with(|| CoreState::new(high, low, close, self.params));
694 Some(state.apply_bar(high, low, close, ph, pl))
695 }
696}
697
698#[inline]
699fn fibonacci_trailing_stop_prepare<'a>(
700 input: &'a FibonacciTrailingStopInput,
701 kernel: Kernel,
702) -> Result<(&'a [f64], &'a [f64], &'a [f64], ResolvedParams, Kernel), FibonacciTrailingStopError> {
703 let (high, low, close) = input.as_slices();
704 if high.is_empty() || low.is_empty() || close.is_empty() {
705 return Err(FibonacciTrailingStopError::EmptyInputData);
706 }
707 if high.len() != low.len() || high.len() != close.len() {
708 return Err(FibonacciTrailingStopError::MismatchedInputLengths {
709 high_len: high.len(),
710 low_len: low.len(),
711 close_len: close.len(),
712 });
713 }
714
715 let first = first_valid_ohlc(high, low, close);
716 if first >= close.len() {
717 return Err(FibonacciTrailingStopError::AllValuesNaN);
718 }
719
720 let params = resolve_params(&input.params, Some(close.len()))?;
721 let needed = params.left_bars + params.right_bars + 1;
722 let valid = max_consecutive_valid_ohlc(high, low, close);
723 if valid < needed {
724 return Err(FibonacciTrailingStopError::NotEnoughValidData { needed, valid });
725 }
726
727 let chosen = match kernel {
728 Kernel::Auto => detect_best_kernel(),
729 other => other.to_non_batch(),
730 };
731 Ok((high, low, close, params, chosen))
732}
733
734fn fibonacci_trailing_stop_row_from_slices(
735 high: &[f64],
736 low: &[f64],
737 close: &[f64],
738 params: ResolvedParams,
739 trailing_stop: &mut [f64],
740 long_stop: &mut [f64],
741 short_stop: &mut [f64],
742 direction: &mut [f64],
743) {
744 trailing_stop.fill(f64::NAN);
745 long_stop.fill(f64::NAN);
746 short_stop.fill(f64::NAN);
747 direction.fill(f64::NAN);
748
749 let mut state: Option<CoreState> = None;
750 for i in 0..close.len() {
751 let h = high[i];
752 let l = low[i];
753 let c = close[i];
754 if !(h.is_finite() && l.is_finite() && c.is_finite()) {
755 state = None;
756 continue;
757 }
758
759 let ph = confirmed_pivot_high_at(high, i, params.left_bars, params.right_bars);
760 let pl = confirmed_pivot_low_at(low, i, params.left_bars, params.right_bars);
761
762 let point = state
763 .get_or_insert_with(|| CoreState::new(h, l, c, params))
764 .apply_bar(h, l, c, ph, pl);
765
766 trailing_stop[i] = point.trailing_stop;
767 long_stop[i] = point.long_stop;
768 short_stop[i] = point.short_stop;
769 direction[i] = point.direction;
770 }
771}
772
773#[inline]
774pub fn fibonacci_trailing_stop(
775 input: &FibonacciTrailingStopInput,
776) -> Result<FibonacciTrailingStopOutput, FibonacciTrailingStopError> {
777 fibonacci_trailing_stop_with_kernel(input, Kernel::Auto)
778}
779
780#[inline]
781pub fn fibonacci_trailing_stop_with_kernel(
782 input: &FibonacciTrailingStopInput,
783 kernel: Kernel,
784) -> Result<FibonacciTrailingStopOutput, FibonacciTrailingStopError> {
785 let (high, low, close, params, _chosen) = fibonacci_trailing_stop_prepare(input, kernel)?;
786 let len = close.len();
787 let mut trailing_stop = vec![f64::NAN; len];
788 let mut long_stop = vec![f64::NAN; len];
789 let mut short_stop = vec![f64::NAN; len];
790 let mut direction = vec![f64::NAN; len];
791 fibonacci_trailing_stop_row_from_slices(
792 high,
793 low,
794 close,
795 params,
796 &mut trailing_stop,
797 &mut long_stop,
798 &mut short_stop,
799 &mut direction,
800 );
801 Ok(FibonacciTrailingStopOutput {
802 trailing_stop,
803 long_stop,
804 short_stop,
805 direction,
806 })
807}
808
809#[inline]
810pub fn fibonacci_trailing_stop_into_slices(
811 trailing_stop: &mut [f64],
812 long_stop: &mut [f64],
813 short_stop: &mut [f64],
814 direction: &mut [f64],
815 input: &FibonacciTrailingStopInput,
816 kernel: Kernel,
817) -> Result<(), FibonacciTrailingStopError> {
818 let expected = input.as_slices().2.len();
819 if trailing_stop.len() != expected
820 || long_stop.len() != expected
821 || short_stop.len() != expected
822 || direction.len() != expected
823 {
824 return Err(FibonacciTrailingStopError::OutputLengthMismatch { expected });
825 }
826 let (high, low, close, params, _chosen) = fibonacci_trailing_stop_prepare(input, kernel)?;
827 fibonacci_trailing_stop_row_from_slices(
828 high,
829 low,
830 close,
831 params,
832 trailing_stop,
833 long_stop,
834 short_stop,
835 direction,
836 );
837 Ok(())
838}
839
840#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
841#[inline]
842pub fn fibonacci_trailing_stop_into(
843 input: &FibonacciTrailingStopInput,
844 trailing_stop: &mut [f64],
845 long_stop: &mut [f64],
846 short_stop: &mut [f64],
847 direction: &mut [f64],
848) -> Result<(), FibonacciTrailingStopError> {
849 fibonacci_trailing_stop_into_slices(
850 trailing_stop,
851 long_stop,
852 short_stop,
853 direction,
854 input,
855 Kernel::Auto,
856 )
857}
858
859#[derive(Debug, Clone, PartialEq)]
860#[cfg_attr(
861 all(target_arch = "wasm32", feature = "wasm"),
862 derive(Serialize, Deserialize)
863)]
864pub struct FibonacciTrailingStopBatchRange {
865 pub left_bars: (usize, usize, usize),
866 pub right_bars: (usize, usize, usize),
867 pub level: (f64, f64, f64),
868 pub trigger: Option<String>,
869}
870
871impl Default for FibonacciTrailingStopBatchRange {
872 fn default() -> Self {
873 Self {
874 left_bars: (DEFAULT_LEFT_BARS, DEFAULT_LEFT_BARS, 0),
875 right_bars: (DEFAULT_RIGHT_BARS, DEFAULT_RIGHT_BARS, 0),
876 level: (DEFAULT_LEVEL, DEFAULT_LEVEL, 0.0),
877 trigger: Some(DEFAULT_TRIGGER.to_string()),
878 }
879 }
880}
881
882#[derive(Debug, Clone)]
883pub struct FibonacciTrailingStopBatchOutput {
884 pub trailing_stop: Vec<f64>,
885 pub long_stop: Vec<f64>,
886 pub short_stop: Vec<f64>,
887 pub direction: Vec<f64>,
888 pub combos: Vec<FibonacciTrailingStopParams>,
889 pub rows: usize,
890 pub cols: usize,
891}
892
893#[derive(Clone, Debug, Default)]
894pub struct FibonacciTrailingStopBatchBuilder {
895 range: FibonacciTrailingStopBatchRange,
896 kernel: Kernel,
897}
898
899impl FibonacciTrailingStopBatchBuilder {
900 #[inline]
901 pub fn new() -> Self {
902 Self::default()
903 }
904
905 #[inline]
906 pub fn kernel(mut self, kernel: Kernel) -> Self {
907 self.kernel = kernel;
908 self
909 }
910
911 #[inline]
912 pub fn left_bars_range(mut self, start: usize, end: usize, step: usize) -> Self {
913 self.range.left_bars = (start, end, step);
914 self
915 }
916
917 #[inline]
918 pub fn right_bars_range(mut self, start: usize, end: usize, step: usize) -> Self {
919 self.range.right_bars = (start, end, step);
920 self
921 }
922
923 #[inline]
924 pub fn level_range(mut self, start: f64, end: f64, step: f64) -> Self {
925 self.range.level = (start, end, step);
926 self
927 }
928
929 #[inline]
930 pub fn trigger<T: Into<String>>(mut self, trigger: T) -> Self {
931 self.range.trigger = Some(trigger.into());
932 self
933 }
934
935 #[inline]
936 pub fn apply_slices(
937 self,
938 high: &[f64],
939 low: &[f64],
940 close: &[f64],
941 ) -> Result<FibonacciTrailingStopBatchOutput, FibonacciTrailingStopError> {
942 fibonacci_trailing_stop_batch_with_kernel(high, low, close, &self.range, self.kernel)
943 }
944
945 #[inline]
946 pub fn apply_candles(
947 self,
948 candles: &Candles,
949 ) -> Result<FibonacciTrailingStopBatchOutput, FibonacciTrailingStopError> {
950 self.apply_slices(&candles.high, &candles.low, &candles.close)
951 }
952}
953
954#[inline(always)]
955fn expand_axis_usize(
956 (start, end, step): (usize, usize, usize),
957) -> Result<Vec<usize>, FibonacciTrailingStopError> {
958 if step == 0 || start == end {
959 return Ok(vec![start]);
960 }
961 let mut out = Vec::new();
962 if start < end {
963 let mut value = start;
964 while value <= end {
965 out.push(value);
966 let next = value.saturating_add(step);
967 if next == value {
968 break;
969 }
970 value = next;
971 }
972 } else {
973 let mut value = start;
974 loop {
975 out.push(value);
976 if value == end {
977 break;
978 }
979 let next = value.saturating_sub(step);
980 if next == value || next < end {
981 break;
982 }
983 value = next;
984 }
985 }
986 if out.is_empty() {
987 return Err(FibonacciTrailingStopError::InvalidRange {
988 start: start.to_string(),
989 end: end.to_string(),
990 step: step.to_string(),
991 });
992 }
993 Ok(out)
994}
995
996#[inline(always)]
997fn expand_axis_f64(
998 start: f64,
999 end: f64,
1000 step: f64,
1001) -> Result<Vec<f64>, FibonacciTrailingStopError> {
1002 if !start.is_finite() || !end.is_finite() || !step.is_finite() || start > end {
1003 return Err(FibonacciTrailingStopError::InvalidRange {
1004 start: start.to_string(),
1005 end: end.to_string(),
1006 step: step.to_string(),
1007 });
1008 }
1009 if (start - end).abs() < FLOAT_TOL {
1010 if step.abs() > FLOAT_TOL {
1011 return Err(FibonacciTrailingStopError::InvalidRange {
1012 start: start.to_string(),
1013 end: end.to_string(),
1014 step: step.to_string(),
1015 });
1016 }
1017 return Ok(vec![start]);
1018 }
1019 if step <= 0.0 {
1020 return Err(FibonacciTrailingStopError::InvalidRange {
1021 start: start.to_string(),
1022 end: end.to_string(),
1023 step: step.to_string(),
1024 });
1025 }
1026 let mut out = Vec::new();
1027 let mut value = start;
1028 while value <= end + FLOAT_TOL {
1029 out.push(value.min(end));
1030 value += step;
1031 }
1032 if (out.last().copied().unwrap_or(start) - end).abs() > 1e-9 {
1033 return Err(FibonacciTrailingStopError::InvalidRange {
1034 start: start.to_string(),
1035 end: end.to_string(),
1036 step: step.to_string(),
1037 });
1038 }
1039 Ok(out)
1040}
1041
1042fn expand_grid_fibonacci_trailing_stop(
1043 sweep: &FibonacciTrailingStopBatchRange,
1044) -> Result<Vec<FibonacciTrailingStopParams>, FibonacciTrailingStopError> {
1045 let left_values = expand_axis_usize(sweep.left_bars)?;
1046 let right_values = expand_axis_usize(sweep.right_bars)?;
1047 let level_values = expand_axis_f64(sweep.level.0, sweep.level.1, sweep.level.2)?;
1048 let trigger_name = canonical_trigger_name(sweep.trigger.as_deref());
1049 let mut combos = Vec::with_capacity(
1050 left_values
1051 .len()
1052 .saturating_mul(right_values.len())
1053 .saturating_mul(level_values.len()),
1054 );
1055 for left_bars in left_values {
1056 for &right_bars in &right_values {
1057 for &level in &level_values {
1058 let params = FibonacciTrailingStopParams {
1059 left_bars: Some(left_bars),
1060 right_bars: Some(right_bars),
1061 level: Some(level),
1062 trigger: Some(trigger_name.clone()),
1063 };
1064 let _ = resolve_params(¶ms, None)?;
1065 combos.push(params);
1066 }
1067 }
1068 }
1069 Ok(combos)
1070}
1071
1072#[inline]
1073pub fn fibonacci_trailing_stop_batch_with_kernel(
1074 high: &[f64],
1075 low: &[f64],
1076 close: &[f64],
1077 sweep: &FibonacciTrailingStopBatchRange,
1078 kernel: Kernel,
1079) -> Result<FibonacciTrailingStopBatchOutput, FibonacciTrailingStopError> {
1080 let batch_kernel = match kernel {
1081 Kernel::Auto => detect_best_batch_kernel(),
1082 other if other.is_batch() => other,
1083 other => return Err(FibonacciTrailingStopError::InvalidKernelForBatch(other)),
1084 };
1085 fibonacci_trailing_stop_batch_par_slices(high, low, close, sweep, batch_kernel.to_non_batch())
1086}
1087
1088#[inline]
1089pub fn fibonacci_trailing_stop_batch_slices(
1090 high: &[f64],
1091 low: &[f64],
1092 close: &[f64],
1093 sweep: &FibonacciTrailingStopBatchRange,
1094 kernel: Kernel,
1095) -> Result<FibonacciTrailingStopBatchOutput, FibonacciTrailingStopError> {
1096 fibonacci_trailing_stop_batch_inner(high, low, close, sweep, kernel, false)
1097}
1098
1099#[inline]
1100pub fn fibonacci_trailing_stop_batch_par_slices(
1101 high: &[f64],
1102 low: &[f64],
1103 close: &[f64],
1104 sweep: &FibonacciTrailingStopBatchRange,
1105 kernel: Kernel,
1106) -> Result<FibonacciTrailingStopBatchOutput, FibonacciTrailingStopError> {
1107 fibonacci_trailing_stop_batch_inner(high, low, close, sweep, kernel, true)
1108}
1109
1110pub fn fibonacci_trailing_stop_batch_inner(
1111 high: &[f64],
1112 low: &[f64],
1113 close: &[f64],
1114 sweep: &FibonacciTrailingStopBatchRange,
1115 _kernel: Kernel,
1116 parallel: bool,
1117) -> Result<FibonacciTrailingStopBatchOutput, FibonacciTrailingStopError> {
1118 if high.is_empty() || low.is_empty() || close.is_empty() {
1119 return Err(FibonacciTrailingStopError::EmptyInputData);
1120 }
1121 if high.len() != low.len() || high.len() != close.len() {
1122 return Err(FibonacciTrailingStopError::MismatchedInputLengths {
1123 high_len: high.len(),
1124 low_len: low.len(),
1125 close_len: close.len(),
1126 });
1127 }
1128 let first = first_valid_ohlc(high, low, close);
1129 if first >= close.len() {
1130 return Err(FibonacciTrailingStopError::AllValuesNaN);
1131 }
1132
1133 let combos = expand_grid_fibonacci_trailing_stop(sweep)?;
1134 let rows = combos.len();
1135 let cols = close.len();
1136 let total = rows
1137 .checked_mul(cols)
1138 .ok_or(FibonacciTrailingStopError::OutputLengthMismatch {
1139 expected: usize::MAX,
1140 })?;
1141 let resolved = combos
1142 .iter()
1143 .map(|params| resolve_params(params, Some(cols)))
1144 .collect::<Result<Vec<_>, _>>()?;
1145 let max_valid = max_consecutive_valid_ohlc(high, low, close);
1146 for params in &resolved {
1147 let needed = params.left_bars + params.right_bars + 1;
1148 if max_valid < needed {
1149 return Err(FibonacciTrailingStopError::NotEnoughValidData {
1150 needed,
1151 valid: max_valid,
1152 });
1153 }
1154 }
1155
1156 let zero_prefixes = vec![0usize; rows];
1157 let mut trailing_stop_mu = make_uninit_matrix(rows, cols);
1158 init_matrix_prefixes(&mut trailing_stop_mu, cols, &zero_prefixes);
1159 let mut trailing_stop_guard = ManuallyDrop::new(trailing_stop_mu);
1160 let trailing_stop_out = unsafe {
1161 std::slice::from_raw_parts_mut(trailing_stop_guard.as_mut_ptr() as *mut f64, total)
1162 };
1163
1164 let mut long_stop_mu = make_uninit_matrix(rows, cols);
1165 init_matrix_prefixes(&mut long_stop_mu, cols, &zero_prefixes);
1166 let mut long_stop_guard = ManuallyDrop::new(long_stop_mu);
1167 let long_stop_out =
1168 unsafe { std::slice::from_raw_parts_mut(long_stop_guard.as_mut_ptr() as *mut f64, total) };
1169
1170 let mut short_stop_mu = make_uninit_matrix(rows, cols);
1171 init_matrix_prefixes(&mut short_stop_mu, cols, &zero_prefixes);
1172 let mut short_stop_guard = ManuallyDrop::new(short_stop_mu);
1173 let short_stop_out =
1174 unsafe { std::slice::from_raw_parts_mut(short_stop_guard.as_mut_ptr() as *mut f64, total) };
1175
1176 let mut direction_mu = make_uninit_matrix(rows, cols);
1177 init_matrix_prefixes(&mut direction_mu, cols, &zero_prefixes);
1178 let mut direction_guard = ManuallyDrop::new(direction_mu);
1179 let direction_out =
1180 unsafe { std::slice::from_raw_parts_mut(direction_guard.as_mut_ptr() as *mut f64, total) };
1181
1182 if parallel {
1183 #[cfg(not(target_arch = "wasm32"))]
1184 {
1185 let trailing_stop_ptr = trailing_stop_out.as_mut_ptr() as usize;
1186 let long_stop_ptr = long_stop_out.as_mut_ptr() as usize;
1187 let short_stop_ptr = short_stop_out.as_mut_ptr() as usize;
1188 let direction_ptr = direction_out.as_mut_ptr() as usize;
1189 resolved
1190 .par_iter()
1191 .enumerate()
1192 .for_each(|(row, params)| unsafe {
1193 let start = row * cols;
1194 fibonacci_trailing_stop_row_from_slices(
1195 high,
1196 low,
1197 close,
1198 *params,
1199 std::slice::from_raw_parts_mut(
1200 (trailing_stop_ptr as *mut f64).add(start),
1201 cols,
1202 ),
1203 std::slice::from_raw_parts_mut(
1204 (long_stop_ptr as *mut f64).add(start),
1205 cols,
1206 ),
1207 std::slice::from_raw_parts_mut(
1208 (short_stop_ptr as *mut f64).add(start),
1209 cols,
1210 ),
1211 std::slice::from_raw_parts_mut(
1212 (direction_ptr as *mut f64).add(start),
1213 cols,
1214 ),
1215 );
1216 });
1217 }
1218
1219 #[cfg(target_arch = "wasm32")]
1220 for (row, params) in resolved.iter().enumerate() {
1221 let start = row * cols;
1222 let end = start + cols;
1223 fibonacci_trailing_stop_row_from_slices(
1224 high,
1225 low,
1226 close,
1227 *params,
1228 &mut trailing_stop_out[start..end],
1229 &mut long_stop_out[start..end],
1230 &mut short_stop_out[start..end],
1231 &mut direction_out[start..end],
1232 );
1233 }
1234 } else {
1235 for (row, params) in resolved.iter().enumerate() {
1236 let start = row * cols;
1237 let end = start + cols;
1238 fibonacci_trailing_stop_row_from_slices(
1239 high,
1240 low,
1241 close,
1242 *params,
1243 &mut trailing_stop_out[start..end],
1244 &mut long_stop_out[start..end],
1245 &mut short_stop_out[start..end],
1246 &mut direction_out[start..end],
1247 );
1248 }
1249 }
1250
1251 let trailing_stop = unsafe {
1252 Vec::from_raw_parts(
1253 trailing_stop_guard.as_mut_ptr() as *mut f64,
1254 trailing_stop_guard.len(),
1255 trailing_stop_guard.capacity(),
1256 )
1257 };
1258 let long_stop = unsafe {
1259 Vec::from_raw_parts(
1260 long_stop_guard.as_mut_ptr() as *mut f64,
1261 long_stop_guard.len(),
1262 long_stop_guard.capacity(),
1263 )
1264 };
1265 let short_stop = unsafe {
1266 Vec::from_raw_parts(
1267 short_stop_guard.as_mut_ptr() as *mut f64,
1268 short_stop_guard.len(),
1269 short_stop_guard.capacity(),
1270 )
1271 };
1272 let direction = unsafe {
1273 Vec::from_raw_parts(
1274 direction_guard.as_mut_ptr() as *mut f64,
1275 direction_guard.len(),
1276 direction_guard.capacity(),
1277 )
1278 };
1279 core::mem::forget(trailing_stop_guard);
1280 core::mem::forget(long_stop_guard);
1281 core::mem::forget(short_stop_guard);
1282 core::mem::forget(direction_guard);
1283
1284 Ok(FibonacciTrailingStopBatchOutput {
1285 trailing_stop,
1286 long_stop,
1287 short_stop,
1288 direction,
1289 combos,
1290 rows,
1291 cols,
1292 })
1293}
1294
1295pub fn fibonacci_trailing_stop_batch_inner_into(
1296 high: &[f64],
1297 low: &[f64],
1298 close: &[f64],
1299 sweep: &FibonacciTrailingStopBatchRange,
1300 kernel: Kernel,
1301 parallel: bool,
1302 trailing_stop: &mut [f64],
1303 long_stop: &mut [f64],
1304 short_stop: &mut [f64],
1305 direction: &mut [f64],
1306) -> Result<Vec<FibonacciTrailingStopParams>, FibonacciTrailingStopError> {
1307 let out = fibonacci_trailing_stop_batch_inner(high, low, close, sweep, kernel, parallel)?;
1308 let total = out.rows * out.cols;
1309 if trailing_stop.len() != total
1310 || long_stop.len() != total
1311 || short_stop.len() != total
1312 || direction.len() != total
1313 {
1314 return Err(FibonacciTrailingStopError::OutputLengthMismatch { expected: total });
1315 }
1316 trailing_stop.copy_from_slice(&out.trailing_stop);
1317 long_stop.copy_from_slice(&out.long_stop);
1318 short_stop.copy_from_slice(&out.short_stop);
1319 direction.copy_from_slice(&out.direction);
1320 Ok(out.combos)
1321}
1322
1323#[cfg(feature = "python")]
1324#[pyfunction(name = "fibonacci_trailing_stop")]
1325#[pyo3(signature = (
1326 high,
1327 low,
1328 close,
1329 left_bars=DEFAULT_LEFT_BARS,
1330 right_bars=DEFAULT_RIGHT_BARS,
1331 level=DEFAULT_LEVEL,
1332 trigger=DEFAULT_TRIGGER,
1333 kernel=None
1334))]
1335pub fn fibonacci_trailing_stop_py<'py>(
1336 py: Python<'py>,
1337 high: PyReadonlyArray1<'py, f64>,
1338 low: PyReadonlyArray1<'py, f64>,
1339 close: PyReadonlyArray1<'py, f64>,
1340 left_bars: usize,
1341 right_bars: usize,
1342 level: f64,
1343 trigger: &str,
1344 kernel: Option<&str>,
1345) -> PyResult<(
1346 Bound<'py, PyArray1<f64>>,
1347 Bound<'py, PyArray1<f64>>,
1348 Bound<'py, PyArray1<f64>>,
1349 Bound<'py, PyArray1<f64>>,
1350)> {
1351 let high = high.as_slice()?;
1352 let low = low.as_slice()?;
1353 let close = close.as_slice()?;
1354 let kernel = validate_kernel(kernel, false)?;
1355 let input = FibonacciTrailingStopInput::from_slices(
1356 high,
1357 low,
1358 close,
1359 FibonacciTrailingStopParams {
1360 left_bars: Some(left_bars),
1361 right_bars: Some(right_bars),
1362 level: Some(level),
1363 trigger: Some(trigger.to_string()),
1364 },
1365 );
1366 let out = py
1367 .allow_threads(|| fibonacci_trailing_stop_with_kernel(&input, kernel))
1368 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1369 Ok((
1370 out.trailing_stop.into_pyarray(py),
1371 out.long_stop.into_pyarray(py),
1372 out.short_stop.into_pyarray(py),
1373 out.direction.into_pyarray(py),
1374 ))
1375}
1376
1377#[cfg(feature = "python")]
1378#[pyclass(name = "FibonacciTrailingStopStream")]
1379pub struct FibonacciTrailingStopStreamPy {
1380 stream: FibonacciTrailingStopStream,
1381}
1382
1383#[cfg(feature = "python")]
1384#[pymethods]
1385impl FibonacciTrailingStopStreamPy {
1386 #[new]
1387 #[pyo3(signature = (
1388 left_bars=DEFAULT_LEFT_BARS,
1389 right_bars=DEFAULT_RIGHT_BARS,
1390 level=DEFAULT_LEVEL,
1391 trigger=DEFAULT_TRIGGER
1392 ))]
1393 fn new(left_bars: usize, right_bars: usize, level: f64, trigger: &str) -> PyResult<Self> {
1394 let stream = FibonacciTrailingStopStream::try_new(FibonacciTrailingStopParams {
1395 left_bars: Some(left_bars),
1396 right_bars: Some(right_bars),
1397 level: Some(level),
1398 trigger: Some(trigger.to_string()),
1399 })
1400 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1401 Ok(Self { stream })
1402 }
1403
1404 fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64, f64, f64)> {
1405 self.stream.update(high, low, close).map(|point| {
1406 (
1407 point.trailing_stop,
1408 point.long_stop,
1409 point.short_stop,
1410 point.direction,
1411 )
1412 })
1413 }
1414
1415 fn reset(&mut self) {
1416 self.stream.reset();
1417 }
1418}
1419
1420#[cfg(feature = "python")]
1421#[pyfunction(name = "fibonacci_trailing_stop_batch")]
1422#[pyo3(signature = (
1423 high,
1424 low,
1425 close,
1426 left_bars_range=(DEFAULT_LEFT_BARS, DEFAULT_LEFT_BARS, 0),
1427 right_bars_range=(DEFAULT_RIGHT_BARS, DEFAULT_RIGHT_BARS, 0),
1428 level_range=(DEFAULT_LEVEL, DEFAULT_LEVEL, 0.0),
1429 trigger=DEFAULT_TRIGGER,
1430 kernel=None
1431))]
1432pub fn fibonacci_trailing_stop_batch_py<'py>(
1433 py: Python<'py>,
1434 high: PyReadonlyArray1<'py, f64>,
1435 low: PyReadonlyArray1<'py, f64>,
1436 close: PyReadonlyArray1<'py, f64>,
1437 left_bars_range: (usize, usize, usize),
1438 right_bars_range: (usize, usize, usize),
1439 level_range: (f64, f64, f64),
1440 trigger: &str,
1441 kernel: Option<&str>,
1442) -> PyResult<Bound<'py, PyDict>> {
1443 let high = high.as_slice()?;
1444 let low = low.as_slice()?;
1445 let close = close.as_slice()?;
1446 let kernel = validate_kernel(kernel, true)?;
1447 let sweep = FibonacciTrailingStopBatchRange {
1448 left_bars: left_bars_range,
1449 right_bars: right_bars_range,
1450 level: level_range,
1451 trigger: Some(trigger.to_string()),
1452 };
1453 let combos = expand_grid_fibonacci_trailing_stop(&sweep)
1454 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1455 let rows = combos.len();
1456 let cols = close.len();
1457 let total = rows
1458 .checked_mul(cols)
1459 .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1460
1461 let trailing_stop_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1462 let long_stop_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1463 let short_stop_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1464 let direction_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1465
1466 let trailing_stop_slice = unsafe { trailing_stop_arr.as_slice_mut()? };
1467 let long_stop_slice = unsafe { long_stop_arr.as_slice_mut()? };
1468 let short_stop_slice = unsafe { short_stop_arr.as_slice_mut()? };
1469 let direction_slice = unsafe { direction_arr.as_slice_mut()? };
1470
1471 let combos = py
1472 .allow_threads(|| {
1473 let batch_kernel = match kernel {
1474 Kernel::Auto => detect_best_batch_kernel(),
1475 other => other,
1476 };
1477 fibonacci_trailing_stop_batch_inner_into(
1478 high,
1479 low,
1480 close,
1481 &sweep,
1482 batch_kernel.to_non_batch(),
1483 true,
1484 trailing_stop_slice,
1485 long_stop_slice,
1486 short_stop_slice,
1487 direction_slice,
1488 )
1489 })
1490 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1491
1492 let dict = PyDict::new(py);
1493 dict.set_item("trailing_stop", trailing_stop_arr.reshape((rows, cols))?)?;
1494 dict.set_item("long_stop", long_stop_arr.reshape((rows, cols))?)?;
1495 dict.set_item("short_stop", short_stop_arr.reshape((rows, cols))?)?;
1496 dict.set_item("direction", direction_arr.reshape((rows, cols))?)?;
1497 dict.set_item(
1498 "left_bars",
1499 combos
1500 .iter()
1501 .map(|combo| combo.left_bars.unwrap_or(DEFAULT_LEFT_BARS) as u64)
1502 .collect::<Vec<_>>()
1503 .into_pyarray(py),
1504 )?;
1505 dict.set_item(
1506 "right_bars",
1507 combos
1508 .iter()
1509 .map(|combo| combo.right_bars.unwrap_or(DEFAULT_RIGHT_BARS) as u64)
1510 .collect::<Vec<_>>()
1511 .into_pyarray(py),
1512 )?;
1513 dict.set_item(
1514 "levels",
1515 combos
1516 .iter()
1517 .map(|combo| combo.level.unwrap_or(DEFAULT_LEVEL))
1518 .collect::<Vec<_>>()
1519 .into_pyarray(py),
1520 )?;
1521 dict.set_item("rows", rows)?;
1522 dict.set_item("cols", cols)?;
1523 Ok(dict)
1524}
1525
1526#[cfg(feature = "python")]
1527pub fn register_fibonacci_trailing_stop_module(
1528 module: &Bound<'_, pyo3::types::PyModule>,
1529) -> PyResult<()> {
1530 module.add_function(wrap_pyfunction!(fibonacci_trailing_stop_py, module)?)?;
1531 module.add_function(wrap_pyfunction!(fibonacci_trailing_stop_batch_py, module)?)?;
1532 module.add_class::<FibonacciTrailingStopStreamPy>()?;
1533 Ok(())
1534}
1535
1536#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1537#[derive(Serialize, Deserialize)]
1538pub struct FibonacciTrailingStopJsOutput {
1539 pub trailing_stop: Vec<f64>,
1540 pub long_stop: Vec<f64>,
1541 pub short_stop: Vec<f64>,
1542 pub direction: Vec<f64>,
1543}
1544
1545#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1546#[wasm_bindgen(js_name = "fibonacci_trailing_stop_js")]
1547pub fn fibonacci_trailing_stop_js(
1548 high: &[f64],
1549 low: &[f64],
1550 close: &[f64],
1551 left_bars: usize,
1552 right_bars: usize,
1553 level: f64,
1554 trigger: String,
1555) -> Result<JsValue, JsValue> {
1556 let input = FibonacciTrailingStopInput::from_slices(
1557 high,
1558 low,
1559 close,
1560 FibonacciTrailingStopParams {
1561 left_bars: Some(left_bars),
1562 right_bars: Some(right_bars),
1563 level: Some(level),
1564 trigger: Some(trigger),
1565 },
1566 );
1567 let out = fibonacci_trailing_stop(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
1568 serde_wasm_bindgen::to_value(&FibonacciTrailingStopJsOutput {
1569 trailing_stop: out.trailing_stop,
1570 long_stop: out.long_stop,
1571 short_stop: out.short_stop,
1572 direction: out.direction,
1573 })
1574 .map_err(|e| JsValue::from_str(&e.to_string()))
1575}
1576
1577#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1578#[wasm_bindgen]
1579pub fn fibonacci_trailing_stop_alloc(len: usize) -> *mut f64 {
1580 let mut vec = Vec::<f64>::with_capacity(len);
1581 let ptr = vec.as_mut_ptr();
1582 std::mem::forget(vec);
1583 ptr
1584}
1585
1586#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1587#[wasm_bindgen]
1588pub fn fibonacci_trailing_stop_free(ptr: *mut f64, len: usize) {
1589 if !ptr.is_null() {
1590 unsafe {
1591 let _ = Vec::from_raw_parts(ptr, len, len);
1592 }
1593 }
1594}
1595
1596#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1597fn has_duplicate_ptrs(ptrs: &[usize]) -> bool {
1598 for i in 0..ptrs.len() {
1599 for j in (i + 1)..ptrs.len() {
1600 if ptrs[i] == ptrs[j] {
1601 return true;
1602 }
1603 }
1604 }
1605 false
1606}
1607
1608#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1609#[wasm_bindgen]
1610pub fn fibonacci_trailing_stop_into(
1611 high_ptr: *const f64,
1612 low_ptr: *const f64,
1613 close_ptr: *const f64,
1614 trailing_stop_ptr: *mut f64,
1615 long_stop_ptr: *mut f64,
1616 short_stop_ptr: *mut f64,
1617 direction_ptr: *mut f64,
1618 len: usize,
1619 left_bars: usize,
1620 right_bars: usize,
1621 level: f64,
1622 trigger: String,
1623) -> Result<(), JsValue> {
1624 if high_ptr.is_null()
1625 || low_ptr.is_null()
1626 || close_ptr.is_null()
1627 || trailing_stop_ptr.is_null()
1628 || long_stop_ptr.is_null()
1629 || short_stop_ptr.is_null()
1630 || direction_ptr.is_null()
1631 {
1632 return Err(JsValue::from_str("Null pointer provided"));
1633 }
1634
1635 unsafe {
1636 let high = std::slice::from_raw_parts(high_ptr, len);
1637 let low = std::slice::from_raw_parts(low_ptr, len);
1638 let close = std::slice::from_raw_parts(close_ptr, len);
1639 let input = FibonacciTrailingStopInput::from_slices(
1640 high,
1641 low,
1642 close,
1643 FibonacciTrailingStopParams {
1644 left_bars: Some(left_bars),
1645 right_bars: Some(right_bars),
1646 level: Some(level),
1647 trigger: Some(trigger),
1648 },
1649 );
1650
1651 let output_ptrs = [
1652 trailing_stop_ptr as usize,
1653 long_stop_ptr as usize,
1654 short_stop_ptr as usize,
1655 direction_ptr as usize,
1656 ];
1657 let need_temp = output_ptrs.iter().any(|&ptr| {
1658 ptr == high_ptr as usize || ptr == low_ptr as usize || ptr == close_ptr as usize
1659 }) || has_duplicate_ptrs(&output_ptrs);
1660
1661 if need_temp {
1662 let mut trailing_stop = vec![0.0; len];
1663 let mut long_stop = vec![0.0; len];
1664 let mut short_stop = vec![0.0; len];
1665 let mut direction = vec![0.0; len];
1666 fibonacci_trailing_stop_into_slices(
1667 &mut trailing_stop,
1668 &mut long_stop,
1669 &mut short_stop,
1670 &mut direction,
1671 &input,
1672 Kernel::Auto,
1673 )
1674 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1675 std::slice::from_raw_parts_mut(trailing_stop_ptr, len).copy_from_slice(&trailing_stop);
1676 std::slice::from_raw_parts_mut(long_stop_ptr, len).copy_from_slice(&long_stop);
1677 std::slice::from_raw_parts_mut(short_stop_ptr, len).copy_from_slice(&short_stop);
1678 std::slice::from_raw_parts_mut(direction_ptr, len).copy_from_slice(&direction);
1679 } else {
1680 fibonacci_trailing_stop_into_slices(
1681 std::slice::from_raw_parts_mut(trailing_stop_ptr, len),
1682 std::slice::from_raw_parts_mut(long_stop_ptr, len),
1683 std::slice::from_raw_parts_mut(short_stop_ptr, len),
1684 std::slice::from_raw_parts_mut(direction_ptr, len),
1685 &input,
1686 Kernel::Auto,
1687 )
1688 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1689 }
1690 }
1691 Ok(())
1692}
1693
1694#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1695#[derive(Serialize, Deserialize)]
1696pub struct FibonacciTrailingStopBatchJsConfig {
1697 pub left_bars_range: Option<(usize, usize, usize)>,
1698 pub right_bars_range: Option<(usize, usize, usize)>,
1699 pub level_range: Option<(f64, f64, f64)>,
1700 pub trigger: Option<String>,
1701}
1702
1703#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1704#[derive(Serialize, Deserialize)]
1705pub struct FibonacciTrailingStopBatchJsOutput {
1706 pub trailing_stop: Vec<f64>,
1707 pub long_stop: Vec<f64>,
1708 pub short_stop: Vec<f64>,
1709 pub direction: Vec<f64>,
1710 pub combos: Vec<FibonacciTrailingStopParams>,
1711 pub left_bars: Vec<usize>,
1712 pub right_bars: Vec<usize>,
1713 pub levels: Vec<f64>,
1714 pub rows: usize,
1715 pub cols: usize,
1716}
1717
1718#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1719#[wasm_bindgen(js_name = "fibonacci_trailing_stop_batch_js")]
1720pub fn fibonacci_trailing_stop_batch_js(
1721 high: &[f64],
1722 low: &[f64],
1723 close: &[f64],
1724 config: JsValue,
1725) -> Result<JsValue, JsValue> {
1726 let config: FibonacciTrailingStopBatchJsConfig = serde_wasm_bindgen::from_value(config)
1727 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1728 let sweep = FibonacciTrailingStopBatchRange {
1729 left_bars: config
1730 .left_bars_range
1731 .unwrap_or((DEFAULT_LEFT_BARS, DEFAULT_LEFT_BARS, 0)),
1732 right_bars: config
1733 .right_bars_range
1734 .unwrap_or((DEFAULT_RIGHT_BARS, DEFAULT_RIGHT_BARS, 0)),
1735 level: config
1736 .level_range
1737 .unwrap_or((DEFAULT_LEVEL, DEFAULT_LEVEL, 0.0)),
1738 trigger: config.trigger.or_else(|| Some(DEFAULT_TRIGGER.to_string())),
1739 };
1740 let out = fibonacci_trailing_stop_batch_with_kernel(high, low, close, &sweep, Kernel::Auto)
1741 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1742 serde_wasm_bindgen::to_value(&FibonacciTrailingStopBatchJsOutput {
1743 left_bars: out
1744 .combos
1745 .iter()
1746 .map(|combo| combo.left_bars.unwrap_or(DEFAULT_LEFT_BARS))
1747 .collect(),
1748 right_bars: out
1749 .combos
1750 .iter()
1751 .map(|combo| combo.right_bars.unwrap_or(DEFAULT_RIGHT_BARS))
1752 .collect(),
1753 levels: out
1754 .combos
1755 .iter()
1756 .map(|combo| combo.level.unwrap_or(DEFAULT_LEVEL))
1757 .collect(),
1758 trailing_stop: out.trailing_stop,
1759 long_stop: out.long_stop,
1760 short_stop: out.short_stop,
1761 direction: out.direction,
1762 combos: out.combos,
1763 rows: out.rows,
1764 cols: out.cols,
1765 })
1766 .map_err(|e| JsValue::from_str(&e.to_string()))
1767}
1768
1769#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1770#[wasm_bindgen]
1771pub fn fibonacci_trailing_stop_batch_into(
1772 high_ptr: *const f64,
1773 low_ptr: *const f64,
1774 close_ptr: *const f64,
1775 trailing_stop_ptr: *mut f64,
1776 long_stop_ptr: *mut f64,
1777 short_stop_ptr: *mut f64,
1778 direction_ptr: *mut f64,
1779 len: usize,
1780 left_bars_start: usize,
1781 left_bars_end: usize,
1782 left_bars_step: usize,
1783 right_bars_start: usize,
1784 right_bars_end: usize,
1785 right_bars_step: usize,
1786 level_start: f64,
1787 level_end: f64,
1788 level_step: f64,
1789 trigger: String,
1790) -> Result<usize, JsValue> {
1791 if high_ptr.is_null()
1792 || low_ptr.is_null()
1793 || close_ptr.is_null()
1794 || trailing_stop_ptr.is_null()
1795 || long_stop_ptr.is_null()
1796 || short_stop_ptr.is_null()
1797 || direction_ptr.is_null()
1798 {
1799 return Err(JsValue::from_str("Null pointer provided"));
1800 }
1801
1802 let sweep = FibonacciTrailingStopBatchRange {
1803 left_bars: (left_bars_start, left_bars_end, left_bars_step),
1804 right_bars: (right_bars_start, right_bars_end, right_bars_step),
1805 level: (level_start, level_end, level_step),
1806 trigger: Some(trigger),
1807 };
1808
1809 unsafe {
1810 let high = std::slice::from_raw_parts(high_ptr, len);
1811 let low = std::slice::from_raw_parts(low_ptr, len);
1812 let close = std::slice::from_raw_parts(close_ptr, len);
1813 let combos = expand_grid_fibonacci_trailing_stop(&sweep)
1814 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1815 let rows = combos.len();
1816 let total = rows
1817 .checked_mul(len)
1818 .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1819
1820 let output_ptrs = [
1821 trailing_stop_ptr as usize,
1822 long_stop_ptr as usize,
1823 short_stop_ptr as usize,
1824 direction_ptr as usize,
1825 ];
1826 let need_temp = output_ptrs.iter().any(|&ptr| {
1827 ptr == high_ptr as usize || ptr == low_ptr as usize || ptr == close_ptr as usize
1828 }) || has_duplicate_ptrs(&output_ptrs);
1829
1830 if need_temp {
1831 let mut trailing_stop = vec![0.0; total];
1832 let mut long_stop = vec![0.0; total];
1833 let mut short_stop = vec![0.0; total];
1834 let mut direction = vec![0.0; total];
1835 let rows = fibonacci_trailing_stop_batch_inner_into(
1836 high,
1837 low,
1838 close,
1839 &sweep,
1840 Kernel::Auto,
1841 false,
1842 &mut trailing_stop,
1843 &mut long_stop,
1844 &mut short_stop,
1845 &mut direction,
1846 )
1847 .map_err(|e| JsValue::from_str(&e.to_string()))?
1848 .len();
1849 std::slice::from_raw_parts_mut(trailing_stop_ptr, total)
1850 .copy_from_slice(&trailing_stop);
1851 std::slice::from_raw_parts_mut(long_stop_ptr, total).copy_from_slice(&long_stop);
1852 std::slice::from_raw_parts_mut(short_stop_ptr, total).copy_from_slice(&short_stop);
1853 std::slice::from_raw_parts_mut(direction_ptr, total).copy_from_slice(&direction);
1854 Ok(rows)
1855 } else {
1856 let rows = fibonacci_trailing_stop_batch_inner_into(
1857 high,
1858 low,
1859 close,
1860 &sweep,
1861 Kernel::Auto,
1862 false,
1863 std::slice::from_raw_parts_mut(trailing_stop_ptr, total),
1864 std::slice::from_raw_parts_mut(long_stop_ptr, total),
1865 std::slice::from_raw_parts_mut(short_stop_ptr, total),
1866 std::slice::from_raw_parts_mut(direction_ptr, total),
1867 )
1868 .map_err(|e| JsValue::from_str(&e.to_string()))?
1869 .len();
1870 Ok(rows)
1871 }
1872 }
1873}
1874
1875#[cfg(test)]
1876mod tests {
1877 use super::*;
1878 use std::error::Error;
1879
1880 fn sample_candles(length: usize) -> Candles {
1881 let close = (0..length)
1882 .map(|i| {
1883 let x = i as f64;
1884 100.0 + x * 0.03 + (x * 0.18).sin() * 4.0 + (x * 0.051).cos() * 1.2
1885 })
1886 .collect::<Vec<_>>();
1887 let open = close.iter().map(|v| v - 0.25).collect::<Vec<_>>();
1888 let high = close
1889 .iter()
1890 .enumerate()
1891 .map(|(i, v)| v + 0.9 + (i as f64 * 0.07).cos().abs() * 0.3)
1892 .collect::<Vec<_>>();
1893 let low = close
1894 .iter()
1895 .enumerate()
1896 .map(|(i, v)| v - 0.85 - (i as f64 * 0.05).sin().abs() * 0.25)
1897 .collect::<Vec<_>>();
1898 let volume = vec![1_000.0; length];
1899 Candles::new((0..length as i64).collect(), open, high, low, close, volume)
1900 }
1901
1902 fn assert_series_eq(left: &[f64], right: &[f64], tol: f64) {
1903 assert_eq!(left.len(), right.len());
1904 for (&lhs, &rhs) in left.iter().zip(right.iter()) {
1905 assert!(
1906 (lhs.is_nan() && rhs.is_nan()) || (lhs - rhs).abs() <= tol,
1907 "series mismatch: left={lhs:?}, right={rhs:?}"
1908 );
1909 }
1910 }
1911
1912 #[test]
1913 fn fibonacci_trailing_stop_output_contract() {
1914 let candles = sample_candles(320);
1915 let out =
1916 fibonacci_trailing_stop(&FibonacciTrailingStopInput::with_default_candles(&candles))
1917 .unwrap();
1918 assert_eq!(out.trailing_stop.len(), candles.close.len());
1919 assert_eq!(out.long_stop.len(), candles.close.len());
1920 assert_eq!(out.short_stop.len(), candles.close.len());
1921 assert_eq!(out.direction.len(), candles.close.len());
1922 assert!(out.trailing_stop[0].is_finite());
1923 assert!(out.direction.iter().filter(|v| v.is_finite()).all(|v| {
1924 (*v + 1.0).abs() <= FLOAT_TOL || v.abs() <= FLOAT_TOL || (*v - 1.0).abs() <= FLOAT_TOL
1925 }));
1926 }
1927
1928 #[test]
1929 fn fibonacci_trailing_stop_rejects_invalid_params() {
1930 let candles = sample_candles(16);
1931 let err = fibonacci_trailing_stop(&FibonacciTrailingStopInput::from_candles(
1932 &candles,
1933 FibonacciTrailingStopParams {
1934 left_bars: Some(0),
1935 right_bars: Some(1),
1936 level: Some(DEFAULT_LEVEL),
1937 trigger: Some(DEFAULT_TRIGGER.to_string()),
1938 },
1939 ))
1940 .unwrap_err();
1941 assert!(matches!(
1942 err,
1943 FibonacciTrailingStopError::InvalidLeftBars { .. }
1944 ));
1945
1946 let err = fibonacci_trailing_stop(&FibonacciTrailingStopInput::from_candles(
1947 &candles,
1948 FibonacciTrailingStopParams {
1949 left_bars: Some(4),
1950 right_bars: Some(1),
1951 level: Some(f64::NAN),
1952 trigger: Some(DEFAULT_TRIGGER.to_string()),
1953 },
1954 ))
1955 .unwrap_err();
1956 assert!(matches!(
1957 err,
1958 FibonacciTrailingStopError::InvalidLevel { .. }
1959 ));
1960 }
1961
1962 #[test]
1963 fn fibonacci_trailing_stop_builder_matches_direct() -> Result<(), Box<dyn Error>> {
1964 let candles = sample_candles(320);
1965 let direct = fibonacci_trailing_stop(&FibonacciTrailingStopInput::from_candles(
1966 &candles,
1967 FibonacciTrailingStopParams {
1968 left_bars: Some(12),
1969 right_bars: Some(2),
1970 level: Some(-0.236),
1971 trigger: Some("wick".to_string()),
1972 },
1973 ))?;
1974 let built = FibonacciTrailingStopBuilder::new()
1975 .left_bars(12)
1976 .right_bars(2)
1977 .level(-0.236)
1978 .trigger("wick")?
1979 .apply(&candles)?;
1980
1981 assert_series_eq(&built.trailing_stop, &direct.trailing_stop, 1e-12);
1982 assert_series_eq(&built.long_stop, &direct.long_stop, 1e-12);
1983 assert_series_eq(&built.short_stop, &direct.short_stop, 1e-12);
1984 assert_series_eq(&built.direction, &direct.direction, 1e-12);
1985 Ok(())
1986 }
1987
1988 #[test]
1989 fn fibonacci_trailing_stop_stream_matches_batch_with_reset() -> Result<(), Box<dyn Error>> {
1990 let candles = sample_candles(240);
1991 let mut high = candles.high.clone();
1992 let mut low = candles.low.clone();
1993 let mut close = candles.close.clone();
1994 high[120] = f64::NAN;
1995 low[120] = f64::NAN;
1996 close[120] = f64::NAN;
1997
1998 let batch = fibonacci_trailing_stop(&FibonacciTrailingStopInput::from_slices(
1999 &high,
2000 &low,
2001 &close,
2002 FibonacciTrailingStopParams {
2003 left_bars: Some(10),
2004 right_bars: Some(2),
2005 level: Some(-0.382),
2006 trigger: Some("close".to_string()),
2007 },
2008 ))?;
2009
2010 let mut stream = FibonacciTrailingStopBuilder::new()
2011 .left_bars(10)
2012 .right_bars(2)
2013 .level(-0.382)
2014 .into_stream()?;
2015
2016 let mut trailing_stop = Vec::with_capacity(close.len());
2017 let mut long_stop = Vec::with_capacity(close.len());
2018 let mut short_stop = Vec::with_capacity(close.len());
2019 let mut direction = Vec::with_capacity(close.len());
2020
2021 for i in 0..close.len() {
2022 match stream.update(high[i], low[i], close[i]) {
2023 Some(point) => {
2024 trailing_stop.push(point.trailing_stop);
2025 long_stop.push(point.long_stop);
2026 short_stop.push(point.short_stop);
2027 direction.push(point.direction);
2028 }
2029 None => {
2030 trailing_stop.push(f64::NAN);
2031 long_stop.push(f64::NAN);
2032 short_stop.push(f64::NAN);
2033 direction.push(f64::NAN);
2034 }
2035 }
2036 }
2037
2038 assert_series_eq(&trailing_stop, &batch.trailing_stop, 1e-12);
2039 assert_series_eq(&long_stop, &batch.long_stop, 1e-12);
2040 assert_series_eq(&short_stop, &batch.short_stop, 1e-12);
2041 assert_series_eq(&direction, &batch.direction, 1e-12);
2042 Ok(())
2043 }
2044
2045 #[test]
2046 fn fibonacci_trailing_stop_into_matches_main_api() -> Result<(), Box<dyn Error>> {
2047 let candles = sample_candles(192);
2048 let input = FibonacciTrailingStopInput::from_candles(
2049 &candles,
2050 FibonacciTrailingStopParams {
2051 left_bars: Some(14),
2052 right_bars: Some(1),
2053 level: Some(-0.382),
2054 trigger: Some("close".to_string()),
2055 },
2056 );
2057 let direct = fibonacci_trailing_stop(&input)?;
2058 let mut trailing_stop = vec![f64::NAN; candles.close.len()];
2059 let mut long_stop = vec![f64::NAN; candles.close.len()];
2060 let mut short_stop = vec![f64::NAN; candles.close.len()];
2061 let mut direction = vec![f64::NAN; candles.close.len()];
2062
2063 fibonacci_trailing_stop_into_slices(
2064 &mut trailing_stop,
2065 &mut long_stop,
2066 &mut short_stop,
2067 &mut direction,
2068 &input,
2069 Kernel::Auto,
2070 )?;
2071
2072 assert_series_eq(&trailing_stop, &direct.trailing_stop, 1e-12);
2073 assert_series_eq(&long_stop, &direct.long_stop, 1e-12);
2074 assert_series_eq(&short_stop, &direct.short_stop, 1e-12);
2075 assert_series_eq(&direction, &direct.direction, 1e-12);
2076 Ok(())
2077 }
2078
2079 #[test]
2080 fn fibonacci_trailing_stop_batch_single_param_matches_single() -> Result<(), Box<dyn Error>> {
2081 let candles = sample_candles(200);
2082 let batch = FibonacciTrailingStopBatchBuilder::new()
2083 .left_bars_range(12, 12, 0)
2084 .right_bars_range(2, 2, 0)
2085 .level_range(-0.236, -0.236, 0.0)
2086 .trigger("wick")
2087 .apply_candles(&candles)?;
2088 let single = fibonacci_trailing_stop(&FibonacciTrailingStopInput::from_candles(
2089 &candles,
2090 FibonacciTrailingStopParams {
2091 left_bars: Some(12),
2092 right_bars: Some(2),
2093 level: Some(-0.236),
2094 trigger: Some("wick".to_string()),
2095 },
2096 ))?;
2097
2098 assert_eq!(batch.rows, 1);
2099 assert_eq!(batch.cols, candles.close.len());
2100 assert_eq!(batch.combos.len(), 1);
2101 assert_series_eq(
2102 &batch.trailing_stop[..batch.cols],
2103 &single.trailing_stop,
2104 1e-12,
2105 );
2106 assert_series_eq(&batch.long_stop[..batch.cols], &single.long_stop, 1e-12);
2107 assert_series_eq(&batch.short_stop[..batch.cols], &single.short_stop, 1e-12);
2108 assert_series_eq(&batch.direction[..batch.cols], &single.direction, 1e-12);
2109 Ok(())
2110 }
2111
2112 #[test]
2113 fn fibonacci_trailing_stop_batch_metadata() -> Result<(), Box<dyn Error>> {
2114 let candles = sample_candles(180);
2115 let out = FibonacciTrailingStopBatchBuilder::new()
2116 .left_bars_range(10, 12, 2)
2117 .right_bars_range(1, 2, 1)
2118 .level_range(-0.382, -0.236, 0.146)
2119 .apply_candles(&candles)?;
2120
2121 assert_eq!(out.rows, 8);
2122 assert_eq!(out.cols, candles.close.len());
2123 assert_eq!(out.trailing_stop.len(), out.rows * out.cols);
2124 assert_eq!(out.long_stop.len(), out.rows * out.cols);
2125 assert_eq!(out.short_stop.len(), out.rows * out.cols);
2126 assert_eq!(out.direction.len(), out.rows * out.cols);
2127 assert_eq!(out.combos.len(), out.rows);
2128 Ok(())
2129 }
2130}