finalytics 0.9.0

A rust library for financial data analysis
Documentation
use crate::data::yahoo::config::{
    Interval, OptionContract, Options, Quote, StatementFrequency, StatementType, TickerSummaryStats,
};
use crate::data::yahoo::financials::{
    balance_sheet, cashflow_statement, financial_ratios, income_statement,
};
use crate::data::yahoo::web::get_json_response;
use crate::utils::date_utils::{
    round_datetime_to_day, round_datetime_to_hour, round_datetime_to_minute, time_to_maturity,
    to_date, to_datetime, to_timestamp,
};
use anyhow::Context;
use chrono::{DateTime, NaiveDateTime};
use polars::prelude::*;
use std::error::Error;

/// Fetches Current Ticker Price from Yahoo Finance
pub async fn get_quote(symbol: &str) -> Result<Quote, Box<dyn Error>> {
    let url = format!("https://query2.finance.yahoo.com/v7/finance/options/{symbol}");
    let result = get_json_response(url)
        .await
        .with_context(|| format!("fetching quote for ticker '{symbol}'"))?;
    let value = &result["optionChain"]["result"][0]["quote"].to_string();
    let quote: Quote = serde_json::from_value(value.parse()?)
        .map_err(|e| format!("Failed to deserialize quote for ticker '{symbol}': {e}"))?;
    Ok(quote)
}

/// Fetches Ticker Current Summary Stats from Yahoo Finance
pub async fn get_ticker_stats(symbol: &str) -> Result<TickerSummaryStats, Box<dyn Error>> {
    let url = format!("https://query2.finance.yahoo.com/v10/finance/quoteSummary/{symbol}?modules=defaultKeyStatistics,financialData,summaryDetail");
    let result = get_json_response(url)
        .await
        .with_context(|| format!("fetching summary stats for ticker '{symbol}'"))?;
    let value = &result["quoteSummary"]["result"][0].to_string();
    let stats: TickerSummaryStats = serde_json::from_value(value.parse()?)
        .map_err(|e| format!("Failed to deserialize summary stats for ticker '{symbol}': {e}"))?;
    Ok(stats)
}

/// Returns the Ticker OHLCV Data from Yahoo Finance for a given time range
pub async fn get_chart(
    symbol: &str,
    start_date: &str,
    end_date: &str,
    interval: Interval,
) -> Result<DataFrame, Box<dyn Error>> {
    let period1 = to_timestamp(start_date)?;
    let period2 = to_timestamp(end_date)?;
    let url = format!(
        "https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?period1={period1}&period2={period2}&interval={interval}"
    );
    let result = get_json_response(url)
        .await
        .with_context(|| format!("fetching chart data for ticker '{symbol}' ({start_date} to {end_date}, interval {interval})"))?;

    let value = &result["chart"]["result"][0];
    let timestamp = &value["timestamp"]
        .as_array()
        .ok_or(format!("timestamp array not found for {symbol}: {result}"))?
        .iter()
        .filter_map(|ts| {
            let timestamp = ts.as_i64()?;
            let dt = DateTime::from_timestamp(timestamp, 0)?;
            Some(match interval {
                Interval::OneDay
                | Interval::FiveDays
                | Interval::OneWeek
                | Interval::OneMonth
                | Interval::ThreeMonths => round_datetime_to_day(dt),
                Interval::SixtyMinutes | Interval::OneHour => round_datetime_to_hour(dt),
                Interval::NinetyMinutes
                | Interval::ThirtyMinutes
                | Interval::FifteenMinutes
                | Interval::FiveMinutes
                | Interval::TwoMinutes => round_datetime_to_minute(dt),
            })
        })
        .collect::<Vec<NaiveDateTime>>();

    let indicators = &value["indicators"]["quote"][0];

    let open = indicators["open"]
        .as_array()
        .ok_or(format!("open array not found for {symbol}: {result}"))?
        .iter()
        .map(|o| o.as_f64().unwrap_or(0.0))
        .collect::<Vec<f64>>();

    let high = indicators["high"]
        .as_array()
        .ok_or(format!("high array not found for {symbol}: {result}"))?
        .iter()
        .map(|h| h.as_f64().unwrap_or(0.0))
        .collect::<Vec<f64>>();

    let low = indicators["low"]
        .as_array()
        .ok_or(format!("low array not found for {symbol}: {result}"))?
        .iter()
        .map(|l| l.as_f64().unwrap_or(0.0))
        .collect::<Vec<f64>>();

    let close = indicators["close"]
        .as_array()
        .ok_or(format!("close array not found for {symbol}: {result}"))?
        .iter()
        .map(|c| c.as_f64().unwrap_or(0.0))
        .collect::<Vec<f64>>();

    let volume = indicators["volume"]
        .as_array()
        .ok_or(format!("volume array not found for {symbol}: {result}"))?
        .iter()
        .map(|v| v.as_f64().unwrap_or(0.0))
        .collect::<Vec<f64>>();

    let adjclose = &value["indicators"]["adjclose"][0]["adjclose"]
        .as_array()
        .unwrap_or_else(|| {
            indicators["close"]
                .as_array()
                .expect("close array not found")
        })
        .iter()
        .map(|c| c.as_f64().unwrap_or(0.0))
        .collect::<Vec<f64>>();

    let df = df!(
        "timestamp" => &timestamp,
        "open" => &open,
        "high" => &high,
        "low" => &low,
        "close" => &close,
        "volume" => &volume,
        "adjclose" => &adjclose
    )?;

    // check if any adjclose values are 0.0
    let mask = df.column("adjclose")?.as_materialized_series().gt(0.0)?;
    let df = df.filter(&mask)?;

    // check id any returned dates greater than end date
    let dt = to_datetime(end_date)?;
    let mask = df["timestamp"]
        .datetime()?
        .as_datetime_iter()
        .map(|x| x.map_or(false, |v| v < dt))
        .collect();
    let df = df.filter(&mask)?;
    Ok(df)
}

