#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use crate::adapters::common::encode_path_segment;
use crate::error::Result;
use crate::adapters::fmp::build_client;
use crate::adapters::fmp::models::{FmpQuoteDTO, HistoricalPriceResponseDTO};
fn index_quote_to_canonical(
symbol: &str,
quotes: &[FmpQuoteDTO],
) -> crate::models::indices::IndexQuote {
let q = quotes.first();
crate::models::indices::IndexQuote {
symbol: symbol.to_string(),
name: q.and_then(|q| q.name.clone()),
price: q.and_then(|q| q.price),
change: q.and_then(|q| q.change),
change_percent: q.and_then(|q| q.changes_percentage),
timestamp: None,
}
}
pub async fn fetch_canonical_index_quote(
symbol: &str,
) -> Result<crate::models::indices::IndexQuote> {
let quotes = crate::adapters::fmp::quote::quote(symbol).await?;
Ok(index_quote_to_canonical(symbol, "es))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct IndexConstituentDTO {
pub symbol: Option<String>,
pub name: Option<String>,
pub sector: Option<String>,
#[serde(rename = "subSector")]
pub sub_sector: Option<String>,
#[serde(rename = "headQuarter")]
pub head_quarter: Option<String>,
#[serde(rename = "dateFirstAdded")]
pub date_first_added: Option<String>,
pub cik: Option<String>,
pub founded: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HistoricalConstituentDTO {
pub date: Option<String>,
pub symbol: Option<String>,
#[serde(rename = "addedSecurity")]
pub added_security: Option<String>,
#[serde(rename = "removedTicker")]
pub removed_ticker: Option<String>,
#[serde(rename = "removedSecurity")]
pub removed_security: Option<String>,
pub reason: Option<String>,
}
pub async fn major_indexes_quote() -> Result<Vec<FmpQuoteDTO>> {
let client = build_client()?;
client.get("/api/v3/quotes/index", &[]).await
}
pub async fn sp500_constituents() -> Result<Vec<IndexConstituentDTO>> {
let client = build_client()?;
client.get("/api/v3/sp500_constituent", &[]).await
}
pub async fn nasdaq_constituents() -> Result<Vec<IndexConstituentDTO>> {
let client = build_client()?;
client.get("/api/v3/nasdaq_constituent", &[]).await
}
pub async fn dow_constituents() -> Result<Vec<IndexConstituentDTO>> {
let client = build_client()?;
client.get("/api/v3/dowjones_constituent", &[]).await
}
pub async fn historical_sp500() -> Result<Vec<HistoricalConstituentDTO>> {
let client = build_client()?;
client
.get("/api/v3/historical/sp500_constituent", &[])
.await
}
pub async fn index_historical(
symbol: &str,
params: &[(&str, &str)],
) -> Result<HistoricalPriceResponseDTO> {
let client = build_client()?;
let path = format!(
"/api/v3/historical-price-full/{}",
encode_path_segment(symbol)
);
client.get(&path, params).await
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_sp500_constituents_mock() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", "/api/v3/sp500_constituent")
.match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
"apikey".into(),
"test-key".into(),
)]))
.with_status(200)
.with_body(
serde_json::json!([
{
"symbol": "AAPL",
"name": "Apple Inc.",
"sector": "Information Technology",
"subSector": "Technology Hardware",
"headQuarter": "Cupertino, CA",
"dateFirstAdded": "1982-11-30",
"cik": "0000320193",
"founded": "1976"
}
])
.to_string(),
)
.create_async()
.await;
let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
let result: Vec<IndexConstituentDTO> =
client.get("/api/v3/sp500_constituent", &[]).await.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].symbol.as_deref(), Some("AAPL"));
assert_eq!(result[0].sector.as_deref(), Some("Information Technology"));
}
}