alpaca_data/stocks/
convenience.rs1use 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}