finance_query_core/models/
analysts.rs1use 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 RecommendationsResponse {
104 pub symbol: String,
105 pub recommendations: Vec<RecommendationData>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct UpgradesDowngradesResponse {
110 pub symbol: String,
111 #[serde(rename = "upgradesDowngrades")]
112 pub upgrades_downgrades: Vec<UpgradeDowngrade>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct PriceTargetsResponse {
117 pub symbol: String,
118 #[serde(rename = "priceTargets")]
119 pub price_targets: PriceTarget,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct EarningsEstimateResponse {
124 pub symbol: String,
125 #[serde(rename = "earningsEstimate")]
126 pub earnings_estimate: EarningsEstimate,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct RevenueEstimateResponse {
131 pub symbol: String,
132 #[serde(rename = "revenueEstimate")]
133 pub revenue_estimate: RevenueEstimate,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct EarningsHistoryResponse {
138 pub symbol: String,
139 #[serde(rename = "earningsHistory")]
140 pub earnings_history: Vec<EarningsHistoryItem>,
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use proptest::prelude::*;
147
148 fn optional_i32() -> impl Strategy<Value = Option<i32>> {
149 proptest::option::of(0i32..1000i32)
150 }
151
152 proptest! {
155 #![proptest_config(ProptestConfig::with_cases(100))]
156
157 #[test]
158 fn recommendation_data_roundtrip(
159 period in "[0-9]{4}-[0-9]{2}",
160 strong_buy in optional_i32(),
161 buy in optional_i32(),
162 hold in optional_i32(),
163 sell in optional_i32(),
164 strong_sell in optional_i32(),
165 ) {
166 let rec = RecommendationData {
167 period: period.clone(),
168 strong_buy,
169 buy,
170 hold,
171 sell,
172 strong_sell,
173 };
174
175 let json = serde_json::to_string(&rec).unwrap();
176 let parsed: RecommendationData = serde_json::from_str(&json).unwrap();
177
178 prop_assert_eq!(rec.period, parsed.period);
179 prop_assert_eq!(rec.strong_buy, parsed.strong_buy);
180 prop_assert_eq!(rec.buy, parsed.buy);
181 prop_assert_eq!(rec.hold, parsed.hold);
182 prop_assert_eq!(rec.sell, parsed.sell);
183 prop_assert_eq!(rec.strong_sell, parsed.strong_sell);
184 }
185
186 #[test]
187 fn analysis_type_roundtrip(at in prop_oneof![
188 Just(AnalysisType::Recommendations),
189 Just(AnalysisType::UpgradesDowngrades),
190 Just(AnalysisType::PriceTargets),
191 Just(AnalysisType::EarningsEstimate),
192 Just(AnalysisType::RevenueEstimate),
193 Just(AnalysisType::EarningsHistory),
194 ]) {
195 let json = serde_json::to_string(&at).unwrap();
196 let parsed: AnalysisType = serde_json::from_str(&json).unwrap();
197
198 prop_assert_eq!(at.as_str(), parsed.as_str());
199 }
200 }
201}