use crate::{
client::FinnhubClient,
error::Result,
models::stock::{
EarningsCallLive, EarningsCallTranscript, EarningsCallTranscriptsList, Filing,
InternationalFiling, InvestorPresentations, SimilarityIndex,
},
};
pub struct FilingsEndpoints<'a> {
client: &'a FinnhubClient,
}
impl<'a> FilingsEndpoints<'a> {
pub fn new(client: &'a FinnhubClient) -> Self {
Self { client }
}
pub async fn sec(
&self,
symbol: Option<&str>,
cik: Option<&str>,
access_number: Option<&str>,
form: Option<&str>,
from: Option<&str>,
to: Option<&str>,
) -> Result<Vec<Filing>> {
let mut params = vec![];
if let Some(s) = symbol {
params.push(format!("symbol={}", s));
}
if let Some(c) = cik {
params.push(format!("cik={}", c));
}
if let Some(a) = access_number {
params.push(format!("accessNumber={}", a));
}
if let Some(f) = form {
params.push(format!("form={}", f));
}
if let Some(from_date) = from {
params.push(format!("from={}", from_date));
}
if let Some(to_date) = to {
params.push(format!("to={}", to_date));
}
let query = if params.is_empty() {
String::from("/stock/filings")
} else {
format!("/stock/filings?{}", params.join("&"))
};
self.client.get(&query).await
}
pub async fn international(
&self,
symbol: Option<&str>,
country: Option<&str>,
from: Option<&str>,
to: Option<&str>,
) -> Result<Vec<InternationalFiling>> {
let mut params = vec![];
if let Some(s) = symbol {
params.push(format!("symbol={}", s));
}
if let Some(c) = country {
params.push(format!("country={}", c));
}
if let Some(f) = from {
params.push(format!("from={}", f));
}
if let Some(t) = to {
params.push(format!("to={}", t));
}
let query = if params.is_empty() {
String::from("/stock/international-filings")
} else {
format!("/stock/international-filings?{}", params.join("&"))
};
self.client.get(&query).await
}
pub async fn transcript(&self, id: &str) -> Result<EarningsCallTranscript> {
self.client
.get(&format!("/stock/transcripts?id={}", id))
.await
}
pub async fn transcripts_list(&self, symbol: &str) -> Result<EarningsCallTranscriptsList> {
self.client
.get(&format!("/stock/transcripts/list?symbol={}", symbol))
.await
}
pub async fn earnings_call_live(&self, from: &str, to: &str) -> Result<EarningsCallLive> {
self.client
.get(&format!(
"/stock/earnings-call-live?from={}&to={}",
from, to
))
.await
}
pub async fn presentations(&self, symbol: &str) -> Result<InvestorPresentations> {
self.client
.get(&format!("/stock/presentation?symbol={}", symbol))
.await
}
pub async fn similarity_index(
&self,
symbol: Option<&str>,
cik: Option<&str>,
freq: Option<&str>,
) -> Result<SimilarityIndex> {
let mut params = vec![];
if let Some(s) = symbol {
params.push(format!("symbol={}", s));
}
if let Some(c) = cik {
params.push(format!("cik={}", c));
}
if let Some(f) = freq {
params.push(format!("freq={}", f));
}
if params.is_empty() {
return Err(crate::Error::InvalidRequest(
"At least one of symbol or cik must be provided".to_string(),
));
}
let query = format!("/stock/similarity-index?{}", params.join("&"));
self.client.get(&query).await
}
}
#[cfg(test)]
mod tests {
use crate::{ClientConfig, FinnhubClient, RateLimitStrategy};
async fn test_client() -> FinnhubClient {
dotenv::dotenv().ok();
let api_key = std::env::var("FINNHUB_API_KEY").unwrap_or_else(|_| "test_key".to_string());
let mut config = ClientConfig::default();
config.rate_limit_strategy = RateLimitStrategy::FifteenSecondWindow;
FinnhubClient::with_config(api_key, config)
}
#[tokio::test]
#[ignore = "requires API key"]
async fn test_sec_filings() {
let client = test_client().await;
let result = client
.stock()
.sec_filings(Some("AAPL"), None, None, None, None, None)
.await;
assert!(
result.is_ok(),
"Failed to get SEC filings: {:?}",
result.err()
);
}
#[tokio::test]
#[ignore = "requires API key"]
async fn test_sec_filings_with_form_filter() {
let client = test_client().await;
let result = client
.stock()
.sec_filings(Some("MSFT"), None, None, Some("10-K"), None, None)
.await;
assert!(
result.is_ok(),
"Failed to get SEC filings with form filter: {:?}",
result.err()
);
}
#[tokio::test]
#[ignore = "requires API key"]
async fn test_sec_filings_with_date_range() {
let client = test_client().await;
let from = "2023-01-01";
let to = "2023-12-31";
let result = client
.stock()
.sec_filings(Some("GOOGL"), None, None, None, Some(from), Some(to))
.await;
assert!(
result.is_ok(),
"Failed to get SEC filings with date range: {:?}",
result.err()
);
}
#[tokio::test]
#[ignore = "requires API key"]
async fn test_international_filings() {
let client = test_client().await;
let result = client
.stock()
.international_filings(
Some("NVO"), None,
None,
None,
)
.await;
assert!(
result.is_ok(),
"Failed to get international filings: {:?}",
result.err()
);
}
#[tokio::test]
#[ignore = "requires API key"]
async fn test_transcripts_list() {
let client = test_client().await;
let result = client.stock().transcripts_list("AAPL").await;
assert!(
result.is_ok(),
"Failed to get transcripts list: {:?}",
result.err()
);
}
#[tokio::test]
#[ignore = "requires API key"]
async fn test_transcript() {
let client = test_client().await;
let result = client.stock().transcripts("AAPL_162777").await;
assert!(
result.is_ok(),
"Failed to get transcript: {:?}",
result.err()
);
let transcript = result.unwrap();
assert!(!transcript.transcript.is_empty(), "Transcript should have content");
assert!(!transcript.participant.is_empty(), "Transcript should have participants");
}
#[tokio::test]
#[ignore = "requires API key"]
async fn test_earnings_call_live() {
let client = test_client().await;
let from = chrono::Local::now().format("%Y-%m-%d").to_string();
let to = (chrono::Local::now() + chrono::Duration::days(30))
.format("%Y-%m-%d")
.to_string();
let result = client.stock().earnings_call_live(&from, &to).await;
assert!(
result.is_ok(),
"Failed to get earnings call live: {:?}",
result.err()
);
}
#[tokio::test]
#[ignore = "requires API key"]
async fn test_presentations() {
let client = test_client().await;
let result = client.stock().presentations("AAPL").await;
assert!(
result.is_ok(),
"Failed to get presentations: {:?}",
result.err()
);
}
#[tokio::test]
#[ignore = "requires API key"]
async fn test_similarity_index() {
let client = test_client().await;
let result = client
.stock()
.similarity_index(Some("AAPL"), None, Some("annual"))
.await;
assert!(
result.is_ok(),
"Failed to get similarity index: {:?}",
result.err()
);
}
#[tokio::test]
#[ignore = "requires API key"]
async fn test_similarity_index_error() {
let client = test_client().await;
let result = client.stock().similarity_index(None, None, None).await;
assert!(
result.is_err(),
"Should fail when neither symbol nor cik is provided"
);
}
}