/// Returns Ticker Option Chain Data from Yahoo Finance for all available expirations
pub async fn get_options(symbol: &str) -> Result<Options, Box<dyn Error>> {
    let url = format!("https://query2.finance.yahoo.com/v7/finance/options/{symbol}");
    let result = get_json_response(url)
        .await
        .with_context(|| format!("fetching options chain for ticker '{symbol}'"))?;

    let ticker_price = result["optionChain"]["result"][0]["quote"]["regularMarketPrice"]
        .as_f64()
        .ok_or(format!(
            "Failed to parse regularMarketPrice for ticker '{symbol}'"
        ))?;

    let expiration_dates = result["optionChain"]["result"][0]["expirationDates"]
        .as_array()
        .ok_or(format!(
            "Failed to parse expirationDates for ticker '{symbol}'"
        ))?;

    let strike_values = result["optionChain"]["result"][0]["strikes"]
        .as_array()
        .ok_or(format!("Failed to parse strikes for ticker '{symbol}'"))?;

    let strikes = strike_values
        .iter()
        .map(|x| x.as_f64().ok_or("Failed to parse strike as f64"))
        .collect::<Result<Vec<f64>, _>>()?;

    let timestamps = expiration_dates
        .iter()
        .map(|x| x.as_i64().ok_or("Failed to parse expiration date as i64"))
        .collect::<Result<Vec<i64>, _>>()?;

    let ttms = timestamps
        .iter()
        .map(|x| time_to_maturity(*x))
        .collect::<Vec<f64>>();

    let expiration_dates = timestamps
        .iter()
        .map(|x| to_date(*x))
        .collect::<Vec<String>>();

    let mut options_chain = DataFrame::default();

    for t in &timestamps {
        let url = format!("https://query2.finance.yahoo.com/v7/finance/options/{symbol}?date={t}");
        let result = get_json_response(url)
            .await
            .with_context(|| format!("fetching options for ticker '{symbol}' at expiration {t}"))?;
        let expiration = to_date(*t);
        let ttm = time_to_maturity(*t);

        let calls = &result["optionChain"]["result"][0]["options"][0]["calls"];
        let calls_vec: Vec<OptionContract> = serde_json::from_value(calls.clone())
            .map_err(|e| format!("Failed to deserialize calls for ticker '{symbol}' expiration {expiration}: {e}"))?;

        let calls_df = df!(
            "expiration" => calls_vec.iter().map(|_| &*expiration).collect::<Vec<&str>>(),
            "ttm" => calls_vec.iter().map(|_| ttm).collect::<Vec<f64>>(),
            "type" => calls_vec.iter().map(|_| "call").collect::<Vec<&str>>(),
            "contractSymbol" => calls_vec.iter().map(|x| x.contractSymbol.as_str()).collect::<Vec<&str>>(),
            "strike" => calls_vec.iter().map(|x| x.strike).collect::<Vec<f64>>(),
            "currency" => calls_vec.iter().map(|x| x.currency.as_str()).collect::<Vec<&str>>(),
            "lastPrice" => calls_vec.iter().map(|x| x.lastPrice).collect::<Vec<f64>>(),
            "change" => calls_vec.iter().map(|x| x.change).collect::<Vec<f64>>(),
            "percentChange" => calls_vec.iter().map(|x| x.percentChange).collect::<Vec<f64>>(),
            "openInterest" => calls_vec.iter().map(|x| x.openInterest).collect::<Vec<f64>>(),
            "bid" => calls_vec.iter().map(|x| x.bid).collect::<Vec<f64>>(),
            "ask" => calls_vec.iter().map(|x| x.ask).collect::<Vec<f64>>(),
            "contractSize" => calls_vec.iter().map(|x| x.contractSize.as_str()).collect::<Vec<&str>>(),
            "lastTradeDate" => calls_vec.iter().map(|x| DateTime::from_timestamp(x.lastTradeDate, 0).unwrap_or_default().naive_local()).collect::<Vec<NaiveDateTime>>(),
            "impliedVolatility" => calls_vec.iter().map(|x| x.impliedVolatility).collect::<Vec<f64>>(),
            "inTheMoney" => calls_vec.iter().map(|x| x.inTheMoney).collect::<Vec<bool>>(),
        )?;

        let puts = &result["optionChain"]["result"][0]["options"][0]["puts"];
        let puts_vec: Vec<OptionContract> = serde_json::from_value(puts.clone()).map_err(|e| {
            format!("Failed to deserialize puts for ticker '{symbol}' expiration {expiration}: {e}")
        })?;

        let puts_df = df!(
            "expiration" => puts_vec.iter().map(|_| &*expiration).collect::<Vec<&str>>(),
            "ttm" => puts_vec.iter().map(|_| ttm).collect::<Vec<f64>>(),
            "type" => puts_vec.iter().map(|_| "put").collect::<Vec<&str>>(),
            "contractSymbol" => puts_vec.iter().map(|x| x.contractSymbol.as_str()).collect::<Vec<&str>>(),
            "strike" => puts_vec.iter().map(|x| x.strike).collect::<Vec<f64>>(),
            "currency" => puts_vec.iter().map(|x| x.currency.as_str()).collect::<Vec<&str>>(),
            "lastPrice" => puts_vec.iter().map(|x| x.lastPrice).collect::<Vec<f64>>(),
            "change" => puts_vec.iter().map(|x| x.change).collect::<Vec<f64>>(),
            "percentChange" => puts_vec.iter().map(|x| x.percentChange).collect::<Vec<f64>>(),
            "openInterest" => puts_vec.iter().map(|x| x.openInterest).collect::<Vec<f64>>(),
            "bid" => puts_vec.iter().map(|x| x.bid).collect::<Vec<f64>>(),
            "ask" => puts_vec.iter().map(|x| x.ask).collect::<Vec<f64>>(),
            "contractSize" => puts_vec.iter().map(|x| x.contractSize.as_str()).collect::<Vec<&str>>(),
            "lastTradeDate" => puts_vec.iter().map(|x| DateTime::from_timestamp(x.lastTradeDate, 0).unwrap_or_default().naive_local()).collect::<Vec<NaiveDateTime>>(),
            "impliedVolatility" => puts_vec.iter().map(|x| x.impliedVolatility).collect::<Vec<f64>>(),
            "inTheMoney" => puts_vec.iter().map(|x| x.inTheMoney).collect::<Vec<bool>>(),
        )?;

        let df = calls_df.vstack(&puts_df)?;
        options_chain.vstack_mut(&df)?;
    }

    Ok(Options {
        ticker_price,
        expiration_dates,
        ttms,
        strikes,
        chain: options_chain,
    })
}

