diff --git a/Cargo.lock b/Cargo.lock index b59538c..4500d25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1482,6 +1482,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "number_prefix" version = "0.4.0" @@ -2394,7 +2403,9 @@ checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", diff --git a/Cargo.toml b/Cargo.toml index bd4700a..130f8c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,9 +46,11 @@ uuid = { version = "1.6.1", features = [ ] } time = { version = "0.3.31", features = [ "serde", + "serde-well-known", + "serde-human-readable", "formatting", "macros", - "serde-well-known", + "local-offset", ] } backoff = { version = "0.4.0", features = [ "tokio", diff --git a/src/types/alpaca/api/incoming/calendar.rs b/src/types/alpaca/api/incoming/calendar.rs new file mode 100644 index 0000000..da7c119 --- /dev/null +++ b/src/types/alpaca/api/incoming/calendar.rs @@ -0,0 +1,55 @@ +use crate::{config::ALPACA_API_URL, types::alpaca::api::outgoing, utils::de}; +use backoff::{future::retry_notify, ExponentialBackoff}; +use governor::DefaultDirectRateLimiter; +use log::warn; +use reqwest::{Client, Error}; +use serde::Deserialize; +use std::time::Duration; +use time::{Date, Time}; + +#[derive(Deserialize)] +pub struct Calendar { + pub date: Date, + #[serde(deserialize_with = "de::human_time_hh_mm")] + pub open: Time, + #[serde(deserialize_with = "de::human_time_hh_mm")] + pub close: Time, + pub settlement_date: Date, +} + +pub async fn get( + alpaca_client: &Client, + alpaca_rate_limiter: &DefaultDirectRateLimiter, + query: &outgoing::calendar::Calendar, + backoff: Option, +) -> Result, Error> { + retry_notify( + backoff.unwrap_or_default(), + || async { + alpaca_rate_limiter.until_ready().await; + alpaca_client + .get(&format!("{}/calendar", *ALPACA_API_URL)) + .query(query) + .send() + .await? + .error_for_status() + .map_err(|e| match e.status() { + Some(reqwest::StatusCode::BAD_REQUEST | reqwest::StatusCode::FORBIDDEN) => { + backoff::Error::Permanent(e) + } + _ => e.into(), + })? + .json::>() + .await + .map_err(backoff::Error::Permanent) + }, + |e, duration: Duration| { + warn!( + "Failed to get calendar, will retry in {} seconds: {}", + duration.as_secs(), + e + ); + }, + ) + .await +} diff --git a/src/types/alpaca/api/incoming/mod.rs b/src/types/alpaca/api/incoming/mod.rs index 6bac836..89c9883 100644 --- a/src/types/alpaca/api/incoming/mod.rs +++ b/src/types/alpaca/api/incoming/mod.rs @@ -1,6 +1,7 @@ pub mod account; pub mod asset; pub mod bar; +pub mod calendar; pub mod clock; pub mod news; pub mod order; diff --git a/src/types/alpaca/api/outgoing/calendar.rs b/src/types/alpaca/api/outgoing/calendar.rs new file mode 100644 index 0000000..9ffef0b --- /dev/null +++ b/src/types/alpaca/api/outgoing/calendar.rs @@ -0,0 +1,20 @@ +use serde::Serialize; +use time::OffsetDateTime; + +#[derive(Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[allow(dead_code)] +pub enum DateType { + Trading, + Settlement, +} + +#[derive(Serialize)] +pub struct Calendar { + #[serde(with = "time::serde::rfc3339")] + pub start: OffsetDateTime, + #[serde(with = "time::serde::rfc3339")] + pub end: OffsetDateTime, + #[serde(rename = "date")] + pub date_type: DateType, +} diff --git a/src/types/alpaca/api/outgoing/mod.rs b/src/types/alpaca/api/outgoing/mod.rs index 7c65f0a..f67cfaf 100644 --- a/src/types/alpaca/api/outgoing/mod.rs +++ b/src/types/alpaca/api/outgoing/mod.rs @@ -1,3 +1,4 @@ pub mod bar; +pub mod calendar; pub mod news; pub mod order; diff --git a/src/utils/de.rs b/src/utils/de.rs index b82244d..779d1af 100644 --- a/src/utils/de.rs +++ b/src/utils/de.rs @@ -5,9 +5,11 @@ use serde::{ Deserializer, }; use std::fmt; +use time::{format_description::OwnedFormatItem, macros::format_description, Time}; lazy_static! { static ref RE_SLASH: Regex = Regex::new(r"^(.+)(BTC|USD.?)$").unwrap(); + static ref FMT_HH_MM: OwnedFormatItem = format_description!("[hour]:[minute]").into(); } fn add_slash(pair: &str) -> String { @@ -75,3 +77,27 @@ where deserializer.deserialize_seq(VecStringVisitor) } + +pub fn human_time_hh_mm<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + struct TimeVisitor; + + impl<'de> Visitor<'de> for TimeVisitor { + type Value = time::Time; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string in the format HH:MM") + } + + fn visit_str(self, time: &str) -> Result + where + E: de::Error, + { + Time::parse(time, &FMT_HH_MM).map_err(|e| de::Error::custom(e.to_string())) + } + } + + deserializer.deserialize_str(TimeVisitor) +}