finance-query 2.6.1

A Rust library for querying financial data
Documentation
//! Futures snapshot endpoints.
#![allow(dead_code)]

use serde::{Deserialize, Serialize};

use crate::adapters::common::encode_path_segment;
use crate::error::{FinanceError, Result};
use crate::models::futures::FuturesQuote;

use super::super::build_client;

/// Session data within a futures snapshot.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FuturesSessionDTO {
    /// Change from previous close.
    pub change: Option<f64>,
    /// Change percent from previous close.
    pub change_percent: Option<f64>,
    /// Close price.
    pub close: Option<f64>,
    /// High price.
    pub high: Option<f64>,
    /// Low price.
    pub low: Option<f64>,
    /// Open price.
    pub open: Option<f64>,
    /// Previous close price.
    pub previous_close: Option<f64>,
    /// Settlement price.
    pub settlement: Option<f64>,
    /// Volume.
    pub volume: Option<f64>,
}

/// A single futures snapshot.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FuturesSnapshotDTO {
    /// Ticker symbol.
    pub ticker: Option<String>,
    /// Name of the contract.
    pub name: Option<String>,
    /// Market status.
    pub market_status: Option<String>,
    /// Type.
    #[serde(rename = "type")]
    pub snapshot_type: Option<String>,
    /// Session data.
    pub session: Option<FuturesSessionDTO>,
    /// Last updated timestamp.
    pub last_updated: Option<i64>,
}

/// Response wrapper for futures snapshots.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FuturesSnapshotResponseDTO {
    /// Response status.
    pub status: Option<String>,
    /// Request identifier.
    pub request_id: Option<String>,
    /// Snapshot results.
    pub results: Option<Vec<FuturesSnapshotDTO>>,
}

/// Fetch snapshot for a futures ticker.
///
/// * `ticker` - Futures ticker symbol (e.g., `"ESZ4"`)
pub async fn futures_snapshot(ticker: &str) -> Result<FuturesSnapshotResponseDTO> {
    let client = build_client()?;
    let path = format!("/v3/snapshot/futures/{}", encode_path_segment(ticker));
    let json = client.get_raw(&path, &[]).await?;
    serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
        field: "futures_snapshot".to_string(),
        context: format!("Failed to parse futures snapshot response: {e}"),
    })
}

/// Fetch futures quote (canonical) for a symbol.
pub async fn fetch_futures_quote_response(symbol: &str) -> Result<FuturesQuote> {
    let resp = futures_snapshot(symbol).await?;
    let snap = resp.results.and_then(|mut v| {
        if v.is_empty() {
            None
        } else {
            Some(v.remove(0))
        }
    });
    let session = snap.as_ref().and_then(|s| s.session.as_ref());
    Ok(FuturesQuote {
        symbol: snap
            .as_ref()
            .and_then(|s| s.ticker.clone())
            .unwrap_or_else(|| symbol.to_string()),
        name: snap.as_ref().and_then(|s| s.name.clone()),
        underlying: None,
        exchange: None,
        expiration_date: None,
        price: session.and_then(|s| s.close),
        change: session.and_then(|s| s.change),
        change_percent: session.and_then(|s| s.change_percent),
        open_interest: None,
        volume: None,
        timestamp: None,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_futures_snapshot_mock() {
        let mut server = mockito::Server::new_async().await;
        let _mock = server
            .mock("GET", "/v3/snapshot/futures/ESZ4")
            .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_status": "open",
                            "type": "futures",
                            "session": {
                                "change": 15.0,
                                "change_percent": 0.31,
                                "close": 4790.0,
                                "high": 4800.0,
                                "low": 4760.0,
                                "open": 4775.0,
                                "previous_close": 4775.0,
                                "settlement": 4785.0,
                                "volume": 1500000.0
                            },
                            "last_updated": 1705363200000000000_i64
                        }
                    ]
                })
                .to_string(),
            )
            .create_async()
            .await;

        let client = super::super::super::build_test_client(&server.url()).unwrap();
        let json = client
            .get_raw("/v3/snapshot/futures/ESZ4", &[])
            .await
            .unwrap();

        let resp: FuturesSnapshotResponseDTO = serde_json::from_value(json).unwrap();
        assert_eq!(resp.status.as_deref(), Some("OK"));
        let results = resp.results.unwrap();
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].ticker.as_deref(), Some("ESZ4"));
        let session = results[0].session.as_ref().unwrap();
        assert!((session.change.unwrap() - 15.0).abs() < 0.01);
        assert!((session.close.unwrap() - 4790.0).abs() < 0.01);
    }
}