finance_query_core/models/
historical.rs1use serde::{Deserialize, Serialize};
2use std::collections::HashSet;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5#[allow(clippy::upper_case_acronyms)]
6pub enum IndicatorType {
7 SMA,
8 EMA,
9}
10
11impl IndicatorType {
12 pub fn parse(s: &str) -> Option<Self> {
13 match s.to_lowercase().as_str() {
14 "sma" => Some(IndicatorType::SMA),
15 "ema" => Some(IndicatorType::EMA),
16 _ => None,
17 }
18 }
19
20 pub fn parse_list(s: &str) -> HashSet<Self> {
21 s.split(',')
22 .map(|s| s.trim())
23 .filter_map(IndicatorType::parse)
24 .collect()
25 }
26}
27
28#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum TimeRange {
31 #[serde(rename = "1d")]
32 Day,
33 #[serde(rename = "5d")]
34 FiveDays,
35 #[serde(rename = "1mo")]
36 OneMonth,
37 #[serde(rename = "3mo")]
38 ThreeMonths,
39 #[serde(rename = "6mo")]
40 SixMonths,
41 #[serde(rename = "1y")]
42 Year,
43 #[serde(rename = "2y")]
44 TwoYears,
45 #[serde(rename = "5y")]
46 FiveYears,
47 #[serde(rename = "10y")]
48 TenYears,
49 #[serde(rename = "ytd")]
50 Ytd,
51 #[serde(rename = "max")]
52 Max,
53}
54
55impl TimeRange {
56 pub fn as_str(&self) -> &'static str {
57 match self {
58 TimeRange::Day => "1d",
59 TimeRange::FiveDays => "5d",
60 TimeRange::OneMonth => "1mo",
61 TimeRange::ThreeMonths => "3mo",
62 TimeRange::SixMonths => "6mo",
63 TimeRange::Year => "1y",
64 TimeRange::TwoYears => "2y",
65 TimeRange::FiveYears => "5y",
66 TimeRange::TenYears => "10y",
67 TimeRange::Ytd => "ytd",
68 TimeRange::Max => "max",
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum Interval {
76 #[serde(rename = "1m")]
77 OneMinute,
78 #[serde(rename = "3m")]
79 ThreeMinutes,
80 #[serde(rename = "5m")]
81 FiveMinutes,
82 #[serde(rename = "10m")]
83 TenMinutes,
84 #[serde(rename = "15m")]
85 FifteenMinutes,
86 #[serde(rename = "20m")]
87 TwentyMinutes,
88 #[serde(rename = "30m")]
89 ThirtyMinutes,
90 #[serde(rename = "65m")]
91 SixtyFiveMinutes,
92 #[serde(rename = "95m")]
93 NinetyFiveMinutes,
94 #[serde(rename = "1h")]
95 OneHour,
96 #[serde(rename = "1d")]
97 Daily,
98 #[serde(rename = "1wk")]
99 Weekly,
100 #[serde(rename = "1mo")]
101 Monthly,
102}
103
104impl Interval {
105 pub fn as_str(&self) -> &'static str {
106 match self {
107 Interval::OneMinute => "1m",
108 Interval::ThreeMinutes => "3m",
109 Interval::FiveMinutes => "5m",
110 Interval::TenMinutes => "10m",
111 Interval::FifteenMinutes => "15m",
112 Interval::TwentyMinutes => "20m",
113 Interval::ThirtyMinutes => "30m",
114 Interval::SixtyFiveMinutes => "65m",
115 Interval::NinetyFiveMinutes => "95m",
116 Interval::OneHour => "1h",
117 Interval::Daily => "1d",
118 Interval::Weekly => "1wk",
119 Interval::Monthly => "1mo",
120 }
121 }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(rename_all = "snake_case")]
126pub struct HistoricalData {
127 pub open: f64,
128 pub high: f64,
129 pub low: f64,
130 pub close: f64,
131 pub volume: i64,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub adj_close: Option<f64>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub sma: Option<std::collections::HashMap<String, f64>>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub ema: Option<std::collections::HashMap<String, f64>>,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
141#[serde(rename_all = "snake_case")]
142pub struct HistoricalResponse {
143 pub data: std::collections::HashMap<String, HistoricalData>,
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use proptest::prelude::*;
150
151 fn approx_eq(a: f64, b: f64) -> bool {
153 (a - b).abs() < 1e-10
154 }
155
156 fn approx_eq_opt(a: Option<f64>, b: Option<f64>) -> bool {
157 match (a, b) {
158 (Some(x), Some(y)) => approx_eq(x, y),
159 (None, None) => true,
160 _ => false,
161 }
162 }
163
164 proptest! {
167 #![proptest_config(ProptestConfig::with_cases(100))]
168
169 #[test]
170 fn historical_data_roundtrip(
171 open in 0.01f64..10000.0f64,
172 high in 0.01f64..10000.0f64,
173 low in 0.01f64..10000.0f64,
174 close in 0.01f64..10000.0f64,
175 volume in 0i64..1_000_000_000i64,
176 adj_close in proptest::option::of(0.01f64..10000.0f64),
177 ) {
178 let data = HistoricalData {
179 open,
180 high,
181 low,
182 close,
183 volume,
184 adj_close,
185 sma: None,
186 ema: None,
187 };
188
189 let json = serde_json::to_string(&data).unwrap();
190 let parsed: HistoricalData = serde_json::from_str(&json).unwrap();
191
192 prop_assert!(approx_eq(data.open, parsed.open), "open mismatch");
194 prop_assert!(approx_eq(data.high, parsed.high), "high mismatch");
195 prop_assert!(approx_eq(data.low, parsed.low), "low mismatch");
196 prop_assert!(approx_eq(data.close, parsed.close), "close mismatch");
197 prop_assert_eq!(data.volume, parsed.volume);
198 prop_assert!(approx_eq_opt(data.adj_close, parsed.adj_close), "adj_close mismatch");
199 }
200
201 #[test]
202 fn time_range_roundtrip(range in prop_oneof![
203 Just(TimeRange::Day),
204 Just(TimeRange::FiveDays),
205 Just(TimeRange::OneMonth),
206 Just(TimeRange::ThreeMonths),
207 Just(TimeRange::SixMonths),
208 Just(TimeRange::Year),
209 Just(TimeRange::TwoYears),
210 Just(TimeRange::FiveYears),
211 Just(TimeRange::TenYears),
212 Just(TimeRange::Ytd),
213 Just(TimeRange::Max),
214 ]) {
215 let json = serde_json::to_string(&range).unwrap();
216 let parsed: TimeRange = serde_json::from_str(&json).unwrap();
217
218 prop_assert_eq!(range.as_str(), parsed.as_str());
219 }
220
221 #[test]
222 fn interval_roundtrip(interval in prop_oneof![
223 Just(Interval::OneMinute),
224 Just(Interval::ThreeMinutes),
225 Just(Interval::FiveMinutes),
226 Just(Interval::TenMinutes),
227 Just(Interval::FifteenMinutes),
228 Just(Interval::TwentyMinutes),
229 Just(Interval::ThirtyMinutes),
230 Just(Interval::SixtyFiveMinutes),
231 Just(Interval::NinetyFiveMinutes),
232 Just(Interval::OneHour),
233 Just(Interval::Daily),
234 Just(Interval::Weekly),
235 Just(Interval::Monthly),
236 ]) {
237 let json = serde_json::to_string(&interval).unwrap();
238 let parsed: Interval = serde_json::from_str(&json).unwrap();
239
240 prop_assert_eq!(interval.as_str(), parsed.as_str());
241 }
242 }
243}