Skip to main content

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