use chrono::NaiveDate;
use ibapi::contracts::{OptionChain, OptionComputation};
use rust_decimal::Decimal;
use rustrade_instrument::{exchange::ExchangeId, instrument::market_data::OptionChainDescriptor};
use serde::{Deserialize, Serialize};
pub use crate::subscription::greeks::OptionGreeks;
impl OptionGreeks {
pub(crate) fn from_ib(computation: &OptionComputation) -> Self {
Self {
delta: computation.delta,
gamma: computation.gamma,
theta: computation.theta,
vega: computation.vega,
implied_volatility: computation.implied_volatility,
theoretical_price: computation.option_price,
underlying_price: computation.underlying_price,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OptionChainEntry {
pub underlying_contract_id: i32,
pub trading_class: String,
pub multiplier: String,
pub exchange: String,
pub expirations: Vec<NaiveDate>,
pub strikes: Vec<Decimal>,
}
impl OptionChainEntry {
pub fn from_ib(chain: &OptionChain) -> Self {
Self {
underlying_contract_id: chain.underlying_contract_id,
trading_class: chain.trading_class.clone(),
multiplier: chain.multiplier.clone(),
exchange: chain.exchange.clone(),
expirations: chain
.expirations
.iter()
.filter_map(|s| NaiveDate::parse_from_str(s, "%Y%m%d").ok())
.collect(),
strikes: chain
.strikes
.iter()
.filter_map(|&s| super::decimal_from_f64(s))
.collect(),
}
}
pub fn to_descriptor(&self) -> Result<OptionChainDescriptor, rust_decimal::Error> {
let multiplier = self.multiplier.parse::<Decimal>()?;
Ok(OptionChainDescriptor::new(
ExchangeId::Ibkr,
multiplier,
self.expirations.clone(),
self.strikes.clone(),
None,
))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
#[test]
fn option_greeks_from_ib_computation() {
use ibapi::contracts::{OptionComputation, tick_types::TickType};
let computation = OptionComputation {
field: TickType::ModelOption,
tick_attribute: Some(1),
delta: Some(0.55),
gamma: Some(0.02),
theta: Some(-0.05),
vega: Some(0.15),
implied_volatility: Some(0.25),
option_price: Some(5.50),
underlying_price: Some(150.0),
present_value_dividend: Some(0.0),
};
let greeks = OptionGreeks::from_ib(&computation);
assert_eq!(greeks.delta, Some(0.55));
assert_eq!(greeks.gamma, Some(0.02));
assert_eq!(greeks.theta, Some(-0.05));
assert_eq!(greeks.vega, Some(0.15));
assert_eq!(greeks.implied_volatility, Some(0.25));
assert_eq!(greeks.theoretical_price, Some(5.50));
assert_eq!(greeks.underlying_price, Some(150.0));
assert!(greeks.has_any_greek());
}
#[test]
fn option_greeks_empty_has_any_greek_false() {
let greeks = OptionGreeks::default();
assert!(!greeks.has_any_greek());
}
#[test]
fn option_chain_entry_from_ib() {
let chain = OptionChain {
underlying_contract_id: 265598,
trading_class: "AAPL".to_string(),
multiplier: "100".to_string(),
exchange: "SMART".to_string(),
expirations: vec!["20240119".to_string(), "20240216".to_string()],
strikes: vec![140.0, 145.0, 150.0, 155.0, 160.0],
};
let entry = OptionChainEntry::from_ib(&chain);
assert_eq!(entry.underlying_contract_id, 265598);
assert_eq!(entry.trading_class, "AAPL");
assert_eq!(entry.multiplier, "100");
assert_eq!(entry.exchange, "SMART");
assert_eq!(entry.expirations.len(), 2);
assert_eq!(
entry.expirations[0],
NaiveDate::from_ymd_opt(2024, 1, 19).unwrap()
);
assert_eq!(
entry.expirations[1],
NaiveDate::from_ymd_opt(2024, 2, 16).unwrap()
);
assert_eq!(entry.strikes.len(), 5);
}
#[test]
fn option_chain_entry_skips_invalid_expirations() {
let chain = OptionChain {
underlying_contract_id: 265598,
trading_class: "AAPL".to_string(),
multiplier: "100".to_string(),
exchange: "SMART".to_string(),
expirations: vec![
"20240119".to_string(),
"invalid".to_string(),
"20240216".to_string(),
],
strikes: vec![150.0],
};
let entry = OptionChainEntry::from_ib(&chain);
assert_eq!(entry.expirations.len(), 2);
}
#[test]
fn option_chain_entry_to_descriptor() {
use rust_decimal_macros::dec;
let entry = OptionChainEntry {
underlying_contract_id: 265598,
trading_class: "AAPL".to_string(),
multiplier: "100".to_string(),
exchange: "SMART".to_string(),
expirations: vec![
NaiveDate::from_ymd_opt(2024, 1, 19).unwrap(),
NaiveDate::from_ymd_opt(2024, 2, 16).unwrap(),
],
strikes: vec![dec!(145), dec!(150), dec!(155)],
};
let descriptor = entry.to_descriptor().unwrap();
assert_eq!(descriptor.exchange, ExchangeId::Ibkr);
assert_eq!(descriptor.multiplier, dec!(100));
assert_eq!(descriptor.expirations.len(), 2);
assert_eq!(descriptor.strikes.len(), 3);
assert!(descriptor.exercise.is_none());
}
#[test]
fn option_chain_entry_to_descriptor_invalid_multiplier() {
let entry = OptionChainEntry {
underlying_contract_id: 265598,
trading_class: "AAPL".to_string(),
multiplier: "not_a_number".to_string(),
exchange: "SMART".to_string(),
expirations: vec![],
strikes: vec![],
};
assert!(entry.to_descriptor().is_err());
}
}