Skip to main content

finance_query/adapters/polygon/options/
snapshots.rs

1//! Options snapshot endpoints: options chain, single contract snapshot.
2
3use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::{FinanceError, Result};
7
8use super::super::build_client;
9use super::super::models::*;
10
11/// Greeks for an options contract snapshot.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct OptionsGreeks {
15    /// Delta: rate of change of the option price with respect to the underlying.
16    pub delta: Option<f64>,
17    /// Gamma: rate of change of delta with respect to the underlying.
18    pub gamma: Option<f64>,
19    /// Theta: rate of change of the option price with respect to time.
20    pub theta: Option<f64>,
21    /// Vega: rate of change of the option price with respect to volatility.
22    pub vega: Option<f64>,
23}
24
25/// Contract details within an options snapshot.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[non_exhaustive]
28pub struct OptionsSnapshotDetails {
29    /// Contract type: `"call"` or `"put"`.
30    pub contract_type: Option<String>,
31    /// Exercise style: `"american"` or `"european"`.
32    pub exercise_style: Option<String>,
33    /// Expiration date (`"YYYY-MM-DD"`).
34    pub expiration_date: Option<String>,
35    /// Number of shares per contract.
36    pub shares_per_contract: Option<u32>,
37    /// Strike price.
38    pub strike_price: Option<f64>,
39    /// Options ticker symbol.
40    pub ticker: Option<String>,
41}
42
43/// Underlying asset data within an options snapshot.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[non_exhaustive]
46pub struct OptionsUnderlyingAsset {
47    /// Change in price since previous close.
48    pub change_to_break_even: Option<f64>,
49    /// Last updated timestamp (nanoseconds).
50    pub last_updated: Option<i64>,
51    /// Current price of the underlying.
52    pub price: Option<f64>,
53    /// Underlying ticker symbol.
54    pub ticker: Option<String>,
55    /// Timeframe of the underlying data.
56    pub timeframe: Option<String>,
57}
58
59/// Last quote data within an options snapshot.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[non_exhaustive]
62pub struct OptionsSnapshotQuote {
63    /// Ask price.
64    pub ask: Option<f64>,
65    /// Ask size.
66    pub ask_size: Option<f64>,
67    /// Bid price.
68    pub bid: Option<f64>,
69    /// Bid size.
70    pub bid_size: Option<f64>,
71    /// Last updated timestamp (nanoseconds).
72    pub last_updated: Option<i64>,
73    /// Midpoint price.
74    pub midpoint: Option<f64>,
75    /// Timeframe of the quote data.
76    pub timeframe: Option<String>,
77}
78
79/// Last trade data within an options snapshot.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81#[non_exhaustive]
82pub struct OptionsSnapshotTrade {
83    /// Conditions.
84    pub conditions: Option<Vec<i32>>,
85    /// Exchange ID.
86    pub exchange: Option<i32>,
87    /// Trade price.
88    pub price: Option<f64>,
89    /// SIP timestamp (nanoseconds).
90    pub sip_timestamp: Option<i64>,
91    /// Trade size.
92    pub size: Option<f64>,
93    /// Timeframe of the trade data.
94    pub timeframe: Option<String>,
95}
96
97/// A single options contract snapshot from the chain or individual lookup.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[non_exhaustive]
100pub struct OptionsSnapshot {
101    /// Break-even price for the contract.
102    pub break_even_price: Option<f64>,
103    /// Current day aggregate data.
104    pub day: Option<SnapshotAgg>,
105    /// Contract details (strike, expiration, type).
106    pub details: Option<OptionsSnapshotDetails>,
107    /// Option greeks (delta, gamma, theta, vega).
108    pub greeks: Option<OptionsGreeks>,
109    /// Implied volatility.
110    pub implied_volatility: Option<f64>,
111    /// Last quote for this contract.
112    pub last_quote: Option<OptionsSnapshotQuote>,
113    /// Last trade for this contract.
114    pub last_trade: Option<OptionsSnapshotTrade>,
115    /// Open interest.
116    pub open_interest: Option<u64>,
117    /// Underlying asset data.
118    pub underlying_asset: Option<OptionsUnderlyingAsset>,
119}
120
121/// Response wrapper for a single options contract snapshot.
122#[derive(Debug, Clone, Serialize, Deserialize)]
123#[non_exhaustive]
124pub struct OptionsContractSnapshotResponse {
125    /// Request ID.
126    pub request_id: Option<String>,
127    /// Response status.
128    pub status: Option<String>,
129    /// The snapshot result.
130    pub results: Option<OptionsSnapshot>,
131}
132
133/// Fetch the options chain snapshot for an underlying ticker.
134///
135/// Returns a paginated list of options contract snapshots.
136///
137/// # Arguments
138///
139/// * `underlying` - Underlying stock ticker (e.g., `"AAPL"`)
140/// * `params` - Query params such as `strike_price`, `expiration_date`,
141///   `contract_type`, `order`, `limit`, `sort`
142pub async fn options_chain_snapshot(
143    underlying: &str,
144    params: &[(&str, &str)],
145) -> Result<PaginatedResponse<OptionsSnapshot>> {
146    let client = build_client()?;
147    let path = format!("/v3/snapshot/options/{}", encode_path_segment(underlying));
148    client.get(&path, params).await
149}
150
151/// Fetch a snapshot for a single options contract.
152///
153/// * `underlying` - Underlying stock ticker (e.g., `"AAPL"`)
154/// * `contract` - Options contract ticker (e.g., `"O:AAPL250117C00150000"`)
155pub async fn options_contract_snapshot(
156    underlying: &str,
157    contract: &str,
158) -> Result<OptionsContractSnapshotResponse> {
159    let client = build_client()?;
160    let path = format!(
161        "/v3/snapshot/options/{}/{}",
162        encode_path_segment(underlying),
163        encode_path_segment(contract)
164    );
165    let json = client.get_raw(&path, &[]).await?;
166    serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
167        field: "options_contract_snapshot".to_string(),
168        context: format!("Failed to parse options contract snapshot response: {e}"),
169    })
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[tokio::test]
177    async fn test_options_chain_snapshot_mock() {
178        let mut server = mockito::Server::new_async().await;
179        let _mock = server
180            .mock("GET", "/v3/snapshot/options/AAPL")
181            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
182                "apiKey".into(),
183                "test-key".into(),
184            )]))
185            .with_status(200)
186            .with_header("content-type", "application/json")
187            .with_body(
188                serde_json::json!({
189                    "request_id": "abc123",
190                    "status": "OK",
191                    "results": [
192                        {
193                            "break_even_price": 155.30,
194                            "day": { "o": 5.10, "h": 5.50, "l": 4.90, "c": 5.30, "v": 1200.0 },
195                            "details": {
196                                "contract_type": "call",
197                                "exercise_style": "american",
198                                "expiration_date": "2025-01-17",
199                                "shares_per_contract": 100,
200                                "strike_price": 150.0,
201                                "ticker": "O:AAPL250117C00150000"
202                            },
203                            "greeks": {
204                                "delta": 0.65,
205                                "gamma": 0.03,
206                                "theta": -0.05,
207                                "vega": 0.25
208                            },
209                            "implied_volatility": 0.32,
210                            "last_quote": {
211                                "ask": 5.40,
212                                "ask_size": 10.0,
213                                "bid": 5.20,
214                                "bid_size": 15.0,
215                                "last_updated": 1705363200000000000_i64,
216                                "midpoint": 5.30
217                            },
218                            "last_trade": {
219                                "price": 5.30,
220                                "size": 5.0,
221                                "exchange": 4,
222                                "sip_timestamp": 1705363200000000000_i64
223                            },
224                            "open_interest": 25000,
225                            "underlying_asset": {
226                                "change_to_break_even": 5.30,
227                                "last_updated": 1705363200000000000_i64,
228                                "price": 150.00,
229                                "ticker": "AAPL",
230                                "timeframe": "2024-01-15"
231                            }
232                        }
233                    ],
234                    "resultsCount": 1
235                })
236                .to_string(),
237            )
238            .create_async()
239            .await;
240
241        let client = super::super::super::build_test_client(&server.url()).unwrap();
242        let resp: PaginatedResponse<OptionsSnapshot> =
243            client.get("/v3/snapshot/options/AAPL", &[]).await.unwrap();
244
245        let results = resp.results.unwrap();
246        assert_eq!(results.len(), 1);
247        assert!((results[0].break_even_price.unwrap() - 155.30).abs() < 0.01);
248        assert!((results[0].implied_volatility.unwrap() - 0.32).abs() < 0.01);
249
250        let greeks = results[0].greeks.as_ref().unwrap();
251        assert!((greeks.delta.unwrap() - 0.65).abs() < 0.01);
252        assert!((greeks.theta.unwrap() - (-0.05)).abs() < 0.01);
253
254        let details = results[0].details.as_ref().unwrap();
255        assert_eq!(details.contract_type.as_deref(), Some("call"));
256        assert!((details.strike_price.unwrap() - 150.0).abs() < 0.01);
257
258        assert_eq!(results[0].open_interest, Some(25000));
259    }
260
261    #[tokio::test]
262    async fn test_options_contract_snapshot_mock() {
263        let mut server = mockito::Server::new_async().await;
264        let _mock = server
265            .mock("GET", "/v3/snapshot/options/AAPL/O:AAPL250117C00150000")
266            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
267                "apiKey".into(),
268                "test-key".into(),
269            )]))
270            .with_status(200)
271            .with_header("content-type", "application/json")
272            .with_body(
273                serde_json::json!({
274                    "request_id": "abc123",
275                    "status": "OK",
276                    "results": {
277                        "break_even_price": 155.30,
278                        "day": { "o": 5.10, "h": 5.50, "l": 4.90, "c": 5.30, "v": 1200.0 },
279                        "details": {
280                            "contract_type": "call",
281                            "expiration_date": "2025-01-17",
282                            "strike_price": 150.0,
283                            "ticker": "O:AAPL250117C00150000"
284                        },
285                        "greeks": {
286                            "delta": 0.65,
287                            "gamma": 0.03,
288                            "theta": -0.05,
289                            "vega": 0.25
290                        },
291                        "implied_volatility": 0.32,
292                        "open_interest": 25000,
293                        "underlying_asset": {
294                            "price": 150.00,
295                            "ticker": "AAPL"
296                        }
297                    }
298                })
299                .to_string(),
300            )
301            .create_async()
302            .await;
303
304        let client = super::super::super::build_test_client(&server.url()).unwrap();
305        let json = client
306            .get_raw("/v3/snapshot/options/AAPL/O:AAPL250117C00150000", &[])
307            .await
308            .unwrap();
309
310        let resp: OptionsContractSnapshotResponse = serde_json::from_value(json).unwrap();
311        assert_eq!(resp.status.as_deref(), Some("OK"));
312        let snap = resp.results.unwrap();
313        assert!((snap.break_even_price.unwrap() - 155.30).abs() < 0.01);
314        assert_eq!(snap.open_interest, Some(25000));
315
316        let greeks = snap.greeks.unwrap();
317        assert!((greeks.vega.unwrap() - 0.25).abs() < 0.01);
318    }
319}