Skip to main content

finance_query/adapters/fmp/
insider_trading.rs

1//! Insider trading, congressional trading, CIK mapping, and fail-to-deliver endpoints.
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/// Insider trading transaction record.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[non_exhaustive]
17pub struct InsiderTrade {
18    /// Ticker symbol.
19    pub symbol: Option<String>,
20    /// Filing date.
21    #[serde(rename = "filingDate")]
22    pub filing_date: Option<String>,
23    /// Transaction date.
24    #[serde(rename = "transactionDate")]
25    pub transaction_date: Option<String>,
26    /// Reporting CIK.
27    #[serde(rename = "reportingCik")]
28    pub reporting_cik: Option<String>,
29    /// Reporting person name.
30    #[serde(rename = "reportingName")]
31    pub reporting_name: Option<String>,
32    /// Transaction type (e.g., "P-Purchase", "S-Sale").
33    #[serde(rename = "transactionType")]
34    pub transaction_type: Option<String>,
35    /// Number of securities transacted.
36    #[serde(rename = "securitiesTransacted")]
37    pub securities_transacted: Option<f64>,
38    /// Price per share.
39    pub price: Option<f64>,
40    /// Securities owned after transaction.
41    #[serde(rename = "securitiesOwned")]
42    pub securities_owned: Option<f64>,
43    /// SEC form type.
44    #[serde(rename = "typeOfOwner")]
45    pub type_of_owner: Option<String>,
46    /// Link to SEC filing.
47    pub link: Option<String>,
48}
49
50/// CIK mapping entry.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[non_exhaustive]
53pub struct CikMapping {
54    /// Reporting CIK.
55    #[serde(rename = "reportingCik")]
56    pub reporting_cik: Option<String>,
57    /// Reporting name.
58    #[serde(rename = "reportingName")]
59    pub reporting_name: Option<String>,
60    /// Company CIK.
61    #[serde(rename = "companyCik")]
62    pub company_cik: Option<String>,
63    /// Company name.
64    #[serde(rename = "companyName")]
65    pub company_name: Option<String>,
66}
67
68/// Fail-to-deliver record.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70#[non_exhaustive]
71pub struct FailToDeliver {
72    /// Ticker symbol.
73    pub symbol: Option<String>,
74    /// Date (YYYY-MM-DD).
75    pub date: Option<String>,
76    /// Quantity of fails.
77    pub quantity: Option<f64>,
78    /// Price.
79    pub price: Option<f64>,
80    /// Security name.
81    pub name: Option<String>,
82    /// Description.
83    pub description: Option<String>,
84}
85
86/// Congressional/senate trading record.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88#[non_exhaustive]
89pub struct CongressionalTrade {
90    /// Ticker symbol.
91    pub symbol: Option<String>,
92    /// Transaction date.
93    #[serde(rename = "transactionDate")]
94    pub transaction_date: Option<String>,
95    /// Disclosure date.
96    #[serde(rename = "disclosureDate")]
97    pub disclosure_date: Option<String>,
98    /// First name.
99    #[serde(rename = "firstName")]
100    pub first_name: Option<String>,
101    /// Last name.
102    #[serde(rename = "lastName")]
103    pub last_name: Option<String>,
104    /// Office.
105    pub office: Option<String>,
106    /// District.
107    pub district: Option<String>,
108    /// Transaction type.
109    #[serde(rename = "type")]
110    pub trade_type: Option<String>,
111    /// Amount range.
112    pub amount: Option<String>,
113    /// Asset description.
114    #[serde(rename = "assetDescription")]
115    pub asset_description: Option<String>,
116    /// Link to filing.
117    pub link: Option<String>,
118}
119
120// ============================================================================
121// Public API
122// ============================================================================
123
124/// Fetch insider trading transactions for a symbol.
125pub async fn insider_trading(symbol: &str, limit: u32) -> Result<Vec<InsiderTrade>> {
126    let client = build_client()?;
127    let limit_str = limit.to_string();
128    client
129        .get(
130            "/api/v4/insider-trading",
131            &[("symbol", symbol), ("limit", &limit_str)],
132        )
133        .await
134}
135
136/// Fetch the insider trading RSS feed.
137pub async fn insider_trading_rss(limit: u32) -> Result<Vec<InsiderTrade>> {
138    let client = build_client()?;
139    let limit_str = limit.to_string();
140    client
141        .get("/api/v4/insider-trading-rss-feed", &[("limit", &limit_str)])
142        .await
143}
144
145/// Search CIK mappings by name.
146pub async fn cik_mapper(name: &str) -> Result<Vec<CikMapping>> {
147    let client = build_client()?;
148    client
149        .get("/api/v4/mapper-cik-name", &[("name", name)])
150        .await
151}
152
153/// Fetch CIK mapping by company name/identifier.
154pub async fn cik_mapper_by_company(name: &str) -> Result<Vec<CikMapping>> {
155    let client = build_client()?;
156    let path = format!("/api/v4/mapper-cik-company/{}", encode_path_segment(name));
157    client.get(&path, &[]).await
158}
159
160/// Fetch fail-to-deliver data for a symbol.
161pub async fn fail_to_deliver(symbol: &str) -> Result<Vec<FailToDeliver>> {
162    let client = build_client()?;
163    client
164        .get("/api/v4/fail_to_deliver", &[("symbol", symbol)])
165        .await
166}
167
168/// Fetch congressional (senate) trading data for a symbol.
169pub async fn congressional_trading(symbol: &str) -> Result<Vec<CongressionalTrade>> {
170    let client = build_client()?;
171    client
172        .get("/api/v4/senate-trading", &[("symbol", symbol)])
173        .await
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[tokio::test]
181    async fn test_insider_trading_mock() {
182        let mut server = mockito::Server::new_async().await;
183        let _mock = server
184            .mock("GET", "/api/v4/insider-trading")
185            .match_query(mockito::Matcher::AllOf(vec![
186                mockito::Matcher::UrlEncoded("apikey".into(), "test-key".into()),
187                mockito::Matcher::UrlEncoded("symbol".into(), "AAPL".into()),
188                mockito::Matcher::UrlEncoded("limit".into(), "10".into()),
189            ]))
190            .with_status(200)
191            .with_body(
192                serde_json::json!([
193                    {
194                        "symbol": "AAPL",
195                        "filingDate": "2024-01-15",
196                        "transactionDate": "2024-01-12",
197                        "reportingCik": "0001234567",
198                        "reportingName": "Cook Timothy D",
199                        "transactionType": "S-Sale",
200                        "securitiesTransacted": 50000.0,
201                        "price": 185.50,
202                        "securitiesOwned": 3200000.0,
203                        "typeOfOwner": "officer"
204                    }
205                ])
206                .to_string(),
207            )
208            .create_async()
209            .await;
210
211        let client = super::super::build_test_client(&server.url()).unwrap();
212        let resp: Vec<InsiderTrade> = client
213            .get(
214                "/api/v4/insider-trading",
215                &[("symbol", "AAPL"), ("limit", "10")],
216            )
217            .await
218            .unwrap();
219        assert_eq!(resp.len(), 1);
220        assert_eq!(resp[0].reporting_name.as_deref(), Some("Cook Timothy D"));
221        assert!((resp[0].price.unwrap() - 185.50).abs() < 0.01);
222    }
223
224    #[tokio::test]
225    async fn test_congressional_trading_mock() {
226        let mut server = mockito::Server::new_async().await;
227        let _mock = server
228            .mock("GET", "/api/v4/senate-trading")
229            .match_query(mockito::Matcher::AllOf(vec![
230                mockito::Matcher::UrlEncoded("apikey".into(), "test-key".into()),
231                mockito::Matcher::UrlEncoded("symbol".into(), "AAPL".into()),
232            ]))
233            .with_status(200)
234            .with_body(
235                serde_json::json!([
236                    {
237                        "symbol": "AAPL",
238                        "transactionDate": "2024-01-10",
239                        "disclosureDate": "2024-01-20",
240                        "firstName": "John",
241                        "lastName": "Doe",
242                        "office": "Senate",
243                        "type": "Purchase",
244                        "amount": "$1,001 - $15,000"
245                    }
246                ])
247                .to_string(),
248            )
249            .create_async()
250            .await;
251
252        let client = super::super::build_test_client(&server.url()).unwrap();
253        let resp: Vec<CongressionalTrade> = client
254            .get("/api/v4/senate-trading", &[("symbol", "AAPL")])
255            .await
256            .unwrap();
257        assert_eq!(resp.len(), 1);
258        assert_eq!(resp[0].last_name.as_deref(), Some("Doe"));
259    }
260}