Skip to main content

alpaca_data/stocks/
request.rs

1use alpaca_core::{QueryWriter, pagination::PaginatedRequest};
2
3use crate::Error;
4use crate::symbols::display_stock_symbol;
5
6use super::{Adjustment, AuctionFeed, Currency, DataFeed, Sort, Tape, TickType, TimeFrame};
7
8#[derive(Clone, Debug, Default)]
9pub struct BarsRequest {
10    pub symbols: Vec<String>,
11    pub timeframe: TimeFrame,
12    pub start: Option<String>,
13    pub end: Option<String>,
14    pub limit: Option<u32>,
15    pub adjustment: Option<Adjustment>,
16    pub feed: Option<DataFeed>,
17    pub sort: Option<Sort>,
18    pub asof: Option<String>,
19    pub currency: Option<Currency>,
20    pub page_token: Option<String>,
21}
22
23#[derive(Clone, Debug, Default)]
24pub struct AuctionsRequest {
25    pub symbols: Vec<String>,
26    pub start: Option<String>,
27    pub end: Option<String>,
28    pub limit: Option<u32>,
29    pub asof: Option<String>,
30    pub feed: Option<AuctionFeed>,
31    pub currency: Option<Currency>,
32    pub page_token: Option<String>,
33    pub sort: Option<Sort>,
34}
35
36#[derive(Clone, Debug, Default)]
37pub struct QuotesRequest {
38    pub symbols: Vec<String>,
39    pub start: Option<String>,
40    pub end: Option<String>,
41    pub limit: Option<u32>,
42    pub feed: Option<DataFeed>,
43    pub sort: Option<Sort>,
44    pub asof: Option<String>,
45    pub currency: Option<Currency>,
46    pub page_token: Option<String>,
47}
48
49#[derive(Clone, Debug, Default)]
50pub struct TradesRequest {
51    pub symbols: Vec<String>,
52    pub start: Option<String>,
53    pub end: Option<String>,
54    pub limit: Option<u32>,
55    pub feed: Option<DataFeed>,
56    pub sort: Option<Sort>,
57    pub asof: Option<String>,
58    pub currency: Option<Currency>,
59    pub page_token: Option<String>,
60}
61
62#[derive(Clone, Debug, Default)]
63pub struct LatestBarsRequest {
64    pub symbols: Vec<String>,
65    pub feed: Option<DataFeed>,
66    pub currency: Option<Currency>,
67}
68
69#[derive(Clone, Debug, Default)]
70pub struct LatestQuotesRequest {
71    pub symbols: Vec<String>,
72    pub feed: Option<DataFeed>,
73    pub currency: Option<Currency>,
74}
75
76#[derive(Clone, Debug, Default)]
77pub struct LatestTradesRequest {
78    pub symbols: Vec<String>,
79    pub feed: Option<DataFeed>,
80    pub currency: Option<Currency>,
81}
82
83#[derive(Clone, Debug, Default)]
84pub struct SnapshotsRequest {
85    pub symbols: Vec<String>,
86    pub feed: Option<DataFeed>,
87    pub currency: Option<Currency>,
88}
89
90#[derive(Clone, Debug, Default)]
91pub struct ConditionCodesRequest {
92    pub ticktype: TickType,
93    pub tape: Tape,
94}
95
96impl BarsRequest {
97    pub(crate) fn validate(&self) -> Result<(), Error> {
98        validate_required_symbols(&self.symbols)?;
99        validate_limit(self.limit, 1, 10_000)
100    }
101
102    pub(crate) fn into_query(self) -> Vec<(String, String)> {
103        let mut query = QueryWriter::default();
104        query.push_csv("symbols", normalized_stock_symbols(&self.symbols));
105        query.push_opt("timeframe", Some(self.timeframe));
106        query.push_opt("start", self.start);
107        query.push_opt("end", self.end);
108        query.push_opt("limit", self.limit);
109        query.push_opt("adjustment", self.adjustment);
110        query.push_opt("feed", self.feed);
111        query.push_opt("sort", self.sort);
112        query.push_opt("asof", self.asof);
113        query.push_opt("currency", self.currency);
114        query.push_opt("page_token", self.page_token);
115        query.finish()
116    }
117
118    pub(crate) fn single_symbol(&self) -> Option<String> {
119        normalized_single_stock_symbol(&self.symbols)
120    }
121
122    pub(crate) fn into_single_query(mut self) -> Vec<(String, String)> {
123        self.symbols.clear();
124        self.into_query()
125    }
126}
127
128impl AuctionsRequest {
129    pub(crate) fn validate(&self) -> Result<(), Error> {
130        validate_required_symbols(&self.symbols)?;
131        validate_limit(self.limit, 1, 10_000)
132    }
133
134    pub(crate) fn into_query(self) -> Vec<(String, String)> {
135        let mut query = QueryWriter::default();
136        query.push_csv("symbols", normalized_stock_symbols(&self.symbols));
137        query.push_opt("start", self.start);
138        query.push_opt("end", self.end);
139        query.push_opt("limit", self.limit);
140        query.push_opt("asof", self.asof);
141        query.push_opt("feed", self.feed);
142        query.push_opt("currency", self.currency);
143        query.push_opt("page_token", self.page_token);
144        query.push_opt("sort", self.sort);
145        query.finish()
146    }
147
148    pub(crate) fn single_symbol(&self) -> Option<String> {
149        normalized_single_stock_symbol(&self.symbols)
150    }
151
152    pub(crate) fn into_single_query(mut self) -> Vec<(String, String)> {
153        self.symbols.clear();
154        self.into_query()
155    }
156}
157
158impl QuotesRequest {
159    pub(crate) fn validate(&self) -> Result<(), Error> {
160        validate_required_symbols(&self.symbols)?;
161        validate_limit(self.limit, 1, 10_000)
162    }
163
164    pub(crate) fn into_query(self) -> Vec<(String, String)> {
165        let mut query = QueryWriter::default();
166        query.push_csv("symbols", normalized_stock_symbols(&self.symbols));
167        query.push_opt("start", self.start);
168        query.push_opt("end", self.end);
169        query.push_opt("limit", self.limit);
170        query.push_opt("feed", self.feed);
171        query.push_opt("sort", self.sort);
172        query.push_opt("asof", self.asof);
173        query.push_opt("currency", self.currency);
174        query.push_opt("page_token", self.page_token);
175        query.finish()
176    }
177
178    pub(crate) fn single_symbol(&self) -> Option<String> {
179        normalized_single_stock_symbol(&self.symbols)
180    }
181
182    pub(crate) fn into_single_query(mut self) -> Vec<(String, String)> {
183        self.symbols.clear();
184        self.into_query()
185    }
186}
187
188impl TradesRequest {
189    pub(crate) fn validate(&self) -> Result<(), Error> {
190        validate_required_symbols(&self.symbols)?;
191        validate_limit(self.limit, 1, 10_000)
192    }
193
194    pub(crate) fn into_query(self) -> Vec<(String, String)> {
195        let mut query = QueryWriter::default();
196        query.push_csv("symbols", normalized_stock_symbols(&self.symbols));
197        query.push_opt("start", self.start);
198        query.push_opt("end", self.end);
199        query.push_opt("limit", self.limit);
200        query.push_opt("feed", self.feed);
201        query.push_opt("sort", self.sort);
202        query.push_opt("asof", self.asof);
203        query.push_opt("currency", self.currency);
204        query.push_opt("page_token", self.page_token);
205        query.finish()
206    }
207
208    pub(crate) fn single_symbol(&self) -> Option<String> {
209        normalized_single_stock_symbol(&self.symbols)
210    }
211
212    pub(crate) fn into_single_query(mut self) -> Vec<(String, String)> {
213        self.symbols.clear();
214        self.into_query()
215    }
216}
217
218impl LatestBarsRequest {
219    pub(crate) fn validate(&self) -> Result<(), Error> {
220        validate_required_symbols(&self.symbols)
221    }
222
223    pub(crate) fn into_query(self) -> Vec<(String, String)> {
224        latest_batch_query(self.symbols, self.feed, self.currency)
225    }
226
227    pub(crate) fn single_symbol(&self) -> Option<String> {
228        normalized_single_stock_symbol(&self.symbols)
229    }
230
231    pub(crate) fn into_single_query(mut self) -> Vec<(String, String)> {
232        self.symbols.clear();
233        self.into_query()
234    }
235}
236
237impl LatestQuotesRequest {
238    pub(crate) fn validate(&self) -> Result<(), Error> {
239        validate_required_symbols(&self.symbols)
240    }
241
242    pub(crate) fn into_query(self) -> Vec<(String, String)> {
243        latest_batch_query(self.symbols, self.feed, self.currency)
244    }
245
246    pub(crate) fn single_symbol(&self) -> Option<String> {
247        normalized_single_stock_symbol(&self.symbols)
248    }
249
250    pub(crate) fn into_single_query(mut self) -> Vec<(String, String)> {
251        self.symbols.clear();
252        self.into_query()
253    }
254}
255
256impl LatestTradesRequest {
257    pub(crate) fn validate(&self) -> Result<(), Error> {
258        validate_required_symbols(&self.symbols)
259    }
260
261    pub(crate) fn into_query(self) -> Vec<(String, String)> {
262        latest_batch_query(self.symbols, self.feed, self.currency)
263    }
264
265    pub(crate) fn single_symbol(&self) -> Option<String> {
266        normalized_single_stock_symbol(&self.symbols)
267    }
268
269    pub(crate) fn into_single_query(mut self) -> Vec<(String, String)> {
270        self.symbols.clear();
271        self.into_query()
272    }
273}
274
275impl SnapshotsRequest {
276    pub(crate) fn validate(&self) -> Result<(), Error> {
277        validate_required_symbols(&self.symbols)
278    }
279
280    pub(crate) fn into_query(self) -> Vec<(String, String)> {
281        latest_batch_query(self.symbols, self.feed, self.currency)
282    }
283
284    pub(crate) fn single_symbol(&self) -> Option<String> {
285        normalized_single_stock_symbol(&self.symbols)
286    }
287
288    pub(crate) fn into_single_query(mut self) -> Vec<(String, String)> {
289        self.symbols.clear();
290        self.into_query()
291    }
292}
293
294impl ConditionCodesRequest {
295    pub(crate) fn into_query(self) -> Vec<(String, String)> {
296        let mut query = QueryWriter::default();
297        query.push_opt("tape", Some(self.tape));
298        query.finish()
299    }
300}
301
302impl PaginatedRequest for BarsRequest {
303    fn with_page_token(&self, page_token: Option<String>) -> Self {
304        let mut next = self.clone();
305        next.page_token = page_token;
306        next
307    }
308}
309
310impl PaginatedRequest for AuctionsRequest {
311    fn with_page_token(&self, page_token: Option<String>) -> Self {
312        let mut next = self.clone();
313        next.page_token = page_token;
314        next
315    }
316}
317
318impl PaginatedRequest for QuotesRequest {
319    fn with_page_token(&self, page_token: Option<String>) -> Self {
320        let mut next = self.clone();
321        next.page_token = page_token;
322        next
323    }
324}
325
326impl PaginatedRequest for TradesRequest {
327    fn with_page_token(&self, page_token: Option<String>) -> Self {
328        let mut next = self.clone();
329        next.page_token = page_token;
330        next
331    }
332}
333
334fn latest_batch_query(
335    symbols: Vec<String>,
336    feed: Option<DataFeed>,
337    currency: Option<Currency>,
338) -> Vec<(String, String)> {
339    let mut query = QueryWriter::default();
340    query.push_csv("symbols", normalized_stock_symbols(&symbols));
341    query.push_opt("feed", feed);
342    query.push_opt("currency", currency);
343    query.finish()
344}
345
346fn validate_required_symbols(symbols: &[String]) -> Result<(), Error> {
347    if symbols.is_empty() {
348        return Err(Error::InvalidRequest(
349            "symbols are invalid: must not be empty".to_owned(),
350        ));
351    }
352
353    if symbols
354        .iter()
355        .any(|symbol| normalized_stock_symbol(symbol).is_empty())
356    {
357        return Err(Error::InvalidRequest(
358            "symbols are invalid: must not contain empty or whitespace-only entries".to_owned(),
359        ));
360    }
361
362    Ok(())
363}
364
365fn normalized_stock_symbol(symbol: &str) -> String {
366    display_stock_symbol(symbol)
367}
368
369fn normalized_stock_symbols(symbols: &[String]) -> Vec<String> {
370    symbols
371        .iter()
372        .map(|symbol| normalized_stock_symbol(symbol))
373        .collect()
374}
375
376fn normalized_single_stock_symbol(symbols: &[String]) -> Option<String> {
377    (symbols.len() == 1).then(|| encoded_stock_path_segment(&normalized_stock_symbol(&symbols[0])))
378}
379
380fn encoded_stock_path_segment(symbol: &str) -> String {
381    let mut url = reqwest::Url::parse("https://example.invalid/")
382        .expect("constant stock path encoding URL should parse");
383    url.path_segments_mut()
384        .expect("constant stock path encoding URL should support path segments")
385        .push(symbol);
386    url.path().trim_start_matches('/').to_owned()
387}
388
389fn validate_limit(limit: Option<u32>, min: u32, max: u32) -> Result<(), Error> {
390    if let Some(limit) = limit
391        && !(min..=max).contains(&limit)
392    {
393        return Err(Error::InvalidRequest(format!(
394            "limit must be between {min} and {max}"
395        )));
396    }
397
398    Ok(())
399}