quantstats_rs/
utils.rs

1use chrono::NaiveDate;
2
3#[derive(Debug)]
4pub enum DataError {
5    Empty,
6    LengthMismatch { dates: usize, values: usize },
7}
8
9impl std::fmt::Display for DataError {
10    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11        match self {
12            DataError::Empty => write!(f, "time series is empty"),
13            DataError::LengthMismatch { dates, values } => write!(
14                f,
15                "time series length mismatch: {} dates vs {} values",
16                dates, values
17            ),
18        }
19    }
20}
21
22impl std::error::Error for DataError {}
23
24#[derive(Clone, Debug)]
25pub struct ReturnSeries {
26    pub dates: Vec<NaiveDate>,
27    pub values: Vec<f64>,
28    pub name: Option<String>,
29}
30
31impl ReturnSeries {
32    pub fn new(
33        dates: Vec<NaiveDate>,
34        values: Vec<f64>,
35        name: Option<String>,
36    ) -> Result<Self, DataError> {
37        if dates.is_empty() || values.is_empty() {
38            return Err(DataError::Empty);
39        }
40
41        if dates.len() != values.len() {
42            return Err(DataError::LengthMismatch {
43                dates: dates.len(),
44                values: values.len(),
45            });
46        }
47
48        let mut paired: Vec<(NaiveDate, f64)> = dates.into_iter().zip(values.into_iter()).collect();
49        paired.sort_by_key(|(d, _)| *d);
50
51        let (sorted_dates, sorted_values): (Vec<_>, Vec<_>) = paired.into_iter().unzip();
52
53        Ok(Self {
54            dates: sorted_dates,
55            values: sorted_values,
56            name,
57        })
58    }
59
60    pub fn len(&self) -> usize {
61        self.dates.len()
62    }
63
64    pub fn is_empty(&self) -> bool {
65        self.dates.is_empty()
66    }
67
68    pub fn date_range(&self) -> Option<(NaiveDate, NaiveDate)> {
69        if self.dates.is_empty() {
70            None
71        } else {
72            Some((
73                *self.dates.first().expect("len checked"),
74                *self.dates.last().expect("len checked"),
75            ))
76        }
77    }
78}
79
80pub fn align_start_dates(a: &ReturnSeries, b: &ReturnSeries) -> (ReturnSeries, ReturnSeries) {
81    let idx_a = first_non_zero_index(&a.values).unwrap_or(0);
82    let idx_b = first_non_zero_index(&b.values).unwrap_or(0);
83    let start_idx = idx_a.max(idx_b);
84
85    let slice_a = ReturnSeries {
86        dates: a.dates[start_idx..].to_vec(),
87        values: a.values[start_idx..].to_vec(),
88        name: a.name.clone(),
89    };
90
91    let slice_b = ReturnSeries {
92        dates: b.dates[start_idx..].to_vec(),
93        values: b.values[start_idx..].to_vec(),
94        name: b.name.clone(),
95    };
96
97    (slice_a, slice_b)
98}
99
100fn first_non_zero_index(values: &[f64]) -> Option<usize> {
101    values.iter().position(|v| !v.is_nan() && *v != 0.0)
102}