finance_query/models/options/response.rs
1use super::contract::{Contracts, OptionContract};
2use crate::Provider;
3/// Options Response module
4///
5/// Handles parsing of Yahoo Finance options API responses.
6/// These types are internal implementation details and not exposed in the public API.
7use serde::{Deserialize, Serialize};
8
9/// Response wrapper for options endpoint
10///
11/// Note: While this type is public for return values, users should not manually construct it.
12/// Use `Ticker::options()` to obtain options data.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct Options {
16 /// Option chain container
17 pub(crate) option_chain: OptionChainContainer,
18
19 /// Which data provider served this data (e.g., "yahoo", "polygon").
20 #[serde(skip_serializing_if = "Option::is_none", default)]
21 pub provider_id: Option<Provider>,
22}
23
24/// Container for option chain results
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub(crate) struct OptionChainContainer {
27 /// Results array
28 pub result: Vec<OptionChainResult>,
29
30 /// Error if any
31 pub error: Option<serde_json::Value>,
32}
33
34/// Single option chain result
35#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase")]
37pub(crate) struct OptionChainResult {
38 /// Underlying symbol
39 pub underlying_symbol: Option<String>,
40
41 /// Available expiration dates (Unix timestamps)
42 pub expiration_dates: Option<Vec<i64>>,
43
44 /// Available strike prices
45 pub strikes: Option<Vec<f64>>,
46
47 /// Whether has mini options
48 pub has_mini_options: Option<bool>,
49
50 /// Quote data
51 pub quote: Option<serde_json::Value>,
52
53 /// Options data (array of option chains)
54 pub options: Vec<OptionChainData>,
55}
56
57/// Option chain data for a specific expiration
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub(crate) struct OptionChainData {
61 /// Expiration date (Unix timestamp)
62 pub expiration_date: i64,
63
64 /// Whether has mini options
65 pub has_mini_options: Option<bool>,
66
67 /// Call options
68 pub calls: Option<Vec<OptionContract>>,
69
70 /// Put options
71 pub puts: Option<Vec<OptionContract>>,
72}
73
74impl Options {
75 /// Get the first result
76 pub(crate) fn first_result(&self) -> Option<&OptionChainResult> {
77 self.option_chain.result.first()
78 }
79
80 /// Get available expiration dates
81 pub fn expiration_dates(&self) -> Vec<i64> {
82 self.first_result()
83 .and_then(|r| r.expiration_dates.clone())
84 .unwrap_or_default()
85 }
86
87 /// Map of expiration timestamp → loaded contract count (calls + puts).
88 ///
89 /// Yahoo only populates chains for the requested expiration, so this
90 /// typically covers a single date; absent expirations are omitted.
91 pub(crate) fn contract_counts(&self) -> std::collections::HashMap<i64, usize> {
92 self.first_result()
93 .map(|r| {
94 r.options
95 .iter()
96 .map(|chain| {
97 let calls = chain.calls.as_ref().map_or(0, Vec::len);
98 let puts = chain.puts.as_ref().map_or(0, Vec::len);
99 (chain.expiration_date, calls + puts)
100 })
101 .collect()
102 })
103 .unwrap_or_default()
104 }
105
106 /// Get strike prices
107 pub fn strikes(&self) -> Vec<f64> {
108 self.first_result()
109 .and_then(|r| r.strikes.clone())
110 .unwrap_or_default()
111 }
112
113 /// Get all call contracts flattened across all expirations.
114 ///
115 /// Returns a `Contracts` wrapper that supports `.to_dataframe()` when
116 /// the `dataframe` feature is enabled.
117 ///
118 /// # Example
119 /// ```ignore
120 /// let options = ticker.options(None).await?;
121 /// for call in &options.calls {
122 /// println!("{}: strike={}", call.contract_symbol, call.strike);
123 /// }
124 /// // With dataframe feature:
125 /// let df = options.calls.to_dataframe()?;
126 /// ```
127 pub fn calls(&self) -> Contracts {
128 let contracts = self
129 .first_result()
130 .map(|r| {
131 r.options
132 .iter()
133 .flat_map(|chain| chain.calls.as_deref().unwrap_or_default().iter())
134 .cloned()
135 .collect()
136 })
137 .unwrap_or_default();
138 Contracts(contracts)
139 }
140
141 /// Get all put contracts flattened across all expirations.
142 ///
143 /// Returns a `Contracts` wrapper that supports `.to_dataframe()` when
144 /// the `dataframe` feature is enabled.
145 ///
146 /// # Example
147 /// ```ignore
148 /// let options = ticker.options(None).await?;
149 /// for put in &options.puts {
150 /// println!("{}: strike={}", put.contract_symbol, put.strike);
151 /// }
152 /// // With dataframe feature:
153 /// let df = options.puts.to_dataframe()?;
154 /// ```
155 pub fn puts(&self) -> Contracts {
156 let contracts = self
157 .first_result()
158 .map(|r| {
159 r.options
160 .iter()
161 .flat_map(|chain| chain.puts.as_deref().unwrap_or_default().iter())
162 .cloned()
163 .collect()
164 })
165 .unwrap_or_default();
166 Contracts(contracts)
167 }
168}
169
170#[cfg(feature = "dataframe")]
171impl Options {
172 /// Converts all option contracts (calls and puts) to a polars DataFrame.
173 ///
174 /// Flattens all contracts across all expiration dates into a single DataFrame
175 /// with an additional `option_type` column ("call" or "put").
176 ///
177 /// For separate DataFrames, use `options.calls.to_dataframe()` or
178 /// `options.puts.to_dataframe()`.
179 pub fn to_dataframe(&self) -> ::polars::prelude::PolarsResult<::polars::prelude::DataFrame> {
180 use polars::prelude::*;
181
182 let calls = self.calls();
183 let puts = self.puts();
184
185 let mut calls_df = calls.to_dataframe()?;
186 let mut puts_df = puts.to_dataframe()?;
187
188 // Add option_type column
189 let call_types = Series::new("option_type".into(), vec!["call"; calls.len()]);
190 let put_types = Series::new("option_type".into(), vec!["put"; puts.len()]);
191
192 calls_df.with_column(call_types.into())?;
193 puts_df.with_column(put_types.into())?;
194
195 // Combine into single DataFrame
196 calls_df.vstack(&puts_df)
197 }
198}