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}