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 from_str(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
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct InstitutionalHolder {
57 pub holder: String,
58 pub shares: i64,
59 #[serde(rename = "dateReported")]
60 pub date_reported: DateTime<Utc>,
61 #[serde(skip_serializing_if = "Option::is_none", rename = "percentOut")]
62 pub percent_out: Option<f64>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub value: Option<i64>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct MutualFundHolder {
69 pub holder: String,
70 pub shares: i64,
71 #[serde(rename = "dateReported")]
72 pub date_reported: DateTime<Utc>,
73 #[serde(skip_serializing_if = "Option::is_none", rename = "percentOut")]
74 pub percent_out: Option<f64>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub value: Option<i64>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct InsiderTransaction {
81 #[serde(rename = "startDate")]
82 pub start_date: DateTime<Utc>,
83 pub insider: String,
84 pub position: String,
85 pub transaction: String,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub shares: Option<i64>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub value: Option<i64>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub ownership: Option<String>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct InsiderPurchase {
96 pub period: String,
97 #[serde(skip_serializing_if = "Option::is_none", rename = "purchasesShares")]
98 pub purchases_shares: Option<i64>,
99 #[serde(skip_serializing_if = "Option::is_none", rename = "purchasesTransactions")]
100 pub purchases_transactions: Option<i64>,
101 #[serde(skip_serializing_if = "Option::is_none", rename = "salesShares")]
102 pub sales_shares: Option<i64>,
103 #[serde(skip_serializing_if = "Option::is_none", rename = "salesTransactions")]
104 pub sales_transactions: Option<i64>,
105 #[serde(skip_serializing_if = "Option::is_none", rename = "netShares")]
106 pub net_shares: Option<i64>,
107 #[serde(skip_serializing_if = "Option::is_none", rename = "netTransactions")]
108 pub net_transactions: Option<i64>,
109 #[serde(skip_serializing_if = "Option::is_none", rename = "totalInsiderShares")]
110 pub total_insider_shares: Option<i64>,
111 #[serde(skip_serializing_if = "Option::is_none", rename = "netPercentInsiderShares")]
112 pub net_percent_insider_shares: Option<f64>,
113 #[serde(skip_serializing_if = "Option::is_none", rename = "buyPercentInsiderShares")]
114 pub buy_percent_insider_shares: Option<f64>,
115 #[serde(skip_serializing_if = "Option::is_none", rename = "sellPercentInsiderShares")]
116 pub sell_percent_insider_shares: Option<f64>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct InsiderRosterMember {
121 pub name: String,
122 pub position: String,
123 #[serde(skip_serializing_if = "Option::is_none", rename = "mostRecentTransaction")]
124 pub most_recent_transaction: Option<String>,
125 #[serde(skip_serializing_if = "Option::is_none", rename = "latestTransactionDate")]
126 pub latest_transaction_date: Option<DateTime<Utc>>,
127 #[serde(skip_serializing_if = "Option::is_none", rename = "sharesOwnedDirectly")]
128 pub shares_owned_directly: Option<i64>,
129 #[serde(skip_serializing_if = "Option::is_none", rename = "sharesOwnedIndirectly")]
130 pub shares_owned_indirectly: Option<i64>,
131 #[serde(skip_serializing_if = "Option::is_none", rename = "positionDirectDate")]
132 pub position_direct_date: Option<DateTime<Utc>>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct MajorHoldersResponse {
138 pub symbol: String,
139 pub breakdown: MajorHoldersBreakdown,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct InstitutionalHoldersResponse {
144 pub symbol: String,
145 pub holders: Vec<InstitutionalHolder>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct MutualFundHoldersResponse {
150 pub symbol: String,
151 pub holders: Vec<MutualFundHolder>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct InsiderTransactionsResponse {
156 pub symbol: String,
157 pub transactions: Vec<InsiderTransaction>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct InsiderPurchasesResponse {
162 pub symbol: String,
163 pub summary: InsiderPurchase,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct InsiderRosterResponse {
168 pub symbol: String,
169 pub roster: Vec<InsiderRosterMember>,
170}
171
172#[derive(Debug, Clone)]
174pub struct HoldersData {
175 pub symbol: String,
176 #[allow(dead_code)]
177 pub holder_type: HolderType,
178 pub major_breakdown: Option<MajorHoldersBreakdown>,
179 pub institutional_holders: Option<Vec<InstitutionalHolder>>,
180 pub mutualfund_holders: Option<Vec<MutualFundHolder>>,
181 pub insider_transactions: Option<Vec<InsiderTransaction>>,
182 pub insider_purchases: Option<InsiderPurchase>,
183 pub insider_roster: Option<Vec<InsiderRosterMember>>,
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use proptest::prelude::*;
190
191 fn optional_f64() -> impl Strategy<Value = Option<f64>> {
192 proptest::option::of(0.0f64..100.0f64)
193 }
194
195 fn optional_i64() -> impl Strategy<Value = Option<i64>> {
196 proptest::option::of(0i64..1_000_000_000i64)
197 }
198
199 proptest! {
202 #![proptest_config(ProptestConfig::with_cases(100))]
203
204 #[test]
205 fn holder_type_roundtrip(ht in prop_oneof![
206 Just(HolderType::Major),
207 Just(HolderType::Institutional),
208 Just(HolderType::MutualFund),
209 Just(HolderType::InsiderTransactions),
210 Just(HolderType::InsiderPurchases),
211 Just(HolderType::InsiderRoster),
212 ]) {
213 let json = serde_json::to_string(&ht).unwrap();
214 let parsed: HolderType = serde_json::from_str(&json).unwrap();
215
216 prop_assert_eq!(ht.as_str(), parsed.as_str());
217 }
218
219 #[test]
220 fn institutional_holder_roundtrip(
221 holder in "[A-Za-z ]{1,50}",
222 shares in 0i64..1_000_000_000i64,
223 percent_out in optional_f64(),
224 value in optional_i64(),
225 ) {
226 let ih = InstitutionalHolder {
227 holder: holder.clone(),
228 shares,
229 date_reported: Utc::now(),
230 percent_out,
231 value,
232 };
233
234 let json = serde_json::to_string(&ih).unwrap();
235 let parsed: InstitutionalHolder = serde_json::from_str(&json).unwrap();
236
237 prop_assert_eq!(ih.holder, parsed.holder);
238 prop_assert_eq!(ih.shares, parsed.shares);
239 prop_assert_eq!(ih.value, parsed.value);
240 }
241
242 #[test]
243 fn insider_purchase_roundtrip(
244 period in "[A-Za-z0-9 ]{1,20}",
245 purchases_shares in optional_i64(),
246 sales_shares in optional_i64(),
247 ) {
248 let ip = InsiderPurchase {
249 period: period.clone(),
250 purchases_shares,
251 purchases_transactions: None,
252 sales_shares,
253 sales_transactions: None,
254 net_shares: None,
255 net_transactions: None,
256 total_insider_shares: None,
257 net_percent_insider_shares: None,
258 buy_percent_insider_shares: None,
259 sell_percent_insider_shares: None,
260 };
261
262 let json = serde_json::to_string(&ip).unwrap();
263 let parsed: InsiderPurchase = serde_json::from_str(&json).unwrap();
264
265 prop_assert_eq!(ip.period, parsed.period);
266 prop_assert_eq!(ip.purchases_shares, parsed.purchases_shares);
267 prop_assert_eq!(ip.sales_shares, parsed.sales_shares);
268 }
269 }
270}