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