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#[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#[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 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}