use ibapi::contracts::{Contract, OptionRight, SecurityType};
pub fn build_option_contract(
symbol: &str,
expiry_ymd: (u16, u8, u8),
strike: f64,
is_call: bool,
exchange: &str,
) -> Contract {
let (year, month, day) = expiry_ymd;
let expiry_str = format!("{year:04}{month:02}{day:02}");
let right = if is_call {
OptionRight::Call
} else {
OptionRight::Put
};
Contract {
symbol: symbol.into(),
security_type: SecurityType::Option,
exchange: exchange.into(),
currency: "USD".into(),
last_trade_date_or_contract_month: expiry_str,
strike,
right: Some(right),
multiplier: "100".into(),
..Default::default()
}
}
pub fn parse_expiry(expiry: &str) -> Result<(u16, u8, u8), ParseExpiryError> {
if expiry.len() != 8 {
return Err(ParseExpiryError(format!(
"invalid expiry format: {expiry}"
)));
}
let year: u16 = expiry[0..4]
.parse()
.map_err(|e| ParseExpiryError(format!("invalid year in {expiry}: {e}")))?;
let month: u8 = expiry[4..6]
.parse()
.map_err(|e| ParseExpiryError(format!("invalid month in {expiry}: {e}")))?;
let day: u8 = expiry[6..8]
.parse()
.map_err(|e| ParseExpiryError(format!("invalid day in {expiry}: {e}")))?;
Ok((year, month, day))
}
#[derive(Debug)]
pub struct ParseExpiryError(pub String);
impl std::fmt::Display for ParseExpiryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for ParseExpiryError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_expiry_valid() {
let (y, m, d) = parse_expiry("20260731").unwrap();
assert_eq!(y, 2026);
assert_eq!(m, 7);
assert_eq!(d, 31);
}
#[test]
fn parse_expiry_invalid_short() {
assert!(parse_expiry("bad").is_err());
}
#[test]
fn parse_expiry_invalid_chars() {
assert!(parse_expiry("2026abcd").is_err());
}
#[test]
fn parse_expiry_error_display() {
let e = parse_expiry("bad").unwrap_err();
assert!(!e.to_string().is_empty());
}
#[test]
fn parse_expiry_january() {
let (y, m, d) = parse_expiry("20260103").unwrap();
assert_eq!(y, 2026);
assert_eq!(m, 1);
assert_eq!(d, 3);
}
#[test]
fn build_put_contract_shape() {
let contract = build_option_contract("SPY", (2026, 7, 17), 700.0, false, "SMART");
assert_eq!(contract.symbol.0, "SPY");
assert_eq!(contract.security_type, SecurityType::Option);
assert_eq!(contract.exchange, "SMART");
assert_eq!(contract.currency, "USD");
assert_eq!(contract.last_trade_date_or_contract_month, "20260717");
assert!((contract.strike - 700.0).abs() < 0.01);
assert_eq!(contract.right, Some(OptionRight::Put));
assert_eq!(contract.multiplier, "100");
}
#[test]
fn build_call_contract_shape() {
let contract = build_option_contract("QQQ", (2026, 8, 21), 380.0, true, "CBOE");
assert_eq!(contract.symbol.0, "QQQ");
assert_eq!(contract.exchange, "CBOE");
assert_eq!(contract.last_trade_date_or_contract_month, "20260821");
assert_eq!(contract.right, Some(OptionRight::Call));
}
#[test]
fn build_contract_defaults_rest() {
let contract = build_option_contract("AAPL", (2027, 1, 15), 200.0, false, "SMART");
assert_eq!(contract.contract_id, 0);
}
}