finance_query_core/models/
options.rs

1use chrono::{DateTime, NaiveDate, TimeZone, Utc};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5/// Represents a single option contract
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub struct OptionContract {
9    pub contract_symbol: String,
10    pub last_trade_date: DateTime<Utc>,
11    pub strike: f64,
12    pub last_price: f64,
13    pub bid: f64,
14    pub ask: f64,
15    pub change: f64,
16    pub percent_change: f64,
17    pub volume: Option<u64>,
18    pub open_interest: Option<u64>,
19    pub implied_volatility: f64,
20    pub in_the_money: bool,
21    pub contract_size: String,
22    pub currency: String,
23}
24
25/// Complete option chain for a symbol
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct OptionChain {
29    pub symbol: String,
30    pub expiration_date: String,
31    pub calls: Vec<OptionContract>,
32    pub puts: Vec<OptionContract>,
33    pub underlying_price: Option<f64>,
34}
35
36impl OptionChain {
37    pub(crate) fn from_yahoo_response(
38        symbol: String,
39        expiration_date: String,
40        response: YahooOptionsResponse,
41    ) -> Result<Self, crate::client::YahooError> {
42        let result = response.option_chain.result.first().ok_or_else(|| {
43            crate::client::YahooError::ParseError("No option chain data in response".to_string())
44        })?;
45
46        let options_data = result.options.first().ok_or_else(|| {
47            crate::client::YahooError::ParseError(
48                "No options data for expiration".to_string(),
49            )
50        })?;
51
52        let calls = options_data
53            .calls
54            .iter()
55            .map(parse_option_contract)
56            .collect::<Result<Vec<_>, _>>()?;
57
58        let puts = options_data
59            .puts
60            .iter()
61            .map(parse_option_contract)
62            .collect::<Result<Vec<_>, _>>()?;
63
64        let underlying_price = result
65            .quote
66            .as_ref()
67            .and_then(|q| q.get("regularMarketPrice"))
68            .and_then(|p| p.as_f64());
69
70        Ok(Self {
71            symbol,
72            expiration_date,
73            calls,
74            puts,
75            underlying_price,
76        })
77    }
78}
79
80/// List of available expiration dates
81#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct OptionExpirations {
84    pub symbol: String,
85    pub expirations: Vec<String>, // YYYY-MM-DD format
86}
87
88impl OptionExpirations {
89    pub(crate) fn from_yahoo_response(
90        symbol: String,
91        response: YahooOptionsResponse,
92    ) -> Result<Self, crate::client::YahooError> {
93        let result = response.option_chain.result.first().ok_or_else(|| {
94            crate::client::YahooError::ParseError("No option chain data".to_string())
95        })?;
96
97        let expirations = result
98            .expiration_dates
99            .iter()
100            .map(|&ts| {
101                Utc.timestamp_opt(ts, 0)
102                    .single()
103                    .map(|dt| dt.format("%Y-%m-%d").to_string())
104                    .ok_or_else(|| {
105                        crate::client::YahooError::ParseError(
106                            "Invalid expiration timestamp".to_string(),
107                        )
108                    })
109            })
110            .collect::<Result<Vec<_>, _>>()?;
111
112        Ok(Self {
113            symbol,
114            expirations,
115        })
116    }
117}
118
119fn parse_option_contract(
120    contract: &YahooOptionContract,
121) -> Result<OptionContract, crate::client::YahooError> {
122    Ok(OptionContract {
123        contract_symbol: contract.contract_symbol.clone(),
124        last_trade_date: Utc
125            .timestamp_opt(contract.last_trade_date, 0)
126            .single()
127            .ok_or_else(|| {
128                crate::client::YahooError::ParseError("Invalid last trade date".to_string())
129            })?,
130        strike: contract.strike,
131        last_price: contract.last_price,
132        bid: contract.bid,
133        ask: contract.ask,
134        change: contract.change,
135        percent_change: contract.percent_change,
136        volume: contract.volume,
137        open_interest: contract.open_interest,
138        implied_volatility: contract.implied_volatility,
139        in_the_money: contract.in_the_money,
140        contract_size: contract.contract_size.clone(),
141        currency: contract.currency.clone(),
142    })
143}
144
145// Internal Yahoo response structures
146#[derive(Debug, Deserialize)]
147pub(crate) struct YahooOptionsResponse {
148    #[serde(rename = "optionChain")]
149    pub option_chain: OptionChainData,
150}
151
152#[derive(Debug, Deserialize)]
153pub(crate) struct OptionChainData {
154    pub result: Vec<OptionChainResult>,
155}
156
157#[derive(Debug, Deserialize)]
158pub(crate) struct OptionChainResult {
159    #[serde(rename = "expirationDates")]
160    pub expiration_dates: Vec<i64>,
161    pub options: Vec<OptionsData>,
162    pub quote: Option<Value>,
163}
164
165#[derive(Debug, Deserialize)]
166pub(crate) struct OptionsData {
167    pub calls: Vec<YahooOptionContract>,
168    pub puts: Vec<YahooOptionContract>,
169}
170
171#[derive(Debug, Deserialize)]
172pub(crate) struct YahooOptionContract {
173    #[serde(rename = "contractSymbol")]
174    pub contract_symbol: String,
175    #[serde(rename = "lastTradeDate")]
176    pub last_trade_date: i64,
177    pub strike: f64,
178    #[serde(rename = "lastPrice")]
179    pub last_price: f64,
180    pub bid: f64,
181    pub ask: f64,
182    pub change: f64,
183    #[serde(rename = "percentChange")]
184    pub percent_change: f64,
185    pub volume: Option<u64>,
186    #[serde(rename = "openInterest")]
187    pub open_interest: Option<u64>,
188    #[serde(rename = "impliedVolatility")]
189    pub implied_volatility: f64,
190    #[serde(rename = "inTheMoney")]
191    pub in_the_money: bool,
192    #[serde(rename = "contractSize")]
193    pub contract_size: String,
194    pub currency: String,
195}
196
197/// Helper to convert date string to Unix timestamp
198pub(crate) fn date_to_timestamp(date_str: &str) -> Result<i64, crate::client::YahooError> {
199    NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
200        .map_err(|e| crate::client::YahooError::ParseError(format!("Invalid date format: {}", e)))?
201        .and_hms_opt(0, 0, 0)
202        .ok_or_else(|| crate::client::YahooError::ParseError("Invalid time".to_string()))?
203        .and_local_timezone(Utc)
204        .single()
205        .ok_or_else(|| crate::client::YahooError::ParseError("Invalid timezone".to_string()))
206        .map(|dt| dt.timestamp())
207}