Skip to main content

vector_ta/indicators/
advance_decline_line.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::{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;
25use std::convert::AsRef;
26use std::mem::ManuallyDrop;
27use thiserror::Error;
28
29impl<'a> AsRef<[f64]> for AdvanceDeclineLineInput<'a> {
30    #[inline(always)]
31    fn as_ref(&self) -> &[f64] {
32        match &self.data {
33            AdvanceDeclineLineData::Slice(slice) => slice,
34            AdvanceDeclineLineData::Candles { candles, source } => source_type(candles, source),
35        }
36    }
37}
38
39#[derive(Debug, Clone)]
40pub enum AdvanceDeclineLineData<'a> {
41    Candles {
42        candles: &'a Candles,
43        source: &'a str,
44    },
45    Slice(&'a [f64]),
46}
47
48#[derive(Debug, Clone)]
49pub struct AdvanceDeclineLineOutput {
50    pub values: Vec<f64>,
51}
52
53#[derive(Debug, Clone, Default)]
54#[cfg_attr(
55    all(target_arch = "wasm32", feature = "wasm"),
56    derive(Serialize, Deserialize)
57)]
58pub struct AdvanceDeclineLineParams;
59
60#[derive(Debug, Clone)]
61pub struct AdvanceDeclineLineInput<'a> {
62    pub data: AdvanceDeclineLineData<'a>,
63    pub params: AdvanceDeclineLineParams,
64}
65
66impl<'a> AdvanceDeclineLineInput<'a> {
67    #[inline]
68    pub fn from_candles(
69        candles: &'a Candles,
70        source: &'a str,
71        params: AdvanceDeclineLineParams,
72    ) -> Self {
73        Self {
74            data: AdvanceDeclineLineData::Candles { candles, source },
75            params,
76        }
77    }
78
79    #[inline]
80    pub fn from_slice(slice: &'a [f64], params: AdvanceDeclineLineParams) -> Self {
81        Self {
82            data: AdvanceDeclineLineData::Slice(slice),
83            params,
84        }
85    }
86
87    #[inline]
88    pub fn with_default_candles(candles: &'a Candles) -> Self {
89        Self::from_candles(candles, "close", AdvanceDeclineLineParams)
90    }
91}
92
93#[derive(Copy, Clone, Debug, Default)]
94pub struct AdvanceDeclineLineBuilder {
95    kernel: Kernel,
96}
97
98impl AdvanceDeclineLineBuilder {
99    #[inline(always)]
100    pub fn new() -> Self {
101        Self::default()
102    }
103
104    #[inline(always)]
105    pub fn kernel(mut self, kernel: Kernel) -> Self {
106        self.kernel = kernel;
107        self
108    }
109
110    #[inline(always)]
111    pub fn apply(
112        self,
113        candles: &Candles,
114        source: &str,
115    ) -> Result<AdvanceDeclineLineOutput, AdvanceDeclineLineError> {
116        let input =
117            AdvanceDeclineLineInput::from_candles(candles, source, AdvanceDeclineLineParams);
118        advance_decline_line_with_kernel(&input, self.kernel)
119    }
120
121    #[inline(always)]
122    pub fn apply_slice(
123        self,
124        data: &[f64],
125    ) -> Result<AdvanceDeclineLineOutput, AdvanceDeclineLineError> {
126        let input = AdvanceDeclineLineInput::from_slice(data, AdvanceDeclineLineParams);
127        advance_decline_line_with_kernel(&input, self.kernel)
128    }
129
130    #[inline(always)]
131    pub fn into_stream(self) -> Result<AdvanceDeclineLineStream, AdvanceDeclineLineError> {
132        let _ = self.kernel;
133        AdvanceDeclineLineStream::try_new()
134    }
135}
136
137#[derive(Debug, Error)]
138pub enum AdvanceDeclineLineError {
139    #[error("advance_decline_line: Input data slice is empty.")]
140    EmptyInputData,
141    #[error("advance_decline_line: All values are NaN.")]
142    AllValuesNaN,
143    #[error("advance_decline_line: Output length mismatch: expected = {expected}, got = {got}")]
144    OutputLengthMismatch { expected: usize, got: usize },
145    #[error("advance_decline_line: Invalid range: start={start}, end={end}, step={step}")]
146    InvalidRange {
147        start: String,
148        end: String,
149        step: String,
150    },
151    #[error("advance_decline_line: Invalid kernel for batch: {0:?}")]
152    InvalidKernelForBatch(Kernel),
153}
154
155#[derive(Debug, Clone, Default)]
156pub struct AdvanceDeclineLineStream {
157    started: bool,
158    sum: f64,
159}
160
161impl AdvanceDeclineLineStream {
162    #[inline]
163    pub fn try_new() -> Result<Self, AdvanceDeclineLineError> {
164        Ok(Self::default())
165    }
166
167    #[inline(always)]
168    fn reset(&mut self) {
169        self.started = false;
170        self.sum = 0.0;
171    }
172
173    #[inline(always)]
174    pub fn update(&mut self, value: f64) -> Option<f64> {
175        if !value.is_finite() {
176            self.reset();
177            return None;
178        }
179        if !self.started {
180            self.started = true;
181            self.sum = value;
182        } else {
183            self.sum += value;
184        }
185        Some(self.sum)
186    }
187
188    #[inline(always)]
189    pub fn get_warmup_period(&self) -> usize {
190        0
191    }
192}
193
194#[inline]
195pub fn advance_decline_line(
196    input: &AdvanceDeclineLineInput,
197) -> Result<AdvanceDeclineLineOutput, AdvanceDeclineLineError> {
198    advance_decline_line_with_kernel(input, Kernel::Auto)
199}
200
201#[inline(always)]
202fn first_valid_value(data: &[f64]) -> usize {
203    let mut i = 0usize;
204    while i < data.len() {
205        if data[i].is_finite() {
206            break;
207        }
208        i += 1;
209    }
210    i.min(data.len())
211}
212
213#[inline(always)]
214fn count_valid_values(data: &[f64]) -> usize {
215    data.iter().filter(|v| v.is_finite()).count()
216}
217
218#[inline(always)]
219fn advance_decline_line_row(data: &[f64], out: &mut [f64]) {
220    let mut started = false;
221    let mut sum = 0.0;
222    for (dst, &value) in out.iter_mut().zip(data.iter()) {
223        if !value.is_finite() {
224            *dst = f64::NAN;
225            started = false;
226            sum = 0.0;
227            continue;
228        }
229        if !started {
230            started = true;
231            sum = value;
232        } else {
233            sum += value;
234        }
235        *dst = sum;
236    }
237}
238
239#[inline(always)]
240fn advance_decline_line_prepare<'a>(
241    input: &'a AdvanceDeclineLineInput,
242    kernel: Kernel,
243) -> Result<(&'a [f64], usize, Kernel), AdvanceDeclineLineError> {
244    let data = input.as_ref();
245    if data.is_empty() {
246        return Err(AdvanceDeclineLineError::EmptyInputData);
247    }
248
249    let first = first_valid_value(data);
250    if first >= data.len() {
251        return Err(AdvanceDeclineLineError::AllValuesNaN);
252    }
253
254    let _valid = count_valid_values(data);
255    let chosen = match kernel {
256        Kernel::Auto => detect_best_kernel(),
257        other => other.to_non_batch(),
258    };
259    Ok((data, first, chosen))
260}
261
262#[inline]
263pub fn advance_decline_line_with_kernel(
264    input: &AdvanceDeclineLineInput,
265    kernel: Kernel,
266) -> Result<AdvanceDeclineLineOutput, AdvanceDeclineLineError> {
267    let (data, first, _chosen) = advance_decline_line_prepare(input, kernel)?;
268    let mut values = alloc_with_nan_prefix(data.len(), first);
269    advance_decline_line_row(data, &mut values);
270    Ok(AdvanceDeclineLineOutput { values })
271}
272
273#[inline]
274pub fn advance_decline_line_into_slice(
275    dst: &mut [f64],
276    input: &AdvanceDeclineLineInput,
277    kernel: Kernel,
278) -> Result<(), AdvanceDeclineLineError> {
279    let (data, _first, _chosen) = advance_decline_line_prepare(input, kernel)?;
280    if dst.len() != data.len() {
281        return Err(AdvanceDeclineLineError::OutputLengthMismatch {
282            expected: data.len(),
283            got: dst.len(),
284        });
285    }
286    advance_decline_line_row(data, dst);
287    Ok(())
288}
289
290#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
291#[inline]
292pub fn advance_decline_line_into(
293    input: &AdvanceDeclineLineInput,
294    out: &mut [f64],
295) -> Result<(), AdvanceDeclineLineError> {
296    advance_decline_line_into_slice(out, input, Kernel::Auto)
297}
298
299#[derive(Clone, Debug, Default)]
300pub struct AdvanceDeclineLineBatchRange;
301
302#[derive(Clone, Debug, Default)]
303pub struct AdvanceDeclineLineBatchBuilder {
304    kernel: Kernel,
305}
306
307impl AdvanceDeclineLineBatchBuilder {
308    #[inline]
309    pub fn new() -> Self {
310        Self::default()
311    }
312
313    #[inline]
314    pub fn kernel(mut self, kernel: Kernel) -> Self {
315        self.kernel = kernel;
316        self
317    }
318
319    #[inline]
320    pub fn apply_slice(
321        self,
322        data: &[f64],
323    ) -> Result<AdvanceDeclineLineBatchOutput, AdvanceDeclineLineError> {
324        advance_decline_line_batch_with_kernel(data, &AdvanceDeclineLineBatchRange, self.kernel)
325    }
326
327    #[inline]
328    pub fn apply_candles(
329        self,
330        candles: &Candles,
331        source: &str,
332    ) -> Result<AdvanceDeclineLineBatchOutput, AdvanceDeclineLineError> {
333        self.apply_slice(source_type(candles, source))
334    }
335
336    #[inline]
337    pub fn with_default_candles(
338        candles: &Candles,
339    ) -> Result<AdvanceDeclineLineBatchOutput, AdvanceDeclineLineError> {
340        AdvanceDeclineLineBatchBuilder::new().apply_candles(candles, "close")
341    }
342}
343
344#[derive(Clone, Debug)]
345pub struct AdvanceDeclineLineBatchOutput {
346    pub values: Vec<f64>,
347    pub combos: Vec<AdvanceDeclineLineParams>,
348    pub rows: usize,
349    pub cols: usize,
350}
351
352impl AdvanceDeclineLineBatchOutput {
353    pub fn row_for_params(&self, _params: &AdvanceDeclineLineParams) -> Option<usize> {
354        if self.rows == 0 {
355            None
356        } else {
357            Some(0)
358        }
359    }
360
361    pub fn values_for(&self, _params: &AdvanceDeclineLineParams) -> Option<&[f64]> {
362        if self.rows == 0 {
363            None
364        } else {
365            self.values.get(0..self.cols)
366        }
367    }
368}
369
370#[inline(always)]
371fn expand_grid_advance_decline_line(
372    range: &AdvanceDeclineLineBatchRange,
373) -> Result<Vec<AdvanceDeclineLineParams>, AdvanceDeclineLineError> {
374    let _ = range;
375    Ok(vec![AdvanceDeclineLineParams])
376}
377
378#[inline]
379pub fn advance_decline_line_batch_with_kernel(
380    data: &[f64],
381    sweep: &AdvanceDeclineLineBatchRange,
382    kernel: Kernel,
383) -> Result<AdvanceDeclineLineBatchOutput, AdvanceDeclineLineError> {
384    let batch_kernel = match kernel {
385        Kernel::Auto => detect_best_batch_kernel(),
386        other if other.is_batch() => other,
387        other => return Err(AdvanceDeclineLineError::InvalidKernelForBatch(other)),
388    };
389    advance_decline_line_batch_par_slice(data, sweep, batch_kernel.to_non_batch())
390}
391
392#[inline]
393pub fn advance_decline_line_batch_slice(
394    data: &[f64],
395    sweep: &AdvanceDeclineLineBatchRange,
396    kernel: Kernel,
397) -> Result<AdvanceDeclineLineBatchOutput, AdvanceDeclineLineError> {
398    advance_decline_line_batch_inner(data, sweep, kernel, false)
399}
400
401#[inline]
402pub fn advance_decline_line_batch_par_slice(
403    data: &[f64],
404    sweep: &AdvanceDeclineLineBatchRange,
405    kernel: Kernel,
406) -> Result<AdvanceDeclineLineBatchOutput, AdvanceDeclineLineError> {
407    advance_decline_line_batch_inner(data, sweep, kernel, true)
408}
409
410#[inline(always)]
411fn advance_decline_line_batch_inner(
412    data: &[f64],
413    sweep: &AdvanceDeclineLineBatchRange,
414    _kernel: Kernel,
415    _parallel: bool,
416) -> Result<AdvanceDeclineLineBatchOutput, AdvanceDeclineLineError> {
417    if data.is_empty() {
418        return Err(AdvanceDeclineLineError::EmptyInputData);
419    }
420    let first = first_valid_value(data);
421    if first >= data.len() {
422        return Err(AdvanceDeclineLineError::AllValuesNaN);
423    }
424
425    let combos = expand_grid_advance_decline_line(sweep)?;
426    let rows = combos.len();
427    let cols = data.len();
428
429    let mut buf_mu = make_uninit_matrix(rows, cols);
430    init_matrix_prefixes(&mut buf_mu, cols, &[first]);
431    let mut guard = ManuallyDrop::new(buf_mu);
432    let out =
433        unsafe { std::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };
434    advance_decline_line_row(data, &mut out[..cols]);
435
436    let values = unsafe {
437        Vec::from_raw_parts(
438            guard.as_mut_ptr() as *mut f64,
439            guard.len(),
440            guard.capacity(),
441        )
442    };
443
444    Ok(AdvanceDeclineLineBatchOutput {
445        values,
446        combos,
447        rows,
448        cols,
449    })
450}
451
452#[inline(always)]
453pub fn advance_decline_line_batch_inner_into(
454    data: &[f64],
455    sweep: &AdvanceDeclineLineBatchRange,
456    _kernel: Kernel,
457    _parallel: bool,
458    out: &mut [f64],
459) -> Result<Vec<AdvanceDeclineLineParams>, AdvanceDeclineLineError> {
460    if data.is_empty() {
461        return Err(AdvanceDeclineLineError::EmptyInputData);
462    }
463    let first = first_valid_value(data);
464    if first >= data.len() {
465        return Err(AdvanceDeclineLineError::AllValuesNaN);
466    }
467    let combos = expand_grid_advance_decline_line(sweep)?;
468    let rows = combos.len();
469    let cols = data.len();
470    let total =
471        rows.checked_mul(cols)
472            .ok_or_else(|| AdvanceDeclineLineError::OutputLengthMismatch {
473                expected: usize::MAX,
474                got: out.len(),
475            })?;
476    if out.len() != total {
477        return Err(AdvanceDeclineLineError::OutputLengthMismatch {
478            expected: total,
479            got: out.len(),
480        });
481    }
482    out[..first].fill(f64::NAN);
483    advance_decline_line_row(data, &mut out[..cols]);
484    Ok(combos)
485}
486
487#[cfg(feature = "python")]
488#[pyfunction(name = "advance_decline_line")]
489#[pyo3(signature = (data, kernel=None))]
490pub fn advance_decline_line_py<'py>(
491    py: Python<'py>,
492    data: PyReadonlyArray1<'py, f64>,
493    kernel: Option<&str>,
494) -> PyResult<Bound<'py, PyArray1<f64>>> {
495    let data_slice: &[f64];
496    let owned;
497    data_slice = if let Ok(slice) = data.as_slice() {
498        slice
499    } else {
500        owned = data.to_owned_array();
501        owned.as_slice().unwrap()
502    };
503    let kern = validate_kernel(kernel, false)?;
504    let input = AdvanceDeclineLineInput::from_slice(data_slice, AdvanceDeclineLineParams);
505    let values = py
506        .allow_threads(|| advance_decline_line_with_kernel(&input, kern).map(|out| out.values))
507        .map_err(|e| PyValueError::new_err(e.to_string()))?;
508    Ok(values.into_pyarray(py))
509}
510
511#[cfg(feature = "python")]
512#[pyclass(name = "AdvanceDeclineLineStream")]
513pub struct AdvanceDeclineLineStreamPy {
514    stream: AdvanceDeclineLineStream,
515}
516
517#[cfg(feature = "python")]
518#[pymethods]
519impl AdvanceDeclineLineStreamPy {
520    #[new]
521    fn new() -> PyResult<Self> {
522        Ok(Self {
523            stream: AdvanceDeclineLineStream::try_new()
524                .map_err(|e| PyValueError::new_err(e.to_string()))?,
525        })
526    }
527
528    fn update(&mut self, value: f64) -> Option<f64> {
529        self.stream.update(value)
530    }
531}
532
533#[cfg(feature = "python")]
534#[pyfunction(name = "advance_decline_line_batch")]
535#[pyo3(signature = (data, kernel=None))]
536pub fn advance_decline_line_batch_py<'py>(
537    py: Python<'py>,
538    data: PyReadonlyArray1<'py, f64>,
539    kernel: Option<&str>,
540) -> PyResult<Bound<'py, PyDict>> {
541    let data_slice: &[f64];
542    let owned;
543    data_slice = if let Ok(slice) = data.as_slice() {
544        slice
545    } else {
546        owned = data.to_owned_array();
547        owned.as_slice().unwrap()
548    };
549    let kern = validate_kernel(kernel, true)?;
550    if data_slice.is_empty() {
551        return Err(PyValueError::new_err(
552            AdvanceDeclineLineError::EmptyInputData.to_string(),
553        ));
554    }
555
556    let rows = 1usize;
557    let cols = data_slice.len();
558    let total = rows
559        .checked_mul(cols)
560        .ok_or_else(|| PyValueError::new_err("advance_decline_line_batch: size overflow"))?;
561    let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
562    let slice_out = unsafe { out_arr.as_slice_mut()? };
563
564    py.allow_threads(|| {
565        let kernel = match kern {
566            Kernel::Auto => detect_best_batch_kernel(),
567            other => other,
568        };
569        advance_decline_line_batch_inner_into(
570            data_slice,
571            &AdvanceDeclineLineBatchRange,
572            kernel,
573            true,
574            slice_out,
575        )
576    })
577    .map_err(|e| PyValueError::new_err(e.to_string()))?;
578
579    let dict = PyDict::new(py);
580    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
581    dict.set_item("params", Vec::<f64>::new().into_pyarray(py))?;
582    dict.set_item("rows", rows)?;
583    dict.set_item("cols", cols)?;
584    Ok(dict)
585}
586
587#[cfg(feature = "python")]
588pub fn register_advance_decline_line_module(
589    module: &Bound<'_, pyo3::types::PyModule>,
590) -> PyResult<()> {
591    module.add_function(wrap_pyfunction!(advance_decline_line_py, module)?)?;
592    module.add_function(wrap_pyfunction!(advance_decline_line_batch_py, module)?)?;
593    module.add_class::<AdvanceDeclineLineStreamPy>()?;
594    Ok(())
595}
596
597#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
598#[wasm_bindgen(js_name = "advance_decline_line_js")]
599pub fn advance_decline_line_js(data: &[f64]) -> Result<Vec<f64>, JsValue> {
600    let input = AdvanceDeclineLineInput::from_slice(data, AdvanceDeclineLineParams);
601    advance_decline_line_with_kernel(&input, Kernel::Auto)
602        .map(|out| out.values)
603        .map_err(|e| JsValue::from_str(&e.to_string()))
604}
605
606#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
607#[wasm_bindgen]
608pub fn advance_decline_line_alloc(len: usize) -> *mut f64 {
609    let mut vec = Vec::<f64>::with_capacity(len);
610    let ptr = vec.as_mut_ptr();
611    std::mem::forget(vec);
612    ptr
613}
614
615#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
616#[wasm_bindgen]
617pub fn advance_decline_line_free(ptr: *mut f64, len: usize) {
618    if !ptr.is_null() {
619        unsafe {
620            let _ = Vec::from_raw_parts(ptr, len, len);
621        }
622    }
623}
624
625#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
626#[wasm_bindgen]
627pub fn advance_decline_line_into(
628    in_ptr: *const f64,
629    out_ptr: *mut f64,
630    len: usize,
631) -> Result<(), JsValue> {
632    if in_ptr.is_null() || out_ptr.is_null() {
633        return Err(JsValue::from_str("Null pointer provided"));
634    }
635    unsafe {
636        let data = std::slice::from_raw_parts(in_ptr, len);
637        let out = std::slice::from_raw_parts_mut(out_ptr, len);
638        let input = AdvanceDeclineLineInput::from_slice(data, AdvanceDeclineLineParams);
639        advance_decline_line_into_slice(out, &input, Kernel::Auto)
640            .map_err(|e| JsValue::from_str(&e.to_string()))
641    }
642}
643
644#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
645#[derive(Serialize, Deserialize)]
646pub struct AdvanceDeclineLineBatchConfig {}
647
648#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
649#[derive(Serialize, Deserialize)]
650pub struct AdvanceDeclineLineBatchJsOutput {
651    pub values: Vec<f64>,
652    pub combos: Vec<AdvanceDeclineLineParams>,
653    pub rows: usize,
654    pub cols: usize,
655}
656
657#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
658#[wasm_bindgen(js_name = "advance_decline_line_batch_js")]
659pub fn advance_decline_line_batch_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
660    let _: AdvanceDeclineLineBatchConfig = serde_wasm_bindgen::from_value(config)
661        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
662    let output =
663        advance_decline_line_batch_with_kernel(data, &AdvanceDeclineLineBatchRange, Kernel::Auto)
664            .map_err(|e| JsValue::from_str(&e.to_string()))?;
665    serde_wasm_bindgen::to_value(&AdvanceDeclineLineBatchJsOutput {
666        values: output.values,
667        combos: output.combos,
668        rows: output.rows,
669        cols: output.cols,
670    })
671    .map_err(|e| JsValue::from_str(&e.to_string()))
672}
673
674#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
675#[wasm_bindgen]
676pub fn advance_decline_line_batch_into(
677    in_ptr: *const f64,
678    out_ptr: *mut f64,
679    len: usize,
680) -> Result<usize, JsValue> {
681    if in_ptr.is_null() || out_ptr.is_null() {
682        return Err(JsValue::from_str("Null pointer provided"));
683    }
684    unsafe {
685        let data = std::slice::from_raw_parts(in_ptr, len);
686        let out = std::slice::from_raw_parts_mut(out_ptr, len);
687        advance_decline_line_batch_inner_into(
688            data,
689            &AdvanceDeclineLineBatchRange,
690            Kernel::Auto,
691            false,
692            out,
693        )
694        .map_err(|e| JsValue::from_str(&e.to_string()))?;
695    }
696    Ok(1)
697}
698
699#[cfg(test)]
700mod tests {
701    use super::*;
702    use crate::utilities::data_loader::read_candles_from_csv;
703    use std::error::Error;
704
705    fn load_close() -> Result<Vec<f64>, Box<dyn Error>> {
706        let candles = read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv")?;
707        Ok(candles.close)
708    }
709
710    #[test]
711    fn advance_decline_line_basic_slice() -> Result<(), Box<dyn Error>> {
712        let data = [1.0, 2.0, 3.0, 4.0];
713        let input = AdvanceDeclineLineInput::from_slice(&data, AdvanceDeclineLineParams);
714        let out = advance_decline_line_with_kernel(&input, Kernel::Scalar)?;
715        assert_eq!(out.values, vec![1.0, 3.0, 6.0, 10.0]);
716        Ok(())
717    }
718
719    #[test]
720    fn advance_decline_line_nan_resets_segment() -> Result<(), Box<dyn Error>> {
721        let data = [f64::NAN, 1.0, 2.0, f64::NAN, 3.0, 4.0];
722        let input = AdvanceDeclineLineInput::from_slice(&data, AdvanceDeclineLineParams);
723        let out = advance_decline_line_with_kernel(&input, Kernel::Scalar)?;
724        assert!(out.values[0].is_nan());
725        assert_eq!(out.values[1], 1.0);
726        assert_eq!(out.values[2], 3.0);
727        assert!(out.values[3].is_nan());
728        assert_eq!(out.values[4], 3.0);
729        assert_eq!(out.values[5], 7.0);
730        Ok(())
731    }
732
733    #[test]
734    fn advance_decline_line_output_contract() -> Result<(), Box<dyn Error>> {
735        let close = load_close()?;
736        let input = AdvanceDeclineLineInput::from_slice(&close, AdvanceDeclineLineParams);
737        let out = advance_decline_line_with_kernel(&input, Kernel::Scalar)?;
738        assert_eq!(out.values.len(), close.len());
739        assert!(out.values.iter().all(|v| v.is_finite()));
740        Ok(())
741    }
742
743    #[test]
744    fn advance_decline_line_auto_matches_scalar() -> Result<(), Box<dyn Error>> {
745        let close = load_close()?;
746        let input = AdvanceDeclineLineInput::from_slice(&close, AdvanceDeclineLineParams);
747        let auto = advance_decline_line_with_kernel(&input, Kernel::Auto)?;
748        let scalar = advance_decline_line_with_kernel(&input, Kernel::Scalar)?;
749        assert_eq!(auto.values, scalar.values);
750        Ok(())
751    }
752
753    #[test]
754    fn advance_decline_line_stream_matches_batch() -> Result<(), Box<dyn Error>> {
755        let close = load_close()?;
756        let input = AdvanceDeclineLineInput::from_slice(&close, AdvanceDeclineLineParams);
757        let batch = advance_decline_line_with_kernel(&input, Kernel::Scalar)?;
758        let mut stream = AdvanceDeclineLineStream::try_new()?;
759        let mut streamed = Vec::with_capacity(close.len());
760        for &value in &close {
761            streamed.push(stream.update(value).unwrap_or(f64::NAN));
762        }
763        assert_eq!(streamed, batch.values);
764        Ok(())
765    }
766
767    #[test]
768    fn advance_decline_line_batch_matches_single() -> Result<(), Box<dyn Error>> {
769        let close = load_close()?;
770        let batch = advance_decline_line_batch_with_kernel(
771            &close,
772            &AdvanceDeclineLineBatchRange,
773            Kernel::ScalarBatch,
774        )?;
775        assert_eq!(batch.rows, 1);
776        assert_eq!(batch.cols, close.len());
777        let single = advance_decline_line_with_kernel(
778            &AdvanceDeclineLineInput::from_slice(&close, AdvanceDeclineLineParams),
779            Kernel::Scalar,
780        )?;
781        assert_eq!(batch.values, single.values);
782        Ok(())
783    }
784}