Skip to main content

finance_query/adapters/fmp/
institutional.rs

1//! Institutional ownership endpoints: institutional holders, ETF holders, mutual fund holders, Form 13F.
2
3use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::Result;
7
8use super::build_client;
9
10// ============================================================================
11// Response types
12// ============================================================================
13
14/// Institutional holder entry.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[non_exhaustive]
17pub struct InstitutionalHolder {
18    /// Institution name.
19    pub holder: Option<String>,
20    /// Number of shares held.
21    pub shares: Option<f64>,
22    /// Date reported.
23    #[serde(rename = "dateReported")]
24    pub date_reported: Option<String>,
25    /// Change in shares.
26    pub change: Option<f64>,
27}
28
29/// ETF holder entry.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[non_exhaustive]
32pub struct EtfHolder {
33    /// Asset name / ticker.
34    pub asset: Option<String>,
35    /// Number of shares held.
36    #[serde(rename = "sharesNumber")]
37    pub shares_number: Option<f64>,
38    /// Weight in ETF as a percentage.
39    #[serde(rename = "weightPercentage")]
40    pub weight_percentage: Option<f64>,
41    /// Market value.
42    #[serde(rename = "marketValue")]
43    pub market_value: Option<f64>,
44    /// Updated date.
45    pub updated: Option<String>,
46}
47
48/// Mutual fund holder entry.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[non_exhaustive]
51pub struct MutualFundHolder {
52    /// Fund name.
53    pub holder: Option<String>,
54    /// Number of shares held.
55    pub shares: Option<f64>,
56    /// Date reported.
57    #[serde(rename = "dateReported")]
58    pub date_reported: Option<String>,
59    /// Change in shares.
60    pub change: Option<f64>,
61    /// Weight percentage.
62    #[serde(rename = "weightPercentage")]
63    pub weight_percentage: Option<f64>,
64}
65
66/// Form 13F filing entry.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68#[non_exhaustive]
69pub struct Form13F {
70    /// Date.
71    pub date: Option<String>,
72    /// Filing date.
73    #[serde(rename = "fillingDate")]
74    pub filling_date: Option<String>,
75    /// Accepted date.
76    #[serde(rename = "acceptedDate")]
77    pub accepted_date: Option<String>,
78    /// CIK.
79    pub cik: Option<String>,
80    /// CUSIP.
81    pub cusip: Option<String>,
82    /// Ticker symbol.
83    #[serde(rename = "tickercusip")]
84    pub ticker_cusip: Option<String>,
85    /// Company name.
86    #[serde(rename = "nameOfIssuer")]
87    pub name_of_issuer: Option<String>,
88    /// Number of shares.
89    pub shares: Option<f64>,
90    /// Value of holding.
91    pub value: Option<f64>,
92    /// Filing link.
93    pub link: Option<String>,
94}
95
96// ============================================================================
97// Public API
98// ============================================================================
99
100/// Fetch institutional holders of a stock.
101pub async fn institutional_holders(symbol: &str) -> Result<Vec<InstitutionalHolder>> {
102    let client = build_client()?;
103    let path = format!(
104        "/api/v3/institutional-holder/{}",
105        encode_path_segment(symbol)
106    );
107    client.get(&path, &[]).await
108}
109
110/// Fetch ETF holders of a stock.
111pub async fn etf_holders(symbol: &str) -> Result<Vec<EtfHolder>> {
112    let client = build_client()?;
113    let path = format!("/api/v3/etf-holder/{}", encode_path_segment(symbol));
114    client.get(&path, &[]).await
115}
116
117/// Fetch mutual fund holders of a stock.
118pub async fn mutual_fund_holders(symbol: &str) -> Result<Vec<MutualFundHolder>> {
119    let client = build_client()?;
120    let path = format!("/api/v3/mutual-fund-holder/{}", encode_path_segment(symbol));
121    client.get(&path, &[]).await
122}
123
124/// Fetch Form 13F filings for a CIK.
125///
126/// * `cik` - Central Index Key
127/// * `date` - Filing date (YYYY-MM-DD)
128pub async fn form_13f(cik: &str, date: &str) -> Result<Vec<Form13F>> {
129    let client = build_client()?;
130    let path = format!("/api/v3/form-thirteen/{}", encode_path_segment(cik));
131    client.get(&path, &[("date", date)]).await
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[tokio::test]
139    async fn test_institutional_holders_mock() {
140        let mut server = mockito::Server::new_async().await;
141        let _mock = server
142            .mock("GET", "/api/v3/institutional-holder/AAPL")
143            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
144                "apikey".into(),
145                "test-key".into(),
146            )]))
147            .with_status(200)
148            .with_body(
149                serde_json::json!([
150                    {
151                        "holder": "Vanguard Group Inc",
152                        "shares": 1300000000.0,
153                        "dateReported": "2024-01-15",
154                        "change": 5000000.0
155                    }
156                ])
157                .to_string(),
158            )
159            .create_async()
160            .await;
161
162        let client = super::super::build_test_client(&server.url()).unwrap();
163        let resp: Vec<InstitutionalHolder> = client
164            .get("/api/v3/institutional-holder/AAPL", &[])
165            .await
166            .unwrap();
167        assert_eq!(resp.len(), 1);
168        assert_eq!(resp[0].holder.as_deref(), Some("Vanguard Group Inc"));
169    }
170
171    #[tokio::test]
172    async fn test_etf_holders_mock() {
173        let mut server = mockito::Server::new_async().await;
174        let _mock = server
175            .mock("GET", "/api/v3/etf-holder/SPY")
176            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
177                "apikey".into(),
178                "test-key".into(),
179            )]))
180            .with_status(200)
181            .with_body(
182                serde_json::json!([
183                    {
184                        "asset": "AAPL",
185                        "sharesNumber": 170000000.0,
186                        "weightPercentage": 7.2,
187                        "marketValue": 31450000000.0,
188                        "updated": "2024-01-15"
189                    }
190                ])
191                .to_string(),
192            )
193            .create_async()
194            .await;
195
196        let client = super::super::build_test_client(&server.url()).unwrap();
197        let resp: Vec<EtfHolder> = client.get("/api/v3/etf-holder/SPY", &[]).await.unwrap();
198        assert_eq!(resp.len(), 1);
199        assert_eq!(resp[0].asset.as_deref(), Some("AAPL"));
200        assert!((resp[0].weight_percentage.unwrap() - 7.2).abs() < 0.01);
201    }
202}