Skip to main content

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}