mod common;
use kestrel_chartkit::option::{
black_76, black_scholes_merton, implied_volatility, normal_cdf, BlackScholesInputs, OptionType,
};
const GOLDEN: &str = include_str!("fixtures/golden_option_diff.txt");
const ABS_FLOOR: f64 = 1e-10;
const REL: f64 = 1e-11;
fn close_enough(ours: f64, reference: f64, label: &str) {
let tolerance = ABS_FLOOR + REL * reference.abs();
assert!(
(ours - reference).abs() <= tolerance,
"{label}: {ours} weicht von {reference} um {} ab (erlaubt {tolerance})",
(ours - reference).abs()
);
}
fn option_type(code: f64) -> OptionType {
if code > 0.0 {
OptionType::Call
} else {
OptionType::Put
}
}
#[test]
fn test_normal_cdf_matches_reference_across_body_and_tails() {
let n = common::golden_value(GOLDEN, "meta_cdf_case_count") as usize;
for i in 1..=n {
let x = common::golden_value(GOLDEN, &format!("cdf{i}_x"));
let reference = common::golden_value(GOLDEN, &format!("cdf{i}_value"));
let tolerance = 1e-16 + 1e-12 * reference.abs();
assert!(
(normal_cdf(x) - reference).abs() <= tolerance,
"N({x}): {} weicht von {reference} ab (erlaubt {tolerance})",
normal_cdf(x)
);
}
}
#[test]
fn test_black_scholes_prices_and_greeks_match_reference() {
let n = common::golden_value(GOLDEN, "meta_option_case_count") as usize;
assert!(n >= 70, "das Parameterfeld darf nicht schrumpfen");
for i in 1..=n {
let p = format!("opt{i}");
let value = |key: &str| common::golden_value(GOLDEN, &format!("{p}_{key}"));
let inputs = BlackScholesInputs {
spot: value("spot"),
strike: value("strike"),
time_to_expiry_years: value("t"),
risk_free_rate: value("r"),
dividend_yield: value("q"),
volatility: value("vol"),
};
let got = black_scholes_merton(option_type(value("type")), &inputs)
.unwrap_or_else(|e| panic!("{p}: Bewertung schlug fehl: {e:?}"));
close_enough(got.price, value("price"), &format!("{p} Preis"));
close_enough(got.greeks.delta, value("delta"), &format!("{p} Delta"));
close_enough(got.greeks.gamma, value("gamma"), &format!("{p} Gamma"));
close_enough(got.greeks.vega, value("vega"), &format!("{p} Vega"));
close_enough(
got.greeks.theta_annual,
value("theta_annual"),
&format!("{p} Theta"),
);
close_enough(got.greeks.rho, value("rho"), &format!("{p} Rho"));
}
}
#[test]
fn test_daily_theta_is_the_annual_reference_over_365() {
let n = common::golden_value(GOLDEN, "meta_option_case_count") as usize;
for i in 1..=n {
let p = format!("opt{i}");
let value = |key: &str| common::golden_value(GOLDEN, &format!("{p}_{key}"));
let inputs = BlackScholesInputs {
spot: value("spot"),
strike: value("strike"),
time_to_expiry_years: value("t"),
risk_free_rate: value("r"),
dividend_yield: value("q"),
volatility: value("vol"),
};
let got = black_scholes_merton(option_type(value("type")), &inputs).unwrap();
close_enough(
got.greeks.theta_daily,
value("theta_annual") / 365.0,
&format!("{p} Theta pro Tag"),
);
}
}
#[test]
fn test_black_76_matches_reference_black_formula() {
let n = common::golden_value(GOLDEN, "meta_black76_case_count") as usize;
assert!(n >= 70, "das Parameterfeld darf nicht schrumpfen");
for i in 1..=n {
let p = format!("b76_{i}");
let value = |key: &str| common::golden_value(GOLDEN, &format!("{p}_{key}"));
let got = black_76(
option_type(value("type")),
value("forward"),
value("strike"),
value("t"),
value("r"),
value("vol"),
)
.unwrap_or_else(|e| panic!("{p}: Bewertung schlug fehl: {e:?}"));
close_enough(got.price, value("price"), &format!("{p} Preis"));
}
}
#[test]
fn test_implied_volatility_recovers_the_reference_input_volatility() {
let n = common::golden_value(GOLDEN, "meta_option_case_count") as usize;
let mut checked = 0;
for i in 1..=n {
let p = format!("opt{i}");
let value = |key: &str| common::golden_value(GOLDEN, &format!("{p}_{key}"));
let inputs = BlackScholesInputs {
spot: value("spot"),
strike: value("strike"),
time_to_expiry_years: value("t"),
risk_free_rate: value("r"),
dividend_yield: value("q"),
volatility: value("vol"),
};
let vega = black_scholes_merton(option_type(value("type")), &inputs)
.unwrap()
.greeks
.vega;
if vega < 1e-4 {
continue;
}
let solved = implied_volatility(
option_type(value("type")),
value("price"),
inputs.spot,
inputs.strike,
inputs.time_to_expiry_years,
inputs.risk_free_rate,
inputs.dividend_yield,
)
.unwrap_or_else(|e| panic!("{p}: Umkehrung schlug fehl: {e:?}"));
assert!(
(solved - value("vol")).abs() < 1e-6,
"{p}: Umkehrung ergab {solved} statt {}",
value("vol")
);
checked += 1;
}
assert!(
checked >= 40,
"zu wenige Fälle mit auswertbarem Vega geprüft: {checked}"
);
}