Skip to main content

alpaca_data/options/
request.rs

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