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())
}
|