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, 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::().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()) }