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, Copy, Debug, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub enum TimeFrame {
#[serde(rename = "1Min")]
OneMinute,
#[serde(rename = "1Hour")]
OneHour,
#[serde(rename = "1Day")]
OneDay,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub enum Adjustment {
#[serde(rename = "raw")]
Raw,
#[serde(rename = "split")]
Split,
#[serde(rename = "dividend")]
Dividend,
#[serde(rename = "all")]
All,
}
#[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 = "timeframe")]
pub timeframe: TimeFrame,
#[serde(rename = "adjustment")]
pub adjustment: Option<Adjustment>,
#[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 adjustment: Option<Adjustment>,
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>,
timeframe: TimeFrame,
) -> ListReq
where
S: Into<String>,
{
ListReq {
symbol: symbol.into(),
start,
end,
timeframe,
limit: self.limit,
adjustment: self.adjustment,
feed: self.feed,
page_token: self.page_token,
_non_exhaustive: (),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct Bar {
#[serde(rename = "t")]
pub time: DateTime<Utc>,
#[serde(rename = "o")]
pub open: Num,
#[serde(rename = "c")]
pub close: Num,
#[serde(rename = "h")]
pub high: Num,
#[serde(rename = "l")]
pub low: Num,
#[serde(rename = "v")]
pub volume: usize,
#[serde(rename = "vw")]
pub weighted_average: Num,
#[doc(hidden)]
#[serde(skip)]
pub _non_exhaustive: (),
}
#[derive(Debug, Deserialize, Eq, PartialEq)]
pub struct Bars {
#[serde(rename = "bars", deserialize_with = "vec_from_str")]
pub bars: Vec<Bar>,
#[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 => Bars, [
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/{}/bars", 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::ops::RangeInclusive;
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;
#[track_caller]
fn assert_in(value: &Num, range: RangeInclusive<u64>) {
assert!(
range.contains(&value.to_u64().unwrap()),
"{value} {range:?}"
)
}
#[test]
fn parse_reference_bars() {
let response = r#"{
"bars": [
{
"t": "2021-02-01T16:01:00Z",
"o": 133.32,
"h": 133.74,
"l": 133.31,
"c": 133.5,
"v": 9876,
"vw": 133.4
},
{
"t": "2021-02-01T16:02:00Z",
"o": 133.5,
"h": 133.58,
"l": 133.44,
"c": 133.58,
"v": 3567,
"vw": 133.6
}
],
"symbol": "AAPL",
"next_page_token": "MjAyMS0wMi0wMVQxNDowMjowMFo7MQ=="
}"#;
let res = from_json::<<List as Endpoint>::Output>(response).unwrap();
let bars = res.bars;
let expected_time = DateTime::<Utc>::from_str("2021-02-01T16:01:00Z").unwrap();
assert_eq!(bars.len(), 2);
assert_eq!(bars[0].time, expected_time);
assert_eq!(bars[0].open, Num::new(13332, 100));
assert_eq!(bars[0].close, Num::new(1335, 10));
assert_eq!(bars[0].high, Num::new(13374, 100));
assert_eq!(bars[0].low, Num::new(13331, 100));
assert_eq!(bars[0].weighted_average, Num::new(1334, 10));
assert_eq!(res.symbol, "AAPL".to_string());
assert!(res.next_page_token.is_some())
}
#[test(tokio::test)]
async fn no_bars() {
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, TimeFrame::OneDay);
let res = client.issue::<List>(&request).await.unwrap();
assert_eq!(res.bars, Vec::new())
}
#[test(tokio::test)]
async fn request_bars() {
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, TimeFrame::OneDay);
let res = client.issue::<List>(&request).await.unwrap();
let bars = res.bars;
assert_eq!(bars.len(), 2);
assert_eq!(
bars[0].time,
DateTime::<Utc>::from_str("2018-12-04T05:00:00Z").unwrap()
);
assert_in(&bars[0].open, 179..=182);
assert_in(&bars[0].close, 175..=177);
assert_in(&bars[0].high, 180..=184);
assert_in(&bars[0].low, 174..=178);
assert_in(&bars[0].weighted_average, 174..=179);
assert_eq!(
bars[1].time,
DateTime::<Utc>::from_str("2018-12-06T05:00:00Z").unwrap()
);
assert_in(&bars[1].open, 169..=173);
assert_in(&bars[1].close, 172..=176);
assert_in(&bars[1].high, 172..=176);
assert_in(&bars[1].low, 168..=172);
assert_in(&bars[1].weighted_average, 167..=173);
}
#[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("2018-12-03T21:47:00Z").unwrap();
let end = DateTime::from_str("2018-12-07T21:47:00Z").unwrap();
let mut request = ListReqInit {
limit: Some(2),
..Default::default()
}
.init("AAPL", start, end, TimeFrame::OneDay);
let mut res = client.issue::<List>(&request).await.unwrap();
let bars = res.bars;
assert_eq!(bars.len(), 2);
request.page_token = res.next_page_token;
res = client.issue::<List>(&request).await.unwrap();
let new_bars = res.bars;
assert_eq!(new_bars.len(), 1);
assert!(new_bars[0].time > bars[1].time);
assert!(res.next_page_token.is_none())
}
async fn request_with_adjustment(adjustment: Adjustment) -> Bars {
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-04T21:47:00Z").unwrap();
let request = ListReqInit {
adjustment: Some(adjustment),
..Default::default()
}
.init("AAPL", start, end, TimeFrame::OneDay);
client.issue::<List>(&request).await.unwrap()
}
#[test(tokio::test)]
async fn request_with_dividend_adjustment() {
let res = request_with_adjustment(Adjustment::Dividend);
let bars = res.await.bars;
assert_eq!(bars.len(), 1);
assert_eq!(
bars[0].time,
DateTime::<Utc>::from_str("2018-12-04T05:00:00Z").unwrap()
);
assert_in(&bars[0].open, 172..=176);
assert_in(&bars[0].close, 168..=172);
assert_in(&bars[0].high, 173..=177);
assert_in(&bars[0].low, 167..=171);
assert_in(&bars[0].weighted_average, 166..=170);
}
#[test(tokio::test)]
async fn request_with_split_adjustment() {
let res = request_with_adjustment(Adjustment::Split);
let bars = res.await.bars;
assert_eq!(bars.len(), 1);
assert_eq!(
bars[0].time,
DateTime::<Utc>::from_str("2018-12-04T05:00:00Z").unwrap()
);
assert_in(&bars[0].open, 42..=46);
assert_in(&bars[0].close, 41..=45);
assert_in(&bars[0].high, 43..=46);
assert_in(&bars[0].low, 41..=45);
assert_in(&bars[0].weighted_average, 41..=46);
}
#[test(tokio::test)]
async fn request_with_all_adjustment() {
let res = request_with_adjustment(Adjustment::All);
let bars = res.await.bars;
assert_eq!(bars.len(), 1);
assert_eq!(
bars[0].time,
DateTime::<Utc>::from_str("2018-12-04T05:00:00Z").unwrap()
);
assert_in(&bars[0].open, 42..=44);
assert_in(&bars[0].close, 41..=43);
assert_in(&bars[0].high, 42..=45);
assert_in(&bars[0].low, 41..=43);
assert_in(&bars[0].weighted_average, 41..=45);
}
#[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-07T21:47:00Z").unwrap();
let request = ListReqInit {
limit: Some(2),
feed: Some(Feed::SIP),
..Default::default()
}
.init("AAPL", start, end, TimeFrame::OneDay);
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, TimeFrame::OneMinute);
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, TimeFrame::OneDay);
let err = client.issue::<List>(&request).await.unwrap_err();
match err {
RequestError::Endpoint(ListError::InvalidInput(Ok(_))) => (),
_ => panic!("Received unexpected error: {err:?}"),
};
}
}