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, detect_best_kernel, init_matrix_prefixes,
21 make_uninit_matrix,
22};
23#[cfg(feature = "python")]
24use crate::utilities::kernel_validation::validate_kernel;
25#[cfg(not(target_arch = "wasm32"))]
26use rayon::prelude::*;
27use std::collections::VecDeque;
28use std::mem::{ManuallyDrop, MaybeUninit};
29use thiserror::Error;
30
31const DEFAULT_SWING_SIZE: usize = 10;
32const DEFAULT_BOS_CONFIRMATION: &str = "Candle Close";
33const DEFAULT_BASIS_LENGTH: usize = 100;
34const DEFAULT_ATR_LENGTH: usize = 14;
35const DEFAULT_ATR_SMOOTH: usize = 21;
36const DEFAULT_VOL_MULT: f64 = 2.0;
37const EPS: f64 = 1e-12;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum MarketStructureConfluenceBosConfirmation {
41 CandleClose,
42 Wicks,
43}
44
45impl MarketStructureConfluenceBosConfirmation {
46 #[inline(always)]
47 fn parse(value: &str) -> Option<Self> {
48 match value {
49 "Candle Close" | "candle_close" | "candle close" => Some(Self::CandleClose),
50 "Wicks" | "wicks" => Some(Self::Wicks),
51 _ => None,
52 }
53 }
54}
55
56#[derive(Debug, Clone)]
57pub enum MarketStructureConfluenceData<'a> {
58 Candles {
59 candles: &'a Candles,
60 },
61 Slices {
62 high: &'a [f64],
63 low: &'a [f64],
64 close: &'a [f64],
65 },
66}
67
68#[derive(Debug, Clone)]
69pub struct MarketStructureConfluenceOutput {
70 pub basis: Vec<f64>,
71 pub upper_band: Vec<f64>,
72 pub lower_band: Vec<f64>,
73 pub structure_direction: Vec<f64>,
74 pub bullish_arrow: Vec<f64>,
75 pub bearish_arrow: Vec<f64>,
76 pub bullish_change: Vec<f64>,
77 pub bearish_change: Vec<f64>,
78 pub hh: Vec<f64>,
79 pub lh: Vec<f64>,
80 pub hl: Vec<f64>,
81 pub ll: Vec<f64>,
82 pub bullish_bos: Vec<f64>,
83 pub bullish_choch: Vec<f64>,
84 pub bearish_bos: Vec<f64>,
85 pub bearish_choch: Vec<f64>,
86}
87
88#[derive(Debug, Clone)]
89#[cfg_attr(
90 all(target_arch = "wasm32", feature = "wasm"),
91 derive(Serialize, Deserialize)
92)]
93pub struct MarketStructureConfluenceParams {
94 pub swing_size: Option<usize>,
95 pub bos_confirmation: Option<String>,
96 pub basis_length: Option<usize>,
97 pub atr_length: Option<usize>,
98 pub atr_smooth: Option<usize>,
99 pub vol_mult: Option<f64>,
100}
101
102impl Default for MarketStructureConfluenceParams {
103 fn default() -> Self {
104 Self {
105 swing_size: Some(DEFAULT_SWING_SIZE),
106 bos_confirmation: Some(DEFAULT_BOS_CONFIRMATION.to_string()),
107 basis_length: Some(DEFAULT_BASIS_LENGTH),
108 atr_length: Some(DEFAULT_ATR_LENGTH),
109 atr_smooth: Some(DEFAULT_ATR_SMOOTH),
110 vol_mult: Some(DEFAULT_VOL_MULT),
111 }
112 }
113}
114
115#[derive(Debug, Clone)]
116pub struct MarketStructureConfluenceInput<'a> {
117 pub data: MarketStructureConfluenceData<'a>,
118 pub params: MarketStructureConfluenceParams,
119}
120
121impl<'a> MarketStructureConfluenceInput<'a> {
122 #[inline]
123 pub fn from_candles(candles: &'a Candles, params: MarketStructureConfluenceParams) -> Self {
124 Self {
125 data: MarketStructureConfluenceData::Candles { candles },
126 params,
127 }
128 }
129
130 #[inline]
131 pub fn from_slices(
132 high: &'a [f64],
133 low: &'a [f64],
134 close: &'a [f64],
135 params: MarketStructureConfluenceParams,
136 ) -> Self {
137 Self {
138 data: MarketStructureConfluenceData::Slices { high, low, close },
139 params,
140 }
141 }
142
143 #[inline]
144 pub fn with_default_candles(candles: &'a Candles) -> Self {
145 Self::from_candles(candles, MarketStructureConfluenceParams::default())
146 }
147}
148
149#[derive(Clone, Debug)]
150pub struct MarketStructureConfluenceBuilder {
151 swing_size: Option<usize>,
152 bos_confirmation: Option<String>,
153 basis_length: Option<usize>,
154 atr_length: Option<usize>,
155 atr_smooth: Option<usize>,
156 vol_mult: Option<f64>,
157 kernel: Kernel,
158}
159
160impl Default for MarketStructureConfluenceBuilder {
161 fn default() -> Self {
162 Self {
163 swing_size: None,
164 bos_confirmation: None,
165 basis_length: None,
166 atr_length: None,
167 atr_smooth: None,
168 vol_mult: None,
169 kernel: Kernel::Auto,
170 }
171 }
172}
173
174impl MarketStructureConfluenceBuilder {
175 #[inline(always)]
176 pub fn new() -> Self {
177 Self::default()
178 }
179
180 #[inline(always)]
181 pub fn swing_size(mut self, value: usize) -> Self {
182 self.swing_size = Some(value);
183 self
184 }
185
186 #[inline(always)]
187 pub fn bos_confirmation<S: Into<String>>(mut self, value: S) -> Self {
188 self.bos_confirmation = Some(value.into());
189 self
190 }
191
192 #[inline(always)]
193 pub fn basis_length(mut self, value: usize) -> Self {
194 self.basis_length = Some(value);
195 self
196 }
197
198 #[inline(always)]
199 pub fn atr_length(mut self, value: usize) -> Self {
200 self.atr_length = Some(value);
201 self
202 }
203
204 #[inline(always)]
205 pub fn atr_smooth(mut self, value: usize) -> Self {
206 self.atr_smooth = Some(value);
207 self
208 }
209
210 #[inline(always)]
211 pub fn vol_mult(mut self, value: f64) -> Self {
212 self.vol_mult = Some(value);
213 self
214 }
215
216 #[inline(always)]
217 pub fn kernel(mut self, value: Kernel) -> Self {
218 self.kernel = value;
219 self
220 }
221}
222
223#[derive(Debug, Error)]
224pub enum MarketStructureConfluenceError {
225 #[error("market_structure_confluence: input data slice is empty")]
226 EmptyInputData,
227 #[error(
228 "market_structure_confluence: data length mismatch: high={high}, low={low}, close={close}"
229 )]
230 DataLengthMismatch {
231 high: usize,
232 low: usize,
233 close: usize,
234 },
235 #[error("market_structure_confluence: all values are NaN")]
236 AllValuesNaN,
237 #[error("market_structure_confluence: invalid swing_size: swing_size = {swing_size}, data length = {data_len}")]
238 InvalidSwingSize { swing_size: usize, data_len: usize },
239 #[error("market_structure_confluence: invalid bos_confirmation: {bos_confirmation}")]
240 InvalidBosConfirmation { bos_confirmation: String },
241 #[error("market_structure_confluence: invalid basis_length: basis_length = {basis_length}, data length = {data_len}")]
242 InvalidBasisLength {
243 basis_length: usize,
244 data_len: usize,
245 },
246 #[error("market_structure_confluence: invalid atr_length: atr_length = {atr_length}, data length = {data_len}")]
247 InvalidAtrLength { atr_length: usize, data_len: usize },
248 #[error("market_structure_confluence: invalid atr_smooth: atr_smooth = {atr_smooth}, data length = {data_len}")]
249 InvalidAtrSmooth { atr_smooth: usize, data_len: usize },
250 #[error("market_structure_confluence: invalid vol_mult: {vol_mult}")]
251 InvalidVolMult { vol_mult: f64 },
252 #[error(
253 "market_structure_confluence: not enough valid data: needed = {needed}, valid = {valid}"
254 )]
255 NotEnoughValidData { needed: usize, valid: usize },
256 #[error("market_structure_confluence: output length mismatch: expected {expected}, got {got}")]
257 OutputLengthMismatch { expected: usize, got: usize },
258 #[error("market_structure_confluence: invalid range: start={start}, end={end}, step={step}")]
259 InvalidRange {
260 start: String,
261 end: String,
262 step: String,
263 },
264 #[error("market_structure_confluence: invalid kernel for batch: {0:?}")]
265 InvalidKernelForBatch(Kernel),
266}
267
268#[derive(Clone, Copy, Debug)]
269struct ResolvedParams {
270 swing_size: usize,
271 bos_confirmation: MarketStructureConfluenceBosConfirmation,
272 basis_length: usize,
273 atr_length: usize,
274 atr_smooth: usize,
275 vol_mult: f64,
276}
277
278#[derive(Clone, Debug)]
279struct PreparedInput<'a> {
280 high: &'a [f64],
281 low: &'a [f64],
282 close: &'a [f64],
283 len: usize,
284 params: ResolvedParams,
285 warmup: usize,
286}
287
288#[derive(Clone, Copy, Debug)]
289struct MarketStructureConfluencePoint {
290 basis: f64,
291 upper_band: f64,
292 lower_band: f64,
293 structure_direction: f64,
294 bullish_arrow: f64,
295 bearish_arrow: f64,
296 bullish_change: f64,
297 bearish_change: f64,
298 hh: f64,
299 lh: f64,
300 hl: f64,
301 ll: f64,
302 bullish_bos: f64,
303 bullish_choch: f64,
304 bearish_bos: f64,
305 bearish_choch: f64,
306}
307
308#[derive(Clone, Copy, Debug, PartialEq)]
309pub struct MarketStructureConfluenceStreamOutput {
310 pub basis: f64,
311 pub upper_band: f64,
312 pub lower_band: f64,
313 pub structure_direction: f64,
314 pub bullish_arrow: f64,
315 pub bearish_arrow: f64,
316 pub bullish_change: f64,
317 pub bearish_change: f64,
318 pub hh: f64,
319 pub lh: f64,
320 pub hl: f64,
321 pub ll: f64,
322 pub bullish_bos: f64,
323 pub bullish_choch: f64,
324 pub bearish_bos: f64,
325 pub bearish_choch: f64,
326}
327
328#[derive(Clone, Debug)]
329struct AtrState {
330 period: usize,
331 count: usize,
332 sum: f64,
333 value: Option<f64>,
334 prev_close: Option<f64>,
335}
336
337impl AtrState {
338 #[inline(always)]
339 fn new(period: usize) -> Self {
340 Self {
341 period,
342 count: 0,
343 sum: 0.0,
344 value: None,
345 prev_close: None,
346 }
347 }
348
349 #[inline(always)]
350 fn reset(&mut self) {
351 self.count = 0;
352 self.sum = 0.0;
353 self.value = None;
354 self.prev_close = None;
355 }
356
357 #[inline(always)]
358 fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
359 let tr = if let Some(prev_close) = self.prev_close {
360 let hl = high - low;
361 let hc = (high - prev_close).abs();
362 let lc = (low - prev_close).abs();
363 hl.max(hc).max(lc)
364 } else {
365 high - low
366 };
367 self.prev_close = Some(close);
368 if let Some(prev) = self.value {
369 let next = ((prev * (self.period as f64 - 1.0)) + tr) / self.period as f64;
370 self.value = Some(next);
371 Some(next)
372 } else {
373 self.count += 1;
374 self.sum += tr;
375 if self.count == self.period {
376 let seeded = self.sum / self.period as f64;
377 self.value = Some(seeded);
378 Some(seeded)
379 } else {
380 None
381 }
382 }
383 }
384}
385
386#[derive(Clone, Debug)]
387struct RollingSma {
388 period: usize,
389 buffer: VecDeque<f64>,
390 sum: f64,
391}
392
393impl RollingSma {
394 #[inline(always)]
395 fn new(period: usize) -> Self {
396 Self {
397 period,
398 buffer: VecDeque::with_capacity(period),
399 sum: 0.0,
400 }
401 }
402
403 #[inline(always)]
404 fn reset(&mut self) {
405 self.buffer.clear();
406 self.sum = 0.0;
407 }
408
409 #[inline(always)]
410 fn update(&mut self, value: f64) -> Option<f64> {
411 if self.buffer.len() == self.period {
412 if let Some(old) = self.buffer.pop_front() {
413 self.sum -= old;
414 }
415 }
416 self.buffer.push_back(value);
417 self.sum += value;
418 if self.buffer.len() < self.period {
419 None
420 } else {
421 Some(self.sum / self.period as f64)
422 }
423 }
424}
425
426#[derive(Clone, Debug)]
427struct WmaState {
428 period: usize,
429 buffer: Vec<f64>,
430 pos: usize,
431 len: usize,
432 sum: f64,
433 weighted_sum: f64,
434 divisor: f64,
435}
436
437impl WmaState {
438 #[inline(always)]
439 fn new(period: usize) -> Self {
440 Self {
441 period,
442 buffer: vec![0.0; period],
443 pos: 0,
444 len: 0,
445 sum: 0.0,
446 weighted_sum: 0.0,
447 divisor: (period as f64) * (period as f64 + 1.0) * 0.5,
448 }
449 }
450
451 #[inline(always)]
452 fn reset(&mut self) {
453 self.buffer.fill(0.0);
454 self.pos = 0;
455 self.len = 0;
456 self.sum = 0.0;
457 self.weighted_sum = 0.0;
458 }
459
460 #[inline(always)]
461 fn update(&mut self, value: f64) -> Option<f64> {
462 if self.len < self.period {
463 self.buffer[self.pos] = value;
464 self.pos = (self.pos + 1) % self.period;
465 self.len += 1;
466 self.sum += value;
467 self.weighted_sum += self.len as f64 * value;
468 if self.len == self.period {
469 Some(self.weighted_sum / self.divisor)
470 } else {
471 None
472 }
473 } else {
474 let old = self.buffer[self.pos];
475 let old_sum = self.sum;
476 self.buffer[self.pos] = value;
477 self.pos = (self.pos + 1) % self.period;
478 self.weighted_sum = self.weighted_sum - old_sum + self.period as f64 * value;
479 self.sum = old_sum - old + value;
480 Some(self.weighted_sum / self.divisor)
481 }
482 }
483}
484
485#[derive(Clone, Debug)]
486struct PivotDetector {
487 period: usize,
488 values: VecDeque<(f64, usize)>,
489 is_high: bool,
490}
491
492impl PivotDetector {
493 #[inline(always)]
494 fn new(period: usize, is_high: bool) -> Self {
495 Self {
496 period,
497 values: VecDeque::with_capacity(period * 2 + 1),
498 is_high,
499 }
500 }
501
502 #[inline(always)]
503 fn reset(&mut self) {
504 self.values.clear();
505 }
506
507 #[inline(always)]
508 fn update(&mut self, value: f64, index: usize) -> Option<(f64, usize)> {
509 self.values.push_back((value, index));
510 let needed = self.period * 2 + 1;
511 if self.values.len() < needed {
512 return None;
513 }
514 let (center_value, center_index) = self.values[self.period];
515 let mut ok = center_value.is_finite();
516 if ok {
517 for (i, (other, _)) in self.values.iter().enumerate() {
518 if i == self.period {
519 continue;
520 }
521 if !other.is_finite() {
522 ok = false;
523 break;
524 }
525 if self.is_high {
526 if *other > center_value {
527 ok = false;
528 break;
529 }
530 } else if *other < center_value {
531 ok = false;
532 break;
533 }
534 }
535 }
536 self.values.pop_front();
537 if ok {
538 Some((center_value, center_index))
539 } else {
540 None
541 }
542 }
543}
544
545#[derive(Clone, Debug)]
546struct MarketStructureConfluenceCore {
547 params: ResolvedParams,
548 basis_state: WmaState,
549 atr_state: AtrState,
550 svol_state: RollingSma,
551 piv_high: PivotDetector,
552 piv_low: PivotDetector,
553 index: usize,
554 prev_high: Option<f64>,
555 prev_low: Option<f64>,
556 prev_high_idx: Option<usize>,
557 prev_low_idx: Option<usize>,
558 high_active: bool,
559 low_active: bool,
560 prev_break_dir: i32,
561}
562
563impl MarketStructureConfluenceCore {
564 #[inline(always)]
565 fn new(params: ResolvedParams) -> Self {
566 Self {
567 basis_state: WmaState::new(params.basis_length),
568 atr_state: AtrState::new(params.atr_length),
569 svol_state: RollingSma::new(params.atr_smooth),
570 piv_high: PivotDetector::new(params.swing_size, true),
571 piv_low: PivotDetector::new(params.swing_size, false),
572 params,
573 index: 0,
574 prev_high: None,
575 prev_low: None,
576 prev_high_idx: None,
577 prev_low_idx: None,
578 high_active: false,
579 low_active: false,
580 prev_break_dir: 0,
581 }
582 }
583
584 #[inline(always)]
585 fn reset(&mut self) {
586 self.basis_state.reset();
587 self.atr_state.reset();
588 self.svol_state.reset();
589 self.piv_high.reset();
590 self.piv_low.reset();
591 self.index = 0;
592 self.prev_high = None;
593 self.prev_low = None;
594 self.prev_high_idx = None;
595 self.prev_low_idx = None;
596 self.high_active = false;
597 self.low_active = false;
598 self.prev_break_dir = 0;
599 }
600
601 #[inline(always)]
602 fn update(
603 &mut self,
604 high: f64,
605 low: f64,
606 close: f64,
607 ) -> Option<MarketStructureConfluencePoint> {
608 let basis = self.basis_state.update(close);
609 let svol = self
610 .atr_state
611 .update(high, low, close)
612 .and_then(|atr| self.svol_state.update(atr));
613
614 let mut hh = 0.0;
615 let mut lh = 0.0;
616 let mut hl = 0.0;
617 let mut ll = 0.0;
618
619 if let Some((pivot_high, pivot_idx)) = self.piv_high.update(high, self.index) {
620 let is_hh = self
621 .prev_high
622 .map(|value| pivot_high >= value)
623 .unwrap_or(true);
624 if is_hh {
625 hh = 1.0;
626 } else {
627 lh = 1.0;
628 }
629 self.prev_high = Some(pivot_high);
630 self.prev_high_idx = Some(pivot_idx);
631 self.high_active = true;
632 }
633
634 if let Some((pivot_low, pivot_idx)) = self.piv_low.update(low, self.index) {
635 let is_hl = self
636 .prev_low
637 .map(|value| pivot_low >= value)
638 .unwrap_or(true);
639 if is_hl {
640 hl = 1.0;
641 } else {
642 ll = 1.0;
643 }
644 self.prev_low = Some(pivot_low);
645 self.prev_low_idx = Some(pivot_idx);
646 self.low_active = true;
647 }
648
649 let high_src = match self.params.bos_confirmation {
650 MarketStructureConfluenceBosConfirmation::CandleClose => close,
651 MarketStructureConfluenceBosConfirmation::Wicks => high,
652 };
653 let low_src = match self.params.bos_confirmation {
654 MarketStructureConfluenceBosConfirmation::CandleClose => close,
655 MarketStructureConfluenceBosConfirmation::Wicks => low,
656 };
657
658 let mut high_broken = false;
659 let mut low_broken = false;
660 if self.high_active {
661 if let Some(prev_high) = self.prev_high {
662 if high_src > prev_high {
663 high_broken = true;
664 self.high_active = false;
665 }
666 }
667 }
668 if self.low_active {
669 if let Some(prev_low) = self.prev_low {
670 if low_src < prev_low {
671 low_broken = true;
672 self.low_active = false;
673 }
674 }
675 }
676
677 let mut bullish_change = 0.0;
678 let mut bearish_change = 0.0;
679 let mut bullish_bos = 0.0;
680 let mut bullish_choch = 0.0;
681 let mut bearish_bos = 0.0;
682 let mut bearish_choch = 0.0;
683
684 if high_broken {
685 let last_break_dir = self.prev_break_dir;
686 if last_break_dir == -1 {
687 bullish_choch = 1.0;
688 } else {
689 bullish_bos = 1.0;
690 }
691 if last_break_dir == -1 || last_break_dir == 0 {
692 bullish_change = 1.0;
693 }
694 self.prev_break_dir = 1;
695 }
696
697 if low_broken {
698 let last_break_dir = self.prev_break_dir;
699 if last_break_dir == 1 {
700 bearish_choch = 1.0;
701 } else {
702 bearish_bos = 1.0;
703 }
704 if last_break_dir == 1 || last_break_dir == 0 {
705 bearish_change = 1.0;
706 }
707 self.prev_break_dir = -1;
708 }
709
710 self.index += 1;
711
712 let (basis, svol) = match (basis, svol) {
713 (Some(basis), Some(svol)) => (basis, svol),
714 _ => return None,
715 };
716
717 let upper_band = basis + self.params.vol_mult * svol;
718 let lower_band = basis - self.params.vol_mult * svol;
719 let structure_direction = self.prev_break_dir as f64;
720 let bullish_arrow = if self.prev_break_dir == 1 && low < lower_band && high > lower_band {
721 1.0
722 } else {
723 0.0
724 };
725 let bearish_arrow = if self.prev_break_dir == -1 && low < upper_band && high > upper_band {
726 1.0
727 } else {
728 0.0
729 };
730
731 Some(MarketStructureConfluencePoint {
732 basis,
733 upper_band,
734 lower_band,
735 structure_direction,
736 bullish_arrow,
737 bearish_arrow,
738 bullish_change,
739 bearish_change,
740 hh,
741 lh,
742 hl,
743 ll,
744 bullish_bos,
745 bullish_choch,
746 bearish_bos,
747 bearish_choch,
748 })
749 }
750}
751
752#[derive(Clone, Debug)]
753pub struct MarketStructureConfluenceStream {
754 core: MarketStructureConfluenceCore,
755}
756
757impl MarketStructureConfluenceStream {
758 #[inline]
759 pub fn try_new(
760 params: MarketStructureConfluenceParams,
761 ) -> Result<Self, MarketStructureConfluenceError> {
762 let resolved = resolve_params(params, usize::MAX)?;
763 Ok(Self {
764 core: MarketStructureConfluenceCore::new(resolved),
765 })
766 }
767
768 #[inline(always)]
769 pub fn update(
770 &mut self,
771 high: f64,
772 low: f64,
773 close: f64,
774 ) -> Option<MarketStructureConfluenceStreamOutput> {
775 self.core
776 .update(high, low, close)
777 .map(|point| MarketStructureConfluenceStreamOutput {
778 basis: point.basis,
779 upper_band: point.upper_band,
780 lower_band: point.lower_band,
781 structure_direction: point.structure_direction,
782 bullish_arrow: point.bullish_arrow,
783 bearish_arrow: point.bearish_arrow,
784 bullish_change: point.bullish_change,
785 bearish_change: point.bearish_change,
786 hh: point.hh,
787 lh: point.lh,
788 hl: point.hl,
789 ll: point.ll,
790 bullish_bos: point.bullish_bos,
791 bullish_choch: point.bullish_choch,
792 bearish_bos: point.bearish_bos,
793 bearish_choch: point.bearish_choch,
794 })
795 }
796}
797
798#[inline]
799pub fn market_structure_confluence(
800 input: &MarketStructureConfluenceInput<'_>,
801) -> Result<MarketStructureConfluenceOutput, MarketStructureConfluenceError> {
802 market_structure_confluence_with_kernel(input, Kernel::Auto)
803}
804
805#[inline]
806pub fn market_structure_confluence_with_kernel(
807 input: &MarketStructureConfluenceInput<'_>,
808 kernel: Kernel,
809) -> Result<MarketStructureConfluenceOutput, MarketStructureConfluenceError> {
810 let prepared = prepare_input(input, kernel)?;
811 let mut basis = alloc_with_nan_prefix(prepared.len, prepared.warmup);
812 let mut upper_band = alloc_with_nan_prefix(prepared.len, prepared.warmup);
813 let mut lower_band = alloc_with_nan_prefix(prepared.len, prepared.warmup);
814 let mut structure_direction = alloc_with_nan_prefix(prepared.len, prepared.warmup);
815 let mut bullish_arrow = alloc_with_nan_prefix(prepared.len, prepared.warmup);
816 let mut bearish_arrow = alloc_with_nan_prefix(prepared.len, prepared.warmup);
817 let mut bullish_change = alloc_with_nan_prefix(prepared.len, prepared.warmup);
818 let mut bearish_change = alloc_with_nan_prefix(prepared.len, prepared.warmup);
819 let mut hh = alloc_with_nan_prefix(prepared.len, prepared.warmup);
820 let mut lh = alloc_with_nan_prefix(prepared.len, prepared.warmup);
821 let mut hl = alloc_with_nan_prefix(prepared.len, prepared.warmup);
822 let mut ll = alloc_with_nan_prefix(prepared.len, prepared.warmup);
823 let mut bullish_bos = alloc_with_nan_prefix(prepared.len, prepared.warmup);
824 let mut bullish_choch = alloc_with_nan_prefix(prepared.len, prepared.warmup);
825 let mut bearish_bos = alloc_with_nan_prefix(prepared.len, prepared.warmup);
826 let mut bearish_choch = alloc_with_nan_prefix(prepared.len, prepared.warmup);
827
828 market_structure_confluence_into_slices(
829 input,
830 kernel,
831 &mut basis,
832 &mut upper_band,
833 &mut lower_band,
834 &mut structure_direction,
835 &mut bullish_arrow,
836 &mut bearish_arrow,
837 &mut bullish_change,
838 &mut bearish_change,
839 &mut hh,
840 &mut lh,
841 &mut hl,
842 &mut ll,
843 &mut bullish_bos,
844 &mut bullish_choch,
845 &mut bearish_bos,
846 &mut bearish_choch,
847 )?;
848
849 Ok(MarketStructureConfluenceOutput {
850 basis,
851 upper_band,
852 lower_band,
853 structure_direction,
854 bullish_arrow,
855 bearish_arrow,
856 bullish_change,
857 bearish_change,
858 hh,
859 lh,
860 hl,
861 ll,
862 bullish_bos,
863 bullish_choch,
864 bearish_bos,
865 bearish_choch,
866 })
867}
868
869#[allow(clippy::too_many_arguments)]
870#[inline]
871pub fn market_structure_confluence_into(
872 input: &MarketStructureConfluenceInput<'_>,
873 basis: &mut [f64],
874 upper_band: &mut [f64],
875 lower_band: &mut [f64],
876 structure_direction: &mut [f64],
877 bullish_arrow: &mut [f64],
878 bearish_arrow: &mut [f64],
879 bullish_change: &mut [f64],
880 bearish_change: &mut [f64],
881 hh: &mut [f64],
882 lh: &mut [f64],
883 hl: &mut [f64],
884 ll: &mut [f64],
885 bullish_bos: &mut [f64],
886 bullish_choch: &mut [f64],
887 bearish_bos: &mut [f64],
888 bearish_choch: &mut [f64],
889) -> Result<(), MarketStructureConfluenceError> {
890 market_structure_confluence_into_slices(
891 input,
892 Kernel::Auto,
893 basis,
894 upper_band,
895 lower_band,
896 structure_direction,
897 bullish_arrow,
898 bearish_arrow,
899 bullish_change,
900 bearish_change,
901 hh,
902 lh,
903 hl,
904 ll,
905 bullish_bos,
906 bullish_choch,
907 bearish_bos,
908 bearish_choch,
909 )
910}
911
912#[allow(clippy::too_many_arguments)]
913#[inline]
914pub fn market_structure_confluence_into_slices(
915 input: &MarketStructureConfluenceInput<'_>,
916 kernel: Kernel,
917 basis: &mut [f64],
918 upper_band: &mut [f64],
919 lower_band: &mut [f64],
920 structure_direction: &mut [f64],
921 bullish_arrow: &mut [f64],
922 bearish_arrow: &mut [f64],
923 bullish_change: &mut [f64],
924 bearish_change: &mut [f64],
925 hh: &mut [f64],
926 lh: &mut [f64],
927 hl: &mut [f64],
928 ll: &mut [f64],
929 bullish_bos: &mut [f64],
930 bullish_choch: &mut [f64],
931 bearish_bos: &mut [f64],
932 bearish_choch: &mut [f64],
933) -> Result<(), MarketStructureConfluenceError> {
934 let prepared = prepare_input(input, kernel)?;
935 let got = *[
936 basis.len(),
937 upper_band.len(),
938 lower_band.len(),
939 structure_direction.len(),
940 bullish_arrow.len(),
941 bearish_arrow.len(),
942 bullish_change.len(),
943 bearish_change.len(),
944 hh.len(),
945 lh.len(),
946 hl.len(),
947 ll.len(),
948 bullish_bos.len(),
949 bullish_choch.len(),
950 bearish_bos.len(),
951 bearish_choch.len(),
952 ]
953 .iter()
954 .min()
955 .unwrap_or(&0);
956 if basis.len() != prepared.len
957 || upper_band.len() != prepared.len
958 || lower_band.len() != prepared.len
959 || structure_direction.len() != prepared.len
960 || bullish_arrow.len() != prepared.len
961 || bearish_arrow.len() != prepared.len
962 || bullish_change.len() != prepared.len
963 || bearish_change.len() != prepared.len
964 || hh.len() != prepared.len
965 || lh.len() != prepared.len
966 || hl.len() != prepared.len
967 || ll.len() != prepared.len
968 || bullish_bos.len() != prepared.len
969 || bullish_choch.len() != prepared.len
970 || bearish_bos.len() != prepared.len
971 || bearish_choch.len() != prepared.len
972 {
973 return Err(MarketStructureConfluenceError::OutputLengthMismatch {
974 expected: prepared.len,
975 got,
976 });
977 }
978
979 compute_into_slices(
980 &prepared,
981 basis,
982 upper_band,
983 lower_band,
984 structure_direction,
985 bullish_arrow,
986 bearish_arrow,
987 bullish_change,
988 bearish_change,
989 hh,
990 lh,
991 hl,
992 ll,
993 bullish_bos,
994 bullish_choch,
995 bearish_bos,
996 bearish_choch,
997 )
998}
999
1000#[inline]
1001fn resolve_data<'a>(
1002 input: &'a MarketStructureConfluenceInput<'a>,
1003) -> Result<(&'a [f64], &'a [f64], &'a [f64]), MarketStructureConfluenceError> {
1004 match &input.data {
1005 MarketStructureConfluenceData::Candles { candles } => Ok((
1006 candles.high.as_slice(),
1007 candles.low.as_slice(),
1008 candles.close.as_slice(),
1009 )),
1010 MarketStructureConfluenceData::Slices { high, low, close } => {
1011 if high.len() != low.len() || high.len() != close.len() {
1012 return Err(MarketStructureConfluenceError::DataLengthMismatch {
1013 high: high.len(),
1014 low: low.len(),
1015 close: close.len(),
1016 });
1017 }
1018 Ok((high, low, close))
1019 }
1020 }
1021}
1022
1023#[inline]
1024fn resolve_params(
1025 params: MarketStructureConfluenceParams,
1026 data_len: usize,
1027) -> Result<ResolvedParams, MarketStructureConfluenceError> {
1028 let swing_size = params.swing_size.unwrap_or(DEFAULT_SWING_SIZE);
1029 let bos_confirmation_raw = params
1030 .bos_confirmation
1031 .unwrap_or_else(|| DEFAULT_BOS_CONFIRMATION.to_string());
1032 let bos_confirmation = MarketStructureConfluenceBosConfirmation::parse(&bos_confirmation_raw)
1033 .ok_or(MarketStructureConfluenceError::InvalidBosConfirmation {
1034 bos_confirmation: bos_confirmation_raw.clone(),
1035 })?;
1036 let basis_length = params.basis_length.unwrap_or(DEFAULT_BASIS_LENGTH);
1037 let atr_length = params.atr_length.unwrap_or(DEFAULT_ATR_LENGTH);
1038 let atr_smooth = params.atr_smooth.unwrap_or(DEFAULT_ATR_SMOOTH);
1039 let vol_mult = params.vol_mult.unwrap_or(DEFAULT_VOL_MULT);
1040
1041 if swing_size < 2 || (data_len != usize::MAX && swing_size * 2 + 1 > data_len) {
1042 return Err(MarketStructureConfluenceError::InvalidSwingSize {
1043 swing_size,
1044 data_len,
1045 });
1046 }
1047 if basis_length == 0 || (data_len != usize::MAX && basis_length > data_len) {
1048 return Err(MarketStructureConfluenceError::InvalidBasisLength {
1049 basis_length,
1050 data_len,
1051 });
1052 }
1053 if atr_length == 0 || (data_len != usize::MAX && atr_length > data_len) {
1054 return Err(MarketStructureConfluenceError::InvalidAtrLength {
1055 atr_length,
1056 data_len,
1057 });
1058 }
1059 if atr_smooth == 0 || (data_len != usize::MAX && atr_smooth > data_len) {
1060 return Err(MarketStructureConfluenceError::InvalidAtrSmooth {
1061 atr_smooth,
1062 data_len,
1063 });
1064 }
1065 if !vol_mult.is_finite() || vol_mult < 0.0 {
1066 return Err(MarketStructureConfluenceError::InvalidVolMult { vol_mult });
1067 }
1068
1069 Ok(ResolvedParams {
1070 swing_size,
1071 bos_confirmation,
1072 basis_length,
1073 atr_length,
1074 atr_smooth,
1075 vol_mult,
1076 })
1077}
1078
1079#[inline]
1080fn prepare_input<'a>(
1081 input: &'a MarketStructureConfluenceInput<'a>,
1082 kernel: Kernel,
1083) -> Result<PreparedInput<'a>, MarketStructureConfluenceError> {
1084 let (high, low, close) = resolve_data(input)?;
1085 let len = close.len();
1086 if len == 0 {
1087 return Err(MarketStructureConfluenceError::EmptyInputData);
1088 }
1089 let first = (0..len)
1090 .find(|&i| high[i].is_finite() && low[i].is_finite() && close[i].is_finite())
1091 .ok_or(MarketStructureConfluenceError::AllValuesNaN)?;
1092 let params = resolve_params(input.params.clone(), len)?;
1093 let valid = (first..len)
1094 .filter(|&i| high[i].is_finite() && low[i].is_finite() && close[i].is_finite())
1095 .count();
1096 let needed = (params.swing_size * 2 + 1)
1097 .max(params.basis_length)
1098 .max(params.atr_length + params.atr_smooth - 1);
1099 if valid < needed {
1100 return Err(MarketStructureConfluenceError::NotEnoughValidData { needed, valid });
1101 }
1102 let _chosen = match kernel {
1103 Kernel::Auto => detect_best_kernel(),
1104 value => value,
1105 };
1106 Ok(PreparedInput {
1107 high,
1108 low,
1109 close,
1110 len,
1111 params,
1112 warmup: first
1113 + (params.swing_size * 2)
1114 .max(params.basis_length.saturating_sub(1))
1115 .max(params.atr_length + params.atr_smooth - 2),
1116 })
1117}
1118
1119#[allow(clippy::too_many_arguments)]
1120#[inline(always)]
1121fn compute_into_slices(
1122 prepared: &PreparedInput<'_>,
1123 dst_basis: &mut [f64],
1124 dst_upper_band: &mut [f64],
1125 dst_lower_band: &mut [f64],
1126 dst_structure_direction: &mut [f64],
1127 dst_bullish_arrow: &mut [f64],
1128 dst_bearish_arrow: &mut [f64],
1129 dst_bullish_change: &mut [f64],
1130 dst_bearish_change: &mut [f64],
1131 dst_hh: &mut [f64],
1132 dst_lh: &mut [f64],
1133 dst_hl: &mut [f64],
1134 dst_ll: &mut [f64],
1135 dst_bullish_bos: &mut [f64],
1136 dst_bullish_choch: &mut [f64],
1137 dst_bearish_bos: &mut [f64],
1138 dst_bearish_choch: &mut [f64],
1139) -> Result<(), MarketStructureConfluenceError> {
1140 dst_basis.fill(f64::NAN);
1141 dst_upper_band.fill(f64::NAN);
1142 dst_lower_band.fill(f64::NAN);
1143 dst_structure_direction.fill(f64::NAN);
1144 dst_bullish_arrow.fill(f64::NAN);
1145 dst_bearish_arrow.fill(f64::NAN);
1146 dst_bullish_change.fill(f64::NAN);
1147 dst_bearish_change.fill(f64::NAN);
1148 dst_hh.fill(f64::NAN);
1149 dst_lh.fill(f64::NAN);
1150 dst_hl.fill(f64::NAN);
1151 dst_ll.fill(f64::NAN);
1152 dst_bullish_bos.fill(f64::NAN);
1153 dst_bullish_choch.fill(f64::NAN);
1154 dst_bearish_bos.fill(f64::NAN);
1155 dst_bearish_choch.fill(f64::NAN);
1156
1157 let mut core = MarketStructureConfluenceCore::new(prepared.params);
1158 core.reset();
1159 for i in 0..prepared.len {
1160 let Some(point) = core.update(prepared.high[i], prepared.low[i], prepared.close[i]) else {
1161 continue;
1162 };
1163 dst_basis[i] = point.basis;
1164 dst_upper_band[i] = point.upper_band;
1165 dst_lower_band[i] = point.lower_band;
1166 dst_structure_direction[i] = point.structure_direction;
1167 dst_bullish_arrow[i] = point.bullish_arrow;
1168 dst_bearish_arrow[i] = point.bearish_arrow;
1169 dst_bullish_change[i] = point.bullish_change;
1170 dst_bearish_change[i] = point.bearish_change;
1171 dst_hh[i] = point.hh;
1172 dst_lh[i] = point.lh;
1173 dst_hl[i] = point.hl;
1174 dst_ll[i] = point.ll;
1175 dst_bullish_bos[i] = point.bullish_bos;
1176 dst_bullish_choch[i] = point.bullish_choch;
1177 dst_bearish_bos[i] = point.bearish_bos;
1178 dst_bearish_choch[i] = point.bearish_choch;
1179 }
1180 Ok(())
1181}
1182
1183#[derive(Clone, Debug)]
1184pub struct MarketStructureConfluenceBatchRange {
1185 pub swing_size: (usize, usize, usize),
1186 pub bos_confirmation: Vec<String>,
1187 pub basis_length: (usize, usize, usize),
1188 pub atr_length: (usize, usize, usize),
1189 pub atr_smooth: (usize, usize, usize),
1190 pub vol_mult: (f64, f64, f64),
1191}
1192
1193impl Default for MarketStructureConfluenceBatchRange {
1194 fn default() -> Self {
1195 Self {
1196 swing_size: (DEFAULT_SWING_SIZE, DEFAULT_SWING_SIZE, 0),
1197 bos_confirmation: vec![DEFAULT_BOS_CONFIRMATION.to_string()],
1198 basis_length: (DEFAULT_BASIS_LENGTH, DEFAULT_BASIS_LENGTH, 0),
1199 atr_length: (DEFAULT_ATR_LENGTH, DEFAULT_ATR_LENGTH, 0),
1200 atr_smooth: (DEFAULT_ATR_SMOOTH, DEFAULT_ATR_SMOOTH, 0),
1201 vol_mult: (DEFAULT_VOL_MULT, DEFAULT_VOL_MULT, 0.0),
1202 }
1203 }
1204}
1205
1206#[derive(Clone, Debug)]
1207pub struct MarketStructureConfluenceBatchOutput {
1208 pub basis: Vec<f64>,
1209 pub upper_band: Vec<f64>,
1210 pub lower_band: Vec<f64>,
1211 pub structure_direction: Vec<f64>,
1212 pub bullish_arrow: Vec<f64>,
1213 pub bearish_arrow: Vec<f64>,
1214 pub bullish_change: Vec<f64>,
1215 pub bearish_change: Vec<f64>,
1216 pub hh: Vec<f64>,
1217 pub lh: Vec<f64>,
1218 pub hl: Vec<f64>,
1219 pub ll: Vec<f64>,
1220 pub bullish_bos: Vec<f64>,
1221 pub bullish_choch: Vec<f64>,
1222 pub bearish_bos: Vec<f64>,
1223 pub bearish_choch: Vec<f64>,
1224 pub combos: Vec<MarketStructureConfluenceParams>,
1225 pub rows: usize,
1226 pub cols: usize,
1227}
1228
1229#[derive(Clone, Debug)]
1230pub struct MarketStructureConfluenceBatchBuilder {
1231 range: MarketStructureConfluenceBatchRange,
1232 kernel: Kernel,
1233}
1234
1235impl Default for MarketStructureConfluenceBatchBuilder {
1236 fn default() -> Self {
1237 Self {
1238 range: MarketStructureConfluenceBatchRange::default(),
1239 kernel: Kernel::Auto,
1240 }
1241 }
1242}
1243
1244impl MarketStructureConfluenceBatchBuilder {
1245 #[inline(always)]
1246 pub fn new() -> Self {
1247 Self::default()
1248 }
1249
1250 #[inline(always)]
1251 pub fn range(mut self, value: MarketStructureConfluenceBatchRange) -> Self {
1252 self.range = value;
1253 self
1254 }
1255
1256 #[inline(always)]
1257 pub fn kernel(mut self, value: Kernel) -> Self {
1258 self.kernel = value;
1259 self
1260 }
1261
1262 #[inline(always)]
1263 pub fn apply(
1264 self,
1265 candles: &Candles,
1266 ) -> Result<MarketStructureConfluenceBatchOutput, MarketStructureConfluenceError> {
1267 self.apply_slices(
1268 candles.high.as_slice(),
1269 candles.low.as_slice(),
1270 candles.close.as_slice(),
1271 )
1272 }
1273
1274 #[inline(always)]
1275 pub fn apply_slices(
1276 self,
1277 high: &[f64],
1278 low: &[f64],
1279 close: &[f64],
1280 ) -> Result<MarketStructureConfluenceBatchOutput, MarketStructureConfluenceError> {
1281 market_structure_confluence_batch_with_kernel(high, low, close, &self.range, self.kernel)
1282 }
1283}
1284
1285fn axis_usize(
1286 (start, end, step): (usize, usize, usize),
1287) -> Result<Vec<usize>, MarketStructureConfluenceError> {
1288 if step == 0 || start == end {
1289 return Ok(vec![start]);
1290 }
1291 let mut out = Vec::new();
1292 if start <= end {
1293 let mut current = start;
1294 while current <= end {
1295 out.push(current);
1296 match current.checked_add(step) {
1297 Some(next) => current = next,
1298 None => break,
1299 }
1300 }
1301 } else {
1302 let mut current = start;
1303 while current >= end {
1304 out.push(current);
1305 match current.checked_sub(step) {
1306 Some(next) => current = next,
1307 None => break,
1308 }
1309 if current < end {
1310 break;
1311 }
1312 }
1313 }
1314 if out.is_empty() {
1315 return Err(MarketStructureConfluenceError::InvalidRange {
1316 start: start.to_string(),
1317 end: end.to_string(),
1318 step: step.to_string(),
1319 });
1320 }
1321 Ok(out)
1322}
1323
1324fn axis_f64(
1325 (start, end, step): (f64, f64, f64),
1326) -> Result<Vec<f64>, MarketStructureConfluenceError> {
1327 if !start.is_finite() || !end.is_finite() || !step.is_finite() {
1328 return Err(MarketStructureConfluenceError::InvalidRange {
1329 start: start.to_string(),
1330 end: end.to_string(),
1331 step: step.to_string(),
1332 });
1333 }
1334 if step.abs() < EPS || (start - end).abs() < EPS {
1335 return Ok(vec![start]);
1336 }
1337 let dir = if end >= start { 1.0 } else { -1.0 };
1338 let step_eff = dir * step.abs();
1339 let mut current = start;
1340 let mut out = Vec::new();
1341 if dir > 0.0 {
1342 while current <= end + EPS {
1343 out.push(current);
1344 current += step_eff;
1345 }
1346 } else {
1347 while current >= end - EPS {
1348 out.push(current);
1349 current += step_eff;
1350 }
1351 }
1352 if out.is_empty() {
1353 return Err(MarketStructureConfluenceError::InvalidRange {
1354 start: start.to_string(),
1355 end: end.to_string(),
1356 step: step.to_string(),
1357 });
1358 }
1359 Ok(out)
1360}
1361
1362fn expand_grid(
1363 range: &MarketStructureConfluenceBatchRange,
1364) -> Result<Vec<MarketStructureConfluenceParams>, MarketStructureConfluenceError> {
1365 let swing_sizes = axis_usize(range.swing_size)?;
1366 let bos_confirmations = if range.bos_confirmation.is_empty() {
1367 vec![DEFAULT_BOS_CONFIRMATION.to_string()]
1368 } else {
1369 range.bos_confirmation.clone()
1370 };
1371 let basis_lengths = axis_usize(range.basis_length)?;
1372 let atr_lengths = axis_usize(range.atr_length)?;
1373 let atr_smooths = axis_usize(range.atr_smooth)?;
1374 let vol_mults = axis_f64(range.vol_mult)?;
1375
1376 let total = swing_sizes
1377 .len()
1378 .checked_mul(bos_confirmations.len())
1379 .and_then(|n| n.checked_mul(basis_lengths.len()))
1380 .and_then(|n| n.checked_mul(atr_lengths.len()))
1381 .and_then(|n| n.checked_mul(atr_smooths.len()))
1382 .and_then(|n| n.checked_mul(vol_mults.len()))
1383 .ok_or_else(|| MarketStructureConfluenceError::InvalidRange {
1384 start: range.swing_size.0.to_string(),
1385 end: range.swing_size.1.to_string(),
1386 step: range.swing_size.2.to_string(),
1387 })?;
1388
1389 let mut out = Vec::with_capacity(total);
1390 for &swing_size in &swing_sizes {
1391 for bos_confirmation in &bos_confirmations {
1392 for &basis_length in &basis_lengths {
1393 for &atr_length in &atr_lengths {
1394 for &atr_smooth in &atr_smooths {
1395 for &vol_mult in &vol_mults {
1396 out.push(MarketStructureConfluenceParams {
1397 swing_size: Some(swing_size),
1398 bos_confirmation: Some(bos_confirmation.clone()),
1399 basis_length: Some(basis_length),
1400 atr_length: Some(atr_length),
1401 atr_smooth: Some(atr_smooth),
1402 vol_mult: Some(vol_mult),
1403 });
1404 }
1405 }
1406 }
1407 }
1408 }
1409 }
1410 Ok(out)
1411}
1412
1413#[allow(clippy::too_many_arguments)]
1414#[inline]
1415pub fn market_structure_confluence_batch_with_kernel(
1416 high: &[f64],
1417 low: &[f64],
1418 close: &[f64],
1419 range: &MarketStructureConfluenceBatchRange,
1420 kernel: Kernel,
1421) -> Result<MarketStructureConfluenceBatchOutput, MarketStructureConfluenceError> {
1422 if high.is_empty() || low.is_empty() || close.is_empty() {
1423 return Err(MarketStructureConfluenceError::EmptyInputData);
1424 }
1425 if high.len() != low.len() || high.len() != close.len() {
1426 return Err(MarketStructureConfluenceError::DataLengthMismatch {
1427 high: high.len(),
1428 low: low.len(),
1429 close: close.len(),
1430 });
1431 }
1432
1433 let batch_kernel = match kernel {
1434 Kernel::Auto => detect_best_batch_kernel(),
1435 value if value.is_batch() => value,
1436 _ => {
1437 return Err(MarketStructureConfluenceError::InvalidKernelForBatch(
1438 kernel,
1439 ))
1440 }
1441 };
1442 let single_kernel = batch_kernel.to_non_batch();
1443 let combos = expand_grid(range)?;
1444 let rows = combos.len();
1445 let cols = close.len();
1446 let first = (0..cols)
1447 .find(|&i| high[i].is_finite() && low[i].is_finite() && close[i].is_finite())
1448 .ok_or(MarketStructureConfluenceError::AllValuesNaN)?;
1449 let warmups: Vec<usize> = combos
1450 .iter()
1451 .map(|combo| {
1452 let swing_size = combo.swing_size.unwrap_or(DEFAULT_SWING_SIZE);
1453 let basis_length = combo.basis_length.unwrap_or(DEFAULT_BASIS_LENGTH);
1454 let atr_length = combo.atr_length.unwrap_or(DEFAULT_ATR_LENGTH);
1455 let atr_smooth = combo.atr_smooth.unwrap_or(DEFAULT_ATR_SMOOTH);
1456 first
1457 + (swing_size * 2)
1458 .max(basis_length.saturating_sub(1))
1459 .max(atr_length + atr_smooth - 2)
1460 })
1461 .collect();
1462
1463 let mut basis_mu = make_uninit_matrix(rows, cols);
1464 let mut upper_band_mu = make_uninit_matrix(rows, cols);
1465 let mut lower_band_mu = make_uninit_matrix(rows, cols);
1466 let mut structure_direction_mu = make_uninit_matrix(rows, cols);
1467 let mut bullish_arrow_mu = make_uninit_matrix(rows, cols);
1468 let mut bearish_arrow_mu = make_uninit_matrix(rows, cols);
1469 let mut bullish_change_mu = make_uninit_matrix(rows, cols);
1470 let mut bearish_change_mu = make_uninit_matrix(rows, cols);
1471 let mut hh_mu = make_uninit_matrix(rows, cols);
1472 let mut lh_mu = make_uninit_matrix(rows, cols);
1473 let mut hl_mu = make_uninit_matrix(rows, cols);
1474 let mut ll_mu = make_uninit_matrix(rows, cols);
1475 let mut bullish_bos_mu = make_uninit_matrix(rows, cols);
1476 let mut bullish_choch_mu = make_uninit_matrix(rows, cols);
1477 let mut bearish_bos_mu = make_uninit_matrix(rows, cols);
1478 let mut bearish_choch_mu = make_uninit_matrix(rows, cols);
1479
1480 init_matrix_prefixes(&mut basis_mu, cols, &warmups);
1481 init_matrix_prefixes(&mut upper_band_mu, cols, &warmups);
1482 init_matrix_prefixes(&mut lower_band_mu, cols, &warmups);
1483 init_matrix_prefixes(&mut structure_direction_mu, cols, &warmups);
1484 init_matrix_prefixes(&mut bullish_arrow_mu, cols, &warmups);
1485 init_matrix_prefixes(&mut bearish_arrow_mu, cols, &warmups);
1486 init_matrix_prefixes(&mut bullish_change_mu, cols, &warmups);
1487 init_matrix_prefixes(&mut bearish_change_mu, cols, &warmups);
1488 init_matrix_prefixes(&mut hh_mu, cols, &warmups);
1489 init_matrix_prefixes(&mut lh_mu, cols, &warmups);
1490 init_matrix_prefixes(&mut hl_mu, cols, &warmups);
1491 init_matrix_prefixes(&mut ll_mu, cols, &warmups);
1492 init_matrix_prefixes(&mut bullish_bos_mu, cols, &warmups);
1493 init_matrix_prefixes(&mut bullish_choch_mu, cols, &warmups);
1494 init_matrix_prefixes(&mut bearish_bos_mu, cols, &warmups);
1495 init_matrix_prefixes(&mut bearish_choch_mu, cols, &warmups);
1496
1497 let mut basis_guard = ManuallyDrop::new(basis_mu);
1498 let mut upper_band_guard = ManuallyDrop::new(upper_band_mu);
1499 let mut lower_band_guard = ManuallyDrop::new(lower_band_mu);
1500 let mut structure_direction_guard = ManuallyDrop::new(structure_direction_mu);
1501 let mut bullish_arrow_guard = ManuallyDrop::new(bullish_arrow_mu);
1502 let mut bearish_arrow_guard = ManuallyDrop::new(bearish_arrow_mu);
1503 let mut bullish_change_guard = ManuallyDrop::new(bullish_change_mu);
1504 let mut bearish_change_guard = ManuallyDrop::new(bearish_change_mu);
1505 let mut hh_guard = ManuallyDrop::new(hh_mu);
1506 let mut lh_guard = ManuallyDrop::new(lh_mu);
1507 let mut hl_guard = ManuallyDrop::new(hl_mu);
1508 let mut ll_guard = ManuallyDrop::new(ll_mu);
1509 let mut bullish_bos_guard = ManuallyDrop::new(bullish_bos_mu);
1510 let mut bullish_choch_guard = ManuallyDrop::new(bullish_choch_mu);
1511 let mut bearish_bos_guard = ManuallyDrop::new(bearish_bos_mu);
1512 let mut bearish_choch_guard = ManuallyDrop::new(bearish_choch_mu);
1513
1514 let basis_all = unsafe { mu_slice_as_f64_slice_mut(&mut basis_guard) };
1515 let upper_band_all = unsafe { mu_slice_as_f64_slice_mut(&mut upper_band_guard) };
1516 let lower_band_all = unsafe { mu_slice_as_f64_slice_mut(&mut lower_band_guard) };
1517 let structure_direction_all =
1518 unsafe { mu_slice_as_f64_slice_mut(&mut structure_direction_guard) };
1519 let bullish_arrow_all = unsafe { mu_slice_as_f64_slice_mut(&mut bullish_arrow_guard) };
1520 let bearish_arrow_all = unsafe { mu_slice_as_f64_slice_mut(&mut bearish_arrow_guard) };
1521 let bullish_change_all = unsafe { mu_slice_as_f64_slice_mut(&mut bullish_change_guard) };
1522 let bearish_change_all = unsafe { mu_slice_as_f64_slice_mut(&mut bearish_change_guard) };
1523 let hh_all = unsafe { mu_slice_as_f64_slice_mut(&mut hh_guard) };
1524 let lh_all = unsafe { mu_slice_as_f64_slice_mut(&mut lh_guard) };
1525 let hl_all = unsafe { mu_slice_as_f64_slice_mut(&mut hl_guard) };
1526 let ll_all = unsafe { mu_slice_as_f64_slice_mut(&mut ll_guard) };
1527 let bullish_bos_all = unsafe { mu_slice_as_f64_slice_mut(&mut bullish_bos_guard) };
1528 let bullish_choch_all = unsafe { mu_slice_as_f64_slice_mut(&mut bullish_choch_guard) };
1529 let bearish_bos_all = unsafe { mu_slice_as_f64_slice_mut(&mut bearish_bos_guard) };
1530 let bearish_choch_all = unsafe { mu_slice_as_f64_slice_mut(&mut bearish_choch_guard) };
1531
1532 let run_row = |row: usize,
1533 basis_row: &mut [f64],
1534 upper_band_row: &mut [f64],
1535 lower_band_row: &mut [f64],
1536 structure_direction_row: &mut [f64],
1537 bullish_arrow_row: &mut [f64],
1538 bearish_arrow_row: &mut [f64],
1539 bullish_change_row: &mut [f64],
1540 bearish_change_row: &mut [f64],
1541 hh_row: &mut [f64],
1542 lh_row: &mut [f64],
1543 hl_row: &mut [f64],
1544 ll_row: &mut [f64],
1545 bullish_bos_row: &mut [f64],
1546 bullish_choch_row: &mut [f64],
1547 bearish_bos_row: &mut [f64],
1548 bearish_choch_row: &mut [f64]|
1549 -> Result<(), MarketStructureConfluenceError> {
1550 let input =
1551 MarketStructureConfluenceInput::from_slices(high, low, close, combos[row].clone());
1552 market_structure_confluence_into_slices(
1553 &input,
1554 single_kernel,
1555 basis_row,
1556 upper_band_row,
1557 lower_band_row,
1558 structure_direction_row,
1559 bullish_arrow_row,
1560 bearish_arrow_row,
1561 bullish_change_row,
1562 bearish_change_row,
1563 hh_row,
1564 lh_row,
1565 hl_row,
1566 ll_row,
1567 bullish_bos_row,
1568 bullish_choch_row,
1569 bearish_bos_row,
1570 bearish_choch_row,
1571 )
1572 };
1573
1574 #[cfg(not(target_arch = "wasm32"))]
1575 {
1576 basis_all
1577 .par_chunks_mut(cols)
1578 .zip(upper_band_all.par_chunks_mut(cols))
1579 .zip(lower_band_all.par_chunks_mut(cols))
1580 .zip(structure_direction_all.par_chunks_mut(cols))
1581 .zip(bullish_arrow_all.par_chunks_mut(cols))
1582 .zip(bearish_arrow_all.par_chunks_mut(cols))
1583 .zip(bullish_change_all.par_chunks_mut(cols))
1584 .zip(bearish_change_all.par_chunks_mut(cols))
1585 .zip(hh_all.par_chunks_mut(cols))
1586 .zip(lh_all.par_chunks_mut(cols))
1587 .zip(hl_all.par_chunks_mut(cols))
1588 .zip(ll_all.par_chunks_mut(cols))
1589 .zip(bullish_bos_all.par_chunks_mut(cols))
1590 .zip(bullish_choch_all.par_chunks_mut(cols))
1591 .zip(bearish_bos_all.par_chunks_mut(cols))
1592 .zip(bearish_choch_all.par_chunks_mut(cols))
1593 .enumerate()
1594 .try_for_each(
1595 |(
1596 row,
1597 (
1598 (
1599 (
1600 (
1601 (
1602 (
1603 (
1604 (
1605 (
1606 (
1607 (
1608 (
1609 (
1610 (
1611 (
1612 basis_row,
1613 upper_band_row,
1614 ),
1615 lower_band_row,
1616 ),
1617 structure_direction_row,
1618 ),
1619 bullish_arrow_row,
1620 ),
1621 bearish_arrow_row,
1622 ),
1623 bullish_change_row,
1624 ),
1625 bearish_change_row,
1626 ),
1627 hh_row,
1628 ),
1629 lh_row,
1630 ),
1631 hl_row,
1632 ),
1633 ll_row,
1634 ),
1635 bullish_bos_row,
1636 ),
1637 bullish_choch_row,
1638 ),
1639 bearish_bos_row,
1640 ),
1641 bearish_choch_row,
1642 ),
1643 )| {
1644 run_row(
1645 row,
1646 basis_row,
1647 upper_band_row,
1648 lower_band_row,
1649 structure_direction_row,
1650 bullish_arrow_row,
1651 bearish_arrow_row,
1652 bullish_change_row,
1653 bearish_change_row,
1654 hh_row,
1655 lh_row,
1656 hl_row,
1657 ll_row,
1658 bullish_bos_row,
1659 bullish_choch_row,
1660 bearish_bos_row,
1661 bearish_choch_row,
1662 )
1663 },
1664 )?;
1665 }
1666
1667 #[cfg(target_arch = "wasm32")]
1668 {
1669 for row in 0..rows {
1670 let start = row * cols;
1671 let end = start + cols;
1672 run_row(
1673 row,
1674 &mut basis_all[start..end],
1675 &mut upper_band_all[start..end],
1676 &mut lower_band_all[start..end],
1677 &mut structure_direction_all[start..end],
1678 &mut bullish_arrow_all[start..end],
1679 &mut bearish_arrow_all[start..end],
1680 &mut bullish_change_all[start..end],
1681 &mut bearish_change_all[start..end],
1682 &mut hh_all[start..end],
1683 &mut lh_all[start..end],
1684 &mut hl_all[start..end],
1685 &mut ll_all[start..end],
1686 &mut bullish_bos_all[start..end],
1687 &mut bullish_choch_all[start..end],
1688 &mut bearish_bos_all[start..end],
1689 &mut bearish_choch_all[start..end],
1690 )?;
1691 }
1692 }
1693
1694 let basis = unsafe { assume_init_vec(basis_guard) };
1695 let upper_band = unsafe { assume_init_vec(upper_band_guard) };
1696 let lower_band = unsafe { assume_init_vec(lower_band_guard) };
1697 let structure_direction = unsafe { assume_init_vec(structure_direction_guard) };
1698 let bullish_arrow = unsafe { assume_init_vec(bullish_arrow_guard) };
1699 let bearish_arrow = unsafe { assume_init_vec(bearish_arrow_guard) };
1700 let bullish_change = unsafe { assume_init_vec(bullish_change_guard) };
1701 let bearish_change = unsafe { assume_init_vec(bearish_change_guard) };
1702 let hh = unsafe { assume_init_vec(hh_guard) };
1703 let lh = unsafe { assume_init_vec(lh_guard) };
1704 let hl = unsafe { assume_init_vec(hl_guard) };
1705 let ll = unsafe { assume_init_vec(ll_guard) };
1706 let bullish_bos = unsafe { assume_init_vec(bullish_bos_guard) };
1707 let bullish_choch = unsafe { assume_init_vec(bullish_choch_guard) };
1708 let bearish_bos = unsafe { assume_init_vec(bearish_bos_guard) };
1709 let bearish_choch = unsafe { assume_init_vec(bearish_choch_guard) };
1710
1711 Ok(MarketStructureConfluenceBatchOutput {
1712 basis,
1713 upper_band,
1714 lower_band,
1715 structure_direction,
1716 bullish_arrow,
1717 bearish_arrow,
1718 bullish_change,
1719 bearish_change,
1720 hh,
1721 lh,
1722 hl,
1723 ll,
1724 bullish_bos,
1725 bullish_choch,
1726 bearish_bos,
1727 bearish_choch,
1728 combos,
1729 rows,
1730 cols,
1731 })
1732}
1733
1734#[inline(always)]
1735unsafe fn mu_slice_as_f64_slice_mut(buf: &mut ManuallyDrop<Vec<MaybeUninit<f64>>>) -> &mut [f64] {
1736 std::slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut f64, buf.len())
1737}
1738
1739#[inline(always)]
1740unsafe fn assume_init_vec(buf: ManuallyDrop<Vec<MaybeUninit<f64>>>) -> Vec<f64> {
1741 let mut buf = buf;
1742 Vec::from_raw_parts(buf.as_mut_ptr() as *mut f64, buf.len(), buf.capacity())
1743}
1744
1745#[cfg(feature = "python")]
1746#[pyfunction(name = "market_structure_confluence")]
1747#[pyo3(signature = (high, low, close, swing_size=DEFAULT_SWING_SIZE, bos_confirmation=DEFAULT_BOS_CONFIRMATION, basis_length=DEFAULT_BASIS_LENGTH, atr_length=DEFAULT_ATR_LENGTH, atr_smooth=DEFAULT_ATR_SMOOTH, vol_mult=DEFAULT_VOL_MULT, kernel=None))]
1748pub fn market_structure_confluence_py<'py>(
1749 py: Python<'py>,
1750 high: PyReadonlyArray1<'py, f64>,
1751 low: PyReadonlyArray1<'py, f64>,
1752 close: PyReadonlyArray1<'py, f64>,
1753 swing_size: usize,
1754 bos_confirmation: &str,
1755 basis_length: usize,
1756 atr_length: usize,
1757 atr_smooth: usize,
1758 vol_mult: f64,
1759 kernel: Option<&str>,
1760) -> PyResult<Bound<'py, PyDict>> {
1761 let high = high.as_slice()?;
1762 let low = low.as_slice()?;
1763 let close = close.as_slice()?;
1764 let kernel = validate_kernel(kernel, false)?;
1765 let input = MarketStructureConfluenceInput::from_slices(
1766 high,
1767 low,
1768 close,
1769 MarketStructureConfluenceParams {
1770 swing_size: Some(swing_size),
1771 bos_confirmation: Some(bos_confirmation.to_string()),
1772 basis_length: Some(basis_length),
1773 atr_length: Some(atr_length),
1774 atr_smooth: Some(atr_smooth),
1775 vol_mult: Some(vol_mult),
1776 },
1777 );
1778 let output = py
1779 .allow_threads(|| market_structure_confluence_with_kernel(&input, kernel))
1780 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1781 let dict = PyDict::new(py);
1782 dict.set_item("basis", output.basis.into_pyarray(py))?;
1783 dict.set_item("upper_band", output.upper_band.into_pyarray(py))?;
1784 dict.set_item("lower_band", output.lower_band.into_pyarray(py))?;
1785 dict.set_item(
1786 "structure_direction",
1787 output.structure_direction.into_pyarray(py),
1788 )?;
1789 dict.set_item("bullish_arrow", output.bullish_arrow.into_pyarray(py))?;
1790 dict.set_item("bearish_arrow", output.bearish_arrow.into_pyarray(py))?;
1791 dict.set_item("bullish_change", output.bullish_change.into_pyarray(py))?;
1792 dict.set_item("bearish_change", output.bearish_change.into_pyarray(py))?;
1793 dict.set_item("hh", output.hh.into_pyarray(py))?;
1794 dict.set_item("lh", output.lh.into_pyarray(py))?;
1795 dict.set_item("hl", output.hl.into_pyarray(py))?;
1796 dict.set_item("ll", output.ll.into_pyarray(py))?;
1797 dict.set_item("bullish_bos", output.bullish_bos.into_pyarray(py))?;
1798 dict.set_item("bullish_choch", output.bullish_choch.into_pyarray(py))?;
1799 dict.set_item("bearish_bos", output.bearish_bos.into_pyarray(py))?;
1800 dict.set_item("bearish_choch", output.bearish_choch.into_pyarray(py))?;
1801 Ok(dict)
1802}
1803
1804#[cfg(feature = "python")]
1805#[pyfunction(name = "market_structure_confluence_batch")]
1806#[pyo3(signature = (high, low, close, swing_size_range=(DEFAULT_SWING_SIZE, DEFAULT_SWING_SIZE, 0), bos_confirmation_options=vec![DEFAULT_BOS_CONFIRMATION.to_string()], basis_length_range=(DEFAULT_BASIS_LENGTH, DEFAULT_BASIS_LENGTH, 0), atr_length_range=(DEFAULT_ATR_LENGTH, DEFAULT_ATR_LENGTH, 0), atr_smooth_range=(DEFAULT_ATR_SMOOTH, DEFAULT_ATR_SMOOTH, 0), vol_mult_range=(DEFAULT_VOL_MULT, DEFAULT_VOL_MULT, 0.0), kernel=None))]
1807pub fn market_structure_confluence_batch_py<'py>(
1808 py: Python<'py>,
1809 high: PyReadonlyArray1<'py, f64>,
1810 low: PyReadonlyArray1<'py, f64>,
1811 close: PyReadonlyArray1<'py, f64>,
1812 swing_size_range: (usize, usize, usize),
1813 bos_confirmation_options: Vec<String>,
1814 basis_length_range: (usize, usize, usize),
1815 atr_length_range: (usize, usize, usize),
1816 atr_smooth_range: (usize, usize, usize),
1817 vol_mult_range: (f64, f64, f64),
1818 kernel: Option<&str>,
1819) -> PyResult<Bound<'py, PyDict>> {
1820 let high = high.as_slice()?;
1821 let low = low.as_slice()?;
1822 let close = close.as_slice()?;
1823 let kernel = validate_kernel(kernel, true)?;
1824 let output = py
1825 .allow_threads(|| {
1826 market_structure_confluence_batch_with_kernel(
1827 high,
1828 low,
1829 close,
1830 &MarketStructureConfluenceBatchRange {
1831 swing_size: swing_size_range,
1832 bos_confirmation: bos_confirmation_options,
1833 basis_length: basis_length_range,
1834 atr_length: atr_length_range,
1835 atr_smooth: atr_smooth_range,
1836 vol_mult: vol_mult_range,
1837 },
1838 kernel,
1839 )
1840 })
1841 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1842
1843 let total = output.rows * output.cols;
1844 let arrays = [
1845 unsafe { PyArray1::<f64>::new(py, [total], false) },
1846 unsafe { PyArray1::<f64>::new(py, [total], false) },
1847 unsafe { PyArray1::<f64>::new(py, [total], false) },
1848 unsafe { PyArray1::<f64>::new(py, [total], false) },
1849 unsafe { PyArray1::<f64>::new(py, [total], false) },
1850 unsafe { PyArray1::<f64>::new(py, [total], false) },
1851 unsafe { PyArray1::<f64>::new(py, [total], false) },
1852 unsafe { PyArray1::<f64>::new(py, [total], false) },
1853 unsafe { PyArray1::<f64>::new(py, [total], false) },
1854 unsafe { PyArray1::<f64>::new(py, [total], false) },
1855 unsafe { PyArray1::<f64>::new(py, [total], false) },
1856 unsafe { PyArray1::<f64>::new(py, [total], false) },
1857 unsafe { PyArray1::<f64>::new(py, [total], false) },
1858 unsafe { PyArray1::<f64>::new(py, [total], false) },
1859 unsafe { PyArray1::<f64>::new(py, [total], false) },
1860 unsafe { PyArray1::<f64>::new(py, [total], false) },
1861 ];
1862 unsafe { arrays[0].as_slice_mut()? }.copy_from_slice(&output.basis);
1863 unsafe { arrays[1].as_slice_mut()? }.copy_from_slice(&output.upper_band);
1864 unsafe { arrays[2].as_slice_mut()? }.copy_from_slice(&output.lower_band);
1865 unsafe { arrays[3].as_slice_mut()? }.copy_from_slice(&output.structure_direction);
1866 unsafe { arrays[4].as_slice_mut()? }.copy_from_slice(&output.bullish_arrow);
1867 unsafe { arrays[5].as_slice_mut()? }.copy_from_slice(&output.bearish_arrow);
1868 unsafe { arrays[6].as_slice_mut()? }.copy_from_slice(&output.bullish_change);
1869 unsafe { arrays[7].as_slice_mut()? }.copy_from_slice(&output.bearish_change);
1870 unsafe { arrays[8].as_slice_mut()? }.copy_from_slice(&output.hh);
1871 unsafe { arrays[9].as_slice_mut()? }.copy_from_slice(&output.lh);
1872 unsafe { arrays[10].as_slice_mut()? }.copy_from_slice(&output.hl);
1873 unsafe { arrays[11].as_slice_mut()? }.copy_from_slice(&output.ll);
1874 unsafe { arrays[12].as_slice_mut()? }.copy_from_slice(&output.bullish_bos);
1875 unsafe { arrays[13].as_slice_mut()? }.copy_from_slice(&output.bullish_choch);
1876 unsafe { arrays[14].as_slice_mut()? }.copy_from_slice(&output.bearish_bos);
1877 unsafe { arrays[15].as_slice_mut()? }.copy_from_slice(&output.bearish_choch);
1878
1879 let dict = PyDict::new(py);
1880 dict.set_item("basis", arrays[0].reshape((output.rows, output.cols))?)?;
1881 dict.set_item("upper_band", arrays[1].reshape((output.rows, output.cols))?)?;
1882 dict.set_item("lower_band", arrays[2].reshape((output.rows, output.cols))?)?;
1883 dict.set_item(
1884 "structure_direction",
1885 arrays[3].reshape((output.rows, output.cols))?,
1886 )?;
1887 dict.set_item(
1888 "bullish_arrow",
1889 arrays[4].reshape((output.rows, output.cols))?,
1890 )?;
1891 dict.set_item(
1892 "bearish_arrow",
1893 arrays[5].reshape((output.rows, output.cols))?,
1894 )?;
1895 dict.set_item(
1896 "bullish_change",
1897 arrays[6].reshape((output.rows, output.cols))?,
1898 )?;
1899 dict.set_item(
1900 "bearish_change",
1901 arrays[7].reshape((output.rows, output.cols))?,
1902 )?;
1903 dict.set_item("hh", arrays[8].reshape((output.rows, output.cols))?)?;
1904 dict.set_item("lh", arrays[9].reshape((output.rows, output.cols))?)?;
1905 dict.set_item("hl", arrays[10].reshape((output.rows, output.cols))?)?;
1906 dict.set_item("ll", arrays[11].reshape((output.rows, output.cols))?)?;
1907 dict.set_item(
1908 "bullish_bos",
1909 arrays[12].reshape((output.rows, output.cols))?,
1910 )?;
1911 dict.set_item(
1912 "bullish_choch",
1913 arrays[13].reshape((output.rows, output.cols))?,
1914 )?;
1915 dict.set_item(
1916 "bearish_bos",
1917 arrays[14].reshape((output.rows, output.cols))?,
1918 )?;
1919 dict.set_item(
1920 "bearish_choch",
1921 arrays[15].reshape((output.rows, output.cols))?,
1922 )?;
1923 dict.set_item(
1924 "swing_sizes",
1925 output
1926 .combos
1927 .iter()
1928 .map(|combo| combo.swing_size.unwrap_or(DEFAULT_SWING_SIZE) as u64)
1929 .collect::<Vec<_>>()
1930 .into_pyarray(py),
1931 )?;
1932 dict.set_item(
1933 "bos_confirmations",
1934 output
1935 .combos
1936 .iter()
1937 .map(|combo| {
1938 combo
1939 .bos_confirmation
1940 .clone()
1941 .unwrap_or_else(|| DEFAULT_BOS_CONFIRMATION.to_string())
1942 })
1943 .collect::<Vec<_>>(),
1944 )?;
1945 dict.set_item(
1946 "basis_lengths",
1947 output
1948 .combos
1949 .iter()
1950 .map(|combo| combo.basis_length.unwrap_or(DEFAULT_BASIS_LENGTH) as u64)
1951 .collect::<Vec<_>>()
1952 .into_pyarray(py),
1953 )?;
1954 dict.set_item(
1955 "atr_lengths",
1956 output
1957 .combos
1958 .iter()
1959 .map(|combo| combo.atr_length.unwrap_or(DEFAULT_ATR_LENGTH) as u64)
1960 .collect::<Vec<_>>()
1961 .into_pyarray(py),
1962 )?;
1963 dict.set_item(
1964 "atr_smooths",
1965 output
1966 .combos
1967 .iter()
1968 .map(|combo| combo.atr_smooth.unwrap_or(DEFAULT_ATR_SMOOTH) as u64)
1969 .collect::<Vec<_>>()
1970 .into_pyarray(py),
1971 )?;
1972 dict.set_item(
1973 "vol_mults",
1974 output
1975 .combos
1976 .iter()
1977 .map(|combo| combo.vol_mult.unwrap_or(DEFAULT_VOL_MULT))
1978 .collect::<Vec<_>>()
1979 .into_pyarray(py),
1980 )?;
1981 dict.set_item("rows", output.rows)?;
1982 dict.set_item("cols", output.cols)?;
1983 Ok(dict)
1984}
1985
1986#[cfg(feature = "python")]
1987#[pyclass(name = "MarketStructureConfluenceStream")]
1988pub struct MarketStructureConfluenceStreamPy {
1989 stream: MarketStructureConfluenceStream,
1990}
1991
1992#[cfg(feature = "python")]
1993#[pymethods]
1994impl MarketStructureConfluenceStreamPy {
1995 #[new]
1996 #[pyo3(signature = (swing_size=DEFAULT_SWING_SIZE, bos_confirmation=DEFAULT_BOS_CONFIRMATION, basis_length=DEFAULT_BASIS_LENGTH, atr_length=DEFAULT_ATR_LENGTH, atr_smooth=DEFAULT_ATR_SMOOTH, vol_mult=DEFAULT_VOL_MULT))]
1997 fn new(
1998 swing_size: usize,
1999 bos_confirmation: &str,
2000 basis_length: usize,
2001 atr_length: usize,
2002 atr_smooth: usize,
2003 vol_mult: f64,
2004 ) -> PyResult<Self> {
2005 let stream = MarketStructureConfluenceStream::try_new(MarketStructureConfluenceParams {
2006 swing_size: Some(swing_size),
2007 bos_confirmation: Some(bos_confirmation.to_string()),
2008 basis_length: Some(basis_length),
2009 atr_length: Some(atr_length),
2010 atr_smooth: Some(atr_smooth),
2011 vol_mult: Some(vol_mult),
2012 })
2013 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2014 Ok(Self { stream })
2015 }
2016
2017 fn update(&mut self, high: f64, low: f64, close: f64) -> Option<Vec<f64>> {
2018 self.stream.update(high, low, close).map(|output| {
2019 vec![
2020 output.basis,
2021 output.upper_band,
2022 output.lower_band,
2023 output.structure_direction,
2024 output.bullish_arrow,
2025 output.bearish_arrow,
2026 output.bullish_change,
2027 output.bearish_change,
2028 output.hh,
2029 output.lh,
2030 output.hl,
2031 output.ll,
2032 output.bullish_bos,
2033 output.bullish_choch,
2034 output.bearish_bos,
2035 output.bearish_choch,
2036 ]
2037 })
2038 }
2039}
2040
2041#[cfg(feature = "python")]
2042pub fn register_market_structure_confluence_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
2043 m.add_function(wrap_pyfunction!(market_structure_confluence_py, m)?)?;
2044 m.add_function(wrap_pyfunction!(market_structure_confluence_batch_py, m)?)?;
2045 m.add_class::<MarketStructureConfluenceStreamPy>()?;
2046 Ok(())
2047}
2048
2049#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2050#[derive(Serialize, Deserialize)]
2051pub struct MarketStructureConfluenceJsOutput {
2052 pub basis: Vec<f64>,
2053 pub upper_band: Vec<f64>,
2054 pub lower_band: Vec<f64>,
2055 pub structure_direction: Vec<f64>,
2056 pub bullish_arrow: Vec<f64>,
2057 pub bearish_arrow: Vec<f64>,
2058 pub bullish_change: Vec<f64>,
2059 pub bearish_change: Vec<f64>,
2060 pub hh: Vec<f64>,
2061 pub lh: Vec<f64>,
2062 pub hl: Vec<f64>,
2063 pub ll: Vec<f64>,
2064 pub bullish_bos: Vec<f64>,
2065 pub bullish_choch: Vec<f64>,
2066 pub bearish_bos: Vec<f64>,
2067 pub bearish_choch: Vec<f64>,
2068}
2069
2070#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2071#[wasm_bindgen(js_name = market_structure_confluence_js)]
2072pub fn market_structure_confluence_js(
2073 high: &[f64],
2074 low: &[f64],
2075 close: &[f64],
2076 swing_size: usize,
2077 bos_confirmation: String,
2078 basis_length: usize,
2079 atr_length: usize,
2080 atr_smooth: usize,
2081 vol_mult: f64,
2082) -> Result<JsValue, JsValue> {
2083 let input = MarketStructureConfluenceInput::from_slices(
2084 high,
2085 low,
2086 close,
2087 MarketStructureConfluenceParams {
2088 swing_size: Some(swing_size),
2089 bos_confirmation: Some(bos_confirmation),
2090 basis_length: Some(basis_length),
2091 atr_length: Some(atr_length),
2092 atr_smooth: Some(atr_smooth),
2093 vol_mult: Some(vol_mult),
2094 },
2095 );
2096 let output = market_structure_confluence_with_kernel(&input, Kernel::Auto)
2097 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2098 serde_wasm_bindgen::to_value(&MarketStructureConfluenceJsOutput {
2099 basis: output.basis,
2100 upper_band: output.upper_band,
2101 lower_band: output.lower_band,
2102 structure_direction: output.structure_direction,
2103 bullish_arrow: output.bullish_arrow,
2104 bearish_arrow: output.bearish_arrow,
2105 bullish_change: output.bullish_change,
2106 bearish_change: output.bearish_change,
2107 hh: output.hh,
2108 lh: output.lh,
2109 hl: output.hl,
2110 ll: output.ll,
2111 bullish_bos: output.bullish_bos,
2112 bullish_choch: output.bullish_choch,
2113 bearish_bos: output.bearish_bos,
2114 bearish_choch: output.bearish_choch,
2115 })
2116 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
2117}
2118
2119#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2120#[derive(Serialize, Deserialize)]
2121pub struct MarketStructureConfluenceBatchConfig {
2122 pub swing_size_range: (usize, usize, usize),
2123 pub bos_confirmation_options: Vec<String>,
2124 pub basis_length_range: (usize, usize, usize),
2125 pub atr_length_range: (usize, usize, usize),
2126 pub atr_smooth_range: (usize, usize, usize),
2127 pub vol_mult_range: (f64, f64, f64),
2128}
2129
2130#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2131#[derive(Serialize, Deserialize)]
2132pub struct MarketStructureConfluenceBatchJsOutput {
2133 pub basis: Vec<f64>,
2134 pub upper_band: Vec<f64>,
2135 pub lower_band: Vec<f64>,
2136 pub structure_direction: Vec<f64>,
2137 pub bullish_arrow: Vec<f64>,
2138 pub bearish_arrow: Vec<f64>,
2139 pub bullish_change: Vec<f64>,
2140 pub bearish_change: Vec<f64>,
2141 pub hh: Vec<f64>,
2142 pub lh: Vec<f64>,
2143 pub hl: Vec<f64>,
2144 pub ll: Vec<f64>,
2145 pub bullish_bos: Vec<f64>,
2146 pub bullish_choch: Vec<f64>,
2147 pub bearish_bos: Vec<f64>,
2148 pub bearish_choch: Vec<f64>,
2149 pub swing_sizes: Vec<usize>,
2150 pub bos_confirmations: Vec<String>,
2151 pub basis_lengths: Vec<usize>,
2152 pub atr_lengths: Vec<usize>,
2153 pub atr_smooths: Vec<usize>,
2154 pub vol_mults: Vec<f64>,
2155 pub rows: usize,
2156 pub cols: usize,
2157}
2158
2159#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2160#[wasm_bindgen(js_name = market_structure_confluence_batch)]
2161pub fn market_structure_confluence_batch_js(
2162 high: &[f64],
2163 low: &[f64],
2164 close: &[f64],
2165 config: JsValue,
2166) -> Result<JsValue, JsValue> {
2167 let cfg: MarketStructureConfluenceBatchConfig =
2168 serde_wasm_bindgen::from_value(config).map_err(|e| JsValue::from_str(&e.to_string()))?;
2169 let output = market_structure_confluence_batch_with_kernel(
2170 high,
2171 low,
2172 close,
2173 &MarketStructureConfluenceBatchRange {
2174 swing_size: cfg.swing_size_range,
2175 bos_confirmation: cfg.bos_confirmation_options,
2176 basis_length: cfg.basis_length_range,
2177 atr_length: cfg.atr_length_range,
2178 atr_smooth: cfg.atr_smooth_range,
2179 vol_mult: cfg.vol_mult_range,
2180 },
2181 Kernel::Auto,
2182 )
2183 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2184
2185 serde_wasm_bindgen::to_value(&MarketStructureConfluenceBatchJsOutput {
2186 basis: output.basis,
2187 upper_band: output.upper_band,
2188 lower_band: output.lower_band,
2189 structure_direction: output.structure_direction,
2190 bullish_arrow: output.bullish_arrow,
2191 bearish_arrow: output.bearish_arrow,
2192 bullish_change: output.bullish_change,
2193 bearish_change: output.bearish_change,
2194 hh: output.hh,
2195 lh: output.lh,
2196 hl: output.hl,
2197 ll: output.ll,
2198 bullish_bos: output.bullish_bos,
2199 bullish_choch: output.bullish_choch,
2200 bearish_bos: output.bearish_bos,
2201 bearish_choch: output.bearish_choch,
2202 swing_sizes: output
2203 .combos
2204 .iter()
2205 .map(|combo| combo.swing_size.unwrap_or(DEFAULT_SWING_SIZE))
2206 .collect(),
2207 bos_confirmations: output
2208 .combos
2209 .iter()
2210 .map(|combo| {
2211 combo
2212 .bos_confirmation
2213 .clone()
2214 .unwrap_or_else(|| DEFAULT_BOS_CONFIRMATION.to_string())
2215 })
2216 .collect(),
2217 basis_lengths: output
2218 .combos
2219 .iter()
2220 .map(|combo| combo.basis_length.unwrap_or(DEFAULT_BASIS_LENGTH))
2221 .collect(),
2222 atr_lengths: output
2223 .combos
2224 .iter()
2225 .map(|combo| combo.atr_length.unwrap_or(DEFAULT_ATR_LENGTH))
2226 .collect(),
2227 atr_smooths: output
2228 .combos
2229 .iter()
2230 .map(|combo| combo.atr_smooth.unwrap_or(DEFAULT_ATR_SMOOTH))
2231 .collect(),
2232 vol_mults: output
2233 .combos
2234 .iter()
2235 .map(|combo| combo.vol_mult.unwrap_or(DEFAULT_VOL_MULT))
2236 .collect(),
2237 rows: output.rows,
2238 cols: output.cols,
2239 })
2240 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
2241}
2242
2243#[cfg(test)]
2244mod tests {
2245 use super::*;
2246
2247 fn sample_ohlc() -> (Vec<f64>, Vec<f64>, Vec<f64>) {
2248 let mut high = Vec::with_capacity(420);
2249 let mut low = Vec::with_capacity(420);
2250 let mut close = Vec::with_capacity(420);
2251 for i in 0..420 {
2252 let base = 100.0 + i as f64 * 0.08 + (i as f64 * 0.11).sin() * 1.7;
2253 let c = base + (i as f64 * 0.07).cos() * 0.45;
2254 let h = c + 0.8 + (i as f64 * 0.09).sin().abs() * 0.4;
2255 let l = c - 0.8 - (i as f64 * 0.13).cos().abs() * 0.35;
2256 high.push(h);
2257 low.push(l);
2258 close.push(c);
2259 }
2260 (high, low, close)
2261 }
2262
2263 #[test]
2264 fn market_structure_confluence_into_matches_single() {
2265 let (high, low, close) = sample_ohlc();
2266 let input = MarketStructureConfluenceInput::from_slices(
2267 &high,
2268 &low,
2269 &close,
2270 MarketStructureConfluenceParams::default(),
2271 );
2272 let out = market_structure_confluence_with_kernel(&input, Kernel::Scalar).expect("single");
2273 let mut basis = vec![0.0; close.len()];
2274 let mut upper_band = vec![0.0; close.len()];
2275 let mut lower_band = vec![0.0; close.len()];
2276 let mut structure_direction = vec![0.0; close.len()];
2277 let mut bullish_arrow = vec![0.0; close.len()];
2278 let mut bearish_arrow = vec![0.0; close.len()];
2279 let mut bullish_change = vec![0.0; close.len()];
2280 let mut bearish_change = vec![0.0; close.len()];
2281 let mut hh = vec![0.0; close.len()];
2282 let mut lh = vec![0.0; close.len()];
2283 let mut hl = vec![0.0; close.len()];
2284 let mut ll = vec![0.0; close.len()];
2285 let mut bullish_bos = vec![0.0; close.len()];
2286 let mut bullish_choch = vec![0.0; close.len()];
2287 let mut bearish_bos = vec![0.0; close.len()];
2288 let mut bearish_choch = vec![0.0; close.len()];
2289
2290 market_structure_confluence_into_slices(
2291 &input,
2292 Kernel::Scalar,
2293 &mut basis,
2294 &mut upper_band,
2295 &mut lower_band,
2296 &mut structure_direction,
2297 &mut bullish_arrow,
2298 &mut bearish_arrow,
2299 &mut bullish_change,
2300 &mut bearish_change,
2301 &mut hh,
2302 &mut lh,
2303 &mut hl,
2304 &mut ll,
2305 &mut bullish_bos,
2306 &mut bullish_choch,
2307 &mut bearish_bos,
2308 &mut bearish_choch,
2309 )
2310 .expect("into");
2311
2312 for i in 0..close.len() {
2313 for (lhs, rhs) in [
2314 (out.basis[i], basis[i]),
2315 (out.upper_band[i], upper_band[i]),
2316 (out.lower_band[i], lower_band[i]),
2317 (out.structure_direction[i], structure_direction[i]),
2318 (out.bullish_arrow[i], bullish_arrow[i]),
2319 (out.bearish_arrow[i], bearish_arrow[i]),
2320 (out.bullish_change[i], bullish_change[i]),
2321 (out.bearish_change[i], bearish_change[i]),
2322 (out.hh[i], hh[i]),
2323 (out.lh[i], lh[i]),
2324 (out.hl[i], hl[i]),
2325 (out.ll[i], ll[i]),
2326 (out.bullish_bos[i], bullish_bos[i]),
2327 (out.bullish_choch[i], bullish_choch[i]),
2328 (out.bearish_bos[i], bearish_bos[i]),
2329 (out.bearish_choch[i], bearish_choch[i]),
2330 ] {
2331 if lhs.is_nan() {
2332 assert!(rhs.is_nan());
2333 } else {
2334 assert!((lhs - rhs).abs() <= 1e-12);
2335 }
2336 }
2337 }
2338 }
2339
2340 #[test]
2341 fn market_structure_confluence_stream_matches_batch() {
2342 let (high, low, close) = sample_ohlc();
2343 let input = MarketStructureConfluenceInput::from_slices(
2344 &high,
2345 &low,
2346 &close,
2347 MarketStructureConfluenceParams::default(),
2348 );
2349 let out = market_structure_confluence(&input).expect("batch");
2350 let mut stream =
2351 MarketStructureConfluenceStream::try_new(MarketStructureConfluenceParams::default())
2352 .expect("stream");
2353 let mut collected = Vec::with_capacity(close.len());
2354 for i in 0..close.len() {
2355 collected.push(stream.update(high[i], low[i], close[i]));
2356 }
2357 for i in 0..close.len() {
2358 let Some(point) = collected[i] else {
2359 assert!(out.basis[i].is_nan());
2360 continue;
2361 };
2362 for (lhs, rhs) in [
2363 (point.basis, out.basis[i]),
2364 (point.upper_band, out.upper_band[i]),
2365 (point.lower_band, out.lower_band[i]),
2366 (point.structure_direction, out.structure_direction[i]),
2367 (point.bullish_arrow, out.bullish_arrow[i]),
2368 (point.bearish_arrow, out.bearish_arrow[i]),
2369 (point.bullish_change, out.bullish_change[i]),
2370 (point.bearish_change, out.bearish_change[i]),
2371 (point.hh, out.hh[i]),
2372 (point.lh, out.lh[i]),
2373 (point.hl, out.hl[i]),
2374 (point.ll, out.ll[i]),
2375 (point.bullish_bos, out.bullish_bos[i]),
2376 (point.bullish_choch, out.bullish_choch[i]),
2377 (point.bearish_bos, out.bearish_bos[i]),
2378 (point.bearish_choch, out.bearish_choch[i]),
2379 ] {
2380 if rhs.is_nan() {
2381 assert!(lhs.is_nan());
2382 } else {
2383 assert!((lhs - rhs).abs() <= 1e-12);
2384 }
2385 }
2386 }
2387 }
2388
2389 #[test]
2390 fn market_structure_confluence_batch_first_row_matches_single() {
2391 let (high, low, close) = sample_ohlc();
2392 let single = market_structure_confluence(&MarketStructureConfluenceInput::from_slices(
2393 &high,
2394 &low,
2395 &close,
2396 MarketStructureConfluenceParams::default(),
2397 ))
2398 .expect("single");
2399 let batch = market_structure_confluence_batch_with_kernel(
2400 &high,
2401 &low,
2402 &close,
2403 &MarketStructureConfluenceBatchRange {
2404 swing_size: (10, 12, 2),
2405 bos_confirmation: vec!["Candle Close".to_string()],
2406 basis_length: (100, 100, 0),
2407 atr_length: (14, 14, 0),
2408 atr_smooth: (21, 21, 0),
2409 vol_mult: (2.0, 2.0, 0.0),
2410 },
2411 Kernel::ScalarBatch,
2412 )
2413 .expect("batch");
2414 assert_eq!(batch.rows, 2);
2415 assert_eq!(batch.cols, close.len());
2416 for i in 0..close.len() {
2417 let idx = i;
2418 for (lhs, rhs) in [
2419 (single.basis[i], batch.basis[idx]),
2420 (single.upper_band[i], batch.upper_band[idx]),
2421 (single.lower_band[i], batch.lower_band[idx]),
2422 (
2423 single.structure_direction[i],
2424 batch.structure_direction[idx],
2425 ),
2426 (single.bullish_arrow[i], batch.bullish_arrow[idx]),
2427 (single.bearish_arrow[i], batch.bearish_arrow[idx]),
2428 (single.bullish_change[i], batch.bullish_change[idx]),
2429 (single.bearish_change[i], batch.bearish_change[idx]),
2430 (single.hh[i], batch.hh[idx]),
2431 (single.lh[i], batch.lh[idx]),
2432 (single.hl[i], batch.hl[idx]),
2433 (single.ll[i], batch.ll[idx]),
2434 (single.bullish_bos[i], batch.bullish_bos[idx]),
2435 (single.bullish_choch[i], batch.bullish_choch[idx]),
2436 (single.bearish_bos[i], batch.bearish_bos[idx]),
2437 (single.bearish_choch[i], batch.bearish_choch[idx]),
2438 ] {
2439 if lhs.is_nan() {
2440 assert!(rhs.is_nan());
2441 } else {
2442 assert!((lhs - rhs).abs() <= 1e-12);
2443 }
2444 }
2445 }
2446 }
2447
2448 #[test]
2449 fn market_structure_confluence_rejects_invalid_params() {
2450 let (high, low, close) = sample_ohlc();
2451 let err = market_structure_confluence(&MarketStructureConfluenceInput::from_slices(
2452 &high,
2453 &low,
2454 &close,
2455 MarketStructureConfluenceParams {
2456 swing_size: Some(1),
2457 ..MarketStructureConfluenceParams::default()
2458 },
2459 ))
2460 .expect_err("invalid swing size");
2461 assert!(err.to_string().contains("invalid swing_size"));
2462
2463 let err = MarketStructureConfluenceStream::try_new(MarketStructureConfluenceParams {
2464 bos_confirmation: Some("bad".to_string()),
2465 ..MarketStructureConfluenceParams::default()
2466 })
2467 .expect_err("invalid confirmation");
2468 assert!(err.to_string().contains("invalid bos_confirmation"));
2469 }
2470}