/// Returns Ticker Financials from Yahoo Finance for a given statement type and frequency
///
/// # Arguments
///
/// * `statement_type` - StatementType
/// * `frequency` - StatementFrequency
/// * `formatted` - Optional boolean to return formatted values
///
/// # Returns
///
/// * `DataFrame` - Ticker Financials
pub async fn get_financials(
    symbol: &str,
    statement_type: StatementType,
    frequency: StatementFrequency,
    formatted: Option<bool>,
) -> Result<DataFrame, Box<dyn Error>> {
    let stmt_name = match statement_type {
        StatementType::IncomeStatement => "income statement",
        StatementType::BalanceSheet => "balance sheet",
        StatementType::CashFlowStatement => "cashflow statement",
        StatementType::FinancialRatios => "financial ratios",
    };
    let df = match statement_type {
        StatementType::IncomeStatement => income_statement(symbol, frequency, formatted).await,
        StatementType::BalanceSheet => balance_sheet(symbol, frequency, formatted).await,
        StatementType::CashFlowStatement => cashflow_statement(symbol, frequency, formatted).await,
        StatementType::FinancialRatios => financial_ratios(symbol, frequency).await,
    }
    .map_err(|e| format!("fetching {stmt_name} for ticker '{symbol}' ({frequency:?}): {e}"))?;
    Ok(df)
}