use serde::{Deserialize, Serialize};
use crate::error::{Result, YfError};
use crate::http::YfSession;
use crate::json::{get, get_f64, get_str, yf_result_first};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OptionContract {
pub contract_symbol: Option<String>,
pub strike: Option<f64>,
pub currency: Option<String>,
pub last_price: Option<f64>,
pub bid: Option<f64>,
pub ask: Option<f64>,
pub change: Option<f64>,
pub percent_change: Option<f64>,
pub volume: Option<f64>,
pub open_interest: Option<f64>,
pub implied_volatility: Option<f64>,
pub in_the_money: Option<bool>,
pub last_trade_date: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExpirationOptions {
pub expiration: String,
pub calls: Vec<OptionContract>,
pub puts: Vec<OptionContract>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptionChain {
pub ticker: String,
pub expirations: Vec<String>,
pub strikes: Vec<f64>,
pub options: Vec<ExpirationOptions>,
}
impl OptionChain {
pub fn expiration_dates(&self) -> &[String] {
&self.expirations
}
}
impl YfSession {
pub async fn option_chain(&self, ticker: &str) -> Result<OptionChain> {
let urls = YfSession::urls();
let url = format!("{}/v7/finance/options/{}", urls.query2, ticker);
let value = self.get_json(&url, &[]).await?;
let result = yf_result_first(&value, "optionChain")
.map_err(|_| YfError::DataMissing(format!("optionChain.result for {ticker}")))?;
let expirations = result
.get("expirationDates")
.and_then(|e| e.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_i64())
.filter_map(|sec| chrono::DateTime::from_timestamp(sec, 0))
.map(|dt| dt.naive_utc().format("%Y-%m-%d").to_string())
.collect()
})
.unwrap_or_default();
let strikes = result
.get("strikes")
.and_then(|s| s.as_array())
.map(|a| a.iter().filter_map(|v| v.as_f64()).collect())
.unwrap_or_default();
let options = result
.get("options")
.and_then(|o| o.as_array())
.map(|a| {
a.iter()
.map(|o| {
let exp = o
.get("expirationDate")
.and_then(|e| e.as_i64())
.and_then(|sec| chrono::DateTime::from_timestamp(sec, 0))
.map(|dt| dt.naive_utc().format("%Y-%m-%d").to_string())
.unwrap_or_default();
ExpirationOptions {
expiration: exp,
calls: o
.get("calls")
.and_then(|c| c.as_array())
.map(|c| c.iter().filter_map(parse_contract).collect())
.unwrap_or_default(),
puts: o
.get("puts")
.and_then(|p| p.as_array())
.map(|p| p.iter().filter_map(parse_contract).collect())
.unwrap_or_default(),
}
})
.collect()
})
.unwrap_or_default();
Ok(OptionChain {
ticker: ticker.to_string(),
expirations,
strikes,
options,
})
}
}
fn parse_contract(v: &serde_json::Value) -> Option<OptionContract> {
let f = |p: &str| get_f64(v, &[p]);
let s = |p: &str| get_str(v, &[p]);
let b = |p: &str| get(v, &[p]).and_then(|x| x.as_bool());
let trade = get(v, &["lastTradeDate"])
.and_then(|x| x.as_i64())
.and_then(|sec| chrono::DateTime::from_timestamp(sec, 0))
.map(|dt| dt.naive_utc().format("%Y-%m-%d %H:%M:%S").to_string());
Some(OptionContract {
contract_symbol: s("contractSymbol"),
strike: f("strike"),
currency: s("currency"),
last_price: f("lastPrice"),
bid: f("bid"),
ask: f("ask"),
change: f("change"),
percent_change: f("percentChange"),
volume: f("volume"),
open_interest: f("openInterest"),
implied_volatility: f("impliedVolatility"),
in_the_money: b("inTheMoney"),
last_trade_date: trade,
})
}