use async_trait::async_trait;
use reqwest::Client;
use std::collections::HashMap;
use crate::core::types::*;
use crate::core::traits::*;
use super::endpoints::*;
use super::auth::*;
use super::parser::*;
pub struct YahooFinanceConnector {
client: Client,
auth: YahooFinanceAuth,
urls: YahooFinanceUrls,
}
impl YahooFinanceConnector {
pub fn new() -> Self {
Self {
client: Client::new(),
auth: YahooFinanceAuth::new(),
urls: YahooFinanceUrls::default(),
}
}
pub fn with_auth(auth: YahooFinanceAuth) -> Self {
Self {
client: Client::new(),
auth,
urls: YahooFinanceUrls::default(),
}
}
pub fn from_env() -> Self {
Self {
client: Client::new(),
auth: YahooFinanceAuth::from_env(),
urls: YahooFinanceUrls::default(),
}
}
pub fn auth_mut(&mut self) -> &mut YahooFinanceAuth {
&mut self.auth
}
pub async fn obtain_crumb(&mut self) -> ExchangeResult<String> {
if self.auth.cookie.is_none() {
return Err(ExchangeError::Auth(
"Cookie required to obtain crumb. Visit https://finance.yahoo.com first.".to_string()
));
}
let url = YahooFinanceEndpoint::GetCrumb.url(self.urls.rest_base, None);
let mut headers = HashMap::new();
self.auth.sign_headers(&mut headers);
let mut request = self.client.get(&url);
for (key, value) in headers {
request = request.header(key, value);
}
let response = request
.send()
.await
.map_err(|e| ExchangeError::Network(format!("Failed to get crumb: {}", e)))?;
if !response.status().is_success() {
let status_code = response.status().as_u16() as i32;
return Err(ExchangeError::Api {
code: status_code,
message: format!("Failed to get crumb: HTTP {}", status_code)
});
}
let crumb_text = response
.text()
.await
.map_err(|e| ExchangeError::Parse(format!("Failed to read crumb: {}", e)))?;
let crumb = YahooFinanceParser::parse_crumb(&crumb_text)?;
self.auth.set_crumb(&crumb);
Ok(crumb)
}
async fn get(
&self,
endpoint: YahooFinanceEndpoint,
symbol: Option<&str>,
mut params: HashMap<String, String>,
) -> ExchangeResult<serde_json::Value> {
let url = endpoint.url(self.urls.rest_base, symbol);
let mut headers = HashMap::new();
self.auth.sign_headers(&mut headers);
if endpoint.requires_crumb() {
self.auth.sign_query(&mut params);
}
let mut request = self.client.get(&url);
for (key, value) in headers {
request = request.header(key, value);
}
if !params.is_empty() {
request = request.query(¶ms);
}
let response = request
.send()
.await
.map_err(|e| ExchangeError::Network(format!("Request failed: {}", e)))?;
if response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
return Err(ExchangeError::RateLimit);
}
if !response.status().is_success() {
let status = response.status();
let status_code = status.as_u16() as i32;
let body = response.text().await.unwrap_or_default();
return Err(ExchangeError::Api {
code: status_code,
message: format!("HTTP {} - {}", status, body)
});
}
let json = response
.json()
.await
.map_err(|e| ExchangeError::Parse(format!("JSON parse error: {}", e)))?;
YahooFinanceParser::check_error(&json)?;
Ok(json)
}
async fn get_quote_internal(&self, yahoo_symbol: &str) -> ExchangeResult<serde_json::Value> {
self.get(YahooFinanceEndpoint::Chart, Some(yahoo_symbol), HashMap::new()).await
}
}
impl Default for YahooFinanceConnector {
fn default() -> Self {
Self::new()
}
}
impl ExchangeIdentity for YahooFinanceConnector {
fn exchange_name(&self) -> &'static str {
"yahoo_finance"
}
fn exchange_id(&self) -> ExchangeId {
ExchangeId::YahooFinance
}
fn is_testnet(&self) -> bool {
false }
fn supported_account_types(&self) -> Vec<AccountType> {
vec![AccountType::Spot]
}
}
#[async_trait]
impl MarketData for YahooFinanceConnector {
async fn get_price(
&self,
symbol: Symbol,
_account_type: AccountType,
) -> ExchangeResult<Price> {
let yahoo_symbol = format_symbol(&symbol.base, &symbol.quote);
let response = self.get_quote_internal(&yahoo_symbol).await?;
YahooFinanceParser::parse_price(&response)
}
async fn get_ticker(
&self,
symbol: Symbol,
_account_type: AccountType,
) -> ExchangeResult<Ticker> {
let yahoo_symbol = format_symbol(&symbol.base, &symbol.quote);
let response = self.get_quote_internal(&yahoo_symbol).await?;
YahooFinanceParser::parse_ticker(&response, &yahoo_symbol)
}
async fn get_orderbook(
&self,
_symbol: Symbol,
_depth: Option<u16>,
_account_type: AccountType,
) -> ExchangeResult<OrderBook> {
Err(ExchangeError::UnsupportedOperation(
"Yahoo Finance does not provide orderbook data - data feed only".to_string(),
))
}
async fn get_klines(
&self,
symbol: Symbol,
interval: &str,
limit: Option<u16>,
_account_type: AccountType,
_end_time: Option<i64>,
) -> ExchangeResult<Vec<Kline>> {
let yahoo_symbol = format_symbol(&symbol.base, &symbol.quote);
let yahoo_interval = map_chart_interval(interval);
let mut params = HashMap::new();
params.insert("interval".to_string(), yahoo_interval.to_string());
if let Some(lim) = limit {
let range = match interval {
"1m" | "2m" | "5m" => format!("{}d", (lim as f64 / 390.0).ceil()), "15m" => format!("{}d", (lim as f64 / 26.0).ceil()), "30m" => format!("{}d", (lim as f64 / 13.0).ceil()), "1h" => format!("{}d", (lim as f64 / 6.5).ceil()), "1d" => format!("{}d", lim),
"1wk" => format!("{}mo", (lim as f64 / 4.0).ceil()),
"1mo" => format!("{}mo", lim),
_ => format!("{}d", lim),
};
params.insert("range".to_string(), range);
} else {
params.insert("range".to_string(), "1mo".to_string());
}
let response = self
.get(
YahooFinanceEndpoint::Chart,
Some(&yahoo_symbol),
params,
)
.await?;
YahooFinanceParser::parse_klines(&response)
}
async fn ping(&self) -> ExchangeResult<()> {
self.get(YahooFinanceEndpoint::MarketSummary, None, HashMap::new())
.await?;
Ok(())
}
}
#[async_trait]
impl Trading for YahooFinanceConnector {
async fn place_order(&self, _req: OrderRequest) -> ExchangeResult<PlaceOrderResponse> {
Err(ExchangeError::UnsupportedOperation(
"Yahoo Finance is a data provider - trading not supported".to_string()
))
}
async fn cancel_order(&self, _req: CancelRequest) -> ExchangeResult<Order> {
Err(ExchangeError::UnsupportedOperation(
"Yahoo Finance is a data provider - trading not supported".to_string()
))
}
async fn get_order(
&self,
_symbol: &str,
_order_id: &str,
_account_type: AccountType,
) -> ExchangeResult<Order> {
Err(ExchangeError::UnsupportedOperation(
"Yahoo Finance is a data provider - trading not supported".to_string()
))
}
async fn get_open_orders(
&self,
_symbol: Option<&str>,
_account_type: AccountType,
) -> ExchangeResult<Vec<Order>> {
Err(ExchangeError::UnsupportedOperation(
"Yahoo Finance is a data provider - trading not supported".to_string()
))
}
async fn get_order_history(
&self,
_filter: OrderHistoryFilter,
_account_type: AccountType,
) -> ExchangeResult<Vec<Order>> {
Err(ExchangeError::UnsupportedOperation(
"Yahoo Finance is a data provider - trading not supported".to_string()
))
}
}
#[async_trait]
impl Account for YahooFinanceConnector {
async fn get_balance(&self, query: BalanceQuery) -> ExchangeResult<Vec<Balance>> {
let _asset = query.asset.clone();
let _account_type = query.account_type;
Err(ExchangeError::UnsupportedOperation(
"Yahoo Finance is a data provider - account operations not supported".to_string(),
))
}
async fn get_account_info(&self, _account_type: AccountType) -> ExchangeResult<AccountInfo> {
Err(ExchangeError::UnsupportedOperation(
"Yahoo Finance is a data provider - account operations not supported".to_string(),
))
}
async fn get_fees(&self, _symbol: Option<&str>) -> ExchangeResult<FeeInfo> {
Err(ExchangeError::UnsupportedOperation(
"Yahoo Finance is a data provider - account operations not supported".to_string()
))
}
}
#[async_trait]
impl Positions for YahooFinanceConnector {
async fn get_positions(&self, _query: PositionQuery) -> ExchangeResult<Vec<Position>> {
Err(ExchangeError::UnsupportedOperation(
"Yahoo Finance is a data provider - position tracking not supported".to_string()
))
}
async fn get_funding_rate(
&self,
_symbol: &str,
_account_type: AccountType,
) -> ExchangeResult<FundingRate> {
Err(ExchangeError::UnsupportedOperation(
"Yahoo Finance is a data provider - position tracking not supported".to_string()
))
}
async fn modify_position(&self, _req: PositionModification) -> ExchangeResult<()> {
Err(ExchangeError::UnsupportedOperation(
"Yahoo Finance is a data provider - position tracking not supported".to_string()
))
}
}
impl YahooFinanceConnector {
pub async fn get_market_summary(&self) -> ExchangeResult<serde_json::Value> {
self.get(YahooFinanceEndpoint::MarketSummary, None, HashMap::new())
.await
}
pub async fn search_symbols(
&self,
query: &str,
quotes_count: Option<u16>,
) -> ExchangeResult<serde_json::Value> {
let mut params = HashMap::new();
params.insert("q".to_string(), query.to_string());
params.insert(
"quotesCount".to_string(),
quotes_count.unwrap_or(10).to_string(),
);
params.insert("enableFuzzyQuery".to_string(), "true".to_string());
self.get(YahooFinanceEndpoint::Search, None, params).await
}
pub async fn get_quote_summary(
&self,
symbol: &str,
modules: &str,
) -> ExchangeResult<serde_json::Value> {
let mut params = HashMap::new();
params.insert("modules".to_string(), modules.to_string());
self.get(YahooFinanceEndpoint::QuoteSummary, Some(symbol), params)
.await
}
pub async fn get_asset_profile(&self, symbol: &str) -> ExchangeResult<serde_json::Value> {
self.get_quote_summary(symbol, quote_summary_modules::ASSET_PROFILE)
.await
}
pub async fn get_financial_data(&self, symbol: &str) -> ExchangeResult<serde_json::Value> {
self.get_quote_summary(symbol, quote_summary_modules::FINANCIAL_DATA)
.await
}
pub async fn get_earnings(&self, symbol: &str) -> ExchangeResult<serde_json::Value> {
self.get_quote_summary(symbol, quote_summary_modules::EARNINGS)
.await
}
pub async fn get_options_chain(
&self,
symbol: &str,
expiration_date: Option<i64>,
) -> ExchangeResult<serde_json::Value> {
let mut params = HashMap::new();
if let Some(date) = expiration_date {
params.insert("date".to_string(), date.to_string());
}
self.get(YahooFinanceEndpoint::Options, Some(symbol), params)
.await
}
pub async fn download_history_csv(
&self,
symbol: &str,
period1: i64,
period2: i64,
interval: &str,
) -> ExchangeResult<String> {
if !self.auth.has_download_auth() {
return Err(ExchangeError::Auth(
"Cookie and crumb required for historical download".to_string(),
));
}
let mut params = HashMap::new();
params.insert("period1".to_string(), period1.to_string());
params.insert("period2".to_string(), period2.to_string());
params.insert("interval".to_string(), interval.to_string());
params.insert("events".to_string(), "history".to_string());
self.auth.sign_query(&mut params);
let url = YahooFinanceEndpoint::DownloadHistory.url(self.urls.rest_base, Some(symbol));
let mut headers = HashMap::new();
self.auth.sign_headers(&mut headers);
let mut request = self.client.get(&url);
for (key, value) in headers {
request = request.header(key, value);
}
request = request.query(¶ms);
let response = request
.send()
.await
.map_err(|e| ExchangeError::Network(format!("Request failed: {}", e)))?;
if !response.status().is_success() {
let status_code = response.status().as_u16() as i32;
return Err(ExchangeError::Api {
code: status_code,
message: format!("HTTP {} - download failed", status_code)
});
}
response
.text()
.await
.map_err(|e| ExchangeError::Parse(format!("Failed to read CSV: {}", e)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_connector_creation() {
let connector = YahooFinanceConnector::new();
assert_eq!(connector.exchange_name(), "yahoo_finance");
assert_eq!(connector.exchange_id(), ExchangeId::YahooFinance);
}
#[test]
fn test_supported_account_types() {
let connector = YahooFinanceConnector::new();
let types = connector.supported_account_types();
assert_eq!(types, vec![AccountType::Spot]);
}
}