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(
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#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct OptionExpirations {
84 pub symbol: String,
85 pub expirations: Vec<String>, }
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#[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
197pub(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}