1use chrono::DateTime;
5use chrono::Utc;
6
7use num_decimal::Num;
8
9use serde::Deserialize;
10use serde::Serialize;
11use serde_urlencoded::to_string as to_query;
12
13use crate::data::v2::Feed;
14use crate::data::DATA_BASE_URL;
15use crate::util::vec_from_str;
16use crate::Str;
17
18
19#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
21pub struct ListReq {
22 #[serde(skip)]
24 pub symbol: String,
25 #[serde(rename = "limit")]
30 pub limit: Option<usize>,
31 #[serde(rename = "start")]
33 pub start: DateTime<Utc>,
34 #[serde(rename = "end")]
36 pub end: DateTime<Utc>,
37 #[serde(rename = "feed")]
42 pub feed: Option<Feed>,
43 #[serde(rename = "page_token", skip_serializing_if = "Option::is_none")]
45 pub page_token: Option<String>,
46 #[doc(hidden)]
48 #[serde(skip)]
49 pub _non_exhaustive: (),
50}
51
52
53#[derive(Clone, Debug, Default, Eq, PartialEq)]
55pub struct ListReqInit {
56 pub limit: Option<usize>,
58 pub feed: Option<Feed>,
60 pub page_token: Option<String>,
62 #[doc(hidden)]
64 pub _non_exhaustive: (),
65}
66
67impl ListReqInit {
68 #[inline]
70 pub fn init<S>(self, symbol: S, start: DateTime<Utc>, end: DateTime<Utc>) -> ListReq
71 where
72 S: Into<String>,
73 {
74 ListReq {
75 symbol: symbol.into(),
76 start,
77 end,
78 limit: self.limit,
79 feed: self.feed,
80 page_token: self.page_token,
81 _non_exhaustive: (),
82 }
83 }
84}
85
86
87#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
89pub struct Trade {
90 #[serde(rename = "t")]
92 pub timestamp: DateTime<Utc>,
93 #[serde(rename = "p")]
95 pub price: Num,
96 #[serde(rename = "s")]
98 pub size: usize,
99 #[doc(hidden)]
101 #[serde(skip)]
102 pub _non_exhaustive: (),
103}
104
105
106#[derive(Debug, Deserialize, Eq, PartialEq)]
108pub struct Trades {
109 #[serde(rename = "trades", deserialize_with = "vec_from_str")]
111 pub trades: Vec<Trade>,
112 #[serde(rename = "symbol")]
114 pub symbol: String,
115 #[serde(rename = "next_page_token")]
117 pub next_page_token: Option<String>,
118 #[doc(hidden)]
120 #[serde(skip)]
121 pub _non_exhaustive: (),
122}
123
124
125Endpoint! {
126 pub List(ListReq),
128 Ok => Trades, [
129 OK,
131 ],
132 Err => ListError, [
133 BAD_REQUEST => InvalidInput,
135 ]
136
137 fn base_url() -> Option<Str> {
138 Some(DATA_BASE_URL.into())
139 }
140
141 fn path(input: &Self::Input) -> Str {
142 format!("/v2/stocks/{}/trades", input.symbol).into()
143 }
144
145 fn query(input: &Self::Input) -> Result<Option<Str>, Self::ConversionError> {
146 Ok(Some(to_query(input)?.into()))
147 }
148}
149
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 use std::str::FromStr as _;
156
157 use http_endpoint::Endpoint;
158
159 use serde_json::from_str as from_json;
160
161 use test_log::test;
162
163 use crate::api_info::ApiInfo;
164 use crate::Client;
165 use crate::RequestError;
166
167
168 #[test]
170 fn parse_reference_trades() {
171 let response = r#"{
172 "trades": [
173 {
174 "t": "2021-02-06T13:04:56.334320128Z",
175 "x": "C",
176 "p": 387.62,
177 "s": 100,
178 "c": [
179 " ",
180 "T"
181 ],
182 "i": 52983525029461,
183 "z": "B"
184 },
185 {
186 "t": "2021-02-06T13:09:42.325484032Z",
187 "x": "C",
188 "p": 387.69,
189 "s": 100,
190 "c": [
191 " ",
192 "T"
193 ],
194 "i": 52983525033813,
195 "z": "B"
196 }
197 ],
198 "symbol": "SPY",
199 "next_page_token": "MjAyMS0wMi0wNlQxMzowOTo0Mlo7MQ=="
200}"#;
201
202 let res = from_json::<<List as Endpoint>::Output>(response).unwrap();
203 let trades = res.trades;
204 let expected_time = "2021-02-06T13:04:56";
205 assert_eq!(trades.len(), 2);
206 let timestamp = trades[0].timestamp.to_rfc3339();
207 assert!(timestamp.starts_with(expected_time), "{timestamp}");
208 assert_eq!(trades[0].price, Num::new(38762, 100));
209 assert_eq!(trades[0].size, 100);
210 assert_eq!(res.symbol, "SPY".to_string());
211 assert!(res.next_page_token.is_some())
212 }
213
214 #[test(tokio::test)]
216 async fn no_trades() {
217 let api_info = ApiInfo::from_env().unwrap();
218 let client = Client::new(api_info);
219 let start = DateTime::from_str("2021-11-05T00:00:00Z").unwrap();
220 let end = DateTime::from_str("2021-11-05T00:00:00Z").unwrap();
221 let request = ListReqInit::default().init("AAPL", start, end);
222
223 let res = client.issue::<List>(&request).await.unwrap();
224 assert_eq!(res.trades, Vec::new())
225 }
226
227 #[test(tokio::test)]
229 async fn request_trades() {
230 let api_info = ApiInfo::from_env().unwrap();
231 let client = Client::new(api_info);
232 let start = DateTime::from_str("2018-12-03T21:47:00Z").unwrap();
233 let end = DateTime::from_str("2018-12-06T21:47:00Z").unwrap();
234 let request = ListReqInit {
235 limit: Some(2),
236 ..Default::default()
237 }
238 .init("AAPL", start, end);
239
240 let res = client.issue::<List>(&request).await.unwrap();
241 let trades = res.trades;
242
243 let expected_time = "2018-12-03T21:47:01";
244 assert_eq!(trades.len(), 2);
245 let timestamp = trades[0].timestamp.to_rfc3339();
246 assert!(timestamp.starts_with(expected_time), "{timestamp}");
247 assert_eq!(trades[0].price, Num::new(4608, 25));
248 assert_eq!(trades[0].size, 6);
249 assert_eq!(res.symbol, "AAPL".to_string());
250 assert!(res.next_page_token.is_some())
251 }
252
253 #[test(tokio::test)]
255 async fn can_follow_pagination() {
256 let api_info = ApiInfo::from_env().unwrap();
257 let client = Client::new(api_info);
258 let start = DateTime::from_str("2020-12-03T21:47:00Z").unwrap();
259 let end = DateTime::from_str("2020-12-07T21:47:00Z").unwrap();
260 let mut request = ListReqInit {
261 limit: Some(2),
262 ..Default::default()
263 }
264 .init("AAPL", start, end);
265
266 let mut res = client.issue::<List>(&request).await.unwrap();
267 let trades = res.trades;
268
269 assert_eq!(trades.len(), 2);
270 request.page_token = res.next_page_token;
271
272 res = client.issue::<List>(&request).await.unwrap();
273 let new_trades = res.trades;
274
275 assert_eq!(new_trades.len(), 2);
276 assert!(new_trades[0].timestamp > trades[1].timestamp);
277 assert!(res.next_page_token.is_some())
278 }
279
280 #[test(tokio::test)]
282 async fn sip_feed() {
283 let api_info = ApiInfo::from_env().unwrap();
284 let client = Client::new(api_info);
285 let start = DateTime::from_str("2018-12-03T21:47:00Z").unwrap();
286 let end = DateTime::from_str("2018-12-06T21:47:00Z").unwrap();
287 let request = ListReqInit {
288 limit: Some(2),
289 ..Default::default()
290 }
291 .init("AAPL", start, end);
292
293 let result = client.issue::<List>(&request).await;
294 match result {
298 Ok(_) | Err(RequestError::Endpoint(ListError::NotPermitted(_))) => (),
299 err => panic!("Received unexpected error: {err:?}"),
300 }
301 }
302
303 #[test(tokio::test)]
306 async fn invalid_page_token() {
307 let api_info = ApiInfo::from_env().unwrap();
308 let client = Client::new(api_info);
309
310 let start = DateTime::from_str("2018-12-03T21:47:00Z").unwrap();
311 let end = DateTime::from_str("2018-12-07T21:47:00Z").unwrap();
312 let request = ListReqInit {
313 page_token: Some("123456789abcdefghi".to_string()),
314 ..Default::default()
315 }
316 .init("SPY", start, end);
317
318 let err = client.issue::<List>(&request).await.unwrap_err();
319 match err {
320 RequestError::Endpoint(ListError::InvalidInput(_)) => (),
321 _ => panic!("Received unexpected error: {err:?}"),
322 };
323 }
324
325 #[test(tokio::test)]
328 async fn invalid_symbol() {
329 let api_info = ApiInfo::from_env().unwrap();
330 let client = Client::new(api_info);
331
332 let start = DateTime::from_str("2022-02-01T00:00:00Z").unwrap();
333 let end = DateTime::from_str("2022-02-20T00:00:00Z").unwrap();
334 let request = ListReqInit::default().init("ABC123", start, end);
335
336 let err = client.issue::<List>(&request).await.unwrap_err();
337 match err {
338 RequestError::Endpoint(ListError::InvalidInput(Ok(_))) => (),
339 _ => panic!("Received unexpected error: {err:?}"),
340 };
341 }
342}