1use crate::csv_io::{parse_series, Field};
2use crate::error::DataError;
3use crate::source::ObjectSource;
4use ndarray::Array2;
5use std::collections::{BTreeSet, HashMap};
6use yuzu_core::panel::Panel;
7
8pub const PRICES_DIR: &str = "prices";
11
12pub fn load_panel<S: ObjectSource + Sync>(
19 source: &S,
20 symbols: &[String],
21 field: Field,
22 from: i32,
23 to: i32,
24 dir: &str,
25) -> Result<Panel, DataError> {
26 let per_symbol =
29 crate::parallel::fetch_series(source, symbols, dir, from, to, |b| parse_series(b, field))?;
30
31 let mut date_set: BTreeSet<i32> = BTreeSet::new();
32 for map in &per_symbol {
33 date_set.extend(map.keys().copied());
34 }
35 let dates: Vec<i32> = date_set.into_iter().collect();
36 let row_of: HashMap<i32, usize> = dates.iter().enumerate().map(|(i, d)| (*d, i)).collect();
37 let mut data = Array2::from_elem((dates.len(), symbols.len()), f64::NAN);
38 for (c, map) in per_symbol.iter().enumerate() {
39 for (d, v) in map {
40 data[[row_of[d], c]] = *v;
41 }
42 }
43
44 Panel::new(dates, symbols.to_vec(), data).map_err(|e| DataError::Parse(e.to_string()))
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50 use crate::csv_io::{write_series, OhlcvRow};
51 use crate::source::LocalSource;
52 use std::fs;
53
54 fn r(day: i32, c: f64) -> OhlcvRow {
55 OhlcvRow {
56 day,
57 adj_open: c,
58 adj_high: c + 1.0,
59 adj_low: c - 1.0,
60 adj_close: c,
61 volume: 100.0,
62 }
63 }
64
65 fn fixture_dir(tag: &str) -> std::path::PathBuf {
66 let dir = std::env::temp_dir().join(format!("yuzu_data_loader_{tag}"));
68 let _ = fs::remove_dir_all(&dir);
70 fs::create_dir_all(dir.join("prices")).unwrap();
71 fs::write(
72 dir.join("prices/AAPL.csv.gz"),
73 write_series(&[r(20240102, 10.0), r(20240103, 11.0), r(20240104, 12.0)]).unwrap(),
74 )
75 .unwrap();
76 fs::write(
77 dir.join("prices/MSFT.csv.gz"),
78 write_series(&[r(20240103, 50.0), r(20240105, 52.0)]).unwrap(),
79 )
80 .unwrap();
81 dir
82 }
83
84 #[test]
85 fn loads_chosen_field_with_union_dates_and_nan() {
86 let dir = fixture_dir("union");
87 let src = LocalSource::new(&dir);
88 let syms = vec!["AAPL".to_string(), "MSFT".to_string(), "ZZZ".to_string()];
89
90 let close =
91 load_panel(&src, &syms, Field::AdjClose, 20240102, 20240104, PRICES_DIR).unwrap();
92 assert_eq!(close.dates, vec![20240102, 20240103, 20240104]);
93 assert_eq!(close.data[[0, 0]], 10.0); assert!(close.data[[0, 1]].is_nan()); assert_eq!(close.data[[1, 1]], 50.0); assert!(close.data[[2, 2]].is_nan()); let high = load_panel(&src, &syms, Field::AdjHigh, 20240102, 20240104, PRICES_DIR).unwrap();
100 assert_eq!(high.data[[0, 0]], 11.0); }
102
103 #[test]
104 fn loads_plain_csv_files_alongside_gzip() {
105 let dir = std::env::temp_dir().join("yuzu_data_loader_plaincsv");
108 let _ = fs::remove_dir_all(&dir);
109 fs::create_dir_all(dir.join("prices")).unwrap();
110 fs::write(
111 dir.join("prices/AAPL.csv"),
112 "day,adj_open,adj_high,adj_low,adj_close,volume\n\
113 2024-01-02,9,11,9,10,100\n\
114 2024-01-03,10,12,10,11,100\n",
115 )
116 .unwrap();
117 let src = LocalSource::new(&dir);
118 let syms = vec!["AAPL".to_string()];
119 let close =
120 load_panel(&src, &syms, Field::AdjClose, 20240102, 20240103, PRICES_DIR).unwrap();
121 assert_eq!(close.dates, vec![20240102, 20240103]);
122 assert_eq!(close.data[[0, 0]], 10.0);
123 assert_eq!(close.data[[1, 0]], 11.0);
124 }
125
126 #[cfg(feature = "parquet")]
127 #[test]
128 fn loads_parquet_files_through_the_same_path() {
129 use arrow_array::{ArrayRef, Float64Array, Int32Array, RecordBatch};
130 use arrow_schema::{DataType, Field as AField, Schema};
131 use parquet::arrow::ArrowWriter;
132 use std::sync::Arc;
133
134 let dir = std::env::temp_dir().join("yuzu_data_loader_parquet");
135 let _ = fs::remove_dir_all(&dir);
136 fs::create_dir_all(dir.join("prices")).unwrap();
137
138 let schema = Arc::new(Schema::new(vec![
139 AField::new("day", DataType::Int32, false),
140 AField::new("adj_close", DataType::Float64, true),
141 AField::new("volume", DataType::Float64, true),
142 ]));
143 let batch = RecordBatch::try_new(
144 schema.clone(),
145 vec![
146 Arc::new(Int32Array::from(vec![20240102, 20240103])) as ArrayRef,
147 Arc::new(Float64Array::from(vec![10.0, 11.0])) as ArrayRef,
148 Arc::new(Float64Array::from(vec![100.0, 200.0])) as ArrayRef,
149 ],
150 )
151 .unwrap();
152 let mut buf = Vec::new();
153 let mut w = ArrowWriter::try_new(&mut buf, schema, None).unwrap();
154 w.write(&batch).unwrap();
155 w.close().unwrap();
156 fs::write(dir.join("prices/NVDA.parquet"), buf).unwrap();
157
158 let src = LocalSource::new(&dir);
159 let syms = vec!["NVDA".to_string()];
160 let close =
161 load_panel(&src, &syms, Field::AdjClose, 20240102, 20240103, PRICES_DIR).unwrap();
162 assert_eq!(close.dates, vec![20240102, 20240103]);
163 assert_eq!(close.data[[0, 0]], 10.0);
164 assert_eq!(close.data[[1, 0]], 11.0);
165 let vol = load_panel(&src, &syms, Field::Volume, 20240102, 20240103, PRICES_DIR).unwrap();
167 assert_eq!(vol.data[[1, 0]], 200.0);
168 }
169
170 #[test]
171 fn skips_a_corrupt_file_instead_of_failing_the_batch() {
172 let dir = fixture_dir("corrupt");
173 fs::write(dir.join("prices/BAD.csv.gz"), b"not gzip at all").unwrap();
175 let src = LocalSource::new(&dir);
176 let syms = vec!["AAPL".to_string(), "BAD".to_string()];
177
178 let close =
179 load_panel(&src, &syms, Field::AdjClose, 20240102, 20240104, PRICES_DIR).unwrap();
180 assert_eq!(close.data[[0, 0]], 10.0); assert!(close.data[[0, 1]].is_nan()); assert!(close.data[[2, 1]].is_nan());
183 }
184}