alpaca_data/stocks/
request.rs

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