use chrono::{DateTime, NaiveDate, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::data::enums::{
Adjustment, CorporateActionsType, DataFeed, MarketType, MostActivesBy, OptionsFeed,
};
use crate::data::timeframe::TimeFrame;
use crate::types::serde_util::comma_separated;
use crate::types::{ContractType, Sort, SupportedCurrencies};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Symbols(Vec<String>);
impl Symbols {
#[must_use]
pub fn as_slice(&self) -> &[String] {
&self.0
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl std::fmt::Display for Symbols {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0.join(","))
}
}
impl From<&str> for Symbols {
fn from(symbol: &str) -> Self {
Self(vec![symbol.to_owned()])
}
}
impl From<String> for Symbols {
fn from(symbol: String) -> Self {
Self(vec![symbol])
}
}
impl From<Vec<String>> for Symbols {
fn from(symbols: Vec<String>) -> Self {
Self(symbols)
}
}
impl From<Vec<&str>> for Symbols {
fn from(symbols: Vec<&str>) -> Self {
Self(symbols.into_iter().map(ToOwned::to_owned).collect())
}
}
impl<const N: usize> From<[&str; N]> for Symbols {
fn from(symbols: [&str; N]) -> Self {
Self(symbols.iter().map(|s| (*s).to_owned()).collect())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TimeseriesRequest {
#[serde(rename = "symbols")]
pub symbol_or_symbols: Symbols,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub currency: Option<SupportedCurrencies>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sort: Option<Sort>,
}
impl TimeseriesRequest {
#[must_use]
pub fn new(symbols: impl Into<Symbols>) -> Self {
Self {
symbol_or_symbols: symbols.into(),
start: None,
end: None,
limit: None,
currency: None,
sort: None,
}
}
#[must_use]
pub fn start(mut self, start: DateTime<Utc>) -> Self {
self.start = Some(start);
self
}
#[must_use]
pub fn end(mut self, end: DateTime<Utc>) -> Self {
self.end = Some(end);
self
}
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
#[must_use]
pub fn sort(mut self, sort: Sort) -> Self {
self.sort = Some(sort);
self
}
#[must_use]
pub fn currency(mut self, currency: SupportedCurrencies) -> Self {
self.currency = Some(currency);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StockBarsRequest {
#[serde(flatten)]
pub base: TimeseriesRequest,
pub timeframe: TimeFrame,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub adjustment: Option<Adjustment>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub feed: Option<DataFeed>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asof: Option<String>,
}
impl StockBarsRequest {
#[must_use]
pub fn new(symbols: impl Into<Symbols>, timeframe: TimeFrame) -> Self {
Self {
base: TimeseriesRequest::new(symbols),
timeframe,
adjustment: None,
feed: None,
asof: None,
}
}
#[must_use]
pub fn adjustment(mut self, adjustment: Adjustment) -> Self {
self.adjustment = Some(adjustment);
self
}
#[must_use]
pub fn feed(mut self, feed: DataFeed) -> Self {
self.feed = Some(feed);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CryptoBarsRequest {
#[serde(flatten)]
pub base: TimeseriesRequest,
pub timeframe: TimeFrame,
}
impl CryptoBarsRequest {
pub fn new(symbols: impl Into<Symbols>, timeframe: TimeFrame) -> Self {
Self {
base: TimeseriesRequest::new(symbols),
timeframe,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OptionBarsRequest {
#[serde(flatten)]
pub base: TimeseriesRequest,
pub timeframe: TimeFrame,
}
impl OptionBarsRequest {
pub fn new(symbols: impl Into<Symbols>, timeframe: TimeFrame) -> Self {
Self {
base: TimeseriesRequest::new(symbols),
timeframe,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StockTimeseriesRequest {
#[serde(flatten)]
pub base: TimeseriesRequest,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub feed: Option<DataFeed>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asof: Option<String>,
}
impl StockTimeseriesRequest {
pub fn new(symbols: impl Into<Symbols>) -> Self {
Self {
base: TimeseriesRequest::new(symbols),
feed: None,
asof: None,
}
}
#[must_use]
pub fn feed(mut self, feed: DataFeed) -> Self {
self.feed = Some(feed);
self
}
}
macro_rules! timeseries_delegates {
($ty:ty) => {
impl $ty {
#[must_use]
pub fn start(mut self, start: DateTime<Utc>) -> Self {
self.base = self.base.start(start);
self
}
#[must_use]
pub fn end(mut self, end: DateTime<Utc>) -> Self {
self.base = self.base.end(end);
self
}
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.base = self.base.limit(limit);
self
}
#[must_use]
pub fn sort(mut self, sort: Sort) -> Self {
self.base = self.base.sort(sort);
self
}
#[must_use]
pub fn currency(mut self, currency: SupportedCurrencies) -> Self {
self.base = self.base.currency(currency);
self
}
}
};
}
timeseries_delegates!(StockBarsRequest);
timeseries_delegates!(CryptoBarsRequest);
timeseries_delegates!(OptionBarsRequest);
timeseries_delegates!(StockTimeseriesRequest);
timeseries_delegates!(StockAuctionsRequest);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StockLatestRequest {
#[serde(rename = "symbols")]
pub symbol_or_symbols: Symbols,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub feed: Option<DataFeed>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub currency: Option<SupportedCurrencies>,
}
impl StockLatestRequest {
#[must_use]
pub fn new(symbols: impl Into<Symbols>) -> Self {
Self {
symbol_or_symbols: symbols.into(),
feed: None,
currency: None,
}
}
#[must_use]
pub fn feed(mut self, feed: DataFeed) -> Self {
self.feed = Some(feed);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CryptoLatestRequest {
#[serde(rename = "symbols")]
pub symbol_or_symbols: Symbols,
}
impl CryptoLatestRequest {
pub fn new(symbols: impl Into<Symbols>) -> Self {
Self {
symbol_or_symbols: symbols.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OptionLatestRequest {
#[serde(rename = "symbols")]
pub symbol_or_symbols: Symbols,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub feed: Option<OptionsFeed>,
}
impl OptionLatestRequest {
pub fn new(symbols: impl Into<Symbols>) -> Self {
Self {
symbol_or_symbols: symbols.into(),
feed: None,
}
}
#[must_use]
pub fn feed(mut self, feed: OptionsFeed) -> Self {
self.feed = Some(feed);
self
}
}
pub type StockSnapshotRequest = StockLatestRequest;
pub type CryptoSnapshotRequest = CryptoLatestRequest;
pub type OptionSnapshotRequest = OptionLatestRequest;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OptionChainRequest {
#[serde(skip)]
pub underlying_symbol: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub feed: Option<OptionsFeed>,
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
pub contract_type: Option<ContractType>,
#[serde(
default,
with = "crate::types::option_decimal",
skip_serializing_if = "Option::is_none"
)]
pub strike_price_gte: Option<Decimal>,
#[serde(
default,
with = "crate::types::option_decimal",
skip_serializing_if = "Option::is_none"
)]
pub strike_price_lte: Option<Decimal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expiration_date: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expiration_date_gte: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expiration_date_lte: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root_symbol: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub updated_since: Option<DateTime<Utc>>,
}
impl OptionChainRequest {
pub fn new(underlying_symbol: impl Into<String>) -> Self {
Self {
underlying_symbol: underlying_symbol.into(),
feed: None,
contract_type: None,
strike_price_gte: None,
strike_price_lte: None,
expiration_date: None,
expiration_date_gte: None,
expiration_date_lte: None,
root_symbol: None,
updated_since: None,
}
}
#[must_use]
pub fn contract_type(mut self, contract_type: ContractType) -> Self {
self.contract_type = Some(contract_type);
self
}
#[must_use]
pub fn feed(mut self, feed: OptionsFeed) -> Self {
self.feed = Some(feed);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StockAuctionsRequest {
#[serde(flatten)]
pub base: TimeseriesRequest,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub feed: Option<DataFeed>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asof: Option<String>,
}
impl StockAuctionsRequest {
pub fn new(symbols: impl Into<Symbols>) -> Self {
Self {
base: TimeseriesRequest::new(symbols),
feed: None,
asof: None,
}
}
#[must_use]
pub fn feed(mut self, feed: DataFeed) -> Self {
self.feed = Some(feed);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SingleSymbolRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeframe: Option<TimeFrame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub adjustment: Option<Adjustment>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub feed: Option<DataFeed>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asof: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub currency: Option<SupportedCurrencies>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sort: Option<Sort>,
}
impl SingleSymbolRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn start(mut self, start: DateTime<Utc>) -> Self {
self.start = Some(start);
self
}
#[must_use]
pub fn end(mut self, end: DateTime<Utc>) -> Self {
self.end = Some(end);
self
}
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
#[must_use]
pub fn timeframe(mut self, timeframe: TimeFrame) -> Self {
self.timeframe = Some(timeframe);
self
}
#[must_use]
pub fn adjustment(mut self, adjustment: Adjustment) -> Self {
self.adjustment = Some(adjustment);
self
}
#[must_use]
pub fn feed(mut self, feed: DataFeed) -> Self {
self.feed = Some(feed);
self
}
#[must_use]
pub fn sort(mut self, sort: Sort) -> Self {
self.sort = Some(sort);
self
}
#[must_use]
pub fn currency(mut self, currency: SupportedCurrencies) -> Self {
self.currency = Some(currency);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ForexRatesRequest {
#[serde(rename = "currency_pairs")]
pub currency_pairs: Symbols,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeframe: Option<TimeFrame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sort: Option<Sort>,
}
impl ForexRatesRequest {
pub fn new(currency_pairs: impl Into<Symbols>) -> Self {
Self {
currency_pairs: currency_pairs.into(),
timeframe: None,
start: None,
end: None,
limit: None,
sort: None,
}
}
#[must_use]
pub fn timeframe(mut self, timeframe: TimeFrame) -> Self {
self.timeframe = Some(timeframe);
self
}
#[must_use]
pub fn between(mut self, start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
self.start = Some(start);
self.end = Some(end);
self
}
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ForexLatestRatesRequest {
#[serde(rename = "currency_pairs")]
pub currency_pairs: Symbols,
}
impl ForexLatestRatesRequest {
pub fn new(currency_pairs: impl Into<Symbols>) -> Self {
Self {
currency_pairs: currency_pairs.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MostActivesRequest {
pub top: u32,
pub by: MostActivesBy,
}
impl MostActivesRequest {
#[must_use]
pub fn new(top: u32, by: MostActivesBy) -> Self {
Self { top, by }
}
}
impl Default for MostActivesRequest {
fn default() -> Self {
Self {
top: 10,
by: MostActivesBy::Volume,
}
}
}
fn default_market_type() -> MarketType {
MarketType::Stocks
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MarketMoversRequest {
pub top: u32,
#[serde(skip_serializing, default = "default_market_type")]
pub market_type: MarketType,
}
impl MarketMoversRequest {
#[must_use]
pub fn new(top: u32, market_type: MarketType) -> Self {
Self { top, market_type }
}
}
impl Default for MarketMoversRequest {
fn default() -> Self {
Self {
top: 10,
market_type: MarketType::Stocks,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NewsRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sort: Option<Sort>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "comma_separated"
)]
pub symbols: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub include_content: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exclude_contentless: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_token: Option<String>,
}
impl NewsRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn symbols(mut self, symbols: Vec<String>) -> Self {
self.symbols = Some(symbols);
self
}
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CorporateActionsRequest {
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "comma_separated"
)]
pub symbols: Option<Vec<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "comma_separated"
)]
pub cusips: Option<Vec<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "comma_separated"
)]
pub types: Option<Vec<CorporateActionsType>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end: Option<NaiveDate>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "comma_separated"
)]
pub ids: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sort: Option<Sort>,
}
impl CorporateActionsRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn symbols(mut self, symbols: Vec<String>) -> Self {
self.symbols = Some(symbols);
self
}
#[must_use]
pub fn types(mut self, types: Vec<CorporateActionsType>) -> Self {
self.types = Some(types);
self
}
#[must_use]
pub fn between(mut self, start: NaiveDate, end: NaiveDate) -> Self {
self.start = Some(start);
self.end = Some(end);
self
}
}
impl Default for CorporateActionsRequest {
fn default() -> Self {
Self {
symbols: None,
cusips: None,
types: None,
start: None,
end: None,
ids: None,
limit: None,
sort: Some(Sort::Asc),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::timeframe::TimeFrameUnit;
#[test]
fn symbols_render_as_one_comma_separated_parameter() {
let request = TimeseriesRequest::new(["AAPL", "SPY"]);
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["symbols"], serde_json::json!(["AAPL", "SPY"]));
assert_eq!(request.symbol_or_symbols.to_string(), "AAPL,SPY");
}
#[test]
fn a_single_symbol_is_accepted_without_a_vec() {
let request = TimeseriesRequest::new("AAPL");
assert_eq!(request.symbol_or_symbols.as_slice(), ["AAPL"]);
}
#[test]
fn unset_filters_are_omitted() {
let json = serde_json::to_value(TimeseriesRequest::new("AAPL")).unwrap();
let object = json.as_object().unwrap();
assert_eq!(object.len(), 1, "{object:?}");
assert!(object.contains_key("symbols"));
}
#[test]
fn bars_flatten_the_shared_filters_alongside_the_timeframe() {
let request =
StockBarsRequest::new("AAPL", TimeFrame::new(5, TimeFrameUnit::Minute).unwrap())
.feed(DataFeed::Iex);
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["symbols"], serde_json::json!(["AAPL"]));
assert_eq!(json["timeframe"], "5Min");
assert_eq!(json["feed"], "iex");
}
#[test]
fn option_chain_keeps_the_underlying_out_of_the_query() {
let request = OptionChainRequest::new("AAPL").contract_type(ContractType::Call);
let json = serde_json::to_value(&request).unwrap();
assert!(json.get("underlying_symbol").is_none());
assert_eq!(json["type"], "call");
}
#[test]
fn market_movers_keeps_the_market_type_out_of_the_query() {
let request = MarketMoversRequest::new(5, MarketType::Crypto);
let json = serde_json::to_value(&request).unwrap();
assert!(json.get("market_type").is_none());
assert_eq!(json["top"], 5);
}
#[test]
fn corporate_actions_default_to_walking_every_page_ascending() {
let request = CorporateActionsRequest::new();
assert_eq!(request.limit, None);
assert_eq!(request.sort, Some(Sort::Asc));
}
#[test]
fn most_actives_defaults_to_ten_by_volume() {
let request = MostActivesRequest::default();
assert_eq!(request.top, 10);
assert_eq!(request.by, MostActivesBy::Volume);
}
}