use time::OffsetDateTime;
use yahoo_finance_api::YahooConnector;
use super::provider::ChainQuote;
use super::provider::MarketDataProvider;
use super::provider::OptionChain;
use super::provider::PriceHistory;
pub struct YahooProvider {
connector: YahooConnector,
}
impl YahooProvider {
pub fn new() -> anyhow::Result<Self> {
let connector =
YahooConnector::new().map_err(|e| anyhow::anyhow!("YahooConnector init failed: {e}"))?;
Ok(Self { connector })
}
}
impl MarketDataProvider for YahooProvider {
fn price_history(&self, symbol: &str, start: f64, end: f64) -> anyhow::Result<PriceHistory> {
let start_dt = OffsetDateTime::from_unix_timestamp(start as i64)
.map_err(|e| anyhow::anyhow!("invalid start timestamp {start}: {e}"))?;
let end_dt = OffsetDateTime::from_unix_timestamp(end as i64)
.map_err(|e| anyhow::anyhow!("invalid end timestamp {end}: {e}"))?;
let res = tokio_test::block_on(self.connector.get_quote_history(symbol, start_dt, end_dt))
.map_err(|e| anyhow::anyhow!("Yahoo get_quote_history failed for {symbol}: {e}"))?;
let quotes = res
.quotes()
.map_err(|e| anyhow::anyhow!("Yahoo quotes() failed for {symbol}: {e}"))?;
let mut history = PriceHistory {
symbol: symbol.to_string(),
timestamps: Vec::with_capacity(quotes.len()),
open: Vec::with_capacity(quotes.len()),
high: Vec::with_capacity(quotes.len()),
low: Vec::with_capacity(quotes.len()),
close: Vec::with_capacity(quotes.len()),
adj_close: Vec::with_capacity(quotes.len()),
volume: Vec::with_capacity(quotes.len()),
};
for q in "es {
history.timestamps.push(q.timestamp as f64);
history.open.push(q.open);
history.high.push(q.high);
history.low.push(q.low);
history.close.push(q.close);
history.adj_close.push(q.adjclose);
history.volume.push(q.volume);
}
Ok(history)
}
fn option_chain(&self, symbol: &str) -> anyhow::Result<OptionChain> {
let res = tokio_test::block_on(self.connector.search_options(symbol))
.map_err(|e| anyhow::anyhow!("Yahoo search_options failed for {symbol}: {e}"))?;
let result = res
.option_chain
.result
.first()
.ok_or_else(|| anyhow::anyhow!("Yahoo option_chain.result empty for {symbol}"))?;
let spot = result.quote.regular_market_price;
let now = OffsetDateTime::now_utc().unix_timestamp() as f64;
let options = result
.options
.first()
.ok_or_else(|| anyhow::anyhow!("Yahoo options array empty for {symbol}"))?;
let mut quotes = Vec::with_capacity(options.calls.len() + options.puts.len());
for (contracts, is_call) in [(&options.calls, true), (&options.puts, false)] {
for c in contracts {
let (Some(strike), Some(expiration)) = (c.strike, c.expiration) else {
continue;
};
let tau = (expiration as f64 - now) / (365.0 * 86_400.0);
if tau <= 0.0 {
continue;
}
quotes.push(ChainQuote {
strike,
tau,
last: c.last_price.unwrap_or(0.0),
bid: c.bid.unwrap_or(0.0),
ask: c.ask.unwrap_or(0.0),
implied_vol: c.implied_volatility.unwrap_or(0.0),
is_call,
});
}
}
Ok(OptionChain {
symbol: symbol.to_string(),
spot,
quotes,
})
}
}