finance_query/adapters/polygon/options/
contracts.rs1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct AdditionalUnderlying {
15 #[serde(rename = "type")]
17 pub underlying_type: Option<String>,
18 pub underlying: Option<String>,
20 pub amount: Option<f64>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26#[non_exhaustive]
27pub struct OptionsContract {
28 pub ticker: Option<String>,
30 pub underlying_ticker: Option<String>,
32 pub contract_type: Option<String>,
34 pub exercise_style: Option<String>,
36 pub expiration_date: Option<String>,
38 pub strike_price: Option<f64>,
40 pub cfi: Option<String>,
42 pub shares_per_contract: Option<u32>,
44 pub additional_underlyings: Option<Vec<AdditionalUnderlying>>,
46 pub primary_exchange: Option<String>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52#[non_exhaustive]
53pub struct OptionsContractResponse {
54 pub request_id: Option<String>,
56 pub status: Option<String>,
58 pub results: Option<OptionsContract>,
60}
61
62pub 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
76pub 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}