Skip to main content

alpaca_data/stocks/
request.rs

1use crate::Error;
2use crate::common::query::QueryWriter;
3use crate::transport::pagination::PaginatedRequest;
4
5use super::{Adjustment, AuctionFeed, Currency, DataFeed, Sort, Tape, TickType, 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 adjustment: Option<Adjustment>,
15    pub feed: Option<DataFeed>,
16    pub sort: Option<Sort>,
17    pub asof: Option<String>,
18    pub currency: Option<Currency>,
19    pub page_token: Option<String>,
20}
21
22#[derive(Clone, Debug, Default)]
23pub struct BarsSingleRequest {
24    pub symbol: String,
25    pub timeframe: TimeFrame,
26    pub start: Option<String>,
27    pub end: Option<String>,
28    pub limit: Option<u32>,
29    pub adjustment: Option<Adjustment>,
30    pub feed: Option<DataFeed>,
31    pub sort: Option<Sort>,
32    pub asof: Option<String>,
33    pub currency: Option<Currency>,
34    pub page_token: Option<String>,
35}
36
37#[derive(Clone, Debug, Default)]
38pub struct AuctionsRequest {
39    pub symbols: Vec<String>,
40    pub start: Option<String>,
41    pub end: Option<String>,
42    pub limit: Option<u32>,
43    pub asof: Option<String>,
44    pub feed: Option<AuctionFeed>,
45    pub currency: Option<Currency>,
46    pub page_token: Option<String>,
47    pub sort: Option<Sort>,
48}
49
50#[derive(Clone, Debug, Default)]
51pub struct AuctionsSingleRequest {
52    pub symbol: String,
53    pub start: Option<String>,
54    pub end: Option<String>,
55    pub limit: Option<u32>,
56    pub asof: Option<String>,
57    pub feed: Option<AuctionFeed>,
58    pub currency: Option<Currency>,
59    pub page_token: Option<String>,
60    pub sort: Option<Sort>,
61}
62
63#[derive(Clone, Debug, Default)]
64pub struct QuotesRequest {
65    pub symbols: Vec<String>,
66    pub start: Option<String>,
67    pub end: Option<String>,
68    pub limit: Option<u32>,
69    pub feed: Option<DataFeed>,
70    pub sort: Option<Sort>,
71    pub asof: Option<String>,
72    pub currency: Option<Currency>,
73    pub page_token: Option<String>,
74}
75
76#[derive(Clone, Debug, Default)]
77pub struct QuotesSingleRequest {
78    pub symbol: String,
79    pub start: Option<String>,
80    pub end: Option<String>,
81    pub limit: Option<u32>,
82    pub feed: Option<DataFeed>,
83    pub sort: Option<Sort>,
84    pub asof: Option<String>,
85    pub currency: Option<Currency>,
86    pub page_token: Option<String>,
87}
88
89#[derive(Clone, Debug, Default)]
90pub struct TradesRequest {
91    pub symbols: Vec<String>,
92    pub start: Option<String>,
93    pub end: Option<String>,
94    pub limit: Option<u32>,
95    pub feed: Option<DataFeed>,
96    pub sort: Option<Sort>,
97    pub asof: Option<String>,
98    pub currency: Option<Currency>,
99    pub page_token: Option<String>,
100}
101
102#[derive(Clone, Debug, Default)]
103pub struct TradesSingleRequest {
104    pub symbol: String,
105    pub start: Option<String>,
106    pub end: Option<String>,
107    pub limit: Option<u32>,
108    pub feed: Option<DataFeed>,
109    pub sort: Option<Sort>,
110    pub asof: Option<String>,
111    pub currency: Option<Currency>,
112    pub page_token: Option<String>,
113}
114
115#[derive(Clone, Debug, Default)]
116pub struct LatestBarsRequest {
117    pub symbols: Vec<String>,
118    pub feed: Option<DataFeed>,
119    pub currency: Option<Currency>,
120}
121
122#[derive(Clone, Debug, Default)]
123pub struct LatestBarRequest {
124    pub symbol: String,
125    pub feed: Option<DataFeed>,
126    pub currency: Option<Currency>,
127}
128
129#[derive(Clone, Debug, Default)]
130pub struct LatestQuotesRequest {
131    pub symbols: Vec<String>,
132    pub feed: Option<DataFeed>,
133    pub currency: Option<Currency>,
134}
135
136#[derive(Clone, Debug, Default)]
137pub struct LatestQuoteRequest {
138    pub symbol: String,
139    pub feed: Option<DataFeed>,
140    pub currency: Option<Currency>,
141}
142
143#[derive(Clone, Debug, Default)]
144pub struct LatestTradesRequest {
145    pub symbols: Vec<String>,
146    pub feed: Option<DataFeed>,
147    pub currency: Option<Currency>,
148}
149
150#[derive(Clone, Debug, Default)]
151pub struct LatestTradeRequest {
152    pub symbol: String,
153    pub feed: Option<DataFeed>,
154    pub currency: Option<Currency>,
155}
156
157#[derive(Clone, Debug, Default)]
158pub struct SnapshotsRequest {
159    pub symbols: Vec<String>,
160    pub feed: Option<DataFeed>,
161    pub currency: Option<Currency>,
162}
163
164#[derive(Clone, Debug, Default)]
165pub struct SnapshotRequest {
166    pub symbol: String,
167    pub feed: Option<DataFeed>,
168    pub currency: Option<Currency>,
169}
170
171#[derive(Clone, Debug, Default)]
172pub struct ConditionCodesRequest {
173    pub ticktype: TickType,
174    pub tape: Tape,
175}
176
177impl BarsRequest {
178    pub(crate) fn validate(&self) -> Result<(), Error> {
179        validate_required_symbols(&self.symbols)?;
180        validate_limit(self.limit, 1, 10_000)
181    }
182
183    pub(crate) fn to_query(self) -> Vec<(String, String)> {
184        let mut query = QueryWriter::default();
185        query.push_csv("symbols", self.symbols);
186        query.push_opt("timeframe", Some(self.timeframe));
187        query.push_opt("start", self.start);
188        query.push_opt("end", self.end);
189        query.push_opt("limit", self.limit);
190        query.push_opt("adjustment", self.adjustment);
191        query.push_opt("feed", self.feed);
192        query.push_opt("currency", self.currency);
193        query.push_opt("page_token", self.page_token);
194        query.push_opt("sort", self.sort);
195        query.push_opt("asof", self.asof);
196        query.finish()
197    }
198}
199
200impl BarsSingleRequest {
201    pub(crate) fn validate(&self) -> Result<(), Error> {
202        validate_limit(self.limit, 1, 10_000)
203    }
204
205    pub(crate) fn to_query(self) -> Vec<(String, String)> {
206        let mut query = QueryWriter::default();
207        query.push_opt("timeframe", Some(self.timeframe));
208        query.push_opt("start", self.start);
209        query.push_opt("end", self.end);
210        query.push_opt("limit", self.limit);
211        query.push_opt("adjustment", self.adjustment);
212        query.push_opt("feed", self.feed);
213        query.push_opt("currency", self.currency);
214        query.push_opt("page_token", self.page_token);
215        query.push_opt("sort", self.sort);
216        query.push_opt("asof", self.asof);
217        query.finish()
218    }
219}
220
221impl AuctionsRequest {
222    pub(crate) fn validate(&self) -> Result<(), Error> {
223        validate_required_symbols(&self.symbols)?;
224        validate_limit(self.limit, 1, 10_000)
225    }
226
227    pub(crate) fn to_query(self) -> Vec<(String, String)> {
228        let mut query = QueryWriter::default();
229        query.push_csv("symbols", self.symbols);
230        query.push_opt("start", self.start);
231        query.push_opt("end", self.end);
232        query.push_opt("limit", self.limit);
233        query.push_opt("feed", self.feed);
234        query.push_opt("currency", self.currency);
235        query.push_opt("page_token", self.page_token);
236        query.push_opt("sort", self.sort);
237        query.push_opt("asof", self.asof);
238        query.finish()
239    }
240}
241
242impl AuctionsSingleRequest {
243    pub(crate) fn validate(&self) -> Result<(), Error> {
244        validate_limit(self.limit, 1, 10_000)
245    }
246
247    pub(crate) fn to_query(self) -> Vec<(String, String)> {
248        let mut query = QueryWriter::default();
249        query.push_opt("start", self.start);
250        query.push_opt("end", self.end);
251        query.push_opt("limit", self.limit);
252        query.push_opt("feed", self.feed);
253        query.push_opt("currency", self.currency);
254        query.push_opt("page_token", self.page_token);
255        query.push_opt("sort", self.sort);
256        query.push_opt("asof", self.asof);
257        query.finish()
258    }
259}
260
261impl QuotesRequest {
262    pub(crate) fn validate(&self) -> Result<(), Error> {
263        validate_required_symbols(&self.symbols)?;
264        validate_limit(self.limit, 1, 10_000)
265    }
266
267    pub(crate) fn to_query(self) -> Vec<(String, String)> {
268        let mut query = QueryWriter::default();
269        query.push_csv("symbols", self.symbols);
270        query.push_opt("start", self.start);
271        query.push_opt("end", self.end);
272        query.push_opt("limit", self.limit);
273        query.push_opt("feed", self.feed);
274        query.push_opt("currency", self.currency);
275        query.push_opt("page_token", self.page_token);
276        query.push_opt("sort", self.sort);
277        query.push_opt("asof", self.asof);
278        query.finish()
279    }
280}
281
282impl QuotesSingleRequest {
283    pub(crate) fn validate(&self) -> Result<(), Error> {
284        validate_limit(self.limit, 1, 10_000)
285    }
286
287    pub(crate) fn to_query(self) -> Vec<(String, String)> {
288        let mut query = QueryWriter::default();
289        query.push_opt("start", self.start);
290        query.push_opt("end", self.end);
291        query.push_opt("limit", self.limit);
292        query.push_opt("feed", self.feed);
293        query.push_opt("currency", self.currency);
294        query.push_opt("page_token", self.page_token);
295        query.push_opt("sort", self.sort);
296        query.push_opt("asof", self.asof);
297        query.finish()
298    }
299}
300
301impl TradesRequest {
302    pub(crate) fn validate(&self) -> Result<(), Error> {
303        validate_required_symbols(&self.symbols)?;
304        validate_limit(self.limit, 1, 10_000)
305    }
306
307    pub(crate) fn to_query(self) -> Vec<(String, String)> {
308        let mut query = QueryWriter::default();
309        query.push_csv("symbols", self.symbols);
310        query.push_opt("start", self.start);
311        query.push_opt("end", self.end);
312        query.push_opt("limit", self.limit);
313        query.push_opt("feed", self.feed);
314        query.push_opt("currency", self.currency);
315        query.push_opt("page_token", self.page_token);
316        query.push_opt("sort", self.sort);
317        query.push_opt("asof", self.asof);
318        query.finish()
319    }
320}
321
322impl TradesSingleRequest {
323    pub(crate) fn validate(&self) -> Result<(), Error> {
324        validate_limit(self.limit, 1, 10_000)
325    }
326
327    pub(crate) fn to_query(self) -> Vec<(String, String)> {
328        let mut query = QueryWriter::default();
329        query.push_opt("start", self.start);
330        query.push_opt("end", self.end);
331        query.push_opt("limit", self.limit);
332        query.push_opt("feed", self.feed);
333        query.push_opt("currency", self.currency);
334        query.push_opt("page_token", self.page_token);
335        query.push_opt("sort", self.sort);
336        query.push_opt("asof", self.asof);
337        query.finish()
338    }
339}
340
341impl LatestBarsRequest {
342    pub(crate) fn validate(&self) -> Result<(), Error> {
343        validate_required_symbols(&self.symbols)
344    }
345
346    pub(crate) fn to_query(self) -> Vec<(String, String)> {
347        latest_batch_query(self.symbols, self.feed, self.currency)
348    }
349}
350
351impl LatestBarRequest {
352    pub(crate) fn to_query(self) -> Vec<(String, String)> {
353        latest_single_query(self.feed, self.currency)
354    }
355}
356
357impl LatestQuotesRequest {
358    pub(crate) fn validate(&self) -> Result<(), Error> {
359        validate_required_symbols(&self.symbols)
360    }
361
362    pub(crate) fn to_query(self) -> Vec<(String, String)> {
363        latest_batch_query(self.symbols, self.feed, self.currency)
364    }
365}
366
367impl LatestQuoteRequest {
368    pub(crate) fn to_query(self) -> Vec<(String, String)> {
369        latest_single_query(self.feed, self.currency)
370    }
371}
372
373impl LatestTradesRequest {
374    pub(crate) fn validate(&self) -> Result<(), Error> {
375        validate_required_symbols(&self.symbols)
376    }
377
378    pub(crate) fn to_query(self) -> Vec<(String, String)> {
379        latest_batch_query(self.symbols, self.feed, self.currency)
380    }
381}
382
383impl LatestTradeRequest {
384    pub(crate) fn to_query(self) -> Vec<(String, String)> {
385        latest_single_query(self.feed, self.currency)
386    }
387}
388
389impl SnapshotsRequest {
390    pub(crate) fn validate(&self) -> Result<(), Error> {
391        validate_required_symbols(&self.symbols)
392    }
393
394    pub(crate) fn to_query(self) -> Vec<(String, String)> {
395        latest_batch_query(self.symbols, self.feed, self.currency)
396    }
397}
398
399impl SnapshotRequest {
400    pub(crate) fn to_query(self) -> Vec<(String, String)> {
401        latest_single_query(self.feed, self.currency)
402    }
403}
404
405impl ConditionCodesRequest {
406    pub(crate) fn to_query(self) -> Vec<(String, String)> {
407        let mut query = QueryWriter::default();
408        query.push_opt("tape", Some(self.tape));
409        query.finish()
410    }
411}
412
413impl PaginatedRequest for BarsSingleRequest {
414    fn with_page_token(&self, page_token: Option<String>) -> Self {
415        let mut next = self.clone();
416        next.page_token = page_token;
417        next
418    }
419}
420
421impl PaginatedRequest for QuotesSingleRequest {
422    fn with_page_token(&self, page_token: Option<String>) -> Self {
423        let mut next = self.clone();
424        next.page_token = page_token;
425        next
426    }
427}
428
429impl PaginatedRequest for AuctionsSingleRequest {
430    fn with_page_token(&self, page_token: Option<String>) -> Self {
431        let mut next = self.clone();
432        next.page_token = page_token;
433        next
434    }
435}
436
437fn latest_batch_query(
438    symbols: Vec<String>,
439    feed: Option<DataFeed>,
440    currency: Option<Currency>,
441) -> Vec<(String, String)> {
442    let mut query = QueryWriter::default();
443    query.push_csv("symbols", symbols);
444    query.push_opt("feed", feed);
445    query.push_opt("currency", currency);
446    query.finish()
447}
448
449fn validate_required_symbols(symbols: &[String]) -> Result<(), Error> {
450    if symbols.is_empty() {
451        return Err(Error::InvalidRequest("symbols must not be empty".into()));
452    }
453
454    Ok(())
455}
456
457fn validate_limit(limit: Option<u32>, min: u32, max: u32) -> Result<(), Error> {
458    if let Some(limit) = limit {
459        if !(min..=max).contains(&limit) {
460            return Err(Error::InvalidRequest(format!(
461                "limit must be between {min} and {max}"
462            )));
463        }
464    }
465
466    Ok(())
467}
468
469fn latest_single_query(
470    feed: Option<DataFeed>,
471    currency: Option<Currency>,
472) -> Vec<(String, String)> {
473    let mut query = QueryWriter::default();
474    query.push_opt("feed", feed);
475    query.push_opt("currency", currency);
476    query.finish()
477}
478
479impl PaginatedRequest for TradesSingleRequest {
480    fn with_page_token(&self, page_token: Option<String>) -> Self {
481        let mut next = self.clone();
482        next.page_token = page_token;
483        next
484    }
485}
486
487impl PaginatedRequest for BarsRequest {
488    fn with_page_token(&self, page_token: Option<String>) -> Self {
489        let mut next = self.clone();
490        next.page_token = page_token;
491        next
492    }
493}
494
495impl PaginatedRequest for AuctionsRequest {
496    fn with_page_token(&self, page_token: Option<String>) -> Self {
497        let mut next = self.clone();
498        next.page_token = page_token;
499        next
500    }
501}
502
503impl PaginatedRequest for QuotesRequest {
504    fn with_page_token(&self, page_token: Option<String>) -> Self {
505        let mut next = self.clone();
506        next.page_token = page_token;
507        next
508    }
509}
510
511impl PaginatedRequest for TradesRequest {
512    fn with_page_token(&self, page_token: Option<String>) -> Self {
513        let mut next = self.clone();
514        next.page_token = page_token;
515        next
516    }
517}
518
519#[cfg(test)]
520mod tests {
521    use crate::Error;
522
523    use super::*;
524
525    #[test]
526    fn stocks_data_feed_serializes_to_official_strings() {
527        assert_eq!(DataFeed::DelayedSip.to_string(), "delayed_sip");
528        assert_eq!(DataFeed::Iex.to_string(), "iex");
529        assert_eq!(DataFeed::Otc.to_string(), "otc");
530        assert_eq!(DataFeed::Sip.to_string(), "sip");
531        assert_eq!(DataFeed::Boats.to_string(), "boats");
532        assert_eq!(DataFeed::Overnight.to_string(), "overnight");
533    }
534
535    #[test]
536    fn stocks_adjustment_serializes_to_official_strings() {
537        assert_eq!(Adjustment::raw().to_string(), "raw");
538        assert_eq!(Adjustment::split().to_string(), "split");
539        assert_eq!(Adjustment::dividend().to_string(), "dividend");
540        assert_eq!(Adjustment::spin_off().to_string(), "spin-off");
541        assert_eq!(Adjustment::all().to_string(), "all");
542        assert_eq!(
543            Adjustment::from("split,dividend").to_string(),
544            "split,dividend"
545        );
546    }
547
548    #[test]
549    fn stocks_timeframe_serializes_to_official_strings() {
550        assert_eq!(TimeFrame::from("1Min").to_string(), "1Min");
551        assert_eq!(TimeFrame::from("5Min").to_string(), "5Min");
552        assert_eq!(TimeFrame::from("1Day").to_string(), "1Day");
553        assert_eq!(TimeFrame::from("1Week").to_string(), "1Week");
554        assert_eq!(TimeFrame::from("3Month").to_string(), "3Month");
555    }
556
557    #[test]
558    fn bars_request_serializes_official_query_words() {
559        let request = BarsRequest {
560            symbols: vec!["AAPL".into(), "MSFT".into()],
561            timeframe: TimeFrame::from("1Day"),
562            start: Some("2024-03-01T00:00:00Z".into()),
563            end: Some("2024-03-05T00:00:00Z".into()),
564            limit: Some(50),
565            adjustment: Some(Adjustment::from("split,dividend")),
566            feed: Some(DataFeed::Boats),
567            sort: Some(Sort::Desc),
568            asof: Some("2024-03-04".into()),
569            currency: Some(Currency::from("USD")),
570            page_token: Some("page-123".into()),
571        };
572
573        assert_eq!(
574            request.to_query(),
575            vec![
576                ("symbols".to_string(), "AAPL,MSFT".to_string()),
577                ("timeframe".to_string(), "1Day".to_string()),
578                ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
579                ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
580                ("limit".to_string(), "50".to_string()),
581                ("adjustment".to_string(), "split,dividend".to_string()),
582                ("feed".to_string(), "boats".to_string()),
583                ("currency".to_string(), "USD".to_string()),
584                ("page_token".to_string(), "page-123".to_string()),
585                ("sort".to_string(), "desc".to_string()),
586                ("asof".to_string(), "2024-03-04".to_string()),
587            ]
588        );
589    }
590
591    #[test]
592    fn bars_single_request_serializes_official_query_words() {
593        let request = BarsSingleRequest {
594            symbol: "AAPL".into(),
595            timeframe: TimeFrame::from("1Day"),
596            start: Some("2024-03-01T00:00:00Z".into()),
597            end: Some("2024-03-05T00:00:00Z".into()),
598            limit: Some(50),
599            adjustment: Some(Adjustment::from("split,dividend")),
600            feed: Some(DataFeed::Boats),
601            sort: Some(Sort::Desc),
602            asof: Some("2024-03-04".into()),
603            currency: Some(Currency::from("USD")),
604            page_token: Some("page-123".into()),
605        };
606
607        assert_eq!(
608            request.to_query(),
609            vec![
610                ("timeframe".to_string(), "1Day".to_string()),
611                ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
612                ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
613                ("limit".to_string(), "50".to_string()),
614                ("adjustment".to_string(), "split,dividend".to_string()),
615                ("feed".to_string(), "boats".to_string()),
616                ("currency".to_string(), "USD".to_string()),
617                ("page_token".to_string(), "page-123".to_string()),
618                ("sort".to_string(), "desc".to_string()),
619                ("asof".to_string(), "2024-03-04".to_string()),
620            ]
621        );
622    }
623
624    #[test]
625    fn stocks_auction_feed_serializes_to_official_strings() {
626        assert_eq!(AuctionFeed::Sip.to_string(), "sip");
627    }
628
629    #[test]
630    fn auctions_request_serializes_official_query_words() {
631        let request = AuctionsRequest {
632            symbols: vec!["AAPL".into(), "MSFT".into()],
633            start: Some("2024-03-01T00:00:00Z".into()),
634            end: Some("2024-03-05T00:00:00Z".into()),
635            limit: Some(10),
636            asof: Some("2024-03-04".into()),
637            feed: Some(AuctionFeed::Sip),
638            currency: Some(Currency::from("USD")),
639            page_token: Some("page-auctions".into()),
640            sort: Some(Sort::Asc),
641        };
642
643        assert_eq!(
644            request.to_query(),
645            vec![
646                ("symbols".to_string(), "AAPL,MSFT".to_string()),
647                ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
648                ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
649                ("limit".to_string(), "10".to_string()),
650                ("feed".to_string(), "sip".to_string()),
651                ("currency".to_string(), "USD".to_string()),
652                ("page_token".to_string(), "page-auctions".to_string()),
653                ("sort".to_string(), "asc".to_string()),
654                ("asof".to_string(), "2024-03-04".to_string()),
655            ]
656        );
657    }
658
659    #[test]
660    fn auctions_single_request_serializes_official_query_words() {
661        let request = AuctionsSingleRequest {
662            symbol: "AAPL".into(),
663            start: Some("2024-03-01T00:00:00Z".into()),
664            end: Some("2024-03-05T00:00:00Z".into()),
665            limit: Some(10),
666            asof: Some("2024-03-04".into()),
667            feed: Some(AuctionFeed::Sip),
668            currency: Some(Currency::from("USD")),
669            page_token: Some("page-auctions-single".into()),
670            sort: Some(Sort::Desc),
671        };
672
673        assert_eq!(
674            request.to_query(),
675            vec![
676                ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
677                ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
678                ("limit".to_string(), "10".to_string()),
679                ("feed".to_string(), "sip".to_string()),
680                ("currency".to_string(), "USD".to_string()),
681                ("page_token".to_string(), "page-auctions-single".to_string()),
682                ("sort".to_string(), "desc".to_string()),
683                ("asof".to_string(), "2024-03-04".to_string()),
684            ]
685        );
686    }
687
688    #[test]
689    fn quotes_request_serializes_official_query_words() {
690        let request = QuotesRequest {
691            symbols: vec!["AAPL".into(), "MSFT".into()],
692            start: Some("2024-03-01T00:00:00Z".into()),
693            end: Some("2024-03-05T00:00:00Z".into()),
694            limit: Some(25),
695            feed: Some(DataFeed::Iex),
696            sort: Some(Sort::Asc),
697            asof: Some("2024-03-04".into()),
698            currency: Some(Currency::from("USD")),
699            page_token: Some("page-456".into()),
700        };
701
702        assert_eq!(
703            request.to_query(),
704            vec![
705                ("symbols".to_string(), "AAPL,MSFT".to_string()),
706                ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
707                ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
708                ("limit".to_string(), "25".to_string()),
709                ("feed".to_string(), "iex".to_string()),
710                ("currency".to_string(), "USD".to_string()),
711                ("page_token".to_string(), "page-456".to_string()),
712                ("sort".to_string(), "asc".to_string()),
713                ("asof".to_string(), "2024-03-04".to_string()),
714            ]
715        );
716    }
717
718    #[test]
719    fn quotes_single_request_serializes_official_query_words() {
720        let request = QuotesSingleRequest {
721            symbol: "AAPL".into(),
722            start: Some("2024-03-01T00:00:00Z".into()),
723            end: Some("2024-03-05T00:00:00Z".into()),
724            limit: Some(25),
725            feed: Some(DataFeed::Iex),
726            sort: Some(Sort::Asc),
727            asof: Some("2024-03-04".into()),
728            currency: Some(Currency::from("USD")),
729            page_token: Some("page-456".into()),
730        };
731
732        assert_eq!(
733            request.to_query(),
734            vec![
735                ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
736                ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
737                ("limit".to_string(), "25".to_string()),
738                ("feed".to_string(), "iex".to_string()),
739                ("currency".to_string(), "USD".to_string()),
740                ("page_token".to_string(), "page-456".to_string()),
741                ("sort".to_string(), "asc".to_string()),
742                ("asof".to_string(), "2024-03-04".to_string()),
743            ]
744        );
745    }
746
747    #[test]
748    fn trades_request_serializes_official_query_words() {
749        let request = TradesRequest {
750            symbols: vec!["AAPL".into(), "MSFT".into()],
751            start: Some("2024-03-01T00:00:00Z".into()),
752            end: Some("2024-03-05T00:00:00Z".into()),
753            limit: Some(10),
754            feed: Some(DataFeed::Sip),
755            sort: Some(Sort::Desc),
756            asof: Some("2024-03-04".into()),
757            currency: Some(Currency::from("USD")),
758            page_token: Some("page-789".into()),
759        };
760
761        assert_eq!(
762            request.to_query(),
763            vec![
764                ("symbols".to_string(), "AAPL,MSFT".to_string()),
765                ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
766                ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
767                ("limit".to_string(), "10".to_string()),
768                ("feed".to_string(), "sip".to_string()),
769                ("currency".to_string(), "USD".to_string()),
770                ("page_token".to_string(), "page-789".to_string()),
771                ("sort".to_string(), "desc".to_string()),
772                ("asof".to_string(), "2024-03-04".to_string()),
773            ]
774        );
775    }
776
777    #[test]
778    fn trades_single_request_serializes_official_query_words() {
779        let request = TradesSingleRequest {
780            symbol: "AAPL".into(),
781            start: Some("2024-03-01T00:00:00Z".into()),
782            end: Some("2024-03-05T00:00:00Z".into()),
783            limit: Some(10),
784            feed: Some(DataFeed::Sip),
785            sort: Some(Sort::Desc),
786            asof: Some("2024-03-04".into()),
787            currency: Some(Currency::from("USD")),
788            page_token: Some("page-789".into()),
789        };
790
791        assert_eq!(
792            request.to_query(),
793            vec![
794                ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
795                ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
796                ("limit".to_string(), "10".to_string()),
797                ("feed".to_string(), "sip".to_string()),
798                ("currency".to_string(), "USD".to_string()),
799                ("page_token".to_string(), "page-789".to_string()),
800                ("sort".to_string(), "desc".to_string()),
801                ("asof".to_string(), "2024-03-04".to_string()),
802            ]
803        );
804    }
805
806    #[test]
807    fn latest_batch_requests_serialize_official_query_words() {
808        let bars = LatestBarsRequest {
809            symbols: vec!["AAPL".into(), "MSFT".into()],
810            feed: Some(DataFeed::DelayedSip),
811            currency: Some(Currency::from("USD")),
812        };
813
814        assert_eq!(
815            bars.to_query(),
816            vec![
817                ("symbols".to_string(), "AAPL,MSFT".to_string()),
818                ("feed".to_string(), "delayed_sip".to_string()),
819                ("currency".to_string(), "USD".to_string()),
820            ]
821        );
822
823        let trades = LatestTradesRequest {
824            symbols: vec!["AAPL".into(), "MSFT".into()],
825            feed: Some(DataFeed::Iex),
826            currency: Some(Currency::from("USD")),
827        };
828
829        assert_eq!(
830            trades.to_query(),
831            vec![
832                ("symbols".to_string(), "AAPL,MSFT".to_string()),
833                ("feed".to_string(), "iex".to_string()),
834                ("currency".to_string(), "USD".to_string()),
835            ]
836        );
837    }
838
839    #[test]
840    fn latest_single_requests_serialize_official_query_words() {
841        let bar = LatestBarRequest {
842            symbol: "AAPL".into(),
843            feed: Some(DataFeed::Sip),
844            currency: Some(Currency::from("USD")),
845        };
846
847        assert_eq!(
848            bar.to_query(),
849            vec![
850                ("feed".to_string(), "sip".to_string()),
851                ("currency".to_string(), "USD".to_string()),
852            ]
853        );
854
855        let trade = LatestTradeRequest {
856            symbol: "AAPL".into(),
857            feed: Some(DataFeed::Boats),
858            currency: Some(Currency::from("USD")),
859        };
860
861        assert_eq!(
862            trade.to_query(),
863            vec![
864                ("feed".to_string(), "boats".to_string()),
865                ("currency".to_string(), "USD".to_string()),
866            ]
867        );
868    }
869
870    #[test]
871    fn snapshot_requests_serialize_official_query_words() {
872        let batch = SnapshotsRequest {
873            symbols: vec!["AAPL".into(), "MSFT".into()],
874            feed: Some(DataFeed::Overnight),
875            currency: Some(Currency::from("USD")),
876        };
877
878        assert_eq!(
879            batch.to_query(),
880            vec![
881                ("symbols".to_string(), "AAPL,MSFT".to_string()),
882                ("feed".to_string(), "overnight".to_string()),
883                ("currency".to_string(), "USD".to_string()),
884            ]
885        );
886
887        let single = SnapshotRequest {
888            symbol: "AAPL".into(),
889            feed: Some(DataFeed::Otc),
890            currency: Some(Currency::from("USD")),
891        };
892
893        assert_eq!(
894            single.to_query(),
895            vec![
896                ("feed".to_string(), "otc".to_string()),
897                ("currency".to_string(), "USD".to_string()),
898            ]
899        );
900    }
901
902    #[test]
903    fn stocks_ticktype_and_tape_serialize_to_official_strings() {
904        assert_eq!(TickType::Trade.as_str(), "trade");
905        assert_eq!(TickType::Quote.as_str(), "quote");
906        assert_eq!(Tape::A.as_str(), "A");
907        assert_eq!(Tape::B.as_str(), "B");
908        assert_eq!(Tape::C.as_str(), "C");
909    }
910
911    #[test]
912    fn metadata_request_serializes_official_query_words() {
913        let request = ConditionCodesRequest {
914            ticktype: TickType::Trade,
915            tape: Tape::A,
916        };
917
918        assert_eq!(
919            request.to_query(),
920            vec![("tape".to_string(), "A".to_string()),]
921        );
922    }
923
924    #[test]
925    fn batch_requests_reject_empty_symbols_for_required_symbol_endpoints() {
926        let errors = [
927            BarsRequest::default()
928                .validate()
929                .expect_err("bars symbols must be required"),
930            AuctionsRequest::default()
931                .validate()
932                .expect_err("auctions symbols must be required"),
933            QuotesRequest::default()
934                .validate()
935                .expect_err("quotes symbols must be required"),
936            TradesRequest::default()
937                .validate()
938                .expect_err("trades symbols must be required"),
939            LatestBarsRequest::default()
940                .validate()
941                .expect_err("latest bars symbols must be required"),
942            LatestQuotesRequest::default()
943                .validate()
944                .expect_err("latest quotes symbols must be required"),
945            LatestTradesRequest::default()
946                .validate()
947                .expect_err("latest trades symbols must be required"),
948            SnapshotsRequest::default()
949                .validate()
950                .expect_err("snapshots symbols must be required"),
951        ];
952
953        for error in errors {
954            assert!(matches!(
955                error,
956                Error::InvalidRequest(message)
957                    if message.contains("symbols") && message.contains("empty")
958            ));
959        }
960    }
961
962    #[test]
963    fn historical_requests_reject_limits_outside_documented_range() {
964        let errors = [
965            BarsRequest {
966                symbols: vec!["AAPL".into()],
967                limit: Some(0),
968                ..BarsRequest::default()
969            }
970            .validate()
971            .expect_err("bars limit below one must fail"),
972            BarsSingleRequest {
973                limit: Some(10_001),
974                ..BarsSingleRequest::default()
975            }
976            .validate()
977            .expect_err("single bars limit above ten thousand must fail"),
978            AuctionsRequest {
979                symbols: vec!["AAPL".into()],
980                limit: Some(0),
981                ..AuctionsRequest::default()
982            }
983            .validate()
984            .expect_err("auctions limit below one must fail"),
985            AuctionsSingleRequest {
986                limit: Some(10_001),
987                ..AuctionsSingleRequest::default()
988            }
989            .validate()
990            .expect_err("single auctions limit above ten thousand must fail"),
991            QuotesRequest {
992                symbols: vec!["AAPL".into()],
993                limit: Some(0),
994                ..QuotesRequest::default()
995            }
996            .validate()
997            .expect_err("quotes limit below one must fail"),
998            QuotesSingleRequest {
999                limit: Some(10_001),
1000                ..QuotesSingleRequest::default()
1001            }
1002            .validate()
1003            .expect_err("single quotes limit above ten thousand must fail"),
1004            TradesRequest {
1005                symbols: vec!["AAPL".into()],
1006                limit: Some(0),
1007                ..TradesRequest::default()
1008            }
1009            .validate()
1010            .expect_err("trades limit below one must fail"),
1011            TradesSingleRequest {
1012                limit: Some(10_001),
1013                ..TradesSingleRequest::default()
1014            }
1015            .validate()
1016            .expect_err("single trades limit above ten thousand must fail"),
1017        ];
1018
1019        for error in errors {
1020            assert!(matches!(
1021                error,
1022                Error::InvalidRequest(message)
1023                    if message.contains("limit") && message.contains("10000")
1024            ));
1025        }
1026    }
1027}