futures_option/
futures_option.rs1mod common;
7
8use chrono::NaiveDate;
9use rustyqlib::core::trade::PutOrCall;
10use rustyqlib::core::traits::Instrument;
11use rustyqlib::equity::black76::{price, FuturesSettlement};
12use rustyqlib::equity::blackscholes::bs_price;
13use rustyqlib::equity::builder::EquityOptionBuilder;
14use rustyqlib::equity::utils::Engine;
15use rustyqlib::equity::vanila_option::EquityOption;
16
17const F: f64 = 100.0; const K: f64 = 100.0;
19const VOL: f64 = 0.30;
20const R: f64 = 0.05;
21const T: f64 = 1.0;
22
23fn futures_option(pc: PutOrCall, settlement: FuturesSettlement) -> EquityOption {
24 EquityOptionBuilder::new()
25 .symbol("FUT")
26 .spot(F) .strike(K)
28 .flat_vol(VOL)
29 .flat_rate(R)
30 .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
31 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
32 .vanilla(pc)
33 .on_future(settlement)
34 .engine(Engine::BlackScholes)
35 .build()
36}
37
38fn main() {
39 common::title("OPTIONS ON FUTURES (Black-76) — F=100 K=100 sigma=30% r=5% T=1y");
40
41 for (name, settlement) in [
42 ("Discounted (standard Black-76)", FuturesSettlement::Discounted),
43 ("Margined (futures-style)", FuturesSettlement::Margined),
44 ] {
45 common::section(name);
46 common::table_header();
47 common::row("call", &futures_option(PutOrCall::Call, settlement));
48 common::row("put", &futures_option(PutOrCall::Put, settlement));
49 }
50 common::note("margined has zero rho (no discounting) and a larger vega/theta");
51
52 common::section("Settlement effect: margined = discounted / e^{-rT}");
53 let disc = futures_option(PutOrCall::Call, FuturesSettlement::Discounted).npv();
54 let marg = futures_option(PutOrCall::Call, FuturesSettlement::Margined).npv();
55 println!(" discounted call {disc:.6} margined call {marg:.6} ratio {:.6} (= e^rT {:.6})",
56 marg / disc, (R * T).exp());
57
58 common::section("Identities");
59 let dc = futures_option(PutOrCall::Call, FuturesSettlement::Discounted);
60 let dp = futures_option(PutOrCall::Put, FuturesSettlement::Discounted);
61 common::check(
62 "discounted parity C - P = e^{-rT}(F - K)",
63 dc.npv() - dp.npv(),
64 (-R * T).exp() * (F - K),
65 1e-10,
66 );
67 let mc = futures_option(PutOrCall::Call, FuturesSettlement::Margined);
68 let mp = futures_option(PutOrCall::Put, FuturesSettlement::Margined);
69 common::check("margined parity C - P = F - K", mc.npv() - mp.npv(), F - K, 1e-10);
70 common::check(
71 "margined rho is exactly zero",
72 futures_option(PutOrCall::Call, FuturesSettlement::Margined).rho(),
73 0.0,
74 1e-15,
75 );
76
77 common::section("Black-76 on the forward reproduces spot Black-Scholes");
78 let (s, q) = (100.0, 0.02);
80 let fwd = s * ((R - q) * T).exp();
81 let on_forward = price(fwd, K, R, VOL, T, PutOrCall::Call, FuturesSettlement::Discounted);
82 let spot_bsm = bs_price(s, K, R, q, VOL, T, PutOrCall::Call);
83 common::check("black76(F = S e^{(r-q)T}) = BSM(S, q)", on_forward, spot_bsm, 1e-10);
84
85 common::section("Skew across strikes (discounted put)");
86 common::table_header();
87 for k in [80.0, 90.0, 100.0, 110.0, 120.0] {
88 common::row(
89 &format!("K = {k}"),
90 &EquityOptionBuilder::new()
91 .spot(F)
92 .strike(k)
93 .flat_vol(VOL)
94 .flat_rate(R)
95 .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
96 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
97 .vanilla(PutOrCall::Put)
98 .on_future(FuturesSettlement::Discounted)
99 .engine(Engine::BlackScholes)
100 .build(),
101 );
102 }
103 println!();
104}