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 mark_price(&self) -> Option<Decimal> {
46 match (self.bid_price(), self.ask_price()) {
47 (Some(bid), Some(ask)) => Some((bid + ask) / Decimal::from(2u8)),
48 (Some(bid), None) => Some(bid),
49 (None, Some(ask)) => Some(ask),
50 (None, None) => None,
51 }
52 }
53}
54
55#[must_use]
56pub fn ordered_snapshots(snapshots: &HashMap<String, Snapshot>) -> Vec<(&str, &Snapshot)> {
57 let mut symbols = snapshots.keys().map(String::as_str).collect::<Vec<_>>();
58 symbols.sort_unstable();
59 symbols
60 .into_iter()
61 .filter_map(|symbol| {
62 snapshots
63 .get_key_value(symbol)
64 .map(|(symbol, snapshot)| (symbol.as_str(), snapshot))
65 })
66 .collect()
67}
68
69#[must_use]
70pub fn preferred_feed() -> OptionsFeed {
71 OptionsFeed::Opra
72}
73
74#[cfg(test)]
75mod tests {
76 use std::collections::HashMap;
77
78 use rust_decimal::Decimal;
79
80 use super::{Snapshot, ordered_snapshots, preferred_feed};
81 use crate::options::{Bar, OptionsFeed, Quote, Trade};
82
83 #[test]
84 fn snapshot_timestamp_prefers_the_freshest_available_value() {
85 let snapshot = Snapshot {
86 latest_trade: Some(Trade {
87 t: Some("2026-04-13T13:30:01Z".to_owned()),
88 ..Trade::default()
89 }),
90 latest_quote: Some(Quote {
91 t: Some("2026-04-13T13:30:05Z".to_owned()),
92 ..Quote::default()
93 }),
94 minute_bar: Some(Bar {
95 t: Some("2026-04-13T13:30:00Z".to_owned()),
96 ..Bar::default()
97 }),
98 ..Snapshot::default()
99 };
100
101 assert_eq!(snapshot.timestamp(), Some("2026-04-13T13:30:05Z"));
102 }
103
104 #[test]
105 fn snapshot_mark_price_absorbs_single_sided_quotes() {
106 let with_both_sides = Snapshot {
107 latest_quote: Some(Quote {
108 bp: Some(Decimal::new(125, 2)),
109 ap: Some(Decimal::new(135, 2)),
110 ..Quote::default()
111 }),
112 ..Snapshot::default()
113 };
114 let with_bid_only = Snapshot {
115 latest_quote: Some(Quote {
116 bp: Some(Decimal::new(125, 2)),
117 ..Quote::default()
118 }),
119 ..Snapshot::default()
120 };
121
122 assert_eq!(with_both_sides.mark_price(), Some(Decimal::new(130, 2)));
123 assert_eq!(with_bid_only.mark_price(), Some(Decimal::new(125, 2)));
124 }
125
126 #[test]
127 fn ordered_snapshots_returns_stable_contract_order() {
128 let mut snapshots = HashMap::new();
129 snapshots.insert("QQQ250620C00500000".to_owned(), Snapshot::default());
130 snapshots.insert("AAPL250620C00200000".to_owned(), Snapshot::default());
131
132 let ordered = ordered_snapshots(&snapshots);
133 assert_eq!(ordered[0].0, "AAPL250620C00200000");
134 assert_eq!(ordered[1].0, "QQQ250620C00500000");
135 }
136
137 #[test]
138 fn preferred_feed_uses_opra() {
139 assert_eq!(preferred_feed(), OptionsFeed::Opra);
140 }
141}