Skip to main content

rustrade_data/exchange/alpaca/options/
mod.rs

1//! Alpaca options market data client.
2//!
3//! Provides REST-based option contract discovery and chain snapshots with Greeks.
4//!
5//! # Endpoints
6//!
7//! - [`AlpacaOptionsClient::fetch_contracts`]: `GET /v2/options/contracts` — discover available contracts
8//! - [`AlpacaOptionsClient::fetch_snapshots`]: `GET /v1beta1/options/snapshots` — chain snapshots with Greeks
9//!
10//! # Authentication
11//!
12//! Requires `ALPACA_API_KEY` and `ALPACA_SECRET_KEY` environment variables.
13//!
14//! # Data Feeds
15//!
16//! - [`AlpacaOptionFeed::Opra`]: Real-time OPRA feed (requires paid subscription)
17//! - [`AlpacaOptionFeed::Indicative`]: 15-minute delayed feed (free)
18//!
19//! # Example
20//!
21//! ```ignore
22//! use rustrade_data::exchange::alpaca::options::{
23//!     AlpacaOptionsClient, AlpacaOptionContractQuery, AlpacaOptionFeed,
24//! };
25//! use chrono::NaiveDate;
26//!
27//! let client = AlpacaOptionsClient::from_env()?;
28//!
29//! // Discover AAPL options expiring in next 30 days
30//! let query = AlpacaOptionContractQuery::new(vec!["AAPL".into()])
31//!     .expiration_gte(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap())
32//!     .expiration_lte(NaiveDate::from_ymd_opt(2024, 1, 31).unwrap());
33//!
34//! let contracts = client.fetch_contracts(&query).await?;
35//!
36//! // Fetch snapshots with Greeks (using free delayed feed)
37//! let symbols: Vec<_> = contracts.iter().map(|c| c.symbol.clone()).collect();
38//! let snapshots = client.fetch_snapshots(&symbols, AlpacaOptionFeed::Indicative).await?;
39//! ```
40//!
41//! # Testing Status
42//!
43//! **Tested locally, CI planned (free tier — paper trading allowed):**
44//! - Contract discovery via `fetch_contracts()`
45//! - Indicative feed (15-min delayed) via `fetch_snapshots()`
46//!
47//! **NOT tested (requires OPRA subscription via Algo Trader Plus):**
48//! - OPRA real-time feed — implemented but unverified against real endpoints
49//!
50//! # Limitations
51//!
52//! - **REST only**: Greeks streaming is NOT available via WebSocket. Use snapshots for
53//!   point-in-time Greeks data.
54//! - **Rate limits**: Alpaca applies rate limits; the client handles 429 responses with retry.
55
56mod contracts;
57mod snapshots;
58
59pub use contracts::{AlpacaOptionContract, AlpacaOptionContractQuery};
60pub use snapshots::{AlpacaOptionFeed, AlpacaOptionQuote, AlpacaOptionSnapshot, AlpacaOptionTrade};
61
62use super::rest::AlpacaRestClient;
63
64/// Errors from Alpaca options API operations.
65///
66/// Alias of the shared [`AlpacaRestError`](super::rest::AlpacaRestError): options requests
67/// have no failure modes beyond the common transport/protocol ones, so the single REST error type
68/// is reused rather than duplicated per endpoint family.
69pub use super::rest::AlpacaRestError as AlpacaOptionsError;
70
71/// Alpaca options market data client.
72///
73/// Provides access to option contract discovery and chain snapshots via REST API.
74/// The client handles authentication, rate limiting, and pagination automatically.
75///
76/// # Construction
77///
78/// Use [`AlpacaOptionsClient::from_env`] to create a client using environment variables,
79/// or [`AlpacaOptionsClient::new`] for explicit credentials.
80///
81/// Internally this is a thin wrapper over the shared [`AlpacaRestClient`] transport.
82#[derive(Clone, Debug)]
83pub struct AlpacaOptionsClient {
84    rest: AlpacaRestClient,
85}
86
87impl AlpacaOptionsClient {
88    /// Create a new client with explicit credentials.
89    ///
90    /// # Arguments
91    ///
92    /// * `api_key` - Alpaca API key
93    /// * `api_secret` - Alpaca API secret
94    /// * `paper` - Use paper trading endpoint if true
95    ///
96    /// # Errors
97    ///
98    /// Returns error if the HTTP client cannot be built (invalid headers).
99    pub fn new(api_key: &str, api_secret: &str, paper: bool) -> Result<Self, AlpacaOptionsError> {
100        Ok(Self {
101            rest: AlpacaRestClient::new(api_key, api_secret, paper)?,
102        })
103    }
104
105    /// Create a client from environment variables.
106    ///
107    /// Reads:
108    /// - `ALPACA_API_KEY` - API key (required)
109    /// - `ALPACA_SECRET_KEY` - API secret (required)
110    /// - `ALPACA_PAPER` - Set to "true" for paper trading (optional, defaults to false)
111    ///
112    /// # Errors
113    ///
114    /// Returns error if required environment variables are missing.
115    pub fn from_env() -> Result<Self, AlpacaOptionsError> {
116        Ok(Self {
117            rest: AlpacaRestClient::from_env()?,
118        })
119    }
120
121    /// Override both API base URLs (broker and data) on the underlying transport.
122    ///
123    /// See [`AlpacaRestClient::with_base_urls`] — points the client at API-compatible
124    /// endpoints other than the production Alpaca hosts (a local mock server in tests, or a
125    /// proxy/gateway).
126    #[must_use]
127    pub fn with_base_urls(
128        mut self,
129        broker_base: impl Into<String>,
130        data_base: impl Into<String>,
131    ) -> Self {
132        self.rest = self.rest.with_base_urls(broker_base, data_base);
133        self
134    }
135}
136
137#[cfg(test)]
138#[allow(clippy::unwrap_used, clippy::expect_used)] // Test code: panics on bad input are acceptable
139mod tests {
140    use super::AlpacaOptionsClient;
141
142    #[test]
143    fn client_debug_hides_credentials() {
144        let client = AlpacaOptionsClient::new("test-key-id", "test-secret-value", true)
145            .expect("client construction with ASCII credentials should succeed");
146        let debug_str = format!("{client:?}");
147
148        assert!(!debug_str.contains("test-key-id"));
149        assert!(!debug_str.contains("test-secret-value"));
150    }
151}