use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiResponse {
#[serde(rename = "optionChain")]
pub option_chain: OptionChainWrapper,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptionChainWrapper {
pub result: Vec<OptionChainResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptionChainResult {
#[serde(rename = "underlyingSymbol")]
pub underlying_symbol: String,
#[serde(rename = "expirationDates")]
pub expiration_dates: Vec<i64>,
pub strikes: Vec<f64>,
#[serde(rename = "hasMiniOptions")]
pub has_mini_options: Option<bool>,
pub quote: Quote,
pub options: Vec<OptionData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Quote {
#[serde(rename = "regularMarketPrice")]
pub regular_market_price: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptionData {
#[serde(rename = "expirationDate")]
pub expiration_date: i64,
#[serde(rename = "hasMiniOptions")]
pub has_mini_options: Option<bool>,
pub calls: Vec<OptionContract>,
pub puts: Vec<OptionContract>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptionContract {
#[serde(rename = "contractSymbol")]
pub contract_symbol: String,
pub strike: f64,
pub currency: Option<String>,
#[serde(rename = "lastPrice")]
pub last_price: f64,
pub change: Option<f64>,
#[serde(rename = "percentChange")]
pub percent_change: Option<f64>,
pub volume: Option<i64>,
#[serde(rename = "openInterest")]
pub open_interest: Option<i64>,
pub bid: Option<f64>,
pub ask: Option<f64>,
#[serde(rename = "contractSize")]
pub contract_size: Option<String>,
pub expiration: i64,
#[serde(rename = "lastTradeDate")]
pub last_trade_date: Option<i64>,
#[serde(rename = "impliedVolatility")]
pub implied_volatility: f64,
#[serde(rename = "inTheMoney")]
pub in_the_money: bool,
#[serde(skip)]
pub greeks: Option<Greeks>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Greeks {
pub delta: f64,
pub gamma: f64,
pub theta: f64,
pub vega: f64,
pub rho: f64,
}
#[derive(Debug, Clone, Serialize)]
pub struct OptionChain {
pub symbol: String,
pub underlying_price: f64,
pub expiration_dates: Vec<i64>,
pub strikes: Vec<f64>,
pub options: Vec<ExpirationData>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ExpirationData {
pub expiration_date: i64,
pub calls: Vec<OptionContract>,
pub puts: Vec<OptionContract>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_option_contract_serialization() {
let contract = OptionContract {
contract_symbol: "AAPL240315C00150000".to_string(),
strike: 150.0,
currency: Some("USD".to_string()),
last_price: 5.50,
change: Some(0.25),
percent_change: Some(4.76),
volume: Some(1000),
open_interest: Some(5000),
bid: Some(5.40),
ask: Some(5.60),
contract_size: Some("REGULAR".to_string()),
expiration: 1710460800,
last_trade_date: Some(1710374400),
implied_volatility: 0.25,
in_the_money: false,
greeks: None,
};
let json = serde_json::to_string(&contract).unwrap();
assert!(json.contains("AAPL240315C00150000"));
assert!(json.contains("150"));
assert!(json.contains("5.5"));
}
#[test]
fn test_option_contract_with_greeks() {
let greeks = Greeks {
delta: 0.55,
gamma: 0.02,
theta: -0.05,
vega: 0.15,
rho: 0.10,
};
let contract = OptionContract {
contract_symbol: "AAPL240315C00150000".to_string(),
strike: 150.0,
currency: Some("USD".to_string()),
last_price: 5.50,
change: None,
percent_change: None,
volume: Some(1000),
open_interest: Some(5000),
bid: Some(5.40),
ask: Some(5.60),
contract_size: Some("REGULAR".to_string()),
expiration: 1710460800,
last_trade_date: Some(1710374400),
implied_volatility: 0.25,
in_the_money: true,
greeks: Some(greeks.clone()),
};
let json = serde_json::to_string(&contract).unwrap();
assert!(!json.contains("delta"));
assert!(!json.contains("gamma"));
}
#[test]
fn test_deserialize_yahoo_response() {
let json = r#"{
"optionChain": {
"result": [{
"underlyingSymbol": "AAPL",
"expirationDates": [1708646400],
"strikes": [150.0, 155.0, 160.0],
"quote": {"regularMarketPrice": 175.50},
"options": []
}]
}
}"#;
let response: ApiResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.option_chain.result.len(), 1);
assert_eq!(response.option_chain.result[0].underlying_symbol, "AAPL");
assert_eq!(response.option_chain.result[0].quote.regular_market_price, 175.50);
assert_eq!(response.option_chain.result[0].expiration_dates, vec![1708646400]);
assert_eq!(response.option_chain.result[0].strikes.len(), 3);
}
#[test]
fn test_deserialize_option_contract() {
let json = r#"{
"contractSymbol": "AAPL240315C00150000",
"strike": 150.0,
"lastPrice": 26.50,
"bid": 26.30,
"ask": 26.70,
"volume": 500,
"openInterest": 2500,
"impliedVolatility": 0.28,
"inTheMoney": true,
"expiration": 1710460800
}"#;
let contract: OptionContract = serde_json::from_str(json).unwrap();
assert_eq!(contract.contract_symbol, "AAPL240315C00150000");
assert_eq!(contract.strike, 150.0);
assert_eq!(contract.last_price, 26.50);
assert_eq!(contract.bid, Some(26.30));
assert_eq!(contract.ask, Some(26.70));
assert_eq!(contract.volume, Some(500));
assert_eq!(contract.open_interest, Some(2500));
assert_eq!(contract.implied_volatility, 0.28);
assert!(contract.in_the_money);
}
#[test]
fn test_greeks_serialization() {
let greeks = Greeks {
delta: 0.55,
gamma: 0.02,
theta: -0.05,
vega: 0.15,
rho: 0.10,
};
let json = serde_json::to_string(&greeks).unwrap();
assert!(json.contains("0.55"));
assert!(json.contains("0.02"));
assert!(json.contains("-0.05"));
assert!(json.contains("0.15"));
assert!(json.contains("0.1")); }
#[test]
fn test_filter_itm_calls() {
let contracts = [
OptionContract {
contract_symbol: "ITM".to_string(),
strike: 90.0,
in_the_money: true,
currency: None,
last_price: 10.0,
change: None,
percent_change: None,
volume: None,
open_interest: None,
bid: None,
ask: None,
contract_size: None,
expiration: 0,
last_trade_date: None,
implied_volatility: 0.2,
greeks: None,
},
OptionContract {
contract_symbol: "OTM".to_string(),
strike: 110.0,
in_the_money: false,
currency: None,
last_price: 1.0,
change: None,
percent_change: None,
volume: None,
open_interest: None,
bid: None,
ask: None,
contract_size: None,
expiration: 0,
last_trade_date: None,
implied_volatility: 0.2,
greeks: None,
},
];
let itm: Vec<_> = contracts.iter().filter(|c| c.in_the_money).collect();
assert_eq!(itm.len(), 1);
assert_eq!(itm[0].strike, 90.0);
}
#[test]
fn test_filter_strike_range() {
let contracts = [
OptionContract {
contract_symbol: "C1".to_string(),
strike: 140.0,
currency: None,
last_price: 1.0,
change: None,
percent_change: None,
volume: None,
open_interest: None,
bid: None,
ask: None,
contract_size: None,
expiration: 0,
last_trade_date: None,
implied_volatility: 0.2,
in_the_money: false,
greeks: None,
},
OptionContract {
contract_symbol: "C2".to_string(),
strike: 150.0,
currency: None,
last_price: 1.0,
change: None,
percent_change: None,
volume: None,
open_interest: None,
bid: None,
ask: None,
contract_size: None,
expiration: 0,
last_trade_date: None,
implied_volatility: 0.2,
in_the_money: false,
greeks: None,
},
OptionContract {
contract_symbol: "C3".to_string(),
strike: 160.0,
currency: None,
last_price: 1.0,
change: None,
percent_change: None,
volume: None,
open_interest: None,
bid: None,
ask: None,
contract_size: None,
expiration: 0,
last_trade_date: None,
implied_volatility: 0.2,
in_the_money: false,
greeks: None,
},
OptionContract {
contract_symbol: "C4".to_string(),
strike: 170.0,
currency: None,
last_price: 1.0,
change: None,
percent_change: None,
volume: None,
open_interest: None,
bid: None,
ask: None,
contract_size: None,
expiration: 0,
last_trade_date: None,
implied_volatility: 0.2,
in_the_money: false,
greeks: None,
},
];
let filtered: Vec<_> = contracts
.iter()
.filter(|c| c.strike >= 150.0 && c.strike <= 160.0)
.collect();
assert_eq!(filtered.len(), 2);
assert_eq!(filtered[0].strike, 150.0);
assert_eq!(filtered[1].strike, 160.0);
}
#[test]
fn test_option_chain_serialization() {
let chain = OptionChain {
symbol: "AAPL".to_string(),
underlying_price: 175.50,
expiration_dates: vec![1710460800, 1711065600],
strikes: vec![150.0, 155.0, 160.0],
options: vec![],
};
let json = serde_json::to_string(&chain).unwrap();
assert!(json.contains("AAPL"));
assert!(json.contains("175.5"));
}
}