use std::collections::HashMap;
use serde::Deserialize as _;
use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::auth::Credentials;
use crate::config::BaseUrl;
use crate::data::corporate_actions::CorporateActions;
use crate::data::enums::CryptoFeed;
use crate::data::events::CorporateActionEventsRequest;
use crate::data::meta::{Codes, Tape, TapeQuery, TickType};
use crate::data::models::{
AuctionSet, Bar, BarSet, DailyAuctions, ForexRate, ForexRateSet, MostActives, Movers, News,
NewsSet, OptionsSnapshot, Orderbook, Quote, QuoteSet, Snapshot, Trade, TradeSet, WithSymbol,
};
use crate::data::pagination::Merged;
use crate::data::pagination::{MarketDataRequest, get_marketdata};
use crate::data::requests::{
CorporateActionsRequest, CryptoBarsRequest, CryptoLatestRequest, CryptoSnapshotRequest,
ForexLatestRatesRequest, ForexRatesRequest, MarketMoversRequest, MostActivesRequest,
NewsRequest, OptionBarsRequest, OptionChainRequest, OptionLatestRequest, OptionSnapshotRequest,
SingleSymbolRequest, StockAuctionsRequest, StockBarsRequest, StockLatestRequest,
StockSnapshotRequest, StockTimeseriesRequest, TimeseriesRequest,
};
use crate::error::{Error, Result};
use crate::rest::{Empty, RestClient, RestConfig};
use crate::types::LogoRequest;
use crate::types::path::segment;
fn is_absent(value: &Value) -> bool {
value.is_null()
}
fn into_sets<T>(merged: Merged) -> Result<HashMap<String, Vec<T>>>
where
T: DeserializeOwned + WithSymbol,
{
let mut sets = HashMap::with_capacity(merged.data.len());
for (symbol, value) in merged.data {
if is_absent(&value) {
continue;
}
let mut records: Vec<T> =
Vec::<T>::deserialize(&value).map_err(|source| Error::Decode {
path: merged.path.clone(),
body: decode_body(&symbol, &value),
source,
})?;
for record in &mut records {
record.set_symbol(&symbol);
}
sets.insert(symbol, records);
}
Ok(sets)
}
fn decode_body(key: &str, value: &Value) -> String {
crate::rest::truncate(&format!("{key}: {value}"))
}
fn into_single<T>(mut merged: Merged, key: &str, symbol: &str) -> Result<Vec<T>>
where
T: DeserializeOwned + WithSymbol,
{
let records = merged.data.remove(key).unwrap_or(Value::Array(Vec::new()));
if is_absent(&records) {
return Ok(Vec::new());
}
let mut records: Vec<T> = Vec::<T>::deserialize(&records).map_err(|source| Error::Decode {
path: merged.path.clone(),
body: decode_body(key, &records),
source,
})?;
for record in &mut records {
record.set_symbol(symbol);
}
Ok(records)
}
fn into_latest<T>(merged: Merged) -> Result<HashMap<String, T>>
where
T: DeserializeOwned + WithSymbol,
{
let mut latest = HashMap::with_capacity(merged.data.len());
for (symbol, value) in merged.data {
if is_absent(&value) {
continue;
}
let mut record: T = T::deserialize(&value).map_err(|source| Error::Decode {
path: merged.path.clone(),
body: decode_body(&symbol, &value),
source,
})?;
record.set_symbol(&symbol);
latest.insert(symbol, record);
}
Ok(latest)
}
#[derive(Debug, Clone)]
pub struct StockHistoricalDataClient {
rest: RestClient,
}
impl StockHistoricalDataClient {
pub fn new(credentials: &Credentials) -> Result<Self> {
Self::with_config(
credentials,
RestConfig::from(BaseUrl::Data).api_version("v2"),
)
}
pub fn with_config(credentials: &Credentials, config: RestConfig) -> Result<Self> {
Ok(Self {
rest: RestClient::new(credentials, config)?,
})
}
#[must_use]
pub fn rest(&self) -> &RestClient {
&self.rest
}
pub async fn get_stock_bars(&self, request: &StockBarsRequest) -> Result<BarSet> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::paged("/stocks/bars"),
request,
)
.await?;
into_sets(merged)
}
pub async fn get_stock_quotes(&self, request: &StockTimeseriesRequest) -> Result<QuoteSet> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::paged("/stocks/quotes"),
request,
)
.await?;
into_sets(merged)
}
pub async fn get_stock_trades(&self, request: &StockTimeseriesRequest) -> Result<TradeSet> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::paged("/stocks/trades"),
request,
)
.await?;
into_sets(merged)
}
pub async fn get_stock_latest_trade(
&self,
request: &StockLatestRequest,
) -> Result<HashMap<String, Trade>> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::latest("/stocks/trades/latest"),
request,
)
.await?;
into_latest(merged)
}
pub async fn get_stock_latest_quote(
&self,
request: &StockLatestRequest,
) -> Result<HashMap<String, Quote>> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::latest("/stocks/quotes/latest"),
request,
)
.await?;
into_latest(merged)
}
pub async fn get_stock_latest_bar(
&self,
request: &StockLatestRequest,
) -> Result<HashMap<String, Bar>> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::latest("/stocks/bars/latest"),
request,
)
.await?;
into_latest(merged)
}
pub async fn get_stock_snapshot(
&self,
request: &StockSnapshotRequest,
) -> Result<HashMap<String, Snapshot>> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::latest("/stocks/snapshots").whole_body(),
request,
)
.await?;
into_latest(merged)
}
pub async fn get_stock_auctions(&self, request: &StockAuctionsRequest) -> Result<AuctionSet> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::paged("/stocks/auctions"),
request,
)
.await?;
into_sets(merged)
}
pub async fn get_stock_bars_for_symbol(
&self,
symbol: &str,
request: &SingleSymbolRequest,
) -> Result<Vec<Bar>> {
let path = format!("/stocks/{}/bars", segment(symbol)?);
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::paged(&path).whole_body(),
request,
)
.await?;
into_single(merged, "bars", symbol)
}
pub async fn get_stock_quotes_for_symbol(
&self,
symbol: &str,
request: &SingleSymbolRequest,
) -> Result<Vec<Quote>> {
let path = format!("/stocks/{}/quotes", segment(symbol)?);
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::paged(&path).whole_body(),
request,
)
.await?;
into_single(merged, "quotes", symbol)
}
pub async fn get_stock_trades_for_symbol(
&self,
symbol: &str,
request: &SingleSymbolRequest,
) -> Result<Vec<Trade>> {
let path = format!("/stocks/{}/trades", segment(symbol)?);
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::paged(&path).whole_body(),
request,
)
.await?;
into_single(merged, "trades", symbol)
}
pub async fn get_stock_auctions_for_symbol(
&self,
symbol: &str,
request: &SingleSymbolRequest,
) -> Result<Vec<DailyAuctions>> {
let path = format!("/stocks/{}/auctions", segment(symbol)?);
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::paged(&path).whole_body(),
request,
)
.await?;
into_single(merged, "auctions", symbol)
}
pub async fn get_stock_latest_bar_for_symbol(
&self,
symbol: &str,
request: &SingleSymbolRequest,
) -> Result<Bar> {
let path = format!("/stocks/{}/bars/latest", segment(symbol)?);
self.latest_for_symbol(&path, "bar", symbol, request).await
}
pub async fn get_stock_latest_quote_for_symbol(
&self,
symbol: &str,
request: &SingleSymbolRequest,
) -> Result<Quote> {
let path = format!("/stocks/{}/quotes/latest", segment(symbol)?);
self.latest_for_symbol(&path, "quote", symbol, request)
.await
}
pub async fn get_stock_latest_trade_for_symbol(
&self,
symbol: &str,
request: &SingleSymbolRequest,
) -> Result<Trade> {
let path = format!("/stocks/{}/trades/latest", segment(symbol)?);
self.latest_for_symbol(&path, "trade", symbol, request)
.await
}
pub async fn get_stock_snapshot_for_symbol(
&self,
symbol: &str,
request: &SingleSymbolRequest,
) -> Result<Snapshot> {
let path = format!("/stocks/{}/snapshot", segment(symbol)?);
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::latest(&path).whole_body(),
request,
)
.await?;
let body = Value::Object(merged.data);
let mut snapshot: Snapshot =
Snapshot::deserialize(&body).map_err(|source| Error::Decode {
path: merged.path,
body: crate::rest::truncate(&body.to_string()),
source,
})?;
snapshot.set_symbol(symbol);
Ok(snapshot)
}
async fn latest_for_symbol<T>(
&self,
path: &str,
key: &str,
symbol: &str,
request: &SingleSymbolRequest,
) -> Result<T>
where
T: DeserializeOwned + WithSymbol,
{
let mut merged = get_marketdata(
&self.rest,
&MarketDataRequest::latest(path).whole_body(),
request,
)
.await?;
let Some(record) = merged.data.remove(key) else {
let body = serde_json::to_string(&merged.data).unwrap_or_default();
return Err(Error::decode_shape(
path,
&body,
format_args!("the response carried no `{key}`"),
));
};
if is_absent(&record) {
return Err(Error::decode_shape(
path,
"null",
format_args!("the response carried no `{key}` for this symbol"),
));
}
let mut record: T = T::deserialize(&record).map_err(|source| Error::Decode {
path: path.to_owned(),
body: decode_body(key, &record),
source,
})?;
record.set_symbol(symbol);
Ok(record)
}
pub async fn get_stock_exchange_codes(&self) -> Result<Codes> {
self.rest.get("/stocks/meta/exchanges", &Empty).await
}
pub async fn get_stock_condition_codes(
&self,
tick_type: &TickType,
tape: Tape,
) -> Result<Codes> {
let path = format!("/stocks/meta/conditions/{}", segment(tick_type)?);
self.rest.get(&path, &TapeQuery { tape }).await
}
}
#[derive(Debug, Clone)]
pub struct CryptoHistoricalDataClient {
rest: RestClient,
}
impl CryptoHistoricalDataClient {
pub fn new() -> Result<Self> {
Ok(Self {
rest: RestClient::unauthenticated(
RestConfig::from(BaseUrl::Data).api_version("v1beta3"),
)?,
})
}
pub fn with_credentials(credentials: &Credentials) -> Result<Self> {
Self::with_config(
Some(credentials),
RestConfig::from(BaseUrl::Data).api_version("v1beta3"),
)
}
pub fn with_config(credentials: Option<&Credentials>, config: RestConfig) -> Result<Self> {
let rest = match credentials {
Some(credentials) => RestClient::new(credentials, config)?,
None => RestClient::unauthenticated(config)?,
};
Ok(Self { rest })
}
#[must_use]
pub fn rest(&self) -> &RestClient {
&self.rest
}
pub async fn get_crypto_bars(
&self,
request: &CryptoBarsRequest,
feed: CryptoFeed,
) -> Result<BarSet> {
let path = format!("/crypto/{}/bars", segment(feed)?);
let merged = get_marketdata(&self.rest, &MarketDataRequest::paged(&path), request).await?;
into_sets(merged)
}
pub async fn get_crypto_quotes(
&self,
request: &TimeseriesRequest,
feed: CryptoFeed,
) -> Result<QuoteSet> {
let path = format!("/crypto/{}/quotes", segment(feed)?);
let merged = get_marketdata(&self.rest, &MarketDataRequest::paged(&path), request).await?;
into_sets(merged)
}
pub async fn get_crypto_trades(
&self,
request: &TimeseriesRequest,
feed: CryptoFeed,
) -> Result<TradeSet> {
let path = format!("/crypto/{}/trades", segment(feed)?);
let merged = get_marketdata(&self.rest, &MarketDataRequest::paged(&path), request).await?;
into_sets(merged)
}
pub async fn get_crypto_latest_trade(
&self,
request: &CryptoLatestRequest,
feed: CryptoFeed,
) -> Result<HashMap<String, Trade>> {
let path = format!("/crypto/{}/latest/trades", segment(feed)?);
let merged = get_marketdata(&self.rest, &MarketDataRequest::latest(&path), request).await?;
into_latest(merged)
}
pub async fn get_crypto_latest_quote(
&self,
request: &CryptoLatestRequest,
feed: CryptoFeed,
) -> Result<HashMap<String, Quote>> {
let path = format!("/crypto/{}/latest/quotes", segment(feed)?);
let merged = get_marketdata(&self.rest, &MarketDataRequest::latest(&path), request).await?;
into_latest(merged)
}
pub async fn get_crypto_latest_bar(
&self,
request: &CryptoLatestRequest,
feed: CryptoFeed,
) -> Result<HashMap<String, Bar>> {
let path = format!("/crypto/{}/latest/bars", segment(feed)?);
let merged = get_marketdata(&self.rest, &MarketDataRequest::latest(&path), request).await?;
into_latest(merged)
}
pub async fn get_crypto_latest_orderbook(
&self,
request: &CryptoLatestRequest,
feed: CryptoFeed,
) -> Result<HashMap<String, Orderbook>> {
let path = format!("/crypto/{}/latest/orderbooks", segment(feed)?);
let merged = get_marketdata(&self.rest, &MarketDataRequest::latest(&path), request).await?;
into_latest(merged)
}
pub async fn get_crypto_snapshot(
&self,
request: &CryptoSnapshotRequest,
feed: CryptoFeed,
) -> Result<HashMap<String, Snapshot>> {
let path = format!("/crypto/{}/snapshots", segment(feed)?);
let merged = get_marketdata(&self.rest, &MarketDataRequest::latest(&path), request).await?;
into_latest(merged)
}
}
#[derive(Debug, Clone)]
pub struct OptionHistoricalDataClient {
rest: RestClient,
}
impl OptionHistoricalDataClient {
pub fn new(credentials: &Credentials) -> Result<Self> {
Self::with_config(
credentials,
RestConfig::from(BaseUrl::Data).api_version("v1beta1"),
)
}
pub fn with_config(credentials: &Credentials, config: RestConfig) -> Result<Self> {
Ok(Self {
rest: RestClient::new(credentials, config)?,
})
}
#[must_use]
pub fn rest(&self) -> &RestClient {
&self.rest
}
pub async fn get_option_bars(&self, request: &OptionBarsRequest) -> Result<BarSet> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::paged("/options/bars"),
request,
)
.await?;
into_sets(merged)
}
pub async fn get_option_trades(&self, request: &TimeseriesRequest) -> Result<TradeSet> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::paged("/options/trades"),
request,
)
.await?;
into_sets(merged)
}
pub async fn get_option_latest_quote(
&self,
request: &OptionLatestRequest,
) -> Result<HashMap<String, Quote>> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::latest("/options/quotes/latest"),
request,
)
.await?;
into_latest(merged)
}
pub async fn get_option_latest_trade(
&self,
request: &OptionLatestRequest,
) -> Result<HashMap<String, Trade>> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::latest("/options/trades/latest"),
request,
)
.await?;
into_latest(merged)
}
pub async fn get_option_snapshot(
&self,
request: &OptionSnapshotRequest,
) -> Result<HashMap<String, OptionsSnapshot>> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::paged_with_limit("/options/snapshots", 1000),
request,
)
.await?;
into_latest(merged)
}
pub async fn get_option_chain(
&self,
request: &OptionChainRequest,
) -> Result<HashMap<String, OptionsSnapshot>> {
let path = format!(
"/options/snapshots/{}",
segment(&request.underlying_symbol)?
);
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::paged_with_limit(&path, 1000),
request,
)
.await?;
into_latest(merged)
}
pub async fn get_option_exchange_codes(&self) -> Result<HashMap<String, String>> {
self.rest.get("/options/meta/exchanges", &Empty).await
}
pub async fn get_option_condition_codes(&self, tick_type: &TickType) -> Result<Codes> {
let path = format!("/options/meta/conditions/{}", segment(tick_type)?);
self.rest.get(&path, &Empty).await
}
}
#[derive(Debug, Clone)]
pub struct ForexDataClient {
rest: RestClient,
}
impl ForexDataClient {
pub fn new(credentials: &Credentials) -> Result<Self> {
Self::with_config(
credentials,
RestConfig::from(BaseUrl::Data).api_version("v1beta1"),
)
}
pub fn with_config(credentials: &Credentials, config: RestConfig) -> Result<Self> {
Ok(Self {
rest: RestClient::new(credentials, config)?,
})
}
#[must_use]
pub fn rest(&self) -> &RestClient {
&self.rest
}
pub async fn get_forex_rates(&self, request: &ForexRatesRequest) -> Result<ForexRateSet> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::paged_with_limit("/forex/rates", 1000),
request,
)
.await?;
into_sets(merged)
}
pub async fn get_forex_latest_rates(
&self,
request: &ForexLatestRatesRequest,
) -> Result<HashMap<String, ForexRate>> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::latest("/forex/latest/rates"),
request,
)
.await?;
into_latest(merged)
}
}
#[derive(Debug, Clone)]
pub struct LogoClient {
rest: RestClient,
}
impl LogoClient {
pub fn new(credentials: &Credentials) -> Result<Self> {
Self::with_config(
credentials,
RestConfig::from(BaseUrl::Data).api_version("v1beta1"),
)
}
pub fn with_config(credentials: &Credentials, config: RestConfig) -> Result<Self> {
Ok(Self {
rest: RestClient::new(credentials, config)?,
})
}
#[must_use]
pub fn rest(&self) -> &RestClient {
&self.rest
}
pub async fn get_logo(&self, symbol: &str, request: &LogoRequest) -> Result<Vec<u8>> {
let path = format!("/logos/{}", segment(symbol)?);
self.rest.get_bytes(&path, request).await
}
}
#[derive(Debug, Clone)]
pub struct NewsClient {
rest: RestClient,
}
impl NewsClient {
pub fn new(credentials: &Credentials) -> Result<Self> {
Self::with_config(
credentials,
RestConfig::from(BaseUrl::Data).api_version("v1beta1"),
)
}
pub fn with_config(credentials: &Credentials, config: RestConfig) -> Result<Self> {
Ok(Self {
rest: RestClient::new(credentials, config)?,
})
}
#[must_use]
pub fn rest(&self) -> &RestClient {
&self.rest
}
pub async fn get_news_page(&self, request: &NewsRequest) -> Result<NewsSet> {
self.rest.get("/news", request).await
}
pub async fn get_news(&self, request: &NewsRequest) -> Result<NewsSet> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::paged_with_limit("/news", 50),
request,
)
.await?;
let articles: Vec<News> = match merged.data.get("news") {
Some(value) => Vec::<News>::deserialize(value).map_err(|source| Error::Decode {
path: merged.path.clone(),
body: decode_body("news", value),
source,
})?,
None => Vec::new(),
};
Ok(NewsSet {
news: articles,
next_page_token: None,
})
}
}
#[derive(Debug, Clone)]
pub struct ScreenerClient {
rest: RestClient,
}
impl ScreenerClient {
pub fn new(credentials: &Credentials) -> Result<Self> {
Self::with_config(
credentials,
RestConfig::from(BaseUrl::Data).api_version("v1beta1"),
)
}
pub fn with_config(credentials: &Credentials, config: RestConfig) -> Result<Self> {
Ok(Self {
rest: RestClient::new(credentials, config)?,
})
}
#[must_use]
pub fn rest(&self) -> &RestClient {
&self.rest
}
pub async fn get_most_actives(&self, request: &MostActivesRequest) -> Result<MostActives> {
self.rest
.get("/screener/stocks/most-actives", request)
.await
}
pub async fn get_market_movers(&self, request: &MarketMoversRequest) -> Result<Movers> {
let path = format!("/screener/{}/movers", segment(&request.market_type)?);
self.rest.get(&path, request).await
}
}
#[derive(Debug, Clone)]
pub struct CorporateActionsClient {
rest: RestClient,
raw: reqwest::Client,
base_url: String,
}
impl CorporateActionsClient {
pub fn new(credentials: &Credentials) -> Result<Self> {
Self::with_config(
credentials,
RestConfig::from(BaseUrl::Data).api_version("v1"),
)
}
pub fn with_config(credentials: &Credentials, config: RestConfig) -> Result<Self> {
Ok(Self {
raw: crate::sse::streaming_client(credentials)?,
base_url: config.base_url.clone(),
rest: RestClient::new(credentials, config)?,
})
}
#[must_use]
pub fn rest(&self) -> &RestClient {
&self.rest
}
pub async fn get_corporate_actions(
&self,
request: &CorporateActionsRequest,
) -> Result<CorporateActions> {
let merged = get_marketdata(
&self.rest,
&MarketDataRequest::paged_with_limit("/corporate-actions", 1000),
request,
)
.await?;
let body = Value::Object(merged.data);
CorporateActions::deserialize(&body).map_err(|source| Error::Decode {
path: merged.path,
body: crate::rest::truncate(&body.to_string()),
source,
})
}
pub async fn get_corporate_action_events(
&self,
filter: Option<&CorporateActionEventsRequest>,
) -> Result<impl futures_util::Stream<Item = Result<crate::sse::Event>> + use<>> {
let path = "/events/corporate-actions";
let url = format!("{}/v1beta1{path}", self.base_url.trim_end_matches('/'));
let query = filter
.map(CorporateActionEventsRequest::query)
.unwrap_or_default();
crate::sse::subscribe(&self.raw, &url, path, &query).await
}
}