1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum AnalysisType {
8 #[serde(rename = "recommendations")]
9 Recommendations,
10 #[serde(rename = "upgrades_downgrades")]
11 UpgradesDowngrades,
12 #[serde(rename = "price_targets")]
13 PriceTargets,
14 #[serde(rename = "earnings_estimate")]
15 EarningsEstimate,
16 #[serde(rename = "revenue_estimate")]
17 RevenueEstimate,
18 #[serde(rename = "earnings_history")]
19 EarningsHistory,
20}
21
22impl AnalysisType {
23 pub fn as_str(&self) -> &'static str {
24 match self {
25 AnalysisType::Recommendations => "recommendations",
26 AnalysisType::UpgradesDowngrades => "upgrades_downgrades",
27 AnalysisType::PriceTargets => "price_targets",
28 AnalysisType::EarningsEstimate => "earnings_estimate",
29 AnalysisType::RevenueEstimate => "revenue_estimate",
30 AnalysisType::EarningsHistory => "earnings_history",
31 }
32 }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct RecommendationData {
37 pub period: String,
38 #[serde(skip_serializing_if = "Option::is_none", rename = "strongBuy")]
39 pub strong_buy: Option<i32>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub buy: Option<i32>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub hold: Option<i32>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub sell: Option<i32>,
46 #[serde(skip_serializing_if = "Option::is_none", rename = "strongSell")]
47 pub strong_sell: Option<i32>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct UpgradeDowngrade {
52 pub firm: String,
53 #[serde(skip_serializing_if = "Option::is_none", rename = "toGrade")]
54 pub to_grade: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none", rename = "fromGrade")]
56 pub from_grade: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub action: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub date: Option<DateTime<Utc>>,
61}
62
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct PriceTarget {
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub current: Option<f64>,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub mean: Option<f64>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub median: Option<f64>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub low: Option<f64>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub high: Option<f64>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct EarningsEstimate {
80 pub estimates: HashMap<String, serde_json::Value>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct RevenueEstimate {
85 pub estimates: HashMap<String, serde_json::Value>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct EarningsHistoryItem {
90 pub date: DateTime<Utc>,
91 #[serde(skip_serializing_if = "Option::is_none", rename = "epsActual")]
92 pub eps_actual: Option<f64>,
93 #[serde(skip_serializing_if = "Option::is_none", rename = "epsEstimate")]
94 pub eps_estimate: Option<f64>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub surprise: Option<f64>,
97 #[serde(skip_serializing_if = "Option::is_none", rename = "surprisePercent")]
98 pub surprise_percent: Option<f64>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct EpsTrend {
104 pub period: String,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub current: Option<f64>,
107 #[serde(skip_serializing_if = "Option::is_none", rename = "7daysAgo")]
108 pub days_7_ago: Option<f64>,
109 #[serde(skip_serializing_if = "Option::is_none", rename = "30daysAgo")]
110 pub days_30_ago: Option<f64>,
111 #[serde(skip_serializing_if = "Option::is_none", rename = "60daysAgo")]
112 pub days_60_ago: Option<f64>,
113 #[serde(skip_serializing_if = "Option::is_none", rename = "90daysAgo")]
114 pub days_90_ago: Option<f64>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct EpsRevisions {
120 pub period: String,
121 #[serde(skip_serializing_if = "Option::is_none", rename = "upLast7days")]
122 pub up_last_7_days: Option<i32>,
123 #[serde(skip_serializing_if = "Option::is_none", rename = "upLast30days")]
124 pub up_last_30_days: Option<i32>,
125 #[serde(skip_serializing_if = "Option::is_none", rename = "downLast7days")]
126 pub down_last_7_days: Option<i32>,
127 #[serde(skip_serializing_if = "Option::is_none", rename = "downLast30days")]
128 pub down_last_30_days: Option<i32>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct GrowthEstimate {
134 pub period: String,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub stock: Option<f64>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub industry: Option<f64>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub sector: Option<f64>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub index: Option<f64>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct RecommendationsResponse {
148 pub symbol: String,
149 pub recommendations: Vec<RecommendationData>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct UpgradesDowngradesResponse {
154 pub symbol: String,
155 #[serde(rename = "upgradesDowngrades")]
156 pub upgrades_downgrades: Vec<UpgradeDowngrade>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct PriceTargetsResponse {
161 pub symbol: String,
162 #[serde(rename = "priceTargets")]
163 pub price_targets: PriceTarget,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct EarningsEstimateResponse {
168 pub symbol: String,
169 #[serde(rename = "earningsEstimate")]
170 pub earnings_estimate: EarningsEstimate,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct RevenueEstimateResponse {
175 pub symbol: String,
176 #[serde(rename = "revenueEstimate")]
177 pub revenue_estimate: RevenueEstimate,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct EarningsHistoryResponse {
182 pub symbol: String,
183 #[serde(rename = "earningsHistory")]
184 pub earnings_history: Vec<EarningsHistoryItem>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct EpsTrendResponse {
189 pub symbol: String,
190 #[serde(rename = "epsTrend")]
191 pub eps_trend: Vec<EpsTrend>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct EpsRevisionsResponse {
196 pub symbol: String,
197 #[serde(rename = "epsRevisions")]
198 pub eps_revisions: Vec<EpsRevisions>,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct GrowthEstimatesResponse {
203 pub symbol: String,
204 #[serde(rename = "growthEstimates")]
205 pub growth_estimates: Vec<GrowthEstimate>,
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use proptest::prelude::*;
212
213 fn optional_i32() -> impl Strategy<Value = Option<i32>> {
214 proptest::option::of(0i32..1000i32)
215 }
216
217 proptest! {
220 #![proptest_config(ProptestConfig::with_cases(100))]
221
222 #[test]
223 fn recommendation_data_roundtrip(
224 period in "[0-9]{4}-[0-9]{2}",
225 strong_buy in optional_i32(),
226 buy in optional_i32(),
227 hold in optional_i32(),
228 sell in optional_i32(),
229 strong_sell in optional_i32(),
230 ) {
231 let rec = RecommendationData {
232 period: period.clone(),
233 strong_buy,
234 buy,
235 hold,
236 sell,
237 strong_sell,
238 };
239
240 let json = serde_json::to_string(&rec).unwrap();
241 let parsed: RecommendationData = serde_json::from_str(&json).unwrap();
242
243 prop_assert_eq!(rec.period, parsed.period);
244 prop_assert_eq!(rec.strong_buy, parsed.strong_buy);
245 prop_assert_eq!(rec.buy, parsed.buy);
246 prop_assert_eq!(rec.hold, parsed.hold);
247 prop_assert_eq!(rec.sell, parsed.sell);
248 prop_assert_eq!(rec.strong_sell, parsed.strong_sell);
249 }
250
251 #[test]
252 fn analysis_type_roundtrip(at in prop_oneof![
253 Just(AnalysisType::Recommendations),
254 Just(AnalysisType::UpgradesDowngrades),
255 Just(AnalysisType::PriceTargets),
256 Just(AnalysisType::EarningsEstimate),
257 Just(AnalysisType::RevenueEstimate),
258 Just(AnalysisType::EarningsHistory),
259 ]) {
260 let json = serde_json::to_string(&at).unwrap();
261 let parsed: AnalysisType = serde_json::from_str(&json).unwrap();
262
263 prop_assert_eq!(at.as_str(), parsed.as_str());
264 }
265 }
266}