Skip to main content

futures_option/
futures_option.rs

1//! Options on futures priced with Black-76: standard (discounted, premium
2//! paid up front) and futures-style (margined, undiscounted).
3//!
4//! Run with:  cargo run --release --example futures_option
5
6mod 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; // futures price
18const 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) // the futures price F
27        .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    // an option on F = S e^{(r-q)T} equals the equivalent spot option
79    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}