finance_query/adapters/fmp/
insider_trading.rs1use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::Result;
7
8use super::build_client;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
16#[non_exhaustive]
17pub struct InsiderTrade {
18 pub symbol: Option<String>,
20 #[serde(rename = "filingDate")]
22 pub filing_date: Option<String>,
23 #[serde(rename = "transactionDate")]
25 pub transaction_date: Option<String>,
26 #[serde(rename = "reportingCik")]
28 pub reporting_cik: Option<String>,
29 #[serde(rename = "reportingName")]
31 pub reporting_name: Option<String>,
32 #[serde(rename = "transactionType")]
34 pub transaction_type: Option<String>,
35 #[serde(rename = "securitiesTransacted")]
37 pub securities_transacted: Option<f64>,
38 pub price: Option<f64>,
40 #[serde(rename = "securitiesOwned")]
42 pub securities_owned: Option<f64>,
43 #[serde(rename = "typeOfOwner")]
45 pub type_of_owner: Option<String>,
46 pub link: Option<String>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52#[non_exhaustive]
53pub struct CikMapping {
54 #[serde(rename = "reportingCik")]
56 pub reporting_cik: Option<String>,
57 #[serde(rename = "reportingName")]
59 pub reporting_name: Option<String>,
60 #[serde(rename = "companyCik")]
62 pub company_cik: Option<String>,
63 #[serde(rename = "companyName")]
65 pub company_name: Option<String>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70#[non_exhaustive]
71pub struct FailToDeliver {
72 pub symbol: Option<String>,
74 pub date: Option<String>,
76 pub quantity: Option<f64>,
78 pub price: Option<f64>,
80 pub name: Option<String>,
82 pub description: Option<String>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88#[non_exhaustive]
89pub struct CongressionalTrade {
90 pub symbol: Option<String>,
92 #[serde(rename = "transactionDate")]
94 pub transaction_date: Option<String>,
95 #[serde(rename = "disclosureDate")]
97 pub disclosure_date: Option<String>,
98 #[serde(rename = "firstName")]
100 pub first_name: Option<String>,
101 #[serde(rename = "lastName")]
103 pub last_name: Option<String>,
104 pub office: Option<String>,
106 pub district: Option<String>,
108 #[serde(rename = "type")]
110 pub trade_type: Option<String>,
111 pub amount: Option<String>,
113 #[serde(rename = "assetDescription")]
115 pub asset_description: Option<String>,
116 pub link: Option<String>,
118}
119
120pub 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
136pub 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
145pub 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
153pub 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
160pub 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
168pub 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}