finance_query_core/models/
analysts.rs

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// Response types
102#[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    // **Feature: crate-extraction, Property 1: Model Serialization Round-Trip**
153    // **Validates: Requirements 2.2**
154    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}