Skip to main content

finance_query_core/models/
holders.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 HolderType {
8    #[serde(rename = "major")]
9    Major,
10    #[serde(rename = "institutional")]
11    Institutional,
12    #[serde(rename = "mutualfund")]
13    MutualFund,
14    #[serde(rename = "insider_transactions")]
15    InsiderTransactions,
16    #[serde(rename = "insider_purchases")]
17    InsiderPurchases,
18    #[serde(rename = "insider_roster")]
19    InsiderRoster,
20}
21
22impl HolderType {
23    pub fn as_str(&self) -> &'static str {
24        match self {
25            HolderType::Major => "major",
26            HolderType::Institutional => "institutional",
27            HolderType::MutualFund => "mutualfund",
28            HolderType::InsiderTransactions => "insider_transactions",
29            HolderType::InsiderPurchases => "insider_purchases",
30            HolderType::InsiderRoster => "insider_roster",
31        }
32    }
33
34    #[allow(dead_code)]
35    pub fn parse(s: &str) -> Option<Self> {
36        match s {
37            "major" => Some(HolderType::Major),
38            "institutional" => Some(HolderType::Institutional),
39            "mutualfund" => Some(HolderType::MutualFund),
40            "insider_transactions" => Some(HolderType::InsiderTransactions),
41            "insider_purchases" => Some(HolderType::InsiderPurchases),
42            "insider_roster" => Some(HolderType::InsiderRoster),
43            _ => None,
44        }
45    }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct MajorHoldersBreakdown {
50    #[serde(rename = "breakdownData")]
51    pub breakdown_data: HashMap<String, serde_json::Value>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct InstitutionalHolder {
56    pub holder: String,
57    pub shares: i64,
58    #[serde(rename = "dateReported")]
59    pub date_reported: DateTime<Utc>,
60    #[serde(skip_serializing_if = "Option::is_none", rename = "percentOut")]
61    pub percent_out: Option<f64>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub value: Option<i64>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct MutualFundHolder {
68    pub holder: String,
69    pub shares: i64,
70    #[serde(rename = "dateReported")]
71    pub date_reported: DateTime<Utc>,
72    #[serde(skip_serializing_if = "Option::is_none", rename = "percentOut")]
73    pub percent_out: Option<f64>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub value: Option<i64>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct InsiderTransaction {
80    #[serde(rename = "startDate")]
81    pub start_date: DateTime<Utc>,
82    pub insider: String,
83    pub position: String,
84    pub transaction: String,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub shares: Option<i64>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub value: Option<i64>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub ownership: Option<String>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct InsiderPurchase {
95    pub period: String,
96    #[serde(skip_serializing_if = "Option::is_none", rename = "purchasesShares")]
97    pub purchases_shares: Option<i64>,
98    #[serde(
99        skip_serializing_if = "Option::is_none",
100        rename = "purchasesTransactions"
101    )]
102    pub purchases_transactions: Option<i64>,
103    #[serde(skip_serializing_if = "Option::is_none", rename = "salesShares")]
104    pub sales_shares: Option<i64>,
105    #[serde(skip_serializing_if = "Option::is_none", rename = "salesTransactions")]
106    pub sales_transactions: Option<i64>,
107    #[serde(skip_serializing_if = "Option::is_none", rename = "netShares")]
108    pub net_shares: Option<i64>,
109    #[serde(skip_serializing_if = "Option::is_none", rename = "netTransactions")]
110    pub net_transactions: Option<i64>,
111    #[serde(skip_serializing_if = "Option::is_none", rename = "totalInsiderShares")]
112    pub total_insider_shares: Option<i64>,
113    #[serde(
114        skip_serializing_if = "Option::is_none",
115        rename = "netPercentInsiderShares"
116    )]
117    pub net_percent_insider_shares: Option<f64>,
118    #[serde(
119        skip_serializing_if = "Option::is_none",
120        rename = "buyPercentInsiderShares"
121    )]
122    pub buy_percent_insider_shares: Option<f64>,
123    #[serde(
124        skip_serializing_if = "Option::is_none",
125        rename = "sellPercentInsiderShares"
126    )]
127    pub sell_percent_insider_shares: Option<f64>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct InsiderRosterMember {
132    pub name: String,
133    pub position: String,
134    #[serde(
135        skip_serializing_if = "Option::is_none",
136        rename = "mostRecentTransaction"
137    )]
138    pub most_recent_transaction: Option<String>,
139    #[serde(
140        skip_serializing_if = "Option::is_none",
141        rename = "latestTransactionDate"
142    )]
143    pub latest_transaction_date: Option<DateTime<Utc>>,
144    #[serde(
145        skip_serializing_if = "Option::is_none",
146        rename = "sharesOwnedDirectly"
147    )]
148    pub shares_owned_directly: Option<i64>,
149    #[serde(
150        skip_serializing_if = "Option::is_none",
151        rename = "sharesOwnedIndirectly"
152    )]
153    pub shares_owned_indirectly: Option<i64>,
154    #[serde(skip_serializing_if = "Option::is_none", rename = "positionDirectDate")]
155    pub position_direct_date: Option<DateTime<Utc>>,
156}
157
158// Response types
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct MajorHoldersResponse {
161    pub symbol: String,
162    pub breakdown: MajorHoldersBreakdown,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct InstitutionalHoldersResponse {
167    pub symbol: String,
168    pub holders: Vec<InstitutionalHolder>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct MutualFundHoldersResponse {
173    pub symbol: String,
174    pub holders: Vec<MutualFundHolder>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct InsiderTransactionsResponse {
179    pub symbol: String,
180    pub transactions: Vec<InsiderTransaction>,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct InsiderPurchasesResponse {
185    pub symbol: String,
186    pub summary: InsiderPurchase,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct InsiderRosterResponse {
191    pub symbol: String,
192    pub roster: Vec<InsiderRosterMember>,
193}
194
195// Internal data structure used by service layer
196#[derive(Debug, Clone)]
197pub struct HoldersData {
198    pub symbol: String,
199    #[allow(dead_code)]
200    pub holder_type: HolderType,
201    pub major_breakdown: Option<MajorHoldersBreakdown>,
202    pub institutional_holders: Option<Vec<InstitutionalHolder>>,
203    pub mutualfund_holders: Option<Vec<MutualFundHolder>>,
204    pub insider_transactions: Option<Vec<InsiderTransaction>>,
205    pub insider_purchases: Option<InsiderPurchase>,
206    pub insider_roster: Option<Vec<InsiderRosterMember>>,
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use proptest::prelude::*;
213
214    fn optional_f64() -> impl Strategy<Value = Option<f64>> {
215        proptest::option::of(0.0f64..100.0f64)
216    }
217
218    fn optional_i64() -> impl Strategy<Value = Option<i64>> {
219        proptest::option::of(0i64..1_000_000_000i64)
220    }
221
222    // **Feature: crate-extraction, Property 1: Model Serialization Round-Trip**
223    // **Validates: Requirements 2.2**
224    proptest! {
225        #![proptest_config(ProptestConfig::with_cases(100))]
226
227        #[test]
228        fn holder_type_roundtrip(ht in prop_oneof![
229            Just(HolderType::Major),
230            Just(HolderType::Institutional),
231            Just(HolderType::MutualFund),
232            Just(HolderType::InsiderTransactions),
233            Just(HolderType::InsiderPurchases),
234            Just(HolderType::InsiderRoster),
235        ]) {
236            let json = serde_json::to_string(&ht).unwrap();
237            let parsed: HolderType = serde_json::from_str(&json).unwrap();
238
239            prop_assert_eq!(ht.as_str(), parsed.as_str());
240        }
241
242        #[test]
243        fn institutional_holder_roundtrip(
244            holder in "[A-Za-z ]{1,50}",
245            shares in 0i64..1_000_000_000i64,
246            percent_out in optional_f64(),
247            value in optional_i64(),
248        ) {
249            let ih = InstitutionalHolder {
250                holder: holder.clone(),
251                shares,
252                date_reported: Utc::now(),
253                percent_out,
254                value,
255            };
256
257            let json = serde_json::to_string(&ih).unwrap();
258            let parsed: InstitutionalHolder = serde_json::from_str(&json).unwrap();
259
260            prop_assert_eq!(ih.holder, parsed.holder);
261            prop_assert_eq!(ih.shares, parsed.shares);
262            prop_assert_eq!(ih.value, parsed.value);
263        }
264
265        #[test]
266        fn insider_purchase_roundtrip(
267            period in "[A-Za-z0-9 ]{1,20}",
268            purchases_shares in optional_i64(),
269            sales_shares in optional_i64(),
270        ) {
271            let ip = InsiderPurchase {
272                period: period.clone(),
273                purchases_shares,
274                purchases_transactions: None,
275                sales_shares,
276                sales_transactions: None,
277                net_shares: None,
278                net_transactions: None,
279                total_insider_shares: None,
280                net_percent_insider_shares: None,
281                buy_percent_insider_shares: None,
282                sell_percent_insider_shares: None,
283            };
284
285            let json = serde_json::to_string(&ip).unwrap();
286            let parsed: InsiderPurchase = serde_json::from_str(&json).unwrap();
287
288            prop_assert_eq!(ip.period, parsed.period);
289            prop_assert_eq!(ip.purchases_shares, parsed.purchases_shares);
290            prop_assert_eq!(ip.sales_shares, parsed.sales_shares);
291        }
292    }
293}