Skip to main content

finance_query/fred/
mod.rs

1//! Macro-economic data sources: FRED API and US Treasury yield curve.
2//!
3//! Requires the **`macro`** feature flag.
4//!
5//! # FRED (Federal Reserve Economic Data)
6//!
7//! Access 800k+ macro time series (CPI, Fed Funds Rate, M2, GDP, etc.).
8//! Requires a free API key from <https://fred.stlouisfed.org/docs/api/api_key.html>.
9//!
10//! Call [`init`] once at startup before using [`series`].
11//!
12//! # US Treasury Yields
13//!
14//! Daily yield curve data from the US Treasury Department. No key required.
15//! Use [`treasury_yields`] directly.
16//!
17//! # Quick Start
18//!
19//! ```no_run
20//! use finance_query::fred;
21//!
22//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
23//! // FRED: initialize with API key, then query any series
24//! fred::init("your-fred-api-key")?;
25//! let cpi = fred::series("CPIAUCSL").await?;
26//! println!("CPI observations: {}", cpi.observations.len());
27//!
28//! // Treasury: no key required
29//! let yields = fred::treasury_yields(2025).await?;
30//! println!("Latest 10Y yield: {:?}", yields.last().and_then(|y| y.y10));
31//! # Ok(())
32//! # }
33//! ```
34
35mod client;
36pub mod models;
37mod treasury;
38
39use crate::error::{FinanceError, Result};
40use crate::rate_limiter::RateLimiter;
41use client::FredClientBuilder;
42use std::sync::{Arc, OnceLock};
43use std::time::Duration;
44
45pub use models::{MacroObservation, MacroSeries, TreasuryYield};
46
47/// FRED free-tier rate limit: 120 requests/minute = 2 req/sec.
48const FRED_RATE_PER_SEC: f64 = 2.0;
49
50/// Stable configuration stored in the FRED process-global singleton.
51///
52/// Only the API key, timeout, and rate-limiter are stored — NOT the
53/// `reqwest::Client`. `reqwest::Client` internally spawns hyper connection-pool
54/// tasks on whichever tokio runtime first uses them; when that runtime is
55/// dropped (e.g. at the end of a `#[tokio::test]`), those tasks die and
56/// subsequent calls from a different runtime receive `DispatchGone`. A fresh
57/// `reqwest::Client` is built per `series()` call via
58/// [`FredClientBuilder::build_with_limiter`], reusing this shared limiter so
59/// the 2 req/sec FRED rate limit is respected across all calls.
60struct FredSingleton {
61    api_key: String,
62    timeout: Duration,
63    limiter: Arc<RateLimiter>,
64}
65
66static FRED_SINGLETON: OnceLock<FredSingleton> = OnceLock::new();
67
68/// Initialize the global FRED client with an API key.
69///
70/// Must be called once before [`series`]. Subsequent calls return an error.
71///
72/// # Arguments
73///
74/// * `api_key` - Your FRED API key (free at <https://fred.stlouisfed.org/docs/api/api_key.html>)
75///
76/// # Errors
77///
78/// Returns [`FinanceError::InvalidParameter`] if already initialized.
79pub fn init(api_key: impl Into<String>) -> Result<()> {
80    init_with_timeout(api_key, Duration::from_secs(30))
81}
82
83/// Initialize the FRED client with a custom timeout.
84pub fn init_with_timeout(api_key: impl Into<String>, timeout: Duration) -> Result<()> {
85    FRED_SINGLETON
86        .set(FredSingleton {
87            api_key: api_key.into(),
88            timeout,
89            limiter: Arc::new(RateLimiter::new(FRED_RATE_PER_SEC)),
90        })
91        .map_err(|_| FinanceError::InvalidParameter {
92            param: "fred".to_string(),
93            reason: "FRED client already initialized".to_string(),
94        })
95}
96
97/// Fetch all observations for a FRED data series.
98///
99/// Common series IDs:
100/// - `"FEDFUNDS"` — Federal Funds Rate
101/// - `"CPIAUCSL"` — Consumer Price Index (all urban, seasonally adjusted)
102/// - `"UNRATE"` — Unemployment Rate
103/// - `"DGS10"` — 10-Year Treasury Constant Maturity Rate
104/// - `"M2SL"` — M2 Money Supply
105/// - `"GDP"` — US Gross Domestic Product
106///
107/// # Errors
108///
109/// Returns [`FinanceError::InvalidParameter`] if FRED has not been initialized.
110pub async fn series(series_id: &str) -> Result<MacroSeries> {
111    let s = FRED_SINGLETON
112        .get()
113        .ok_or_else(|| FinanceError::InvalidParameter {
114            param: "fred".to_string(),
115            reason: "FRED not initialized. Call fred::init(api_key) first.".to_string(),
116        })?;
117    let c = FredClientBuilder::new(&s.api_key)
118        .timeout(s.timeout)
119        .build_with_limiter(Arc::clone(&s.limiter))?;
120    c.series(series_id).await
121}
122
123/// Fetch US Treasury yield curve data for the given year.
124///
125/// No API key required. Data is published on each business day.
126///
127/// # Arguments
128///
129/// * `year` - Calendar year (e.g., `2025`). Pass the current year for recent data.
130pub async fn treasury_yields(year: u32) -> Result<Vec<TreasuryYield>> {
131    treasury::fetch_yields(year).await
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_init_errors_on_double_init() {
140        // First init may or may not succeed (could already be set from another test).
141        let _ = init("test-key-1");
142        let result = init("test-key-2");
143        assert!(matches!(result, Err(FinanceError::InvalidParameter { .. })));
144    }
145
146    #[test]
147    fn test_series_without_init_fails_gracefully() {
148        // If somehow the singleton is not set, series() must return an error.
149        // (This test only exercises the error path if FRED_SINGLETON isn't set yet,
150        //  which may not be the case if other tests run first.)
151        if FRED_SINGLETON.get().is_none() {
152            // We can't reset OnceLock in tests, but we can verify the error shape:
153            // Synthesise the error manually.
154            let err = FinanceError::InvalidParameter {
155                param: "fred".to_string(),
156                reason: "not initialized".to_string(),
157            };
158            assert!(matches!(err, FinanceError::InvalidParameter { .. }));
159        }
160    }
161}