yfinance-rs 0.9.1

Ergonomic Rust client for Yahoo Finance, supporting historical prices, real-time streaming, options, fundamentals, and more.
Documentation
use chrono::{Duration, Utc};
use std::fmt::Display;
use std::time::Duration as StdDuration;
use yfinance_rs::core::Interval;
use yfinance_rs::{
    DownloadBuilder, NewsTab, StreamBuilder, StreamMethod, Ticker, YfClient, YfClientBuilder,
    YfError,
};

fn display_opt<T: Display>(value: Option<&T>) -> String {
    value.map_or_else(|| "N/A".to_string(), ToString::to_string)
}

#[tokio::main]
async fn main() -> Result<(), YfError> {
    let client = YfClientBuilder::default()
        .timeout(StdDuration::from_secs(5))
        .build()?;

    section_info(&client).await?;
    section_fast_info(&client).await?;
    section_batch_quotes(&client).await?;
    section_download(&client).await?;
    section_options(&client).await?;
    section_stream(&client).await?;
    section_news(&client).await?;
    Ok(())
}

async fn section_info(client: &YfClient) -> Result<(), YfError> {
    let msft = Ticker::new(client, "MSFT");
    let info = msft.info().await?;
    println!("--- Ticker Info for {} ---", info.snapshot.instrument);
    println!("Name: {}", info.snapshot.name.unwrap_or_default());
    println!("Last Price: {}", display_opt(info.snapshot.last.as_ref()));
    if let Some(v) = info.snapshot.volume {
        println!("Volume (day): {v}");
    }
    if let Some(pt) = info.price_target.as_ref()
        && let Some(mean) = pt.mean.as_ref()
    {
        println!("Price target mean: {mean}");
    }
    if let Some(rs) = info.recommendation_summary.as_ref() {
        let mean = rs.mean.unwrap_or_default();
        let text = rs.mean_rating_text.as_deref().unwrap_or("N/A");
        println!("Recommendation mean: {mean:.2} ({text})");
    }
    println!();
    Ok(())
}

async fn section_fast_info(client: &YfClient) -> Result<(), YfError> {
    println!("--- Fast Info for NVDA ---");
    let nvda = Ticker::new(client, "NVDA");
    let fast_info = nvda.fast_info().await?;
    let price_money = fast_info
        .snapshot
        .last
        .clone()
        .or_else(|| fast_info.snapshot.previous_close.clone())
        .expect("last or previous_close present");
    println!(
        "{} is trading at {} in {}",
        fast_info.snapshot.instrument,
        price_money,
        fast_info
            .snapshot
            .instrument
            .exchange
            .as_ref()
            .map(std::string::ToString::to_string)
            .unwrap_or_default()
    );
    if let Some(v) = fast_info.snapshot.volume {
        println!("Day volume: {v}");
    }
    println!();
    Ok(())
}

async fn section_batch_quotes(client: &YfClient) -> Result<(), YfError> {
    println!("--- Batch Quotes for Multiple Symbols ---");
    let quotes = yfinance_rs::quotes(client, vec!["AMD", "INTC", "QCOM"]).await?;
    for quote in quotes {
        let vol = quote
            .day_volume
            .map(|v| format!(" (vol: {v})"))
            .unwrap_or_default();
        println!(
            "  {}: {}{}",
            quote.instrument,
            display_opt(quote.price.as_ref()),
            vol
        );
    }
    println!();
    Ok(())
}

async fn section_download(client: &YfClient) -> Result<(), YfError> {
    let symbols = vec!["AAPL", "GOOG", "TSLA"];
    let today = Utc::now();
    let three_months_ago = today - Duration::days(90);
    println!("--- Historical Data for Multiple Symbols ---");
    let results = DownloadBuilder::new(client)
        .symbols(symbols)
        .between(three_months_ago, today)
        .interval(Interval::D1)
        .run()
        .await?;
    for entry in &results.entries {
        let symbol = entry.instrument.symbol.as_str();
        let candles = &entry.history.candles;
        println!("{symbol} has {} data points.", candles.len());
        if let Some(last_candle) = candles.last() {
            println!("  Last close price: {}", last_candle.ohlc.close);
        }
    }
    println!();
    Ok(())
}

async fn section_options(client: &YfClient) -> Result<(), YfError> {
    let aapl = Ticker::new(client, "AAPL");
    let expirations = aapl.options().await?;
    if let Some(first_expiry) = expirations.first() {
        println!("--- Options Chain for AAPL ({first_expiry}) ---");
        let chain = aapl.option_chain(Some(*first_expiry)).await?;
        println!(
            "  Found {} calls and {} puts.",
            chain.calls().count(),
            chain.puts().count()
        );
        if let Some(first_call) = chain.calls().next() {
            let contract = first_call
                .key
                .contract_instrument
                .as_ref()
                .unwrap_or(&first_call.key.underlying);
            println!(
                "  First call option: {} @ {}",
                contract, first_call.key.strike
            );
        }
    }
    println!();
    Ok(())
}

async fn section_stream(client: &YfClient) -> Result<(), YfError> {
    println!("--- Streaming Real-time Quotes for MSFT and GOOG ---");
    println!("(Streaming for 10 seconds or until stopped...)");
    let (handle, mut receiver) = StreamBuilder::new(client)
        .symbols(vec!["MSFT", "GOOG"])
        .method(StreamMethod::WebsocketWithFallback)
        .start()
        .await?;

    let stream_task = tokio::spawn(async move {
        let mut count = 0;
        while let Some(update) = receiver.recv().await {
            let vol = update
                .volume
                .map(|v| format!(" (volume: {v})"))
                .unwrap_or_default();
            println!(
                "[{}] {} @ {}{}",
                update.ts,
                update.instrument,
                display_opt(update.price.as_ref()),
                vol
            );
            count += 1;
            if count >= 1000 {
                break;
            }
        }
        println!("Finished streaming after {count} updates.");
    });

    tokio::select! {
        () = tokio::time::sleep(StdDuration::from_secs(10)) => {
            println!("Stopping stream due to timeout.");
            handle.stop().await;
        }
        _ = stream_task => {
            println!("Stream task completed on its own.");
        }
    };
    Ok(())
}

async fn section_news(client: &YfClient) -> Result<(), YfError> {
    let tesla_news = Ticker::new(client, "TSLA");
    let articles = tesla_news
        .news_builder()
        .tab(NewsTab::PressReleases)
        .count(5)
        .fetch()
        .await?;
    println!("\n--- Latest 5 Press Releases for TSLA ---");
    for article in articles {
        println!(
            "- {} by {}",
            article.title,
            article.publisher.unwrap_or_else(|| "Unknown".to_string())
        );
    }
    Ok(())
}