alpaca_data/options/
convenience.rs1use std::collections::HashMap;
2
3use rust_decimal::Decimal;
4
5use super::{OptionsFeed, Snapshot};
6
7impl Snapshot {
8 #[must_use]
9 pub fn timestamp(&self) -> Option<&str> {
10 [
11 self.latest_trade
12 .as_ref()
13 .and_then(|trade| trade.t.as_deref()),
14 self.latest_quote
15 .as_ref()
16 .and_then(|quote| quote.t.as_deref()),
17 self.minute_bar.as_ref().and_then(|bar| bar.t.as_deref()),
18 self.daily_bar.as_ref().and_then(|bar| bar.t.as_deref()),
19 self.prev_daily_bar
20 .as_ref()
21 .and_then(|bar| bar.t.as_deref()),
22 ]
23 .into_iter()
24 .flatten()
25 .filter(|value| !value.trim().is_empty())
26 .max()
27 }
28
29 #[must_use]
30 pub fn bid_price(&self) -> Option<Decimal> {
31 self.latest_quote.as_ref().and_then(|quote| quote.bp)
32 }
33
34 #[must_use]
35 pub fn ask_price(&self) -> Option<Decimal> {
36 self.latest_quote.as_ref().and_then(|quote| quote.ap)
37 }
38
39 #[must_use]
40 pub fn last_price(&self) -> Option<Decimal> {
41 self.latest_trade.as_ref().and_then(|trade| trade.p)
42 }
43
44 #[must_use]
45 pub fn minute_volume(&self) -> Option<u64> {
46 self.minute_bar.as_ref().and_then(|bar| bar.v)
47 }
48
49 #[must_use]
50 pub fn daily_volume(&self) -> Option<u64> {
51 self.daily_bar.as_ref().and_then(|bar| bar.v)
52 }
53
54 #[must_use]
55 pub fn latest_trade_size(&self) -> Option<u64> {
56 self.latest_trade.as_ref().and_then(|trade| trade.s)
57 }
58
59 #[must_use]
60 pub fn bid_size(&self) -> Option<u64> {
61 self.latest_quote.as_ref().and_then(|quote| quote.bs)
62 }
63
64 #[must_use]
65 pub fn ask_size(&self) -> Option<u64> {
66 self.latest_quote.as_ref().and_then(|quote| quote.r#as)
67 }
68
69 #[must_use]
70 pub fn mark_price(&self) -> Option<Decimal> {
71 match (self.bid_price(), self.ask_price()) {
72 (Some(bid), Some(ask)) => Some((bid + ask) / Decimal::from(2u8)),
73 (Some(bid), None) => Some(bid),
74 (None, Some(ask)) => Some(ask),
75 (None, None) => None,
76 }
77 }
78}
79
80#[must_use]
81pub fn ordered_snapshots(snapshots: &HashMap<String, Snapshot>) -> Vec<(&str, &Snapshot)> {
82 let mut symbols = snapshots.keys().map(String::as_str).collect::<Vec<_>>();
83 symbols.sort_unstable();
84 symbols
85 .into_iter()
86 .filter_map(|symbol| {
87 snapshots
88 .get_key_value(symbol)
89 .map(|(symbol, snapshot)| (symbol.as_str(), snapshot))
90 })
91 .collect()
92}
93
94#[must_use]
95pub fn preferred_feed() -> OptionsFeed {
96 OptionsFeed::Opra
97}
98
99#[cfg(test)]
100mod tests {
101 use std::collections::HashMap;
102
103 use rust_decimal::Decimal;
104
105 use super::{Snapshot, ordered_snapshots, preferred_feed};
106 use crate::options::{Bar, OptionsFeed, Quote, Trade};
107
108 #[test]
109 fn snapshot_timestamp_prefers_the_freshest_available_value() {
110 let snapshot = Snapshot {
111 latest_trade: Some(Trade {
112 t: Some("2026-04-13T13:30:01Z".to_owned()),
113 ..Trade::default()
114 }),
115 latest_quote: Some(Quote {
116 t: Some("2026-04-13T13:30:05Z".to_owned()),
117 ..Quote::default()
118 }),
119 minute_bar: Some(Bar {
120 t: Some("2026-04-13T13:30:00Z".to_owned()),
121 ..Bar::default()
122 }),
123 ..Snapshot::default()
124 };
125
126 assert_eq!(snapshot.timestamp(), Some("2026-04-13T13:30:05Z"));
127 }
128
129 #[test]
130 fn snapshot_mark_price_absorbs_single_sided_quotes() {
131 let with_both_sides = Snapshot {
132 latest_quote: Some(Quote {
133 bp: Some(Decimal::new(125, 2)),
134 ap: Some(Decimal::new(135, 2)),
135 ..Quote::default()
136 }),
137 ..Snapshot::default()
138 };
139 let with_bid_only = Snapshot {
140 latest_quote: Some(Quote {
141 bp: Some(Decimal::new(125, 2)),
142 ..Quote::default()
143 }),
144 ..Snapshot::default()
145 };
146
147 assert_eq!(with_both_sides.mark_price(), Some(Decimal::new(130, 2)));
148 assert_eq!(with_bid_only.mark_price(), Some(Decimal::new(125, 2)));
149 }
150
151 #[test]
152 fn ordered_snapshots_returns_stable_contract_order() {
153 let mut snapshots = HashMap::new();
154 snapshots.insert("QQQ250620C00500000".to_owned(), Snapshot::default());
155 snapshots.insert("AAPL250620C00200000".to_owned(), Snapshot::default());
156
157 let ordered = ordered_snapshots(&snapshots);
158 assert_eq!(ordered[0].0, "AAPL250620C00200000");
159 assert_eq!(ordered[1].0, "QQQ250620C00500000");
160 }
161
162 #[test]
163 fn preferred_feed_uses_opra() {
164 assert_eq!(preferred_feed(), OptionsFeed::Opra);
165 }
166}