1use crate::{
4 client::FmpClient,
5 error::Result,
6 models::bulk::{
7 BulkDataInfo, BulkEarningsEstimate, BulkEtfHolding, BulkFinancialStatement,
8 BulkHistoricalPricesMeta, BulkInsiderTrade, BulkInstitutionalHolding, BulkStockPrice,
9 },
10};
11use serde::Serialize;
12
13pub struct Bulk {
19 client: FmpClient,
20}
21
22impl Bulk {
23 pub(crate) fn new(client: FmpClient) -> Self {
24 Self { client }
25 }
26
27 pub async fn get_bulk_stock_prices(&self) -> Result<Vec<BulkStockPrice>> {
49 #[derive(Serialize)]
50 struct Query<'a> {
51 apikey: &'a str,
52 }
53
54 let url = self.client.build_url("/v3/quotes/nyse");
55 self.client
56 .get_with_query(
57 &url,
58 &Query {
59 apikey: self.client.api_key(),
60 },
61 )
62 .await
63 }
64
65 pub async fn get_bulk_prices_sample(&self, limit: usize) -> Result<Vec<BulkStockPrice>> {
86 let all_prices = self.get_bulk_stock_prices().await?;
87 Ok(all_prices.into_iter().take(limit).collect())
88 }
89
90 pub async fn get_bulk_financial_statements(
112 &self,
113 period: &str,
114 year: Option<i32>,
115 ) -> Result<Vec<BulkFinancialStatement>> {
116 #[derive(Serialize)]
117 struct Query<'a> {
118 period: &'a str,
119 apikey: &'a str,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 year: Option<i32>,
122 }
123
124 let url = self.client.build_url("/v4/financial-statements-list");
125 self.client
126 .get_with_query(
127 &url,
128 &Query {
129 period,
130 apikey: self.client.api_key(),
131 year,
132 },
133 )
134 .await
135 }
136
137 pub async fn get_bulk_financials_sample(
144 &self,
145 period: &str,
146 year: Option<i32>,
147 limit: usize,
148 ) -> Result<Vec<BulkFinancialStatement>> {
149 let all_statements = self.get_bulk_financial_statements(period, year).await?;
150 Ok(all_statements.into_iter().take(limit).collect())
151 }
152
153 pub async fn get_bulk_etf_holdings(&self) -> Result<Vec<BulkEtfHolding>> {
169 #[derive(Serialize)]
170 struct Query<'a> {
171 apikey: &'a str,
172 }
173
174 let url = self.client.build_url("/v4/etf-holdings");
175 self.client
176 .get_with_query(
177 &url,
178 &Query {
179 apikey: self.client.api_key(),
180 },
181 )
182 .await
183 }
184
185 pub async fn get_bulk_etf_holdings_sample(&self, limit: usize) -> Result<Vec<BulkEtfHolding>> {
190 let all_holdings = self.get_bulk_etf_holdings().await?;
191 Ok(all_holdings.into_iter().take(limit).collect())
192 }
193
194 pub async fn get_bulk_insider_trades(&self) -> Result<Vec<BulkInsiderTrade>> {
210 #[derive(Serialize)]
211 struct Query<'a> {
212 apikey: &'a str,
213 }
214
215 let url = self.client.build_url("/v4/insider-trading-list");
216 self.client
217 .get_with_query(
218 &url,
219 &Query {
220 apikey: self.client.api_key(),
221 },
222 )
223 .await
224 }
225
226 pub async fn get_bulk_insider_trades_sample(
231 &self,
232 limit: usize,
233 ) -> Result<Vec<BulkInsiderTrade>> {
234 let all_trades = self.get_bulk_insider_trades().await?;
235 Ok(all_trades.into_iter().take(limit).collect())
236 }
237
238 pub async fn get_bulk_institutional_holdings(
257 &self,
258 date: Option<&str>,
259 ) -> Result<Vec<BulkInstitutionalHolding>> {
260 #[derive(Serialize)]
261 struct Query<'a> {
262 apikey: &'a str,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 date: Option<&'a str>,
265 }
266
267 let url = self.client.build_url("/v4/institutional-holdings-list");
268 self.client
269 .get_with_query(
270 &url,
271 &Query {
272 apikey: self.client.api_key(),
273 date,
274 },
275 )
276 .await
277 }
278
279 pub async fn get_bulk_institutional_holdings_sample(
285 &self,
286 date: Option<&str>,
287 limit: usize,
288 ) -> Result<Vec<BulkInstitutionalHolding>> {
289 let all_holdings = self.get_bulk_institutional_holdings(date).await?;
290 Ok(all_holdings.into_iter().take(limit).collect())
291 }
292
293 pub async fn get_bulk_earnings_estimates(
310 &self,
311 period: &str,
312 ) -> Result<Vec<BulkEarningsEstimate>> {
313 #[derive(Serialize)]
314 struct Query<'a> {
315 period: &'a str,
316 apikey: &'a str,
317 }
318
319 let url = self.client.build_url("/v4/earnings-estimates");
320 self.client
321 .get_with_query(
322 &url,
323 &Query {
324 period,
325 apikey: self.client.api_key(),
326 },
327 )
328 .await
329 }
330
331 pub async fn get_bulk_earnings_estimates_sample(
337 &self,
338 period: &str,
339 limit: usize,
340 ) -> Result<Vec<BulkEarningsEstimate>> {
341 let all_estimates = self.get_bulk_earnings_estimates(period).await?;
342 Ok(all_estimates.into_iter().take(limit).collect())
343 }
344
345 pub async fn get_bulk_data_info(&self) -> Result<Vec<BulkDataInfo>> {
368 #[derive(Serialize)]
369 struct Query<'a> {
370 apikey: &'a str,
371 }
372
373 let url = self.client.build_url("/v4/bulk-data-info");
374 self.client
375 .get_with_query(
376 &url,
377 &Query {
378 apikey: self.client.api_key(),
379 },
380 )
381 .await
382 }
383
384 pub async fn get_historical_prices_metadata(
410 &self,
411 exchange: Option<&str>,
412 ) -> Result<Vec<BulkHistoricalPricesMeta>> {
413 #[derive(Serialize)]
414 struct Query<'a> {
415 apikey: &'a str,
416 #[serde(skip_serializing_if = "Option::is_none")]
417 exchange: Option<&'a str>,
418 }
419
420 let url = self.client.build_url("/v4/bulk-historical-metadata");
421 self.client
422 .get_with_query(
423 &url,
424 &Query {
425 apikey: self.client.api_key(),
426 exchange,
427 },
428 )
429 .await
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 fn create_test_client() -> FmpClient {
438 FmpClient::builder().api_key("test_key").build().unwrap()
439 }
440
441 fn get_test_bulk() -> Bulk {
442 Bulk::new(create_test_client())
443 }
444
445 #[test]
446 fn test_new() {
447 let client = create_test_client();
448 let _bulk = Bulk::new(client);
449 }
450
451 #[tokio::test]
454 #[ignore] async fn test_get_bulk_prices_sample() {
456 let bulk = get_test_bulk();
457 let result = bulk.get_bulk_prices_sample(5).await;
458 assert!(result.is_ok());
459
460 let prices = result.unwrap();
461 assert!(prices.len() <= 5);
462 if !prices.is_empty() {
463 let price = &prices[0];
464 assert!(price.symbol.is_some() || price.price.is_some());
465 }
466 }
467
468 #[tokio::test]
469 #[ignore] async fn test_get_bulk_financials_sample() {
471 let bulk = get_test_bulk();
472 let result = bulk
473 .get_bulk_financials_sample("annual", Some(2023), 3)
474 .await;
475 assert!(result.is_ok());
476
477 let statements = result.unwrap();
478 assert!(statements.len() <= 3);
479 }
480
481 #[tokio::test]
482 #[ignore] async fn test_get_bulk_etf_holdings_sample() {
484 let bulk = get_test_bulk();
485 let result = bulk.get_bulk_etf_holdings_sample(5).await;
486 assert!(result.is_ok());
487
488 let holdings = result.unwrap();
489 assert!(holdings.len() <= 5);
490 }
491
492 #[tokio::test]
493 #[ignore] async fn test_get_bulk_insider_trades_sample() {
495 let bulk = get_test_bulk();
496 let result = bulk.get_bulk_insider_trades_sample(3).await;
497 assert!(result.is_ok());
498
499 let trades = result.unwrap();
500 assert!(trades.len() <= 3);
501 }
502
503 #[tokio::test]
504 #[ignore] async fn test_get_bulk_institutional_holdings_sample() {
506 let bulk = get_test_bulk();
507 let result = bulk.get_bulk_institutional_holdings_sample(None, 3).await;
508 assert!(result.is_ok());
509
510 let holdings = result.unwrap();
511 assert!(holdings.len() <= 3);
512 }
513
514 #[tokio::test]
515 #[ignore] async fn test_get_bulk_earnings_estimates_sample() {
517 let bulk = get_test_bulk();
518 let result = bulk.get_bulk_earnings_estimates_sample("quarter", 3).await;
519 assert!(result.is_ok());
520
521 let estimates = result.unwrap();
522 assert!(estimates.len() <= 3);
523 }
524
525 #[tokio::test]
526 #[ignore] async fn test_get_bulk_data_info() {
528 let bulk = get_test_bulk();
529 let result = bulk.get_bulk_data_info().await;
530 assert!(result.is_ok());
531
532 let info = result.unwrap();
533 for dataset in &info {
535 if let Some(name) = &dataset.dataset {
536 println!("Available dataset: {}", name);
537 }
538 }
539 }
540
541 #[tokio::test]
542 #[ignore] async fn test_get_historical_prices_metadata() {
544 let bulk = get_test_bulk();
545 let result = bulk.get_historical_prices_metadata(Some("NYSE")).await;
546 assert!(result.is_ok());
547
548 let meta = result.unwrap();
549 for info in &meta {
551 if let Some(count) = info.symbols_count {
552 assert!(count > 0);
553 }
554 }
555 }
556
557 #[tokio::test]
559 #[ignore] async fn test_get_bulk_stock_prices() {
561 let bulk = get_test_bulk();
562 let result = bulk.get_bulk_stock_prices().await;
564 match result {
566 Ok(prices) => {
567 println!("Successfully retrieved {} stock prices", prices.len());
568 if !prices.is_empty() {
570 assert!(prices[0].symbol.is_some() || prices[0].price.is_some());
571 }
572 }
573 Err(e) => {
574 println!("Bulk download failed (expected for large datasets): {}", e);
576 }
577 }
578 }
579
580 #[tokio::test]
581 #[ignore] async fn test_get_bulk_financial_statements() {
583 let bulk = get_test_bulk();
584 let result = bulk
585 .get_bulk_financial_statements("annual", Some(2023))
586 .await;
587 match result {
588 Ok(statements) => {
589 println!(
590 "Successfully retrieved {} financial statements",
591 statements.len()
592 );
593 if !statements.is_empty() {
594 assert!(statements[0].symbol.is_some());
595 }
596 }
597 Err(e) => {
598 println!("Bulk financial download failed (expected): {}", e);
599 }
600 }
601 }
602
603 #[tokio::test]
604 #[ignore] async fn test_get_bulk_etf_holdings() {
606 let bulk = get_test_bulk();
607 let result = bulk.get_bulk_etf_holdings().await;
608 match result {
609 Ok(holdings) => {
610 println!("Successfully retrieved {} ETF holdings", holdings.len());
611 if !holdings.is_empty() {
612 assert!(holdings[0].etf_symbol.is_some() || holdings[0].asset_symbol.is_some());
613 }
614 }
615 Err(e) => {
616 println!("Bulk ETF holdings download failed (expected): {}", e);
617 }
618 }
619 }
620
621 #[tokio::test]
622 #[ignore] async fn test_get_bulk_insider_trades() {
624 let bulk = get_test_bulk();
625 let result = bulk.get_bulk_insider_trades().await;
626 match result {
627 Ok(trades) => {
628 println!("Successfully retrieved {} insider trades", trades.len());
629 if !trades.is_empty() {
630 assert!(trades[0].symbol.is_some());
631 }
632 }
633 Err(e) => {
634 println!("Bulk insider trades download failed (expected): {}", e);
635 }
636 }
637 }
638
639 #[tokio::test]
640 #[ignore] async fn test_get_bulk_institutional_holdings() {
642 let bulk = get_test_bulk();
643 let result = bulk.get_bulk_institutional_holdings(None).await;
644 match result {
645 Ok(holdings) => {
646 println!(
647 "Successfully retrieved {} institutional holdings",
648 holdings.len()
649 );
650 if !holdings.is_empty() {
651 assert!(holdings[0].ticker_symbol.is_some());
652 }
653 }
654 Err(e) => {
655 println!(
656 "Bulk institutional holdings download failed (expected): {}",
657 e
658 );
659 }
660 }
661 }
662
663 #[tokio::test]
664 #[ignore] async fn test_get_bulk_earnings_estimates() {
666 let bulk = get_test_bulk();
667 let result = bulk.get_bulk_earnings_estimates("quarter").await;
668 match result {
669 Ok(estimates) => {
670 println!(
671 "Successfully retrieved {} earnings estimates",
672 estimates.len()
673 );
674 if !estimates.is_empty() {
675 assert!(estimates[0].symbol.is_some());
676 }
677 }
678 Err(e) => {
679 println!("Bulk earnings estimates download failed (expected): {}", e);
680 }
681 }
682 }
683
684 #[tokio::test]
686 #[ignore] async fn test_get_bulk_prices_sample_edge_cases() {
688 let bulk = get_test_bulk();
689
690 let result = bulk.get_bulk_prices_sample(0).await;
692 match result {
693 Ok(prices) => assert!(prices.is_empty()),
694 Err(_) => {} }
696
697 let result = bulk.get_bulk_prices_sample(1000000).await;
699 match result {
700 Ok(prices) => {
701 assert!(prices.len() <= 1000000);
703 }
704 Err(_) => {} }
706 }
707
708 #[tokio::test]
709 #[ignore] async fn test_get_bulk_financials_sample_edge_cases() {
711 let bulk = get_test_bulk();
712
713 let result = bulk
715 .get_bulk_financials_sample("invalid_period", Some(2023), 3)
716 .await;
717 match result {
718 Ok(statements) => assert!(statements.is_empty()),
719 Err(_) => {} }
721
722 let result = bulk
724 .get_bulk_financials_sample("annual", Some(2030), 3)
725 .await;
726 match result {
727 Ok(statements) => assert!(statements.is_empty()), Err(_) => {} }
730
731 let result = bulk
733 .get_bulk_financials_sample("annual", Some(1900), 3)
734 .await;
735 match result {
736 Ok(statements) => assert!(statements.is_empty()),
737 Err(_) => {} }
739 }
740
741 #[tokio::test]
742 #[ignore] async fn test_get_historical_prices_metadata_edge_cases() {
744 let bulk = get_test_bulk();
745
746 let result = bulk
748 .get_historical_prices_metadata(Some("INVALID_EXCHANGE"))
749 .await;
750 match result {
751 Ok(meta) => assert!(meta.is_empty()),
752 Err(_) => {} }
754
755 let result = bulk.get_historical_prices_metadata(None).await;
757 assert!(result.is_ok()); }
759
760 #[tokio::test]
761 #[ignore] async fn test_bulk_sample_data_validation() {
763 let bulk = get_test_bulk();
764 let result = bulk.get_bulk_prices_sample(3).await;
765 assert!(result.is_ok());
766
767 let prices = result.unwrap();
768 for price in &prices {
769 if let Some(ref symbol) = price.symbol {
771 assert!(!symbol.is_empty());
772 assert!(symbol.len() <= 10); }
774 if let Some(price_val) = price.price {
775 assert!(price_val > 0.0); }
777 if let Some(volume) = price.volume {
778 assert!(volume >= 0); }
780 }
781 }
782
783 #[test]
785 fn test_bulk_stock_price_serialization() {
786 let price = BulkStockPrice {
787 symbol: Some("AAPL".to_string()),
788 name: Some("Apple Inc".to_string()),
789 price: Some(150.25),
790 change: Some(2.50),
791 changes_percentage: Some(1.69),
792 volume: Some(50000000),
793 market_cap: Some(2500000000000.0),
794 pe: Some(28.5),
795 exchange: Some("NASDAQ".to_string()),
796 day_low: Some(148.50),
797 day_high: Some(151.00),
798 year_low: Some(124.17),
799 year_high: Some(182.94),
800 avg_volume: Some(60000000),
801 open: Some(149.50),
802 previous_close: Some(147.75),
803 eps: Some(5.28),
804 shares_outstanding: Some(16500000000),
805 timestamp: Some(1640995200),
806 };
807
808 let json = serde_json::to_string(&price).unwrap();
809 let deserialized: BulkStockPrice = serde_json::from_str(&json).unwrap();
810 assert_eq!(deserialized.symbol, price.symbol);
811 assert_eq!(deserialized.price, price.price);
812 }
813
814 #[test]
815 fn test_bulk_data_info_serialization() {
816 let info = BulkDataInfo {
817 dataset: Some("stock_prices".to_string()),
818 last_updated: Some("2024-01-15T10:30:00Z".to_string()),
819 file_size: Some(104857600), record_count: Some(1000000),
821 download_url: Some("https://api.fmp.com/bulk/stock_prices.csv".to_string()),
822 format: Some("CSV".to_string()),
823 compression: Some("gzip".to_string()),
824 schema_version: Some("v1.2".to_string()),
825 };
826
827 let json = serde_json::to_string(&info).unwrap();
828 let deserialized: BulkDataInfo = serde_json::from_str(&json).unwrap();
829 assert_eq!(deserialized.dataset, info.dataset);
830 assert_eq!(deserialized.file_size, info.file_size);
831 }
832
833 #[tokio::test]
835 #[ignore = "requires FMP API key"]
836 async fn test_get_bulk_income_statements() {
837 let client = FmpClient::new().unwrap();
838 let result = client.bulk().get_bulk_income_statements().await;
839 assert!(result.is_ok());
840 let statements = result.unwrap();
841 if !statements.is_empty() {
842 assert!(statements[0].symbol.is_some());
843 assert!(statements[0].revenue.is_some() || statements[0].net_income.is_some());
844 }
845 }
846
847 #[tokio::test]
848 #[ignore = "requires FMP API key"]
849 async fn test_get_bulk_income_statements_sample() {
850 let client = FmpClient::new().unwrap();
851 let result = client.bulk().get_bulk_income_statements_sample().await;
852 assert!(result.is_ok());
853 let statements = result.unwrap();
854 assert!(statements.len() <= 100); if !statements.is_empty() {
856 assert!(statements[0].symbol.is_some());
857 }
858 }
859
860 #[tokio::test]
861 #[ignore = "requires FMP API key"]
862 async fn test_get_bulk_balance_sheets() {
863 let client = FmpClient::new().unwrap();
864 let result = client.bulk().get_bulk_balance_sheets().await;
865 assert!(result.is_ok());
866 let sheets = result.unwrap();
867 if !sheets.is_empty() {
868 assert!(sheets[0].symbol.is_some());
869 assert!(sheets[0].total_assets.is_some() || sheets[0].total_liabilities.is_some());
870 }
871 }
872
873 #[tokio::test]
874 #[ignore = "requires FMP API key"]
875 async fn test_get_bulk_balance_sheets_sample() {
876 let client = FmpClient::new().unwrap();
877 let result = client.bulk().get_bulk_balance_sheets_sample().await;
878 assert!(result.is_ok());
879 let sheets = result.unwrap();
880 assert!(sheets.len() <= 100); }
882
883 #[tokio::test]
884 #[ignore = "requires FMP API key"]
885 async fn test_get_bulk_cash_flow_statements() {
886 let client = FmpClient::new().unwrap();
887 let result = client.bulk().get_bulk_cash_flow_statements().await;
888 assert!(result.is_ok());
889 let cash_flows = result.unwrap();
890 if !cash_flows.is_empty() {
891 assert!(cash_flows[0].symbol.is_some());
892 assert!(
893 cash_flows[0].operating_cash_flow.is_some()
894 || cash_flows[0].free_cash_flow.is_some()
895 );
896 }
897 }
898
899 #[tokio::test]
900 #[ignore = "requires FMP API key"]
901 async fn test_get_bulk_cash_flow_statements_sample() {
902 let client = FmpClient::new().unwrap();
903 let result = client.bulk().get_bulk_cash_flow_statements_sample().await;
904 assert!(result.is_ok());
905 let cash_flows = result.unwrap();
906 assert!(cash_flows.len() <= 100); }
908
909 #[tokio::test]
910 #[ignore = "requires FMP API key"]
911 async fn test_get_bulk_etf_holdings() {
912 let client = FmpClient::new().unwrap();
913 let result = client.bulk().get_bulk_etf_holdings().await;
914 assert!(result.is_ok());
915 let holdings = result.unwrap();
916 if !holdings.is_empty() {
917 assert!(holdings[0].etf_symbol.is_some());
918 assert!(holdings[0].holding_symbol.is_some());
919 }
920 }
921
922 #[tokio::test]
923 #[ignore = "requires FMP API key"]
924 async fn test_get_bulk_etf_holdings_sample() {
925 let client = FmpClient::new().unwrap();
926 let result = client.bulk().get_bulk_etf_holdings_sample().await;
927 assert!(result.is_ok());
928 let holdings = result.unwrap();
929 assert!(holdings.len() <= 100); }
931
932 #[tokio::test]
933 #[ignore = "requires FMP API key"]
934 async fn test_get_bulk_data_info() {
935 let client = FmpClient::new().unwrap();
936 let result = client.bulk().get_bulk_data_info().await;
937 assert!(result.is_ok());
938 let info = result.unwrap();
939 if !info.is_empty() {
940 assert!(info[0].dataset.is_some());
941 assert!(info[0].file_size.is_some() || info[0].record_count.is_some());
942 }
943 }
944
945 #[tokio::test]
946 #[ignore = "requires FMP API key"]
947 async fn test_get_historical_prices_metadata() {
948 let client = FmpClient::new().unwrap();
949 let result = client
950 .bulk()
951 .get_historical_prices_metadata(Some("NYSE"))
952 .await;
953 assert!(result.is_ok());
954 let metadata = result.unwrap();
955 if !metadata.is_empty() {
956 assert!(metadata[0].exchange.is_some());
957 }
958 }
959
960 #[tokio::test]
961 #[ignore = "requires FMP API key"]
962 async fn test_get_historical_prices_metadata_no_exchange() {
963 let client = FmpClient::new().unwrap();
964 let result = client.bulk().get_historical_prices_metadata(None).await;
965 assert!(result.is_ok());
966 }
968
969 #[tokio::test]
971 #[ignore = "requires FMP API key"]
972 async fn test_bulk_endpoints_error_handling() {
973 let client = FmpClient::builder()
974 .api_key("invalid_key_12345")
975 .build()
976 .unwrap();
977
978 let result1 = client.bulk().get_bulk_stock_prices().await;
979 let result2 = client.bulk().get_bulk_income_statements().await;
980 let result3 = client.bulk().get_bulk_data_info().await;
981
982 assert!(result1.is_err());
984 assert!(result2.is_err());
985 assert!(result3.is_err());
986 }
987
988 #[tokio::test]
989 #[ignore = "requires FMP API key"]
990 async fn test_sample_vs_full_data_consistency() {
991 let client = FmpClient::new().unwrap();
992
993 let sample_result = client.bulk().get_bulk_stock_prices_sample().await;
995 let full_result = client.bulk().get_bulk_stock_prices().await;
996
997 assert!(sample_result.is_ok());
998 assert!(full_result.is_ok());
999
1000 let sample_data = sample_result.unwrap();
1001 let full_data = full_result.unwrap();
1002
1003 if !sample_data.is_empty() && !full_data.is_empty() {
1004 assert!(sample_data.len() <= full_data.len());
1006
1007 if sample_data[0].symbol.is_some() {
1009 assert!(full_data.iter().any(|p| p.symbol.is_some()));
1010 }
1011 }
1012 }
1013}