1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::{PyDict, PyList};
9#[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::{source_type, 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::convert::AsRef;
28use std::mem::ManuallyDrop;
29use thiserror::Error;
30
31const DEFAULT_LENGTH: usize = 20;
32const DEFAULT_INDEX_SMOOTH: usize = 5;
33const DEFAULT_SOURCE: &str = "close";
34
35impl<'a> AsRef<[f64]> for MonotonicityIndexInput<'a> {
36 #[inline(always)]
37 fn as_ref(&self) -> &[f64] {
38 match &self.data {
39 MonotonicityIndexData::Slice(slice) => slice,
40 MonotonicityIndexData::Candles { candles, source } => source_type(candles, source),
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
46pub enum MonotonicityIndexData<'a> {
47 Candles {
48 candles: &'a Candles,
49 source: &'a str,
50 },
51 Slice(&'a [f64]),
52}
53
54#[derive(Debug, Clone)]
55pub struct MonotonicityIndexOutput {
56 pub index: Vec<f64>,
57 pub cumulative_mean: Vec<f64>,
58 pub upper_bound: Vec<f64>,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
62#[cfg_attr(
63 all(target_arch = "wasm32", feature = "wasm"),
64 derive(Serialize, Deserialize)
65)]
66#[cfg_attr(
67 all(target_arch = "wasm32", feature = "wasm"),
68 serde(rename_all = "snake_case")
69)]
70pub enum MonotonicityIndexMode {
71 Complexity,
72 #[default]
73 Efficiency,
74}
75
76impl MonotonicityIndexMode {
77 #[inline]
78 pub fn parse(value: &str) -> Option<Self> {
79 if value.eq_ignore_ascii_case("complexity") {
80 Some(Self::Complexity)
81 } else if value.eq_ignore_ascii_case("efficiency") {
82 Some(Self::Efficiency)
83 } else {
84 None
85 }
86 }
87
88 #[inline]
89 pub fn as_str(self) -> &'static str {
90 match self {
91 Self::Complexity => "complexity",
92 Self::Efficiency => "efficiency",
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98#[cfg_attr(
99 all(target_arch = "wasm32", feature = "wasm"),
100 derive(Serialize, Deserialize)
101)]
102pub struct MonotonicityIndexParams {
103 pub length: Option<usize>,
104 pub mode: Option<MonotonicityIndexMode>,
105 pub index_smooth: Option<usize>,
106}
107
108impl Default for MonotonicityIndexParams {
109 fn default() -> Self {
110 Self {
111 length: Some(DEFAULT_LENGTH),
112 mode: Some(MonotonicityIndexMode::Efficiency),
113 index_smooth: Some(DEFAULT_INDEX_SMOOTH),
114 }
115 }
116}
117
118#[derive(Debug, Clone)]
119pub struct MonotonicityIndexInput<'a> {
120 pub data: MonotonicityIndexData<'a>,
121 pub params: MonotonicityIndexParams,
122}
123
124impl<'a> MonotonicityIndexInput<'a> {
125 #[inline]
126 pub fn from_candles(
127 candles: &'a Candles,
128 source: &'a str,
129 params: MonotonicityIndexParams,
130 ) -> Self {
131 Self {
132 data: MonotonicityIndexData::Candles { candles, source },
133 params,
134 }
135 }
136
137 #[inline]
138 pub fn from_slice(slice: &'a [f64], params: MonotonicityIndexParams) -> Self {
139 Self {
140 data: MonotonicityIndexData::Slice(slice),
141 params,
142 }
143 }
144
145 #[inline]
146 pub fn with_default_candles(candles: &'a Candles) -> Self {
147 Self::from_candles(candles, DEFAULT_SOURCE, MonotonicityIndexParams::default())
148 }
149}
150
151#[derive(Clone, Copy, Debug, Default)]
152pub struct MonotonicityIndexBuilder {
153 length: Option<usize>,
154 mode: Option<MonotonicityIndexMode>,
155 index_smooth: Option<usize>,
156 kernel: Kernel,
157}
158
159impl MonotonicityIndexBuilder {
160 #[inline]
161 pub fn new() -> Self {
162 Self::default()
163 }
164
165 #[inline]
166 pub fn length(mut self, length: usize) -> Self {
167 self.length = Some(length);
168 self
169 }
170
171 #[inline]
172 pub fn mode(mut self, mode: MonotonicityIndexMode) -> Self {
173 self.mode = Some(mode);
174 self
175 }
176
177 #[inline]
178 pub fn index_smooth(mut self, index_smooth: usize) -> Self {
179 self.index_smooth = Some(index_smooth);
180 self
181 }
182
183 #[inline]
184 pub fn kernel(mut self, kernel: Kernel) -> Self {
185 self.kernel = kernel;
186 self
187 }
188
189 #[inline]
190 pub fn apply(
191 self,
192 candles: &Candles,
193 source: &str,
194 ) -> Result<MonotonicityIndexOutput, MonotonicityIndexError> {
195 let input = MonotonicityIndexInput::from_candles(
196 candles,
197 source,
198 MonotonicityIndexParams {
199 length: self.length,
200 mode: self.mode,
201 index_smooth: self.index_smooth,
202 },
203 );
204 monotonicity_index_with_kernel(&input, self.kernel)
205 }
206
207 #[inline]
208 pub fn apply_slice(
209 self,
210 data: &[f64],
211 ) -> Result<MonotonicityIndexOutput, MonotonicityIndexError> {
212 let input = MonotonicityIndexInput::from_slice(
213 data,
214 MonotonicityIndexParams {
215 length: self.length,
216 mode: self.mode,
217 index_smooth: self.index_smooth,
218 },
219 );
220 monotonicity_index_with_kernel(&input, self.kernel)
221 }
222
223 #[inline]
224 pub fn into_stream(self) -> Result<MonotonicityIndexStream, MonotonicityIndexError> {
225 MonotonicityIndexStream::try_new(MonotonicityIndexParams {
226 length: self.length,
227 mode: self.mode,
228 index_smooth: self.index_smooth,
229 })
230 }
231}
232
233#[derive(Debug, Error)]
234pub enum MonotonicityIndexError {
235 #[error("monotonicity_index: Input data slice is empty.")]
236 EmptyInputData,
237 #[error("monotonicity_index: All values are NaN.")]
238 AllValuesNaN,
239 #[error("monotonicity_index: Invalid length: {length}")]
240 InvalidLength { length: usize },
241 #[error("monotonicity_index: Invalid index_smooth: {index_smooth}")]
242 InvalidIndexSmooth { index_smooth: usize },
243 #[error("monotonicity_index: Invalid mode: {mode}")]
244 InvalidMode { mode: String },
245 #[error("monotonicity_index: Not enough valid data: needed = {needed}, valid = {valid}")]
246 NotEnoughValidData { needed: usize, valid: usize },
247 #[error(
248 "monotonicity_index: Output length mismatch: expected = {expected}, index = {index_got}, cumulative_mean = {cumulative_mean_got}, upper_bound = {upper_bound_got}"
249 )]
250 OutputLengthMismatch {
251 expected: usize,
252 index_got: usize,
253 cumulative_mean_got: usize,
254 upper_bound_got: usize,
255 },
256 #[error("monotonicity_index: Invalid range: start={start}, end={end}, step={step}")]
257 InvalidRange {
258 start: String,
259 end: String,
260 step: String,
261 },
262 #[error("monotonicity_index: Invalid kernel for batch: {0:?}")]
263 InvalidKernelForBatch(Kernel),
264}
265
266#[derive(Clone, Copy, Debug)]
267struct ResolvedParams {
268 length: usize,
269 mode: MonotonicityIndexMode,
270 index_smooth: usize,
271 warmup_period: usize,
272 needed_valid: usize,
273}
274
275#[derive(Clone, Copy, Debug, Default)]
276struct PavaFitSummary {
277 mse: f64,
278 pools: usize,
279 start_value: f64,
280 end_value: f64,
281}
282
283#[derive(Clone, Debug, Default)]
284struct PavaScratch {
285 inc_pool_vals: Vec<f64>,
286 inc_pool_weights: Vec<usize>,
287 dec_pool_vals: Vec<f64>,
288 dec_pool_weights: Vec<usize>,
289}
290
291impl PavaScratch {
292 #[inline]
293 fn fit(&mut self, data: &[f64], non_decreasing: bool) -> PavaFitSummary {
294 let (pool_vals, pool_weights) = if non_decreasing {
295 (&mut self.inc_pool_vals, &mut self.inc_pool_weights)
296 } else {
297 (&mut self.dec_pool_vals, &mut self.dec_pool_weights)
298 };
299 pool_vals.clear();
300 pool_weights.clear();
301
302 for &value in data {
303 let mut current_pool = value;
304 let mut current_weight = 1usize;
305 while let Some(&prev_pool) = pool_vals.last() {
306 let violation = if non_decreasing {
307 prev_pool > current_pool
308 } else {
309 prev_pool < current_pool
310 };
311 if !violation {
312 break;
313 }
314
315 let prev_weight = pool_weights.pop().unwrap();
316 let last_pool = pool_vals.pop().unwrap();
317 let combined_weight = prev_weight + current_weight;
318 current_pool = (last_pool * prev_weight as f64
319 + current_pool * current_weight as f64)
320 / combined_weight as f64;
321 current_weight = combined_weight;
322 }
323
324 pool_vals.push(current_pool);
325 pool_weights.push(current_weight);
326 }
327
328 let mut total_error = 0.0;
329 let mut idx = 0usize;
330 for (&pool_value, &pool_weight) in pool_vals.iter().zip(pool_weights.iter()) {
331 for _ in 0..pool_weight {
332 let delta = data[idx] - pool_value;
333 total_error += delta * delta;
334 idx += 1;
335 }
336 }
337
338 PavaFitSummary {
339 mse: total_error / data.len() as f64,
340 pools: pool_vals.len(),
341 start_value: pool_vals.first().copied().unwrap_or(0.0),
342 end_value: pool_vals.last().copied().unwrap_or(0.0),
343 }
344 }
345}
346
347#[derive(Clone, Debug)]
348struct RollingWindow {
349 buf: Vec<f64>,
350 next: usize,
351 len: usize,
352}
353
354impl RollingWindow {
355 #[inline]
356 fn new(capacity: usize) -> Self {
357 Self {
358 buf: vec![0.0; capacity],
359 next: 0,
360 len: 0,
361 }
362 }
363
364 #[inline]
365 fn reset(&mut self) {
366 self.next = 0;
367 self.len = 0;
368 }
369
370 #[inline]
371 fn capacity(&self) -> usize {
372 self.buf.len()
373 }
374
375 #[inline]
376 fn len(&self) -> usize {
377 self.len
378 }
379
380 #[inline]
381 fn push(&mut self, value: f64) {
382 self.buf[self.next] = value;
383 self.next += 1;
384 if self.next == self.buf.len() {
385 self.next = 0;
386 }
387 if self.len < self.buf.len() {
388 self.len += 1;
389 }
390 }
391
392 #[inline]
393 fn copy_to_vec(&self, out: &mut Vec<f64>) {
394 out.clear();
395 if self.len == 0 {
396 return;
397 }
398
399 if self.len < self.buf.len() {
400 out.extend_from_slice(&self.buf[..self.len]);
401 return;
402 }
403
404 out.extend_from_slice(&self.buf[self.next..]);
405 out.extend_from_slice(&self.buf[..self.next]);
406 }
407}
408
409#[derive(Clone, Debug)]
410struct RollingSma {
411 buf: Vec<f64>,
412 next: usize,
413 len: usize,
414 sum: f64,
415}
416
417impl RollingSma {
418 #[inline]
419 fn new(period: usize) -> Self {
420 Self {
421 buf: vec![0.0; period],
422 next: 0,
423 len: 0,
424 sum: 0.0,
425 }
426 }
427
428 #[inline]
429 fn reset(&mut self) {
430 self.next = 0;
431 self.len = 0;
432 self.sum = 0.0;
433 }
434
435 #[inline]
436 fn update(&mut self, value: f64) -> Option<f64> {
437 if self.len == self.buf.len() {
438 self.sum -= self.buf[self.next];
439 } else {
440 self.len += 1;
441 }
442 self.buf[self.next] = value;
443 self.sum += value;
444 self.next += 1;
445 if self.next == self.buf.len() {
446 self.next = 0;
447 }
448
449 if self.len == self.buf.len() {
450 Some(self.sum / self.buf.len() as f64)
451 } else {
452 None
453 }
454 }
455}
456
457#[derive(Clone, Debug)]
458pub struct MonotonicityIndexStream {
459 params: ResolvedParams,
460 price_window: RollingWindow,
461 raw_sma: RollingSma,
462 cumulative_sum: f64,
463 cumulative_count: usize,
464 scratch: PavaScratch,
465 window_data: Vec<f64>,
466}
467
468impl MonotonicityIndexStream {
469 #[inline]
470 pub fn try_new(params: MonotonicityIndexParams) -> Result<Self, MonotonicityIndexError> {
471 let params = resolve_params(¶ms)?;
472 Ok(Self {
473 price_window: RollingWindow::new(params.length),
474 raw_sma: RollingSma::new(params.index_smooth),
475 cumulative_sum: 0.0,
476 cumulative_count: 0,
477 scratch: PavaScratch::default(),
478 window_data: Vec::with_capacity(params.length),
479 params,
480 })
481 }
482
483 #[inline]
484 pub fn reset(&mut self) {
485 self.price_window.reset();
486 self.raw_sma.reset();
487 self.cumulative_sum = 0.0;
488 self.cumulative_count = 0;
489 self.window_data.clear();
490 }
491
492 #[inline]
493 pub fn get_warmup_period(&self) -> usize {
494 self.params.warmup_period
495 }
496
497 #[inline]
498 pub fn update(&mut self, value: f64) -> Option<(f64, f64, f64)> {
499 if !value.is_finite() {
500 self.reset();
501 return None;
502 }
503
504 self.price_window.push(value);
505 if self.price_window.len() < self.price_window.capacity() {
506 return None;
507 }
508
509 self.price_window.copy_to_vec(&mut self.window_data);
510 let raw_index = compute_raw_index(&self.window_data, self.params.mode, &mut self.scratch);
511 let smoothed = self.raw_sma.update(raw_index)?;
512 self.cumulative_sum += smoothed;
513 self.cumulative_count += 1;
514 let cumulative_mean = self.cumulative_sum / self.cumulative_count as f64;
515 Some((smoothed, cumulative_mean, cumulative_mean * 2.0))
516 }
517}
518
519#[inline(always)]
520fn first_valid_value(data: &[f64]) -> usize {
521 let mut i = 0usize;
522 while i < data.len() {
523 if data[i].is_finite() {
524 return i;
525 }
526 i += 1;
527 }
528 data.len()
529}
530
531#[inline(always)]
532fn max_consecutive_valid_values(data: &[f64]) -> usize {
533 let mut best = 0usize;
534 let mut run = 0usize;
535 for &value in data {
536 if value.is_finite() {
537 run += 1;
538 if run > best {
539 best = run;
540 }
541 } else {
542 run = 0;
543 }
544 }
545 best
546}
547
548#[inline(always)]
549fn resolve_params(
550 params: &MonotonicityIndexParams,
551) -> Result<ResolvedParams, MonotonicityIndexError> {
552 let length = params.length.unwrap_or(DEFAULT_LENGTH);
553 if length < 2 {
554 return Err(MonotonicityIndexError::InvalidLength { length });
555 }
556
557 let index_smooth = params.index_smooth.unwrap_or(DEFAULT_INDEX_SMOOTH);
558 if index_smooth == 0 {
559 return Err(MonotonicityIndexError::InvalidIndexSmooth { index_smooth });
560 }
561
562 let mode = params.mode.unwrap_or_default();
563 let needed_valid = length
564 .checked_add(index_smooth)
565 .and_then(|x| x.checked_sub(1))
566 .ok_or(MonotonicityIndexError::InvalidLength { length })?;
567
568 Ok(ResolvedParams {
569 length,
570 mode,
571 index_smooth,
572 warmup_period: needed_valid - 1,
573 needed_valid,
574 })
575}
576
577#[inline(always)]
578fn compute_raw_index(data: &[f64], mode: MonotonicityIndexMode, scratch: &mut PavaScratch) -> f64 {
579 let inc_fit = scratch.fit(data, true);
580 let dec_fit = scratch.fit(data, false);
581 let best_fit = if inc_fit.mse < dec_fit.mse {
582 inc_fit
583 } else {
584 dec_fit
585 };
586
587 match mode {
588 MonotonicityIndexMode::Efficiency => {
589 let mut price_path = 0.0;
590 let mut i = 1usize;
591 while i < data.len() {
592 price_path += (data[i] - data[i - 1]).abs();
593 i += 1;
594 }
595 if price_path > 0.0 {
596 (best_fit.end_value - best_fit.start_value).abs() / price_path * 100.0
597 } else {
598 0.0
599 }
600 }
601 MonotonicityIndexMode::Complexity => {
602 (best_fit.pools.saturating_sub(1) as f64 / (data.len() - 1) as f64) * 100.0
603 }
604 }
605}
606
607#[inline(always)]
608fn monotonicity_index_prepare<'a>(
609 input: &'a MonotonicityIndexInput,
610 kernel: Kernel,
611) -> Result<(&'a [f64], usize, ResolvedParams, Kernel), MonotonicityIndexError> {
612 let data = input.as_ref();
613 if data.is_empty() {
614 return Err(MonotonicityIndexError::EmptyInputData);
615 }
616
617 let first = first_valid_value(data);
618 if first >= data.len() {
619 return Err(MonotonicityIndexError::AllValuesNaN);
620 }
621
622 let params = resolve_params(&input.params)?;
623 let valid = max_consecutive_valid_values(data);
624 if valid < params.needed_valid {
625 return Err(MonotonicityIndexError::NotEnoughValidData {
626 needed: params.needed_valid,
627 valid,
628 });
629 }
630
631 let chosen = match kernel {
632 Kernel::Auto => detect_best_kernel(),
633 other => other.to_non_batch(),
634 };
635 Ok((data, first, params, chosen))
636}
637
638#[inline(always)]
639fn monotonicity_index_row_from_slice(
640 data: &[f64],
641 params: ResolvedParams,
642 index_out: &mut [f64],
643 cumulative_mean_out: &mut [f64],
644 upper_bound_out: &mut [f64],
645) {
646 let mut stream = MonotonicityIndexStream::try_new(MonotonicityIndexParams {
647 length: Some(params.length),
648 mode: Some(params.mode),
649 index_smooth: Some(params.index_smooth),
650 })
651 .unwrap();
652
653 for (((index_slot, cumulative_mean_slot), upper_bound_slot), &value) in index_out
654 .iter_mut()
655 .zip(cumulative_mean_out.iter_mut())
656 .zip(upper_bound_out.iter_mut())
657 .zip(data.iter())
658 {
659 if let Some((index, cumulative_mean, upper_bound)) = stream.update(value) {
660 *index_slot = index;
661 *cumulative_mean_slot = cumulative_mean;
662 *upper_bound_slot = upper_bound;
663 } else {
664 *index_slot = f64::NAN;
665 *cumulative_mean_slot = f64::NAN;
666 *upper_bound_slot = f64::NAN;
667 }
668 }
669}
670
671#[inline]
672pub fn monotonicity_index(
673 input: &MonotonicityIndexInput,
674) -> Result<MonotonicityIndexOutput, MonotonicityIndexError> {
675 monotonicity_index_with_kernel(input, Kernel::Auto)
676}
677
678#[inline]
679pub fn monotonicity_index_with_kernel(
680 input: &MonotonicityIndexInput,
681 kernel: Kernel,
682) -> Result<MonotonicityIndexOutput, MonotonicityIndexError> {
683 let (data, first, params, _chosen) = monotonicity_index_prepare(input, kernel)?;
684 let warmup = first.saturating_add(params.warmup_period).min(data.len());
685 let mut index = alloc_with_nan_prefix(data.len(), warmup);
686 let mut cumulative_mean = alloc_with_nan_prefix(data.len(), warmup);
687 let mut upper_bound = alloc_with_nan_prefix(data.len(), warmup);
688 monotonicity_index_row_from_slice(
689 data,
690 params,
691 &mut index,
692 &mut cumulative_mean,
693 &mut upper_bound,
694 );
695 Ok(MonotonicityIndexOutput {
696 index,
697 cumulative_mean,
698 upper_bound,
699 })
700}
701
702#[inline]
703pub fn monotonicity_index_into_slices(
704 index_out: &mut [f64],
705 cumulative_mean_out: &mut [f64],
706 upper_bound_out: &mut [f64],
707 input: &MonotonicityIndexInput,
708 kernel: Kernel,
709) -> Result<(), MonotonicityIndexError> {
710 let expected = input.as_ref().len();
711 if index_out.len() != expected
712 || cumulative_mean_out.len() != expected
713 || upper_bound_out.len() != expected
714 {
715 return Err(MonotonicityIndexError::OutputLengthMismatch {
716 expected,
717 index_got: index_out.len(),
718 cumulative_mean_got: cumulative_mean_out.len(),
719 upper_bound_got: upper_bound_out.len(),
720 });
721 }
722
723 let (data, _first, params, _chosen) = monotonicity_index_prepare(input, kernel)?;
724 monotonicity_index_row_from_slice(
725 data,
726 params,
727 index_out,
728 cumulative_mean_out,
729 upper_bound_out,
730 );
731 Ok(())
732}
733
734#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
735#[inline]
736pub fn monotonicity_index_into(
737 input: &MonotonicityIndexInput,
738 index_out: &mut [f64],
739 cumulative_mean_out: &mut [f64],
740 upper_bound_out: &mut [f64],
741) -> Result<(), MonotonicityIndexError> {
742 monotonicity_index_into_slices(
743 index_out,
744 cumulative_mean_out,
745 upper_bound_out,
746 input,
747 Kernel::Auto,
748 )
749}
750
751#[derive(Debug, Clone)]
752#[cfg_attr(
753 all(target_arch = "wasm32", feature = "wasm"),
754 derive(Serialize, Deserialize)
755)]
756pub struct MonotonicityIndexBatchRange {
757 pub length: (usize, usize, usize),
758 pub index_smooth: (usize, usize, usize),
759 pub mode: MonotonicityIndexMode,
760}
761
762impl Default for MonotonicityIndexBatchRange {
763 fn default() -> Self {
764 Self {
765 length: (DEFAULT_LENGTH, DEFAULT_LENGTH, 0),
766 index_smooth: (DEFAULT_INDEX_SMOOTH, DEFAULT_INDEX_SMOOTH, 0),
767 mode: MonotonicityIndexMode::Efficiency,
768 }
769 }
770}
771
772#[derive(Debug, Clone)]
773pub struct MonotonicityIndexBatchOutput {
774 pub index: Vec<f64>,
775 pub cumulative_mean: Vec<f64>,
776 pub upper_bound: Vec<f64>,
777 pub combos: Vec<MonotonicityIndexParams>,
778 pub rows: usize,
779 pub cols: usize,
780}
781
782impl MonotonicityIndexBatchOutput {
783 #[inline]
784 pub fn row_for_params(&self, params: &MonotonicityIndexParams) -> Option<usize> {
785 self.combos.iter().position(|combo| {
786 combo.length.unwrap_or(DEFAULT_LENGTH) == params.length.unwrap_or(DEFAULT_LENGTH)
787 && combo.mode.unwrap_or_default() == params.mode.unwrap_or_default()
788 && combo.index_smooth.unwrap_or(DEFAULT_INDEX_SMOOTH)
789 == params.index_smooth.unwrap_or(DEFAULT_INDEX_SMOOTH)
790 })
791 }
792
793 #[inline]
794 pub fn row_slices(&self, row: usize) -> Option<(&[f64], &[f64], &[f64])> {
795 if row >= self.rows {
796 return None;
797 }
798 let start = row * self.cols;
799 let end = start + self.cols;
800 Some((
801 &self.index[start..end],
802 &self.cumulative_mean[start..end],
803 &self.upper_bound[start..end],
804 ))
805 }
806}
807
808#[derive(Clone, Debug, Default)]
809pub struct MonotonicityIndexBatchBuilder {
810 range: MonotonicityIndexBatchRange,
811 kernel: Kernel,
812}
813
814impl MonotonicityIndexBatchBuilder {
815 #[inline]
816 pub fn new() -> Self {
817 Self::default()
818 }
819
820 #[inline]
821 pub fn kernel(mut self, kernel: Kernel) -> Self {
822 self.kernel = kernel;
823 self
824 }
825
826 #[inline]
827 pub fn length_range(mut self, start: usize, end: usize, step: usize) -> Self {
828 self.range.length = (start, end, step);
829 self
830 }
831
832 #[inline]
833 pub fn index_smooth_range(mut self, start: usize, end: usize, step: usize) -> Self {
834 self.range.index_smooth = (start, end, step);
835 self
836 }
837
838 #[inline]
839 pub fn mode(mut self, mode: MonotonicityIndexMode) -> Self {
840 self.range.mode = mode;
841 self
842 }
843
844 #[inline]
845 pub fn apply_slice(
846 self,
847 data: &[f64],
848 ) -> Result<MonotonicityIndexBatchOutput, MonotonicityIndexError> {
849 monotonicity_index_batch_with_kernel(data, &self.range, self.kernel)
850 }
851
852 #[inline]
853 pub fn apply_candles(
854 self,
855 candles: &Candles,
856 source: &str,
857 ) -> Result<MonotonicityIndexBatchOutput, MonotonicityIndexError> {
858 self.apply_slice(source_type(candles, source))
859 }
860}
861
862#[inline(always)]
863fn expand_axis_usize(
864 (start, end, step): (usize, usize, usize),
865) -> Result<Vec<usize>, MonotonicityIndexError> {
866 if step == 0 || start == end {
867 return Ok(vec![start]);
868 }
869
870 let mut out = Vec::new();
871 if start < end {
872 let mut x = start;
873 while x <= end {
874 out.push(x);
875 let next = x.saturating_add(step);
876 if next == x {
877 break;
878 }
879 x = next;
880 }
881 } else {
882 let mut x = start;
883 loop {
884 out.push(x);
885 if x == end {
886 break;
887 }
888 let next = x.saturating_sub(step);
889 if next == x || next < end {
890 break;
891 }
892 x = next;
893 }
894 }
895
896 if out.is_empty() {
897 return Err(MonotonicityIndexError::InvalidRange {
898 start: start.to_string(),
899 end: end.to_string(),
900 step: step.to_string(),
901 });
902 }
903 Ok(out)
904}
905
906#[inline(always)]
907fn expand_grid_monotonicity_index(
908 sweep: &MonotonicityIndexBatchRange,
909) -> Result<Vec<MonotonicityIndexParams>, MonotonicityIndexError> {
910 let lengths = expand_axis_usize(sweep.length)?;
911 let index_smooths = expand_axis_usize(sweep.index_smooth)?;
912
913 let mut combos = Vec::with_capacity(lengths.len() * index_smooths.len());
914 for length in lengths {
915 for index_smooth in index_smooths.iter().copied() {
916 let combo = MonotonicityIndexParams {
917 length: Some(length),
918 mode: Some(sweep.mode),
919 index_smooth: Some(index_smooth),
920 };
921 let _ = resolve_params(&combo)?;
922 combos.push(combo);
923 }
924 }
925 Ok(combos)
926}
927
928#[inline]
929pub fn monotonicity_index_batch_with_kernel(
930 data: &[f64],
931 sweep: &MonotonicityIndexBatchRange,
932 kernel: Kernel,
933) -> Result<MonotonicityIndexBatchOutput, MonotonicityIndexError> {
934 let batch_kernel = match kernel {
935 Kernel::Auto => detect_best_batch_kernel(),
936 other if other.is_batch() => other,
937 other => return Err(MonotonicityIndexError::InvalidKernelForBatch(other)),
938 };
939 monotonicity_index_batch_par_slice(data, sweep, batch_kernel.to_non_batch())
940}
941
942#[inline]
943pub fn monotonicity_index_batch_slice(
944 data: &[f64],
945 sweep: &MonotonicityIndexBatchRange,
946 kernel: Kernel,
947) -> Result<MonotonicityIndexBatchOutput, MonotonicityIndexError> {
948 monotonicity_index_batch_inner(data, sweep, kernel, false)
949}
950
951#[inline]
952pub fn monotonicity_index_batch_par_slice(
953 data: &[f64],
954 sweep: &MonotonicityIndexBatchRange,
955 kernel: Kernel,
956) -> Result<MonotonicityIndexBatchOutput, MonotonicityIndexError> {
957 monotonicity_index_batch_inner(data, sweep, kernel, true)
958}
959
960#[inline]
961pub fn monotonicity_index_batch_inner(
962 data: &[f64],
963 sweep: &MonotonicityIndexBatchRange,
964 _kernel: Kernel,
965 parallel: bool,
966) -> Result<MonotonicityIndexBatchOutput, MonotonicityIndexError> {
967 let combos = expand_grid_monotonicity_index(sweep)?;
968 let rows = combos.len();
969 let cols = data.len();
970 if cols == 0 {
971 return Err(MonotonicityIndexError::EmptyInputData);
972 }
973
974 let first = first_valid_value(data);
975 if first >= cols {
976 return Err(MonotonicityIndexError::AllValuesNaN);
977 }
978
979 let max_needed = combos
980 .iter()
981 .map(|combo| resolve_params(combo).unwrap().needed_valid)
982 .max()
983 .unwrap_or(0);
984 let max_warmup = combos
985 .iter()
986 .map(|combo| resolve_params(combo).unwrap().warmup_period)
987 .max()
988 .unwrap_or(0);
989 let valid = max_consecutive_valid_values(data);
990 if valid < max_needed {
991 return Err(MonotonicityIndexError::NotEnoughValidData {
992 needed: max_needed,
993 valid,
994 });
995 }
996
997 let mut index_mu = make_uninit_matrix(rows, cols);
998 let mut cumulative_mean_mu = make_uninit_matrix(rows, cols);
999 let mut upper_bound_mu = make_uninit_matrix(rows, cols);
1000 init_matrix_prefixes(
1001 &mut index_mu,
1002 cols,
1003 &vec![first.saturating_add(max_warmup).min(cols); rows],
1004 );
1005 init_matrix_prefixes(
1006 &mut cumulative_mean_mu,
1007 cols,
1008 &vec![first.saturating_add(max_warmup).min(cols); rows],
1009 );
1010 init_matrix_prefixes(
1011 &mut upper_bound_mu,
1012 cols,
1013 &vec![first.saturating_add(max_warmup).min(cols); rows],
1014 );
1015
1016 let mut index_guard = ManuallyDrop::new(index_mu);
1017 let mut cumulative_mean_guard = ManuallyDrop::new(cumulative_mean_mu);
1018 let mut upper_bound_guard = ManuallyDrop::new(upper_bound_mu);
1019 let index_out = unsafe {
1020 std::slice::from_raw_parts_mut(index_guard.as_mut_ptr() as *mut f64, index_guard.len())
1021 };
1022 let cumulative_mean_out = unsafe {
1023 std::slice::from_raw_parts_mut(
1024 cumulative_mean_guard.as_mut_ptr() as *mut f64,
1025 cumulative_mean_guard.len(),
1026 )
1027 };
1028 let upper_bound_out = unsafe {
1029 std::slice::from_raw_parts_mut(
1030 upper_bound_guard.as_mut_ptr() as *mut f64,
1031 upper_bound_guard.len(),
1032 )
1033 };
1034
1035 let combos = monotonicity_index_batch_inner_into(
1036 data,
1037 sweep,
1038 _kernel,
1039 parallel,
1040 index_out,
1041 cumulative_mean_out,
1042 upper_bound_out,
1043 )?;
1044
1045 let index = unsafe {
1046 Vec::from_raw_parts(
1047 index_guard.as_mut_ptr() as *mut f64,
1048 index_guard.len(),
1049 index_guard.capacity(),
1050 )
1051 };
1052 let cumulative_mean = unsafe {
1053 Vec::from_raw_parts(
1054 cumulative_mean_guard.as_mut_ptr() as *mut f64,
1055 cumulative_mean_guard.len(),
1056 cumulative_mean_guard.capacity(),
1057 )
1058 };
1059 let upper_bound = unsafe {
1060 Vec::from_raw_parts(
1061 upper_bound_guard.as_mut_ptr() as *mut f64,
1062 upper_bound_guard.len(),
1063 upper_bound_guard.capacity(),
1064 )
1065 };
1066
1067 Ok(MonotonicityIndexBatchOutput {
1068 index,
1069 cumulative_mean,
1070 upper_bound,
1071 combos,
1072 rows,
1073 cols,
1074 })
1075}
1076
1077#[inline]
1078pub fn monotonicity_index_batch_inner_into(
1079 data: &[f64],
1080 sweep: &MonotonicityIndexBatchRange,
1081 _kernel: Kernel,
1082 parallel: bool,
1083 index_out: &mut [f64],
1084 cumulative_mean_out: &mut [f64],
1085 upper_bound_out: &mut [f64],
1086) -> Result<Vec<MonotonicityIndexParams>, MonotonicityIndexError> {
1087 let combos = expand_grid_monotonicity_index(sweep)?;
1088 let rows = combos.len();
1089 let cols = data.len();
1090 if cols == 0 {
1091 return Err(MonotonicityIndexError::EmptyInputData);
1092 }
1093
1094 let total = rows
1095 .checked_mul(cols)
1096 .ok_or(MonotonicityIndexError::OutputLengthMismatch {
1097 expected: usize::MAX,
1098 index_got: index_out.len(),
1099 cumulative_mean_got: cumulative_mean_out.len(),
1100 upper_bound_got: upper_bound_out.len(),
1101 })?;
1102 if index_out.len() != total
1103 || cumulative_mean_out.len() != total
1104 || upper_bound_out.len() != total
1105 {
1106 return Err(MonotonicityIndexError::OutputLengthMismatch {
1107 expected: total,
1108 index_got: index_out.len(),
1109 cumulative_mean_got: cumulative_mean_out.len(),
1110 upper_bound_got: upper_bound_out.len(),
1111 });
1112 }
1113
1114 let first = first_valid_value(data);
1115 if first >= cols {
1116 return Err(MonotonicityIndexError::AllValuesNaN);
1117 }
1118
1119 let max_needed = combos
1120 .iter()
1121 .map(|combo| resolve_params(combo).unwrap().needed_valid)
1122 .max()
1123 .unwrap_or(0);
1124 let valid = max_consecutive_valid_values(data);
1125 if valid < max_needed {
1126 return Err(MonotonicityIndexError::NotEnoughValidData {
1127 needed: max_needed,
1128 valid,
1129 });
1130 }
1131
1132 if parallel {
1133 #[cfg(not(target_arch = "wasm32"))]
1134 index_out
1135 .par_chunks_mut(cols)
1136 .zip(cumulative_mean_out.par_chunks_mut(cols))
1137 .zip(upper_bound_out.par_chunks_mut(cols))
1138 .enumerate()
1139 .for_each(
1140 |(row, ((index_row, cumulative_mean_row), upper_bound_row))| {
1141 let params = resolve_params(&combos[row]).unwrap();
1142 monotonicity_index_row_from_slice(
1143 data,
1144 params,
1145 index_row,
1146 cumulative_mean_row,
1147 upper_bound_row,
1148 );
1149 },
1150 );
1151
1152 #[cfg(target_arch = "wasm32")]
1153 for (row, ((index_row, cumulative_mean_row), upper_bound_row)) in index_out
1154 .chunks_mut(cols)
1155 .zip(cumulative_mean_out.chunks_mut(cols))
1156 .zip(upper_bound_out.chunks_mut(cols))
1157 .enumerate()
1158 {
1159 let params = resolve_params(&combos[row]).unwrap();
1160 monotonicity_index_row_from_slice(
1161 data,
1162 params,
1163 index_row,
1164 cumulative_mean_row,
1165 upper_bound_row,
1166 );
1167 }
1168 } else {
1169 for (row, ((index_row, cumulative_mean_row), upper_bound_row)) in index_out
1170 .chunks_mut(cols)
1171 .zip(cumulative_mean_out.chunks_mut(cols))
1172 .zip(upper_bound_out.chunks_mut(cols))
1173 .enumerate()
1174 {
1175 let params = resolve_params(&combos[row]).unwrap();
1176 monotonicity_index_row_from_slice(
1177 data,
1178 params,
1179 index_row,
1180 cumulative_mean_row,
1181 upper_bound_row,
1182 );
1183 }
1184 }
1185
1186 Ok(combos)
1187}
1188
1189#[cfg(feature = "python")]
1190fn parse_mode_py(value: &str) -> PyResult<MonotonicityIndexMode> {
1191 MonotonicityIndexMode::parse(value)
1192 .ok_or_else(|| PyValueError::new_err(format!("Invalid mode: {value}")))
1193}
1194
1195#[cfg(feature = "python")]
1196#[pyfunction(name = "monotonicity_index")]
1197#[pyo3(signature = (
1198 data,
1199 length=DEFAULT_LENGTH,
1200 mode="efficiency",
1201 index_smooth=DEFAULT_INDEX_SMOOTH,
1202 kernel=None
1203))]
1204pub fn monotonicity_index_py<'py>(
1205 py: Python<'py>,
1206 data: PyReadonlyArray1<'py, f64>,
1207 length: usize,
1208 mode: &str,
1209 index_smooth: usize,
1210 kernel: Option<&str>,
1211) -> PyResult<(
1212 Bound<'py, PyArray1<f64>>,
1213 Bound<'py, PyArray1<f64>>,
1214 Bound<'py, PyArray1<f64>>,
1215)> {
1216 let data = data.as_slice()?;
1217 let kernel = validate_kernel(kernel, false)?;
1218 let input = MonotonicityIndexInput::from_slice(
1219 data,
1220 MonotonicityIndexParams {
1221 length: Some(length),
1222 mode: Some(parse_mode_py(mode)?),
1223 index_smooth: Some(index_smooth),
1224 },
1225 );
1226 let output = py
1227 .allow_threads(|| monotonicity_index_with_kernel(&input, kernel))
1228 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1229 Ok((
1230 output.index.into_pyarray(py),
1231 output.cumulative_mean.into_pyarray(py),
1232 output.upper_bound.into_pyarray(py),
1233 ))
1234}
1235
1236#[cfg(feature = "python")]
1237#[pyclass(name = "MonotonicityIndexStream")]
1238pub struct MonotonicityIndexStreamPy {
1239 stream: MonotonicityIndexStream,
1240}
1241
1242#[cfg(feature = "python")]
1243#[pymethods]
1244impl MonotonicityIndexStreamPy {
1245 #[new]
1246 #[pyo3(signature = (
1247 length=DEFAULT_LENGTH,
1248 mode="efficiency",
1249 index_smooth=DEFAULT_INDEX_SMOOTH
1250 ))]
1251 fn new(length: usize, mode: &str, index_smooth: usize) -> PyResult<Self> {
1252 let stream = MonotonicityIndexStream::try_new(MonotonicityIndexParams {
1253 length: Some(length),
1254 mode: Some(parse_mode_py(mode)?),
1255 index_smooth: Some(index_smooth),
1256 })
1257 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1258 Ok(Self { stream })
1259 }
1260
1261 fn update(&mut self, value: f64) -> Option<(f64, f64, f64)> {
1262 self.stream.update(value)
1263 }
1264
1265 #[getter]
1266 fn warmup_period(&self) -> usize {
1267 self.stream.get_warmup_period()
1268 }
1269}
1270
1271#[cfg(feature = "python")]
1272#[pyfunction(name = "monotonicity_index_batch")]
1273#[pyo3(signature = (
1274 data,
1275 length_range=(DEFAULT_LENGTH, DEFAULT_LENGTH, 0),
1276 index_smooth_range=(DEFAULT_INDEX_SMOOTH, DEFAULT_INDEX_SMOOTH, 0),
1277 mode="efficiency",
1278 kernel=None
1279))]
1280pub fn monotonicity_index_batch_py<'py>(
1281 py: Python<'py>,
1282 data: PyReadonlyArray1<'py, f64>,
1283 length_range: (usize, usize, usize),
1284 index_smooth_range: (usize, usize, usize),
1285 mode: &str,
1286 kernel: Option<&str>,
1287) -> PyResult<Bound<'py, PyDict>> {
1288 let data = data.as_slice()?;
1289 let kernel = validate_kernel(kernel, true)?;
1290 let sweep = MonotonicityIndexBatchRange {
1291 length: length_range,
1292 index_smooth: index_smooth_range,
1293 mode: parse_mode_py(mode)?,
1294 };
1295 let combos =
1296 expand_grid_monotonicity_index(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1297 let rows = combos.len();
1298 let cols = data.len();
1299 let total = rows
1300 .checked_mul(cols)
1301 .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1302
1303 let index_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1304 let cumulative_mean_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1305 let upper_bound_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1306 let index_slice = unsafe { index_arr.as_slice_mut()? };
1307 let cumulative_mean_slice = unsafe { cumulative_mean_arr.as_slice_mut()? };
1308 let upper_bound_slice = unsafe { upper_bound_arr.as_slice_mut()? };
1309
1310 let combos = py
1311 .allow_threads(|| {
1312 let batch = match kernel {
1313 Kernel::Auto => detect_best_batch_kernel(),
1314 other => other,
1315 };
1316 monotonicity_index_batch_inner_into(
1317 data,
1318 &sweep,
1319 batch.to_non_batch(),
1320 true,
1321 index_slice,
1322 cumulative_mean_slice,
1323 upper_bound_slice,
1324 )
1325 })
1326 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1327
1328 let dict = PyDict::new(py);
1329 dict.set_item("index", index_arr.reshape((rows, cols))?)?;
1330 dict.set_item(
1331 "cumulative_mean",
1332 cumulative_mean_arr.reshape((rows, cols))?,
1333 )?;
1334 dict.set_item("upper_bound", upper_bound_arr.reshape((rows, cols))?)?;
1335 dict.set_item(
1336 "lengths",
1337 combos
1338 .iter()
1339 .map(|combo| combo.length.unwrap_or(DEFAULT_LENGTH) as u64)
1340 .collect::<Vec<_>>()
1341 .into_pyarray(py),
1342 )?;
1343 dict.set_item(
1344 "modes",
1345 PyList::new(
1346 py,
1347 combos
1348 .iter()
1349 .map(|combo| combo.mode.unwrap_or_default().as_str())
1350 .collect::<Vec<_>>(),
1351 )?,
1352 )?;
1353 dict.set_item(
1354 "index_smooths",
1355 combos
1356 .iter()
1357 .map(|combo| combo.index_smooth.unwrap_or(DEFAULT_INDEX_SMOOTH) as u64)
1358 .collect::<Vec<_>>()
1359 .into_pyarray(py),
1360 )?;
1361 dict.set_item("rows", rows)?;
1362 dict.set_item("cols", cols)?;
1363 Ok(dict)
1364}
1365
1366#[cfg(feature = "python")]
1367pub fn register_monotonicity_index_module(
1368 module: &Bound<'_, pyo3::types::PyModule>,
1369) -> PyResult<()> {
1370 module.add_function(wrap_pyfunction!(monotonicity_index_py, module)?)?;
1371 module.add_function(wrap_pyfunction!(monotonicity_index_batch_py, module)?)?;
1372 module.add_class::<MonotonicityIndexStreamPy>()?;
1373 Ok(())
1374}
1375
1376#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1377fn parse_mode_js(value: &str) -> Result<MonotonicityIndexMode, JsValue> {
1378 MonotonicityIndexMode::parse(value)
1379 .ok_or_else(|| JsValue::from_str(&format!("Invalid mode: {value}")))
1380}
1381
1382#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1383#[derive(Serialize, Deserialize)]
1384pub struct MonotonicityIndexJsOutput {
1385 pub index: Vec<f64>,
1386 pub cumulative_mean: Vec<f64>,
1387 pub upper_bound: Vec<f64>,
1388}
1389
1390#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1391#[wasm_bindgen(js_name = "monotonicity_index_js")]
1392pub fn monotonicity_index_js(
1393 data: &[f64],
1394 length: usize,
1395 mode: &str,
1396 index_smooth: usize,
1397) -> Result<JsValue, JsValue> {
1398 let input = MonotonicityIndexInput::from_slice(
1399 data,
1400 MonotonicityIndexParams {
1401 length: Some(length),
1402 mode: Some(parse_mode_js(mode)?),
1403 index_smooth: Some(index_smooth),
1404 },
1405 );
1406 let output = monotonicity_index(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
1407 serde_wasm_bindgen::to_value(&MonotonicityIndexJsOutput {
1408 index: output.index,
1409 cumulative_mean: output.cumulative_mean,
1410 upper_bound: output.upper_bound,
1411 })
1412 .map_err(|e| JsValue::from_str(&e.to_string()))
1413}
1414
1415#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1416#[wasm_bindgen]
1417pub fn monotonicity_index_alloc(len: usize) -> *mut f64 {
1418 let mut vec = Vec::<f64>::with_capacity(len);
1419 let ptr = vec.as_mut_ptr();
1420 std::mem::forget(vec);
1421 ptr
1422}
1423
1424#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1425#[wasm_bindgen]
1426pub fn monotonicity_index_free(ptr: *mut f64, len: usize) {
1427 if !ptr.is_null() {
1428 unsafe {
1429 let _ = Vec::from_raw_parts(ptr, len, len);
1430 }
1431 }
1432}
1433
1434#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1435#[wasm_bindgen]
1436pub fn monotonicity_index_into(
1437 in_ptr: *const f64,
1438 index_out_ptr: *mut f64,
1439 cumulative_mean_out_ptr: *mut f64,
1440 upper_bound_out_ptr: *mut f64,
1441 len: usize,
1442 length: usize,
1443 mode: &str,
1444 index_smooth: usize,
1445) -> Result<(), JsValue> {
1446 if in_ptr.is_null()
1447 || index_out_ptr.is_null()
1448 || cumulative_mean_out_ptr.is_null()
1449 || upper_bound_out_ptr.is_null()
1450 {
1451 return Err(JsValue::from_str("Null pointer provided"));
1452 }
1453
1454 unsafe {
1455 let data = std::slice::from_raw_parts(in_ptr, len);
1456 let input = MonotonicityIndexInput::from_slice(
1457 data,
1458 MonotonicityIndexParams {
1459 length: Some(length),
1460 mode: Some(parse_mode_js(mode)?),
1461 index_smooth: Some(index_smooth),
1462 },
1463 );
1464 let index_out = std::slice::from_raw_parts_mut(index_out_ptr, len);
1465 let cumulative_mean_out = std::slice::from_raw_parts_mut(cumulative_mean_out_ptr, len);
1466 let upper_bound_out = std::slice::from_raw_parts_mut(upper_bound_out_ptr, len);
1467 monotonicity_index_into_slices(
1468 index_out,
1469 cumulative_mean_out,
1470 upper_bound_out,
1471 &input,
1472 Kernel::Auto,
1473 )
1474 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1475 }
1476 Ok(())
1477}
1478
1479#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1480#[derive(Serialize, Deserialize)]
1481pub struct MonotonicityIndexBatchJsConfig {
1482 pub length_range: Option<(usize, usize, usize)>,
1483 pub index_smooth_range: Option<(usize, usize, usize)>,
1484 pub mode: Option<MonotonicityIndexMode>,
1485}
1486
1487#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1488#[derive(Serialize, Deserialize)]
1489pub struct MonotonicityIndexBatchJsOutput {
1490 pub index: Vec<f64>,
1491 pub cumulative_mean: Vec<f64>,
1492 pub upper_bound: Vec<f64>,
1493 pub combos: Vec<MonotonicityIndexParams>,
1494 pub rows: usize,
1495 pub cols: usize,
1496}
1497
1498#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1499#[wasm_bindgen(js_name = "monotonicity_index_batch_js")]
1500pub fn monotonicity_index_batch_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
1501 let config: MonotonicityIndexBatchJsConfig =
1502 serde_wasm_bindgen::from_value(config).map_err(|e| JsValue::from_str(&e.to_string()))?;
1503 let sweep = MonotonicityIndexBatchRange {
1504 length: config
1505 .length_range
1506 .unwrap_or((DEFAULT_LENGTH, DEFAULT_LENGTH, 0)),
1507 index_smooth: config.index_smooth_range.unwrap_or((
1508 DEFAULT_INDEX_SMOOTH,
1509 DEFAULT_INDEX_SMOOTH,
1510 0,
1511 )),
1512 mode: config.mode.unwrap_or_default(),
1513 };
1514 let output = monotonicity_index_batch_with_kernel(data, &sweep, Kernel::Auto)
1515 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1516 serde_wasm_bindgen::to_value(&MonotonicityIndexBatchJsOutput {
1517 index: output.index,
1518 cumulative_mean: output.cumulative_mean,
1519 upper_bound: output.upper_bound,
1520 combos: output.combos,
1521 rows: output.rows,
1522 cols: output.cols,
1523 })
1524 .map_err(|e| JsValue::from_str(&e.to_string()))
1525}
1526
1527#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1528#[wasm_bindgen]
1529pub fn monotonicity_index_batch_into(
1530 in_ptr: *const f64,
1531 index_out_ptr: *mut f64,
1532 cumulative_mean_out_ptr: *mut f64,
1533 upper_bound_out_ptr: *mut f64,
1534 len: usize,
1535 length_start: usize,
1536 length_end: usize,
1537 length_step: usize,
1538 index_smooth_start: usize,
1539 index_smooth_end: usize,
1540 index_smooth_step: usize,
1541 mode: &str,
1542) -> Result<usize, JsValue> {
1543 if in_ptr.is_null()
1544 || index_out_ptr.is_null()
1545 || cumulative_mean_out_ptr.is_null()
1546 || upper_bound_out_ptr.is_null()
1547 {
1548 return Err(JsValue::from_str("Null pointer provided"));
1549 }
1550
1551 let sweep = MonotonicityIndexBatchRange {
1552 length: (length_start, length_end, length_step),
1553 index_smooth: (index_smooth_start, index_smooth_end, index_smooth_step),
1554 mode: parse_mode_js(mode)?,
1555 };
1556
1557 unsafe {
1558 let data = std::slice::from_raw_parts(in_ptr, len);
1559 let combos = expand_grid_monotonicity_index(&sweep)
1560 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1561 let rows = combos.len();
1562 let total = rows
1563 .checked_mul(len)
1564 .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1565 let index_out = std::slice::from_raw_parts_mut(index_out_ptr, total);
1566 let cumulative_mean_out = std::slice::from_raw_parts_mut(cumulative_mean_out_ptr, total);
1567 let upper_bound_out = std::slice::from_raw_parts_mut(upper_bound_out_ptr, total);
1568 let rows = monotonicity_index_batch_inner_into(
1569 data,
1570 &sweep,
1571 Kernel::Auto,
1572 false,
1573 index_out,
1574 cumulative_mean_out,
1575 upper_bound_out,
1576 )
1577 .map_err(|e| JsValue::from_str(&e.to_string()))?
1578 .len();
1579 Ok(rows)
1580 }
1581}
1582
1583#[cfg(test)]
1584mod tests {
1585 use super::*;
1586 use crate::utilities::data_loader::Candles;
1587
1588 fn sample_source(length: usize) -> Vec<f64> {
1589 let mut out = Vec::with_capacity(length);
1590 for i in 0..length {
1591 let x = i as f64;
1592 out.push(100.0 + x * 0.05 + (x * 0.17).sin() * 2.4 + (x * 0.04).cos() * 0.8);
1593 }
1594 out
1595 }
1596
1597 fn sample_candles(length: usize) -> Candles {
1598 let open: Vec<f64> = (0..length)
1599 .map(|i| 100.0 + i as f64 * 0.04 + (i as f64 * 0.08).sin())
1600 .collect();
1601 let close: Vec<f64> = open
1602 .iter()
1603 .enumerate()
1604 .map(|(i, &o)| o + (i as f64 * 0.13).cos() * 0.9)
1605 .collect();
1606 let high: Vec<f64> = open
1607 .iter()
1608 .zip(close.iter())
1609 .enumerate()
1610 .map(|(i, (&o, &c))| o.max(c) + 0.6 + (i as f64 * 0.05).sin().abs() * 0.2)
1611 .collect();
1612 let low: Vec<f64> = open
1613 .iter()
1614 .zip(close.iter())
1615 .enumerate()
1616 .map(|(i, (&o, &c))| o.min(c) - 0.6 - (i as f64 * 0.03).cos().abs() * 0.2)
1617 .collect();
1618 Candles::new(
1619 (0..length as i64).collect(),
1620 open,
1621 high,
1622 low,
1623 close,
1624 vec![1_000.0; length],
1625 )
1626 }
1627
1628 fn assert_series_eq(left: &[f64], right: &[f64], tol: f64) {
1629 assert_eq!(left.len(), right.len());
1630 for (&lhs, &rhs) in left.iter().zip(right.iter()) {
1631 if lhs.is_nan() && rhs.is_nan() {
1632 continue;
1633 }
1634 assert!((lhs - rhs).abs() <= tol, "lhs={lhs}, rhs={rhs}");
1635 }
1636 }
1637
1638 #[test]
1639 fn monotonicity_index_output_contract() {
1640 let data = sample_source(256);
1641 let out = monotonicity_index(&MonotonicityIndexInput::from_slice(
1642 &data,
1643 MonotonicityIndexParams::default(),
1644 ))
1645 .unwrap();
1646
1647 assert_eq!(out.index.len(), data.len());
1648 assert_eq!(out.cumulative_mean.len(), data.len());
1649 assert_eq!(out.upper_bound.len(), data.len());
1650 assert_eq!(out.index.iter().position(|v| v.is_finite()), Some(23));
1651 assert_eq!(
1652 out.cumulative_mean.iter().position(|v| v.is_finite()),
1653 Some(23)
1654 );
1655 assert_eq!(out.upper_bound.iter().position(|v| v.is_finite()), Some(23));
1656 assert!(out.index.last().copied().unwrap().is_finite());
1657 assert!(out.cumulative_mean.last().copied().unwrap().is_finite());
1658 assert!(out.upper_bound.last().copied().unwrap().is_finite());
1659 }
1660
1661 #[test]
1662 fn monotonicity_index_rejects_invalid_parameters() {
1663 let data = sample_source(64);
1664
1665 let err = monotonicity_index(&MonotonicityIndexInput::from_slice(
1666 &data,
1667 MonotonicityIndexParams {
1668 length: Some(1),
1669 ..MonotonicityIndexParams::default()
1670 },
1671 ))
1672 .unwrap_err();
1673 assert!(matches!(err, MonotonicityIndexError::InvalidLength { .. }));
1674
1675 let err = monotonicity_index(&MonotonicityIndexInput::from_slice(
1676 &data,
1677 MonotonicityIndexParams {
1678 index_smooth: Some(0),
1679 ..MonotonicityIndexParams::default()
1680 },
1681 ))
1682 .unwrap_err();
1683 assert!(matches!(
1684 err,
1685 MonotonicityIndexError::InvalidIndexSmooth { .. }
1686 ));
1687 }
1688
1689 #[test]
1690 fn monotonicity_index_builder_supports_candles() {
1691 let candles = sample_candles(220);
1692 let out = MonotonicityIndexBuilder::new()
1693 .mode(MonotonicityIndexMode::Complexity)
1694 .apply(&candles, "close")
1695 .unwrap();
1696 assert_eq!(out.index.len(), candles.close.len());
1697 assert_eq!(out.cumulative_mean.len(), candles.close.len());
1698 assert_eq!(out.upper_bound.len(), candles.close.len());
1699 assert!(out.index.last().copied().unwrap().is_finite());
1700 }
1701
1702 #[test]
1703 fn monotonicity_index_stream_matches_batch_with_reset() {
1704 let mut data = sample_source(240);
1705 data[120] = f64::NAN;
1706
1707 let batch = monotonicity_index(&MonotonicityIndexInput::from_slice(
1708 &data,
1709 MonotonicityIndexParams::default(),
1710 ))
1711 .unwrap();
1712 let mut stream =
1713 MonotonicityIndexStream::try_new(MonotonicityIndexParams::default()).unwrap();
1714
1715 let mut index = Vec::with_capacity(data.len());
1716 let mut cumulative_mean = Vec::with_capacity(data.len());
1717 let mut upper_bound = Vec::with_capacity(data.len());
1718 for &value in &data {
1719 if let Some((idx, mean, upper)) = stream.update(value) {
1720 index.push(idx);
1721 cumulative_mean.push(mean);
1722 upper_bound.push(upper);
1723 } else {
1724 index.push(f64::NAN);
1725 cumulative_mean.push(f64::NAN);
1726 upper_bound.push(f64::NAN);
1727 }
1728 }
1729
1730 assert_series_eq(&index, &batch.index, 1e-12);
1731 assert_series_eq(&cumulative_mean, &batch.cumulative_mean, 1e-12);
1732 assert_series_eq(&upper_bound, &batch.upper_bound, 1e-12);
1733 }
1734
1735 #[test]
1736 fn monotonicity_index_batch_single_param_matches_single() {
1737 let data = sample_source(192);
1738 let sweep = MonotonicityIndexBatchRange::default();
1739 let batch = monotonicity_index_batch_with_kernel(&data, &sweep, Kernel::Auto).unwrap();
1740 let single = monotonicity_index(&MonotonicityIndexInput::from_slice(
1741 &data,
1742 MonotonicityIndexParams::default(),
1743 ))
1744 .unwrap();
1745
1746 assert_eq!(batch.rows, 1);
1747 assert_eq!(batch.cols, data.len());
1748 let (index_row, cumulative_mean_row, upper_bound_row) = batch.row_slices(0).unwrap();
1749 assert_series_eq(index_row, &single.index, 1e-12);
1750 assert_series_eq(cumulative_mean_row, &single.cumulative_mean, 1e-12);
1751 assert_series_eq(upper_bound_row, &single.upper_bound, 1e-12);
1752 }
1753
1754 #[test]
1755 fn monotonicity_index_batch_metadata() {
1756 let data = sample_source(160);
1757 let sweep = MonotonicityIndexBatchRange {
1758 length: (18, 20, 2),
1759 index_smooth: (4, 5, 1),
1760 mode: MonotonicityIndexMode::Complexity,
1761 };
1762 let batch = monotonicity_index_batch_with_kernel(&data, &sweep, Kernel::Auto).unwrap();
1763
1764 assert_eq!(batch.rows, 4);
1765 assert_eq!(batch.cols, data.len());
1766 assert_eq!(batch.index.len(), 4 * data.len());
1767 assert_eq!(batch.cumulative_mean.len(), 4 * data.len());
1768 assert_eq!(batch.upper_bound.len(), 4 * data.len());
1769 assert_eq!(
1770 batch.row_for_params(&MonotonicityIndexParams {
1771 length: Some(20),
1772 mode: Some(MonotonicityIndexMode::Complexity),
1773 index_smooth: Some(5),
1774 }),
1775 Some(3)
1776 );
1777 }
1778}