finance_query_core/models/
options.rs1use chrono::{DateTime, NaiveDate, TimeZone, Utc};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
80#[serde(rename_all = "camelCase")]
81pub struct OptionExpirations {
82 pub symbol: String,
83 pub expirations: Vec<String>, }
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#[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
195pub(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}