use super::{PriceSource, user_agent};
use crate::cmd::price::{PriceRequest, PriceResponse};
use anyhow::{Context, Result};
use std::time::Duration;
#[derive(Debug)]
pub struct YahooFinanceSource {}
impl YahooFinanceSource {
pub const fn new(_timeout: Duration) -> Self {
Self {}
}
fn build_url(&self, symbol: &str) -> String {
format!("https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?interval=1d&range=1d")
}
}
impl PriceSource for YahooFinanceSource {
fn name(&self) -> &'static str {
"yahoo"
}
fn description(&self) -> &'static str {
"Yahoo Finance - stocks, ETFs, crypto, forex"
}
fn fetch_price(&self, request: &PriceRequest) -> Result<PriceResponse> {
let url = self.build_url(&request.ticker);
let mut response = ureq::get(&url)
.header("User-Agent", user_agent())
.call()
.with_context(|| format!("Failed to fetch price for {}", request.ticker))?;
let json: serde_json::Value = response
.body_mut()
.read_json()
.with_context(|| format!("Failed to parse response for {}", request.ticker))?;
if let Some(chart) = json.get("chart")
&& let Some(error) = chart.get("error")
&& !error.is_null()
{
let description = error
.get("description")
.and_then(serde_json::Value::as_str)
.unwrap_or("Unknown error");
anyhow::bail!("Yahoo Finance error: {description}");
}
let meta = json
.get("chart")
.and_then(|c| c.get("result"))
.and_then(|r| r.get(0))
.and_then(|r| r.get("meta"))
.with_context(|| format!("Invalid response structure for {}", request.ticker))?;
let price_value = meta
.get("regularMarketPrice")
.with_context(|| format!("No price found for {}", request.ticker))?;
let price = crate::cmd::price::price_decimal_from_json(price_value)
.with_context(|| format!("Invalid price for {}", request.ticker))?;
let currency = meta
.get("currency")
.and_then(serde_json::Value::as_str)
.unwrap_or(&request.currency)
.to_string();
let date = request.date.unwrap_or_else(|| jiff::Zoned::now().date());
Ok(PriceResponse {
price,
currency,
date,
source: self.name().to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_url() {
let source = YahooFinanceSource::new(Duration::from_secs(30));
let url = source.build_url("AAPL");
assert!(url.contains("AAPL"));
assert!(url.contains("query1.finance.yahoo.com"));
}
#[test]
fn test_source_metadata() {
let source = YahooFinanceSource::new(Duration::from_secs(30));
assert_eq!(source.name(), "yahoo");
assert!(!source.requires_api_key());
assert!(source.description().contains("Yahoo"));
}
}