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}