yfinance-rs 0.1.1

Ergonomic Rust client for Yahoo Finance, supporting historical prices, real-time streaming, options, fundamentals, and more.
Documentation
use super::model::{
    InsiderRosterHolder, InsiderTransaction, InstitutionalHolder, MajorHolder,
    NetSharePurchaseActivity,
};
use super::wire::{RawNum, V10Result};
use crate::core::{
    YfClient, YfError,
    client::{CacheMode, RetryConfig},
    quotesummary,
};

const MODULES: &str = "institutionOwnership,fundOwnership,majorHoldersBreakdown,insiderTransactions,insiderHolders,netSharePurchaseActivity";

async fn fetch_holders_modules(
    client: &YfClient,
    symbol: &str,
    cache_mode: CacheMode,
    retry_override: Option<&RetryConfig>,
) -> Result<V10Result, YfError> {
    quotesummary::fetch_module_result(
        client,
        symbol,
        MODULES,
        "holders",
        cache_mode,
        retry_override,
    )
    .await
}

fn f<T: Copy>(r: Option<RawNum<T>>) -> Option<T> {
    r.and_then(|n| n.raw)
}

pub(super) async fn major_holders(
    client: &YfClient,
    symbol: &str,
    cache_mode: CacheMode,
    retry_override: Option<&RetryConfig>,
) -> Result<Vec<MajorHolder>, YfError> {
    let root = fetch_holders_modules(client, symbol, cache_mode, retry_override).await?;
    let breakdown = root
        .major_holders_breakdown
        .ok_or_else(|| YfError::Data("majorHoldersBreakdown missing".into()))?;

    let mut result = Vec::new();
    let percent_fmt = |v: Option<f64>| v.map(|p| format!("{:.2}%", p * 100.0)).unwrap_or_default();

    if let Some(v) = f(breakdown.insiders_percent_held) {
        result.push(MajorHolder {
            category: "% of Shares Held by All Insiders".into(),
            value: percent_fmt(Some(v)),
        });
    }
    if let Some(v) = f(breakdown.institutions_percent_held) {
        result.push(MajorHolder {
            category: "% of Shares Held by Institutions".into(),
            value: percent_fmt(Some(v)),
        });
    }
    if let Some(v) = f(breakdown.institutions_float_percent_held) {
        result.push(MajorHolder {
            category: "% of Float Held by Institutions".into(),
            value: percent_fmt(Some(v)),
        });
    }
    if let Some(v) = f(breakdown.institutions_count) {
        result.push(MajorHolder {
            category: "Number of Institutions Holding Shares".into(),
            value: v.to_string(),
        });
    }

    Ok(result)
}

fn map_ownership_list(node: Option<super::wire::OwnershipNode>) -> Vec<InstitutionalHolder> {
    node.and_then(|n| n.ownership_list)
        .unwrap_or_default()
        .into_iter()
        .map(|h| InstitutionalHolder {
            holder: h.organization.unwrap_or_default(),
            shares: f(h.shares).unwrap_or(0),
            date_reported: h.date_reported.and_then(|d| d.raw).unwrap_or(0),
            pct_held: f(h.pct_held).unwrap_or(0.0),
            value: f(h.value).unwrap_or(0),
        })
        .collect()
}

pub(super) async fn institutional_holders(
    client: &YfClient,
    symbol: &str,
    cache_mode: CacheMode,
    retry_override: Option<&RetryConfig>,
) -> Result<Vec<InstitutionalHolder>, YfError> {
    let root = fetch_holders_modules(client, symbol, cache_mode, retry_override).await?;
    Ok(map_ownership_list(root.institution_ownership))
}

pub(super) async fn mutual_fund_holders(
    client: &YfClient,
    symbol: &str,
    cache_mode: CacheMode,
    retry_override: Option<&RetryConfig>,
) -> Result<Vec<InstitutionalHolder>, YfError> {
    let root = fetch_holders_modules(client, symbol, cache_mode, retry_override).await?;
    Ok(map_ownership_list(root.fund_ownership))
}

pub(super) async fn insider_transactions(
    client: &YfClient,
    symbol: &str,
    cache_mode: CacheMode,
    retry_override: Option<&RetryConfig>,
) -> Result<Vec<InsiderTransaction>, YfError> {
    let root = fetch_holders_modules(client, symbol, cache_mode, retry_override).await?;
    let transactions = root
        .insider_transactions
        .and_then(|it| it.transactions)
        .unwrap_or_default();

    Ok(transactions
        .into_iter()
        .map(|t| InsiderTransaction {
            insider: t.insider.unwrap_or_default(),
            position: t.position.unwrap_or_default(),
            transaction: t.transaction.unwrap_or_default(),
            shares: f(t.shares).unwrap_or(0),
            value: f(t.value).unwrap_or(0),
            start_date: t.start_date.and_then(|d| d.raw).unwrap_or(0),
            url: t.url.unwrap_or_default(),
        })
        .collect())
}

pub(super) async fn insider_roster_holders(
    client: &YfClient,
    symbol: &str,
    cache_mode: CacheMode,
    retry_override: Option<&RetryConfig>,
) -> Result<Vec<InsiderRosterHolder>, YfError> {
    let root = fetch_holders_modules(client, symbol, cache_mode, retry_override).await?;
    let holders = root
        .insider_holders
        .and_then(|ih| ih.holders)
        .unwrap_or_default();

    Ok(holders
        .into_iter()
        .map(|h| InsiderRosterHolder {
            name: h.name.unwrap_or_default(),
            position: h.relation.unwrap_or_default(),
            most_recent_transaction: h.most_recent_transaction.unwrap_or_default(),
            latest_transaction_date: h.latest_transaction_date.and_then(|d| d.raw).unwrap_or(0),
            shares_owned_directly: f(h.shares_owned_directly).unwrap_or(0),
            position_direct_date: h.position_direct_date.and_then(|d| d.raw).unwrap_or(0),
        })
        .collect())
}

pub(super) async fn net_share_purchase_activity(
    client: &YfClient,
    symbol: &str,
    cache_mode: CacheMode,
    retry_override: Option<&RetryConfig>,
) -> Result<Option<NetSharePurchaseActivity>, YfError> {
    let root = fetch_holders_modules(client, symbol, cache_mode, retry_override).await?;
    Ok(root
        .net_share_purchase_activity
        .map(|n| NetSharePurchaseActivity {
            period: n.period.unwrap_or_default(),
            buy_info_shares: f(n.buy_info_shares).unwrap_or(0),
            buy_info_count: f(n.buy_info_count).unwrap_or(0),
            sell_info_shares: f(n.sell_info_shares).unwrap_or(0),
            sell_info_count: f(n.sell_info_count).unwrap_or(0),
            net_info_shares: f(n.net_info_shares).unwrap_or(0),
            net_info_count: f(n.net_info_count).unwrap_or(0),
            total_insider_shares: f(n.total_insider_shares).unwrap_or(0),
            net_percent_insider_shares: f(n.net_percent_insider_shares).unwrap_or(0.0),
        }))
}