use chrono::DateTime;
use chrono::Utc;
use num_decimal::Num;
use serde::Deserialize;
use serde::Serialize;
use serde_urlencoded::to_string as to_query;
use crate::data::v2::Feed;
use crate::data::DATA_BASE_URL;
use crate::util::vec_from_str;
use crate::Str;
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct ListReq {
#[serde(skip)]
pub symbol: String,
#[serde(rename = "limit")]
pub limit: Option<usize>,
#[serde(rename = "start")]
pub start: DateTime<Utc>,
#[serde(rename = "end")]
pub end: DateTime<Utc>,
#[serde(rename = "feed")]
pub feed: Option<Feed>,
#[serde(rename = "page_token", skip_serializing_if = "Option::is_none")]
pub page_token: Option<String>,
#[doc(hidden)]
#[serde(skip)]
pub _non_exhaustive: (),
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ListReqInit {
pub limit: Option<usize>,
pub feed: Option<Feed>,
pub page_token: Option<String>,
#[doc(hidden)]
pub _non_exhaustive: (),
}
impl ListReqInit {
#[inline]
pub fn init<S>(self, symbol: S, start: DateTime<Utc>, end: DateTime<Utc>) -> ListReq
where
S: Into<String>,
{
ListReq {
symbol: symbol.into(),
start,
end,
limit: self.limit,
feed: self.feed,
page_token: self.page_token,
_non_exhaustive: (),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct Trade {
#[serde(rename = "t")]
pub timestamp: DateTime<Utc>,
#[serde(rename = "p")]
pub price: Num,
#[serde(rename = "s")]
pub size: usize,
#[doc(hidden)]
#[serde(skip)]
pub _non_exhaustive: (),
}
#[derive(Debug, Deserialize, Eq, PartialEq)]
pub struct Trades {
#[serde(rename = "trades", deserialize_with = "vec_from_str")]
pub trades: Vec<Trade>,
#[serde(rename = "symbol")]
pub symbol: String,
#[serde(rename = "next_page_token")]
pub next_page_token: Option<String>,
#[doc(hidden)]
#[serde(skip)]
pub _non_exhaustive: (),
}
Endpoint! {
pub List(ListReq),
Ok => Trades, [
OK,
],
Err => ListError, [
BAD_REQUEST => InvalidInput,
]
fn base_url() -> Option<Str> {
Some(DATA_BASE_URL.into())
}
fn path(input: &Self::Input) -> Str {
format!("/v2/stocks/{}/trades", input.symbol).into()
}
fn query(input: &Self::Input) -> Result<Option<Str>, Self::ConversionError> {
Ok(Some(to_query(input)?.into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr as _;
use http_endpoint::Endpoint;
use serde_json::from_str as from_json;
use test_log::test;
use crate::api_info::ApiInfo;
use crate::Client;
use crate::RequestError;
#[test]
fn parse_reference_trades() {
let response = r#"{
"trades": [
{
"t": "2021-02-06T13:04:56.334320128Z",
"x": "C",
"p": 387.62,
"s": 100,
"c": [
" ",
"T"
],
"i": 52983525029461,
"z": "B"
},
{
"t": "2021-02-06T13:09:42.325484032Z",
"x": "C",
"p": 387.69,
"s": 100,
"c": [
" ",
"T"
],
"i": 52983525033813,
"z": "B"
}
],
"symbol": "SPY",
"next_page_token": "MjAyMS0wMi0wNlQxMzowOTo0Mlo7MQ=="
}"#;
let res = from_json::<<List as Endpoint>::Output>(response).unwrap();
let trades = res.trades;
let expected_time = "2021-02-06T13:04:56";
assert_eq!(trades.len(), 2);
let timestamp = trades[0].timestamp.to_rfc3339();
assert!(timestamp.starts_with(expected_time), "{timestamp}");
assert_eq!(trades[0].price, Num::new(38762, 100));
assert_eq!(trades[0].size, 100);
assert_eq!(res.symbol, "SPY".to_string());
assert!(res.next_page_token.is_some())
}
#[test(tokio::test)]
async fn no_trades() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let start = DateTime::from_str("2021-11-05T00:00:00Z").unwrap();
let end = DateTime::from_str("2021-11-05T00:00:00Z").unwrap();
let request = ListReqInit::default().init("AAPL", start, end);
let res = client.issue::<List>(&request).await.unwrap();
assert_eq!(res.trades, Vec::new())
}
#[test(tokio::test)]
async fn request_trades() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let start = DateTime::from_str("2018-12-03T21:47:00Z").unwrap();
let end = DateTime::from_str("2018-12-06T21:47:00Z").unwrap();
let request = ListReqInit {
limit: Some(2),
..Default::default()
}
.init("AAPL", start, end);
let res = client.issue::<List>(&request).await.unwrap();
let trades = res.trades;
let expected_time = "2018-12-03T21:47:01";
assert_eq!(trades.len(), 2);
let timestamp = trades[0].timestamp.to_rfc3339();
assert!(timestamp.starts_with(expected_time), "{timestamp}");
assert_eq!(trades[0].price, Num::new(4608, 25));
assert_eq!(trades[0].size, 6);
assert_eq!(res.symbol, "AAPL".to_string());
assert!(res.next_page_token.is_some())
}
#[test(tokio::test)]
async fn can_follow_pagination() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let start = DateTime::from_str("2020-12-03T21:47:00Z").unwrap();
let end = DateTime::from_str("2020-12-07T21:47:00Z").unwrap();
let mut request = ListReqInit {
limit: Some(2),
..Default::default()
}
.init("AAPL", start, end);
let mut res = client.issue::<List>(&request).await.unwrap();
let trades = res.trades;
assert_eq!(trades.len(), 2);
request.page_token = res.next_page_token;
res = client.issue::<List>(&request).await.unwrap();
let new_trades = res.trades;
assert_eq!(new_trades.len(), 2);
assert!(new_trades[0].timestamp > trades[1].timestamp);
assert!(res.next_page_token.is_some())
}
#[test(tokio::test)]
async fn sip_feed() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let start = DateTime::from_str("2018-12-03T21:47:00Z").unwrap();
let end = DateTime::from_str("2018-12-06T21:47:00Z").unwrap();
let request = ListReqInit {
limit: Some(2),
..Default::default()
}
.init("AAPL", start, end);
let result = client.issue::<List>(&request).await;
match result {
Ok(_) | Err(RequestError::Endpoint(ListError::NotPermitted(_))) => (),
err => panic!("Received unexpected error: {err:?}"),
}
}
#[test(tokio::test)]
async fn invalid_page_token() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let start = DateTime::from_str("2018-12-03T21:47:00Z").unwrap();
let end = DateTime::from_str("2018-12-07T21:47:00Z").unwrap();
let request = ListReqInit {
page_token: Some("123456789abcdefghi".to_string()),
..Default::default()
}
.init("SPY", start, end);
let err = client.issue::<List>(&request).await.unwrap_err();
match err {
RequestError::Endpoint(ListError::InvalidInput(_)) => (),
_ => panic!("Received unexpected error: {err:?}"),
};
}
#[test(tokio::test)]
async fn invalid_symbol() {
let api_info = ApiInfo::from_env().unwrap();
let client = Client::new(api_info);
let start = DateTime::from_str("2022-02-01T00:00:00Z").unwrap();
let end = DateTime::from_str("2022-02-20T00:00:00Z").unwrap();
let request = ListReqInit::default().init("ABC123", start, end);
let err = client.issue::<List>(&request).await.unwrap_err();
match err {
RequestError::Endpoint(ListError::InvalidInput(Ok(_))) => (),
_ => panic!("Received unexpected error: {err:?}"),
};
}
}