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}