finql 0.12.0

A quantitative finance toolbox
Documentation
use cal_calc::Calendar;
use chrono::{DateTime, Local, NaiveDate};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::error::Error;
use std::fmt;

#[derive(Debug)]
pub enum TimeSeriesError {
    IsEmpty,
}

impl fmt::Display for TimeSeriesError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            TimeSeriesError::IsEmpty => write!(f, "Time series is empty."),
        }
    }
}

impl Error for TimeSeriesError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        None
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TimeValue {
    pub time: DateTime<Local>,
    pub value: f64,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TimeSeries {
    pub series: Vec<TimeValue>,
    pub title: String,
}

impl TimeSeries {
    pub fn new(title: &str) -> Self {
        TimeSeries {
            series: vec![],
            title: title.to_owned(),
        }
    }
    pub fn min_max(&self) -> Result<(NaiveDate, NaiveDate, f64, f64), TimeSeriesError> {
        if self.series.is_empty() {
            return Err(TimeSeriesError::IsEmpty);
        }
        let mut min_val = self.series[0].value;
        let mut max_val = min_val;
        let min_time = self.series[0].time;
        let max_time = self.series.last().unwrap().time;
        for v in &self.series {
            if min_val > v.value {
                min_val = v.value;
            }
            if max_val < v.value {
                max_val = v.value;
            }
        }
        Ok((
            min_time.naive_local().date(),
            max_time.naive_local().date(),
            min_val,
            max_val,
        ))
    }

    pub fn find_gaps(
        &self,
        cal: &Calendar,
        min_size: usize,
    ) -> Result<Vec<(NaiveDate, NaiveDate)>, TimeSeriesError> {
        let mut gaps = Vec::new();
        let (min_date, _, _, _) = self.min_max()?;
        let today = Local::now().naive_local().date();
        let dates: HashSet<NaiveDate> = self
            .series
            .iter()
            .map(|t| t.time.naive_local().date())
            .collect();
        let mut gap_begin = None;
        let mut date = min_date;
        let mut gap_size = 0;
        while date <= today {
            match gap_begin {
                None => {
                    if !dates.contains(&date) {
                        gap_begin = Some(date);
                        gap_size = 1;
                    }
                }

                Some(d) => {
                    if dates.contains(&date) {
                        if gap_size >= min_size {
                            gaps.push((d, cal.prev_bday(date)));
                        }
                        gap_begin = None;
                    } else {
                        gap_size += 1;
                    }
                }
            }
            date = cal.next_bday(date);
        }

        if let Some(d) = gap_begin {
            gaps.push((d, today));
        }

        Ok(gaps)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::datatypes::date_time_helper::make_time;
    use cal_calc::Holiday;
    use chrono::{Datelike, Weekday};

    #[test]
    fn finding_gaps() {
        let holidays = vec![
            Holiday::SingularDay(NaiveDate::from_ymd(2021, 11, 4)),
            Holiday::SingularDay(NaiveDate::from_ymd(2021, 11, 5)),
            Holiday::SingularDay(NaiveDate::from_ymd(2021, 11, 8)),
            Holiday::WeekDay(Weekday::Sat),
            Holiday::WeekDay(Weekday::Sun),
        ];
        let today = Local::now().naive_local().date();
        let cal = Calendar::calc_calendar(&holidays, 2021, today.year());

        let mut ts = TimeSeries {
            title: "test".to_string(),
            series: Vec::new(),
        };
        ts.series.push(TimeValue {
            time: make_time(2021, 10, 28, 20, 0, 0).unwrap(),
            value: 1.0,
        });
        ts.series.push(TimeValue {
            time: make_time(2021, 11, 1, 20, 0, 0).unwrap(),
            value: 1.0,
        });
        ts.series.push(TimeValue {
            time: make_time(2021, 11, 8, 20, 0, 0).unwrap(),
            value: 1.0,
        });
        ts.series.push(TimeValue {
            time: make_time(2021, 11, 9, 20, 0, 0).unwrap(),
            value: 1.0,
        });

        let gaps = ts.find_gaps(&cal, 1).unwrap();
        assert_eq!(gaps.len(), 3);
        assert_eq!(gaps[0].0, NaiveDate::from_ymd(2021, 10, 29));
        assert_eq!(gaps[0].1, NaiveDate::from_ymd(2021, 10, 29));
        assert_eq!(gaps[1].0, NaiveDate::from_ymd(2021, 11, 2));
        assert_eq!(gaps[1].1, NaiveDate::from_ymd(2021, 11, 3));
        assert_eq!(gaps[2].0, NaiveDate::from_ymd(2021, 11, 10));
        assert_eq!(gaps[2].1, today);
    }
}