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}&region={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;