Skip to main content

alpaca_data/options/
convenience.rs

1use 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}