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
74#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum Interval {
77 #[serde(rename = "1m")]
78 OneMinute,
79 #[serde(rename = "3m")]
80 ThreeMinutes,
81 #[serde(rename = "5m")]
82 FiveMinutes,
83 #[serde(rename = "10m")]
84 TenMinutes,
85 #[serde(rename = "15m")]
86 FifteenMinutes,
87 #[serde(rename = "20m")]
88 TwentyMinutes,
89 #[serde(rename = "30m")]
90 ThirtyMinutes,
91 #[serde(rename = "65m")]
92 SixtyFiveMinutes,
93 #[serde(rename = "95m")]
94 NinetyFiveMinutes,
95 #[serde(rename = "1h")]
96 OneHour,
97 #[serde(rename = "1d")]
98 Daily,
99 #[serde(rename = "1wk")]
100 Weekly,
101 #[serde(rename = "1mo")]
102 Monthly,
103}
104
105impl Interval {
106 pub fn as_str(&self) -> &'static str {
107 match self {
108 Interval::OneMinute => "1m",
109 Interval::ThreeMinutes => "3m",
110 Interval::FiveMinutes => "5m",
111 Interval::TenMinutes => "10m",
112 Interval::FifteenMinutes => "15m",
113 Interval::TwentyMinutes => "20m",
114 Interval::ThirtyMinutes => "30m",
115 Interval::SixtyFiveMinutes => "65m",
116 Interval::NinetyFiveMinutes => "95m",
117 Interval::OneHour => "1h",
118 Interval::Daily => "1d",
119 Interval::Weekly => "1wk",
120 Interval::Monthly => "1mo",
121 }
122 }
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
126#[serde(rename_all = "snake_case")]
127pub struct HistoricalData {
128 pub open: f64,
129 pub high: f64,
130 pub low: f64,
131 pub close: f64,
132 pub volume: i64,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub adj_close: Option<f64>,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub sma: Option<std::collections::HashMap<String, f64>>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub ema: Option<std::collections::HashMap<String, f64>>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
142#[serde(rename_all = "snake_case")]
143pub struct HistoricalResponse {
144 pub data: std::collections::HashMap<String, HistoricalData>,
145}
146
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use proptest::prelude::*;
152
153 fn approx_eq(a: f64, b: f64) -> bool {
155 (a - b).abs() < 1e-10
156 }
157
158 fn approx_eq_opt(a: Option<f64>, b: Option<f64>) -> bool {
159 match (a, b) {
160 (Some(x), Some(y)) => approx_eq(x, y),
161 (None, None) => true,
162 _ => false,
163 }
164 }
165
166 proptest! {
169 #![proptest_config(ProptestConfig::with_cases(100))]
170
171 #[test]
172 fn historical_data_roundtrip(
173 open in 0.01f64..10000.0f64,
174 high in 0.01f64..10000.0f64,
175 low in 0.01f64..10000.0f64,
176 close in 0.01f64..10000.0f64,
177 volume in 0i64..1_000_000_000i64,
178 adj_close in proptest::option::of(0.01f64..10000.0f64),
179 ) {
180 let data = HistoricalData {
181 open,
182 high,
183 low,
184 close,
185 volume,
186 adj_close,
187 sma: None,
188 ema: None,
189 };
190
191 let json = serde_json::to_string(&data).unwrap();
192 let parsed: HistoricalData = serde_json::from_str(&json).unwrap();
193
194 prop_assert!(approx_eq(data.open, parsed.open), "open mismatch");
196 prop_assert!(approx_eq(data.high, parsed.high), "high mismatch");
197 prop_assert!(approx_eq(data.low, parsed.low), "low mismatch");
198 prop_assert!(approx_eq(data.close, parsed.close), "close mismatch");
199 prop_assert_eq!(data.volume, parsed.volume);
200 prop_assert!(approx_eq_opt(data.adj_close, parsed.adj_close), "adj_close mismatch");
201 }
202
203 #[test]
204 fn time_range_roundtrip(range in prop_oneof![
205 Just(TimeRange::Day),
206 Just(TimeRange::FiveDays),
207 Just(TimeRange::OneMonth),
208 Just(TimeRange::ThreeMonths),
209 Just(TimeRange::SixMonths),
210 Just(TimeRange::Year),
211 Just(TimeRange::TwoYears),
212 Just(TimeRange::FiveYears),
213 Just(TimeRange::TenYears),
214 Just(TimeRange::Ytd),
215 Just(TimeRange::Max),
216 ]) {
217 let json = serde_json::to_string(&range).unwrap();
218 let parsed: TimeRange = serde_json::from_str(&json).unwrap();
219
220 prop_assert_eq!(range.as_str(), parsed.as_str());
221 }
222
223 #[test]
224 fn interval_roundtrip(interval in prop_oneof![
225 Just(Interval::OneMinute),
226 Just(Interval::ThreeMinutes),
227 Just(Interval::FiveMinutes),
228 Just(Interval::TenMinutes),
229 Just(Interval::FifteenMinutes),
230 Just(Interval::TwentyMinutes),
231 Just(Interval::ThirtyMinutes),
232 Just(Interval::SixtyFiveMinutes),
233 Just(Interval::NinetyFiveMinutes),
234 Just(Interval::OneHour),
235 Just(Interval::Daily),
236 Just(Interval::Weekly),
237 Just(Interval::Monthly),
238 ]) {
239 let json = serde_json::to_string(&interval).unwrap();
240 let parsed: Interval = serde_json::from_str(&json).unwrap();
241
242 prop_assert_eq!(interval.as_str(), parsed.as_str());
243 }
244 }
245}