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, PyList};
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, make_uninit_matrix,
19};
20#[cfg(feature = "python")]
21use crate::utilities::kernel_validation::validate_kernel;
22
23use std::collections::VecDeque;
24use std::mem::{ManuallyDrop, MaybeUninit};
25use thiserror::Error;
26
27const DEFAULT_SWING_LENGTH: usize = 3;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[cfg_attr(
31 all(target_arch = "wasm32", feature = "wasm"),
32 derive(Serialize, Deserialize)
33)]
34#[cfg_attr(
35 all(target_arch = "wasm32", feature = "wasm"),
36 serde(rename_all = "snake_case")
37)]
38pub enum IctPropulsionBlockMitigationPrice {
39 Close,
40 Wick,
41}
42
43impl Default for IctPropulsionBlockMitigationPrice {
44 fn default() -> Self {
45 Self::Close
46 }
47}
48
49impl IctPropulsionBlockMitigationPrice {
50 #[inline]
51 fn as_str(self) -> &'static str {
52 match self {
53 Self::Close => "close",
54 Self::Wick => "wick",
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
60pub enum IctPropulsionBlockData<'a> {
61 Candles(&'a Candles),
62 Slices {
63 open: &'a [f64],
64 high: &'a [f64],
65 low: &'a [f64],
66 close: &'a [f64],
67 },
68}
69
70#[derive(Debug, Clone)]
71pub struct IctPropulsionBlockOutput {
72 pub bullish_high: Vec<f64>,
73 pub bullish_low: Vec<f64>,
74 pub bullish_kind: Vec<f64>,
75 pub bullish_active: Vec<f64>,
76 pub bullish_mitigated: Vec<f64>,
77 pub bullish_new: Vec<f64>,
78 pub bearish_high: Vec<f64>,
79 pub bearish_low: Vec<f64>,
80 pub bearish_kind: Vec<f64>,
81 pub bearish_active: Vec<f64>,
82 pub bearish_mitigated: Vec<f64>,
83 pub bearish_new: Vec<f64>,
84}
85
86#[derive(Debug, Clone)]
87#[cfg_attr(
88 all(target_arch = "wasm32", feature = "wasm"),
89 derive(Serialize, Deserialize)
90)]
91pub struct IctPropulsionBlockParams {
92 pub swing_length: Option<usize>,
93 pub mitigation_price: Option<IctPropulsionBlockMitigationPrice>,
94}
95
96impl Default for IctPropulsionBlockParams {
97 fn default() -> Self {
98 Self {
99 swing_length: Some(DEFAULT_SWING_LENGTH),
100 mitigation_price: Some(IctPropulsionBlockMitigationPrice::Close),
101 }
102 }
103}
104
105#[derive(Debug, Clone)]
106pub struct IctPropulsionBlockInput<'a> {
107 pub data: IctPropulsionBlockData<'a>,
108 pub params: IctPropulsionBlockParams,
109}
110
111impl<'a> IctPropulsionBlockInput<'a> {
112 #[inline]
113 pub fn from_candles(candles: &'a Candles, params: IctPropulsionBlockParams) -> Self {
114 Self {
115 data: IctPropulsionBlockData::Candles(candles),
116 params,
117 }
118 }
119
120 #[inline]
121 pub fn from_slices(
122 open: &'a [f64],
123 high: &'a [f64],
124 low: &'a [f64],
125 close: &'a [f64],
126 params: IctPropulsionBlockParams,
127 ) -> Self {
128 Self {
129 data: IctPropulsionBlockData::Slices {
130 open,
131 high,
132 low,
133 close,
134 },
135 params,
136 }
137 }
138
139 #[inline]
140 pub fn with_default_candles(candles: &'a Candles) -> Self {
141 Self::from_candles(candles, IctPropulsionBlockParams::default())
142 }
143
144 #[inline]
145 pub fn get_swing_length(&self) -> usize {
146 self.params.swing_length.unwrap_or(DEFAULT_SWING_LENGTH)
147 }
148
149 #[inline]
150 pub fn get_mitigation_price(&self) -> IctPropulsionBlockMitigationPrice {
151 self.params
152 .mitigation_price
153 .unwrap_or(IctPropulsionBlockMitigationPrice::Close)
154 }
155
156 #[inline]
157 pub fn as_refs(&'a self) -> (&'a [f64], &'a [f64], &'a [f64], &'a [f64]) {
158 match &self.data {
159 IctPropulsionBlockData::Candles(candles) => {
160 (&candles.open, &candles.high, &candles.low, &candles.close)
161 }
162 IctPropulsionBlockData::Slices {
163 open,
164 high,
165 low,
166 close,
167 } => (*open, *high, *low, *close),
168 }
169 }
170}
171
172#[derive(Clone, Debug)]
173pub struct IctPropulsionBlockBuilder {
174 swing_length: Option<usize>,
175 mitigation_price: Option<IctPropulsionBlockMitigationPrice>,
176 kernel: Kernel,
177}
178
179impl Default for IctPropulsionBlockBuilder {
180 fn default() -> Self {
181 Self {
182 swing_length: None,
183 mitigation_price: None,
184 kernel: Kernel::Auto,
185 }
186 }
187}
188
189impl IctPropulsionBlockBuilder {
190 #[inline]
191 pub fn new() -> Self {
192 Self::default()
193 }
194
195 #[inline]
196 pub fn swing_length(mut self, value: usize) -> Self {
197 self.swing_length = Some(value);
198 self
199 }
200
201 #[inline]
202 pub fn mitigation_price(mut self, value: IctPropulsionBlockMitigationPrice) -> Self {
203 self.mitigation_price = Some(value);
204 self
205 }
206
207 #[inline]
208 pub fn kernel(mut self, value: Kernel) -> Self {
209 self.kernel = value;
210 self
211 }
212
213 #[inline]
214 pub fn apply(
215 self,
216 candles: &Candles,
217 ) -> Result<IctPropulsionBlockOutput, IctPropulsionBlockError> {
218 let input = IctPropulsionBlockInput::from_candles(
219 candles,
220 IctPropulsionBlockParams {
221 swing_length: self.swing_length,
222 mitigation_price: self.mitigation_price,
223 },
224 );
225 ict_propulsion_block_with_kernel(&input, self.kernel)
226 }
227
228 #[inline]
229 pub fn apply_slices(
230 self,
231 open: &[f64],
232 high: &[f64],
233 low: &[f64],
234 close: &[f64],
235 ) -> Result<IctPropulsionBlockOutput, IctPropulsionBlockError> {
236 let input = IctPropulsionBlockInput::from_slices(
237 open,
238 high,
239 low,
240 close,
241 IctPropulsionBlockParams {
242 swing_length: self.swing_length,
243 mitigation_price: self.mitigation_price,
244 },
245 );
246 ict_propulsion_block_with_kernel(&input, self.kernel)
247 }
248
249 #[inline]
250 pub fn into_stream(self) -> Result<IctPropulsionBlockStream, IctPropulsionBlockError> {
251 IctPropulsionBlockStream::try_new(IctPropulsionBlockParams {
252 swing_length: self.swing_length,
253 mitigation_price: self.mitigation_price,
254 })
255 }
256}
257
258#[derive(Debug, Error)]
259pub enum IctPropulsionBlockError {
260 #[error("ict_propulsion_block: Empty input data.")]
261 EmptyInputData,
262 #[error(
263 "ict_propulsion_block: Input length mismatch: open={open}, high={high}, low={low}, close={close}"
264 )]
265 DataLengthMismatch {
266 open: usize,
267 high: usize,
268 low: usize,
269 close: usize,
270 },
271 #[error("ict_propulsion_block: All input values are invalid.")]
272 AllValuesNaN,
273 #[error("ict_propulsion_block: Invalid swing_length: {swing_length}")]
274 InvalidSwingLength { swing_length: usize },
275 #[error("ict_propulsion_block: Invalid mitigation_price: {mitigation_price}")]
276 InvalidMitigationPrice { mitigation_price: String },
277 #[error("ict_propulsion_block: Output length mismatch: expected={expected}, got={got}")]
278 OutputLengthMismatch { expected: usize, got: usize },
279 #[error("ict_propulsion_block: Invalid range: start={start}, end={end}, step={step}")]
280 InvalidRange {
281 start: String,
282 end: String,
283 step: String,
284 },
285 #[error("ict_propulsion_block: Invalid kernel for batch: {0:?}")]
286 InvalidKernelForBatch(Kernel),
287}
288
289#[derive(Clone, Copy, Debug)]
290struct SwingState {
291 value: f64,
292 index: usize,
293 cross: bool,
294}
295
296impl SwingState {
297 #[inline]
298 fn na() -> Self {
299 Self {
300 value: f64::NAN,
301 index: 0,
302 cross: false,
303 }
304 }
305
306 #[inline]
307 fn is_valid(self) -> bool {
308 self.value.is_finite()
309 }
310}
311
312#[derive(Clone, Copy, Debug)]
313struct BlockSeed {
314 index: usize,
315 open: f64,
316 high: f64,
317 low: f64,
318 close: f64,
319}
320
321#[derive(Clone, Copy, Debug)]
322struct BlockState {
323 start_index: usize,
324 end_index: usize,
325 confirmed_index: usize,
326 open: f64,
327 high: f64,
328 low: f64,
329 close: f64,
330 is_propulsion: bool,
331 is_active: bool,
332 is_mitigated: bool,
333}
334
335impl BlockState {
336 #[inline]
337 fn new(seed: BlockSeed, confirmed_index: usize, is_propulsion: bool) -> Self {
338 Self {
339 start_index: seed.index,
340 end_index: confirmed_index,
341 confirmed_index,
342 open: seed.open,
343 high: seed.high,
344 low: seed.low,
345 close: seed.close,
346 is_propulsion,
347 is_active: true,
348 is_mitigated: false,
349 }
350 }
351
352 #[inline]
353 fn kind_value(self) -> f64 {
354 if self.is_propulsion {
355 2.0
356 } else {
357 1.0
358 }
359 }
360}
361
362#[inline(always)]
363fn valid_bar(open: f64, high: f64, low: f64, close: f64) -> bool {
364 open.is_finite() && high.is_finite() && low.is_finite() && close.is_finite() && high >= low
365}
366
367#[inline(always)]
368fn validate_lengths(
369 open: &[f64],
370 high: &[f64],
371 low: &[f64],
372 close: &[f64],
373) -> Result<(), IctPropulsionBlockError> {
374 if open.is_empty() || high.is_empty() || low.is_empty() || close.is_empty() {
375 return Err(IctPropulsionBlockError::EmptyInputData);
376 }
377 if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
378 return Err(IctPropulsionBlockError::DataLengthMismatch {
379 open: open.len(),
380 high: high.len(),
381 low: low.len(),
382 close: close.len(),
383 });
384 }
385 Ok(())
386}
387
388#[inline(always)]
389fn first_valid_bar(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Option<usize> {
390 (0..close.len()).find(|&i| valid_bar(open[i], high[i], low[i], close[i]))
391}
392
393#[inline(always)]
394fn validate_params(
395 swing_length: usize,
396 mitigation_price: IctPropulsionBlockMitigationPrice,
397) -> Result<(), IctPropulsionBlockError> {
398 if swing_length == 0 {
399 return Err(IctPropulsionBlockError::InvalidSwingLength { swing_length });
400 }
401 match mitigation_price {
402 IctPropulsionBlockMitigationPrice::Close | IctPropulsionBlockMitigationPrice::Wick => {}
403 }
404 Ok(())
405}
406
407#[inline(always)]
408fn push_front_limited(blocks: &mut Vec<BlockState>, block: BlockState) {
409 blocks.insert(0, block);
410 if blocks.len() > 2 {
411 blocks.truncate(2);
412 }
413}
414
415#[inline(always)]
416fn reset_deque(deque: &mut VecDeque<usize>) {
417 deque.clear();
418}
419
420#[inline(always)]
421fn select_bullish_seed(
422 current: usize,
423 swing_index: usize,
424 open: &[f64],
425 high: &[f64],
426 low: &[f64],
427 close: &[f64],
428) -> BlockSeed {
429 let mut best = BlockSeed {
430 index: current - 1,
431 open: open[current - 1],
432 high: high[current - 1],
433 low: low[current - 1],
434 close: close[current - 1],
435 };
436 let diff = current.saturating_sub(swing_index);
437 for offset in 1..diff {
438 let idx = current - offset;
439 if open[idx] > close[idx] && low[idx] <= best.low {
440 best = BlockSeed {
441 index: idx,
442 open: open[idx],
443 high: high[idx],
444 low: low[idx],
445 close: close[idx],
446 };
447 }
448 }
449 best
450}
451
452#[inline(always)]
453fn select_bearish_seed(
454 current: usize,
455 swing_index: usize,
456 open: &[f64],
457 high: &[f64],
458 low: &[f64],
459 close: &[f64],
460) -> BlockSeed {
461 let mut best = BlockSeed {
462 index: current - 1,
463 open: open[current - 1],
464 high: high[current - 1],
465 low: low[current - 1],
466 close: close[current - 1],
467 };
468 let diff = current.saturating_sub(swing_index);
469 for offset in 1..diff {
470 let idx = current - offset;
471 if open[idx] < close[idx] && high[idx] >= best.high {
472 best = BlockSeed {
473 index: idx,
474 open: open[idx],
475 high: high[idx],
476 low: low[idx],
477 close: close[idx],
478 };
479 }
480 }
481 best
482}
483
484#[inline(always)]
485fn maybe_insert_bullish_order_block(
486 blocks: &mut Vec<BlockState>,
487 seed: BlockSeed,
488 current: usize,
489) -> bool {
490 if blocks.is_empty() {
491 push_front_limited(blocks, BlockState::new(seed, current, false));
492 return true;
493 }
494
495 if blocks[0].is_mitigated
496 && blocks[0].is_propulsion
497 && blocks.len() > 1
498 && !blocks[1].is_propulsion
499 {
500 blocks[1].is_mitigated = true;
501 }
502
503 let recent = blocks[0];
504 if !(recent.is_mitigated
505 || (!recent.is_mitigated && seed.high > recent.high && seed.index > recent.start_index))
506 {
507 return false;
508 }
509
510 push_front_limited(blocks, BlockState::new(seed, current, false));
511 if blocks.len() > 1 {
512 blocks[1].is_active = false;
513 if seed.index <= blocks[1].end_index
514 && blocks[0].low <= blocks[1].high
515 && blocks[0].high > blocks[1].high
516 {
517 blocks[0].is_propulsion = true;
518 }
519 }
520 true
521}
522
523#[inline(always)]
524fn maybe_insert_bearish_order_block(
525 blocks: &mut Vec<BlockState>,
526 seed: BlockSeed,
527 current: usize,
528) -> bool {
529 if blocks.is_empty() {
530 push_front_limited(blocks, BlockState::new(seed, current, false));
531 return true;
532 }
533
534 if blocks[0].is_mitigated
535 && blocks[0].is_propulsion
536 && blocks.len() > 1
537 && !blocks[1].is_propulsion
538 {
539 blocks[1].is_mitigated = true;
540 }
541
542 let recent = blocks[0];
543 if !(recent.is_mitigated
544 || (!recent.is_mitigated && seed.low < recent.low && seed.index > recent.start_index))
545 {
546 return false;
547 }
548
549 push_front_limited(blocks, BlockState::new(seed, current, false));
550 if blocks.len() > 1 {
551 blocks[1].is_active = false;
552 if seed.index <= blocks[1].end_index
553 && blocks[0].high >= blocks[1].low
554 && blocks[0].low < blocks[1].low
555 {
556 blocks[0].is_propulsion = true;
557 }
558 }
559 true
560}
561
562#[inline(always)]
563fn insert_bullish_propulsion(
564 blocks: &mut Vec<BlockState>,
565 breach_index: usize,
566 breach_high: f64,
567 current: usize,
568 open: &[f64],
569 low: &[f64],
570 close: &[f64],
571) -> bool {
572 if blocks.is_empty() {
573 return false;
574 }
575 blocks[0].is_active = false;
576 blocks[0].end_index = current;
577 let seed = BlockSeed {
578 index: breach_index,
579 open: open[breach_index],
580 high: breach_high,
581 low: low[breach_index],
582 close: close[breach_index],
583 };
584 push_front_limited(blocks, BlockState::new(seed, current, true));
585 true
586}
587
588#[inline(always)]
589fn insert_bearish_propulsion(
590 blocks: &mut Vec<BlockState>,
591 breach_index: usize,
592 breach_low: f64,
593 current: usize,
594 open: &[f64],
595 high: &[f64],
596 close: &[f64],
597) -> bool {
598 if blocks.is_empty() {
599 return false;
600 }
601 blocks[0].is_active = false;
602 blocks[0].end_index = current;
603 let seed = BlockSeed {
604 index: breach_index,
605 open: open[breach_index],
606 high: high[breach_index],
607 low: breach_low,
608 close: close[breach_index],
609 };
610 push_front_limited(blocks, BlockState::new(seed, current, true));
611 true
612}
613
614#[inline(always)]
615fn write_snapshot(
616 block: Option<&BlockState>,
617 new_flag: f64,
618 out_high: &mut [f64],
619 out_low: &mut [f64],
620 out_kind: &mut [f64],
621 out_active: &mut [f64],
622 out_mitigated: &mut [f64],
623 out_new: &mut [f64],
624 index: usize,
625) {
626 if let Some(block) = block {
627 out_high[index] = block.high;
628 out_low[index] = block.low;
629 out_kind[index] = block.kind_value();
630 out_active[index] = if block.is_active { 1.0 } else { 0.0 };
631 out_mitigated[index] = if block.is_mitigated { 1.0 } else { 0.0 };
632 out_new[index] = new_flag;
633 } else {
634 out_high[index] = f64::NAN;
635 out_low[index] = f64::NAN;
636 out_kind[index] = 0.0;
637 out_active[index] = 0.0;
638 out_mitigated[index] = 0.0;
639 out_new[index] = new_flag;
640 }
641}
642
643#[inline(always)]
644fn normalize_kernel(kernel: Kernel) -> Kernel {
645 match kernel {
646 Kernel::Auto => Kernel::Scalar,
647 other if other.is_batch() => other.to_non_batch(),
648 other => other,
649 }
650}
651
652#[allow(clippy::too_many_arguments)]
653fn ict_propulsion_block_row_scalar(
654 open: &[f64],
655 high: &[f64],
656 low: &[f64],
657 close: &[f64],
658 swing_length: usize,
659 mitigation_price: IctPropulsionBlockMitigationPrice,
660 out_bullish_high: &mut [f64],
661 out_bullish_low: &mut [f64],
662 out_bullish_kind: &mut [f64],
663 out_bullish_active: &mut [f64],
664 out_bullish_mitigated: &mut [f64],
665 out_bullish_new: &mut [f64],
666 out_bearish_high: &mut [f64],
667 out_bearish_low: &mut [f64],
668 out_bearish_kind: &mut [f64],
669 out_bearish_active: &mut [f64],
670 out_bearish_mitigated: &mut [f64],
671 out_bearish_new: &mut [f64],
672) {
673 let len = close.len();
674 let mut swing_os = 0i8;
675 let mut swing_high = SwingState::na();
676 let mut swing_low = SwingState::na();
677 let mut bullish_breach = SwingState::na();
678 let mut bearish_breach = SwingState::na();
679 let mut bullish_breach_low_prev = f64::NAN;
680 let mut bullish_breach_high_prev = f64::NAN;
681 let mut bullish_breach_index_prev = 0usize;
682 let mut bearish_breach_low_prev = f64::NAN;
683 let mut bearish_breach_high_prev = f64::NAN;
684 let mut bearish_breach_index_prev = 0usize;
685 let mut bullish_blocks: Vec<BlockState> = Vec::with_capacity(2);
686 let mut bearish_blocks: Vec<BlockState> = Vec::with_capacity(2);
687 let mut max_high_window: VecDeque<usize> = VecDeque::with_capacity(swing_length + 1);
688 let mut min_low_window: VecDeque<usize> = VecDeque::with_capacity(swing_length + 1);
689
690 for i in 0..len {
691 if !valid_bar(open[i], high[i], low[i], close[i]) {
692 out_bullish_high[i] = f64::NAN;
693 out_bullish_low[i] = f64::NAN;
694 out_bullish_kind[i] = f64::NAN;
695 out_bullish_active[i] = f64::NAN;
696 out_bullish_mitigated[i] = f64::NAN;
697 out_bullish_new[i] = f64::NAN;
698 out_bearish_high[i] = f64::NAN;
699 out_bearish_low[i] = f64::NAN;
700 out_bearish_kind[i] = f64::NAN;
701 out_bearish_active[i] = f64::NAN;
702 out_bearish_mitigated[i] = f64::NAN;
703 out_bearish_new[i] = f64::NAN;
704 swing_os = 0;
705 swing_high = SwingState::na();
706 swing_low = SwingState::na();
707 bullish_breach = SwingState::na();
708 bearish_breach = SwingState::na();
709 bullish_breach_low_prev = f64::NAN;
710 bullish_breach_high_prev = f64::NAN;
711 bearish_breach_low_prev = f64::NAN;
712 bearish_breach_high_prev = f64::NAN;
713 bullish_blocks.clear();
714 bearish_blocks.clear();
715 reset_deque(&mut max_high_window);
716 reset_deque(&mut min_low_window);
717 continue;
718 }
719
720 while let Some(&idx) = max_high_window.back() {
721 if high[idx] <= high[i] {
722 max_high_window.pop_back();
723 } else {
724 break;
725 }
726 }
727 max_high_window.push_back(i);
728
729 while let Some(&idx) = min_low_window.back() {
730 if low[idx] >= low[i] {
731 min_low_window.pop_back();
732 } else {
733 break;
734 }
735 }
736 min_low_window.push_back(i);
737
738 let window_start = i.saturating_sub(swing_length.saturating_sub(1));
739 while let Some(&idx) = max_high_window.front() {
740 if idx < window_start {
741 max_high_window.pop_front();
742 } else {
743 break;
744 }
745 }
746 while let Some(&idx) = min_low_window.front() {
747 if idx < window_start {
748 min_low_window.pop_front();
749 } else {
750 break;
751 }
752 }
753
754 if i >= swing_length {
755 let candidate = i - swing_length;
756 let upper = high[*max_high_window.front().unwrap()];
757 let lower = low[*min_low_window.front().unwrap()];
758 let mut next_os = swing_os;
759 if high[candidate] > upper {
760 next_os = 0;
761 } else if low[candidate] < lower {
762 next_os = 1;
763 }
764
765 if next_os == 0 && swing_os != 0 {
766 swing_high = SwingState {
767 value: high[candidate],
768 index: candidate,
769 cross: false,
770 };
771 }
772 if next_os == 1 && swing_os != 1 {
773 swing_low = SwingState {
774 value: low[candidate],
775 index: candidate,
776 cross: false,
777 };
778 }
779 swing_os = next_os;
780 }
781
782 let mut breach_low = low[i];
783 let mut breach_high = high[i];
784 let mut breach_index = i;
785 if let Some(current) = bullish_blocks.first() {
786 let condition = low[i] <= current.high
787 && low[i] > current.low
788 && i > current.confirmed_index
789 && !current.is_mitigated
790 && current.is_active
791 && !current.is_propulsion
792 && open[i] > current.high;
793 if condition {
794 let prev_low = if bullish_breach_low_prev.is_finite() {
795 bullish_breach_low_prev
796 } else {
797 low[i]
798 };
799 breach_low = low[i].min(prev_low);
800 if breach_low == low[i] || !bullish_breach_high_prev.is_finite() {
801 breach_high = high[i];
802 breach_index = i;
803 } else {
804 breach_high = bullish_breach_high_prev;
805 breach_index = bullish_breach_index_prev;
806 }
807 bullish_breach = SwingState {
808 value: breach_high,
809 index: breach_index,
810 cross: false,
811 };
812 }
813 }
814 bullish_breach_low_prev = breach_low;
815 bullish_breach_high_prev = breach_high;
816 bullish_breach_index_prev = breach_index;
817
818 let mut bear_breach_low = low[i];
819 let mut bear_breach_high = high[i];
820 let mut bear_breach_index = i;
821 if let Some(current) = bearish_blocks.first() {
822 let condition = high[i] >= current.low
823 && high[i] < current.high
824 && i > current.confirmed_index
825 && !current.is_mitigated
826 && current.is_active
827 && !current.is_propulsion
828 && open[i] < current.low;
829 if condition {
830 let prev_high = if bearish_breach_high_prev.is_finite() {
831 bearish_breach_high_prev
832 } else {
833 high[i]
834 };
835 bear_breach_high = high[i].max(prev_high);
836 if bear_breach_high == high[i] || !bearish_breach_low_prev.is_finite() {
837 bear_breach_low = low[i];
838 bear_breach_index = i;
839 } else {
840 bear_breach_low = bearish_breach_low_prev;
841 bear_breach_index = bearish_breach_index_prev;
842 }
843 bearish_breach = SwingState {
844 value: bear_breach_low,
845 index: bear_breach_index,
846 cross: false,
847 };
848 }
849 }
850 bearish_breach_low_prev = bear_breach_low;
851 bearish_breach_high_prev = bear_breach_high;
852 bearish_breach_index_prev = bear_breach_index;
853
854 let mut bullish_new = 0.0;
855 let mut bearish_new = 0.0;
856
857 if swing_high.is_valid()
858 && !swing_high.cross
859 && close[i] > swing_high.value
860 && i > swing_high.index
861 {
862 swing_high.cross = true;
863 let seed = select_bullish_seed(i, swing_high.index, open, high, low, close);
864 if maybe_insert_bullish_order_block(&mut bullish_blocks, seed, i) {
865 bullish_new = 1.0;
866 }
867 }
868
869 if let Some(recent) = bullish_blocks.first() {
870 let create_pb = bullish_breach.is_valid()
871 && close[i] > bullish_breach.value
872 && !bullish_breach.cross
873 && !recent.is_mitigated
874 && bullish_breach.index > recent.confirmed_index;
875 if create_pb {
876 bullish_breach.cross = true;
877 if insert_bullish_propulsion(
878 &mut bullish_blocks,
879 bullish_breach.index,
880 bullish_breach.value,
881 i,
882 open,
883 low,
884 close,
885 ) {
886 bullish_new = 1.0;
887 }
888 }
889 }
890
891 for block in &mut bullish_blocks {
892 if block.is_active && !block.is_mitigated {
893 let mitigated = match mitigation_price {
894 IctPropulsionBlockMitigationPrice::Close => close[i] < block.low,
895 IctPropulsionBlockMitigationPrice::Wick => low[i] < block.low,
896 };
897 if mitigated {
898 block.is_mitigated = true;
899 }
900 block.end_index = i;
901 }
902 }
903
904 if swing_low.is_valid()
905 && !swing_low.cross
906 && close[i] < swing_low.value
907 && i > swing_low.index
908 {
909 swing_low.cross = true;
910 let seed = select_bearish_seed(i, swing_low.index, open, high, low, close);
911 if maybe_insert_bearish_order_block(&mut bearish_blocks, seed, i) {
912 bearish_new = 1.0;
913 }
914 }
915
916 if let Some(recent) = bearish_blocks.first() {
917 let create_pb = bearish_breach.is_valid()
918 && close[i] < bearish_breach.value
919 && !bearish_breach.cross
920 && !recent.is_mitigated
921 && bearish_breach.index > recent.confirmed_index;
922 if create_pb {
923 bearish_breach.cross = true;
924 if insert_bearish_propulsion(
925 &mut bearish_blocks,
926 bearish_breach.index,
927 bearish_breach.value,
928 i,
929 open,
930 high,
931 close,
932 ) {
933 bearish_new = 1.0;
934 }
935 }
936 }
937
938 for block in &mut bearish_blocks {
939 if block.is_active && !block.is_mitigated {
940 let mitigated = match mitigation_price {
941 IctPropulsionBlockMitigationPrice::Close => close[i] > block.high,
942 IctPropulsionBlockMitigationPrice::Wick => high[i] > block.high,
943 };
944 if mitigated {
945 block.is_mitigated = true;
946 }
947 block.end_index = i;
948 }
949 }
950
951 write_snapshot(
952 bullish_blocks.first(),
953 bullish_new,
954 out_bullish_high,
955 out_bullish_low,
956 out_bullish_kind,
957 out_bullish_active,
958 out_bullish_mitigated,
959 out_bullish_new,
960 i,
961 );
962 write_snapshot(
963 bearish_blocks.first(),
964 bearish_new,
965 out_bearish_high,
966 out_bearish_low,
967 out_bearish_kind,
968 out_bearish_active,
969 out_bearish_mitigated,
970 out_bearish_new,
971 i,
972 );
973 }
974}
975
976#[inline]
977pub fn ict_propulsion_block(
978 input: &IctPropulsionBlockInput,
979) -> Result<IctPropulsionBlockOutput, IctPropulsionBlockError> {
980 ict_propulsion_block_with_kernel(input, Kernel::Auto)
981}
982
983#[inline]
984pub fn ict_propulsion_block_with_kernel(
985 input: &IctPropulsionBlockInput,
986 kernel: Kernel,
987) -> Result<IctPropulsionBlockOutput, IctPropulsionBlockError> {
988 let (open, high, low, close) = input.as_refs();
989 validate_lengths(open, high, low, close)?;
990 let swing_length = input.get_swing_length();
991 let mitigation_price = input.get_mitigation_price();
992 validate_params(swing_length, mitigation_price)?;
993 let first_valid =
994 first_valid_bar(open, high, low, close).ok_or(IctPropulsionBlockError::AllValuesNaN)?;
995 let _kernel = normalize_kernel(kernel);
996 let len = close.len();
997
998 let mut bullish_high = alloc_with_nan_prefix(len, first_valid);
999 let mut bullish_low = alloc_with_nan_prefix(len, first_valid);
1000 let mut bullish_kind = alloc_with_nan_prefix(len, first_valid);
1001 let mut bullish_active = alloc_with_nan_prefix(len, first_valid);
1002 let mut bullish_mitigated = alloc_with_nan_prefix(len, first_valid);
1003 let mut bullish_new = alloc_with_nan_prefix(len, first_valid);
1004 let mut bearish_high = alloc_with_nan_prefix(len, first_valid);
1005 let mut bearish_low = alloc_with_nan_prefix(len, first_valid);
1006 let mut bearish_kind = alloc_with_nan_prefix(len, first_valid);
1007 let mut bearish_active = alloc_with_nan_prefix(len, first_valid);
1008 let mut bearish_mitigated = alloc_with_nan_prefix(len, first_valid);
1009 let mut bearish_new = alloc_with_nan_prefix(len, first_valid);
1010
1011 ict_propulsion_block_row_scalar(
1012 open,
1013 high,
1014 low,
1015 close,
1016 swing_length,
1017 mitigation_price,
1018 &mut bullish_high,
1019 &mut bullish_low,
1020 &mut bullish_kind,
1021 &mut bullish_active,
1022 &mut bullish_mitigated,
1023 &mut bullish_new,
1024 &mut bearish_high,
1025 &mut bearish_low,
1026 &mut bearish_kind,
1027 &mut bearish_active,
1028 &mut bearish_mitigated,
1029 &mut bearish_new,
1030 );
1031
1032 Ok(IctPropulsionBlockOutput {
1033 bullish_high,
1034 bullish_low,
1035 bullish_kind,
1036 bullish_active,
1037 bullish_mitigated,
1038 bullish_new,
1039 bearish_high,
1040 bearish_low,
1041 bearish_kind,
1042 bearish_active,
1043 bearish_mitigated,
1044 bearish_new,
1045 })
1046}
1047
1048#[inline]
1049pub fn ict_propulsion_block_into_slice(
1050 out_bullish_high: &mut [f64],
1051 out_bullish_low: &mut [f64],
1052 out_bullish_kind: &mut [f64],
1053 out_bullish_active: &mut [f64],
1054 out_bullish_mitigated: &mut [f64],
1055 out_bullish_new: &mut [f64],
1056 out_bearish_high: &mut [f64],
1057 out_bearish_low: &mut [f64],
1058 out_bearish_kind: &mut [f64],
1059 out_bearish_active: &mut [f64],
1060 out_bearish_mitigated: &mut [f64],
1061 out_bearish_new: &mut [f64],
1062 input: &IctPropulsionBlockInput,
1063 kernel: Kernel,
1064) -> Result<(), IctPropulsionBlockError> {
1065 let (open, high, low, close) = input.as_refs();
1066 validate_lengths(open, high, low, close)?;
1067 let len = close.len();
1068 if out_bullish_high.len() != len
1069 || out_bullish_low.len() != len
1070 || out_bullish_kind.len() != len
1071 || out_bullish_active.len() != len
1072 || out_bullish_mitigated.len() != len
1073 || out_bullish_new.len() != len
1074 || out_bearish_high.len() != len
1075 || out_bearish_low.len() != len
1076 || out_bearish_kind.len() != len
1077 || out_bearish_active.len() != len
1078 || out_bearish_mitigated.len() != len
1079 || out_bearish_new.len() != len
1080 {
1081 return Err(IctPropulsionBlockError::OutputLengthMismatch {
1082 expected: len,
1083 got: out_bullish_high
1084 .len()
1085 .max(out_bullish_low.len())
1086 .max(out_bullish_kind.len())
1087 .max(out_bullish_active.len())
1088 .max(out_bullish_mitigated.len())
1089 .max(out_bullish_new.len())
1090 .max(out_bearish_high.len())
1091 .max(out_bearish_low.len())
1092 .max(out_bearish_kind.len())
1093 .max(out_bearish_active.len())
1094 .max(out_bearish_mitigated.len())
1095 .max(out_bearish_new.len()),
1096 });
1097 }
1098
1099 let swing_length = input.get_swing_length();
1100 let mitigation_price = input.get_mitigation_price();
1101 validate_params(swing_length, mitigation_price)?;
1102 let _kernel = normalize_kernel(kernel);
1103
1104 ict_propulsion_block_row_scalar(
1105 open,
1106 high,
1107 low,
1108 close,
1109 swing_length,
1110 mitigation_price,
1111 out_bullish_high,
1112 out_bullish_low,
1113 out_bullish_kind,
1114 out_bullish_active,
1115 out_bullish_mitigated,
1116 out_bullish_new,
1117 out_bearish_high,
1118 out_bearish_low,
1119 out_bearish_kind,
1120 out_bearish_active,
1121 out_bearish_mitigated,
1122 out_bearish_new,
1123 );
1124 Ok(())
1125}
1126
1127#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1128#[inline]
1129pub fn ict_propulsion_block_into(
1130 input: &IctPropulsionBlockInput,
1131 out_bullish_high: &mut [f64],
1132 out_bullish_low: &mut [f64],
1133 out_bullish_kind: &mut [f64],
1134 out_bullish_active: &mut [f64],
1135 out_bullish_mitigated: &mut [f64],
1136 out_bullish_new: &mut [f64],
1137 out_bearish_high: &mut [f64],
1138 out_bearish_low: &mut [f64],
1139 out_bearish_kind: &mut [f64],
1140 out_bearish_active: &mut [f64],
1141 out_bearish_mitigated: &mut [f64],
1142 out_bearish_new: &mut [f64],
1143) -> Result<(), IctPropulsionBlockError> {
1144 ict_propulsion_block_into_slice(
1145 out_bullish_high,
1146 out_bullish_low,
1147 out_bullish_kind,
1148 out_bullish_active,
1149 out_bullish_mitigated,
1150 out_bullish_new,
1151 out_bearish_high,
1152 out_bearish_low,
1153 out_bearish_kind,
1154 out_bearish_active,
1155 out_bearish_mitigated,
1156 out_bearish_new,
1157 input,
1158 Kernel::Auto,
1159 )
1160}
1161
1162#[derive(Clone, Debug)]
1163pub struct IctPropulsionBlockStream {
1164 swing_length: usize,
1165 mitigation_price: IctPropulsionBlockMitigationPrice,
1166 open: Vec<f64>,
1167 high: Vec<f64>,
1168 low: Vec<f64>,
1169 close: Vec<f64>,
1170}
1171
1172impl IctPropulsionBlockStream {
1173 #[inline]
1174 pub fn try_new(params: IctPropulsionBlockParams) -> Result<Self, IctPropulsionBlockError> {
1175 let swing_length = params.swing_length.unwrap_or(DEFAULT_SWING_LENGTH);
1176 let mitigation_price = params
1177 .mitigation_price
1178 .unwrap_or(IctPropulsionBlockMitigationPrice::Close);
1179 validate_params(swing_length, mitigation_price)?;
1180 Ok(Self {
1181 swing_length,
1182 mitigation_price,
1183 open: Vec::new(),
1184 high: Vec::new(),
1185 low: Vec::new(),
1186 close: Vec::new(),
1187 })
1188 }
1189
1190 #[inline]
1191 pub fn update(
1192 &mut self,
1193 open: f64,
1194 high: f64,
1195 low: f64,
1196 close: f64,
1197 ) -> Option<(f64, f64, f64, f64, f64, f64, f64, f64, f64, f64, f64, f64)> {
1198 if !valid_bar(open, high, low, close) {
1199 self.open.clear();
1200 self.high.clear();
1201 self.low.clear();
1202 self.close.clear();
1203 return None;
1204 }
1205
1206 self.open.push(open);
1207 self.high.push(high);
1208 self.low.push(low);
1209 self.close.push(close);
1210
1211 let input = IctPropulsionBlockInput::from_slices(
1212 &self.open,
1213 &self.high,
1214 &self.low,
1215 &self.close,
1216 IctPropulsionBlockParams {
1217 swing_length: Some(self.swing_length),
1218 mitigation_price: Some(self.mitigation_price),
1219 },
1220 );
1221 let out = ict_propulsion_block_with_kernel(&input, Kernel::Scalar).ok()?;
1222 let last = self.close.len() - 1;
1223 Some((
1224 out.bullish_high[last],
1225 out.bullish_low[last],
1226 out.bullish_kind[last],
1227 out.bullish_active[last],
1228 out.bullish_mitigated[last],
1229 out.bullish_new[last],
1230 out.bearish_high[last],
1231 out.bearish_low[last],
1232 out.bearish_kind[last],
1233 out.bearish_active[last],
1234 out.bearish_mitigated[last],
1235 out.bearish_new[last],
1236 ))
1237 }
1238}
1239
1240#[derive(Clone, Debug)]
1241pub struct IctPropulsionBlockBatchRange {
1242 pub swing_length: (usize, usize, usize),
1243 pub mitigation_price: (bool, bool),
1244}
1245
1246impl Default for IctPropulsionBlockBatchRange {
1247 fn default() -> Self {
1248 Self {
1249 swing_length: (DEFAULT_SWING_LENGTH, DEFAULT_SWING_LENGTH, 0),
1250 mitigation_price: (true, false),
1251 }
1252 }
1253}
1254
1255#[derive(Clone, Debug)]
1256pub struct IctPropulsionBlockBatchOutput {
1257 pub bullish_high: Vec<f64>,
1258 pub bullish_low: Vec<f64>,
1259 pub bullish_kind: Vec<f64>,
1260 pub bullish_active: Vec<f64>,
1261 pub bullish_mitigated: Vec<f64>,
1262 pub bullish_new: Vec<f64>,
1263 pub bearish_high: Vec<f64>,
1264 pub bearish_low: Vec<f64>,
1265 pub bearish_kind: Vec<f64>,
1266 pub bearish_active: Vec<f64>,
1267 pub bearish_mitigated: Vec<f64>,
1268 pub bearish_new: Vec<f64>,
1269 pub combos: Vec<IctPropulsionBlockParams>,
1270 pub rows: usize,
1271 pub cols: usize,
1272}
1273
1274#[derive(Clone, Debug)]
1275pub struct IctPropulsionBlockBatchBuilder {
1276 range: IctPropulsionBlockBatchRange,
1277 kernel: Kernel,
1278}
1279
1280impl Default for IctPropulsionBlockBatchBuilder {
1281 fn default() -> Self {
1282 Self {
1283 range: IctPropulsionBlockBatchRange::default(),
1284 kernel: Kernel::Auto,
1285 }
1286 }
1287}
1288
1289impl IctPropulsionBlockBatchBuilder {
1290 #[inline]
1291 pub fn new() -> Self {
1292 Self::default()
1293 }
1294
1295 #[inline]
1296 pub fn swing_length_range(mut self, range: (usize, usize, usize)) -> Self {
1297 self.range.swing_length = range;
1298 self
1299 }
1300
1301 #[inline]
1302 pub fn mitigation_price_toggle(mut self, include_close: bool, include_wick: bool) -> Self {
1303 self.range.mitigation_price = (include_close, include_wick);
1304 self
1305 }
1306
1307 #[inline]
1308 pub fn kernel(mut self, kernel: Kernel) -> Self {
1309 self.kernel = kernel;
1310 self
1311 }
1312
1313 #[inline]
1314 pub fn apply_slices(
1315 self,
1316 open: &[f64],
1317 high: &[f64],
1318 low: &[f64],
1319 close: &[f64],
1320 ) -> Result<IctPropulsionBlockBatchOutput, IctPropulsionBlockError> {
1321 ict_propulsion_block_batch_with_kernel(open, high, low, close, &self.range, self.kernel)
1322 }
1323
1324 #[inline]
1325 pub fn apply(
1326 self,
1327 candles: &Candles,
1328 ) -> Result<IctPropulsionBlockBatchOutput, IctPropulsionBlockError> {
1329 ict_propulsion_block_batch_with_kernel(
1330 &candles.open,
1331 &candles.high,
1332 &candles.low,
1333 &candles.close,
1334 &self.range,
1335 self.kernel,
1336 )
1337 }
1338}
1339
1340#[inline]
1341fn expand_axis_usize(
1342 (start, end, step): (usize, usize, usize),
1343) -> Result<Vec<usize>, IctPropulsionBlockError> {
1344 if step == 0 || start == end {
1345 return Ok(vec![start]);
1346 }
1347 let mut out = Vec::new();
1348 if start <= end {
1349 let mut value = start;
1350 while value <= end {
1351 out.push(value);
1352 match value.checked_add(step) {
1353 Some(next) => value = next,
1354 None => break,
1355 }
1356 }
1357 } else {
1358 let mut value = start;
1359 loop {
1360 if value < end {
1361 break;
1362 }
1363 out.push(value);
1364 match value.checked_sub(step) {
1365 Some(next) => value = next,
1366 None => break,
1367 }
1368 }
1369 }
1370 if out.is_empty() {
1371 return Err(IctPropulsionBlockError::InvalidRange {
1372 start: start.to_string(),
1373 end: end.to_string(),
1374 step: step.to_string(),
1375 });
1376 }
1377 Ok(out)
1378}
1379
1380#[inline]
1381pub fn expand_grid_ict_propulsion_block(
1382 range: &IctPropulsionBlockBatchRange,
1383) -> Result<Vec<IctPropulsionBlockParams>, IctPropulsionBlockError> {
1384 let swing_lengths = expand_axis_usize(range.swing_length)?;
1385 let mut mitigation_prices = Vec::new();
1386 if range.mitigation_price.0 {
1387 mitigation_prices.push(IctPropulsionBlockMitigationPrice::Close);
1388 }
1389 if range.mitigation_price.1 {
1390 mitigation_prices.push(IctPropulsionBlockMitigationPrice::Wick);
1391 }
1392 if mitigation_prices.is_empty() {
1393 mitigation_prices.push(IctPropulsionBlockMitigationPrice::Close);
1394 }
1395
1396 let mut out = Vec::with_capacity(swing_lengths.len().saturating_mul(mitigation_prices.len()));
1397 for &swing_length in &swing_lengths {
1398 for &mitigation_price in &mitigation_prices {
1399 out.push(IctPropulsionBlockParams {
1400 swing_length: Some(swing_length),
1401 mitigation_price: Some(mitigation_price),
1402 });
1403 }
1404 }
1405 Ok(out)
1406}
1407
1408#[inline]
1409pub fn ict_propulsion_block_batch_with_kernel(
1410 open: &[f64],
1411 high: &[f64],
1412 low: &[f64],
1413 close: &[f64],
1414 sweep: &IctPropulsionBlockBatchRange,
1415 kernel: Kernel,
1416) -> Result<IctPropulsionBlockBatchOutput, IctPropulsionBlockError> {
1417 let batch_kernel = match kernel {
1418 Kernel::Auto => detect_best_batch_kernel(),
1419 other if other.is_batch() => other,
1420 other => return Err(IctPropulsionBlockError::InvalidKernelForBatch(other)),
1421 };
1422 ict_propulsion_block_batch_par_slice(open, high, low, close, sweep, batch_kernel.to_non_batch())
1423}
1424
1425#[inline]
1426pub fn ict_propulsion_block_batch_slice(
1427 open: &[f64],
1428 high: &[f64],
1429 low: &[f64],
1430 close: &[f64],
1431 sweep: &IctPropulsionBlockBatchRange,
1432 kernel: Kernel,
1433) -> Result<IctPropulsionBlockBatchOutput, IctPropulsionBlockError> {
1434 ict_propulsion_block_batch_inner(open, high, low, close, sweep, kernel)
1435}
1436
1437#[inline]
1438pub fn ict_propulsion_block_batch_par_slice(
1439 open: &[f64],
1440 high: &[f64],
1441 low: &[f64],
1442 close: &[f64],
1443 sweep: &IctPropulsionBlockBatchRange,
1444 kernel: Kernel,
1445) -> Result<IctPropulsionBlockBatchOutput, IctPropulsionBlockError> {
1446 ict_propulsion_block_batch_inner(open, high, low, close, sweep, kernel)
1447}
1448
1449fn ict_propulsion_block_batch_inner(
1450 open: &[f64],
1451 high: &[f64],
1452 low: &[f64],
1453 close: &[f64],
1454 sweep: &IctPropulsionBlockBatchRange,
1455 _kernel: Kernel,
1456) -> Result<IctPropulsionBlockBatchOutput, IctPropulsionBlockError> {
1457 validate_lengths(open, high, low, close)?;
1458 let combos = expand_grid_ict_propulsion_block(sweep)?;
1459 for params in &combos {
1460 validate_params(
1461 params.swing_length.unwrap_or(DEFAULT_SWING_LENGTH),
1462 params
1463 .mitigation_price
1464 .unwrap_or(IctPropulsionBlockMitigationPrice::Close),
1465 )?;
1466 }
1467
1468 let _first_valid =
1469 first_valid_bar(open, high, low, close).ok_or(IctPropulsionBlockError::AllValuesNaN)?;
1470 let rows = combos.len();
1471 let cols = close.len();
1472 let total = rows
1473 .checked_mul(cols)
1474 .ok_or(IctPropulsionBlockError::OutputLengthMismatch {
1475 expected: usize::MAX,
1476 got: 0,
1477 })?;
1478
1479 let bullish_high_matrix = make_uninit_matrix(rows, cols);
1480 let bullish_low_matrix = make_uninit_matrix(rows, cols);
1481 let bullish_kind_matrix = make_uninit_matrix(rows, cols);
1482 let bullish_active_matrix = make_uninit_matrix(rows, cols);
1483 let bullish_mitigated_matrix = make_uninit_matrix(rows, cols);
1484 let bullish_new_matrix = make_uninit_matrix(rows, cols);
1485 let bearish_high_matrix = make_uninit_matrix(rows, cols);
1486 let bearish_low_matrix = make_uninit_matrix(rows, cols);
1487 let bearish_kind_matrix = make_uninit_matrix(rows, cols);
1488 let bearish_active_matrix = make_uninit_matrix(rows, cols);
1489 let bearish_mitigated_matrix = make_uninit_matrix(rows, cols);
1490 let bearish_new_matrix = make_uninit_matrix(rows, cols);
1491
1492 let mut bullish_high_guard = ManuallyDrop::new(bullish_high_matrix);
1493 let mut bullish_low_guard = ManuallyDrop::new(bullish_low_matrix);
1494 let mut bullish_kind_guard = ManuallyDrop::new(bullish_kind_matrix);
1495 let mut bullish_active_guard = ManuallyDrop::new(bullish_active_matrix);
1496 let mut bullish_mitigated_guard = ManuallyDrop::new(bullish_mitigated_matrix);
1497 let mut bullish_new_guard = ManuallyDrop::new(bullish_new_matrix);
1498 let mut bearish_high_guard = ManuallyDrop::new(bearish_high_matrix);
1499 let mut bearish_low_guard = ManuallyDrop::new(bearish_low_matrix);
1500 let mut bearish_kind_guard = ManuallyDrop::new(bearish_kind_matrix);
1501 let mut bearish_active_guard = ManuallyDrop::new(bearish_active_matrix);
1502 let mut bearish_mitigated_guard = ManuallyDrop::new(bearish_mitigated_matrix);
1503 let mut bearish_new_guard = ManuallyDrop::new(bearish_new_matrix);
1504
1505 let bullish_high_mu: &mut [MaybeUninit<f64>] = unsafe {
1506 std::slice::from_raw_parts_mut(bullish_high_guard.as_mut_ptr(), bullish_high_guard.len())
1507 };
1508 let bullish_low_mu: &mut [MaybeUninit<f64>] = unsafe {
1509 std::slice::from_raw_parts_mut(bullish_low_guard.as_mut_ptr(), bullish_low_guard.len())
1510 };
1511 let bullish_kind_mu: &mut [MaybeUninit<f64>] = unsafe {
1512 std::slice::from_raw_parts_mut(bullish_kind_guard.as_mut_ptr(), bullish_kind_guard.len())
1513 };
1514 let bullish_active_mu: &mut [MaybeUninit<f64>] = unsafe {
1515 std::slice::from_raw_parts_mut(
1516 bullish_active_guard.as_mut_ptr(),
1517 bullish_active_guard.len(),
1518 )
1519 };
1520 let bullish_mitigated_mu: &mut [MaybeUninit<f64>] = unsafe {
1521 std::slice::from_raw_parts_mut(
1522 bullish_mitigated_guard.as_mut_ptr(),
1523 bullish_mitigated_guard.len(),
1524 )
1525 };
1526 let bullish_new_mu: &mut [MaybeUninit<f64>] = unsafe {
1527 std::slice::from_raw_parts_mut(bullish_new_guard.as_mut_ptr(), bullish_new_guard.len())
1528 };
1529 let bearish_high_mu: &mut [MaybeUninit<f64>] = unsafe {
1530 std::slice::from_raw_parts_mut(bearish_high_guard.as_mut_ptr(), bearish_high_guard.len())
1531 };
1532 let bearish_low_mu: &mut [MaybeUninit<f64>] = unsafe {
1533 std::slice::from_raw_parts_mut(bearish_low_guard.as_mut_ptr(), bearish_low_guard.len())
1534 };
1535 let bearish_kind_mu: &mut [MaybeUninit<f64>] = unsafe {
1536 std::slice::from_raw_parts_mut(bearish_kind_guard.as_mut_ptr(), bearish_kind_guard.len())
1537 };
1538 let bearish_active_mu: &mut [MaybeUninit<f64>] = unsafe {
1539 std::slice::from_raw_parts_mut(
1540 bearish_active_guard.as_mut_ptr(),
1541 bearish_active_guard.len(),
1542 )
1543 };
1544 let bearish_mitigated_mu: &mut [MaybeUninit<f64>] = unsafe {
1545 std::slice::from_raw_parts_mut(
1546 bearish_mitigated_guard.as_mut_ptr(),
1547 bearish_mitigated_guard.len(),
1548 )
1549 };
1550 let bearish_new_mu: &mut [MaybeUninit<f64>] = unsafe {
1551 std::slice::from_raw_parts_mut(bearish_new_guard.as_mut_ptr(), bearish_new_guard.len())
1552 };
1553
1554 for row in 0..rows {
1555 let base = row * cols;
1556 let out_bullish_high = unsafe {
1557 std::slice::from_raw_parts_mut(
1558 bullish_high_mu[base..base + cols].as_mut_ptr() as *mut f64,
1559 cols,
1560 )
1561 };
1562 let out_bullish_low = unsafe {
1563 std::slice::from_raw_parts_mut(
1564 bullish_low_mu[base..base + cols].as_mut_ptr() as *mut f64,
1565 cols,
1566 )
1567 };
1568 let out_bullish_kind = unsafe {
1569 std::slice::from_raw_parts_mut(
1570 bullish_kind_mu[base..base + cols].as_mut_ptr() as *mut f64,
1571 cols,
1572 )
1573 };
1574 let out_bullish_active = unsafe {
1575 std::slice::from_raw_parts_mut(
1576 bullish_active_mu[base..base + cols].as_mut_ptr() as *mut f64,
1577 cols,
1578 )
1579 };
1580 let out_bullish_mitigated = unsafe {
1581 std::slice::from_raw_parts_mut(
1582 bullish_mitigated_mu[base..base + cols].as_mut_ptr() as *mut f64,
1583 cols,
1584 )
1585 };
1586 let out_bullish_new = unsafe {
1587 std::slice::from_raw_parts_mut(
1588 bullish_new_mu[base..base + cols].as_mut_ptr() as *mut f64,
1589 cols,
1590 )
1591 };
1592 let out_bearish_high = unsafe {
1593 std::slice::from_raw_parts_mut(
1594 bearish_high_mu[base..base + cols].as_mut_ptr() as *mut f64,
1595 cols,
1596 )
1597 };
1598 let out_bearish_low = unsafe {
1599 std::slice::from_raw_parts_mut(
1600 bearish_low_mu[base..base + cols].as_mut_ptr() as *mut f64,
1601 cols,
1602 )
1603 };
1604 let out_bearish_kind = unsafe {
1605 std::slice::from_raw_parts_mut(
1606 bearish_kind_mu[base..base + cols].as_mut_ptr() as *mut f64,
1607 cols,
1608 )
1609 };
1610 let out_bearish_active = unsafe {
1611 std::slice::from_raw_parts_mut(
1612 bearish_active_mu[base..base + cols].as_mut_ptr() as *mut f64,
1613 cols,
1614 )
1615 };
1616 let out_bearish_mitigated = unsafe {
1617 std::slice::from_raw_parts_mut(
1618 bearish_mitigated_mu[base..base + cols].as_mut_ptr() as *mut f64,
1619 cols,
1620 )
1621 };
1622 let out_bearish_new = unsafe {
1623 std::slice::from_raw_parts_mut(
1624 bearish_new_mu[base..base + cols].as_mut_ptr() as *mut f64,
1625 cols,
1626 )
1627 };
1628
1629 ict_propulsion_block_row_scalar(
1630 open,
1631 high,
1632 low,
1633 close,
1634 combos[row].swing_length.unwrap_or(DEFAULT_SWING_LENGTH),
1635 combos[row]
1636 .mitigation_price
1637 .unwrap_or(IctPropulsionBlockMitigationPrice::Close),
1638 out_bullish_high,
1639 out_bullish_low,
1640 out_bullish_kind,
1641 out_bullish_active,
1642 out_bullish_mitigated,
1643 out_bullish_new,
1644 out_bearish_high,
1645 out_bearish_low,
1646 out_bearish_kind,
1647 out_bearish_active,
1648 out_bearish_mitigated,
1649 out_bearish_new,
1650 );
1651 }
1652
1653 let bullish_high = unsafe {
1654 Vec::from_raw_parts(
1655 bullish_high_guard.as_mut_ptr() as *mut f64,
1656 total,
1657 bullish_high_guard.capacity(),
1658 )
1659 };
1660 let bullish_low = unsafe {
1661 Vec::from_raw_parts(
1662 bullish_low_guard.as_mut_ptr() as *mut f64,
1663 total,
1664 bullish_low_guard.capacity(),
1665 )
1666 };
1667 let bullish_kind = unsafe {
1668 Vec::from_raw_parts(
1669 bullish_kind_guard.as_mut_ptr() as *mut f64,
1670 total,
1671 bullish_kind_guard.capacity(),
1672 )
1673 };
1674 let bullish_active = unsafe {
1675 Vec::from_raw_parts(
1676 bullish_active_guard.as_mut_ptr() as *mut f64,
1677 total,
1678 bullish_active_guard.capacity(),
1679 )
1680 };
1681 let bullish_mitigated = unsafe {
1682 Vec::from_raw_parts(
1683 bullish_mitigated_guard.as_mut_ptr() as *mut f64,
1684 total,
1685 bullish_mitigated_guard.capacity(),
1686 )
1687 };
1688 let bullish_new = unsafe {
1689 Vec::from_raw_parts(
1690 bullish_new_guard.as_mut_ptr() as *mut f64,
1691 total,
1692 bullish_new_guard.capacity(),
1693 )
1694 };
1695 let bearish_high = unsafe {
1696 Vec::from_raw_parts(
1697 bearish_high_guard.as_mut_ptr() as *mut f64,
1698 total,
1699 bearish_high_guard.capacity(),
1700 )
1701 };
1702 let bearish_low = unsafe {
1703 Vec::from_raw_parts(
1704 bearish_low_guard.as_mut_ptr() as *mut f64,
1705 total,
1706 bearish_low_guard.capacity(),
1707 )
1708 };
1709 let bearish_kind = unsafe {
1710 Vec::from_raw_parts(
1711 bearish_kind_guard.as_mut_ptr() as *mut f64,
1712 total,
1713 bearish_kind_guard.capacity(),
1714 )
1715 };
1716 let bearish_active = unsafe {
1717 Vec::from_raw_parts(
1718 bearish_active_guard.as_mut_ptr() as *mut f64,
1719 total,
1720 bearish_active_guard.capacity(),
1721 )
1722 };
1723 let bearish_mitigated = unsafe {
1724 Vec::from_raw_parts(
1725 bearish_mitigated_guard.as_mut_ptr() as *mut f64,
1726 total,
1727 bearish_mitigated_guard.capacity(),
1728 )
1729 };
1730 let bearish_new = unsafe {
1731 Vec::from_raw_parts(
1732 bearish_new_guard.as_mut_ptr() as *mut f64,
1733 total,
1734 bearish_new_guard.capacity(),
1735 )
1736 };
1737
1738 Ok(IctPropulsionBlockBatchOutput {
1739 bullish_high,
1740 bullish_low,
1741 bullish_kind,
1742 bullish_active,
1743 bullish_mitigated,
1744 bullish_new,
1745 bearish_high,
1746 bearish_low,
1747 bearish_kind,
1748 bearish_active,
1749 bearish_mitigated,
1750 bearish_new,
1751 combos,
1752 rows,
1753 cols,
1754 })
1755}
1756
1757#[allow(clippy::too_many_arguments)]
1758fn ict_propulsion_block_batch_inner_into(
1759 open: &[f64],
1760 high: &[f64],
1761 low: &[f64],
1762 close: &[f64],
1763 sweep: &IctPropulsionBlockBatchRange,
1764 kernel: Kernel,
1765 out_bullish_high: &mut [f64],
1766 out_bullish_low: &mut [f64],
1767 out_bullish_kind: &mut [f64],
1768 out_bullish_active: &mut [f64],
1769 out_bullish_mitigated: &mut [f64],
1770 out_bullish_new: &mut [f64],
1771 out_bearish_high: &mut [f64],
1772 out_bearish_low: &mut [f64],
1773 out_bearish_kind: &mut [f64],
1774 out_bearish_active: &mut [f64],
1775 out_bearish_mitigated: &mut [f64],
1776 out_bearish_new: &mut [f64],
1777) -> Result<Vec<IctPropulsionBlockParams>, IctPropulsionBlockError> {
1778 validate_lengths(open, high, low, close)?;
1779 let combos = expand_grid_ict_propulsion_block(sweep)?;
1780 for params in &combos {
1781 validate_params(
1782 params.swing_length.unwrap_or(DEFAULT_SWING_LENGTH),
1783 params
1784 .mitigation_price
1785 .unwrap_or(IctPropulsionBlockMitigationPrice::Close),
1786 )?;
1787 }
1788 let _first_valid =
1789 first_valid_bar(open, high, low, close).ok_or(IctPropulsionBlockError::AllValuesNaN)?;
1790 let rows = combos.len();
1791 let cols = close.len();
1792 let total = rows
1793 .checked_mul(cols)
1794 .ok_or(IctPropulsionBlockError::OutputLengthMismatch {
1795 expected: usize::MAX,
1796 got: 0,
1797 })?;
1798
1799 if out_bullish_high.len() != total
1800 || out_bullish_low.len() != total
1801 || out_bullish_kind.len() != total
1802 || out_bullish_active.len() != total
1803 || out_bullish_mitigated.len() != total
1804 || out_bullish_new.len() != total
1805 || out_bearish_high.len() != total
1806 || out_bearish_low.len() != total
1807 || out_bearish_kind.len() != total
1808 || out_bearish_active.len() != total
1809 || out_bearish_mitigated.len() != total
1810 || out_bearish_new.len() != total
1811 {
1812 return Err(IctPropulsionBlockError::OutputLengthMismatch {
1813 expected: total,
1814 got: out_bullish_high
1815 .len()
1816 .max(out_bullish_low.len())
1817 .max(out_bullish_kind.len())
1818 .max(out_bullish_active.len())
1819 .max(out_bullish_mitigated.len())
1820 .max(out_bullish_new.len())
1821 .max(out_bearish_high.len())
1822 .max(out_bearish_low.len())
1823 .max(out_bearish_kind.len())
1824 .max(out_bearish_active.len())
1825 .max(out_bearish_mitigated.len())
1826 .max(out_bearish_new.len()),
1827 });
1828 }
1829
1830 let _kernel = kernel;
1831 for row in 0..rows {
1832 let base = row * cols;
1833 ict_propulsion_block_row_scalar(
1834 open,
1835 high,
1836 low,
1837 close,
1838 combos[row].swing_length.unwrap_or(DEFAULT_SWING_LENGTH),
1839 combos[row]
1840 .mitigation_price
1841 .unwrap_or(IctPropulsionBlockMitigationPrice::Close),
1842 &mut out_bullish_high[base..base + cols],
1843 &mut out_bullish_low[base..base + cols],
1844 &mut out_bullish_kind[base..base + cols],
1845 &mut out_bullish_active[base..base + cols],
1846 &mut out_bullish_mitigated[base..base + cols],
1847 &mut out_bullish_new[base..base + cols],
1848 &mut out_bearish_high[base..base + cols],
1849 &mut out_bearish_low[base..base + cols],
1850 &mut out_bearish_kind[base..base + cols],
1851 &mut out_bearish_active[base..base + cols],
1852 &mut out_bearish_mitigated[base..base + cols],
1853 &mut out_bearish_new[base..base + cols],
1854 );
1855 }
1856 Ok(combos)
1857}
1858
1859fn parse_mitigation_price(
1860 value: &str,
1861) -> Result<IctPropulsionBlockMitigationPrice, IctPropulsionBlockError> {
1862 if value.eq_ignore_ascii_case("close") || value.eq_ignore_ascii_case("closing_price") {
1863 return Ok(IctPropulsionBlockMitigationPrice::Close);
1864 }
1865 if value.eq_ignore_ascii_case("wick") {
1866 return Ok(IctPropulsionBlockMitigationPrice::Wick);
1867 }
1868 Err(IctPropulsionBlockError::InvalidMitigationPrice {
1869 mitigation_price: value.to_string(),
1870 })
1871}
1872
1873#[cfg(feature = "python")]
1874#[pyfunction(name = "ict_propulsion_block")]
1875#[pyo3(signature = (open, high, low, close, swing_length=DEFAULT_SWING_LENGTH, mitigation_price="close", kernel=None))]
1876pub fn ict_propulsion_block_py<'py>(
1877 py: Python<'py>,
1878 open: PyReadonlyArray1<'py, f64>,
1879 high: PyReadonlyArray1<'py, f64>,
1880 low: PyReadonlyArray1<'py, f64>,
1881 close: PyReadonlyArray1<'py, f64>,
1882 swing_length: usize,
1883 mitigation_price: &str,
1884 kernel: Option<&str>,
1885) -> PyResult<(
1886 Bound<'py, PyArray1<f64>>,
1887 Bound<'py, PyArray1<f64>>,
1888 Bound<'py, PyArray1<f64>>,
1889 Bound<'py, PyArray1<f64>>,
1890 Bound<'py, PyArray1<f64>>,
1891 Bound<'py, PyArray1<f64>>,
1892 Bound<'py, PyArray1<f64>>,
1893 Bound<'py, PyArray1<f64>>,
1894 Bound<'py, PyArray1<f64>>,
1895 Bound<'py, PyArray1<f64>>,
1896 Bound<'py, PyArray1<f64>>,
1897 Bound<'py, PyArray1<f64>>,
1898)> {
1899 let open = open.as_slice()?;
1900 let high = high.as_slice()?;
1901 let low = low.as_slice()?;
1902 let close = close.as_slice()?;
1903 let input = IctPropulsionBlockInput::from_slices(
1904 open,
1905 high,
1906 low,
1907 close,
1908 IctPropulsionBlockParams {
1909 swing_length: Some(swing_length),
1910 mitigation_price: Some(
1911 parse_mitigation_price(mitigation_price)
1912 .map_err(|e| PyValueError::new_err(e.to_string()))?,
1913 ),
1914 },
1915 );
1916 let kernel = validate_kernel(kernel, false)?;
1917 let out = py
1918 .allow_threads(|| ict_propulsion_block_with_kernel(&input, kernel))
1919 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1920 Ok((
1921 out.bullish_high.into_pyarray(py),
1922 out.bullish_low.into_pyarray(py),
1923 out.bullish_kind.into_pyarray(py),
1924 out.bullish_active.into_pyarray(py),
1925 out.bullish_mitigated.into_pyarray(py),
1926 out.bullish_new.into_pyarray(py),
1927 out.bearish_high.into_pyarray(py),
1928 out.bearish_low.into_pyarray(py),
1929 out.bearish_kind.into_pyarray(py),
1930 out.bearish_active.into_pyarray(py),
1931 out.bearish_mitigated.into_pyarray(py),
1932 out.bearish_new.into_pyarray(py),
1933 ))
1934}
1935
1936#[cfg(feature = "python")]
1937#[pyclass(name = "IctPropulsionBlockStream")]
1938pub struct IctPropulsionBlockStreamPy {
1939 stream: IctPropulsionBlockStream,
1940}
1941
1942#[cfg(feature = "python")]
1943#[pymethods]
1944impl IctPropulsionBlockStreamPy {
1945 #[new]
1946 #[pyo3(signature = (swing_length=DEFAULT_SWING_LENGTH, mitigation_price="close"))]
1947 fn new(swing_length: usize, mitigation_price: &str) -> PyResult<Self> {
1948 let stream = IctPropulsionBlockStream::try_new(IctPropulsionBlockParams {
1949 swing_length: Some(swing_length),
1950 mitigation_price: Some(
1951 parse_mitigation_price(mitigation_price)
1952 .map_err(|e| PyValueError::new_err(e.to_string()))?,
1953 ),
1954 })
1955 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1956 Ok(Self { stream })
1957 }
1958
1959 fn update(
1960 &mut self,
1961 open: f64,
1962 high: f64,
1963 low: f64,
1964 close: f64,
1965 ) -> Option<(f64, f64, f64, f64, f64, f64, f64, f64, f64, f64, f64, f64)> {
1966 self.stream.update(open, high, low, close)
1967 }
1968}
1969
1970#[cfg(feature = "python")]
1971#[pyfunction(name = "ict_propulsion_block_batch")]
1972#[pyo3(signature = (open, high, low, close, swing_length_range, mitigation_price_toggle=(true, false), kernel=None))]
1973pub fn ict_propulsion_block_batch_py<'py>(
1974 py: Python<'py>,
1975 open: PyReadonlyArray1<'py, f64>,
1976 high: PyReadonlyArray1<'py, f64>,
1977 low: PyReadonlyArray1<'py, f64>,
1978 close: PyReadonlyArray1<'py, f64>,
1979 swing_length_range: (usize, usize, usize),
1980 mitigation_price_toggle: (bool, bool),
1981 kernel: Option<&str>,
1982) -> PyResult<Bound<'py, PyDict>> {
1983 let open = open.as_slice()?;
1984 let high = high.as_slice()?;
1985 let low = low.as_slice()?;
1986 let close = close.as_slice()?;
1987 let sweep = IctPropulsionBlockBatchRange {
1988 swing_length: swing_length_range,
1989 mitigation_price: mitigation_price_toggle,
1990 };
1991 let combos = expand_grid_ict_propulsion_block(&sweep)
1992 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1993 let rows = combos.len();
1994 let cols = close.len();
1995 let total = rows
1996 .checked_mul(cols)
1997 .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1998
1999 let bullish_high_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2000 let bullish_low_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2001 let bullish_kind_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2002 let bullish_active_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2003 let bullish_mitigated_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2004 let bullish_new_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2005 let bearish_high_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2006 let bearish_low_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2007 let bearish_kind_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2008 let bearish_active_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2009 let bearish_mitigated_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2010 let bearish_new_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2011
2012 let out_bullish_high = unsafe { bullish_high_arr.as_slice_mut()? };
2013 let out_bullish_low = unsafe { bullish_low_arr.as_slice_mut()? };
2014 let out_bullish_kind = unsafe { bullish_kind_arr.as_slice_mut()? };
2015 let out_bullish_active = unsafe { bullish_active_arr.as_slice_mut()? };
2016 let out_bullish_mitigated = unsafe { bullish_mitigated_arr.as_slice_mut()? };
2017 let out_bullish_new = unsafe { bullish_new_arr.as_slice_mut()? };
2018 let out_bearish_high = unsafe { bearish_high_arr.as_slice_mut()? };
2019 let out_bearish_low = unsafe { bearish_low_arr.as_slice_mut()? };
2020 let out_bearish_kind = unsafe { bearish_kind_arr.as_slice_mut()? };
2021 let out_bearish_active = unsafe { bearish_active_arr.as_slice_mut()? };
2022 let out_bearish_mitigated = unsafe { bearish_mitigated_arr.as_slice_mut()? };
2023 let out_bearish_new = unsafe { bearish_new_arr.as_slice_mut()? };
2024
2025 let kernel = validate_kernel(kernel, true)?;
2026 py.allow_threads(|| {
2027 let batch_kernel = match kernel {
2028 Kernel::Auto => detect_best_batch_kernel(),
2029 other => other,
2030 };
2031 ict_propulsion_block_batch_inner_into(
2032 open,
2033 high,
2034 low,
2035 close,
2036 &sweep,
2037 batch_kernel.to_non_batch(),
2038 out_bullish_high,
2039 out_bullish_low,
2040 out_bullish_kind,
2041 out_bullish_active,
2042 out_bullish_mitigated,
2043 out_bullish_new,
2044 out_bearish_high,
2045 out_bearish_low,
2046 out_bearish_kind,
2047 out_bearish_active,
2048 out_bearish_mitigated,
2049 out_bearish_new,
2050 )
2051 })
2052 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2053
2054 let swing_lengths: Vec<u64> = combos
2055 .iter()
2056 .map(|params| params.swing_length.unwrap_or(DEFAULT_SWING_LENGTH) as u64)
2057 .collect();
2058 let mitigation_prices: Vec<&str> = combos
2059 .iter()
2060 .map(|params| {
2061 params
2062 .mitigation_price
2063 .unwrap_or(IctPropulsionBlockMitigationPrice::Close)
2064 .as_str()
2065 })
2066 .collect();
2067
2068 let dict = PyDict::new(py);
2069 dict.set_item("bullish_high", bullish_high_arr.reshape((rows, cols))?)?;
2070 dict.set_item("bullish_low", bullish_low_arr.reshape((rows, cols))?)?;
2071 dict.set_item("bullish_kind", bullish_kind_arr.reshape((rows, cols))?)?;
2072 dict.set_item("bullish_active", bullish_active_arr.reshape((rows, cols))?)?;
2073 dict.set_item(
2074 "bullish_mitigated",
2075 bullish_mitigated_arr.reshape((rows, cols))?,
2076 )?;
2077 dict.set_item("bullish_new", bullish_new_arr.reshape((rows, cols))?)?;
2078 dict.set_item("bearish_high", bearish_high_arr.reshape((rows, cols))?)?;
2079 dict.set_item("bearish_low", bearish_low_arr.reshape((rows, cols))?)?;
2080 dict.set_item("bearish_kind", bearish_kind_arr.reshape((rows, cols))?)?;
2081 dict.set_item("bearish_active", bearish_active_arr.reshape((rows, cols))?)?;
2082 dict.set_item(
2083 "bearish_mitigated",
2084 bearish_mitigated_arr.reshape((rows, cols))?,
2085 )?;
2086 dict.set_item("bearish_new", bearish_new_arr.reshape((rows, cols))?)?;
2087 dict.set_item("rows", rows)?;
2088 dict.set_item("cols", cols)?;
2089 dict.set_item("swing_lengths", swing_lengths.into_pyarray(py))?;
2090 dict.set_item("mitigation_prices", PyList::new(py, mitigation_prices)?)?;
2091 Ok(dict)
2092}
2093
2094#[cfg(feature = "python")]
2095pub fn register_ict_propulsion_block_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
2096 m.add_function(wrap_pyfunction!(ict_propulsion_block_py, m)?)?;
2097 m.add_function(wrap_pyfunction!(ict_propulsion_block_batch_py, m)?)?;
2098 m.add_class::<IctPropulsionBlockStreamPy>()?;
2099 Ok(())
2100}
2101
2102#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2103#[derive(Debug, Clone, Serialize, Deserialize)]
2104struct IctPropulsionBlockJsOutput {
2105 bullish_high: Vec<f64>,
2106 bullish_low: Vec<f64>,
2107 bullish_kind: Vec<f64>,
2108 bullish_active: Vec<f64>,
2109 bullish_mitigated: Vec<f64>,
2110 bullish_new: Vec<f64>,
2111 bearish_high: Vec<f64>,
2112 bearish_low: Vec<f64>,
2113 bearish_kind: Vec<f64>,
2114 bearish_active: Vec<f64>,
2115 bearish_mitigated: Vec<f64>,
2116 bearish_new: Vec<f64>,
2117}
2118
2119#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2120#[derive(Debug, Clone, Serialize, Deserialize)]
2121struct IctPropulsionBlockBatchConfig {
2122 swing_length_range: Vec<usize>,
2123 mitigation_price_toggle: Vec<bool>,
2124}
2125
2126#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2127#[derive(Debug, Clone, Serialize, Deserialize)]
2128struct IctPropulsionBlockBatchJsOutput {
2129 bullish_high: Vec<f64>,
2130 bullish_low: Vec<f64>,
2131 bullish_kind: Vec<f64>,
2132 bullish_active: Vec<f64>,
2133 bullish_mitigated: Vec<f64>,
2134 bullish_new: Vec<f64>,
2135 bearish_high: Vec<f64>,
2136 bearish_low: Vec<f64>,
2137 bearish_kind: Vec<f64>,
2138 bearish_active: Vec<f64>,
2139 bearish_mitigated: Vec<f64>,
2140 bearish_new: Vec<f64>,
2141 rows: usize,
2142 cols: usize,
2143 combos: Vec<IctPropulsionBlockParams>,
2144}
2145
2146#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2147#[wasm_bindgen(js_name = "ict_propulsion_block")]
2148pub fn ict_propulsion_block_js(
2149 open: &[f64],
2150 high: &[f64],
2151 low: &[f64],
2152 close: &[f64],
2153 swing_length: usize,
2154 mitigation_price: &str,
2155) -> Result<JsValue, JsValue> {
2156 let input = IctPropulsionBlockInput::from_slices(
2157 open,
2158 high,
2159 low,
2160 close,
2161 IctPropulsionBlockParams {
2162 swing_length: Some(swing_length),
2163 mitigation_price: Some(
2164 parse_mitigation_price(mitigation_price)
2165 .map_err(|e| JsValue::from_str(&e.to_string()))?,
2166 ),
2167 },
2168 );
2169 let out = ict_propulsion_block_with_kernel(&input, Kernel::Scalar)
2170 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2171 serde_wasm_bindgen::to_value(&IctPropulsionBlockJsOutput {
2172 bullish_high: out.bullish_high,
2173 bullish_low: out.bullish_low,
2174 bullish_kind: out.bullish_kind,
2175 bullish_active: out.bullish_active,
2176 bullish_mitigated: out.bullish_mitigated,
2177 bullish_new: out.bullish_new,
2178 bearish_high: out.bearish_high,
2179 bearish_low: out.bearish_low,
2180 bearish_kind: out.bearish_kind,
2181 bearish_active: out.bearish_active,
2182 bearish_mitigated: out.bearish_mitigated,
2183 bearish_new: out.bearish_new,
2184 })
2185 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
2186}
2187
2188#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2189#[wasm_bindgen]
2190pub fn ict_propulsion_block_into(
2191 open_ptr: *const f64,
2192 high_ptr: *const f64,
2193 low_ptr: *const f64,
2194 close_ptr: *const f64,
2195 out_ptr: *mut f64,
2196 len: usize,
2197 swing_length: usize,
2198 mitigation_price: &str,
2199) -> Result<(), JsValue> {
2200 if open_ptr.is_null()
2201 || high_ptr.is_null()
2202 || low_ptr.is_null()
2203 || close_ptr.is_null()
2204 || out_ptr.is_null()
2205 {
2206 return Err(JsValue::from_str(
2207 "null pointer passed to ict_propulsion_block_into",
2208 ));
2209 }
2210
2211 unsafe {
2212 let open = std::slice::from_raw_parts(open_ptr, len);
2213 let high = std::slice::from_raw_parts(high_ptr, len);
2214 let low = std::slice::from_raw_parts(low_ptr, len);
2215 let close = std::slice::from_raw_parts(close_ptr, len);
2216 let out = std::slice::from_raw_parts_mut(out_ptr, len * 12);
2217 let (out_bullish_high, rest) = out.split_at_mut(len);
2218 let (out_bullish_low, rest) = rest.split_at_mut(len);
2219 let (out_bullish_kind, rest) = rest.split_at_mut(len);
2220 let (out_bullish_active, rest) = rest.split_at_mut(len);
2221 let (out_bullish_mitigated, rest) = rest.split_at_mut(len);
2222 let (out_bullish_new, rest) = rest.split_at_mut(len);
2223 let (out_bearish_high, rest) = rest.split_at_mut(len);
2224 let (out_bearish_low, rest) = rest.split_at_mut(len);
2225 let (out_bearish_kind, rest) = rest.split_at_mut(len);
2226 let (out_bearish_active, rest) = rest.split_at_mut(len);
2227 let (out_bearish_mitigated, out_bearish_new) = rest.split_at_mut(len);
2228 let input = IctPropulsionBlockInput::from_slices(
2229 open,
2230 high,
2231 low,
2232 close,
2233 IctPropulsionBlockParams {
2234 swing_length: Some(swing_length),
2235 mitigation_price: Some(
2236 parse_mitigation_price(mitigation_price)
2237 .map_err(|e| JsValue::from_str(&e.to_string()))?,
2238 ),
2239 },
2240 );
2241 ict_propulsion_block_into_slice(
2242 out_bullish_high,
2243 out_bullish_low,
2244 out_bullish_kind,
2245 out_bullish_active,
2246 out_bullish_mitigated,
2247 out_bullish_new,
2248 out_bearish_high,
2249 out_bearish_low,
2250 out_bearish_kind,
2251 out_bearish_active,
2252 out_bearish_mitigated,
2253 out_bearish_new,
2254 &input,
2255 Kernel::Scalar,
2256 )
2257 .map_err(|e| JsValue::from_str(&e.to_string()))
2258 }
2259}
2260
2261#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2262#[wasm_bindgen(js_name = "ict_propulsion_block_into_host")]
2263pub fn ict_propulsion_block_into_host(
2264 open: &[f64],
2265 high: &[f64],
2266 low: &[f64],
2267 close: &[f64],
2268 out_ptr: *mut f64,
2269 swing_length: usize,
2270 mitigation_price: &str,
2271) -> Result<(), JsValue> {
2272 if out_ptr.is_null() {
2273 return Err(JsValue::from_str(
2274 "null pointer passed to ict_propulsion_block_into_host",
2275 ));
2276 }
2277
2278 unsafe {
2279 let len = close.len();
2280 let out = std::slice::from_raw_parts_mut(out_ptr, len * 12);
2281 let (out_bullish_high, rest) = out.split_at_mut(len);
2282 let (out_bullish_low, rest) = rest.split_at_mut(len);
2283 let (out_bullish_kind, rest) = rest.split_at_mut(len);
2284 let (out_bullish_active, rest) = rest.split_at_mut(len);
2285 let (out_bullish_mitigated, rest) = rest.split_at_mut(len);
2286 let (out_bullish_new, rest) = rest.split_at_mut(len);
2287 let (out_bearish_high, rest) = rest.split_at_mut(len);
2288 let (out_bearish_low, rest) = rest.split_at_mut(len);
2289 let (out_bearish_kind, rest) = rest.split_at_mut(len);
2290 let (out_bearish_active, rest) = rest.split_at_mut(len);
2291 let (out_bearish_mitigated, out_bearish_new) = rest.split_at_mut(len);
2292 let input = IctPropulsionBlockInput::from_slices(
2293 open,
2294 high,
2295 low,
2296 close,
2297 IctPropulsionBlockParams {
2298 swing_length: Some(swing_length),
2299 mitigation_price: Some(
2300 parse_mitigation_price(mitigation_price)
2301 .map_err(|e| JsValue::from_str(&e.to_string()))?,
2302 ),
2303 },
2304 );
2305 ict_propulsion_block_into_slice(
2306 out_bullish_high,
2307 out_bullish_low,
2308 out_bullish_kind,
2309 out_bullish_active,
2310 out_bullish_mitigated,
2311 out_bullish_new,
2312 out_bearish_high,
2313 out_bearish_low,
2314 out_bearish_kind,
2315 out_bearish_active,
2316 out_bearish_mitigated,
2317 out_bearish_new,
2318 &input,
2319 Kernel::Scalar,
2320 )
2321 .map_err(|e| JsValue::from_str(&e.to_string()))
2322 }
2323}
2324
2325#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2326#[wasm_bindgen]
2327pub fn ict_propulsion_block_alloc(len: usize) -> *mut f64 {
2328 let mut buf = vec![0.0_f64; len * 12];
2329 let ptr = buf.as_mut_ptr();
2330 std::mem::forget(buf);
2331 ptr
2332}
2333
2334#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2335#[wasm_bindgen]
2336pub fn ict_propulsion_block_free(ptr: *mut f64, len: usize) {
2337 if ptr.is_null() {
2338 return;
2339 }
2340 unsafe {
2341 let _ = Vec::from_raw_parts(ptr, len * 12, len * 12);
2342 }
2343}
2344
2345#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2346#[wasm_bindgen(js_name = "ict_propulsion_block_batch")]
2347pub fn ict_propulsion_block_batch_js(
2348 open: &[f64],
2349 high: &[f64],
2350 low: &[f64],
2351 close: &[f64],
2352 config: JsValue,
2353) -> Result<JsValue, JsValue> {
2354 let config: IctPropulsionBlockBatchConfig = serde_wasm_bindgen::from_value(config)
2355 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
2356 if config.swing_length_range.len() != 3 {
2357 return Err(JsValue::from_str(
2358 "Invalid config: swing_length_range must have exactly 3 elements [start, end, step]",
2359 ));
2360 }
2361 if config.mitigation_price_toggle.len() != 2 {
2362 return Err(JsValue::from_str(
2363 "Invalid config: mitigation_price_toggle must have exactly 2 booleans [include_close, include_wick]",
2364 ));
2365 }
2366
2367 let sweep = IctPropulsionBlockBatchRange {
2368 swing_length: (
2369 config.swing_length_range[0],
2370 config.swing_length_range[1],
2371 config.swing_length_range[2],
2372 ),
2373 mitigation_price: (
2374 config.mitigation_price_toggle[0],
2375 config.mitigation_price_toggle[1],
2376 ),
2377 };
2378 let out =
2379 ict_propulsion_block_batch_with_kernel(open, high, low, close, &sweep, Kernel::ScalarBatch)
2380 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2381 serde_wasm_bindgen::to_value(&IctPropulsionBlockBatchJsOutput {
2382 bullish_high: out.bullish_high,
2383 bullish_low: out.bullish_low,
2384 bullish_kind: out.bullish_kind,
2385 bullish_active: out.bullish_active,
2386 bullish_mitigated: out.bullish_mitigated,
2387 bullish_new: out.bullish_new,
2388 bearish_high: out.bearish_high,
2389 bearish_low: out.bearish_low,
2390 bearish_kind: out.bearish_kind,
2391 bearish_active: out.bearish_active,
2392 bearish_mitigated: out.bearish_mitigated,
2393 bearish_new: out.bearish_new,
2394 rows: out.rows,
2395 cols: out.cols,
2396 combos: out.combos,
2397 })
2398 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
2399}
2400
2401#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2402#[wasm_bindgen]
2403#[allow(clippy::too_many_arguments)]
2404pub fn ict_propulsion_block_batch_into(
2405 open_ptr: *const f64,
2406 high_ptr: *const f64,
2407 low_ptr: *const f64,
2408 close_ptr: *const f64,
2409 bullish_high_ptr: *mut f64,
2410 bullish_low_ptr: *mut f64,
2411 bullish_kind_ptr: *mut f64,
2412 bullish_active_ptr: *mut f64,
2413 bullish_mitigated_ptr: *mut f64,
2414 bullish_new_ptr: *mut f64,
2415 bearish_high_ptr: *mut f64,
2416 bearish_low_ptr: *mut f64,
2417 bearish_kind_ptr: *mut f64,
2418 bearish_active_ptr: *mut f64,
2419 bearish_mitigated_ptr: *mut f64,
2420 bearish_new_ptr: *mut f64,
2421 len: usize,
2422 swing_start: usize,
2423 swing_end: usize,
2424 swing_step: usize,
2425 include_close: bool,
2426 include_wick: bool,
2427) -> Result<usize, JsValue> {
2428 if open_ptr.is_null()
2429 || high_ptr.is_null()
2430 || low_ptr.is_null()
2431 || close_ptr.is_null()
2432 || bullish_high_ptr.is_null()
2433 || bullish_low_ptr.is_null()
2434 || bullish_kind_ptr.is_null()
2435 || bullish_active_ptr.is_null()
2436 || bullish_mitigated_ptr.is_null()
2437 || bullish_new_ptr.is_null()
2438 || bearish_high_ptr.is_null()
2439 || bearish_low_ptr.is_null()
2440 || bearish_kind_ptr.is_null()
2441 || bearish_active_ptr.is_null()
2442 || bearish_mitigated_ptr.is_null()
2443 || bearish_new_ptr.is_null()
2444 {
2445 return Err(JsValue::from_str(
2446 "null pointer passed to ict_propulsion_block_batch_into",
2447 ));
2448 }
2449
2450 unsafe {
2451 let open = std::slice::from_raw_parts(open_ptr, len);
2452 let high = std::slice::from_raw_parts(high_ptr, len);
2453 let low = std::slice::from_raw_parts(low_ptr, len);
2454 let close = std::slice::from_raw_parts(close_ptr, len);
2455 let sweep = IctPropulsionBlockBatchRange {
2456 swing_length: (swing_start, swing_end, swing_step),
2457 mitigation_price: (include_close, include_wick),
2458 };
2459 let combos = expand_grid_ict_propulsion_block(&sweep)
2460 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2461 let total = combos.len().checked_mul(len).ok_or_else(|| {
2462 JsValue::from_str("rows*cols overflow in ict_propulsion_block_batch_into")
2463 })?;
2464
2465 let out_bullish_high = std::slice::from_raw_parts_mut(bullish_high_ptr, total);
2466 let out_bullish_low = std::slice::from_raw_parts_mut(bullish_low_ptr, total);
2467 let out_bullish_kind = std::slice::from_raw_parts_mut(bullish_kind_ptr, total);
2468 let out_bullish_active = std::slice::from_raw_parts_mut(bullish_active_ptr, total);
2469 let out_bullish_mitigated = std::slice::from_raw_parts_mut(bullish_mitigated_ptr, total);
2470 let out_bullish_new = std::slice::from_raw_parts_mut(bullish_new_ptr, total);
2471 let out_bearish_high = std::slice::from_raw_parts_mut(bearish_high_ptr, total);
2472 let out_bearish_low = std::slice::from_raw_parts_mut(bearish_low_ptr, total);
2473 let out_bearish_kind = std::slice::from_raw_parts_mut(bearish_kind_ptr, total);
2474 let out_bearish_active = std::slice::from_raw_parts_mut(bearish_active_ptr, total);
2475 let out_bearish_mitigated = std::slice::from_raw_parts_mut(bearish_mitigated_ptr, total);
2476 let out_bearish_new = std::slice::from_raw_parts_mut(bearish_new_ptr, total);
2477
2478 ict_propulsion_block_batch_inner_into(
2479 open,
2480 high,
2481 low,
2482 close,
2483 &sweep,
2484 Kernel::Scalar,
2485 out_bullish_high,
2486 out_bullish_low,
2487 out_bullish_kind,
2488 out_bullish_active,
2489 out_bullish_mitigated,
2490 out_bullish_new,
2491 out_bearish_high,
2492 out_bearish_low,
2493 out_bearish_kind,
2494 out_bearish_active,
2495 out_bearish_mitigated,
2496 out_bearish_new,
2497 )
2498 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2499 Ok(combos.len())
2500 }
2501}
2502
2503#[cfg(test)]
2504mod tests {
2505 use super::*;
2506 use crate::indicators::dispatch::{
2507 compute_cpu_batch, IndicatorBatchRequest, IndicatorDataRef, IndicatorParamSet, ParamKV,
2508 ParamValue,
2509 };
2510 use crate::utilities::data_loader::read_candles_from_csv;
2511 use crate::utilities::enums::Kernel;
2512
2513 fn load_candles() -> Candles {
2514 read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv")
2515 .expect("test candles")
2516 }
2517
2518 fn eq_or_both_nan(lhs: &[f64], rhs: &[f64]) -> bool {
2519 lhs.iter()
2520 .zip(rhs.iter())
2521 .all(|(a, b)| (a.is_nan() && b.is_nan()) || a == b)
2522 }
2523
2524 #[test]
2525 fn output_contract() {
2526 let candles = load_candles();
2527 let input = IctPropulsionBlockInput::from_slices(
2528 &candles.open[..320],
2529 &candles.high[..320],
2530 &candles.low[..320],
2531 &candles.close[..320],
2532 IctPropulsionBlockParams::default(),
2533 );
2534 let out = ict_propulsion_block(&input).expect("ict_propulsion_block");
2535 assert_eq!(out.bullish_high.len(), 320);
2536 assert_eq!(out.bearish_high.len(), 320);
2537 assert!(out
2538 .bullish_kind
2539 .iter()
2540 .any(|v| v.is_finite() && (*v == 1.0 || *v == 2.0)));
2541 for &kind in out.bullish_kind.iter().chain(out.bearish_kind.iter()) {
2542 assert!(kind.is_nan() || kind == 0.0 || kind == 1.0 || kind == 2.0);
2543 }
2544 }
2545
2546 #[test]
2547 fn invalid_params() {
2548 let candles = load_candles();
2549 let input = IctPropulsionBlockInput::from_slices(
2550 &candles.open[..64],
2551 &candles.high[..64],
2552 &candles.low[..64],
2553 &candles.close[..64],
2554 IctPropulsionBlockParams {
2555 swing_length: Some(0),
2556 mitigation_price: Some(IctPropulsionBlockMitigationPrice::Close),
2557 },
2558 );
2559 assert!(matches!(
2560 ict_propulsion_block(&input),
2561 Err(IctPropulsionBlockError::InvalidSwingLength { swing_length: 0 })
2562 ));
2563 }
2564
2565 #[test]
2566 fn into_matches_direct() {
2567 let candles = load_candles();
2568 let input = IctPropulsionBlockInput::from_slices(
2569 &candles.open[..220],
2570 &candles.high[..220],
2571 &candles.low[..220],
2572 &candles.close[..220],
2573 IctPropulsionBlockParams::default(),
2574 );
2575 let direct = ict_propulsion_block(&input).expect("direct");
2576 let mut bullish_high = vec![f64::NAN; 220];
2577 let mut bullish_low = vec![f64::NAN; 220];
2578 let mut bullish_kind = vec![f64::NAN; 220];
2579 let mut bullish_active = vec![f64::NAN; 220];
2580 let mut bullish_mitigated = vec![f64::NAN; 220];
2581 let mut bullish_new = vec![f64::NAN; 220];
2582 let mut bearish_high = vec![f64::NAN; 220];
2583 let mut bearish_low = vec![f64::NAN; 220];
2584 let mut bearish_kind = vec![f64::NAN; 220];
2585 let mut bearish_active = vec![f64::NAN; 220];
2586 let mut bearish_mitigated = vec![f64::NAN; 220];
2587 let mut bearish_new = vec![f64::NAN; 220];
2588
2589 ict_propulsion_block_into_slice(
2590 &mut bullish_high,
2591 &mut bullish_low,
2592 &mut bullish_kind,
2593 &mut bullish_active,
2594 &mut bullish_mitigated,
2595 &mut bullish_new,
2596 &mut bearish_high,
2597 &mut bearish_low,
2598 &mut bearish_kind,
2599 &mut bearish_active,
2600 &mut bearish_mitigated,
2601 &mut bearish_new,
2602 &input,
2603 Kernel::Scalar,
2604 )
2605 .expect("into");
2606
2607 assert!(eq_or_both_nan(&bullish_high, &direct.bullish_high));
2608 assert!(eq_or_both_nan(&bearish_kind, &direct.bearish_kind));
2609 assert!(eq_or_both_nan(&bullish_new, &direct.bullish_new));
2610 }
2611
2612 #[test]
2613 fn stream_matches_batch() {
2614 let candles = load_candles();
2615 let open = &candles.open[..180];
2616 let high = &candles.high[..180];
2617 let low = &candles.low[..180];
2618 let close = &candles.close[..180];
2619 let input = IctPropulsionBlockInput::from_slices(
2620 open,
2621 high,
2622 low,
2623 close,
2624 IctPropulsionBlockParams::default(),
2625 );
2626 let batch = ict_propulsion_block(&input).expect("batch");
2627 let mut stream =
2628 IctPropulsionBlockStream::try_new(IctPropulsionBlockParams::default()).expect("stream");
2629 let mut bullish_high = Vec::new();
2630 let mut bearish_new = Vec::new();
2631 for i in 0..open.len() {
2632 let out = stream
2633 .update(open[i], high[i], low[i], close[i])
2634 .expect("stream update");
2635 bullish_high.push(out.0);
2636 bearish_new.push(out.11);
2637 }
2638 assert!(eq_or_both_nan(&bullish_high, &batch.bullish_high));
2639 assert!(eq_or_both_nan(&bearish_new, &batch.bearish_new));
2640 }
2641
2642 #[test]
2643 fn batch_single_param_matches_single() {
2644 let candles = load_candles();
2645 let open = &candles.open[..160];
2646 let high = &candles.high[..160];
2647 let low = &candles.low[..160];
2648 let close = &candles.close[..160];
2649 let batch = ict_propulsion_block_batch_with_kernel(
2650 open,
2651 high,
2652 low,
2653 close,
2654 &IctPropulsionBlockBatchRange {
2655 swing_length: (3, 3, 0),
2656 mitigation_price: (true, false),
2657 },
2658 Kernel::ScalarBatch,
2659 )
2660 .expect("batch");
2661 let single = ict_propulsion_block(&IctPropulsionBlockInput::from_slices(
2662 open,
2663 high,
2664 low,
2665 close,
2666 IctPropulsionBlockParams::default(),
2667 ))
2668 .expect("single");
2669 assert_eq!(batch.rows, 1);
2670 assert_eq!(batch.cols, close.len());
2671 assert!(eq_or_both_nan(
2672 &batch.bullish_high[..close.len()],
2673 &single.bullish_high[..]
2674 ));
2675 assert!(eq_or_both_nan(
2676 &batch.bearish_low[..close.len()],
2677 &single.bearish_low[..]
2678 ));
2679 }
2680
2681 #[test]
2682 fn dispatch_matches_direct() {
2683 let candles = load_candles();
2684 let combos = [IndicatorParamSet {
2685 params: &[
2686 ParamKV {
2687 key: "swing_length",
2688 value: ParamValue::Int(3),
2689 },
2690 ParamKV {
2691 key: "mitigation_price",
2692 value: ParamValue::EnumString("close"),
2693 },
2694 ],
2695 }];
2696 let dispatched = compute_cpu_batch(IndicatorBatchRequest {
2697 indicator_id: "ict_propulsion_block",
2698 output_id: Some("bullish_high"),
2699 data: IndicatorDataRef::Ohlc {
2700 open: &candles.open[..160],
2701 high: &candles.high[..160],
2702 low: &candles.low[..160],
2703 close: &candles.close[..160],
2704 },
2705 combos: &combos,
2706 kernel: Kernel::ScalarBatch,
2707 })
2708 .expect("dispatch");
2709 let direct = ict_propulsion_block(&IctPropulsionBlockInput::from_slices(
2710 &candles.open[..160],
2711 &candles.high[..160],
2712 &candles.low[..160],
2713 &candles.close[..160],
2714 IctPropulsionBlockParams::default(),
2715 ))
2716 .expect("direct");
2717 assert!(eq_or_both_nan(
2718 &dispatched.values_f64.expect("f64"),
2719 &direct.bullish_high
2720 ));
2721 }
2722}