use crate::{
Client, Result,
endpoints::{
EndpointSpec, QueryEncoder, QueryParameters,
metadata::{EndpointMetadata, GeographicAvailability},
},
query::{Quarter, Year},
responses::funds::{
EtfAssetExposure, EtfCountryWeighting, EtfFundHolding, EtfFundInfo, EtfSectorWeighting,
FundDisclosure, FundDisclosureDate, FundDisclosureHolder, FundDisclosureSearchResult,
},
types::{Cik, SearchTerm, Ticker},
};
macro_rules! required_symbol_query {
($name:ident, $description:literal) => {
#[doc = $description]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct $name {
symbol: Ticker,
}
impl $name {
pub const fn new(symbol: Ticker) -> Self {
Self { symbol }
}
pub const fn symbol(&self) -> &Ticker {
&self.symbol
}
}
impl From<Ticker> for $name {
fn from(symbol: Ticker) -> Self {
Self::new(symbol)
}
}
impl From<&Ticker> for $name {
fn from(symbol: &Ticker) -> Self {
Self::new(symbol.clone())
}
}
impl QueryParameters for $name {
fn encode(&self, encoder: &mut QueryEncoder<'_>) {
encoder.required("symbol", &self.symbol);
}
}
};
}
required_symbol_query!(
EtfHoldingsQuery,
"Required fund ticker for retrieving ETF or mutual-fund holdings."
);
required_symbol_query!(
EtfInfoQuery,
"Required fund ticker for retrieving ETF or mutual-fund information."
);
required_symbol_query!(
EtfCountryWeightingsQuery,
"Required fund ticker for retrieving country allocation."
);
required_symbol_query!(
EtfAssetExposureQuery,
"Required asset ticker for discovering ETF exposure."
);
required_symbol_query!(
EtfSectorWeightingsQuery,
"Required fund ticker for retrieving sector allocation."
);
required_symbol_query!(
LatestFundDisclosureHoldersQuery,
"Required asset ticker for retrieving the latest disclosed fund holders."
);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FundDisclosureQuery {
symbol: Ticker,
year: Year,
quarter: Quarter,
cik: Option<Cik>,
}
impl FundDisclosureQuery {
pub const fn new(symbol: Ticker, year: Year, quarter: Quarter) -> Self {
Self {
symbol,
year,
quarter,
cik: None,
}
}
pub fn with_cik(mut self, cik: Cik) -> Self {
self.cik = Some(cik);
self
}
pub const fn symbol(&self) -> &Ticker {
&self.symbol
}
pub const fn year(&self) -> Year {
self.year
}
pub const fn quarter(&self) -> Quarter {
self.quarter
}
pub const fn cik(&self) -> Option<&Cik> {
self.cik.as_ref()
}
}
impl QueryParameters for FundDisclosureQuery {
fn encode(&self, encoder: &mut QueryEncoder<'_>) {
encoder.required("symbol", &self.symbol);
encoder.required("year", self.year);
encoder.required("quarter", self.quarter);
encoder.optional("cik", self.cik.as_ref());
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FundDisclosureHolderSearchQuery {
name: SearchTerm,
}
impl FundDisclosureHolderSearchQuery {
pub const fn new(name: SearchTerm) -> Self {
Self { name }
}
pub const fn name(&self) -> &SearchTerm {
&self.name
}
}
impl From<SearchTerm> for FundDisclosureHolderSearchQuery {
fn from(name: SearchTerm) -> Self {
Self::new(name)
}
}
impl From<&SearchTerm> for FundDisclosureHolderSearchQuery {
fn from(name: &SearchTerm) -> Self {
Self::new(name.clone())
}
}
impl QueryParameters for FundDisclosureHolderSearchQuery {
fn encode(&self, encoder: &mut QueryEncoder<'_>) {
encoder.required("name", &self.name);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FundDisclosureDatesQuery {
symbol: Ticker,
cik: Option<Cik>,
}
impl FundDisclosureDatesQuery {
pub const fn new(symbol: Ticker) -> Self {
Self { symbol, cik: None }
}
pub fn with_cik(mut self, cik: Cik) -> Self {
self.cik = Some(cik);
self
}
pub const fn symbol(&self) -> &Ticker {
&self.symbol
}
pub const fn cik(&self) -> Option<&Cik> {
self.cik.as_ref()
}
}
impl From<Ticker> for FundDisclosureDatesQuery {
fn from(symbol: Ticker) -> Self {
Self::new(symbol)
}
}
impl From<&Ticker> for FundDisclosureDatesQuery {
fn from(symbol: &Ticker) -> Self {
Self::new(symbol.clone())
}
}
impl QueryParameters for FundDisclosureDatesQuery {
fn encode(&self, encoder: &mut QueryEncoder<'_>) {
encoder.required("symbol", &self.symbol);
encoder.optional("cik", self.cik.as_ref());
}
}
const WORLDWIDE: EndpointMetadata =
EndpointMetadata::new().with_geography(GeographicAvailability::Worldwide);
const US_ONLY: EndpointMetadata =
EndpointMetadata::new().with_geography(GeographicAvailability::UsOnly);
pub fn etf_holdings(
query: EtfHoldingsQuery,
) -> EndpointSpec<EtfHoldingsQuery, Vec<EtfFundHolding>> {
EndpointSpec::get("etf/holdings", "etf/holdings", query).with_metadata(WORLDWIDE)
}
pub fn etf_info(query: EtfInfoQuery) -> EndpointSpec<EtfInfoQuery, Vec<EtfFundInfo>> {
EndpointSpec::get("etf/info", "etf/info", query).with_metadata(WORLDWIDE)
}
pub fn etf_country_weightings(
query: EtfCountryWeightingsQuery,
) -> EndpointSpec<EtfCountryWeightingsQuery, Vec<EtfCountryWeighting>> {
EndpointSpec::get("etf/country-weightings", "etf/country-weightings", query)
.with_metadata(WORLDWIDE)
}
pub fn etf_asset_exposure(
query: EtfAssetExposureQuery,
) -> EndpointSpec<EtfAssetExposureQuery, Vec<EtfAssetExposure>> {
EndpointSpec::get("etf/asset-exposure", "etf/asset-exposure", query).with_metadata(WORLDWIDE)
}
pub fn etf_sector_weightings(
query: EtfSectorWeightingsQuery,
) -> EndpointSpec<EtfSectorWeightingsQuery, Vec<EtfSectorWeighting>> {
EndpointSpec::get("etf/sector-weightings", "etf/sector-weightings", query)
.with_metadata(WORLDWIDE)
}
pub fn latest_fund_disclosure_holders(
query: LatestFundDisclosureHoldersQuery,
) -> EndpointSpec<LatestFundDisclosureHoldersQuery, Vec<FundDisclosureHolder>> {
EndpointSpec::get(
"funds/disclosure-holders-latest",
"funds/disclosure-holders-latest",
query,
)
.with_metadata(US_ONLY)
}
pub fn fund_disclosures(
query: FundDisclosureQuery,
) -> EndpointSpec<FundDisclosureQuery, Vec<FundDisclosure>> {
EndpointSpec::get("funds/disclosure", "funds/disclosure", query).with_metadata(US_ONLY)
}
pub fn search_fund_disclosure_holders(
query: FundDisclosureHolderSearchQuery,
) -> EndpointSpec<FundDisclosureHolderSearchQuery, Vec<FundDisclosureSearchResult>> {
EndpointSpec::get(
"funds/disclosure-holders-search",
"funds/disclosure-holders-search",
query,
)
.with_metadata(US_ONLY)
}
pub fn fund_disclosure_dates(
query: FundDisclosureDatesQuery,
) -> EndpointSpec<FundDisclosureDatesQuery, Vec<FundDisclosureDate>> {
EndpointSpec::get("funds/disclosure-dates", "funds/disclosure-dates", query)
.with_metadata(US_ONLY)
}
impl Client {
pub async fn etf_holdings(&self, query: EtfHoldingsQuery) -> Result<Vec<EtfFundHolding>> {
self.execute(&etf_holdings(query)).await
}
pub async fn etf_info(&self, query: EtfInfoQuery) -> Result<Vec<EtfFundInfo>> {
self.execute(&etf_info(query)).await
}
pub async fn etf_country_weightings(
&self,
query: EtfCountryWeightingsQuery,
) -> Result<Vec<EtfCountryWeighting>> {
self.execute(&etf_country_weightings(query)).await
}
pub async fn etf_asset_exposure(
&self,
query: EtfAssetExposureQuery,
) -> Result<Vec<EtfAssetExposure>> {
self.execute(&etf_asset_exposure(query)).await
}
pub async fn etf_sector_weightings(
&self,
query: EtfSectorWeightingsQuery,
) -> Result<Vec<EtfSectorWeighting>> {
self.execute(&etf_sector_weightings(query)).await
}
pub async fn latest_fund_disclosure_holders(
&self,
query: LatestFundDisclosureHoldersQuery,
) -> Result<Vec<FundDisclosureHolder>> {
self.execute(&latest_fund_disclosure_holders(query)).await
}
pub async fn fund_disclosures(
&self,
query: FundDisclosureQuery,
) -> Result<Vec<FundDisclosure>> {
self.execute(&fund_disclosures(query)).await
}
pub async fn search_fund_disclosure_holders(
&self,
query: FundDisclosureHolderSearchQuery,
) -> Result<Vec<FundDisclosureSearchResult>> {
self.execute(&search_fund_disclosure_holders(query)).await
}
pub async fn fund_disclosure_dates(
&self,
query: FundDisclosureDatesQuery,
) -> Result<Vec<FundDisclosureDate>> {
self.execute(&fund_disclosure_dates(query)).await
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pairs(query: &impl QueryParameters) -> Vec<(String, String)> {
let mut pairs = Vec::new();
let mut visitor = |name: &str, value: &str| {
pairs.push((name.to_owned(), value.to_owned()));
};
query.encode(&mut QueryEncoder::new(&mut visitor));
pairs
}
#[test]
fn symbol_queries_emit_the_exact_only_pair() {
let symbol = Ticker::new("BRK.B / Class A").unwrap();
macro_rules! assert_symbol_query {
($query:ty) => {
assert_eq!(
pairs(&<$query>::from(&symbol)),
[("symbol".to_owned(), "BRK.B / Class A".to_owned())]
);
};
}
assert_symbol_query!(EtfHoldingsQuery);
assert_symbol_query!(EtfInfoQuery);
assert_symbol_query!(EtfCountryWeightingsQuery);
assert_symbol_query!(EtfAssetExposureQuery);
assert_symbol_query!(EtfSectorWeightingsQuery);
assert_symbol_query!(LatestFundDisclosureHoldersQuery);
}
#[test]
fn disclosures_emit_required_values_then_optional_cik() {
let base =
FundDisclosureQuery::new(Ticker::new("000089.SZ").unwrap(), Year(0), Quarter::Q4);
assert_eq!(
pairs(&base),
[
("symbol".to_owned(), "000089.SZ".to_owned()),
("year".to_owned(), "0".to_owned()),
("quarter".to_owned(), "4".to_owned()),
]
);
let with_cik = base.with_cik(Cik::new("0000857489").unwrap());
assert_eq!(
pairs(&with_cik),
[
("symbol".to_owned(), "000089.SZ".to_owned()),
("year".to_owned(), "0".to_owned()),
("quarter".to_owned(), "4".to_owned()),
("cik".to_owned(), "0000857489".to_owned()),
]
);
}
#[test]
fn search_and_dates_preserve_exact_values_order_and_omission() {
let name = SearchTerm::new("Federated Hermes Government Income Securities, Inc.").unwrap();
assert_eq!(
pairs(&FundDisclosureHolderSearchQuery::from(&name)),
[("name".to_owned(), name.to_string())]
);
let dates = FundDisclosureDatesQuery::new(Ticker::new("VWO").unwrap());
assert_eq!(pairs(&dates), [("symbol".to_owned(), "VWO".to_owned())]);
assert_eq!(
pairs(&dates.with_cik(Cik::new("0000036405").unwrap())),
[
("symbol".to_owned(), "VWO".to_owned()),
("cik".to_owned(), "0000036405".to_owned()),
]
);
}
}