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}