finance_query/adapters/polygon/options/
snapshots.rs1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct OptionsGreeks {
15 pub delta: Option<f64>,
17 pub gamma: Option<f64>,
19 pub theta: Option<f64>,
21 pub vega: Option<f64>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27#[non_exhaustive]
28pub struct OptionsSnapshotDetails {
29 pub contract_type: Option<String>,
31 pub exercise_style: Option<String>,
33 pub expiration_date: Option<String>,
35 pub shares_per_contract: Option<u32>,
37 pub strike_price: Option<f64>,
39 pub ticker: Option<String>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45#[non_exhaustive]
46pub struct OptionsUnderlyingAsset {
47 pub change_to_break_even: Option<f64>,
49 pub last_updated: Option<i64>,
51 pub price: Option<f64>,
53 pub ticker: Option<String>,
55 pub timeframe: Option<String>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61#[non_exhaustive]
62pub struct OptionsSnapshotQuote {
63 pub ask: Option<f64>,
65 pub ask_size: Option<f64>,
67 pub bid: Option<f64>,
69 pub bid_size: Option<f64>,
71 pub last_updated: Option<i64>,
73 pub midpoint: Option<f64>,
75 pub timeframe: Option<String>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81#[non_exhaustive]
82pub struct OptionsSnapshotTrade {
83 pub conditions: Option<Vec<i32>>,
85 pub exchange: Option<i32>,
87 pub price: Option<f64>,
89 pub sip_timestamp: Option<i64>,
91 pub size: Option<f64>,
93 pub timeframe: Option<String>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99#[non_exhaustive]
100pub struct OptionsSnapshot {
101 pub break_even_price: Option<f64>,
103 pub day: Option<SnapshotAgg>,
105 pub details: Option<OptionsSnapshotDetails>,
107 pub greeks: Option<OptionsGreeks>,
109 pub implied_volatility: Option<f64>,
111 pub last_quote: Option<OptionsSnapshotQuote>,
113 pub last_trade: Option<OptionsSnapshotTrade>,
115 pub open_interest: Option<u64>,
117 pub underlying_asset: Option<OptionsUnderlyingAsset>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123#[non_exhaustive]
124pub struct OptionsContractSnapshotResponse {
125 pub request_id: Option<String>,
127 pub status: Option<String>,
129 pub results: Option<OptionsSnapshot>,
131}
132
133pub 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
151pub 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}