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