rustrade_data/exchange/alpaca/options/
snapshots.rs1use super::super::pagination::PaginationGuard;
6use super::{AlpacaOptionsClient, AlpacaOptionsError};
7use crate::subscription::greeks::OptionGreeks;
8use chrono::{DateTime, Utc};
9use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use tracing::debug;
13
14const MAX_SYMBOLS_PER_REQUEST: usize = 100;
16
17const MAX_PAGES: usize = 100;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
23#[serde(rename_all = "lowercase")]
24pub enum AlpacaOptionFeed {
25 Opra,
27 #[default]
29 Indicative,
30}
31
32impl AlpacaOptionFeed {
33 pub fn as_str(&self) -> &'static str {
35 match self {
36 Self::Opra => "opra",
37 Self::Indicative => "indicative",
38 }
39 }
40}
41
42#[non_exhaustive]
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub struct AlpacaOptionQuote {
46 #[serde(rename = "t")]
48 pub timestamp: DateTime<Utc>,
49 #[serde(rename = "ax")]
51 pub ask_exchange: String,
52 #[serde(rename = "ap")]
54 pub ask_price: Decimal,
55 #[serde(rename = "as")]
57 pub ask_size: u32,
58 #[serde(rename = "bx")]
60 pub bid_exchange: String,
61 #[serde(rename = "bp")]
63 pub bid_price: Decimal,
64 #[serde(rename = "bs")]
66 pub bid_size: u32,
67}
68
69#[non_exhaustive]
71#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72pub struct AlpacaOptionTrade {
73 #[serde(rename = "t")]
75 pub timestamp: DateTime<Utc>,
76 #[serde(rename = "x")]
78 pub exchange: String,
79 #[serde(rename = "p")]
81 pub price: Decimal,
82 #[serde(rename = "s")]
84 pub size: u32,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Default, Deserialize)]
89struct AlpacaGreeks {
90 #[serde(default)]
91 delta: Option<f64>,
92 #[serde(default)]
93 gamma: Option<f64>,
94 #[serde(default)]
95 theta: Option<f64>,
96 #[serde(default)]
97 vega: Option<f64>,
98 #[serde(default)]
99 rho: Option<f64>,
100}
101
102impl From<AlpacaGreeks> for OptionGreeks {
103 fn from(g: AlpacaGreeks) -> Self {
104 Self {
105 delta: g.delta,
106 gamma: g.gamma,
107 theta: g.theta,
108 vega: g.vega,
109 rho: g.rho,
110 implied_volatility: None,
111 theoretical_price: None,
112 underlying_price: None,
113 }
114 }
115}
116
117#[non_exhaustive]
121#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
122pub struct AlpacaOptionSnapshot {
123 #[serde(default)]
126 pub symbol: String,
127 #[serde(default)]
129 pub latest_quote: Option<AlpacaOptionQuote>,
130 #[serde(default)]
132 pub latest_trade: Option<AlpacaOptionTrade>,
133 #[serde(default, skip_serializing)]
135 greeks: Option<AlpacaGreeks>,
136 #[serde(default)]
138 pub implied_volatility: Option<f64>,
139}
140
141impl AlpacaOptionSnapshot {
142 pub fn greeks(&self) -> OptionGreeks {
147 let mut greeks: OptionGreeks = self.greeks.unwrap_or_default().into();
148 greeks.implied_volatility = self.implied_volatility;
149 greeks
150 }
151
152 pub fn has_greeks(&self) -> bool {
154 self.greeks.is_some() || self.implied_volatility.is_some()
155 }
156}
157
158#[derive(Debug, Deserialize)]
160struct SnapshotsResponse {
161 snapshots: Option<HashMap<String, AlpacaOptionSnapshot>>,
162 #[serde(default)]
163 next_page_token: Option<String>,
164}
165
166impl AlpacaOptionsClient {
167 pub async fn fetch_snapshots(
195 &self,
196 symbols: &[String],
197 feed: AlpacaOptionFeed,
198 ) -> Result<Vec<AlpacaOptionSnapshot>, AlpacaOptionsError> {
199 if symbols.is_empty() {
200 return Ok(Vec::new());
201 }
202
203 let mut all_snapshots = Vec::new();
204
205 for chunk in symbols.chunks(MAX_SYMBOLS_PER_REQUEST) {
207 let chunk_snapshots = self.fetch_snapshots_batch(chunk, feed).await?;
208 all_snapshots.extend(chunk_snapshots);
209 }
210
211 Ok(all_snapshots)
212 }
213
214 async fn fetch_snapshots_batch(
216 &self,
217 symbols: &[String],
218 feed: AlpacaOptionFeed,
219 ) -> Result<Vec<AlpacaOptionSnapshot>, AlpacaOptionsError> {
220 let mut all_snapshots = Vec::new();
221 let mut page_token: Option<String> = None;
222 let mut guard = PaginationGuard::new(MAX_PAGES);
223
224 let symbols_param = symbols.join(",");
225
226 loop {
227 guard.observe(page_token.as_deref())?;
228
229 let mut params: Vec<(&str, &str)> =
230 vec![("symbols", &symbols_param), ("feed", feed.as_str())];
231
232 let token_string;
234 if let Some(ref token) = page_token {
235 token_string = token.clone();
236 params.push(("page_token", &token_string));
237 }
238
239 let url = format!("{}/v1beta1/options/snapshots", self.rest.data_base());
240 let request = self.rest.get(&url).query(¶ms);
241
242 let response: SnapshotsResponse = self.rest.request_with_retry(request).await?;
243
244 if let Some(snapshots_map) = response.snapshots {
245 let count = snapshots_map.len();
246 all_snapshots.extend(snapshots_map.into_iter().map(|(symbol, mut snapshot)| {
247 snapshot.symbol = symbol;
248 snapshot
249 }));
250
251 debug!(
252 page = guard.pages(),
253 count,
254 total = all_snapshots.len(),
255 "fetched snapshots page"
256 );
257 }
258
259 match response.next_page_token {
260 Some(token) if !token.is_empty() => {
261 page_token = Some(token);
262 }
263 _ => break,
264 }
265 }
266
267 Ok(all_snapshots)
268 }
269
270 pub async fn fetch_chain_snapshots(
284 &self,
285 underlying: &str,
286 feed: AlpacaOptionFeed,
287 ) -> Result<Vec<AlpacaOptionSnapshot>, AlpacaOptionsError> {
288 use super::AlpacaOptionContractQuery;
289
290 let query = AlpacaOptionContractQuery::new(vec![underlying.to_string()]);
292 let contracts = self.fetch_contracts(&query).await?;
293
294 if contracts.is_empty() {
295 return Ok(Vec::new());
296 }
297
298 debug!(
299 underlying,
300 contracts = contracts.len(),
301 "fetching snapshots for chain"
302 );
303
304 let symbols: Vec<String> = contracts.iter().map(|c| c.symbol.clone()).collect();
306 self.fetch_snapshots(&symbols, feed).await
307 }
308}
309
310#[cfg(test)]
311#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
313 use super::*;
314 use rust_decimal_macros::dec;
315
316 #[test]
317 fn feed_as_str() {
318 assert_eq!(AlpacaOptionFeed::Opra.as_str(), "opra");
319 assert_eq!(AlpacaOptionFeed::Indicative.as_str(), "indicative");
320 }
321
322 #[test]
323 fn quote_deserialize() {
324 let json = r#"{
325 "t": "2024-01-15T14:27:51.742904322Z",
326 "ax": "C",
327 "ap": 2.95,
328 "as": 50,
329 "bx": "N",
330 "bp": 2.85,
331 "bs": 75
332 }"#;
333
334 let quote: AlpacaOptionQuote = serde_json::from_str(json).unwrap();
335
336 assert_eq!(quote.ask_exchange, "C");
337 assert_eq!(quote.ask_price, dec!(2.95));
338 assert_eq!(quote.ask_size, 50);
339 assert_eq!(quote.bid_exchange, "N");
340 assert_eq!(quote.bid_price, dec!(2.85));
341 assert_eq!(quote.bid_size, 75);
342 }
343
344 #[test]
345 fn trade_deserialize() {
346 let json = r#"{
347 "t": "2024-01-15T14:25:48.889796106Z",
348 "x": "N",
349 "p": 2.84,
350 "s": 100
351 }"#;
352
353 let trade: AlpacaOptionTrade = serde_json::from_str(json).unwrap();
354
355 assert_eq!(trade.exchange, "N");
356 assert_eq!(trade.price, dec!(2.84));
357 assert_eq!(trade.size, 100);
358 }
359
360 #[test]
361 fn snapshot_deserialize_full() {
362 let json = r#"{
363 "symbol": "AAPL240119C00150000",
364 "latest_quote": {
365 "t": "2024-01-15T14:27:51.742904322Z",
366 "ax": "C",
367 "ap": 2.95,
368 "as": 50,
369 "bx": "N",
370 "bp": 2.85,
371 "bs": 75
372 },
373 "latest_trade": {
374 "t": "2024-01-15T14:25:48.889796106Z",
375 "x": "N",
376 "p": 2.84,
377 "s": 100
378 },
379 "greeks": {
380 "delta": 0.6234,
381 "gamma": 0.0412,
382 "theta": -0.0285,
383 "vega": 0.3156,
384 "rho": 0.1829
385 },
386 "implied_volatility": 0.287
387 }"#;
388
389 let snapshot: AlpacaOptionSnapshot = serde_json::from_str(json).unwrap();
390
391 assert_eq!(snapshot.symbol, "AAPL240119C00150000");
392 assert!(snapshot.latest_quote.is_some());
393 assert!(snapshot.latest_trade.is_some());
394 assert!(snapshot.has_greeks());
395
396 let greeks = snapshot.greeks();
397 assert_eq!(greeks.delta, Some(0.6234));
398 assert_eq!(greeks.gamma, Some(0.0412));
399 assert_eq!(greeks.theta, Some(-0.0285));
400 assert_eq!(greeks.vega, Some(0.3156));
401 assert_eq!(greeks.rho, Some(0.1829));
402 assert_eq!(greeks.implied_volatility, Some(0.287));
403 }
404
405 #[test]
406 fn snapshot_deserialize_minimal() {
407 let json = r#"{
408 "symbol": "AAPL240119C00150000"
409 }"#;
410
411 let snapshot: AlpacaOptionSnapshot = serde_json::from_str(json).unwrap();
412
413 assert_eq!(snapshot.symbol, "AAPL240119C00150000");
414 assert!(snapshot.latest_quote.is_none());
415 assert!(snapshot.latest_trade.is_none());
416 assert!(!snapshot.has_greeks());
417 }
418
419 #[test]
420 fn greeks_conversion() {
421 let alpaca_greeks = AlpacaGreeks {
422 delta: Some(0.55),
423 gamma: Some(0.02),
424 theta: Some(-0.05),
425 vega: Some(0.15),
426 rho: Some(0.10),
427 };
428
429 let greeks: OptionGreeks = alpaca_greeks.into();
430
431 assert_eq!(greeks.delta, Some(0.55));
432 assert_eq!(greeks.gamma, Some(0.02));
433 assert_eq!(greeks.theta, Some(-0.05));
434 assert_eq!(greeks.vega, Some(0.15));
435 assert_eq!(greeks.rho, Some(0.10));
436 assert!(greeks.implied_volatility.is_none());
438 }
439
440 #[test]
441 fn snapshots_response_deserialize() {
442 let json = r#"{
443 "snapshots": {
444 "AAPL240119C00150000": {
445 "symbol": "",
446 "implied_volatility": 0.25
447 }
448 },
449 "next_page_token": "abc123"
450 }"#;
451
452 let response: SnapshotsResponse = serde_json::from_str(json).unwrap();
453
454 assert!(response.snapshots.is_some());
455 assert_eq!(response.snapshots.unwrap().len(), 1);
456 assert_eq!(response.next_page_token, Some("abc123".to_string()));
457 }
458
459 #[test]
460 fn snapshots_response_empty() {
461 let json = r#"{}"#;
462
463 let response: SnapshotsResponse = serde_json::from_str(json).unwrap();
464
465 assert!(response.snapshots.is_none());
466 assert!(response.next_page_token.is_none());
467 }
468}