Skip to main content

finance_query/adapters/polygon/
mod.rs

1//! Polygon.io API client for financial data.
2//!
3//! Requires the **`polygon`** feature flag and an API key from
4//! <https://polygon.io/>.
5//!
6//! Call [`init`] once at startup before using any query functions.
7//!
8//! # Quick Start
9//!
10//! ```no_run
11//! use finance_query::adapters::polygon;
12//! use finance_query::adapters::polygon::Timespan;
13//!
14//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
15//! polygon::init("YOUR_API_KEY")?;
16//!
17//! // Stock aggregate bars
18//! let bars = polygon::stock_aggregates("AAPL", 1, Timespan::Day, "2024-01-01", "2024-01-31", None).await?;
19//!
20//! // Snapshot
21//! let snap = polygon::stock_snapshot("AAPL").await?;
22//!
23//! // Last trade
24//! let trade = polygon::stock_last_trade("AAPL").await?;
25//! # Ok(())
26//! # }
27//! ```
28
29mod client;
30pub mod models;
31
32mod reference;
33mod stocks;
34
35mod crypto;
36mod forex;
37mod futures;
38mod indices;
39mod options;
40
41mod alternative;
42mod benzinga;
43mod corporate_events;
44mod economy;
45mod etf;
46pub mod websocket;
47
48use crate::error::{FinanceError, Result};
49use crate::rate_limiter::RateLimiter;
50use client::PolygonClientBuilder;
51use std::sync::{Arc, OnceLock};
52use std::time::Duration;
53
54pub use alternative::*;
55pub use benzinga::*;
56pub use corporate_events::*;
57pub use economy::*;
58pub use etf::*;
59pub use models::*;
60pub use reference::*;
61
62// Re-export items explicitly for modules with overlapping submodule names
63// to avoid ambiguous glob re-exports (aggregates, snapshots, etc.).
64pub use crypto::{
65    CryptoDailyOpenClose, CryptoLastTrade, CryptoLastTradeResponse, CryptoOpenCloseTrade,
66    crypto_aggregates, crypto_daily_open_close, crypto_ema, crypto_grouped_daily,
67    crypto_last_trade, crypto_macd, crypto_previous_close, crypto_rsi, crypto_sma, crypto_snapshot,
68    crypto_snapshots_all, crypto_top_movers, crypto_trades,
69};
70pub use forex::{
71    ConversionLast, CurrencyConversion, ForexLastQuote, ForexQuoteResponse, currency_conversion,
72    forex_aggregates, forex_ema, forex_grouped_daily, forex_last_quote, forex_macd,
73    forex_previous_close, forex_quotes, forex_rsi, forex_sma, forex_snapshot, forex_snapshots_all,
74    forex_top_movers,
75};
76pub use futures::{
77    FuturesContract, FuturesProduct, FuturesSchedule, FuturesSession, FuturesSnapshot,
78    FuturesSnapshotResponse, futures_aggregates, futures_contracts, futures_products,
79    futures_quotes, futures_schedules, futures_snapshot, futures_trades,
80};
81pub use indices::{
82    IndexSession, IndexSnapshot, IndexSnapshotResponse, index_aggregates, index_daily_open_close,
83    index_ema, index_macd, index_previous_close, index_rsi, index_sma, index_snapshot,
84};
85pub use options::{
86    AdditionalUnderlying, OptionsContract, OptionsContractResponse,
87    OptionsContractSnapshotResponse, OptionsGreeks, OptionsSnapshot, OptionsSnapshotDetails,
88    OptionsSnapshotQuote, OptionsSnapshotTrade, OptionsUnderlyingAsset, options_aggregates,
89    options_chain_snapshot, options_contract_details, options_contract_snapshot, options_contracts,
90    options_daily_open_close, options_ema, options_last_trade, options_macd,
91    options_previous_close, options_quotes, options_rsi, options_sma, options_trades,
92};
93pub use stocks::*;
94
95/// Polygon.io free-tier rate limit: 5 req/sec.
96const PG_RATE_PER_SEC: f64 = 5.0;
97
98struct PolygonSingleton {
99    api_key: String,
100    timeout: Duration,
101    limiter: Arc<RateLimiter>,
102}
103
104static PG_SINGLETON: OnceLock<PolygonSingleton> = OnceLock::new();
105
106/// Initialize the global Polygon client with an API key.
107///
108/// Must be called once before using any query functions. Subsequent calls return an error.
109///
110/// # Errors
111///
112/// Returns [`FinanceError::InvalidParameter`] if already initialized.
113pub fn init(api_key: impl Into<String>) -> Result<()> {
114    init_with_timeout(api_key, Duration::from_secs(30))
115}
116
117/// Initialize the Polygon client with a custom timeout.
118pub fn init_with_timeout(api_key: impl Into<String>, timeout: Duration) -> Result<()> {
119    PG_SINGLETON
120        .set(PolygonSingleton {
121            api_key: api_key.into(),
122            timeout,
123            limiter: Arc::new(RateLimiter::new(PG_RATE_PER_SEC)),
124        })
125        .map_err(|_| FinanceError::InvalidParameter {
126            param: "polygon".to_string(),
127            reason: "Polygon client already initialized".to_string(),
128        })
129}
130
131/// Build a fresh client from the singleton state.
132pub(crate) fn build_client() -> Result<client::PolygonClient> {
133    let s = PG_SINGLETON
134        .get()
135        .ok_or_else(|| FinanceError::InvalidParameter {
136            param: "polygon".to_string(),
137            reason: "Polygon not initialized. Call polygon::init(api_key) first.".to_string(),
138        })?;
139    PolygonClientBuilder::new(&s.api_key)
140        .timeout(s.timeout)
141        .build_with_limiter(Arc::clone(&s.limiter))
142}
143
144/// Build a test client pointing at a mock server URL.
145#[cfg(test)]
146pub(crate) fn build_test_client(base_url: &str) -> Result<client::PolygonClient> {
147    PolygonClientBuilder::new("test-key")
148        .timeout(Duration::from_secs(5))
149        .base_url(base_url)
150        .build_with_limiter(Arc::new(RateLimiter::new(100.0)))
151}
152
153/// Internal: read the configured API key. Used by the websocket module.
154pub(crate) fn api_key() -> Result<String> {
155    PG_SINGLETON
156        .get()
157        .map(|s| s.api_key.clone())
158        .ok_or_else(|| FinanceError::InvalidParameter {
159            param: "polygon".to_string(),
160            reason: "Polygon not initialized. Call polygon::init(api_key) first.".to_string(),
161        })
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn test_init_errors_on_double_init() {
170        let _ = init("test-key-1");
171        let result = init("test-key-2");
172        assert!(matches!(result, Err(FinanceError::InvalidParameter { .. })));
173    }
174}