eeyf/lib.rs
1//! This project provides a set of functions to receive data from the
2//! the [yahoo! finance](https://finance.yahoo.com) website via their API.
3//!
4//! This project is licensed under Apache 2.0 or MIT license (see files
5//! LICENSE-Apache2.0 and LICENSE-MIT).
6//!
7//! All requests to the yahoo API return ```async``` futures.
8//! Therefore, the functions need to be called from an ```async``` function with
9//! ```.await``` or via functions like ```block_on```. The examples are based on
10//! the ```tokio``` runtime applying the ```tokio-test``` crate.
11//!
12//! # Get the latest available quote:
13//! ```rust
14//! use eeyf as yahoo;
15//! use time::OffsetDateTime;
16//! use tokio_test;
17//!
18//! fn main() {
19//! let provider = yahoo::YahooConnector::new().unwrap();
20//! // get the latest quotes in 1 minute intervals
21//! let response = tokio_test::block_on(provider.get_latest_quotes("AAPL",
22//! "1d")).unwrap(); // extract just the latest valid quote summery
23//! // including timestamp,open,close,high,low,volume
24//! let quote = response.last_quote().unwrap();
25//! let time: OffsetDateTime =
26//! OffsetDateTime::from_unix_timestamp(quote.timestamp).unwrap(); println!
27//! ("At {} quote price of Apple was {}", time, quote.close); }
28//! ```
29//! # Get history of quotes for given time period:
30//! ```rust
31//! use eeyf as yahoo;
32//! use time::{macros::datetime, OffsetDateTime};
33//! use tokio_test;
34//!
35//! fn main() {
36//! let provider = yahoo::YahooConnector::new().unwrap();
37//! let start = datetime!(2020-1-1 0:00:00.00 UTC);
38//! let end = datetime!(2020-1-31 23:59:59.99 UTC);
39//! // returns historic quotes with daily interval
40//! let resp = tokio_test::block_on(provider.get_quote_history("AAPL",
41//! start, end)).unwrap(); let quotes = resp.quotes().unwrap();
42//! println!("Apple's quotes in January: {:?}", quotes);
43//! }
44//! ```
45//! # Get the history of quotes for time range
46//! Another method to retrieve a range of quotes is by requesting the quotes for
47//! a given period and lookup frequency. Here is an example retrieving the daily
48//! quotes for the last month: ```rust
49//! use eeyf as yahoo;
50//! use tokio_test;
51//!
52//! fn main() {
53//! let provider = yahoo::YahooConnector::new().unwrap();
54//! let response = tokio_test::block_on(provider.get_quote_range("AAPL", "1d", "1mo")).unwrap();
55//! let quotes = response.quotes().unwrap();
56//! println!("Apple's quotes of the last month: {:?}", quotes);
57//! }
58//! ```
59//!
60//! # Search for a ticker given a search string (e.g. company name):
61//! ```rust
62//! use eeyf as yahoo;
63//! use tokio_test;
64//!
65//! fn main() {
66//! let provider = yahoo::YahooConnector::new().unwrap();
67//! let resp = tokio_test::block_on(provider.search_ticker("Apple")).unwrap();
68//!
69//! let mut apple_found = false;
70//! println!("All tickers found while searching for 'Apple':");
71//! for item in resp.quotes {
72//! println!("{}", item.symbol)
73//! }
74//! }
75//! ```
76//! Some fields like `longname` are only optional and will be replaced by
77//! default values if missing (e.g. empty string). If you do not like this
78//! behavior, use `search_ticker_opt` instead which contains `Option<String>`
79//! fields, returning `None` if the field found missing in the response.
80
81#[cfg(feature = "debug")]
82extern crate serde_json_path_to_error as serde_json;
83
84use std::{sync::Arc, time::Duration};
85
86// re-export time crate
87pub use quotes::decimal::Decimal;
88use reqwest::{Client, ClientBuilder, Proxy};
89pub use time;
90use time::OffsetDateTime;
91
92mod quotes;
93pub mod rate_limiter;
94mod search_result;
95pub mod yahoo_error;
96
97// Builder and preset management
98pub mod builder;
99pub mod presets;
100
101// Enterprise modules
102pub mod circuit_breaker;
103pub mod connection_pool;
104pub mod enterprise;
105pub mod error_categories;
106pub mod observability;
107pub mod request_deduplication;
108pub mod response_cache;
109pub mod retry;
110
111// Phase 2: Observability & Configuration modules
112pub mod health;
113pub mod metrics;
114pub mod tracing;
115
116#[cfg(feature = "config-management")]
117pub mod config;
118
119#[cfg(feature = "config-management")]
120pub mod runtime_config;
121
122// Phase 3: Performance & Reliability modules
123#[cfg(feature = "performance-cache")]
124pub mod advanced_cache;
125#[cfg(feature = "performance-pool")]
126pub mod connection_pool_advanced;
127#[cfg(feature = "performance-rate-limit")]
128pub mod intelligent_rate_limit;
129#[cfg(feature = "performance-optimization")]
130pub mod performance_optimization;
131
132// Phase 4: WebSocket streaming for real-time data
133#[cfg(feature = "websocket-streaming")]
134pub mod websocket;
135
136// Phase 4: Batch operations for parallel fetching
137pub mod batch;
138
139// Phase 4: Symbol validation and lookup
140pub mod validation;
141
142// Phase 4: Market hours checking
143pub mod market_hours;
144
145// Phase 4.2: Stock screener API
146pub mod screener;
147
148// Phase 7: Production Hardening
149#[cfg(feature = "phase7")]
150pub mod security;
151
152#[cfg(feature = "phase7")]
153pub mod audit;
154
155#[cfg(feature = "phase7")]
156pub mod fallback;
157
158// Phase 8: Runtime Flexibility
159pub mod runtime;
160
161// Phase 9: Advanced Features
162#[cfg(feature = "phase9")]
163pub mod analytics;
164
165// Phase 4.3: Data processing features
166pub mod export;
167
168// EXPERIMENTAL MODULES (Temporarily Disabled)
169// These modules are under refactoring to work with both f64 and
170// rust_decimal::Decimal types. They will be re-enabled in a future release
171// (v0.2.0 or v0.1.1).
172//
173// To use these features now, enable the `decimal` feature in your Cargo.toml:
174// ```toml
175// [dependencies]
176// eeyf = { version = "0.1", features = ["decimal"] }
177// ```
178//
179// Or wait for the refactored versions that work with plain f64.
180//
181// pub mod timeseries; // Time series utilities (resampling, timezone handling)
182// pub mod transform; // Data transformation (OHLC aggregation, technical
183// indicators) pub mod validate; // Data validation (integrity checks,
184// anomaly detection)
185
186// Phase 5: Performance & Optimization modules
187#[cfg(feature = "phase5-compression")]
188pub mod compression;
189#[cfg(feature = "phase5-http2")]
190pub mod http2;
191#[cfg(feature = "phase5-limits")]
192pub mod limits;
193#[cfg(feature = "phase5-shutdown")]
194pub mod shutdown;
195
196// Builder and preset management
197pub use builder::YahooConnectorBuilder as EnterpriseYahooConnectorBuilder;
198// Enterprise features
199pub use circuit_breaker::{
200 CircuitBreaker, CircuitBreakerConfig, CircuitBreakerStats, CircuitState,
201};
202pub use connection_pool::{ConnectionPool, ConnectionPoolConfig, ConnectionStats};
203pub use enterprise::{
204 EnterpriseConfig, EnterpriseHealthStatus, EnterpriseMetrics, EnterpriseYahooConnector,
205};
206pub use error_categories::{ErrorCategorizer, ErrorCategory, ErrorInfo};
207pub use observability::{
208 HealthCheck, HealthStatus, LibraryMetrics, ObservabilityConfig, ObservabilityManager,
209 RequestContext,
210};
211pub use presets::{PresetConfig, PresetFormat, PresetManager};
212pub use quotes::{
213 AdjClose, AssetProfile, CapitalGain, CurrentTradingPeriod, DefaultKeyStatistics, Dividend,
214 ExtendedQuoteSummary, FinancialData, FinancialEvent, PeriodInfo, Quote, QuoteBlock, QuoteList,
215 QuoteType, Split, SummaryDetail, TradingPeriods, YChart, YMetaData, YQuoteBlock, YQuoteSummary,
216 YResponse, YSummaryData,
217};
218pub use rate_limiter::{RateLimitConfig, RateLimitError, RateLimitStatus, RateLimiter};
219pub use request_deduplication::{DeduplicationConfig, DeduplicationStats, RequestDeduplicator};
220pub use response_cache::{CacheStats, ResponseCache, ResponseCacheConfig};
221pub use retry::{RetryConfig, RetryPolicy, RetryStats};
222pub use search_result::{
223 YNewsItem, YOptionChain, YOptionChainData, YOptionChainResult, YOptionContract, YOptionDetails,
224 YQuote, YQuoteItem, YQuoteItemOpt, YSearchResult, YSearchResultOpt,
225};
226pub use yahoo_error::{ErrorContext, YahooError, YahooErrorCode, YahooErrorWithContext};
227
228const YCHART_URL: &str = "https://query1.finance.yahoo.com/v8/finance/chart";
229const YSEARCH_URL: &str = "https://query2.finance.yahoo.com/v1/finance/search";
230const Y_GET_COOKIE_URL: &str = "https://fc.yahoo.com";
231const Y_GET_CRUMB_URL: &str = "https://query1.finance.yahoo.com/v1/test/getcrumb";
232const Y_EARNINGS_URL: &str = "https://query1.finance.yahoo.com/v1/finance/visualization";
233
234// special yahoo hardcoded keys and headers
235const Y_COOKIE_REQUEST_HEADER: &str = "set-cookie";
236const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) \
237 Chrome/122.0.0.0 Safari/537.36";
238
239// Macros instead of constants,
240macro_rules! YCHART_PERIOD_QUERY {
241 () => {
242 "{url}/{symbol}?symbol={symbol}&period1={start}&period2={end}&interval={interval}&\
243 events=div|split|capitalGains"
244 };
245}
246macro_rules! YCHART_PERIOD_QUERY_PRE_POST {
247 () => {
248 "{url}/{symbol}?symbol={symbol}&period1={start}&period2={end}&interval={interval}&\
249 events=div|split|capitalGains&includePrePost={prepost}"
250 };
251}
252macro_rules! YCHART_RANGE_QUERY {
253 () => {
254 "{url}/{symbol}?symbol={symbol}&interval={interval}&range={range}&\
255 events=div|split|capitalGains"
256 };
257}
258macro_rules! YCHART_PERIOD_INTERVAL_QUERY {
259 () => {
260 "{url}/{symbol}?symbol={symbol}&range={range}&interval={interval}&includePrePost={prepost}"
261 };
262}
263macro_rules! YTICKER_QUERY {
264 () => {
265 "{url}?q={name}"
266 };
267}
268macro_rules! YQUOTE_SUMMARY_QUERY {
269 () => {
270 "https://query2.finance.yahoo.com/v10/finance/quoteSummary/{symbol}?modules=financialData,quoteType,defaultKeyStatistics,assetProfile,summaryDetail&corsDomain=finance.yahoo.com&formatted=false&symbol={symbol}&crumb={crumb}"
271 }
272}
273macro_rules! YEARNINGS_QUERY {
274 () => {
275 "{url}?lang={lang}®ion={region}&crumb={crumb}"
276 };
277}
278
279/// Container for connection parameters to yahoo! finance server
280#[derive(Debug, Clone)]
281pub struct YahooConnector {
282 client: Client,
283 url: &'static str,
284 search_url: &'static str,
285 timeout: Option<Duration>,
286 user_agent: Option<String>,
287 proxy: Option<Proxy>,
288 cookie: Option<String>,
289 crumb: Option<String>,
290 pub rate_limiter: Option<Arc<RateLimiter>>,
291}
292
293#[derive(Default)]
294pub struct YahooConnectorBuilderLegacy {
295 inner: ClientBuilder,
296 timeout: Option<Duration>,
297 user_agent: Option<String>,
298 proxy: Option<Proxy>,
299 rate_limit_config: Option<RateLimitConfig>,
300}
301
302impl YahooConnector {
303 /// Constructor for a new instance of the yahoo connector with **production
304 /// defaults**.
305 ///
306 /// Production defaults prioritize:
307 /// - Safety (strict circuit breaker)
308 /// - Stability (conservative rate limits)
309 /// - Reliability (extended retries)
310 /// - Efficiency (longer cache TTL)
311 ///
312 /// For development/testing, use [`YahooConnector::builder()`] instead.
313 /// For custom presets, use [`YahooConnector::from_preset()`].
314 ///
315 /// # Examples
316 ///
317 /// ```no_run
318 /// use eeyf::YahooConnector;
319 ///
320 /// #[tokio::main]
321 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
322 /// // Production defaults: safe, stable, comprehensive
323 /// let connector = YahooConnector::new()?;
324 /// let quote = connector.get_latest_quotes("AAPL", "1d").await?;
325 /// Ok(())
326 /// }
327 /// ```
328 pub fn new() -> Result<YahooConnector, YahooError> {
329 // TODO: Create production defaults via EnterpriseYahooConnector
330 // For now, use existing implementation
331 Self::builder().build()
332 }
333
334 /// Creates a builder with **development defaults**.
335 ///
336 /// Development defaults prioritize:
337 /// - Fast failure detection (lenient circuit breaker)
338 /// - Fresh data (short cache TTL)
339 /// - Debugging visibility (verbose logging)
340 /// - Rapid iteration (permissive rate limits)
341 ///
342 /// # Examples
343 ///
344 /// ```no_run
345 /// use std::time::Duration;
346 ///
347 /// use eeyf::YahooConnector;
348 ///
349 /// #[tokio::main]
350 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
351 /// // Start with development defaults, then customize
352 /// let connector = YahooConnector::builder()
353 /// .rate_limit_config(eeyf::RateLimitConfig::new(5.0))
354 /// .timeout(Duration::from_secs(45))
355 /// .build()?;
356 ///
357 /// let quote = connector.get_latest_quotes("AAPL", "1d").await?;
358 /// Ok(())
359 /// }
360 /// ```
361 pub fn builder() -> crate::builder::YahooConnectorBuilder {
362 crate::builder::YahooConnectorBuilder::default()
363 }
364
365 /// Creates a connector from a named preset configuration.
366 ///
367 /// Searches for presets in this order:
368 /// 1. Built-in presets: "production", "development", "enterprise",
369 /// "minimal"
370 /// 2. Project-local presets (./.eeyf/presets/)
371 /// 3. User presets (~/.config/eeyf/presets/ or %APPDATA%\eeyf\presets\)
372 ///
373 /// # Built-in Presets
374 ///
375 /// - **"production"** - Safe defaults (same as [`YahooConnector::new()`])
376 /// - **"development"** - Fast feedback (same as
377 /// [`YahooConnector::builder()`])
378 /// - **"enterprise"** - Conservative rate limits, extended caching
379 /// - **"minimal"** - Bare minimum for testing, no caching/retries
380 ///
381 /// # Examples
382 ///
383 /// ```no_run
384 /// use eeyf::YahooConnector;
385 ///
386 /// #[tokio::main]
387 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
388 /// // Load enterprise preset (conservative settings)
389 /// let connector = YahooConnector::from_preset("enterprise")?;
390 ///
391 /// // Load custom user-defined preset
392 /// let connector = YahooConnector::from_preset("my-staging-config")?;
393 ///
394 /// let quote = connector.get_latest_quotes("AAPL", "1d").await?;
395 /// Ok(())
396 /// }
397 /// ```
398 ///
399 /// # Errors
400 ///
401 /// Returns an error if the preset is not found or cannot be loaded.
402 pub fn from_preset(name: &str) -> Result<YahooConnector, YahooError> {
403 use crate::{
404 enterprise::{EnterpriseConfig, EnterpriseYahooConnector},
405 presets::PresetManager,
406 };
407
408 let manager = PresetManager::new();
409 let preset = manager.load_preset(name)?;
410
411 // Convert PresetConfig to EnterpriseConfig
412 let enterprise_config = EnterpriseConfig::from(preset);
413
414 // TODO: We need to decide how to integrate EnterpriseYahooConnector with
415 // YahooConnector For now, create a basic YahooConnector with rate
416 // limiting from the preset
417 let rate_limit_config = enterprise_config.rate_limiter.clone();
418
419 // Create EnterpriseYahooConnector and wrap it
420 let _enterprise_connector = EnterpriseYahooConnector::new(enterprise_config)?;
421
422 Self::builder().rate_limit(rate_limit_config.requests_per_hour as f64).build()
423 }
424
425 /// Saves the current connector configuration as a named preset.
426 ///
427 /// Presets are saved to the user configuration directory:
428 /// - Linux/macOS: `~/.config/eeyf/presets/{name}.toml`
429 /// - Windows: `%APPDATA%\eeyf\presets\{name}.toml`
430 ///
431 /// # Examples
432 ///
433 /// ```no_run
434 /// use std::time::Duration;
435 ///
436 /// use eeyf::YahooConnector;
437 ///
438 /// #[tokio::main]
439 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
440 /// let connector = YahooConnector::builder()
441 /// .rate_limit_config(eeyf::RateLimitConfig::new(2.5))
442 /// .timeout(Duration::from_secs(45))
443 /// .build()?;
444 ///
445 /// // Save for later reuse
446 /// connector.save_preset("my-staging-config")?;
447 ///
448 /// // Later, reload it
449 /// let reloaded = YahooConnector::from_preset("my-staging-config")?;
450 /// Ok(())
451 /// }
452 /// ```
453 ///
454 /// # Errors
455 ///
456 /// Returns an error if:
457 /// - Attempting to save a built-in preset name
458 /// - Unable to create the presets directory
459 /// - Unable to write the preset file
460 pub fn save_preset(&self, _name: &str) -> Result<(), YahooError> {
461 // TODO: Extract current configuration and save via PresetManager
462 // For now, return error indicating not yet implemented
463 Err(YahooError::ConnectionFailed(
464 format!(
465 "Preset saving not yet implemented. Need to extract current configuration from \
466 YahooConnector and convert to PresetConfig."
467 )
468 .into(),
469 ))
470 }
471
472 /// Internal default implementation used exclusively by the builder.
473 /// Note: This default implementation does not set the user agent in the
474 /// client, so it does not work on its own. The builder will set the
475 /// user agent.
476 fn default_internal() -> Self {
477 YahooConnector {
478 client: Client::default(),
479 url: YCHART_URL,
480 search_url: YSEARCH_URL,
481 timeout: None,
482 user_agent: Some(USER_AGENT.to_string()),
483 proxy: None,
484 cookie: None,
485 crumb: None,
486 rate_limiter: None,
487 }
488 }
489}
490
491impl YahooConnectorBuilderLegacy {
492 pub fn new() -> Self {
493 YahooConnectorBuilderLegacy {
494 inner: Client::builder(),
495 user_agent: Some(USER_AGENT.to_string()),
496 ..Default::default()
497 }
498 }
499
500 pub fn timeout(mut self, timeout: Duration) -> Self {
501 self.timeout = Some(timeout);
502 self
503 }
504
505 pub fn user_agent(mut self, user_agent: &str) -> Self {
506 self.user_agent = Some(user_agent.to_string());
507 self
508 }
509
510 pub fn proxy(mut self, proxy: Proxy) -> Self {
511 self.proxy = Some(proxy);
512 self
513 }
514
515 pub fn rate_limit_config(mut self, config: RateLimitConfig) -> Self {
516 self.rate_limit_config = Some(config);
517 self
518 }
519
520 pub fn build(mut self) -> Result<YahooConnector, YahooError> {
521 if let Some(timeout) = &self.timeout {
522 self.inner = self.inner.timeout(*timeout);
523 }
524 if let Some(user_agent) = &self.user_agent {
525 self.inner = self.inner.user_agent(user_agent.clone());
526 }
527 if let Some(proxy) = &self.proxy {
528 self.inner = self.inner.proxy(proxy.clone());
529 }
530
531 let rate_limiter = self.rate_limit_config.map(|config| Arc::new(RateLimiter::new(config)));
532
533 Ok(YahooConnector {
534 client: self.inner.use_rustls_tls().build()?,
535 timeout: self.timeout,
536 user_agent: self.user_agent,
537 proxy: self.proxy,
538 rate_limiter,
539 ..YahooConnector::default_internal()
540 })
541 }
542
543 pub fn build_with_client(client: Client) -> Result<YahooConnector, YahooError> {
544 Ok(YahooConnector {
545 client,
546 ..YahooConnector::default_internal()
547 })
548 }
549}
550
551impl YahooConnector {
552 /// Enable rate limiting with default configuration
553 pub fn with_rate_limiting() -> Result<YahooConnector, YahooError> {
554 Self::builder()
555 .rate_limit(RateLimitConfig::default().requests_per_hour as f64)
556 .build()
557 }
558
559 /// Enable rate limiting with custom configuration
560 pub fn with_custom_rate_limiting(
561 config: RateLimitConfig,
562 ) -> Result<YahooConnector, YahooError> {
563 Self::builder().rate_limit(config.requests_per_hour as f64).build()
564 }
565
566 /// Get the current rate limit status
567 pub fn rate_limit_status(&self) -> Option<RateLimitStatus> {
568 self.rate_limiter.as_ref().map(|limiter| limiter.status())
569 }
570
571 /// Check if rate limiting is enabled
572 pub fn is_rate_limited(&self) -> bool {
573 self.rate_limiter.is_some()
574 }
575}
576
577pub mod async_impl;