use serde::{Deserialize, Serialize};
use crate::error::Result;
use crate::adapters::fmp::build_client;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct EarningsCalendarEntryDTO {
pub date: Option<String>,
pub symbol: Option<String>,
#[serde(rename = "epsActual")]
pub eps: Option<f64>,
#[serde(rename = "epsEstimated")]
pub eps_estimated: Option<f64>,
#[serde(rename = "revenueActual")]
pub revenue: Option<f64>,
#[serde(rename = "revenueEstimated")]
pub revenue_estimated: Option<f64>,
#[serde(rename = "lastUpdated")]
pub last_updated: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct IpoCalendarEntryDTO {
pub date: Option<String>,
pub company: Option<String>,
pub symbol: Option<String>,
pub exchange: Option<String>,
pub actions: Option<String>,
pub shares: Option<f64>,
#[serde(rename = "priceRange")]
pub price_range: Option<String>,
#[serde(rename = "marketCap")]
pub market_cap: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StockSplitCalendarEntryDTO {
pub date: Option<String>,
pub symbol: Option<String>,
pub numerator: Option<f64>,
pub denominator: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DividendCalendarEntryDTO {
pub date: Option<String>,
pub symbol: Option<String>,
pub dividend: Option<f64>,
#[serde(rename = "adjDividend")]
pub adj_dividend: Option<f64>,
#[serde(rename = "recordDate")]
pub record_date: Option<String>,
#[serde(rename = "paymentDate")]
pub payment_date: Option<String>,
#[serde(rename = "declarationDate")]
pub declaration_date: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct EconomicCalendarEntryDTO {
pub event: Option<String>,
pub date: Option<String>,
pub country: Option<String>,
pub actual: Option<f64>,
pub previous: Option<f64>,
pub change: Option<f64>,
#[serde(rename = "changePercentage")]
pub change_percentage: Option<f64>,
pub estimate: Option<f64>,
pub impact: Option<String>,
}
pub async fn earnings_calendar(from: &str, to: &str) -> Result<Vec<EarningsCalendarEntryDTO>> {
let client = build_client()?;
client
.get("/stable/earnings-calendar", &[("from", from), ("to", to)])
.await
}
pub async fn ipo_calendar(from: &str, to: &str) -> Result<Vec<IpoCalendarEntryDTO>> {
let client = build_client()?;
client
.get("/stable/ipos-calendar", &[("from", from), ("to", to)])
.await
}
pub async fn stock_split_calendar(from: &str, to: &str) -> Result<Vec<StockSplitCalendarEntryDTO>> {
let client = build_client()?;
client
.get("/stable/splits-calendar", &[("from", from), ("to", to)])
.await
}
pub async fn dividend_calendar(from: &str, to: &str) -> Result<Vec<DividendCalendarEntryDTO>> {
let client = build_client()?;
client
.get("/stable/dividends-calendar", &[("from", from), ("to", to)])
.await
}
pub async fn economic_calendar(from: &str, to: &str) -> Result<Vec<EconomicCalendarEntryDTO>> {
let client = build_client()?;
client
.get("/stable/economic-calendar", &[("from", from), ("to", to)])
.await
}
pub async fn fetch_market_calendar_response(
kind: crate::models::calendar::market::CalendarKind,
from: &str,
to: &str,
) -> Result<Vec<crate::models::calendar::market::MarketCalendarEntry>> {
use crate::models::calendar::market::{CalendarDetail, CalendarKind, MarketCalendarEntry};
let entry = |symbol: Option<String>, date: Option<String>, detail: CalendarDetail| {
MarketCalendarEntry {
symbol,
date,
detail,
}
};
Ok(match kind {
CalendarKind::MarketHoliday | CalendarKind::MarketStatus => {
let operation = kind.operation();
return Err(crate::error::FinanceError::NotSupported {
provider: crate::Provider::Fmp,
operation,
candidates: operation.capability().candidate_providers(),
});
}
CalendarKind::Earnings => earnings_calendar(from, to)
.await?
.into_iter()
.map(|e| {
entry(
e.symbol,
e.date,
CalendarDetail::Earnings {
eps: e.eps,
eps_estimated: e.eps_estimated,
revenue: e.revenue,
revenue_estimated: e.revenue_estimated,
fiscal_date_ending: None,
time: None,
},
)
})
.collect(),
CalendarKind::Ipo => ipo_calendar(from, to)
.await?
.into_iter()
.map(|e| {
entry(
e.symbol,
e.date,
CalendarDetail::Ipo {
company: e.company,
exchange: e.exchange,
actions: e.actions,
shares: e.shares,
price_range: e.price_range,
market_cap: e.market_cap,
},
)
})
.collect(),
CalendarKind::Dividend => dividend_calendar(from, to)
.await?
.into_iter()
.map(|e| {
entry(
e.symbol,
e.date,
CalendarDetail::Dividend {
dividend: e.dividend,
adj_dividend: e.adj_dividend,
record_date: e.record_date,
payment_date: e.payment_date,
declaration_date: e.declaration_date,
},
)
})
.collect(),
CalendarKind::Split => stock_split_calendar(from, to)
.await?
.into_iter()
.map(|e| {
entry(
e.symbol,
e.date,
CalendarDetail::Split {
numerator: e.numerator,
denominator: e.denominator,
},
)
})
.collect(),
CalendarKind::Economic => economic_calendar(from, to)
.await?
.into_iter()
.map(|e| {
entry(
None,
e.date,
CalendarDetail::Economic {
event: e.event,
country: e.country,
actual: e.actual,
previous: e.previous,
estimate: e.estimate,
change: e.change,
change_percentage: e.change_percentage,
impact: e.impact,
},
)
})
.collect(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_earnings_calendar_mock() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", "/stable/earnings-calendar")
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("apikey".into(), "test-key".into()),
mockito::Matcher::UrlEncoded("from".into(), "2024-01-01".into()),
mockito::Matcher::UrlEncoded("to".into(), "2024-01-31".into()),
]))
.with_status(200)
.with_body(
r#"[{
"symbol": "MSFT",
"date": "2024-01-25",
"epsActual": 2.93,
"epsEstimated": 2.78,
"revenueActual": 62020000000.0,
"revenueEstimated": 61100000000.0,
"lastUpdated": "2024-01-26"
}]"#,
)
.create_async()
.await;
let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
let resp: Vec<EarningsCalendarEntryDTO> = client
.get(
"/stable/earnings-calendar",
&[("from", "2024-01-01"), ("to", "2024-01-31")],
)
.await
.unwrap();
let row = &resp[0];
assert_eq!(row.symbol.as_deref(), Some("MSFT"));
assert_eq!(row.date.as_deref(), Some("2024-01-25"));
assert_eq!(row.eps, Some(2.93));
assert_eq!(row.eps_estimated, Some(2.78));
assert_eq!(row.revenue, Some(62_020_000_000.0));
assert_eq!(row.revenue_estimated, Some(61_100_000_000.0));
assert_eq!(row.last_updated.as_deref(), Some("2024-01-26"));
}
#[tokio::test]
async fn test_economic_calendar_mock() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", "/stable/economic-calendar")
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("apikey".into(), "test-key".into()),
mockito::Matcher::UrlEncoded("from".into(), "2024-01-01".into()),
mockito::Matcher::UrlEncoded("to".into(), "2024-01-31".into()),
]))
.with_status(200)
.with_body(
serde_json::json!([
{
"event": "CPI",
"date": "2024-01-11",
"country": "US",
"actual": 3.4,
"previous": 3.1,
"estimate": 3.2,
"impact": "High"
}
])
.to_string(),
)
.create_async()
.await;
let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
let resp: Vec<EconomicCalendarEntryDTO> = client
.get(
"/stable/economic-calendar",
&[("from", "2024-01-01"), ("to", "2024-01-31")],
)
.await
.unwrap();
assert_eq!(resp.len(), 1);
assert_eq!(resp[0].event.as_deref(), Some("CPI"));
}
}