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#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct PriceTarget {
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub current: Option<f64>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub mean: Option<f64>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub median: Option<f64>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub low: Option<f64>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub high: Option<f64>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct EarningsEstimate {
79 pub estimates: HashMap<String, serde_json::Value>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct RevenueEstimate {
84 pub estimates: HashMap<String, serde_json::Value>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct EarningsHistoryItem {
89 pub date: DateTime<Utc>,
90 #[serde(skip_serializing_if = "Option::is_none", rename = "epsActual")]
91 pub eps_actual: Option<f64>,
92 #[serde(skip_serializing_if = "Option::is_none", rename = "epsEstimate")]
93 pub eps_estimate: Option<f64>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub surprise: Option<f64>,
96 #[serde(skip_serializing_if = "Option::is_none", rename = "surprisePercent")]
97 pub surprise_percent: Option<f64>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct EpsTrend {
103 pub period: String,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub current: Option<f64>,
106 #[serde(skip_serializing_if = "Option::is_none", rename = "7daysAgo")]
107 pub days_7_ago: Option<f64>,
108 #[serde(skip_serializing_if = "Option::is_none", rename = "30daysAgo")]
109 pub days_30_ago: Option<f64>,
110 #[serde(skip_serializing_if = "Option::is_none", rename = "60daysAgo")]
111 pub days_60_ago: Option<f64>,
112 #[serde(skip_serializing_if = "Option::is_none", rename = "90daysAgo")]
113 pub days_90_ago: Option<f64>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct EpsRevisions {
119 pub period: String,
120 #[serde(skip_serializing_if = "Option::is_none", rename = "upLast7days")]
121 pub up_last_7_days: Option<i32>,
122 #[serde(skip_serializing_if = "Option::is_none", rename = "upLast30days")]
123 pub up_last_30_days: Option<i32>,
124 #[serde(skip_serializing_if = "Option::is_none", rename = "downLast7days")]
125 pub down_last_7_days: Option<i32>,
126 #[serde(skip_serializing_if = "Option::is_none", rename = "downLast30days")]
127 pub down_last_30_days: Option<i32>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct GrowthEstimate {
133 pub period: String,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub stock: Option<f64>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub industry: Option<f64>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub sector: Option<f64>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub index: Option<f64>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct RecommendationsResponse {
147 pub symbol: String,
148 pub recommendations: Vec<RecommendationData>,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct UpgradesDowngradesResponse {
153 pub symbol: String,
154 #[serde(rename = "upgradesDowngrades")]
155 pub upgrades_downgrades: Vec<UpgradeDowngrade>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct PriceTargetsResponse {
160 pub symbol: String,
161 #[serde(rename = "priceTargets")]
162 pub price_targets: PriceTarget,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct EarningsEstimateResponse {
167 pub symbol: String,
168 #[serde(rename = "earningsEstimate")]
169 pub earnings_estimate: EarningsEstimate,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct RevenueEstimateResponse {
174 pub symbol: String,
175 #[serde(rename = "revenueEstimate")]
176 pub revenue_estimate: RevenueEstimate,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct EarningsHistoryResponse {
181 pub symbol: String,
182 #[serde(rename = "earningsHistory")]
183 pub earnings_history: Vec<EarningsHistoryItem>,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct EpsTrendResponse {
188 pub symbol: String,
189 #[serde(rename = "epsTrend")]
190 pub eps_trend: Vec<EpsTrend>,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct EpsRevisionsResponse {
195 pub symbol: String,
196 #[serde(rename = "epsRevisions")]
197 pub eps_revisions: Vec<EpsRevisions>,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct GrowthEstimatesResponse {
202 pub symbol: String,
203 #[serde(rename = "growthEstimates")]
204 pub growth_estimates: Vec<GrowthEstimate>,
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use proptest::prelude::*;
211
212 fn optional_i32() -> impl Strategy<Value = Option<i32>> {
213 proptest::option::of(0i32..1000i32)
214 }
215
216 proptest! {
219 #![proptest_config(ProptestConfig::with_cases(100))]
220
221 #[test]
222 fn recommendation_data_roundtrip(
223 period in "[0-9]{4}-[0-9]{2}",
224 strong_buy in optional_i32(),
225 buy in optional_i32(),
226 hold in optional_i32(),
227 sell in optional_i32(),
228 strong_sell in optional_i32(),
229 ) {
230 let rec = RecommendationData {
231 period: period.clone(),
232 strong_buy,
233 buy,
234 hold,
235 sell,
236 strong_sell,
237 };
238
239 let json = serde_json::to_string(&rec).unwrap();
240 let parsed: RecommendationData = serde_json::from_str(&json).unwrap();
241
242 prop_assert_eq!(rec.period, parsed.period);
243 prop_assert_eq!(rec.strong_buy, parsed.strong_buy);
244 prop_assert_eq!(rec.buy, parsed.buy);
245 prop_assert_eq!(rec.hold, parsed.hold);
246 prop_assert_eq!(rec.sell, parsed.sell);
247 prop_assert_eq!(rec.strong_sell, parsed.strong_sell);
248 }
249
250 #[test]
251 fn analysis_type_roundtrip(at in prop_oneof![
252 Just(AnalysisType::Recommendations),
253 Just(AnalysisType::UpgradesDowngrades),
254 Just(AnalysisType::PriceTargets),
255 Just(AnalysisType::EarningsEstimate),
256 Just(AnalysisType::RevenueEstimate),
257 Just(AnalysisType::EarningsHistory),
258 ]) {
259 let json = serde_json::to_string(&at).unwrap();
260 let parsed: AnalysisType = serde_json::from_str(&json).unwrap();
261
262 prop_assert_eq!(at.as_str(), parsed.as_str());
263 }
264 }
265}