use serde::{Deserialize, Serialize};
use crate::error::{FinanceError, Result};
use super::super::build_client;
use super::super::models::PaginatedResponse;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FuturesContract {
pub ticker: Option<String>,
pub name: Option<String>,
pub market: Option<String>,
pub asset_class: Option<String>,
pub expiration_date: Option<String>,
pub first_trade_date: Option<String>,
pub last_trade_date: Option<String>,
pub contract_size: Option<f64>,
pub contract_unit: Option<String>,
pub tick_size: Option<f64>,
pub underlying_ticker: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FuturesProduct {
pub ticker: Option<String>,
pub name: Option<String>,
pub asset_class: Option<String>,
pub market: Option<String>,
pub exchange: Option<String>,
pub contract_size: Option<f64>,
pub contract_unit: Option<String>,
pub tick_size: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FuturesSchedule {
pub ticker: Option<String>,
pub session_type: Option<String>,
pub start_time: Option<String>,
pub end_time: Option<String>,
pub timezone: Option<String>,
pub day_of_week: Option<String>,
}
pub async fn futures_contracts(
params: &[(&str, &str)],
) -> Result<PaginatedResponse<FuturesContract>> {
let client = build_client()?;
let path = "/v3/reference/futures/contracts";
let json = client.get_raw(path, params).await?;
serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
field: "futures_contracts".to_string(),
context: format!("Failed to parse futures contracts response: {e}"),
})
}
pub async fn futures_products(
params: &[(&str, &str)],
) -> Result<PaginatedResponse<FuturesProduct>> {
let client = build_client()?;
let path = "/v3/reference/futures/products";
let json = client.get_raw(path, params).await?;
serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
field: "futures_products".to_string(),
context: format!("Failed to parse futures products response: {e}"),
})
}
pub async fn futures_schedules(
params: &[(&str, &str)],
) -> Result<PaginatedResponse<FuturesSchedule>> {
let client = build_client()?;
let path = "/v3/reference/futures/schedules";
let json = client.get_raw(path, params).await?;
serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
field: "futures_schedules".to_string(),
context: format!("Failed to parse futures schedules response: {e}"),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_futures_contracts_mock() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", "/v3/reference/futures/contracts")
.match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
"apiKey".into(),
"test-key".into(),
)]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
serde_json::json!({
"status": "OK",
"request_id": "abc123",
"results": [
{
"ticker": "ESZ4",
"name": "E-mini S&P 500 Dec 2024",
"market": "futures",
"asset_class": "equity_index",
"expiration_date": "2024-12-20",
"first_trade_date": "2024-06-21",
"contract_size": 50.0,
"contract_unit": "USD",
"tick_size": 0.25,
"underlying_ticker": "I:SPX"
}
],
"resultsCount": 1
})
.to_string(),
)
.create_async()
.await;
let client = super::super::super::build_test_client(&server.url()).unwrap();
let json = client
.get_raw("/v3/reference/futures/contracts", &[])
.await
.unwrap();
let resp: PaginatedResponse<FuturesContract> = serde_json::from_value(json).unwrap();
let results = resp.results.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].ticker.as_deref(), Some("ESZ4"));
assert_eq!(results[0].name.as_deref(), Some("E-mini S&P 500 Dec 2024"));
}
}