diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..74eeefd --- /dev/null +++ b/flake.lock @@ -0,0 +1,82 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1772674223, + "narHash": "sha256-/suKbHSaSmuC9UY7G0VRQ3aO+QKqxAQPQ19wG7QNkF8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "66d9241e3dc2296726dc522e62dbfe89c7b449f3", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1772679930, + "narHash": "sha256-FxYmdacqrdDVeE9QqZKTIpNLjv2B8GSKssgwlZuTR98=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "9b741db17141331fdb26270a1b66b81be8be9edd", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/src/api/yahoo.rs b/src/api/yahoo.rs index 43c202d..9e75262 100644 --- a/src/api/yahoo.rs +++ b/src/api/yahoo.rs @@ -6,7 +6,9 @@ use crate::api::types::{Interval, Ohlc, Period, Quote}; use crate::api::MarketDataProvider; use crate::error::IdxError; -const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"; +const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"; +// query2 works without crumb/cookie auth from datacenter IPs; query1 requires consent cookies +const BASE_URL: &str = "https://query2.finance.yahoo.com"; pub struct YahooProvider { agent: ureq::Agent, @@ -19,33 +21,18 @@ impl YahooProvider { } } - fn crumb(&self) -> Result { - let resp = self - .agent - .get("https://query1.finance.yahoo.com/v1/test/getcrumb") - .header("User-Agent", USER_AGENT) - .call() - .map_err(|e| IdxError::Http(e.to_string()))?; - - let mut body = resp.into_body(); - body.read_to_string() - .map(|s| s.trim().to_string()) - .map_err(|e| IdxError::Http(e.to_string())) - } - - fn chart_url(symbol: &str, period: &Period, interval: &Interval, crumb: &str) -> String { + fn chart_url(symbol: &str, period: &Period, interval: &Interval) -> String { format!( - "https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?range={}&interval={}&crumb={crumb}", + "{BASE_URL}/v8/finance/chart/{symbol}?range={}&interval={}", period.as_str(), interval.as_str() ) } fn fetch_chart(&self, symbol: &str, period: &Period, interval: &Interval) -> Result { - let crumb = self.crumb()?; let mut wait = Duration::from_millis(250); - for _ in 0..3 { - let url = Self::chart_url(symbol, period, interval, &crumb); + for attempt in 0..3 { + let url = Self::chart_url(symbol, period, interval); let response = self .agent .get(&url) @@ -59,8 +46,10 @@ impl YahooProvider { .map_err(|e| IdxError::ParseError(e.to_string())); } Err(ureq::Error::StatusCode(429)) => { - std::thread::sleep(wait + jitter()); - wait *= 2; + if attempt < 2 { + std::thread::sleep(wait + jitter()); + wait *= 2; + } } Err(e) => return Err(IdxError::Http(e.to_string())), } @@ -98,7 +87,7 @@ fn parse_quote(symbol: &str, chart: &ChartResponse) -> Result { .ok_or(IdxError::ProviderUnavailable)?; let meta = result.meta.as_ref().ok_or(IdxError::ProviderUnavailable)?; let price = meta.regular_market_price.ok_or(IdxError::SymbolNotFound(symbol.to_string()))?; - let prev_close = meta.previous_close; + let prev_close = meta.previous_close.or(meta.chart_previous_close); let change = prev_close.map_or(0.0, |p| price - p); let change_pct = prev_close.map_or(0.0, |p| if p != 0.0 { (change / p) * 100.0 } else { 0.0 }); @@ -192,11 +181,15 @@ struct ChartResult { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] +#[allow(dead_code)] struct ChartMeta { symbol: Option, regular_market_price: Option, previous_close: Option, + chart_previous_close: Option, regular_market_volume: Option, + regular_market_day_high: Option, + regular_market_day_low: Option, market_cap: Option, fifty_two_week_high: Option, fifty_two_week_low: Option,