yfinance-rs 0.4.0

Ergonomic Rust client for Yahoo Finance, supporting historical prices, real-time streaming, options, fundamentals, and more.
Documentation
use crate::core::{
    YfClient, YfError,
    client::{CacheMode, RetryConfig},
    net,
};
use serde::Deserialize;

#[cfg(feature = "debug-dumps")]
use crate::profile::debug::debug_dump_api;

#[derive(Deserialize)]
pub struct V10Envelope {
    #[serde(rename = "quoteSummary")]
    pub(crate) quote_summary: Option<V10QuoteSummary>,
}

#[derive(Deserialize)]
pub struct V10QuoteSummary {
    pub(crate) result: Option<Vec<serde_json::Value>>,
    pub(crate) error: Option<V10Error>,
}

#[derive(Deserialize)]
pub struct V10Error {
    pub(crate) description: String,
}

pub async fn fetch(
    client: &YfClient,
    symbol: &str,
    modules: &str,
    caller: &str,
    cache_mode: CacheMode,
    retry_override: Option<&RetryConfig>,
) -> Result<V10Envelope, YfError> {
    async fn attempt_fetch(
        client: &YfClient,
        symbol: &str,
        modules: &str,
        caller: &str,
        cache_mode: CacheMode,
        retry_override: Option<&RetryConfig>,
    ) -> Result<V10Envelope, YfError> {
        client.ensure_credentials().await?;

        let crumb = client
            .crumb()
            .await
            .ok_or_else(|| YfError::Auth("Crumb is not set".into()))?;

        let mut url = client.base_quote_api().join(symbol)?;
        {
            let mut qp = url.query_pairs_mut();
            qp.append_pair("modules", modules);
            qp.append_pair("crumb", &crumb);
        }

        if cache_mode == CacheMode::Use
            && let Some(text) = client.cache_get(&url).await
        {
            #[cfg(feature = "debug-dumps")]
            let _ = debug_dump_api(symbol, &text);
            return serde_json::from_str(&text).map_err(YfError::Json);
        }

        let req = client.http().get(url.clone());
        let resp = client.send_with_retry(req, retry_override).await?;

        // Create a sanitized key from module names for a unique fixture filename.
        let module_key = modules
            .replace(',', "-")
            .replace(|c: char| !c.is_alphanumeric() && c != '-', "");
        let fixture_endpoint = format!("{caller}_api_{module_key}");
        let text = net::get_text(resp, &fixture_endpoint, symbol, "json").await?;

        #[cfg(feature = "debug-dumps")]
        let _ = debug_dump_api(symbol, &text);

        if cache_mode != CacheMode::Bypass {
            client.cache_put(&url, &text, None).await;
        }

        serde_json::from_str(&text).map_err(YfError::Json)
    }

    for attempt in 0..=1 {
        let env =
            attempt_fetch(client, symbol, modules, caller, cache_mode, retry_override).await?;

        if let Some(error) = env.quote_summary.as_ref().and_then(|qs| qs.error.as_ref()) {
            let desc = error.description.to_ascii_lowercase();
            if desc.contains("invalid crumb") && attempt == 0 {
                if std::env::var("YF_DEBUG").ok().as_deref() == Some("1") {
                    eprintln!("YF_DEBUG: Invalid crumb in {caller}; refreshing and retrying.");
                }
                client.clear_crumb().await;
                continue;
            }
            return Err(YfError::Api(format!("yahoo error: {}", error.description)));
        }

        return Ok(env);
    }

    Err(YfError::Api(format!(
        "{caller} API call failed after retry"
    )))
}

pub async fn fetch_module_result<T>(
    client: &YfClient,
    symbol: &str,
    modules: &str,
    caller: &str,
    cache_mode: CacheMode,
    retry_override: Option<&RetryConfig>,
) -> Result<T, YfError>
where
    T: for<'de> serde::Deserialize<'de>,
{
    let env = fetch(client, symbol, modules, caller, cache_mode, retry_override).await?;

    let result_val = env
        .quote_summary
        .and_then(|qs| qs.result)
        .and_then(|mut v| v.pop())
        .ok_or_else(|| YfError::MissingData("empty quoteSummary result".into()))?;

    serde_json::from_value(result_val).map_err(YfError::Json)
}