Skip to main content

alpaca_data/options/
request.rs

1use rust_decimal::Decimal;
2
3use alpaca_core::{QueryWriter, pagination::PaginatedRequest};
4
5use crate::Error;
6
7use super::{ContractType, OptionsFeed, Sort, TickType, TimeFrame};
8
9#[derive(Clone, Debug, Default)]
10pub struct BarsRequest {
11    pub symbols: Vec<String>,
12    pub timeframe: TimeFrame,
13    pub start: Option<String>,
14    pub end: Option<String>,
15    pub limit: Option<u32>,
16    pub sort: Option<Sort>,
17    pub page_token: Option<String>,
18}
19
20#[derive(Clone, Debug, Default)]
21pub struct TradesRequest {
22    pub symbols: Vec<String>,
23    pub start: Option<String>,
24    pub end: Option<String>,
25    pub limit: Option<u32>,
26    pub sort: Option<Sort>,
27    pub page_token: Option<String>,
28}
29
30#[derive(Clone, Debug, Default)]
31pub struct LatestQuotesRequest {
32    pub symbols: Vec<String>,
33    pub feed: Option<OptionsFeed>,
34}
35
36#[derive(Clone, Debug, Default)]
37pub struct LatestTradesRequest {
38    pub symbols: Vec<String>,
39    pub feed: Option<OptionsFeed>,
40}
41
42#[derive(Clone, Debug, Default)]
43pub struct SnapshotsRequest {
44    pub symbols: Vec<String>,
45    pub feed: Option<OptionsFeed>,
46    pub limit: Option<u32>,
47    pub page_token: Option<String>,
48}
49
50#[derive(Clone, Debug, Default)]
51pub struct ChainRequest {
52    pub underlying_symbol: String,
53    pub feed: Option<OptionsFeed>,
54    pub r#type: Option<ContractType>,
55    pub strike_price_gte: Option<Decimal>,
56    pub strike_price_lte: Option<Decimal>,
57    pub expiration_date: Option<String>,
58    pub expiration_date_gte: Option<String>,
59    pub expiration_date_lte: Option<String>,
60    pub root_symbol: Option<String>,
61    pub updated_since: Option<String>,
62    pub limit: Option<u32>,
63    pub page_token: Option<String>,
64}
65
66#[derive(Clone, Debug, Default)]
67pub struct ConditionCodesRequest {
68    pub ticktype: TickType,
69}
70
71impl BarsRequest {
72    pub(crate) fn validate(&self) -> Result<(), Error> {
73        validate_option_symbols(&self.symbols)?;
74        validate_limit(self.limit, 1, 10_000)
75    }
76
77    pub(crate) fn into_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("sort", self.sort);
85        query.push_opt("page_token", self.page_token);
86        query.finish()
87    }
88}
89
90impl TradesRequest {
91    pub(crate) fn validate(&self) -> Result<(), Error> {
92        validate_option_symbols(&self.symbols)?;
93        validate_limit(self.limit, 1, 10_000)
94    }
95
96    pub(crate) fn into_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("sort", self.sort);
103        query.push_opt("page_token", self.page_token);
104        query.finish()
105    }
106}
107
108impl LatestQuotesRequest {
109    pub(crate) fn validate(&self) -> Result<(), Error> {
110        validate_option_symbols(&self.symbols)
111    }
112
113    pub(crate) fn into_query(self) -> Vec<(String, String)> {
114        latest_query(self.symbols, self.feed)
115    }
116}
117
118impl LatestTradesRequest {
119    pub(crate) fn validate(&self) -> Result<(), Error> {
120        validate_option_symbols(&self.symbols)
121    }
122
123    pub(crate) fn into_query(self) -> Vec<(String, String)> {
124        latest_query(self.symbols, self.feed)
125    }
126}
127
128impl SnapshotsRequest {
129    pub(crate) fn validate(&self) -> Result<(), Error> {
130        validate_option_symbols(&self.symbols)?;
131        validate_limit(self.limit, 1, 1_000)
132    }
133
134    pub(crate) fn into_query(self) -> Vec<(String, String)> {
135        let mut query = QueryWriter::default();
136        query.push_csv("symbols", self.symbols);
137        query.push_opt("feed", self.feed);
138        query.push_opt("limit", self.limit);
139        query.push_opt("page_token", self.page_token);
140        query.finish()
141    }
142}
143
144impl ChainRequest {
145    pub(crate) fn validate(&self) -> Result<(), Error> {
146        validate_required_symbol(&self.underlying_symbol, "underlying_symbol")?;
147        validate_limit(self.limit, 1, 1_000)
148    }
149
150    pub(crate) fn into_query(self) -> Vec<(String, String)> {
151        let mut query = QueryWriter::default();
152        query.push_opt("feed", self.feed);
153        query.push_opt("type", self.r#type);
154        query.push_opt("strike_price_gte", self.strike_price_gte);
155        query.push_opt("strike_price_lte", self.strike_price_lte);
156        query.push_opt("expiration_date", self.expiration_date);
157        query.push_opt("expiration_date_gte", self.expiration_date_gte);
158        query.push_opt("expiration_date_lte", self.expiration_date_lte);
159        query.push_opt("root_symbol", self.root_symbol);
160        query.push_opt("updated_since", self.updated_since);
161        query.push_opt("limit", self.limit);
162        query.push_opt("page_token", self.page_token);
163        query.finish()
164    }
165}
166
167impl PaginatedRequest for BarsRequest {
168    fn with_page_token(&self, page_token: Option<String>) -> Self {
169        let mut next = self.clone();
170        next.page_token = page_token;
171        next
172    }
173}
174
175impl PaginatedRequest for TradesRequest {
176    fn with_page_token(&self, page_token: Option<String>) -> Self {
177        let mut next = self.clone();
178        next.page_token = page_token;
179        next
180    }
181}
182
183impl PaginatedRequest for SnapshotsRequest {
184    fn with_page_token(&self, page_token: Option<String>) -> Self {
185        let mut next = self.clone();
186        next.page_token = page_token;
187        next
188    }
189}
190
191impl PaginatedRequest for ChainRequest {
192    fn with_page_token(&self, page_token: Option<String>) -> Self {
193        let mut next = self.clone();
194        next.page_token = page_token;
195        next
196    }
197}
198
199fn latest_query(symbols: Vec<String>, feed: Option<OptionsFeed>) -> Vec<(String, String)> {
200    let mut query = QueryWriter::default();
201    query.push_csv("symbols", symbols);
202    query.push_opt("feed", feed);
203    query.finish()
204}
205
206fn validate_required_symbol(symbol: &str, field_name: &str) -> Result<(), Error> {
207    if symbol.trim().is_empty() {
208        return Err(Error::InvalidRequest(format!(
209            "{field_name} is invalid: must not be empty or whitespace-only"
210        )));
211    }
212
213    Ok(())
214}
215
216fn validate_option_symbols(symbols: &[String]) -> Result<(), Error> {
217    if symbols.is_empty() {
218        return Err(Error::InvalidRequest(
219            "symbols are invalid: must not be empty".to_owned(),
220        ));
221    }
222
223    if symbols.len() > 100 {
224        return Err(Error::InvalidRequest(
225            "symbols must contain at most 100 contract symbols".to_owned(),
226        ));
227    }
228
229    if symbols.iter().any(|symbol| symbol.trim().is_empty()) {
230        return Err(Error::InvalidRequest(
231            "symbols are invalid: must not contain empty or whitespace-only entries".to_owned(),
232        ));
233    }
234
235    Ok(())
236}
237
238fn validate_limit(limit: Option<u32>, min: u32, max: u32) -> Result<(), Error> {
239    if let Some(limit) = limit
240        && !(min..=max).contains(&limit)
241    {
242        return Err(Error::InvalidRequest(format!(
243            "limit must be between {min} and {max}"
244        )));
245    }
246
247    Ok(())
248}