Skip to main content

alpaca_data/crypto/
request.rs

1use crate::Error;
2use crate::common::query::QueryWriter;
3use crate::transport::pagination::PaginatedRequest;
4
5use super::{Loc, Sort, TimeFrame};
6
7#[derive(Clone, Debug, Default)]
8pub struct BarsRequest {
9    pub symbols: Vec<String>,
10    pub timeframe: TimeFrame,
11    pub start: Option<String>,
12    pub end: Option<String>,
13    pub limit: Option<u32>,
14    pub sort: Option<Sort>,
15    pub loc: Option<Loc>,
16    pub page_token: Option<String>,
17}
18
19#[derive(Clone, Debug, Default)]
20pub struct QuotesRequest {
21    pub symbols: Vec<String>,
22    pub start: Option<String>,
23    pub end: Option<String>,
24    pub limit: Option<u32>,
25    pub sort: Option<Sort>,
26    pub loc: Option<Loc>,
27    pub page_token: Option<String>,
28}
29
30#[derive(Clone, Debug, Default)]
31pub struct TradesRequest {
32    pub symbols: Vec<String>,
33    pub start: Option<String>,
34    pub end: Option<String>,
35    pub limit: Option<u32>,
36    pub sort: Option<Sort>,
37    pub loc: Option<Loc>,
38    pub page_token: Option<String>,
39}
40
41#[derive(Clone, Debug, Default)]
42pub struct LatestBarsRequest {
43    pub symbols: Vec<String>,
44    pub loc: Option<Loc>,
45}
46
47#[derive(Clone, Debug, Default)]
48pub struct LatestQuotesRequest {
49    pub symbols: Vec<String>,
50    pub loc: Option<Loc>,
51}
52
53#[derive(Clone, Debug, Default)]
54pub struct LatestTradesRequest {
55    pub symbols: Vec<String>,
56    pub loc: Option<Loc>,
57}
58
59#[derive(Clone, Debug, Default)]
60pub struct LatestOrderbooksRequest {
61    pub symbols: Vec<String>,
62    pub loc: Option<Loc>,
63}
64
65#[derive(Clone, Debug, Default)]
66pub struct SnapshotsRequest {
67    pub symbols: Vec<String>,
68    pub loc: Option<Loc>,
69}
70
71impl BarsRequest {
72    pub(crate) fn validate(&self) -> Result<(), Error> {
73        validate_required_symbols(&self.symbols)?;
74        validate_limit(self.limit, 1, 10_000)
75    }
76
77    pub(crate) fn to_query(self) -> Vec<(String, String)> {
78        let mut query = QueryWriter::default();
79        query.push_csv("symbols", self.symbols);
80        query.push_opt("timeframe", Some(self.timeframe));
81        query.push_opt("start", self.start);
82        query.push_opt("end", self.end);
83        query.push_opt("limit", self.limit);
84        query.push_opt("page_token", self.page_token);
85        query.push_opt("sort", self.sort);
86        query.finish()
87    }
88}
89
90impl QuotesRequest {
91    pub(crate) fn validate(&self) -> Result<(), Error> {
92        validate_required_symbols(&self.symbols)?;
93        validate_limit(self.limit, 1, 10_000)
94    }
95
96    pub(crate) fn to_query(self) -> Vec<(String, String)> {
97        let mut query = QueryWriter::default();
98        query.push_csv("symbols", self.symbols);
99        query.push_opt("start", self.start);
100        query.push_opt("end", self.end);
101        query.push_opt("limit", self.limit);
102        query.push_opt("page_token", self.page_token);
103        query.push_opt("sort", self.sort);
104        query.finish()
105    }
106}
107
108impl TradesRequest {
109    pub(crate) fn validate(&self) -> Result<(), Error> {
110        validate_required_symbols(&self.symbols)?;
111        validate_limit(self.limit, 1, 10_000)
112    }
113
114    pub(crate) fn to_query(self) -> Vec<(String, String)> {
115        let mut query = QueryWriter::default();
116        query.push_csv("symbols", self.symbols);
117        query.push_opt("start", self.start);
118        query.push_opt("end", self.end);
119        query.push_opt("limit", self.limit);
120        query.push_opt("page_token", self.page_token);
121        query.push_opt("sort", self.sort);
122        query.finish()
123    }
124}
125
126impl LatestBarsRequest {
127    pub(crate) fn validate(&self) -> Result<(), Error> {
128        validate_required_symbols(&self.symbols)
129    }
130
131    pub(crate) fn to_query(self) -> Vec<(String, String)> {
132        latest_query(self.symbols)
133    }
134}
135
136impl LatestQuotesRequest {
137    pub(crate) fn validate(&self) -> Result<(), Error> {
138        validate_required_symbols(&self.symbols)
139    }
140
141    pub(crate) fn to_query(self) -> Vec<(String, String)> {
142        latest_query(self.symbols)
143    }
144}
145
146impl LatestTradesRequest {
147    pub(crate) fn validate(&self) -> Result<(), Error> {
148        validate_required_symbols(&self.symbols)
149    }
150
151    pub(crate) fn to_query(self) -> Vec<(String, String)> {
152        latest_query(self.symbols)
153    }
154}
155
156impl LatestOrderbooksRequest {
157    pub(crate) fn validate(&self) -> Result<(), Error> {
158        validate_required_symbols(&self.symbols)
159    }
160
161    pub(crate) fn to_query(self) -> Vec<(String, String)> {
162        latest_query(self.symbols)
163    }
164}
165
166impl SnapshotsRequest {
167    pub(crate) fn validate(&self) -> Result<(), Error> {
168        validate_required_symbols(&self.symbols)
169    }
170
171    pub(crate) fn to_query(self) -> Vec<(String, String)> {
172        latest_query(self.symbols)
173    }
174}
175
176impl PaginatedRequest for BarsRequest {
177    fn with_page_token(&self, page_token: Option<String>) -> Self {
178        let mut next = self.clone();
179        next.page_token = page_token;
180        next
181    }
182}
183
184impl PaginatedRequest for QuotesRequest {
185    fn with_page_token(&self, page_token: Option<String>) -> Self {
186        let mut next = self.clone();
187        next.page_token = page_token;
188        next
189    }
190}
191
192impl PaginatedRequest for TradesRequest {
193    fn with_page_token(&self, page_token: Option<String>) -> Self {
194        let mut next = self.clone();
195        next.page_token = page_token;
196        next
197    }
198}
199
200fn latest_query(symbols: Vec<String>) -> Vec<(String, String)> {
201    let mut query = QueryWriter::default();
202    query.push_csv("symbols", symbols);
203    query.finish()
204}
205
206fn validate_required_symbols(symbols: &[String]) -> Result<(), Error> {
207    if symbols.is_empty() {
208        return Err(Error::InvalidRequest("symbols must not be empty".into()));
209    }
210
211    Ok(())
212}
213
214fn validate_limit(limit: Option<u32>, min: u32, max: u32) -> Result<(), Error> {
215    if let Some(limit) = limit {
216        if !(min..=max).contains(&limit) {
217            return Err(Error::InvalidRequest(format!(
218                "limit must be between {min} and {max}"
219            )));
220        }
221    }
222
223    Ok(())
224}
225
226#[cfg(test)]
227mod tests {
228    use crate::Error;
229    use crate::transport::pagination::PaginatedRequest;
230
231    use super::{
232        BarsRequest, LatestBarsRequest, LatestOrderbooksRequest, LatestQuotesRequest,
233        LatestTradesRequest, Loc, QuotesRequest, SnapshotsRequest, Sort, TimeFrame, TradesRequest,
234    };
235
236    #[test]
237    fn bars_request_serializes_official_query_words_without_loc() {
238        let query = BarsRequest {
239            symbols: vec!["BTC/USD".into(), "ETH/USD".into()],
240            timeframe: TimeFrame::from("1Min"),
241            start: Some("2026-04-04T00:00:00Z".into()),
242            end: Some("2026-04-04T00:02:00Z".into()),
243            limit: Some(2),
244            sort: Some(Sort::Desc),
245            loc: Some(Loc::Eu1),
246            page_token: Some("page-2".into()),
247        }
248        .to_query();
249
250        assert_eq!(
251            query,
252            vec![
253                ("symbols".to_string(), "BTC/USD,ETH/USD".to_string()),
254                ("timeframe".to_string(), "1Min".to_string()),
255                ("start".to_string(), "2026-04-04T00:00:00Z".to_string()),
256                ("end".to_string(), "2026-04-04T00:02:00Z".to_string()),
257                ("limit".to_string(), "2".to_string()),
258                ("page_token".to_string(), "page-2".to_string()),
259                ("sort".to_string(), "desc".to_string()),
260            ]
261        );
262    }
263
264    #[test]
265    fn quotes_and_trades_requests_serialize_official_query_words_without_loc() {
266        let quotes_query = QuotesRequest {
267            symbols: vec!["BTC/USD".into()],
268            start: Some("2026-04-04T00:00:00Z".into()),
269            end: Some("2026-04-04T00:00:05Z".into()),
270            limit: Some(1),
271            sort: Some(Sort::Asc),
272            loc: Some(Loc::Us1),
273            page_token: Some("page-3".into()),
274        }
275        .to_query();
276        assert_eq!(
277            quotes_query,
278            vec![
279                ("symbols".to_string(), "BTC/USD".to_string()),
280                ("start".to_string(), "2026-04-04T00:00:00Z".to_string()),
281                ("end".to_string(), "2026-04-04T00:00:05Z".to_string()),
282                ("limit".to_string(), "1".to_string()),
283                ("page_token".to_string(), "page-3".to_string()),
284                ("sort".to_string(), "asc".to_string()),
285            ]
286        );
287
288        let trades_query = TradesRequest {
289            symbols: vec!["BTC/USD".into()],
290            start: Some("2026-04-04T00:01:00Z".into()),
291            end: Some("2026-04-04T00:01:03Z".into()),
292            limit: Some(1),
293            sort: Some(Sort::Desc),
294            loc: Some(Loc::Us),
295            page_token: Some("page-4".into()),
296        }
297        .to_query();
298        assert_eq!(
299            trades_query,
300            vec![
301                ("symbols".to_string(), "BTC/USD".to_string()),
302                ("start".to_string(), "2026-04-04T00:01:00Z".to_string()),
303                ("end".to_string(), "2026-04-04T00:01:03Z".to_string()),
304                ("limit".to_string(), "1".to_string()),
305                ("page_token".to_string(), "page-4".to_string()),
306                ("sort".to_string(), "desc".to_string()),
307            ]
308        );
309    }
310
311    #[test]
312    fn historical_requests_replace_page_token_through_shared_pagination_trait() {
313        let bars = BarsRequest {
314            page_token: Some("page-2".into()),
315            ..BarsRequest::default()
316        };
317        let quotes = QuotesRequest {
318            page_token: Some("page-3".into()),
319            ..QuotesRequest::default()
320        };
321        let trades = TradesRequest {
322            page_token: Some("page-4".into()),
323            ..TradesRequest::default()
324        };
325
326        assert_eq!(
327            bars.with_page_token(Some("page-9".into()))
328                .page_token
329                .as_deref(),
330            Some("page-9")
331        );
332        assert_eq!(
333            quotes
334                .with_page_token(Some("page-8".into()))
335                .page_token
336                .as_deref(),
337            Some("page-8")
338        );
339        assert_eq!(
340            trades
341                .with_page_token(Some("page-7".into()))
342                .page_token
343                .as_deref(),
344            Some("page-7")
345        );
346    }
347
348    #[test]
349    fn latest_requests_serialize_symbols_only_without_loc() {
350        let bars_query = LatestBarsRequest {
351            symbols: vec!["BTC/USD".into(), "ETH/USD".into()],
352            loc: Some(Loc::Us1),
353        }
354        .to_query();
355        assert_eq!(
356            bars_query,
357            vec![("symbols".to_string(), "BTC/USD,ETH/USD".to_string())]
358        );
359
360        let quotes_query = LatestQuotesRequest {
361            symbols: vec!["BTC/USD".into()],
362            loc: Some(Loc::Eu1),
363        }
364        .to_query();
365        assert_eq!(
366            quotes_query,
367            vec![("symbols".to_string(), "BTC/USD".to_string())]
368        );
369
370        let trades_query = LatestTradesRequest {
371            symbols: vec!["BTC/USD".into()],
372            loc: Some(Loc::Us),
373        }
374        .to_query();
375        assert_eq!(
376            trades_query,
377            vec![("symbols".to_string(), "BTC/USD".to_string())]
378        );
379
380        let orderbooks_query = LatestOrderbooksRequest {
381            symbols: vec!["BTC/USD".into()],
382            loc: Some(Loc::Us1),
383        }
384        .to_query();
385        assert_eq!(
386            orderbooks_query,
387            vec![("symbols".to_string(), "BTC/USD".to_string())]
388        );
389    }
390
391    #[test]
392    fn snapshots_request_serializes_symbols_only_without_loc() {
393        let query = SnapshotsRequest {
394            symbols: vec!["BTC/USD".into(), "ETH/USD".into()],
395            loc: Some(Loc::Eu1),
396        }
397        .to_query();
398
399        assert_eq!(
400            query,
401            vec![("symbols".to_string(), "BTC/USD,ETH/USD".to_string())]
402        );
403    }
404
405    #[test]
406    fn requests_reject_empty_symbols_for_required_symbol_endpoints() {
407        let errors = [
408            BarsRequest::default()
409                .validate()
410                .expect_err("bars symbols must be required"),
411            QuotesRequest::default()
412                .validate()
413                .expect_err("quotes symbols must be required"),
414            TradesRequest::default()
415                .validate()
416                .expect_err("trades symbols must be required"),
417            LatestBarsRequest::default()
418                .validate()
419                .expect_err("latest bars symbols must be required"),
420            LatestQuotesRequest::default()
421                .validate()
422                .expect_err("latest quotes symbols must be required"),
423            LatestTradesRequest::default()
424                .validate()
425                .expect_err("latest trades symbols must be required"),
426            LatestOrderbooksRequest::default()
427                .validate()
428                .expect_err("latest orderbooks symbols must be required"),
429            SnapshotsRequest::default()
430                .validate()
431                .expect_err("snapshots symbols must be required"),
432        ];
433
434        for error in errors {
435            assert!(matches!(
436                error,
437                Error::InvalidRequest(message)
438                    if message.contains("symbols") && message.contains("empty")
439            ));
440        }
441    }
442
443    #[test]
444    fn historical_requests_reject_limits_outside_documented_range() {
445        let errors = [
446            BarsRequest {
447                symbols: vec!["BTC/USD".into()],
448                limit: Some(0),
449                ..BarsRequest::default()
450            }
451            .validate()
452            .expect_err("bars limit below one must fail"),
453            QuotesRequest {
454                symbols: vec!["BTC/USD".into()],
455                limit: Some(10_001),
456                ..QuotesRequest::default()
457            }
458            .validate()
459            .expect_err("quotes limit above ten thousand must fail"),
460            TradesRequest {
461                symbols: vec!["BTC/USD".into()],
462                limit: Some(0),
463                ..TradesRequest::default()
464            }
465            .validate()
466            .expect_err("trades limit below one must fail"),
467        ];
468
469        for error in errors {
470            assert!(matches!(
471                error,
472                Error::InvalidRequest(message)
473                    if message.contains("limit") && message.contains("10000")
474            ));
475        }
476    }
477}