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}