Skip to main content

finance_query/adapters/fmp/
indexes.rs

1//! Index endpoints for Financial Modeling Prep.
2
3use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::Result;
7
8use super::build_client;
9use super::models::{FmpQuote, HistoricalPriceResponse};
10
11/// A constituent of a major index (S&P 500, Nasdaq, Dow Jones).
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct IndexConstituent {
15    /// Ticker symbol.
16    pub symbol: Option<String>,
17    /// Company name.
18    pub name: Option<String>,
19    /// Sector.
20    pub sector: Option<String>,
21    /// Sub-sector.
22    #[serde(rename = "subSector")]
23    pub sub_sector: Option<String>,
24    /// Headquarters location.
25    #[serde(rename = "headQuarter")]
26    pub head_quarter: Option<String>,
27    /// Date first added to the index.
28    #[serde(rename = "dateFirstAdded")]
29    pub date_first_added: Option<String>,
30    /// CIK number.
31    pub cik: Option<String>,
32    /// Year the company was founded.
33    pub founded: Option<String>,
34}
35
36/// A historical change in index constituency.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[non_exhaustive]
39pub struct HistoricalConstituent {
40    /// Date of the change.
41    pub date: Option<String>,
42    /// Ticker symbol.
43    pub symbol: Option<String>,
44    /// Security that was added.
45    #[serde(rename = "addedSecurity")]
46    pub added_security: Option<String>,
47    /// Ticker that was removed.
48    #[serde(rename = "removedTicker")]
49    pub removed_ticker: Option<String>,
50    /// Security that was removed.
51    #[serde(rename = "removedSecurity")]
52    pub removed_security: Option<String>,
53    /// Reason for the change.
54    pub reason: Option<String>,
55}
56
57/// Fetch real-time quotes for all major indexes.
58pub async fn major_indexes_quote() -> Result<Vec<FmpQuote>> {
59    let client = build_client()?;
60    client.get("/api/v3/quotes/index", &[]).await
61}
62
63/// Fetch current S&P 500 constituents.
64pub async fn sp500_constituents() -> Result<Vec<IndexConstituent>> {
65    let client = build_client()?;
66    client.get("/api/v3/sp500_constituent", &[]).await
67}
68
69/// Fetch current Nasdaq constituents.
70pub async fn nasdaq_constituents() -> Result<Vec<IndexConstituent>> {
71    let client = build_client()?;
72    client.get("/api/v3/nasdaq_constituent", &[]).await
73}
74
75/// Fetch current Dow Jones constituents.
76pub async fn dow_constituents() -> Result<Vec<IndexConstituent>> {
77    let client = build_client()?;
78    client.get("/api/v3/dowjones_constituent", &[]).await
79}
80
81/// Fetch historical S&P 500 constituent changes.
82pub async fn historical_sp500() -> Result<Vec<HistoricalConstituent>> {
83    let client = build_client()?;
84    client
85        .get("/api/v3/historical/sp500_constituent", &[])
86        .await
87}
88
89/// Fetch daily historical prices for an index.
90///
91/// * `symbol` - e.g., `"^GSPC"` (S&P 500)
92/// * `params` - Optional query params such as `from`, `to`
93pub async fn index_historical(
94    symbol: &str,
95    params: &[(&str, &str)],
96) -> Result<HistoricalPriceResponse> {
97    let client = build_client()?;
98    let path = format!(
99        "/api/v3/historical-price-full/{}",
100        encode_path_segment(symbol)
101    );
102    client.get(&path, params).await
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[tokio::test]
110    async fn test_sp500_constituents_mock() {
111        let mut server = mockito::Server::new_async().await;
112        let _mock = server
113            .mock("GET", "/api/v3/sp500_constituent")
114            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
115                "apikey".into(),
116                "test-key".into(),
117            )]))
118            .with_status(200)
119            .with_body(
120                serde_json::json!([
121                    {
122                        "symbol": "AAPL",
123                        "name": "Apple Inc.",
124                        "sector": "Information Technology",
125                        "subSector": "Technology Hardware",
126                        "headQuarter": "Cupertino, CA",
127                        "dateFirstAdded": "1982-11-30",
128                        "cik": "0000320193",
129                        "founded": "1976"
130                    }
131                ])
132                .to_string(),
133            )
134            .create_async()
135            .await;
136
137        let client = super::super::build_test_client(&server.url()).unwrap();
138        let result: Vec<IndexConstituent> =
139            client.get("/api/v3/sp500_constituent", &[]).await.unwrap();
140        assert_eq!(result.len(), 1);
141        assert_eq!(result[0].symbol.as_deref(), Some("AAPL"));
142        assert_eq!(result[0].sector.as_deref(), Some("Information Technology"));
143    }
144}