summaryrefslogtreecommitdiff
path: root/src/data.rs
blob: c5fc18c85dcaa0e78a4e67f723082eadd0108076 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
use serde::{Deserialize, Serialize};
use std::path::Path;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Candle {
    #[serde(rename = "Date")]
    pub date: String,
    #[serde(rename = "Open")]
    pub open: f64,
    #[serde(rename = "High")]
    pub high: f64,
    #[serde(rename = "Low")]
    pub low: f64,
    #[serde(rename = "Close")]
    pub close: f64,
    #[serde(rename = "Volume")]
    pub volume: f64,
}

/// Read `Date,Open,High,Low,Close,Volume` rows; dates must be `YYYY-MM-DD`.
/// Rows are sorted ascending by date; duplicate dates are an error.
pub fn load_csv(path: &Path) -> Result<Vec<Candle>, String> {
    let mut reader = csv::Reader::from_path(path).map_err(|e| format!("{}: {e}", path.display()))?;
    let mut candles = Vec::new();
    for (i, row) in reader.deserialize::<Candle>().enumerate() {
        let line = i + 2; // header is line 1
        let c = row.map_err(|e| format!("{}: line {line}: {e}", path.display()))?;
        if !valid_date(&c.date) {
            return Err(format!("{}: line {line}: bad date '{}', expected YYYY-MM-DD", path.display(), c.date));
        }
        if c.high < c.low || !(c.low..=c.high).contains(&c.close) {
            return Err(format!("{}: line {line}: inconsistent High/Low/Close", path.display()));
        }
        candles.push(c);
    }
    candles.sort_by(|a, b| a.date.cmp(&b.date));
    if let Some(w) = candles.windows(2).find(|w| w[0].date == w[1].date) {
        return Err(format!("{}: duplicate date {}", path.display(), w[0].date));
    }
    if candles.is_empty() {
        return Err(format!("{}: no data rows", path.display()));
    }
    Ok(candles)
}

fn valid_date(s: &str) -> bool {
    let b = s.as_bytes();
    b.len() == 10
        && b[4] == b'-'
        && b[7] == b'-'
        && b.iter().enumerate().all(|(i, c)| i == 4 || i == 7 || c.is_ascii_digit())
}