Skip to main content

finance_query/adapters/polygon/options/
contracts.rs

1//! Options contract reference endpoints: list contracts, contract details.
2
3use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::{FinanceError, Result};
7
8use super::super::build_client;
9use super::super::models::*;
10
11/// An additional underlying asset for a contract.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct AdditionalUnderlying {
15    /// The type of the additional underlying (e.g., `"equity"`, `"index"`).
16    #[serde(rename = "type")]
17    pub underlying_type: Option<String>,
18    /// The underlying ticker or identifier.
19    pub underlying: Option<String>,
20    /// The number of units of the underlying per contract.
21    pub amount: Option<f64>,
22}
23
24/// An options contract from the Polygon reference API.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[non_exhaustive]
27pub struct OptionsContract {
28    /// The options ticker symbol (e.g., `"O:AAPL250117C00150000"`).
29    pub ticker: Option<String>,
30    /// The underlying stock ticker (e.g., `"AAPL"`).
31    pub underlying_ticker: Option<String>,
32    /// Contract type: `"call"` or `"put"`.
33    pub contract_type: Option<String>,
34    /// Exercise style: `"american"` or `"european"`.
35    pub exercise_style: Option<String>,
36    /// Contract expiration date (`"YYYY-MM-DD"`).
37    pub expiration_date: Option<String>,
38    /// Strike price.
39    pub strike_price: Option<f64>,
40    /// CFI code for the contract.
41    pub cfi: Option<String>,
42    /// Number of shares per contract (typically 100).
43    pub shares_per_contract: Option<u32>,
44    /// Additional underlying assets, if any.
45    pub additional_underlyings: Option<Vec<AdditionalUnderlying>>,
46    /// Primary exchange.
47    pub primary_exchange: Option<String>,
48}
49
50/// Response wrapper for a single options contract detail.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[non_exhaustive]
53pub struct OptionsContractResponse {
54    /// Request ID.
55    pub request_id: Option<String>,
56    /// Response status.
57    pub status: Option<String>,
58    /// The contract result.
59    pub results: Option<OptionsContract>,
60}
61
62/// Fetch a list of options contracts matching the given query parameters.
63///
64/// # Arguments
65///
66/// * `params` - Query params such as `underlying_ticker`, `contract_type`,
67///   `expiration_date`, `strike_price`, `expired`, `order`, `limit`, `sort`
68pub async fn options_contracts(
69    params: &[(&str, &str)],
70) -> Result<PaginatedResponse<OptionsContract>> {
71    let client = build_client()?;
72    let path = "/v3/reference/options/contracts";
73    client.get(path, params).await
74}
75
76/// Fetch details for a single options contract.
77///
78/// * `ticker` - Options ticker symbol with `O:` prefix (e.g., `"O:AAPL250117C00150000"`)
79pub async fn options_contract_details(ticker: &str) -> Result<OptionsContractResponse> {
80    let client = build_client()?;
81    let path = format!(
82        "/v3/reference/options/contracts/{}",
83        encode_path_segment(ticker)
84    );
85    let json = client.get_raw(&path, &[]).await?;
86    serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
87        field: "options_contract_details".to_string(),
88        context: format!("Failed to parse options contract details response: {e}"),
89    })
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[tokio::test]
97    async fn test_options_contracts_mock() {
98        let mut server = mockito::Server::new_async().await;
99        let _mock = server
100            .mock("GET", "/v3/reference/options/contracts")
101            .match_query(mockito::Matcher::AllOf(vec![
102                mockito::Matcher::UrlEncoded("apiKey".into(), "test-key".into()),
103                mockito::Matcher::UrlEncoded("underlying_ticker".into(), "AAPL".into()),
104            ]))
105            .with_status(200)
106            .with_header("content-type", "application/json")
107            .with_body(
108                serde_json::json!({
109                    "request_id": "abc123",
110                    "status": "OK",
111                    "results": [
112                        {
113                            "ticker": "O:AAPL250117C00150000",
114                            "underlying_ticker": "AAPL",
115                            "contract_type": "call",
116                            "exercise_style": "american",
117                            "expiration_date": "2025-01-17",
118                            "strike_price": 150.0,
119                            "cfi": "OCASPS",
120                            "shares_per_contract": 100,
121                            "primary_exchange": "BATO"
122                        },
123                        {
124                            "ticker": "O:AAPL250117P00150000",
125                            "underlying_ticker": "AAPL",
126                            "contract_type": "put",
127                            "exercise_style": "american",
128                            "expiration_date": "2025-01-17",
129                            "strike_price": 150.0,
130                            "cfi": "OPASPS",
131                            "shares_per_contract": 100,
132                            "primary_exchange": "BATO"
133                        }
134                    ],
135                    "resultsCount": 2
136                })
137                .to_string(),
138            )
139            .create_async()
140            .await;
141
142        let client = super::super::super::build_test_client(&server.url()).unwrap();
143        let resp: PaginatedResponse<OptionsContract> = client
144            .get(
145                "/v3/reference/options/contracts",
146                &[("underlying_ticker", "AAPL")],
147            )
148            .await
149            .unwrap();
150
151        let results = resp.results.unwrap();
152        assert_eq!(results.len(), 2);
153        assert_eq!(results[0].ticker.as_deref(), Some("O:AAPL250117C00150000"));
154        assert_eq!(results[0].contract_type.as_deref(), Some("call"));
155        assert!((results[0].strike_price.unwrap() - 150.0).abs() < 0.01);
156        assert_eq!(results[1].contract_type.as_deref(), Some("put"));
157    }
158
159    #[tokio::test]
160    async fn test_options_contract_details_mock() {
161        let mut server = mockito::Server::new_async().await;
162        let _mock = server
163            .mock(
164                "GET",
165                "/v3/reference/options/contracts/O:AAPL250117C00150000",
166            )
167            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
168                "apiKey".into(),
169                "test-key".into(),
170            )]))
171            .with_status(200)
172            .with_header("content-type", "application/json")
173            .with_body(
174                serde_json::json!({
175                    "request_id": "abc123",
176                    "status": "OK",
177                    "results": {
178                        "ticker": "O:AAPL250117C00150000",
179                        "underlying_ticker": "AAPL",
180                        "contract_type": "call",
181                        "exercise_style": "american",
182                        "expiration_date": "2025-01-17",
183                        "strike_price": 150.0,
184                        "cfi": "OCASPS",
185                        "shares_per_contract": 100,
186                        "primary_exchange": "BATO",
187                        "additional_underlyings": [
188                            { "type": "equity", "underlying": "AAPL", "amount": 100.0 }
189                        ]
190                    }
191                })
192                .to_string(),
193            )
194            .create_async()
195            .await;
196
197        let client = super::super::super::build_test_client(&server.url()).unwrap();
198        let json = client
199            .get_raw("/v3/reference/options/contracts/O:AAPL250117C00150000", &[])
200            .await
201            .unwrap();
202
203        let resp: OptionsContractResponse = serde_json::from_value(json).unwrap();
204        assert_eq!(resp.status.as_deref(), Some("OK"));
205        let contract = resp.results.unwrap();
206        assert_eq!(contract.ticker.as_deref(), Some("O:AAPL250117C00150000"));
207        assert_eq!(contract.exercise_style.as_deref(), Some("american"));
208        assert_eq!(contract.shares_per_contract, Some(100));
209        let additional = contract.additional_underlyings.unwrap();
210        assert_eq!(additional.len(), 1);
211        assert_eq!(additional[0].underlying.as_deref(), Some("AAPL"));
212    }
213}