Skip to main content

termichart_data/
csv_adapter.rs

1use std::fs;
2
3use termichart_core::{Candle, DataAdapter, Point, Result, TermichartError};
4
5/// A [`DataAdapter`] that parses candle or point data from CSV text.
6pub struct CsvAdapter {
7    data: String,
8}
9
10impl CsvAdapter {
11    /// Creates a new `CsvAdapter` from a raw CSV string (including headers).
12    pub fn from_str(s: &str) -> Self {
13        Self {
14            data: s.to_owned(),
15        }
16    }
17
18    /// Creates a new `CsvAdapter` by reading the contents of a file.
19    pub fn from_file(path: &str) -> Result<Self> {
20        let data = fs::read_to_string(path)?;
21        Ok(Self { data })
22    }
23}
24
25impl DataAdapter for CsvAdapter {
26    fn candles(&self) -> Result<Vec<Candle>> {
27        let mut reader = csv::Reader::from_reader(self.data.as_bytes());
28        let mut candles = Vec::new();
29
30        for result in reader.records() {
31            let record = result.map_err(|e| TermichartError::ParseError(e.to_string()))?;
32
33            let time: f64 = record
34                .get(0)
35                .ok_or_else(|| TermichartError::ParseError("missing time field".into()))?
36                .trim()
37                .parse()
38                .map_err(|e: std::num::ParseFloatError| TermichartError::ParseError(e.to_string()))?;
39
40            let open: f64 = record
41                .get(1)
42                .ok_or_else(|| TermichartError::ParseError("missing open field".into()))?
43                .trim()
44                .parse()
45                .map_err(|e: std::num::ParseFloatError| TermichartError::ParseError(e.to_string()))?;
46
47            let high: f64 = record
48                .get(2)
49                .ok_or_else(|| TermichartError::ParseError("missing high field".into()))?
50                .trim()
51                .parse()
52                .map_err(|e: std::num::ParseFloatError| TermichartError::ParseError(e.to_string()))?;
53
54            let low: f64 = record
55                .get(3)
56                .ok_or_else(|| TermichartError::ParseError("missing low field".into()))?
57                .trim()
58                .parse()
59                .map_err(|e: std::num::ParseFloatError| TermichartError::ParseError(e.to_string()))?;
60
61            let close: f64 = record
62                .get(4)
63                .ok_or_else(|| TermichartError::ParseError("missing close field".into()))?
64                .trim()
65                .parse()
66                .map_err(|e: std::num::ParseFloatError| TermichartError::ParseError(e.to_string()))?;
67
68            let volume: f64 = record
69                .get(5)
70                .ok_or_else(|| TermichartError::ParseError("missing volume field".into()))?
71                .trim()
72                .parse()
73                .map_err(|e: std::num::ParseFloatError| TermichartError::ParseError(e.to_string()))?;
74
75            candles.push(Candle {
76                time,
77                open,
78                high,
79                low,
80                close,
81                volume,
82            });
83        }
84
85        Ok(candles)
86    }
87
88    fn points(&self) -> Result<Vec<Point>> {
89        let mut reader = csv::Reader::from_reader(self.data.as_bytes());
90        let mut points = Vec::new();
91
92        for result in reader.records() {
93            let record = result.map_err(|e| TermichartError::ParseError(e.to_string()))?;
94
95            let x: f64 = record
96                .get(0)
97                .ok_or_else(|| TermichartError::ParseError("missing x field".into()))?
98                .trim()
99                .parse()
100                .map_err(|e: std::num::ParseFloatError| TermichartError::ParseError(e.to_string()))?;
101
102            let y: f64 = record
103                .get(1)
104                .ok_or_else(|| TermichartError::ParseError("missing y field".into()))?
105                .trim()
106                .parse()
107                .map_err(|e: std::num::ParseFloatError| TermichartError::ParseError(e.to_string()))?;
108
109            points.push(Point { x, y });
110        }
111
112        Ok(points)
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn parse_candles_from_csv() {
122        let csv = "time,open,high,low,close,volume\n1.0,10.0,15.0,9.0,14.0,100.0\n2.0,14.0,16.0,13.0,15.0,200.0\n";
123        let adapter = CsvAdapter::from_str(csv);
124        let candles = adapter.candles().unwrap();
125        assert_eq!(candles.len(), 2);
126        assert_eq!(candles[0].open, 10.0);
127        assert_eq!(candles[1].close, 15.0);
128    }
129
130    #[test]
131    fn parse_points_from_csv() {
132        let csv = "x,y\n1.0,2.0\n3.0,4.0\n";
133        let adapter = CsvAdapter::from_str(csv);
134        let points = adapter.points().unwrap();
135        assert_eq!(points.len(), 2);
136        assert_eq!(points[0].x, 1.0);
137        assert_eq!(points[1].y, 4.0);
138    }
139
140    #[test]
141    fn missing_field_returns_parse_error() {
142        let csv = "time,open,high,low,close\n1.0,10.0,15.0,9.0,14.0\n";
143        let adapter = CsvAdapter::from_str(csv);
144        assert!(adapter.candles().is_err());
145    }
146}