yfinance-rs 0.6.0

Ergonomic Rust client for Yahoo Finance, supporting historical prices, real-time streaming, options, fundamentals, and more.
Documentation
mod actions;
mod adjust;
mod assemble;
mod fetch;

use crate::core::client::{CacheMode, RetryConfig};
// use crate::core::conversions::f64_to_money_with_currency_str;
use crate::core::{YfClient, YfError};
use crate::history::wire::MetaNode;
use chrono_tz::Tz;
use paft::market::action::Action;
use paft::market::requests::history::{Interval, Range};
use paft::market::responses::history::{Candle, HistoryMeta, HistoryResponse};

use actions::extract_actions;
use adjust::cumulative_split_after;
use assemble::assemble_candles;
use fetch::fetch_chart;

/// A builder for fetching historical price data for a single symbol.
///
/// This builder provides fine-grained control over the parameters for a historical
/// data request, including the time range, interval, and data adjustments.
#[derive(Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct HistoryBuilder {
    #[doc(hidden)]
    pub(crate) client: YfClient,
    #[doc(hidden)]
    pub(crate) symbol: String,
    #[doc(hidden)]
    pub(crate) range: Option<Range>,
    #[doc(hidden)]
    pub(crate) period: Option<(i64, i64)>,
    #[doc(hidden)]
    pub(crate) interval: Interval,
    #[doc(hidden)]
    pub(crate) auto_adjust: bool,
    #[doc(hidden)]
    pub(crate) include_prepost: bool,
    #[doc(hidden)]
    pub(crate) include_actions: bool,
    #[doc(hidden)]
    pub(crate) keepna: bool,
    #[doc(hidden)]
    pub(crate) cache_mode: CacheMode,
    #[doc(hidden)]
    pub(crate) retry_override: Option<RetryConfig>,
}

impl HistoryBuilder {
    /// Creates a new `HistoryBuilder` for a given symbol.
    pub fn new(client: &YfClient, symbol: impl Into<String>) -> Self {
        Self {
            client: client.clone(),
            symbol: symbol.into(),
            range: Some(Range::M6),
            period: None,
            interval: Interval::D1,
            auto_adjust: true,
            include_prepost: false,
            include_actions: true,
            keepna: false,
            cache_mode: CacheMode::Use,
            retry_override: None,
        }
    }

    /// Sets the cache mode for this specific API call.
    #[must_use]
    pub const fn cache_mode(mut self, mode: CacheMode) -> Self {
        self.cache_mode = mode;
        self
    }

    /// Overrides the default retry policy for this specific API call.
    #[must_use]
    pub fn retry_policy(mut self, cfg: Option<RetryConfig>) -> Self {
        self.retry_override = cfg;
        self
    }

    /// Sets a relative time range for the request (e.g., `1y`, `6mo`).
    ///
    /// This will override any previously set period using `between()`.
    #[must_use]
    pub const fn range(mut self, range: Range) -> Self {
        self.period = None;
        self.range = Some(range);
        self
    }

    /// Sets an absolute time period for the request using start and end timestamps.
    ///
    /// This will override any previously set range using `range()`.
    #[must_use]
    pub const fn between(
        mut self,
        start: chrono::DateTime<chrono::Utc>,
        end: chrono::DateTime<chrono::Utc>,
    ) -> Self {
        self.range = None;
        self.period = Some((start.timestamp(), end.timestamp()));
        self
    }

    /// Sets the time interval for each data point (candle).
    #[must_use]
    pub const fn interval(mut self, interval: Interval) -> Self {
        self.interval = interval;
        self
    }

    /// Sets whether to automatically adjust prices for splits and dividends. (Default: `true`)
    #[must_use]
    pub const fn auto_adjust(mut self, yes: bool) -> Self {
        self.auto_adjust = yes;
        self
    }

    /// Sets whether to include pre-market and post-market data for intraday intervals. (Default: `false`)
    #[must_use]
    pub const fn prepost(mut self, yes: bool) -> Self {
        self.include_prepost = yes;
        self
    }

    /// Sets whether to include corporate actions (dividends and splits) in the response. (Default: `true`)
    #[must_use]
    pub const fn actions(mut self, yes: bool) -> Self {
        self.include_actions = yes;
        self
    }

    /// Sets whether to keep data rows that have missing OHLC values. (Default: `false`)
    ///
    /// If `true`, missing values are represented as `f64::NAN`. If `false`, rows with any missing
    /// OHLC values are dropped.
    #[must_use]
    pub const fn keepna(mut self, yes: bool) -> Self {
        self.keepna = yes;
        self
    }

    /// Executes the request and returns only the price candles.
    ///
    /// # Errors
    ///
    /// Returns a `YfError` if the network request fails or the API response cannot be parsed.
    pub async fn fetch(self) -> Result<Vec<Candle>, YfError> {
        let resp = self.fetch_full().await?;
        Ok(resp.candles)
    }

    /// Executes the request and returns the full response, including candles, actions, and metadata.
    ///
    /// # Errors
    ///
    /// Returns a `YfError` if the network request fails, the API returns an error,
    /// or the response cannot be parsed.
    #[cfg_attr(
        feature = "tracing",
        tracing::instrument(
            skip(self),
            err,
            fields(
                symbol = %self.symbol,
                interval = %format!("{:?}", self.interval),
                range = %self
                    .range
                    .as_ref()
                    .map_or_else(|| "period".into(), |r| format!("{r:?}"))
            )
        )
    )]
    pub async fn fetch_full(self) -> Result<HistoryResponse, YfError> {
        // 1) Fetch and parse the /chart payload into owned blocks
        let fetched = fetch_chart(
            &self.client,
            &self.symbol,
            self.range,
            self.period,
            self.interval,
            self.include_actions,
            self.include_prepost,
            self.cache_mode,
            self.retry_override.as_ref(),
        )
        .await?;

        // 2) Corporate actions & split ratios
        let reporting_currency = self.client.reporting_currency(&self.symbol, None).await;

        let (mut actions_out, split_events) =
            extract_actions(fetched.events.as_ref(), &reporting_currency);

        // 3) Cumulative split factors after each bar
        let cum_split_after = cumulative_split_after(&fetched.ts, &split_events);

        // 4) Assemble candles (+ raw close) with/without adjustments
        let currency = fetched.meta.as_ref().and_then(|m| m.currency.as_deref());
        let candles = assemble_candles(
            &fetched.ts,
            &fetched.quote,
            &fetched.adjclose,
            self.auto_adjust,
            self.keepna,
            &cum_split_after,
            currency,
        );

        // ensure actions sorted (extract_actions already sorts, keep consistent)
        actions_out.sort_by_key(|a| match a {
            Action::Dividend { ts, .. }
            | Action::Split { ts, .. }
            | Action::CapitalGain { ts, .. } => ts.timestamp(),
        });

        // 5) Map metadata
        let meta_out = map_meta(fetched.meta.as_ref());

        Ok(HistoryResponse {
            candles,
            actions: actions_out,
            adjusted: self.auto_adjust,
            meta: meta_out,
        })
    }
}

/* --- tiny private helper --- */

fn map_meta(m: Option<&MetaNode>) -> Option<HistoryMeta> {
    m.as_ref().map(|mm| HistoryMeta {
        timezone: mm
            .timezone
            .as_ref()
            .and_then(|tz_str| tz_str.parse::<Tz>().ok()),
        utc_offset_seconds: mm.gmtoffset,
    })
}