1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9#[cfg(feature = "python")]
10use pyo3::wrap_pyfunction;
11
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use serde::{Deserialize, Serialize};
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use wasm_bindgen::prelude::*;
16
17use crate::utilities::data_loader::Candles;
18use crate::utilities::enums::Kernel;
19use crate::utilities::helpers::{
20 alloc_with_nan_prefix, detect_best_batch_kernel, init_matrix_prefixes, make_uninit_matrix,
21};
22#[cfg(feature = "python")]
23use crate::utilities::kernel_validation::validate_kernel;
24#[cfg(not(target_arch = "wasm32"))]
25use rayon::prelude::*;
26use std::mem::ManuallyDrop;
27use thiserror::Error;
28
29const DEFAULT_THRESHOLD_LEVEL: f64 = 0.35;
30const DEFAULT_EMA_PERIOD: usize = 14;
31
32#[derive(Debug, Clone)]
33pub enum DailyFactorData<'a> {
34 Candles {
35 candles: &'a Candles,
36 },
37 Slices {
38 open: &'a [f64],
39 high: &'a [f64],
40 low: &'a [f64],
41 close: &'a [f64],
42 },
43}
44
45#[derive(Debug, Clone)]
46pub struct DailyFactorOutput {
47 pub value: Vec<f64>,
48 pub ema: Vec<f64>,
49 pub signal: Vec<f64>,
50}
51
52#[derive(Debug, Clone)]
53#[cfg_attr(
54 all(target_arch = "wasm32", feature = "wasm"),
55 derive(Serialize, Deserialize)
56)]
57pub struct DailyFactorParams {
58 pub threshold_level: Option<f64>,
59}
60
61impl Default for DailyFactorParams {
62 fn default() -> Self {
63 Self {
64 threshold_level: Some(DEFAULT_THRESHOLD_LEVEL),
65 }
66 }
67}
68
69#[derive(Debug, Clone)]
70pub struct DailyFactorInput<'a> {
71 pub data: DailyFactorData<'a>,
72 pub params: DailyFactorParams,
73}
74
75impl<'a> DailyFactorInput<'a> {
76 #[inline]
77 pub fn from_candles(candles: &'a Candles, params: DailyFactorParams) -> Self {
78 Self {
79 data: DailyFactorData::Candles { candles },
80 params,
81 }
82 }
83
84 #[inline]
85 pub fn from_slices(
86 open: &'a [f64],
87 high: &'a [f64],
88 low: &'a [f64],
89 close: &'a [f64],
90 params: DailyFactorParams,
91 ) -> Self {
92 Self {
93 data: DailyFactorData::Slices {
94 open,
95 high,
96 low,
97 close,
98 },
99 params,
100 }
101 }
102
103 #[inline]
104 pub fn with_default_candles(candles: &'a Candles) -> Self {
105 Self::from_candles(candles, DailyFactorParams::default())
106 }
107}
108
109#[derive(Copy, Clone, Debug)]
110pub struct DailyFactorBuilder {
111 threshold_level: Option<f64>,
112 kernel: Kernel,
113}
114
115impl Default for DailyFactorBuilder {
116 fn default() -> Self {
117 Self {
118 threshold_level: None,
119 kernel: Kernel::Auto,
120 }
121 }
122}
123
124impl DailyFactorBuilder {
125 #[inline(always)]
126 pub fn new() -> Self {
127 Self::default()
128 }
129
130 #[inline(always)]
131 pub fn threshold_level(mut self, value: f64) -> Self {
132 self.threshold_level = Some(value);
133 self
134 }
135
136 #[inline(always)]
137 pub fn kernel(mut self, value: Kernel) -> Self {
138 self.kernel = value;
139 self
140 }
141
142 #[inline(always)]
143 pub fn apply(self, candles: &Candles) -> Result<DailyFactorOutput, DailyFactorError> {
144 let input = DailyFactorInput::from_candles(
145 candles,
146 DailyFactorParams {
147 threshold_level: self.threshold_level,
148 },
149 );
150 daily_factor_with_kernel(&input, self.kernel)
151 }
152
153 #[inline(always)]
154 pub fn apply_slices(
155 self,
156 open: &[f64],
157 high: &[f64],
158 low: &[f64],
159 close: &[f64],
160 ) -> Result<DailyFactorOutput, DailyFactorError> {
161 let input = DailyFactorInput::from_slices(
162 open,
163 high,
164 low,
165 close,
166 DailyFactorParams {
167 threshold_level: self.threshold_level,
168 },
169 );
170 daily_factor_with_kernel(&input, self.kernel)
171 }
172
173 #[inline(always)]
174 pub fn into_stream(self) -> Result<DailyFactorStream, DailyFactorError> {
175 DailyFactorStream::try_new(DailyFactorParams {
176 threshold_level: self.threshold_level,
177 })
178 }
179}
180
181#[derive(Debug, Error)]
182pub enum DailyFactorError {
183 #[error("daily_factor: Input data slice is empty.")]
184 EmptyInputData,
185 #[error("daily_factor: All values are NaN.")]
186 AllValuesNaN,
187 #[error("daily_factor: Inconsistent slice lengths: open={open_len}, high={high_len}, low={low_len}, close={close_len}")]
188 InconsistentSliceLengths {
189 open_len: usize,
190 high_len: usize,
191 low_len: usize,
192 close_len: usize,
193 },
194 #[error("daily_factor: Invalid threshold_level: {threshold_level}")]
195 InvalidThresholdLevel { threshold_level: f64 },
196 #[error("daily_factor: Output length mismatch: expected={expected}, got={got}")]
197 OutputLengthMismatch { expected: usize, got: usize },
198 #[error("daily_factor: Invalid range: start={start}, end={end}, step={step}")]
199 InvalidRange {
200 start: String,
201 end: String,
202 step: String,
203 },
204 #[error("daily_factor: Invalid kernel for batch: {0:?}")]
205 InvalidKernelForBatch(Kernel),
206}
207
208#[derive(Clone, Copy, Debug)]
209struct ResolvedParams {
210 threshold_level: f64,
211}
212
213#[inline(always)]
214fn ema_alpha() -> f64 {
215 2.0 / (DEFAULT_EMA_PERIOD as f64 + 1.0)
216}
217
218#[inline(always)]
219fn extract_ohlc<'a>(
220 input: &'a DailyFactorInput<'a>,
221) -> Result<(&'a [f64], &'a [f64], &'a [f64], &'a [f64]), DailyFactorError> {
222 let (open, high, low, close) = match &input.data {
223 DailyFactorData::Candles { candles } => (
224 candles.open.as_slice(),
225 candles.high.as_slice(),
226 candles.low.as_slice(),
227 candles.close.as_slice(),
228 ),
229 DailyFactorData::Slices {
230 open,
231 high,
232 low,
233 close,
234 } => (*open, *high, *low, *close),
235 };
236 if open.is_empty() || high.is_empty() || low.is_empty() || close.is_empty() {
237 return Err(DailyFactorError::EmptyInputData);
238 }
239 if open.len() != high.len() || open.len() != low.len() || open.len() != close.len() {
240 return Err(DailyFactorError::InconsistentSliceLengths {
241 open_len: open.len(),
242 high_len: high.len(),
243 low_len: low.len(),
244 close_len: close.len(),
245 });
246 }
247 Ok((open, high, low, close))
248}
249
250#[inline(always)]
251fn first_valid_ohlc(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Option<usize> {
252 (0..close.len()).find(|&i| {
253 open[i].is_finite() && high[i].is_finite() && low[i].is_finite() && close[i].is_finite()
254 })
255}
256
257#[inline(always)]
258fn resolve_params(params: &DailyFactorParams) -> Result<ResolvedParams, DailyFactorError> {
259 let threshold_level = params.threshold_level.unwrap_or(DEFAULT_THRESHOLD_LEVEL);
260 if !threshold_level.is_finite() || !(0.0..=1.0).contains(&threshold_level) {
261 return Err(DailyFactorError::InvalidThresholdLevel { threshold_level });
262 }
263 Ok(ResolvedParams { threshold_level })
264}
265
266#[inline(always)]
267fn validate_input<'a>(
268 input: &'a DailyFactorInput<'a>,
269 kernel: Kernel,
270) -> Result<
271 (
272 &'a [f64],
273 &'a [f64],
274 &'a [f64],
275 &'a [f64],
276 ResolvedParams,
277 usize,
278 Kernel,
279 ),
280 DailyFactorError,
281> {
282 let (open, high, low, close) = extract_ohlc(input)?;
283 let params = resolve_params(&input.params)?;
284 let first = first_valid_ohlc(open, high, low, close).ok_or(DailyFactorError::AllValuesNaN)?;
285 Ok((open, high, low, close, params, first, kernel.to_non_batch()))
286}
287
288#[inline(always)]
289fn compute_signal(value: f64, ema: f64, close: f64, threshold_level: f64) -> f64 {
290 if !(value.is_finite() && ema.is_finite() && close.is_finite()) {
291 return f64::NAN;
292 }
293 if value > threshold_level && close > ema {
294 2.0
295 } else if value > threshold_level && close < ema {
296 -2.0
297 } else if close > ema {
298 1.0
299 } else if close < ema {
300 -1.0
301 } else {
302 0.0
303 }
304}
305
306#[inline(always)]
307fn compute_base_into(
308 open: &[f64],
309 high: &[f64],
310 low: &[f64],
311 close: &[f64],
312 first: usize,
313 out_value: &mut [f64],
314 out_ema: &mut [f64],
315) -> Result<(), DailyFactorError> {
316 let len = close.len();
317 if out_value.len() != len {
318 return Err(DailyFactorError::OutputLengthMismatch {
319 expected: len,
320 got: out_value.len(),
321 });
322 }
323 if out_ema.len() != len {
324 return Err(DailyFactorError::OutputLengthMismatch {
325 expected: len,
326 got: out_ema.len(),
327 });
328 }
329
330 let alpha = ema_alpha();
331 let mut prev_open = f64::NAN;
332 let mut prev_high = f64::NAN;
333 let mut prev_low = f64::NAN;
334 let mut prev_close = f64::NAN;
335 let mut prev_ema = f64::NAN;
336 let mut has_prev = false;
337
338 for i in first..len {
339 let o = open[i];
340 let h = high[i];
341 let l = low[i];
342 let c = close[i];
343 if !(o.is_finite() && h.is_finite() && l.is_finite() && c.is_finite()) {
344 out_value[i] = f64::NAN;
345 out_ema[i] = f64::NAN;
346 continue;
347 }
348
349 let ema = if prev_ema.is_finite() {
350 prev_ema + alpha * (c - prev_ema)
351 } else {
352 c
353 };
354 let value = if has_prev {
355 let range = prev_high - prev_low;
356 if range.is_finite() && range != 0.0 {
357 (prev_open - prev_close).abs() / range
358 } else {
359 0.0
360 }
361 } else {
362 0.0
363 };
364
365 out_value[i] = value;
366 out_ema[i] = ema;
367 prev_open = o;
368 prev_high = h;
369 prev_low = l;
370 prev_close = c;
371 prev_ema = ema;
372 has_prev = true;
373 }
374
375 Ok(())
376}
377
378#[inline(always)]
379fn compute_signal_into(
380 value: &[f64],
381 ema: &[f64],
382 close: &[f64],
383 threshold_level: f64,
384 out_signal: &mut [f64],
385) -> Result<(), DailyFactorError> {
386 let len = close.len();
387 if value.len() != len || ema.len() != len {
388 return Err(DailyFactorError::OutputLengthMismatch {
389 expected: len,
390 got: value.len().min(ema.len()),
391 });
392 }
393 if out_signal.len() != len {
394 return Err(DailyFactorError::OutputLengthMismatch {
395 expected: len,
396 got: out_signal.len(),
397 });
398 }
399 for i in 0..len {
400 out_signal[i] = compute_signal(value[i], ema[i], close[i], threshold_level);
401 }
402 Ok(())
403}
404
405#[inline]
406pub fn daily_factor(input: &DailyFactorInput) -> Result<DailyFactorOutput, DailyFactorError> {
407 daily_factor_with_kernel(input, Kernel::Auto)
408}
409
410#[inline]
411pub fn daily_factor_with_kernel(
412 input: &DailyFactorInput,
413 kernel: Kernel,
414) -> Result<DailyFactorOutput, DailyFactorError> {
415 let (open, high, low, close, params, first, _kernel) = validate_input(input, kernel)?;
416 let mut value = alloc_with_nan_prefix(close.len(), first);
417 let mut ema = alloc_with_nan_prefix(close.len(), first);
418 let mut signal = alloc_with_nan_prefix(close.len(), first);
419 compute_base_into(open, high, low, close, first, &mut value, &mut ema)?;
420 compute_signal_into(&value, &ema, close, params.threshold_level, &mut signal)?;
421 Ok(DailyFactorOutput { value, ema, signal })
422}
423
424#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
425#[inline]
426pub fn daily_factor_into(
427 out_value: &mut [f64],
428 out_ema: &mut [f64],
429 out_signal: &mut [f64],
430 input: &DailyFactorInput,
431 kernel: Kernel,
432) -> Result<(), DailyFactorError> {
433 daily_factor_into_slice(out_value, out_ema, out_signal, input, kernel)
434}
435
436#[inline]
437pub fn daily_factor_into_slice(
438 out_value: &mut [f64],
439 out_ema: &mut [f64],
440 out_signal: &mut [f64],
441 input: &DailyFactorInput,
442 kernel: Kernel,
443) -> Result<(), DailyFactorError> {
444 let (open, high, low, close, params, first, _kernel) = validate_input(input, kernel)?;
445 out_value.fill(f64::NAN);
446 out_ema.fill(f64::NAN);
447 out_signal.fill(f64::NAN);
448 compute_base_into(open, high, low, close, first, out_value, out_ema)?;
449 compute_signal_into(
450 out_value,
451 out_ema,
452 close,
453 params.threshold_level,
454 out_signal,
455 )
456}
457
458#[derive(Clone, Debug)]
459pub struct DailyFactorStream {
460 params: ResolvedParams,
461 prev_open: f64,
462 prev_high: f64,
463 prev_low: f64,
464 prev_close: f64,
465 prev_ema: f64,
466 has_prev: bool,
467}
468
469impl DailyFactorStream {
470 pub fn try_new(params: DailyFactorParams) -> Result<Self, DailyFactorError> {
471 Ok(Self {
472 params: resolve_params(¶ms)?,
473 prev_open: f64::NAN,
474 prev_high: f64::NAN,
475 prev_low: f64::NAN,
476 prev_close: f64::NAN,
477 prev_ema: f64::NAN,
478 has_prev: false,
479 })
480 }
481
482 #[inline(always)]
483 pub fn update(&mut self, open: f64, high: f64, low: f64, close: f64) -> (f64, f64, f64) {
484 if !(open.is_finite() && high.is_finite() && low.is_finite() && close.is_finite()) {
485 return (f64::NAN, f64::NAN, f64::NAN);
486 }
487
488 let ema = if self.prev_ema.is_finite() {
489 self.prev_ema + ema_alpha() * (close - self.prev_ema)
490 } else {
491 close
492 };
493 let value = if self.has_prev {
494 let range = self.prev_high - self.prev_low;
495 if range.is_finite() && range != 0.0 {
496 (self.prev_open - self.prev_close).abs() / range
497 } else {
498 0.0
499 }
500 } else {
501 0.0
502 };
503 let signal = compute_signal(value, ema, close, self.params.threshold_level);
504
505 self.prev_open = open;
506 self.prev_high = high;
507 self.prev_low = low;
508 self.prev_close = close;
509 self.prev_ema = ema;
510 self.has_prev = true;
511
512 (value, ema, signal)
513 }
514}
515
516#[derive(Clone, Copy, Debug)]
517pub struct DailyFactorBatchRange {
518 pub threshold_level: (f64, f64, f64),
519}
520
521#[derive(Clone, Debug)]
522pub struct DailyFactorBatchOutput {
523 pub value: Vec<f64>,
524 pub ema: Vec<f64>,
525 pub signal: Vec<f64>,
526 pub combos: Vec<DailyFactorParams>,
527 pub rows: usize,
528 pub cols: usize,
529}
530
531#[derive(Copy, Clone, Debug)]
532pub struct DailyFactorBatchBuilder {
533 threshold_level: (f64, f64, f64),
534 kernel: Kernel,
535}
536
537impl Default for DailyFactorBatchBuilder {
538 fn default() -> Self {
539 Self {
540 threshold_level: (DEFAULT_THRESHOLD_LEVEL, DEFAULT_THRESHOLD_LEVEL, 0.0),
541 kernel: Kernel::Auto,
542 }
543 }
544}
545
546impl DailyFactorBatchBuilder {
547 #[inline(always)]
548 pub fn new() -> Self {
549 Self::default()
550 }
551
552 #[inline(always)]
553 pub fn threshold_level_range(mut self, value: (f64, f64, f64)) -> Self {
554 self.threshold_level = value;
555 self
556 }
557
558 #[inline(always)]
559 pub fn kernel(mut self, value: Kernel) -> Self {
560 self.kernel = value;
561 self
562 }
563
564 #[inline(always)]
565 pub fn apply(self, candles: &Candles) -> Result<DailyFactorBatchOutput, DailyFactorError> {
566 daily_factor_batch_with_kernel(
567 candles.open.as_slice(),
568 candles.high.as_slice(),
569 candles.low.as_slice(),
570 candles.close.as_slice(),
571 &DailyFactorBatchRange {
572 threshold_level: self.threshold_level,
573 },
574 self.kernel,
575 )
576 }
577
578 #[inline(always)]
579 pub fn apply_slices(
580 self,
581 open: &[f64],
582 high: &[f64],
583 low: &[f64],
584 close: &[f64],
585 ) -> Result<DailyFactorBatchOutput, DailyFactorError> {
586 daily_factor_batch_with_kernel(
587 open,
588 high,
589 low,
590 close,
591 &DailyFactorBatchRange {
592 threshold_level: self.threshold_level,
593 },
594 self.kernel,
595 )
596 }
597}
598
599#[inline(always)]
600fn expand_float_range(start: f64, end: f64, step: f64) -> Result<Vec<f64>, DailyFactorError> {
601 if !start.is_finite() || !end.is_finite() || !step.is_finite() {
602 return Err(DailyFactorError::InvalidRange {
603 start: start.to_string(),
604 end: end.to_string(),
605 step: step.to_string(),
606 });
607 }
608 if step == 0.0 {
609 if (start - end).abs() > 1e-12 {
610 return Err(DailyFactorError::InvalidRange {
611 start: start.to_string(),
612 end: end.to_string(),
613 step: step.to_string(),
614 });
615 }
616 return Ok(vec![start]);
617 }
618 if start > end || step < 0.0 {
619 return Err(DailyFactorError::InvalidRange {
620 start: start.to_string(),
621 end: end.to_string(),
622 step: step.to_string(),
623 });
624 }
625 let mut out = Vec::new();
626 let mut current = start;
627 while current <= end + 1e-12 {
628 out.push(current);
629 if out.len() > 1_000_000 {
630 return Err(DailyFactorError::InvalidRange {
631 start: start.to_string(),
632 end: end.to_string(),
633 step: step.to_string(),
634 });
635 }
636 current += step;
637 }
638 Ok(out)
639}
640
641pub fn expand_grid(
642 sweep: &DailyFactorBatchRange,
643) -> Result<Vec<DailyFactorParams>, DailyFactorError> {
644 let levels = expand_float_range(
645 sweep.threshold_level.0,
646 sweep.threshold_level.1,
647 sweep.threshold_level.2,
648 )?;
649 let mut out = Vec::with_capacity(levels.len());
650 for threshold_level in levels {
651 out.push(DailyFactorParams {
652 threshold_level: Some(threshold_level),
653 });
654 }
655 Ok(out)
656}
657
658#[inline(always)]
659fn validate_raw_slices(
660 open: &[f64],
661 high: &[f64],
662 low: &[f64],
663 close: &[f64],
664) -> Result<usize, DailyFactorError> {
665 if open.is_empty() || high.is_empty() || low.is_empty() || close.is_empty() {
666 return Err(DailyFactorError::EmptyInputData);
667 }
668 if open.len() != high.len() || open.len() != low.len() || open.len() != close.len() {
669 return Err(DailyFactorError::InconsistentSliceLengths {
670 open_len: open.len(),
671 high_len: high.len(),
672 low_len: low.len(),
673 close_len: close.len(),
674 });
675 }
676 first_valid_ohlc(open, high, low, close).ok_or(DailyFactorError::AllValuesNaN)
677}
678
679#[inline(always)]
680fn batch_shape(rows: usize, cols: usize) -> Result<usize, DailyFactorError> {
681 rows.checked_mul(cols)
682 .ok_or_else(|| DailyFactorError::InvalidRange {
683 start: rows.to_string(),
684 end: cols.to_string(),
685 step: "rows*cols".to_string(),
686 })
687}
688
689pub fn daily_factor_batch_with_kernel(
690 open: &[f64],
691 high: &[f64],
692 low: &[f64],
693 close: &[f64],
694 sweep: &DailyFactorBatchRange,
695 kernel: Kernel,
696) -> Result<DailyFactorBatchOutput, DailyFactorError> {
697 let batch_kernel = match kernel {
698 Kernel::Auto => detect_best_batch_kernel(),
699 other if other.is_batch() => other,
700 _ => return Err(DailyFactorError::InvalidKernelForBatch(kernel)),
701 };
702 daily_factor_batch_par_slice(open, high, low, close, sweep, batch_kernel.to_non_batch())
703}
704
705#[inline(always)]
706pub fn daily_factor_batch_slice(
707 open: &[f64],
708 high: &[f64],
709 low: &[f64],
710 close: &[f64],
711 sweep: &DailyFactorBatchRange,
712 kernel: Kernel,
713) -> Result<DailyFactorBatchOutput, DailyFactorError> {
714 daily_factor_batch_inner(open, high, low, close, sweep, kernel, false)
715}
716
717#[inline(always)]
718pub fn daily_factor_batch_par_slice(
719 open: &[f64],
720 high: &[f64],
721 low: &[f64],
722 close: &[f64],
723 sweep: &DailyFactorBatchRange,
724 kernel: Kernel,
725) -> Result<DailyFactorBatchOutput, DailyFactorError> {
726 daily_factor_batch_inner(open, high, low, close, sweep, kernel, true)
727}
728
729fn daily_factor_batch_inner(
730 open: &[f64],
731 high: &[f64],
732 low: &[f64],
733 close: &[f64],
734 sweep: &DailyFactorBatchRange,
735 kernel: Kernel,
736 parallel: bool,
737) -> Result<DailyFactorBatchOutput, DailyFactorError> {
738 let combos = expand_grid(sweep)?;
739 let first = validate_raw_slices(open, high, low, close)?;
740 let rows = combos.len();
741 let cols = close.len();
742 let total = batch_shape(rows, cols)?;
743 let warmups = vec![first; rows];
744
745 let mut value_buf = make_uninit_matrix(rows, cols);
746 let mut ema_buf = make_uninit_matrix(rows, cols);
747 let mut signal_buf = make_uninit_matrix(rows, cols);
748 init_matrix_prefixes(&mut value_buf, cols, &warmups);
749 init_matrix_prefixes(&mut ema_buf, cols, &warmups);
750 init_matrix_prefixes(&mut signal_buf, cols, &warmups);
751
752 let mut value_guard = ManuallyDrop::new(value_buf);
753 let mut ema_guard = ManuallyDrop::new(ema_buf);
754 let mut signal_guard = ManuallyDrop::new(signal_buf);
755 let out_value: &mut [f64] = unsafe {
756 core::slice::from_raw_parts_mut(value_guard.as_mut_ptr() as *mut f64, value_guard.len())
757 };
758 let out_ema: &mut [f64] = unsafe {
759 core::slice::from_raw_parts_mut(ema_guard.as_mut_ptr() as *mut f64, ema_guard.len())
760 };
761 let out_signal: &mut [f64] = unsafe {
762 core::slice::from_raw_parts_mut(signal_guard.as_mut_ptr() as *mut f64, signal_guard.len())
763 };
764
765 daily_factor_batch_inner_into(
766 open, high, low, close, sweep, kernel, parallel, out_value, out_ema, out_signal,
767 )?;
768
769 let value = unsafe {
770 Vec::from_raw_parts(
771 value_guard.as_mut_ptr() as *mut f64,
772 total,
773 value_guard.capacity(),
774 )
775 };
776 let ema = unsafe {
777 Vec::from_raw_parts(
778 ema_guard.as_mut_ptr() as *mut f64,
779 total,
780 ema_guard.capacity(),
781 )
782 };
783 let signal = unsafe {
784 Vec::from_raw_parts(
785 signal_guard.as_mut_ptr() as *mut f64,
786 total,
787 signal_guard.capacity(),
788 )
789 };
790
791 Ok(DailyFactorBatchOutput {
792 value,
793 ema,
794 signal,
795 combos,
796 rows,
797 cols,
798 })
799}
800
801pub fn daily_factor_batch_into_slice(
802 out_value: &mut [f64],
803 out_ema: &mut [f64],
804 out_signal: &mut [f64],
805 open: &[f64],
806 high: &[f64],
807 low: &[f64],
808 close: &[f64],
809 sweep: &DailyFactorBatchRange,
810 kernel: Kernel,
811) -> Result<(), DailyFactorError> {
812 daily_factor_batch_inner_into(
813 open, high, low, close, sweep, kernel, false, out_value, out_ema, out_signal,
814 )?;
815 Ok(())
816}
817
818fn daily_factor_batch_inner_into(
819 open: &[f64],
820 high: &[f64],
821 low: &[f64],
822 close: &[f64],
823 sweep: &DailyFactorBatchRange,
824 _kernel: Kernel,
825 parallel: bool,
826 out_value: &mut [f64],
827 out_ema: &mut [f64],
828 out_signal: &mut [f64],
829) -> Result<Vec<DailyFactorParams>, DailyFactorError> {
830 let combos = expand_grid(sweep)?;
831 let first = validate_raw_slices(open, high, low, close)?;
832 let rows = combos.len();
833 let cols = close.len();
834 let total = batch_shape(rows, cols)?;
835 if out_value.len() != total {
836 return Err(DailyFactorError::OutputLengthMismatch {
837 expected: total,
838 got: out_value.len(),
839 });
840 }
841 if out_ema.len() != total {
842 return Err(DailyFactorError::OutputLengthMismatch {
843 expected: total,
844 got: out_ema.len(),
845 });
846 }
847 if out_signal.len() != total {
848 return Err(DailyFactorError::OutputLengthMismatch {
849 expected: total,
850 got: out_signal.len(),
851 });
852 }
853
854 let mut base_value = alloc_with_nan_prefix(cols, first);
855 let mut base_ema = alloc_with_nan_prefix(cols, first);
856 compute_base_into(
857 open,
858 high,
859 low,
860 close,
861 first,
862 &mut base_value,
863 &mut base_ema,
864 )?;
865 let thresholds: Vec<f64> = combos
866 .iter()
867 .map(|combo| resolve_params(combo).map(|p| p.threshold_level))
868 .collect::<Result<Vec<_>, _>>()?;
869
870 let do_row =
871 |row: usize, value_dst: &mut [f64], ema_dst: &mut [f64], signal_dst: &mut [f64]| {
872 value_dst.copy_from_slice(&base_value);
873 ema_dst.copy_from_slice(&base_ema);
874 compute_signal_into(&base_value, &base_ema, close, thresholds[row], signal_dst)
875 };
876
877 if parallel {
878 #[cfg(not(target_arch = "wasm32"))]
879 {
880 out_value
881 .par_chunks_mut(cols)
882 .zip(out_ema.par_chunks_mut(cols))
883 .zip(out_signal.par_chunks_mut(cols))
884 .enumerate()
885 .try_for_each(|(row, ((value_dst, ema_dst), signal_dst))| {
886 do_row(row, value_dst, ema_dst, signal_dst)
887 })?;
888 }
889 #[cfg(target_arch = "wasm32")]
890 {
891 for row in 0..rows {
892 let start = row * cols;
893 let end = start + cols;
894 do_row(
895 row,
896 &mut out_value[start..end],
897 &mut out_ema[start..end],
898 &mut out_signal[start..end],
899 )?;
900 }
901 }
902 } else {
903 for row in 0..rows {
904 let start = row * cols;
905 let end = start + cols;
906 do_row(
907 row,
908 &mut out_value[start..end],
909 &mut out_ema[start..end],
910 &mut out_signal[start..end],
911 )?;
912 }
913 }
914
915 Ok(combos)
916}
917
918#[cfg(feature = "python")]
919#[pyfunction(name = "daily_factor")]
920#[pyo3(signature = (open, high, low, close, threshold_level=0.35, kernel=None))]
921pub fn daily_factor_py<'py>(
922 py: Python<'py>,
923 open: PyReadonlyArray1<'py, f64>,
924 high: PyReadonlyArray1<'py, f64>,
925 low: PyReadonlyArray1<'py, f64>,
926 close: PyReadonlyArray1<'py, f64>,
927 threshold_level: f64,
928 kernel: Option<&str>,
929) -> PyResult<Bound<'py, PyDict>> {
930 let open = open.as_slice()?;
931 let high = high.as_slice()?;
932 let low = low.as_slice()?;
933 let close = close.as_slice()?;
934 let input = DailyFactorInput::from_slices(
935 open,
936 high,
937 low,
938 close,
939 DailyFactorParams {
940 threshold_level: Some(threshold_level),
941 },
942 );
943 let kernel = validate_kernel(kernel, false)?;
944 let out = py
945 .allow_threads(|| daily_factor_with_kernel(&input, kernel))
946 .map_err(|e| PyValueError::new_err(e.to_string()))?;
947 let dict = PyDict::new(py);
948 dict.set_item("value", out.value.into_pyarray(py))?;
949 dict.set_item("ema", out.ema.into_pyarray(py))?;
950 dict.set_item("signal", out.signal.into_pyarray(py))?;
951 Ok(dict)
952}
953
954#[cfg(feature = "python")]
955#[pyclass(name = "DailyFactorStream")]
956pub struct DailyFactorStreamPy {
957 stream: DailyFactorStream,
958}
959
960#[cfg(feature = "python")]
961#[pymethods]
962impl DailyFactorStreamPy {
963 #[new]
964 #[pyo3(signature = (threshold_level=0.35))]
965 fn new(threshold_level: f64) -> PyResult<Self> {
966 let stream = DailyFactorStream::try_new(DailyFactorParams {
967 threshold_level: Some(threshold_level),
968 })
969 .map_err(|e| PyValueError::new_err(e.to_string()))?;
970 Ok(Self { stream })
971 }
972
973 fn update(&mut self, open: f64, high: f64, low: f64, close: f64) -> (f64, f64, f64) {
974 self.stream.update(open, high, low, close)
975 }
976}
977
978#[cfg(feature = "python")]
979#[pyfunction(name = "daily_factor_batch")]
980#[pyo3(signature = (open, high, low, close, threshold_level_range=(0.35,0.35,0.0), kernel=None))]
981pub fn daily_factor_batch_py<'py>(
982 py: Python<'py>,
983 open: PyReadonlyArray1<'py, f64>,
984 high: PyReadonlyArray1<'py, f64>,
985 low: PyReadonlyArray1<'py, f64>,
986 close: PyReadonlyArray1<'py, f64>,
987 threshold_level_range: (f64, f64, f64),
988 kernel: Option<&str>,
989) -> PyResult<Bound<'py, PyDict>> {
990 let open = open.as_slice()?;
991 let high = high.as_slice()?;
992 let low = low.as_slice()?;
993 let close = close.as_slice()?;
994 let sweep = DailyFactorBatchRange {
995 threshold_level: threshold_level_range,
996 };
997 let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
998 let rows = combos.len();
999 let cols = close.len();
1000 let total = rows
1001 .checked_mul(cols)
1002 .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1003
1004 let out_value = unsafe { PyArray1::<f64>::new(py, [total], false) };
1005 let out_ema = unsafe { PyArray1::<f64>::new(py, [total], false) };
1006 let out_signal = unsafe { PyArray1::<f64>::new(py, [total], false) };
1007 let value_slice = unsafe { out_value.as_slice_mut()? };
1008 let ema_slice = unsafe { out_ema.as_slice_mut()? };
1009 let signal_slice = unsafe { out_signal.as_slice_mut()? };
1010 let kernel = validate_kernel(kernel, true)?;
1011
1012 py.allow_threads(|| {
1013 let batch_kernel = match kernel {
1014 Kernel::Auto => detect_best_batch_kernel(),
1015 other => other,
1016 };
1017 daily_factor_batch_inner_into(
1018 open,
1019 high,
1020 low,
1021 close,
1022 &sweep,
1023 batch_kernel.to_non_batch(),
1024 true,
1025 value_slice,
1026 ema_slice,
1027 signal_slice,
1028 )
1029 })
1030 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1031
1032 let dict = PyDict::new(py);
1033 dict.set_item("value", out_value.reshape((rows, cols))?)?;
1034 dict.set_item("ema", out_ema.reshape((rows, cols))?)?;
1035 dict.set_item("signal", out_signal.reshape((rows, cols))?)?;
1036 dict.set_item(
1037 "threshold_levels",
1038 combos
1039 .iter()
1040 .map(|combo| combo.threshold_level.unwrap_or(DEFAULT_THRESHOLD_LEVEL))
1041 .collect::<Vec<_>>()
1042 .into_pyarray(py),
1043 )?;
1044 dict.set_item("rows", rows)?;
1045 dict.set_item("cols", cols)?;
1046 Ok(dict)
1047}
1048
1049#[cfg(feature = "python")]
1050pub fn register_daily_factor_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
1051 m.add_function(wrap_pyfunction!(daily_factor_py, m)?)?;
1052 m.add_function(wrap_pyfunction!(daily_factor_batch_py, m)?)?;
1053 m.add_class::<DailyFactorStreamPy>()?;
1054 Ok(())
1055}
1056
1057#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1058#[derive(Serialize, Deserialize)]
1059pub struct DailyFactorJsOutput {
1060 pub value: Vec<f64>,
1061 pub ema: Vec<f64>,
1062 pub signal: Vec<f64>,
1063}
1064
1065#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1066#[wasm_bindgen(js_name = "daily_factor_js")]
1067pub fn daily_factor_js(
1068 open: &[f64],
1069 high: &[f64],
1070 low: &[f64],
1071 close: &[f64],
1072 threshold_level: f64,
1073) -> Result<JsValue, JsValue> {
1074 let input = DailyFactorInput::from_slices(
1075 open,
1076 high,
1077 low,
1078 close,
1079 DailyFactorParams {
1080 threshold_level: Some(threshold_level),
1081 },
1082 );
1083 let out = daily_factor_with_kernel(&input, Kernel::Auto)
1084 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1085 serde_wasm_bindgen::to_value(&DailyFactorJsOutput {
1086 value: out.value,
1087 ema: out.ema,
1088 signal: out.signal,
1089 })
1090 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1091}
1092
1093#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1094#[derive(Serialize, Deserialize)]
1095pub struct DailyFactorBatchConfig {
1096 pub threshold_level_range: Vec<f64>,
1097}
1098
1099#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1100#[derive(Serialize, Deserialize)]
1101pub struct DailyFactorBatchJsOutput {
1102 pub value: Vec<f64>,
1103 pub ema: Vec<f64>,
1104 pub signal: Vec<f64>,
1105 pub threshold_levels: Vec<f64>,
1106 pub rows: usize,
1107 pub cols: usize,
1108}
1109
1110#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1111fn js_vec3_to_f64(name: &str, values: &[f64]) -> Result<(f64, f64, f64), JsValue> {
1112 if values.len() != 3 {
1113 return Err(JsValue::from_str(&format!(
1114 "Invalid config: {name} must have exactly 3 elements [start, end, step]"
1115 )));
1116 }
1117 if !values.iter().all(|v| v.is_finite()) {
1118 return Err(JsValue::from_str(&format!(
1119 "Invalid config: {name} entries must be finite numbers"
1120 )));
1121 }
1122 Ok((values[0], values[1], values[2]))
1123}
1124
1125#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1126#[wasm_bindgen(js_name = "daily_factor_batch_js")]
1127pub fn daily_factor_batch_js(
1128 open: &[f64],
1129 high: &[f64],
1130 low: &[f64],
1131 close: &[f64],
1132 config: JsValue,
1133) -> Result<JsValue, JsValue> {
1134 let config: DailyFactorBatchConfig = serde_wasm_bindgen::from_value(config)
1135 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1136 let sweep = DailyFactorBatchRange {
1137 threshold_level: js_vec3_to_f64("threshold_level_range", &config.threshold_level_range)?,
1138 };
1139 let out = daily_factor_batch_with_kernel(open, high, low, close, &sweep, Kernel::Auto)
1140 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1141 let threshold_levels = out
1142 .combos
1143 .iter()
1144 .map(|combo| combo.threshold_level.unwrap_or(DEFAULT_THRESHOLD_LEVEL))
1145 .collect();
1146 serde_wasm_bindgen::to_value(&DailyFactorBatchJsOutput {
1147 value: out.value,
1148 ema: out.ema,
1149 signal: out.signal,
1150 threshold_levels,
1151 rows: out.rows,
1152 cols: out.cols,
1153 })
1154 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1155}
1156
1157#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1158#[wasm_bindgen]
1159pub fn daily_factor_alloc(len: usize) -> *mut f64 {
1160 let mut vec = Vec::<f64>::with_capacity(len);
1161 let ptr = vec.as_mut_ptr();
1162 std::mem::forget(vec);
1163 ptr
1164}
1165
1166#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1167#[wasm_bindgen]
1168pub fn daily_factor_free(ptr: *mut f64, len: usize) {
1169 if !ptr.is_null() {
1170 unsafe {
1171 let _ = Vec::from_raw_parts(ptr, len, len);
1172 }
1173 }
1174}
1175
1176#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1177#[wasm_bindgen]
1178pub fn daily_factor_into(
1179 open_ptr: *const f64,
1180 high_ptr: *const f64,
1181 low_ptr: *const f64,
1182 close_ptr: *const f64,
1183 out_value_ptr: *mut f64,
1184 out_ema_ptr: *mut f64,
1185 out_signal_ptr: *mut f64,
1186 len: usize,
1187 threshold_level: f64,
1188) -> Result<(), JsValue> {
1189 if open_ptr.is_null()
1190 || high_ptr.is_null()
1191 || low_ptr.is_null()
1192 || close_ptr.is_null()
1193 || out_value_ptr.is_null()
1194 || out_ema_ptr.is_null()
1195 || out_signal_ptr.is_null()
1196 {
1197 return Err(JsValue::from_str("Null pointer provided"));
1198 }
1199 unsafe {
1200 let open = std::slice::from_raw_parts(open_ptr, len);
1201 let high = std::slice::from_raw_parts(high_ptr, len);
1202 let low = std::slice::from_raw_parts(low_ptr, len);
1203 let close = std::slice::from_raw_parts(close_ptr, len);
1204 let out_value = std::slice::from_raw_parts_mut(out_value_ptr, len);
1205 let out_ema = std::slice::from_raw_parts_mut(out_ema_ptr, len);
1206 let out_signal = std::slice::from_raw_parts_mut(out_signal_ptr, len);
1207 let input = DailyFactorInput::from_slices(
1208 open,
1209 high,
1210 low,
1211 close,
1212 DailyFactorParams {
1213 threshold_level: Some(threshold_level),
1214 },
1215 );
1216 daily_factor_into_slice(out_value, out_ema, out_signal, &input, Kernel::Auto)
1217 .map_err(|e| JsValue::from_str(&e.to_string()))
1218 }
1219}
1220
1221#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1222#[wasm_bindgen]
1223pub fn daily_factor_batch_into(
1224 open_ptr: *const f64,
1225 high_ptr: *const f64,
1226 low_ptr: *const f64,
1227 close_ptr: *const f64,
1228 out_value_ptr: *mut f64,
1229 out_ema_ptr: *mut f64,
1230 out_signal_ptr: *mut f64,
1231 len: usize,
1232 threshold_level_start: f64,
1233 threshold_level_end: f64,
1234 threshold_level_step: f64,
1235) -> Result<usize, JsValue> {
1236 if open_ptr.is_null()
1237 || high_ptr.is_null()
1238 || low_ptr.is_null()
1239 || close_ptr.is_null()
1240 || out_value_ptr.is_null()
1241 || out_ema_ptr.is_null()
1242 || out_signal_ptr.is_null()
1243 {
1244 return Err(JsValue::from_str(
1245 "null pointer passed to daily_factor_batch_into",
1246 ));
1247 }
1248 unsafe {
1249 let open = std::slice::from_raw_parts(open_ptr, len);
1250 let high = std::slice::from_raw_parts(high_ptr, len);
1251 let low = std::slice::from_raw_parts(low_ptr, len);
1252 let close = std::slice::from_raw_parts(close_ptr, len);
1253 let sweep = DailyFactorBatchRange {
1254 threshold_level: (
1255 threshold_level_start,
1256 threshold_level_end,
1257 threshold_level_step,
1258 ),
1259 };
1260 let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1261 let rows = combos.len();
1262 let total = rows
1263 .checked_mul(len)
1264 .ok_or_else(|| JsValue::from_str("rows*cols overflow in daily_factor_batch_into"))?;
1265 let out_value = std::slice::from_raw_parts_mut(out_value_ptr, total);
1266 let out_ema = std::slice::from_raw_parts_mut(out_ema_ptr, total);
1267 let out_signal = std::slice::from_raw_parts_mut(out_signal_ptr, total);
1268 daily_factor_batch_into_slice(
1269 out_value,
1270 out_ema,
1271 out_signal,
1272 open,
1273 high,
1274 low,
1275 close,
1276 &sweep,
1277 Kernel::Auto,
1278 )
1279 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1280 Ok(rows)
1281 }
1282}
1283
1284#[cfg(test)]
1285mod tests {
1286 use super::*;
1287
1288 fn manual_daily_factor(
1289 open: &[f64],
1290 high: &[f64],
1291 low: &[f64],
1292 close: &[f64],
1293 threshold_level: f64,
1294 ) -> DailyFactorOutput {
1295 let len = close.len();
1296 let mut value = vec![f64::NAN; len];
1297 let mut ema = vec![f64::NAN; len];
1298 let mut signal = vec![f64::NAN; len];
1299 let first = first_valid_ohlc(open, high, low, close).unwrap();
1300 compute_base_into(open, high, low, close, first, &mut value, &mut ema).unwrap();
1301 compute_signal_into(&value, &ema, close, threshold_level, &mut signal).unwrap();
1302 DailyFactorOutput { value, ema, signal }
1303 }
1304
1305 fn sample_ohlc(n: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
1306 let open: Vec<f64> = (0..n)
1307 .map(|i| 100.0 + ((i as f64) * 0.17).sin() * 1.4 + (i as f64) * 0.03)
1308 .collect();
1309 let close: Vec<f64> = open
1310 .iter()
1311 .enumerate()
1312 .map(|(i, &o)| o + ((i as f64) * 0.11).cos() * 0.85)
1313 .collect();
1314 let high: Vec<f64> = open
1315 .iter()
1316 .zip(close.iter())
1317 .enumerate()
1318 .map(|(i, (&o, &c))| o.max(c) + 0.9 + ((i as f64) * 0.07).sin().abs())
1319 .collect();
1320 let low: Vec<f64> = open
1321 .iter()
1322 .zip(close.iter())
1323 .enumerate()
1324 .map(|(i, (&o, &c))| o.min(c) - 0.8 - ((i as f64) * 0.09).cos().abs())
1325 .collect();
1326 (open, high, low, close)
1327 }
1328
1329 #[test]
1330 fn matches_manual_reference() {
1331 let (open, high, low, close) = sample_ohlc(96);
1332 let input = DailyFactorInput::from_slices(
1333 &open,
1334 &high,
1335 &low,
1336 &close,
1337 DailyFactorParams {
1338 threshold_level: Some(0.35),
1339 },
1340 );
1341 let out = daily_factor(&input).unwrap();
1342 let expected = manual_daily_factor(&open, &high, &low, &close, 0.35);
1343 assert_eq!(out.value.len(), expected.value.len());
1344 for i in 0..close.len() {
1345 let got = out.value[i];
1346 let want = expected.value[i];
1347 assert!(
1348 (got.is_nan() && want.is_nan()) || (got - want).abs() <= 1e-12,
1349 "value mismatch at {i}: {got} vs {want}"
1350 );
1351 let got = out.ema[i];
1352 let want = expected.ema[i];
1353 assert!(
1354 (got.is_nan() && want.is_nan()) || (got - want).abs() <= 1e-12,
1355 "ema mismatch at {i}: {got} vs {want}"
1356 );
1357 let got = out.signal[i];
1358 let want = expected.signal[i];
1359 assert!(
1360 (got.is_nan() && want.is_nan()) || (got - want).abs() <= 1e-12,
1361 "signal mismatch at {i}: {got} vs {want}"
1362 );
1363 }
1364 }
1365
1366 #[test]
1367 fn stream_matches_batch() {
1368 let (open, high, low, close) = sample_ohlc(80);
1369 let input = DailyFactorInput::from_slices(
1370 &open,
1371 &high,
1372 &low,
1373 &close,
1374 DailyFactorParams {
1375 threshold_level: Some(0.35),
1376 },
1377 );
1378 let batch = daily_factor(&input).unwrap();
1379 let mut stream = DailyFactorStream::try_new(DailyFactorParams {
1380 threshold_level: Some(0.35),
1381 })
1382 .unwrap();
1383 for i in 0..close.len() {
1384 let (value, ema, signal) = stream.update(open[i], high[i], low[i], close[i]);
1385 let cmp = |got: f64, want: f64| {
1386 (got.is_nan() && want.is_nan()) || (got - want).abs() <= 1e-12
1387 };
1388 assert!(cmp(value, batch.value[i]));
1389 assert!(cmp(ema, batch.ema[i]));
1390 assert!(cmp(signal, batch.signal[i]));
1391 }
1392 }
1393
1394 #[test]
1395 fn batch_first_row_matches_single() {
1396 let (open, high, low, close) = sample_ohlc(72);
1397 let sweep = DailyFactorBatchRange {
1398 threshold_level: (0.35, 0.45, 0.10),
1399 };
1400 let out = daily_factor_batch_with_kernel(&open, &high, &low, &close, &sweep, Kernel::Auto)
1401 .unwrap();
1402 assert_eq!(out.rows, 2);
1403 assert_eq!(out.cols, close.len());
1404 let single = manual_daily_factor(&open, &high, &low, &close, 0.35);
1405 let end = close.len();
1406 for i in 0..end {
1407 let got = out.value[i];
1408 let want = single.value[i];
1409 assert!((got.is_nan() && want.is_nan()) || (got - want).abs() <= 1e-12);
1410 let got = out.ema[i];
1411 let want = single.ema[i];
1412 assert!((got.is_nan() && want.is_nan()) || (got - want).abs() <= 1e-12);
1413 let got = out.signal[i];
1414 let want = single.signal[i];
1415 assert!((got.is_nan() && want.is_nan()) || (got - want).abs() <= 1e-12);
1416 }
1417 }
1418
1419 #[test]
1420 fn invalid_threshold_level_fails() {
1421 let (open, high, low, close) = sample_ohlc(16);
1422 let input = DailyFactorInput::from_slices(
1423 &open,
1424 &high,
1425 &low,
1426 &close,
1427 DailyFactorParams {
1428 threshold_level: Some(1.5),
1429 },
1430 );
1431 let err = daily_factor(&input).unwrap_err();
1432 assert!(matches!(
1433 err,
1434 DailyFactorError::InvalidThresholdLevel { .. }
1435 ));
1436 }
1437}