1use crate::DataError;
6use pine_core::{Bar, Data, DataProvider, Ohlcv, ProviderError, SymInfo, Timeframe};
7use std::io::Read;
8use std::path::Path;
9
10pub struct StaticProvider {
12 data: Data,
13}
14
15impl StaticProvider {
16 pub fn new(data: Data) -> Self {
17 Self { data }
18 }
19
20 pub fn from_csv(path: impl AsRef<Path>) -> Result<Self, DataError> {
24 Ok(Self::new(Data::from_ohlcv(read_csv(path.as_ref())?)))
25 }
26
27 pub fn with_syminfo(mut self, syminfo: SymInfo) -> Self {
29 self.data.syminfo = syminfo;
30 self
31 }
32
33 pub fn data(&self) -> &Data {
35 &self.data
36 }
37}
38
39impl DataProvider for StaticProvider {
40 fn request(&self, symbol: &str, timeframe: Timeframe) -> Result<Data, ProviderError> {
41 let syminfo = &self.data.syminfo;
43 if !symbol.is_empty() && symbol != syminfo.tickerid && symbol != syminfo.ticker {
44 return Err(format!("no data for symbol {symbol:?}").into());
45 }
46 let tf_ms = timeframe
47 .to_millis()
48 .ok_or_else(|| format!("cannot resample to timeframe {:?}", timeframe.period()))?;
49 let native = self
51 .data
52 .bars
53 .windows(2)
54 .next()
55 .map_or(tf_ms, |w| w[1].time - w[0].time);
56 if tf_ms <= native {
57 return Ok(self.data.clone());
58 }
59 Ok(Data {
60 syminfo: self.data.syminfo.clone(),
61 bars: resample(&self.data.bars, tf_ms),
62 })
63 }
64}
65
66pub fn resample(bars: &[Bar], tf_ms: i64) -> Vec<Bar> {
71 let mut out: Vec<Bar> = Vec::new();
72 let mut current = None;
73 for bar in bars {
74 let key = bar.time.div_euclid(tf_ms);
75 if current == Some(key) {
76 let bucket = out.last_mut().expect("a bucket exists once current is set");
77 bucket.high = bucket.high.max(bar.high);
78 bucket.low = bucket.low.min(bar.low);
79 bucket.close = bar.close;
80 bucket.volume += bar.volume;
81 bucket.time = bar.time;
82 } else {
83 let mut bucket = bar.clone();
84 bucket.index = out.len() as u64;
85 out.push(bucket);
86 current = Some(key);
87 }
88 }
89 out
90}
91
92#[derive(serde::Deserialize)]
93struct Row {
94 time: i64,
96 open: f64,
97 high: f64,
98 low: f64,
99 close: f64,
100 volume: f64,
101}
102
103fn read_csv(path: &Path) -> Result<Vec<Ohlcv>, DataError> {
104 let file = std::fs::File::open(path).map_err(|source| DataError::Read {
105 path: path.display().to_string(),
106 source: source.into(),
107 })?;
108 read(file).map_err(|source| DataError::Read {
109 path: path.display().to_string(),
110 source,
111 })
112}
113
114fn read(source: impl Read) -> Result<Vec<Ohlcv>, csv::Error> {
115 csv::ReaderBuilder::new()
116 .comment(Some(b'#'))
117 .trim(csv::Trim::All)
118 .from_reader(source)
119 .deserialize()
120 .map(|row| {
121 let row: Row = row?;
122 Ok(Ohlcv {
123 time: row.time,
124 open: row.open,
125 high: row.high,
126 low: row.low,
127 close: row.close,
128 volume: row.volume,
129 })
130 })
131 .collect()
132}
133
134#[cfg(test)]
135mod tests {
136 use super::read;
137
138 #[test]
139 fn reads_rows_skipping_comments() {
140 let rows = read(
141 "time,open,high,low,close,volume\n\
142 # first bar\n\
143 0,100,105,95,102,1000\n\
144 60000,101,106,96,103,1010\n"
145 .as_bytes(),
146 )
147 .unwrap();
148
149 assert_eq!(rows.len(), 2);
150 assert_eq!(rows[0].time, 0);
151 assert_eq!(rows[0].close, 102.0);
152 assert_eq!(rows[1].time, 60000);
153 assert_eq!(rows[1].volume, 1010.0);
154 }
155
156 #[test]
157 fn columns_are_matched_by_header_not_position() {
158 let rows =
159 read("volume,close,low,high,open,time\n1000,102,95,105,100,0\n".as_bytes()).unwrap();
160
161 assert_eq!(rows[0].open, 100.0);
162 assert_eq!(rows[0].close, 102.0);
163 assert_eq!(rows[0].volume, 1000.0);
164 }
165
166 #[test]
167 fn reports_where_a_bad_row_is() {
168 let error = read(
169 "time,open,high,low,close,volume\n\
170 0,100,105,95,102,1000\n\
171 60000,101,106,96,oops,1010\n"
172 .as_bytes(),
173 )
174 .unwrap_err();
175
176 assert!(error.to_string().contains("line: 3"), "{error}");
177 }
178
179 #[test]
180 fn reports_a_missing_column() {
181 let error = read("time,open,high,low,close\n0,100,105,95,102\n".as_bytes()).unwrap_err();
182
183 assert!(error.to_string().contains("volume"), "{error}");
184 }
185}