use chrono::NaiveDate;
use rust_decimal::{Decimal, prelude::ToPrimitive};
use rustrade_instrument::instrument::{
kind::option::OptionKind, market_data::kind::MarketDataOptionContract,
};
use smol_str::{SmolStr, StrExt, format_smolstr};
const SUFFIX_LEN: usize = 15;
const MAX_STRIKE_THOUSANDTHS: u64 = 99_999_999;
pub(crate) fn symbol(root: &str, contract: &MarketDataOptionContract) -> Option<SmolStr> {
let thousandths = contract.strike.checked_mul(Decimal::ONE_THOUSAND)?;
if !thousandths.fract().is_zero() || thousandths <= Decimal::ZERO {
return None;
}
let thousandths = thousandths
.to_u64()
.filter(|t| *t <= MAX_STRIKE_THOUSANDTHS)?;
let right = match contract.kind {
OptionKind::Call => 'C',
OptionKind::Put => 'P',
};
Some(format_smolstr!(
"{}{}{right}{thousandths:08}",
root.to_uppercase_smolstr(),
contract.expiry.date_naive().format("%y%m%d"),
))
}
pub(crate) fn root(symbol: &str) -> Option<&str> {
let split = symbol.len().checked_sub(SUFFIX_LEN)?;
let (root, suffix) = (symbol.get(..split)?, symbol.get(split..)?.as_bytes());
let well_formed = !root.is_empty()
&& !root.contains(char::is_whitespace)
&& suffix[..6].iter().all(u8::is_ascii_digit)
&& matches!(suffix[6], b'C' | b'P')
&& suffix[7..].iter().all(u8::is_ascii_digit);
well_formed.then_some(root)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Parsed<'a> {
pub(crate) root: &'a str,
pub(crate) expiry: NaiveDate,
pub(crate) kind: OptionKind,
pub(crate) strike: Decimal,
}
pub(crate) fn parse(symbol: &str) -> Option<Parsed<'_>> {
let root = root(symbol)?;
let suffix = &symbol[root.len()..];
let number = |range: std::ops::Range<usize>| suffix[range].parse::<u32>().ok();
let year = 2000 + i32::try_from(number(0..2)?).ok()?;
let expiry = NaiveDate::from_ymd_opt(year, number(2..4)?, number(4..6)?)?;
let kind = match suffix.as_bytes()[6] {
b'C' => OptionKind::Call,
_ => OptionKind::Put,
};
let thousandths = number(7..SUFFIX_LEN).filter(|thousandths| *thousandths > 0)?;
Some(Parsed {
root,
expiry,
kind,
strike: Decimal::new(i64::from(thousandths), 3).normalize(),
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use chrono::{DateTime, Utc};
use rust_decimal_macros::dec;
use rustrade_instrument::instrument::kind::option::OptionExercise;
fn contract(kind: OptionKind, expiry: &str, strike: Decimal) -> MarketDataOptionContract {
MarketDataOptionContract {
kind,
exercise: OptionExercise::American,
expiry: expiry.parse::<DateTime<Utc>>().unwrap(),
strike,
}
}
#[test]
fn a_contract_is_spelled_unpadded_in_thousandths() {
let call = contract(OptionKind::Call, "2026-09-30T20:00:00Z", dec!(700));
assert_eq!(symbol("spy", &call).unwrap(), "SPY260930C00700000");
let put = contract(OptionKind::Put, "2026-09-25T00:00:00Z", dec!(327.5));
assert_eq!(symbol("AAPL", &put).unwrap(), "AAPL260925P00327500");
}
#[test]
fn a_fractional_strike_is_spelled_to_the_thousandth() {
let call = contract(OptionKind::Call, "2026-10-16T20:00:00Z", dec!(2.67));
assert_eq!(symbol("F", &call).unwrap(), "F261016C00002670");
}
#[test]
fn a_strike_osi_cannot_carry_has_no_symbol() {
for strike in [dec!(1.2345), dec!(0), dec!(-5), dec!(100000)] {
let call = contract(OptionKind::Call, "2026-09-30T20:00:00Z", strike);
assert_eq!(symbol("SPY", &call), None, "strike {strike}");
}
}
#[test]
fn the_expiry_date_is_read_in_utc() {
for expiry in ["2026-09-30T00:00:00Z", "2026-09-30T21:00:00Z"] {
let call = contract(OptionKind::Call, expiry, dec!(700));
assert_eq!(symbol("SPY", &call).unwrap(), "SPY260930C00700000");
}
let late = contract(OptionKind::Call, "2026-10-01T03:59:00Z", dec!(700));
assert_eq!(symbol("SPY", &late).unwrap(), "SPY261001C00700000");
}
#[test]
fn a_root_is_read_back_from_a_symbol() {
assert_eq!(root("SPY260930C00700000"), Some("SPY"));
assert_eq!(root("F261016P00002670"), Some("F"));
assert_eq!(root("BRK.B261016C00450000"), Some("BRK.B"));
}
#[test]
fn a_symbol_that_is_not_osi_has_no_root() {
for symbol in [
"SPY",
"EUR/USD",
"260930C00700000",
"SPY260930X00700000",
"SPY26093AC00700000",
"SPY260930C0070000",
"SPY 260930C00700000",
"SPY (not an option contract: spot)",
] {
assert_eq!(root(symbol), None, "{symbol}");
}
}
#[test]
fn a_symbol_is_read_back_into_its_parts_exactly() {
let put = parse("SPY261130P00505000").unwrap();
assert_eq!(put.root, "SPY");
assert_eq!(put.expiry, NaiveDate::from_ymd_opt(2026, 11, 30).unwrap());
assert_eq!(put.kind, OptionKind::Put);
assert_eq!(put.strike.to_string(), "505");
let call = parse("F261016C00002670").unwrap();
assert_eq!(call.kind, OptionKind::Call);
assert_eq!(call.strike.to_string(), "2.67");
assert_eq!(parse("BRK.B261016C00450500").unwrap().root, "BRK.B");
}
#[test]
fn a_symbol_that_cannot_be_spelled_back_does_not_parse() {
for symbol in [
"SPY",
"SPY260930X00700000",
"SPY260930C0070000",
"SPY260931C00700000",
"SPY261330C00700000",
"SPY260930C00000000",
] {
assert_eq!(parse(symbol), None, "{symbol}");
}
}
#[test]
fn a_parsed_symbol_spells_itself_back() {
for osi in [
"SPY261130P00505000",
"F261016C00002670",
"BRK.B261016C00450500",
] {
let parsed = parse(osi).unwrap();
let contract = MarketDataOptionContract {
kind: parsed.kind,
exercise: OptionExercise::American,
expiry: parsed.expiry.and_hms_opt(20, 0, 0).unwrap().and_utc(),
strike: parsed.strike,
};
assert_eq!(symbol(parsed.root, &contract).unwrap(), osi);
}
}
#[test]
fn a_spelled_symbol_reads_back_to_its_root() {
let call = contract(OptionKind::Call, "2026-09-30T20:00:00Z", dec!(700));
let spelled = symbol("qqq", &call).unwrap();
assert_eq!(root(&spelled), Some("QQQ"));
}
}