Skip to main content

vector_ta/indicators/
daily_factor.rs

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(&params)?,
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}