Skip to main content

alpaca_data/stocks/
convenience.rs

1use std::collections::HashMap;
2
3use rust_decimal::Decimal;
4
5use super::{Bar, DataFeed, Snapshot};
6
7#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
8pub struct BarPoint {
9    pub timestamp: String,
10    pub open: Decimal,
11    pub high: Decimal,
12    pub low: Decimal,
13    pub close: Decimal,
14    pub volume: i64,
15}
16
17fn timestamp_parts<'a>(
18    latest_trade: Option<&'a str>,
19    latest_quote: Option<&'a str>,
20    minute_bar: Option<&'a str>,
21    daily_bar: Option<&'a str>,
22    prev_daily_bar: Option<&'a str>,
23) -> Option<&'a str> {
24    [
25        latest_trade,
26        latest_quote,
27        minute_bar,
28        daily_bar,
29        prev_daily_bar,
30    ]
31    .into_iter()
32    .flatten()
33    .filter(|value| !value.trim().is_empty())
34    .max()
35}
36
37fn price_parts(
38    latest_trade: Option<Decimal>,
39    bid: Option<Decimal>,
40    ask: Option<Decimal>,
41    minute_close: Option<Decimal>,
42    daily_close: Option<Decimal>,
43    prev_daily_close: Option<Decimal>,
44) -> Option<Decimal> {
45    latest_trade
46        .or_else(|| match (bid, ask) {
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        .or(minute_close)
53        .or(daily_close)
54        .or(prev_daily_close)
55}
56
57fn quote_bid(snapshot: &Snapshot) -> Option<Decimal> {
58    snapshot.latest_quote.as_ref().and_then(|quote| quote.bp)
59}
60
61fn quote_ask(snapshot: &Snapshot) -> Option<Decimal> {
62    snapshot.latest_quote.as_ref().and_then(|quote| quote.ap)
63}
64
65fn session_open(snapshot: &Snapshot) -> Option<Decimal> {
66    snapshot.daily_bar.as_ref().and_then(|bar| bar.o)
67}
68
69fn session_high(snapshot: &Snapshot) -> Option<Decimal> {
70    snapshot.daily_bar.as_ref().and_then(|bar| bar.h)
71}
72
73fn session_low(snapshot: &Snapshot) -> Option<Decimal> {
74    snapshot.daily_bar.as_ref().and_then(|bar| bar.l)
75}
76
77fn session_close(snapshot: &Snapshot) -> Option<Decimal> {
78    snapshot.daily_bar.as_ref().and_then(|bar| bar.c)
79}
80
81fn previous_close(snapshot: &Snapshot) -> Option<Decimal> {
82    snapshot.prev_daily_bar.as_ref().and_then(|bar| bar.c)
83}
84
85fn session_volume(snapshot: &Snapshot) -> Option<u64> {
86    snapshot.daily_bar.as_ref().and_then(|bar| bar.v)
87}
88
89impl Snapshot {
90    #[must_use]
91    pub fn timestamp(&self) -> Option<&str> {
92        timestamp_parts(
93            self.latest_trade
94                .as_ref()
95                .and_then(|trade| trade.t.as_deref()),
96            self.latest_quote
97                .as_ref()
98                .and_then(|quote| quote.t.as_deref()),
99            self.minute_bar.as_ref().and_then(|bar| bar.t.as_deref()),
100            self.daily_bar.as_ref().and_then(|bar| bar.t.as_deref()),
101            self.prev_daily_bar
102                .as_ref()
103                .and_then(|bar| bar.t.as_deref()),
104        )
105    }
106
107    #[must_use]
108    pub fn price(&self) -> Option<Decimal> {
109        price_parts(
110            self.latest_trade.as_ref().and_then(|trade| trade.p),
111            self.bid_price(),
112            self.ask_price(),
113            self.minute_bar.as_ref().and_then(|bar| bar.c),
114            self.session_close(),
115            self.previous_close(),
116        )
117    }
118
119    #[must_use]
120    pub fn bid_price(&self) -> Option<Decimal> {
121        quote_bid(self)
122    }
123
124    #[must_use]
125    pub fn ask_price(&self) -> Option<Decimal> {
126        quote_ask(self)
127    }
128
129    #[must_use]
130    pub fn session_open(&self) -> Option<Decimal> {
131        session_open(self)
132    }
133
134    #[must_use]
135    pub fn session_high(&self) -> Option<Decimal> {
136        session_high(self)
137    }
138
139    #[must_use]
140    pub fn session_low(&self) -> Option<Decimal> {
141        session_low(self)
142    }
143
144    #[must_use]
145    pub fn session_close(&self) -> Option<Decimal> {
146        session_close(self)
147    }
148
149    #[must_use]
150    pub fn previous_close(&self) -> Option<Decimal> {
151        previous_close(self)
152    }
153
154    #[must_use]
155    pub fn session_volume(&self) -> Option<u64> {
156        session_volume(self)
157    }
158}
159
160impl Bar {
161    #[must_use]
162    pub fn point(&self, daily: bool) -> BarPoint {
163        let raw_timestamp = self.t.clone().unwrap_or_default();
164        let timestamp = if daily {
165            raw_timestamp
166                .get(..10)
167                .unwrap_or(raw_timestamp.as_str())
168                .to_owned()
169        } else {
170            raw_timestamp
171        };
172
173        BarPoint {
174            timestamp,
175            open: self.o.unwrap_or_default(),
176            high: self.h.unwrap_or_default(),
177            low: self.l.unwrap_or_default(),
178            close: self.c.unwrap_or_default(),
179            volume: match self.v {
180                Some(value) => i64::try_from(value).unwrap_or(i64::MAX),
181                None => 0,
182            },
183        }
184    }
185}
186
187#[must_use]
188pub fn ordered_snapshots(snapshots: &HashMap<String, Snapshot>) -> Vec<(&str, &Snapshot)> {
189    let mut symbols = snapshots.keys().map(String::as_str).collect::<Vec<_>>();
190    symbols.sort_unstable();
191    symbols
192        .into_iter()
193        .filter_map(|symbol| {
194            snapshots
195                .get_key_value(symbol)
196                .map(|(symbol, snapshot)| (symbol.as_str(), snapshot))
197        })
198        .collect()
199}
200
201#[must_use]
202pub fn preferred_feed(extended_hours: bool) -> DataFeed {
203    if extended_hours {
204        DataFeed::Boats
205    } else {
206        DataFeed::Sip
207    }
208}