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;
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)
}
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)
}
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" => ×tamp,
"open" => &open,
"high" => &high,
"low" => &low,
"close" => &close,
"volume" => &volume,
"adjclose" => &adjclose
)?;
let mask = df.column("adjclose")?.as_materialized_series().gt(0.0)?;
let df = df.filter(&mask)?;
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)
}
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 ×tamps {
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,
})
}
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)
}