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