1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::Candles;
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, make_uninit_matrix,
19};
20#[cfg(feature = "python")]
21use crate::utilities::kernel_validation::validate_kernel;
22#[cfg(not(target_arch = "wasm32"))]
23use rayon::prelude::*;
24#[cfg(test)]
25use std::error::Error as StdError;
26use std::mem::ManuallyDrop;
27use thiserror::Error;
28
29const DEFAULT_RANGE_LENGTH: usize = 20;
30const DEFAULT_CONFIRMATION_LENGTH: usize = 5;
31const ATR_LENGTH: usize = 14;
32const ATR_MULTIPLIER: f64 = 1.2;
33const VOLATILITY_THRESHOLD: f64 = 1.2;
34const BULLISH_LOCATION_WEIGHT: f64 = 0.15;
35const BEARISH_LOCATION_WEIGHT: f64 = 0.85;
36
37#[inline(always)]
38fn open_source(candles: &Candles) -> &[f64] {
39 &candles.open
40}
41
42#[inline(always)]
43fn high_source(candles: &Candles) -> &[f64] {
44 &candles.high
45}
46
47#[inline(always)]
48fn low_source(candles: &Candles) -> &[f64] {
49 &candles.low
50}
51
52#[inline(always)]
53fn close_source(candles: &Candles) -> &[f64] {
54 &candles.close
55}
56
57#[inline(always)]
58fn volume_source(candles: &Candles) -> &[f64] {
59 &candles.volume
60}
61
62#[derive(Debug, Clone)]
63pub enum RangeBreakoutSignalsData<'a> {
64 Candles {
65 candles: &'a Candles,
66 },
67 Slices {
68 open: &'a [f64],
69 high: &'a [f64],
70 low: &'a [f64],
71 close: &'a [f64],
72 volume: &'a [f64],
73 },
74}
75
76#[derive(Debug, Clone)]
77#[cfg_attr(
78 all(target_arch = "wasm32", feature = "wasm"),
79 derive(Serialize, Deserialize)
80)]
81pub struct RangeBreakoutSignalsOutput {
82 pub range_top: Vec<f64>,
83 pub range_bottom: Vec<f64>,
84 pub bullish: Vec<f64>,
85 pub extra_bullish: Vec<f64>,
86 pub bearish: Vec<f64>,
87 pub extra_bearish: Vec<f64>,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
91#[cfg_attr(
92 all(target_arch = "wasm32", feature = "wasm"),
93 derive(Serialize, Deserialize)
94)]
95pub struct RangeBreakoutSignalsParams {
96 pub range_length: Option<usize>,
97 pub confirmation_length: Option<usize>,
98}
99
100impl Default for RangeBreakoutSignalsParams {
101 fn default() -> Self {
102 Self {
103 range_length: Some(DEFAULT_RANGE_LENGTH),
104 confirmation_length: Some(DEFAULT_CONFIRMATION_LENGTH),
105 }
106 }
107}
108
109#[derive(Debug, Clone)]
110pub struct RangeBreakoutSignalsInput<'a> {
111 pub data: RangeBreakoutSignalsData<'a>,
112 pub params: RangeBreakoutSignalsParams,
113}
114
115impl<'a> RangeBreakoutSignalsInput<'a> {
116 #[inline(always)]
117 pub fn from_candles(candles: &'a Candles, params: RangeBreakoutSignalsParams) -> Self {
118 Self {
119 data: RangeBreakoutSignalsData::Candles { candles },
120 params,
121 }
122 }
123
124 #[inline(always)]
125 pub fn from_slices(
126 open: &'a [f64],
127 high: &'a [f64],
128 low: &'a [f64],
129 close: &'a [f64],
130 volume: &'a [f64],
131 params: RangeBreakoutSignalsParams,
132 ) -> Self {
133 Self {
134 data: RangeBreakoutSignalsData::Slices {
135 open,
136 high,
137 low,
138 close,
139 volume,
140 },
141 params,
142 }
143 }
144
145 #[inline(always)]
146 pub fn with_default_candles(candles: &'a Candles) -> Self {
147 Self::from_candles(candles, RangeBreakoutSignalsParams::default())
148 }
149
150 #[inline(always)]
151 pub fn get_range_length(&self) -> usize {
152 self.params.range_length.unwrap_or(DEFAULT_RANGE_LENGTH)
153 }
154
155 #[inline(always)]
156 pub fn get_confirmation_length(&self) -> usize {
157 self.params
158 .confirmation_length
159 .unwrap_or(DEFAULT_CONFIRMATION_LENGTH)
160 }
161
162 #[inline(always)]
163 fn as_ohlcv(&self) -> (&'a [f64], &'a [f64], &'a [f64], &'a [f64], &'a [f64]) {
164 match &self.data {
165 RangeBreakoutSignalsData::Candles { candles } => (
166 open_source(candles),
167 high_source(candles),
168 low_source(candles),
169 close_source(candles),
170 volume_source(candles),
171 ),
172 RangeBreakoutSignalsData::Slices {
173 open,
174 high,
175 low,
176 close,
177 volume,
178 } => (*open, *high, *low, *close, *volume),
179 }
180 }
181}
182
183impl<'a> AsRef<[f64]> for RangeBreakoutSignalsInput<'a> {
184 #[inline(always)]
185 fn as_ref(&self) -> &[f64] {
186 self.as_ohlcv().3
187 }
188}
189
190#[derive(Clone, Debug)]
191pub struct RangeBreakoutSignalsBuilder {
192 range_length: Option<usize>,
193 confirmation_length: Option<usize>,
194 kernel: Kernel,
195}
196
197impl Default for RangeBreakoutSignalsBuilder {
198 fn default() -> Self {
199 Self {
200 range_length: None,
201 confirmation_length: None,
202 kernel: Kernel::Auto,
203 }
204 }
205}
206
207impl RangeBreakoutSignalsBuilder {
208 #[inline(always)]
209 pub fn new() -> Self {
210 Self::default()
211 }
212
213 #[inline(always)]
214 pub fn range_length(mut self, value: usize) -> Self {
215 self.range_length = Some(value);
216 self
217 }
218
219 #[inline(always)]
220 pub fn confirmation_length(mut self, value: usize) -> Self {
221 self.confirmation_length = Some(value);
222 self
223 }
224
225 #[inline(always)]
226 pub fn kernel(mut self, kernel: Kernel) -> Self {
227 self.kernel = kernel;
228 self
229 }
230
231 #[inline(always)]
232 fn params(self) -> RangeBreakoutSignalsParams {
233 RangeBreakoutSignalsParams {
234 range_length: self.range_length,
235 confirmation_length: self.confirmation_length,
236 }
237 }
238
239 #[inline(always)]
240 pub fn apply(
241 self,
242 candles: &Candles,
243 ) -> Result<RangeBreakoutSignalsOutput, RangeBreakoutSignalsError> {
244 let kernel = self.kernel;
245 let input = RangeBreakoutSignalsInput::from_candles(candles, self.params());
246 range_breakout_signals_with_kernel(&input, kernel)
247 }
248
249 #[inline(always)]
250 pub fn apply_slices(
251 self,
252 open: &[f64],
253 high: &[f64],
254 low: &[f64],
255 close: &[f64],
256 volume: &[f64],
257 ) -> Result<RangeBreakoutSignalsOutput, RangeBreakoutSignalsError> {
258 let kernel = self.kernel;
259 let input =
260 RangeBreakoutSignalsInput::from_slices(open, high, low, close, volume, self.params());
261 range_breakout_signals_with_kernel(&input, kernel)
262 }
263
264 #[inline(always)]
265 pub fn into_stream(self) -> Result<RangeBreakoutSignalsStream, RangeBreakoutSignalsError> {
266 RangeBreakoutSignalsStream::try_new(self.params())
267 }
268}
269
270#[derive(Debug, Error)]
271pub enum RangeBreakoutSignalsError {
272 #[error("range_breakout_signals: input data slice is empty.")]
273 EmptyInputData,
274 #[error("range_breakout_signals: all values are NaN.")]
275 AllValuesNaN,
276 #[error(
277 "range_breakout_signals: inconsistent slice lengths: open={open_len}, high={high_len}, low={low_len}, close={close_len}, volume={volume_len}"
278 )]
279 InconsistentSliceLengths {
280 open_len: usize,
281 high_len: usize,
282 low_len: usize,
283 close_len: usize,
284 volume_len: usize,
285 },
286 #[error(
287 "range_breakout_signals: invalid range_length: range_length = {range_length}, data length = {data_len}"
288 )]
289 InvalidRangeLength {
290 range_length: usize,
291 data_len: usize,
292 },
293 #[error(
294 "range_breakout_signals: invalid confirmation_length: confirmation_length = {confirmation_length}"
295 )]
296 InvalidConfirmationLength { confirmation_length: usize },
297 #[error("range_breakout_signals: not enough valid data: needed = {needed}, valid = {valid}")]
298 NotEnoughValidData { needed: usize, valid: usize },
299 #[error("range_breakout_signals: output length mismatch: expected = {expected}, got = {got}")]
300 OutputLengthMismatch { expected: usize, got: usize },
301 #[error("range_breakout_signals: invalid range for {axis}: start = {start}, end = {end}, step = {step}")]
302 InvalidRange {
303 axis: &'static str,
304 start: String,
305 end: String,
306 step: String,
307 },
308 #[error("range_breakout_signals: invalid kernel for batch: {0:?}")]
309 InvalidKernelForBatch(Kernel),
310}
311
312#[derive(Clone, Copy, Debug)]
313struct PreparedRangeBreakoutSignals<'a> {
314 open: &'a [f64],
315 high: &'a [f64],
316 low: &'a [f64],
317 close: &'a [f64],
318 volume: &'a [f64],
319 range_length: usize,
320 confirmation_length: usize,
321 warmup: usize,
322}
323
324#[derive(Clone, Copy, Debug)]
325struct ActiveRange {
326 top: f64,
327 bottom: f64,
328}
329
330#[derive(Clone, Debug)]
331struct MedianSmaWindow {
332 len: usize,
333 ring: Vec<f64>,
334 sorted: Vec<f64>,
335 head: usize,
336 count: usize,
337 sum: f64,
338}
339
340impl MedianSmaWindow {
341 #[inline(always)]
342 fn new(len: usize) -> Self {
343 Self {
344 len,
345 ring: vec![0.0; len],
346 sorted: Vec::with_capacity(len),
347 head: 0,
348 count: 0,
349 sum: 0.0,
350 }
351 }
352
353 #[inline(always)]
354 fn reset(&mut self) {
355 self.sorted.clear();
356 self.head = 0;
357 self.count = 0;
358 self.sum = 0.0;
359 }
360
361 #[inline(always)]
362 fn push(&mut self, value: f64) -> Option<(f64, f64)> {
363 if self.count == self.len {
364 let old = self.ring[self.head];
365 self.sum -= old;
366 if let Some(index) = self.sorted.iter().position(|probe| *probe == old) {
367 self.sorted.remove(index);
368 }
369 self.ring[self.head] = value;
370 self.head = (self.head + 1) % self.len;
371 } else {
372 self.ring[self.count] = value;
373 self.count += 1;
374 if self.count == self.len {
375 self.head = 0;
376 }
377 }
378
379 self.sum += value;
380 let index = self.sorted.partition_point(|probe| *probe <= value);
381 self.sorted.insert(index, value);
382
383 if self.count < self.len {
384 return None;
385 }
386
387 let median = if self.len & 1 == 1 {
388 self.sorted[self.len >> 1]
389 } else {
390 let upper = self.len >> 1;
391 (self.sorted[upper - 1] + self.sorted[upper]) * 0.5
392 };
393 Some((median, self.sum / self.len as f64))
394 }
395}
396
397#[derive(Clone, Debug)]
398struct AtrState {
399 len: usize,
400 count: usize,
401 sum: f64,
402 value: f64,
403 prev_close: f64,
404 have_prev_close: bool,
405}
406
407impl AtrState {
408 #[inline(always)]
409 fn new(len: usize) -> Self {
410 Self {
411 len,
412 count: 0,
413 sum: 0.0,
414 value: f64::NAN,
415 prev_close: f64::NAN,
416 have_prev_close: false,
417 }
418 }
419
420 #[inline(always)]
421 fn reset(&mut self) {
422 self.count = 0;
423 self.sum = 0.0;
424 self.value = f64::NAN;
425 self.prev_close = f64::NAN;
426 self.have_prev_close = false;
427 }
428
429 #[inline(always)]
430 fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
431 let prev_close = if self.have_prev_close {
432 self.prev_close
433 } else {
434 close
435 };
436 let tr = (high - low)
437 .max((high - prev_close).abs())
438 .max((low - prev_close).abs());
439 self.prev_close = close;
440 self.have_prev_close = true;
441
442 if self.count < self.len {
443 self.count += 1;
444 self.sum += tr;
445 if self.count < self.len {
446 return None;
447 }
448 self.value = self.sum / self.len as f64;
449 return Some(self.value);
450 }
451
452 self.value = ((self.value * (self.len - 1) as f64) + tr) / self.len as f64;
453 Some(self.value)
454 }
455}
456
457#[derive(Clone, Debug)]
458struct VolumeWindow {
459 len: usize,
460 up_ring: Vec<f64>,
461 down_ring: Vec<f64>,
462 head: usize,
463 count: usize,
464 up_sum: f64,
465 down_sum: f64,
466}
467
468impl VolumeWindow {
469 #[inline(always)]
470 fn new(len: usize) -> Self {
471 Self {
472 len,
473 up_ring: vec![0.0; len],
474 down_ring: vec![0.0; len],
475 head: 0,
476 count: 0,
477 up_sum: 0.0,
478 down_sum: 0.0,
479 }
480 }
481
482 #[inline(always)]
483 fn reset(&mut self) {
484 self.head = 0;
485 self.count = 0;
486 self.up_sum = 0.0;
487 self.down_sum = 0.0;
488 }
489
490 #[inline(always)]
491 fn push(&mut self, up: f64, down: f64) {
492 if self.count == self.len {
493 self.up_sum -= self.up_ring[self.head];
494 self.down_sum -= self.down_ring[self.head];
495 self.up_ring[self.head] = up;
496 self.down_ring[self.head] = down;
497 self.head = (self.head + 1) % self.len;
498 } else {
499 self.up_ring[self.count] = up;
500 self.down_ring[self.count] = down;
501 self.count += 1;
502 if self.count == self.len {
503 self.head = 0;
504 }
505 }
506 self.up_sum += up;
507 self.down_sum += down;
508 }
509
510 #[inline(always)]
511 fn is_full(&self) -> bool {
512 self.count == self.len
513 }
514}
515
516#[derive(Clone, Debug)]
517struct BoolWindow {
518 ring: Vec<bool>,
519 head: usize,
520 count: usize,
521}
522
523impl BoolWindow {
524 #[inline(always)]
525 fn new(len: usize) -> Self {
526 Self {
527 ring: vec![false; len],
528 head: 0,
529 count: 0,
530 }
531 }
532
533 #[inline(always)]
534 fn reset(&mut self) {
535 self.head = 0;
536 self.count = 0;
537 }
538
539 #[inline(always)]
540 fn push(&mut self, value: bool) {
541 if self.count == self.ring.len() {
542 self.ring[self.head] = value;
543 self.head = (self.head + 1) % self.ring.len();
544 } else {
545 self.ring[self.count] = value;
546 self.count += 1;
547 if self.count == self.ring.len() {
548 self.head = 0;
549 }
550 }
551 }
552
553 #[inline(always)]
554 fn get_ago(&self, ago: usize) -> Option<bool> {
555 if ago >= self.count {
556 return None;
557 }
558 if self.count < self.ring.len() {
559 return Some(self.ring[self.count - 1 - ago]);
560 }
561 let len = self.ring.len();
562 let latest = if self.head == 0 {
563 len - 1
564 } else {
565 self.head - 1
566 };
567 Some(self.ring[(latest + len - ago) % len])
568 }
569
570 #[inline(always)]
571 fn is_full(&self) -> bool {
572 self.count == self.ring.len()
573 }
574}
575
576#[derive(Clone, Debug)]
577struct RangeBreakoutSignalsState {
578 confirmation_length: usize,
579 dist_window: MedianSmaWindow,
580 atr_state: AtrState,
581 volume_window: VolumeWindow,
582 under_window: BoolWindow,
583 prev_volatility: f64,
584 active_range: Option<ActiveRange>,
585}
586
587impl RangeBreakoutSignalsState {
588 #[inline(always)]
589 fn new(range_length: usize, confirmation_length: usize) -> Self {
590 Self {
591 confirmation_length,
592 dist_window: MedianSmaWindow::new(range_length),
593 atr_state: AtrState::new(ATR_LENGTH),
594 volume_window: VolumeWindow::new(confirmation_length + 1),
595 under_window: BoolWindow::new(confirmation_length + 1),
596 prev_volatility: f64::NAN,
597 active_range: None,
598 }
599 }
600
601 #[inline(always)]
602 fn reset(&mut self) {
603 self.dist_window.reset();
604 self.atr_state.reset();
605 self.volume_window.reset();
606 self.under_window.reset();
607 self.prev_volatility = f64::NAN;
608 self.active_range = None;
609 }
610
611 #[inline(always)]
612 fn split_volume(open: f64, close: f64, volume: f64) -> (f64, f64) {
613 if close > open {
614 (volume, 0.0)
615 } else if close < open {
616 (0.0, volume)
617 } else {
618 let half = volume * 0.5;
619 (half, half)
620 }
621 }
622
623 #[inline(always)]
624 fn location(range: ActiveRange, bullish: bool) -> f64 {
625 let span = range.top - range.bottom;
626 let weight = if bullish {
627 BULLISH_LOCATION_WEIGHT
628 } else {
629 BEARISH_LOCATION_WEIGHT
630 };
631 range.bottom + span * weight
632 }
633
634 #[inline(always)]
635 fn update(
636 &mut self,
637 open: f64,
638 high: f64,
639 low: f64,
640 close: f64,
641 volume: f64,
642 ) -> Option<(f64, f64, f64, f64, f64, f64)> {
643 if !open.is_finite()
644 || !high.is_finite()
645 || !low.is_finite()
646 || !close.is_finite()
647 || !volume.is_finite()
648 {
649 self.reset();
650 return None;
651 }
652
653 let previous_volatility = self.prev_volatility;
654 let atr = self.atr_state.update(high, low, close);
655 let volatility = self
656 .dist_window
657 .push((close - open).abs())
658 .and_then(|(median, mean)| (median > 0.0).then_some(mean / median));
659
660 let current_isunder = volatility.is_some_and(|value| value < VOLATILITY_THRESHOLD);
661 let (up_volume, down_volume) = Self::split_volume(open, close, volume);
662 self.volume_window.push(up_volume, down_volume);
663 self.under_window.push(current_isunder);
664
665 let ready = volatility.is_some()
666 && atr.is_some()
667 && previous_volatility.is_finite()
668 && self.volume_window.is_full()
669 && self.under_window.is_full();
670
671 if ready {
672 let under_ago = self
673 .under_window
674 .get_ago(self.confirmation_length)
675 .unwrap_or(false);
676 let current_volatility = volatility.unwrap_or(f64::NAN);
677 let crossed_under = previous_volatility >= VOLATILITY_THRESHOLD
678 && current_volatility < VOLATILITY_THRESHOLD;
679 if self.active_range.is_none() && crossed_under && current_isunder && under_ago {
680 let offset = atr.unwrap_or(f64::NAN) * ATR_MULTIPLIER;
681 self.active_range = Some(ActiveRange {
682 top: close + offset,
683 bottom: close - offset,
684 });
685 }
686 }
687
688 let mut range_top = f64::NAN;
689 let mut range_bottom = f64::NAN;
690 let mut bullish = f64::NAN;
691 let mut extra_bullish = f64::NAN;
692 let mut bearish = f64::NAN;
693 let mut extra_bearish = f64::NAN;
694
695 if let Some(range) = self.active_range {
696 range_top = range.top;
697 range_bottom = range.bottom;
698
699 if close > range.top || close < range.bottom {
700 let bullish_break = close > range.top;
701 let location = Self::location(range, bullish_break);
702 let bullish_volume = self.volume_window.up_sum > self.volume_window.down_sum;
703
704 if bullish_break {
705 bullish = location;
706 if bullish_volume {
707 extra_bullish = location;
708 }
709 } else {
710 bearish = location;
711 if !bullish_volume {
712 extra_bearish = location;
713 }
714 }
715
716 self.active_range = None;
717 }
718 }
719
720 self.prev_volatility = volatility.unwrap_or(f64::NAN);
721
722 ready.then_some((
723 range_top,
724 range_bottom,
725 bullish,
726 extra_bullish,
727 bearish,
728 extra_bearish,
729 ))
730 }
731}
732
733#[derive(Clone, Debug)]
734pub struct RangeBreakoutSignalsStream {
735 params: RangeBreakoutSignalsParams,
736 state: RangeBreakoutSignalsState,
737}
738
739impl RangeBreakoutSignalsStream {
740 #[inline(always)]
741 pub fn try_new(params: RangeBreakoutSignalsParams) -> Result<Self, RangeBreakoutSignalsError> {
742 let range_length = params.range_length.unwrap_or(DEFAULT_RANGE_LENGTH);
743 let confirmation_length = params
744 .confirmation_length
745 .unwrap_or(DEFAULT_CONFIRMATION_LENGTH);
746 validate_params(range_length, confirmation_length, usize::MAX)?;
747 Ok(Self {
748 params,
749 state: RangeBreakoutSignalsState::new(range_length, confirmation_length),
750 })
751 }
752
753 #[inline(always)]
754 pub fn update(
755 &mut self,
756 open: f64,
757 high: f64,
758 low: f64,
759 close: f64,
760 volume: f64,
761 ) -> Option<(f64, f64, f64, f64, f64, f64)> {
762 self.state.update(open, high, low, close, volume)
763 }
764
765 #[inline(always)]
766 pub fn reset(&mut self) {
767 self.state = RangeBreakoutSignalsState::new(
768 self.params.range_length.unwrap_or(DEFAULT_RANGE_LENGTH),
769 self.params
770 .confirmation_length
771 .unwrap_or(DEFAULT_CONFIRMATION_LENGTH),
772 );
773 }
774}
775
776#[inline(always)]
777fn required_valid_bars(range_length: usize, confirmation_length: usize) -> usize {
778 (range_length + 1)
779 .max(ATR_LENGTH)
780 .max(confirmation_length + 1)
781}
782
783#[inline(always)]
784fn validate_params(
785 range_length: usize,
786 confirmation_length: usize,
787 data_len: usize,
788) -> Result<(), RangeBreakoutSignalsError> {
789 if range_length == 0 {
790 return Err(RangeBreakoutSignalsError::InvalidRangeLength {
791 range_length,
792 data_len,
793 });
794 }
795 if confirmation_length == 0 {
796 return Err(RangeBreakoutSignalsError::InvalidConfirmationLength {
797 confirmation_length,
798 });
799 }
800 if data_len != usize::MAX && range_length > data_len {
801 return Err(RangeBreakoutSignalsError::InvalidRangeLength {
802 range_length,
803 data_len,
804 });
805 }
806 Ok(())
807}
808
809fn analyze_valid_segments(
810 open: &[f64],
811 high: &[f64],
812 low: &[f64],
813 close: &[f64],
814 volume: &[f64],
815) -> Result<(usize, usize), RangeBreakoutSignalsError> {
816 if open.is_empty() {
817 return Err(RangeBreakoutSignalsError::EmptyInputData);
818 }
819 if open.len() != high.len()
820 || open.len() != low.len()
821 || open.len() != close.len()
822 || open.len() != volume.len()
823 {
824 return Err(RangeBreakoutSignalsError::InconsistentSliceLengths {
825 open_len: open.len(),
826 high_len: high.len(),
827 low_len: low.len(),
828 close_len: close.len(),
829 volume_len: volume.len(),
830 });
831 }
832
833 let mut run = 0usize;
834 let mut max_run = 0usize;
835 let mut valid = 0usize;
836 for i in 0..open.len() {
837 if open[i].is_finite()
838 && high[i].is_finite()
839 && low[i].is_finite()
840 && close[i].is_finite()
841 && volume[i].is_finite()
842 {
843 valid += 1;
844 run += 1;
845 max_run = max_run.max(run);
846 } else {
847 run = 0;
848 }
849 }
850
851 if valid == 0 {
852 return Err(RangeBreakoutSignalsError::AllValuesNaN);
853 }
854
855 Ok((valid, max_run))
856}
857
858fn prepare_input<'a>(
859 input: &'a RangeBreakoutSignalsInput<'a>,
860 kernel: Kernel,
861) -> Result<PreparedRangeBreakoutSignals<'a>, RangeBreakoutSignalsError> {
862 if matches!(kernel, Kernel::Auto) {
863 let _ = detect_best_kernel();
864 }
865
866 let (open, high, low, close, volume) = input.as_ohlcv();
867 let range_length = input.get_range_length();
868 let confirmation_length = input.get_confirmation_length();
869 validate_params(range_length, confirmation_length, close.len())?;
870
871 let (_, max_run) = analyze_valid_segments(open, high, low, close, volume)?;
872 let needed = required_valid_bars(range_length, confirmation_length);
873 if max_run < needed {
874 return Err(RangeBreakoutSignalsError::NotEnoughValidData {
875 needed,
876 valid: max_run,
877 });
878 }
879
880 Ok(PreparedRangeBreakoutSignals {
881 open,
882 high,
883 low,
884 close,
885 volume,
886 range_length,
887 confirmation_length,
888 warmup: needed - 1,
889 })
890}
891
892fn compute_row(
893 open: &[f64],
894 high: &[f64],
895 low: &[f64],
896 close: &[f64],
897 volume: &[f64],
898 range_length: usize,
899 confirmation_length: usize,
900 range_top_out: &mut [f64],
901 range_bottom_out: &mut [f64],
902 bullish_out: &mut [f64],
903 extra_bullish_out: &mut [f64],
904 bearish_out: &mut [f64],
905 extra_bearish_out: &mut [f64],
906) -> Result<(), RangeBreakoutSignalsError> {
907 let expected = close.len();
908 for out in [
909 &mut *range_top_out,
910 &mut *range_bottom_out,
911 &mut *bullish_out,
912 &mut *extra_bullish_out,
913 &mut *bearish_out,
914 &mut *extra_bearish_out,
915 ] {
916 if out.len() != expected {
917 return Err(RangeBreakoutSignalsError::OutputLengthMismatch {
918 expected,
919 got: out.len(),
920 });
921 }
922 }
923
924 let mut state = RangeBreakoutSignalsState::new(range_length, confirmation_length);
925 for i in 0..expected {
926 if let Some((rt, rb, b, eb, br, ebr)) =
927 state.update(open[i], high[i], low[i], close[i], volume[i])
928 {
929 range_top_out[i] = rt;
930 range_bottom_out[i] = rb;
931 bullish_out[i] = b;
932 extra_bullish_out[i] = eb;
933 bearish_out[i] = br;
934 extra_bearish_out[i] = ebr;
935 } else {
936 range_top_out[i] = f64::NAN;
937 range_bottom_out[i] = f64::NAN;
938 bullish_out[i] = f64::NAN;
939 extra_bullish_out[i] = f64::NAN;
940 bearish_out[i] = f64::NAN;
941 extra_bearish_out[i] = f64::NAN;
942 }
943 }
944
945 Ok(())
946}
947
948#[inline]
949pub fn range_breakout_signals(
950 input: &RangeBreakoutSignalsInput,
951) -> Result<RangeBreakoutSignalsOutput, RangeBreakoutSignalsError> {
952 range_breakout_signals_with_kernel(input, Kernel::Auto)
953}
954
955pub fn range_breakout_signals_with_kernel(
956 input: &RangeBreakoutSignalsInput,
957 kernel: Kernel,
958) -> Result<RangeBreakoutSignalsOutput, RangeBreakoutSignalsError> {
959 let prepared = prepare_input(input, kernel)?;
960 let len = prepared.close.len();
961 let warmup = prepared.warmup;
962 let mut range_top = alloc_with_nan_prefix(len, warmup);
963 let mut range_bottom = alloc_with_nan_prefix(len, warmup);
964 let mut bullish = alloc_with_nan_prefix(len, warmup);
965 let mut extra_bullish = alloc_with_nan_prefix(len, warmup);
966 let mut bearish = alloc_with_nan_prefix(len, warmup);
967 let mut extra_bearish = alloc_with_nan_prefix(len, warmup);
968
969 compute_row(
970 prepared.open,
971 prepared.high,
972 prepared.low,
973 prepared.close,
974 prepared.volume,
975 prepared.range_length,
976 prepared.confirmation_length,
977 &mut range_top,
978 &mut range_bottom,
979 &mut bullish,
980 &mut extra_bullish,
981 &mut bearish,
982 &mut extra_bearish,
983 )?;
984
985 Ok(RangeBreakoutSignalsOutput {
986 range_top,
987 range_bottom,
988 bullish,
989 extra_bullish,
990 bearish,
991 extra_bearish,
992 })
993}
994
995#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
996pub fn range_breakout_signals_into(
997 range_top_out: &mut [f64],
998 range_bottom_out: &mut [f64],
999 bullish_out: &mut [f64],
1000 extra_bullish_out: &mut [f64],
1001 bearish_out: &mut [f64],
1002 extra_bearish_out: &mut [f64],
1003 input: &RangeBreakoutSignalsInput,
1004) -> Result<(), RangeBreakoutSignalsError> {
1005 range_breakout_signals_into_slice(
1006 range_top_out,
1007 range_bottom_out,
1008 bullish_out,
1009 extra_bullish_out,
1010 bearish_out,
1011 extra_bearish_out,
1012 input,
1013 Kernel::Auto,
1014 )
1015}
1016
1017pub fn range_breakout_signals_into_slice(
1018 range_top_out: &mut [f64],
1019 range_bottom_out: &mut [f64],
1020 bullish_out: &mut [f64],
1021 extra_bullish_out: &mut [f64],
1022 bearish_out: &mut [f64],
1023 extra_bearish_out: &mut [f64],
1024 input: &RangeBreakoutSignalsInput,
1025 kernel: Kernel,
1026) -> Result<(), RangeBreakoutSignalsError> {
1027 let prepared = prepare_input(input, kernel)?;
1028 compute_row(
1029 prepared.open,
1030 prepared.high,
1031 prepared.low,
1032 prepared.close,
1033 prepared.volume,
1034 prepared.range_length,
1035 prepared.confirmation_length,
1036 range_top_out,
1037 range_bottom_out,
1038 bullish_out,
1039 extra_bullish_out,
1040 bearish_out,
1041 extra_bearish_out,
1042 )
1043}
1044
1045#[derive(Clone, Debug)]
1046pub struct RangeBreakoutSignalsBatchRange {
1047 pub range_length: (usize, usize, usize),
1048 pub confirmation_length: (usize, usize, usize),
1049}
1050
1051impl Default for RangeBreakoutSignalsBatchRange {
1052 fn default() -> Self {
1053 Self {
1054 range_length: (DEFAULT_RANGE_LENGTH, DEFAULT_RANGE_LENGTH, 0),
1055 confirmation_length: (DEFAULT_CONFIRMATION_LENGTH, DEFAULT_CONFIRMATION_LENGTH, 0),
1056 }
1057 }
1058}
1059
1060#[derive(Clone, Debug, Default)]
1061pub struct RangeBreakoutSignalsBatchBuilder {
1062 range: RangeBreakoutSignalsBatchRange,
1063 kernel: Kernel,
1064}
1065
1066impl RangeBreakoutSignalsBatchBuilder {
1067 #[inline(always)]
1068 pub fn new() -> Self {
1069 Self::default()
1070 }
1071
1072 #[inline(always)]
1073 pub fn kernel(mut self, kernel: Kernel) -> Self {
1074 self.kernel = kernel;
1075 self
1076 }
1077
1078 #[inline(always)]
1079 pub fn range_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1080 self.range.range_length = (start, end, step);
1081 self
1082 }
1083
1084 #[inline(always)]
1085 pub fn confirmation_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1086 self.range.confirmation_length = (start, end, step);
1087 self
1088 }
1089}
1090
1091#[derive(Debug, Clone)]
1092#[cfg_attr(
1093 all(target_arch = "wasm32", feature = "wasm"),
1094 derive(Serialize, Deserialize)
1095)]
1096pub struct RangeBreakoutSignalsBatchOutput {
1097 pub range_top: Vec<f64>,
1098 pub range_bottom: Vec<f64>,
1099 pub bullish: Vec<f64>,
1100 pub extra_bullish: Vec<f64>,
1101 pub bearish: Vec<f64>,
1102 pub extra_bearish: Vec<f64>,
1103 pub combos: Vec<RangeBreakoutSignalsParams>,
1104 pub rows: usize,
1105 pub cols: usize,
1106}
1107
1108#[inline(always)]
1109fn axis_usize(
1110 axis: &'static str,
1111 (start, end, step): (usize, usize, usize),
1112) -> Result<Vec<usize>, RangeBreakoutSignalsError> {
1113 if start == end || step == 0 {
1114 return Ok(vec![start]);
1115 }
1116 let mut out = Vec::new();
1117 if start < end {
1118 let mut value = start;
1119 while value <= end {
1120 out.push(value);
1121 value =
1122 value
1123 .checked_add(step)
1124 .ok_or_else(|| RangeBreakoutSignalsError::InvalidRange {
1125 axis,
1126 start: start.to_string(),
1127 end: end.to_string(),
1128 step: step.to_string(),
1129 })?;
1130 }
1131 } else {
1132 let mut value = start;
1133 while value >= end {
1134 out.push(value);
1135 value =
1136 value
1137 .checked_sub(step)
1138 .ok_or_else(|| RangeBreakoutSignalsError::InvalidRange {
1139 axis,
1140 start: start.to_string(),
1141 end: end.to_string(),
1142 step: step.to_string(),
1143 })?;
1144 }
1145 }
1146 if out.is_empty() || *out.last().unwrap() != end {
1147 return Err(RangeBreakoutSignalsError::InvalidRange {
1148 axis,
1149 start: start.to_string(),
1150 end: end.to_string(),
1151 step: step.to_string(),
1152 });
1153 }
1154 Ok(out)
1155}
1156
1157pub fn expand_grid_range_breakout_signals(
1158 sweep: &RangeBreakoutSignalsBatchRange,
1159) -> Result<Vec<RangeBreakoutSignalsParams>, RangeBreakoutSignalsError> {
1160 let range_lengths = axis_usize("range_length", sweep.range_length)?;
1161 let confirmation_lengths = axis_usize("confirmation_length", sweep.confirmation_length)?;
1162 let mut out = Vec::with_capacity(range_lengths.len() * confirmation_lengths.len());
1163 for &range_length in &range_lengths {
1164 for &confirmation_length in &confirmation_lengths {
1165 out.push(RangeBreakoutSignalsParams {
1166 range_length: Some(range_length),
1167 confirmation_length: Some(confirmation_length),
1168 });
1169 }
1170 }
1171 Ok(out)
1172}
1173
1174fn batch_inner_into(
1175 open: &[f64],
1176 high: &[f64],
1177 low: &[f64],
1178 close: &[f64],
1179 volume: &[f64],
1180 sweep: &RangeBreakoutSignalsBatchRange,
1181 parallel: bool,
1182 range_top_out: &mut [f64],
1183 range_bottom_out: &mut [f64],
1184 bullish_out: &mut [f64],
1185 extra_bullish_out: &mut [f64],
1186 bearish_out: &mut [f64],
1187 extra_bearish_out: &mut [f64],
1188) -> Result<Vec<RangeBreakoutSignalsParams>, RangeBreakoutSignalsError> {
1189 let (_, max_run) = analyze_valid_segments(open, high, low, close, volume)?;
1190 let combos = expand_grid_range_breakout_signals(sweep)?;
1191 let rows = combos.len();
1192 let cols = close.len();
1193 let expected = rows * cols;
1194
1195 for out in [
1196 &mut *range_top_out,
1197 &mut *range_bottom_out,
1198 &mut *bullish_out,
1199 &mut *extra_bullish_out,
1200 &mut *bearish_out,
1201 &mut *extra_bearish_out,
1202 ] {
1203 if out.len() != expected {
1204 return Err(RangeBreakoutSignalsError::OutputLengthMismatch {
1205 expected,
1206 got: out.len(),
1207 });
1208 }
1209 }
1210
1211 for params in &combos {
1212 let needed = required_valid_bars(
1213 params.range_length.unwrap_or(DEFAULT_RANGE_LENGTH),
1214 params
1215 .confirmation_length
1216 .unwrap_or(DEFAULT_CONFIRMATION_LENGTH),
1217 );
1218 if max_run < needed {
1219 return Err(RangeBreakoutSignalsError::NotEnoughValidData {
1220 needed,
1221 valid: max_run,
1222 });
1223 }
1224 }
1225
1226 let do_row = |row: usize,
1227 range_top_row: &mut [f64],
1228 range_bottom_row: &mut [f64],
1229 bullish_row: &mut [f64],
1230 extra_bullish_row: &mut [f64],
1231 bearish_row: &mut [f64],
1232 extra_bearish_row: &mut [f64]| {
1233 let params = &combos[row];
1234 compute_row(
1235 open,
1236 high,
1237 low,
1238 close,
1239 volume,
1240 params.range_length.unwrap_or(DEFAULT_RANGE_LENGTH),
1241 params
1242 .confirmation_length
1243 .unwrap_or(DEFAULT_CONFIRMATION_LENGTH),
1244 range_top_row,
1245 range_bottom_row,
1246 bullish_row,
1247 extra_bullish_row,
1248 bearish_row,
1249 extra_bearish_row,
1250 )
1251 };
1252
1253 if parallel {
1254 #[cfg(not(target_arch = "wasm32"))]
1255 {
1256 range_top_out
1257 .par_chunks_mut(cols)
1258 .zip(range_bottom_out.par_chunks_mut(cols))
1259 .zip(bullish_out.par_chunks_mut(cols))
1260 .zip(extra_bullish_out.par_chunks_mut(cols))
1261 .zip(bearish_out.par_chunks_mut(cols))
1262 .zip(extra_bearish_out.par_chunks_mut(cols))
1263 .enumerate()
1264 .try_for_each(
1265 |(
1266 row,
1267 (
1268 (
1269 (
1270 ((range_top_row, range_bottom_row), bullish_row),
1271 extra_bullish_row,
1272 ),
1273 bearish_row,
1274 ),
1275 extra_bearish_row,
1276 ),
1277 )| {
1278 do_row(
1279 row,
1280 range_top_row,
1281 range_bottom_row,
1282 bullish_row,
1283 extra_bullish_row,
1284 bearish_row,
1285 extra_bearish_row,
1286 )
1287 },
1288 )?;
1289 }
1290 #[cfg(target_arch = "wasm32")]
1291 {
1292 for row in 0..rows {
1293 let start = row * cols;
1294 let end = start + cols;
1295 do_row(
1296 row,
1297 &mut range_top_out[start..end],
1298 &mut range_bottom_out[start..end],
1299 &mut bullish_out[start..end],
1300 &mut extra_bullish_out[start..end],
1301 &mut bearish_out[start..end],
1302 &mut extra_bearish_out[start..end],
1303 )?;
1304 }
1305 }
1306 } else {
1307 for row in 0..rows {
1308 let start = row * cols;
1309 let end = start + cols;
1310 do_row(
1311 row,
1312 &mut range_top_out[start..end],
1313 &mut range_bottom_out[start..end],
1314 &mut bullish_out[start..end],
1315 &mut extra_bullish_out[start..end],
1316 &mut bearish_out[start..end],
1317 &mut extra_bearish_out[start..end],
1318 )?;
1319 }
1320 }
1321
1322 Ok(combos)
1323}
1324
1325pub fn range_breakout_signals_batch_with_kernel(
1326 open: &[f64],
1327 high: &[f64],
1328 low: &[f64],
1329 close: &[f64],
1330 volume: &[f64],
1331 sweep: &RangeBreakoutSignalsBatchRange,
1332 kernel: Kernel,
1333) -> Result<RangeBreakoutSignalsBatchOutput, RangeBreakoutSignalsError> {
1334 match kernel {
1335 Kernel::Auto => {
1336 let _ = detect_best_batch_kernel();
1337 }
1338 k if !k.is_batch() => return Err(RangeBreakoutSignalsError::InvalidKernelForBatch(k)),
1339 _ => {}
1340 }
1341
1342 let rows = expand_grid_range_breakout_signals(sweep)?.len();
1343 let cols = close.len();
1344 let mut top_guard = ManuallyDrop::new(make_uninit_matrix(rows, cols));
1345 let mut bottom_guard = ManuallyDrop::new(make_uninit_matrix(rows, cols));
1346 let mut bullish_guard = ManuallyDrop::new(make_uninit_matrix(rows, cols));
1347 let mut extra_bullish_guard = ManuallyDrop::new(make_uninit_matrix(rows, cols));
1348 let mut bearish_guard = ManuallyDrop::new(make_uninit_matrix(rows, cols));
1349 let mut extra_bearish_guard = ManuallyDrop::new(make_uninit_matrix(rows, cols));
1350
1351 let top: &mut [f64] = unsafe {
1352 core::slice::from_raw_parts_mut(top_guard.as_mut_ptr() as *mut f64, top_guard.len())
1353 };
1354 let bottom: &mut [f64] = unsafe {
1355 core::slice::from_raw_parts_mut(bottom_guard.as_mut_ptr() as *mut f64, bottom_guard.len())
1356 };
1357 let bullish: &mut [f64] = unsafe {
1358 core::slice::from_raw_parts_mut(bullish_guard.as_mut_ptr() as *mut f64, bullish_guard.len())
1359 };
1360 let extra_bullish: &mut [f64] = unsafe {
1361 core::slice::from_raw_parts_mut(
1362 extra_bullish_guard.as_mut_ptr() as *mut f64,
1363 extra_bullish_guard.len(),
1364 )
1365 };
1366 let bearish: &mut [f64] = unsafe {
1367 core::slice::from_raw_parts_mut(bearish_guard.as_mut_ptr() as *mut f64, bearish_guard.len())
1368 };
1369 let extra_bearish: &mut [f64] = unsafe {
1370 core::slice::from_raw_parts_mut(
1371 extra_bearish_guard.as_mut_ptr() as *mut f64,
1372 extra_bearish_guard.len(),
1373 )
1374 };
1375
1376 let combos = batch_inner_into(
1377 open,
1378 high,
1379 low,
1380 close,
1381 volume,
1382 sweep,
1383 !cfg!(target_arch = "wasm32"),
1384 top,
1385 bottom,
1386 bullish,
1387 extra_bullish,
1388 bearish,
1389 extra_bearish,
1390 )?;
1391
1392 Ok(RangeBreakoutSignalsBatchOutput {
1393 range_top: unsafe {
1394 Vec::from_raw_parts(
1395 top_guard.as_mut_ptr() as *mut f64,
1396 top_guard.len(),
1397 top_guard.capacity(),
1398 )
1399 },
1400 range_bottom: unsafe {
1401 Vec::from_raw_parts(
1402 bottom_guard.as_mut_ptr() as *mut f64,
1403 bottom_guard.len(),
1404 bottom_guard.capacity(),
1405 )
1406 },
1407 bullish: unsafe {
1408 Vec::from_raw_parts(
1409 bullish_guard.as_mut_ptr() as *mut f64,
1410 bullish_guard.len(),
1411 bullish_guard.capacity(),
1412 )
1413 },
1414 extra_bullish: unsafe {
1415 Vec::from_raw_parts(
1416 extra_bullish_guard.as_mut_ptr() as *mut f64,
1417 extra_bullish_guard.len(),
1418 extra_bullish_guard.capacity(),
1419 )
1420 },
1421 bearish: unsafe {
1422 Vec::from_raw_parts(
1423 bearish_guard.as_mut_ptr() as *mut f64,
1424 bearish_guard.len(),
1425 bearish_guard.capacity(),
1426 )
1427 },
1428 extra_bearish: unsafe {
1429 Vec::from_raw_parts(
1430 extra_bearish_guard.as_mut_ptr() as *mut f64,
1431 extra_bearish_guard.len(),
1432 extra_bearish_guard.capacity(),
1433 )
1434 },
1435 combos,
1436 rows,
1437 cols,
1438 })
1439}
1440
1441pub fn range_breakout_signals_batch_slice(
1442 open: &[f64],
1443 high: &[f64],
1444 low: &[f64],
1445 close: &[f64],
1446 volume: &[f64],
1447 sweep: &RangeBreakoutSignalsBatchRange,
1448 kernel: Kernel,
1449) -> Result<RangeBreakoutSignalsBatchOutput, RangeBreakoutSignalsError> {
1450 range_breakout_signals_batch_with_kernel(open, high, low, close, volume, sweep, kernel)
1451}
1452
1453pub fn range_breakout_signals_batch_par_slice(
1454 open: &[f64],
1455 high: &[f64],
1456 low: &[f64],
1457 close: &[f64],
1458 volume: &[f64],
1459 sweep: &RangeBreakoutSignalsBatchRange,
1460 kernel: Kernel,
1461) -> Result<RangeBreakoutSignalsBatchOutput, RangeBreakoutSignalsError> {
1462 range_breakout_signals_batch_with_kernel(open, high, low, close, volume, sweep, kernel)
1463}
1464
1465#[cfg(feature = "python")]
1466#[pyfunction(name = "range_breakout_signals")]
1467#[pyo3(signature = (open, high, low, close, volume, range_length=DEFAULT_RANGE_LENGTH, confirmation_length=DEFAULT_CONFIRMATION_LENGTH, kernel=None))]
1468pub fn range_breakout_signals_py<'py>(
1469 py: Python<'py>,
1470 open: PyReadonlyArray1<'py, f64>,
1471 high: PyReadonlyArray1<'py, f64>,
1472 low: PyReadonlyArray1<'py, f64>,
1473 close: PyReadonlyArray1<'py, f64>,
1474 volume: PyReadonlyArray1<'py, f64>,
1475 range_length: usize,
1476 confirmation_length: usize,
1477 kernel: Option<&str>,
1478) -> PyResult<Bound<'py, PyDict>> {
1479 let kernel = validate_kernel(kernel, false)?;
1480 let input = RangeBreakoutSignalsInput::from_slices(
1481 open.as_slice()?,
1482 high.as_slice()?,
1483 low.as_slice()?,
1484 close.as_slice()?,
1485 volume.as_slice()?,
1486 RangeBreakoutSignalsParams {
1487 range_length: Some(range_length),
1488 confirmation_length: Some(confirmation_length),
1489 },
1490 );
1491 let out = py
1492 .allow_threads(|| range_breakout_signals_with_kernel(&input, kernel))
1493 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1494 let dict = PyDict::new(py);
1495 dict.set_item("range_top", out.range_top.into_pyarray(py))?;
1496 dict.set_item("range_bottom", out.range_bottom.into_pyarray(py))?;
1497 dict.set_item("bullish", out.bullish.into_pyarray(py))?;
1498 dict.set_item("extra_bullish", out.extra_bullish.into_pyarray(py))?;
1499 dict.set_item("bearish", out.bearish.into_pyarray(py))?;
1500 dict.set_item("extra_bearish", out.extra_bearish.into_pyarray(py))?;
1501 Ok(dict)
1502}
1503
1504#[cfg(feature = "python")]
1505#[pyfunction(name = "range_breakout_signals_batch")]
1506#[pyo3(signature = (open, high, low, close, volume, range_length_range=(DEFAULT_RANGE_LENGTH, DEFAULT_RANGE_LENGTH, 0), confirmation_length_range=(DEFAULT_CONFIRMATION_LENGTH, DEFAULT_CONFIRMATION_LENGTH, 0), kernel=None))]
1507pub fn range_breakout_signals_batch_py<'py>(
1508 py: Python<'py>,
1509 open: PyReadonlyArray1<'py, f64>,
1510 high: PyReadonlyArray1<'py, f64>,
1511 low: PyReadonlyArray1<'py, f64>,
1512 close: PyReadonlyArray1<'py, f64>,
1513 volume: PyReadonlyArray1<'py, f64>,
1514 range_length_range: (usize, usize, usize),
1515 confirmation_length_range: (usize, usize, usize),
1516 kernel: Option<&str>,
1517) -> PyResult<Bound<'py, PyDict>> {
1518 let kernel = validate_kernel(kernel, true)?;
1519 let out = range_breakout_signals_batch_with_kernel(
1520 open.as_slice()?,
1521 high.as_slice()?,
1522 low.as_slice()?,
1523 close.as_slice()?,
1524 volume.as_slice()?,
1525 &RangeBreakoutSignalsBatchRange {
1526 range_length: range_length_range,
1527 confirmation_length: confirmation_length_range,
1528 },
1529 kernel,
1530 )
1531 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1532 let dict = PyDict::new(py);
1533 dict.set_item(
1534 "range_top",
1535 out.range_top
1536 .into_pyarray(py)
1537 .reshape((out.rows, out.cols))?,
1538 )?;
1539 dict.set_item(
1540 "range_bottom",
1541 out.range_bottom
1542 .into_pyarray(py)
1543 .reshape((out.rows, out.cols))?,
1544 )?;
1545 dict.set_item(
1546 "bullish",
1547 out.bullish.into_pyarray(py).reshape((out.rows, out.cols))?,
1548 )?;
1549 dict.set_item(
1550 "extra_bullish",
1551 out.extra_bullish
1552 .into_pyarray(py)
1553 .reshape((out.rows, out.cols))?,
1554 )?;
1555 dict.set_item(
1556 "bearish",
1557 out.bearish.into_pyarray(py).reshape((out.rows, out.cols))?,
1558 )?;
1559 dict.set_item(
1560 "extra_bearish",
1561 out.extra_bearish
1562 .into_pyarray(py)
1563 .reshape((out.rows, out.cols))?,
1564 )?;
1565 dict.set_item(
1566 "range_lengths",
1567 out.combos
1568 .iter()
1569 .map(|combo| combo.range_length.unwrap_or(DEFAULT_RANGE_LENGTH))
1570 .collect::<Vec<_>>()
1571 .into_pyarray(py),
1572 )?;
1573 dict.set_item(
1574 "confirmation_lengths",
1575 out.combos
1576 .iter()
1577 .map(|combo| {
1578 combo
1579 .confirmation_length
1580 .unwrap_or(DEFAULT_CONFIRMATION_LENGTH)
1581 })
1582 .collect::<Vec<_>>()
1583 .into_pyarray(py),
1584 )?;
1585 dict.set_item("rows", out.rows)?;
1586 dict.set_item("cols", out.cols)?;
1587 Ok(dict)
1588}
1589
1590#[cfg(feature = "python")]
1591#[pyclass(name = "RangeBreakoutSignalsStream")]
1592pub struct RangeBreakoutSignalsStreamPy {
1593 inner: RangeBreakoutSignalsStream,
1594}
1595
1596#[cfg(feature = "python")]
1597#[pymethods]
1598impl RangeBreakoutSignalsStreamPy {
1599 #[new]
1600 #[pyo3(signature = (range_length=None, confirmation_length=None))]
1601 pub fn new(range_length: Option<usize>, confirmation_length: Option<usize>) -> PyResult<Self> {
1602 let inner = RangeBreakoutSignalsStream::try_new(RangeBreakoutSignalsParams {
1603 range_length,
1604 confirmation_length,
1605 })
1606 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1607 Ok(Self { inner })
1608 }
1609
1610 pub fn update(
1611 &mut self,
1612 open: f64,
1613 high: f64,
1614 low: f64,
1615 close: f64,
1616 volume: f64,
1617 ) -> Option<(f64, f64, f64, f64, f64, f64)> {
1618 self.inner.update(open, high, low, close, volume)
1619 }
1620}
1621
1622#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1623#[derive(Serialize, Deserialize)]
1624pub struct RangeBreakoutSignalsBatchConfig {
1625 pub range_length_range: (usize, usize, usize),
1626 pub confirmation_length_range: (usize, usize, usize),
1627}
1628
1629#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1630#[wasm_bindgen]
1631pub fn range_breakout_signals_js(
1632 open: &[f64],
1633 high: &[f64],
1634 low: &[f64],
1635 close: &[f64],
1636 volume: &[f64],
1637 range_length: usize,
1638 confirmation_length: usize,
1639) -> Result<JsValue, JsValue> {
1640 let input = RangeBreakoutSignalsInput::from_slices(
1641 open,
1642 high,
1643 low,
1644 close,
1645 volume,
1646 RangeBreakoutSignalsParams {
1647 range_length: Some(range_length),
1648 confirmation_length: Some(confirmation_length),
1649 },
1650 );
1651 let out = range_breakout_signals_with_kernel(&input, Kernel::Auto)
1652 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1653 serde_wasm_bindgen::to_value(&out).map_err(|e| JsValue::from_str(&e.to_string()))
1654}
1655
1656#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1657#[wasm_bindgen]
1658pub fn range_breakout_signals_alloc(len: usize) -> *mut f64 {
1659 let mut values = Vec::<f64>::with_capacity(len);
1660 let ptr = values.as_mut_ptr();
1661 std::mem::forget(values);
1662 ptr
1663}
1664
1665#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1666#[wasm_bindgen]
1667pub fn range_breakout_signals_free(ptr: *mut f64, len: usize) {
1668 if !ptr.is_null() {
1669 unsafe {
1670 let _ = Vec::from_raw_parts(ptr, len, len);
1671 }
1672 }
1673}
1674
1675#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1676#[wasm_bindgen]
1677pub fn range_breakout_signals_into(
1678 open_ptr: *const f64,
1679 high_ptr: *const f64,
1680 low_ptr: *const f64,
1681 close_ptr: *const f64,
1682 volume_ptr: *const f64,
1683 range_top_ptr: *mut f64,
1684 range_bottom_ptr: *mut f64,
1685 bullish_ptr: *mut f64,
1686 extra_bullish_ptr: *mut f64,
1687 bearish_ptr: *mut f64,
1688 extra_bearish_ptr: *mut f64,
1689 len: usize,
1690 range_length: usize,
1691 confirmation_length: usize,
1692) -> Result<(), JsValue> {
1693 if open_ptr.is_null()
1694 || high_ptr.is_null()
1695 || low_ptr.is_null()
1696 || close_ptr.is_null()
1697 || volume_ptr.is_null()
1698 || range_top_ptr.is_null()
1699 || range_bottom_ptr.is_null()
1700 || bullish_ptr.is_null()
1701 || extra_bullish_ptr.is_null()
1702 || bearish_ptr.is_null()
1703 || extra_bearish_ptr.is_null()
1704 {
1705 return Err(JsValue::from_str("Null pointer provided"));
1706 }
1707
1708 unsafe {
1709 let input = RangeBreakoutSignalsInput::from_slices(
1710 std::slice::from_raw_parts(open_ptr, len),
1711 std::slice::from_raw_parts(high_ptr, len),
1712 std::slice::from_raw_parts(low_ptr, len),
1713 std::slice::from_raw_parts(close_ptr, len),
1714 std::slice::from_raw_parts(volume_ptr, len),
1715 RangeBreakoutSignalsParams {
1716 range_length: Some(range_length),
1717 confirmation_length: Some(confirmation_length),
1718 },
1719 );
1720 let out = range_breakout_signals_with_kernel(&input, Kernel::Auto)
1721 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1722 std::slice::from_raw_parts_mut(range_top_ptr, len).copy_from_slice(&out.range_top);
1723 std::slice::from_raw_parts_mut(range_bottom_ptr, len).copy_from_slice(&out.range_bottom);
1724 std::slice::from_raw_parts_mut(bullish_ptr, len).copy_from_slice(&out.bullish);
1725 std::slice::from_raw_parts_mut(extra_bullish_ptr, len).copy_from_slice(&out.extra_bullish);
1726 std::slice::from_raw_parts_mut(bearish_ptr, len).copy_from_slice(&out.bearish);
1727 std::slice::from_raw_parts_mut(extra_bearish_ptr, len).copy_from_slice(&out.extra_bearish);
1728 }
1729 Ok(())
1730}
1731
1732#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1733#[wasm_bindgen(js_name = range_breakout_signals_batch)]
1734pub fn range_breakout_signals_batch_unified_js(
1735 open: &[f64],
1736 high: &[f64],
1737 low: &[f64],
1738 close: &[f64],
1739 volume: &[f64],
1740 config: JsValue,
1741) -> Result<JsValue, JsValue> {
1742 let config: RangeBreakoutSignalsBatchConfig =
1743 serde_wasm_bindgen::from_value(config).map_err(|e| JsValue::from_str(&e.to_string()))?;
1744 let out = range_breakout_signals_batch_with_kernel(
1745 open,
1746 high,
1747 low,
1748 close,
1749 volume,
1750 &RangeBreakoutSignalsBatchRange {
1751 range_length: config.range_length_range,
1752 confirmation_length: config.confirmation_length_range,
1753 },
1754 Kernel::Auto,
1755 )
1756 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1757 serde_wasm_bindgen::to_value(&out).map_err(|e| JsValue::from_str(&e.to_string()))
1758}
1759
1760#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1761#[wasm_bindgen(js_name = range_breakout_signals_batch_into)]
1762pub fn range_breakout_signals_batch_into(
1763 open_ptr: *const f64,
1764 high_ptr: *const f64,
1765 low_ptr: *const f64,
1766 close_ptr: *const f64,
1767 volume_ptr: *const f64,
1768 range_top_ptr: *mut f64,
1769 range_bottom_ptr: *mut f64,
1770 bullish_ptr: *mut f64,
1771 extra_bullish_ptr: *mut f64,
1772 bearish_ptr: *mut f64,
1773 extra_bearish_ptr: *mut f64,
1774 len: usize,
1775 range_length_start: usize,
1776 range_length_end: usize,
1777 range_length_step: usize,
1778 confirmation_length_start: usize,
1779 confirmation_length_end: usize,
1780 confirmation_length_step: usize,
1781) -> Result<usize, JsValue> {
1782 let sweep = RangeBreakoutSignalsBatchRange {
1783 range_length: (range_length_start, range_length_end, range_length_step),
1784 confirmation_length: (
1785 confirmation_length_start,
1786 confirmation_length_end,
1787 confirmation_length_step,
1788 ),
1789 };
1790 let rows = expand_grid_range_breakout_signals(&sweep)
1791 .map_err(|e| JsValue::from_str(&e.to_string()))?
1792 .len();
1793 let total = rows
1794 .checked_mul(len)
1795 .ok_or_else(|| JsValue::from_str("rows * cols overflow"))?;
1796
1797 unsafe {
1798 let out = range_breakout_signals_batch_with_kernel(
1799 std::slice::from_raw_parts(open_ptr, len),
1800 std::slice::from_raw_parts(high_ptr, len),
1801 std::slice::from_raw_parts(low_ptr, len),
1802 std::slice::from_raw_parts(close_ptr, len),
1803 std::slice::from_raw_parts(volume_ptr, len),
1804 &sweep,
1805 Kernel::Auto,
1806 )
1807 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1808 std::slice::from_raw_parts_mut(range_top_ptr, total).copy_from_slice(&out.range_top);
1809 std::slice::from_raw_parts_mut(range_bottom_ptr, total).copy_from_slice(&out.range_bottom);
1810 std::slice::from_raw_parts_mut(bullish_ptr, total).copy_from_slice(&out.bullish);
1811 std::slice::from_raw_parts_mut(extra_bullish_ptr, total)
1812 .copy_from_slice(&out.extra_bullish);
1813 std::slice::from_raw_parts_mut(bearish_ptr, total).copy_from_slice(&out.bearish);
1814 std::slice::from_raw_parts_mut(extra_bearish_ptr, total)
1815 .copy_from_slice(&out.extra_bearish);
1816 }
1817
1818 Ok(rows)
1819}
1820
1821#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1822#[wasm_bindgen]
1823pub struct RangeBreakoutSignalsStreamWasm {
1824 inner: RangeBreakoutSignalsStream,
1825}
1826
1827#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1828#[wasm_bindgen]
1829impl RangeBreakoutSignalsStreamWasm {
1830 #[wasm_bindgen(constructor)]
1831 pub fn new(
1832 range_length: Option<usize>,
1833 confirmation_length: Option<usize>,
1834 ) -> Result<RangeBreakoutSignalsStreamWasm, JsValue> {
1835 let inner = RangeBreakoutSignalsStream::try_new(RangeBreakoutSignalsParams {
1836 range_length,
1837 confirmation_length,
1838 })
1839 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1840 Ok(Self { inner })
1841 }
1842
1843 pub fn update(
1844 &mut self,
1845 open: f64,
1846 high: f64,
1847 low: f64,
1848 close: f64,
1849 volume: f64,
1850 ) -> Result<JsValue, JsValue> {
1851 serde_wasm_bindgen::to_value(&self.inner.update(open, high, low, close, volume))
1852 .map_err(|e| JsValue::from_str(&e.to_string()))
1853 }
1854}
1855
1856#[cfg(test)]
1857mod tests {
1858 use super::*;
1859
1860 fn sample_ohlcv() -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
1861 let mut open = Vec::with_capacity(96);
1862 let mut high = Vec::with_capacity(96);
1863 let mut low = Vec::with_capacity(96);
1864 let mut close = Vec::with_capacity(96);
1865 let mut volume = Vec::with_capacity(96);
1866
1867 for i in 0..24 {
1868 let base = 100.0 + i as f64 * 0.35;
1869 let o = base - 1.4;
1870 let c = base + if i & 1 == 0 { 1.7 } else { -1.6 };
1871 open.push(o);
1872 close.push(c);
1873 high.push(o.max(c) + 1.1);
1874 low.push(o.min(c) - 1.0);
1875 volume.push(900.0 + (i as f64) * 8.0);
1876 }
1877
1878 for i in 24..36 {
1879 let base = 108.5 + ((i - 24) as f64) * 0.03;
1880 open.push(base - 0.03);
1881 close.push(base + 0.03);
1882 high.push(base + 0.18);
1883 low.push(base - 0.18);
1884 volume.push(1600.0 + (i as f64) * 6.0);
1885 }
1886
1887 open.push(109.2);
1888 high.push(112.4);
1889 low.push(109.0);
1890 close.push(112.1);
1891 volume.push(2200.0);
1892
1893 for i in 37..60 {
1894 let base = 111.8 - ((i - 37) as f64) * 0.08;
1895 let o = base + 0.8;
1896 let c = base - 0.9;
1897 open.push(o);
1898 close.push(c);
1899 high.push(o.max(c) + 0.9);
1900 low.push(o.min(c) - 0.8);
1901 volume.push(1100.0 + (i as f64) * 5.0);
1902 }
1903
1904 for i in 60..72 {
1905 let base = 104.7 - ((i - 60) as f64) * 0.02;
1906 open.push(base + 0.02);
1907 close.push(base - 0.02);
1908 high.push(base + 0.16);
1909 low.push(base - 0.16);
1910 volume.push(1750.0 + (i as f64) * 5.0);
1911 }
1912
1913 open.push(103.9);
1914 high.push(104.0);
1915 low.push(100.6);
1916 close.push(100.9);
1917 volume.push(2400.0);
1918
1919 for i in 73..96 {
1920 let base = 101.4 + ((i - 73) as f64) * 0.06;
1921 let o = base - 0.3;
1922 let c = base + 0.25;
1923 open.push(o);
1924 close.push(c);
1925 high.push(o.max(c) + 0.45);
1926 low.push(o.min(c) - 0.45);
1927 volume.push(1200.0 + (i as f64) * 3.0);
1928 }
1929
1930 (open, high, low, close, volume)
1931 }
1932
1933 #[test]
1934 fn range_breakout_signals_outputs_present() -> Result<(), Box<dyn StdError>> {
1935 let (open, high, low, close, volume) = sample_ohlcv();
1936 let out = range_breakout_signals(&RangeBreakoutSignalsInput::from_slices(
1937 &open,
1938 &high,
1939 &low,
1940 &close,
1941 &volume,
1942 RangeBreakoutSignalsParams::default(),
1943 ))?;
1944 assert!(out.range_top.iter().any(|value| value.is_finite()));
1945 assert!(out.range_bottom.iter().any(|value| value.is_finite()));
1946 Ok(())
1947 }
1948}