Skip to main content

binary_option/
binary_option.rs

1//! Binary (digital) options: cash-or-nothing and asset-or-nothing.
2//!
3//! Run with:  cargo run --release --example binary_option
4
5mod common;
6
7use chrono::NaiveDate;
8use rustyqlib::core::trade::PutOrCall;
9use rustyqlib::core::traits::Instrument;
10use rustyqlib::equity::builder::EquityOptionBuilder;
11use rustyqlib::equity::utils::Engine;
12use rustyqlib::equity::vanila_option::BinaryType;
13
14const SPOT: f64 = 100.0;
15const STRIKE: f64 = 100.0;
16const VOL: f64 = 0.30;
17const RATE: f64 = 0.05;
18const DIV: f64 = 0.02;
19const CASH: f64 = 1.0;
20
21fn base() -> EquityOptionBuilder {
22    EquityOptionBuilder::new()
23        .symbol("BINARY")
24        .spot(SPOT)
25        .strike(STRIKE)
26        .flat_vol(VOL)
27        .flat_rate(RATE)
28        .dividend_yield(DIV)
29        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
30        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
31}
32
33fn main() {
34    common::title("BINARY OPTION — S=100 K=100 sigma=30% r=5% q=2% T=1y");
35
36    for (name, binary_type, cash) in [
37        ("cash-or-nothing (1 unit)", BinaryType::CashOrNothing, CASH),
38        ("asset-or-nothing", BinaryType::AssetOrNothing, 0.0),
39    ] {
40        for pc in [PutOrCall::Call, PutOrCall::Put] {
41            common::section(&format!("{name} {pc:?}"));
42            common::table_header();
43            for (label, engine) in [
44                ("Analytical (closed form)", Engine::BlackScholes),
45                ("Binomial (1000 steps)", Engine::Binomial),
46                ("Finite difference", Engine::FiniteDifference),
47                ("Monte Carlo (Sobol, 100k)", Engine::MonteCarlo),
48            ] {
49                common::row(label, &base().binary(pc, binary_type, cash).engine(engine).build());
50            }
51        }
52    }
53    common::note("the tree oscillates on digitals: the strike falls between terminal nodes");
54
55    common::section("Cash amount scales linearly");
56    common::table_header();
57    for cash in [1.0, 100.0, 1000.0] {
58        common::row(
59            &format!("cash-or-nothing call, cash={cash}"),
60            &base()
61                .binary(PutOrCall::Call, BinaryType::CashOrNothing, cash)
62                .engine(Engine::BlackScholes)
63                .build(),
64        );
65    }
66
67    common::section("Identities");
68    let cash_call = base()
69        .binary(PutOrCall::Call, BinaryType::CashOrNothing, CASH)
70        .engine(Engine::BlackScholes)
71        .build();
72    let cash_put = base()
73        .binary(PutOrCall::Put, BinaryType::CashOrNothing, CASH)
74        .engine(Engine::BlackScholes)
75        .build();
76    common::check(
77        "cash call + cash put = e^{-rT}",
78        cash_call.npv() + cash_put.npv(),
79        (-RATE * 1.0_f64).exp(),
80        1e-12,
81    );
82
83    let asset_call = base()
84        .binary(PutOrCall::Call, BinaryType::AssetOrNothing, 0.0)
85        .engine(Engine::BlackScholes)
86        .build();
87    let asset_put = base()
88        .binary(PutOrCall::Put, BinaryType::AssetOrNothing, 0.0)
89        .engine(Engine::BlackScholes)
90        .build();
91    common::check(
92        "asset call + asset put = S e^{-qT}",
93        asset_call.npv() + asset_put.npv(),
94        SPOT * (-DIV * 1.0_f64).exp(),
95        1e-10,
96    );
97
98    common::section("Replication: asset digital = vanilla call + K cash digitals");
99    let vanilla = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build();
100    let k_cash = base()
101        .binary(PutOrCall::Call, BinaryType::CashOrNothing, STRIKE)
102        .engine(Engine::BlackScholes)
103        .build();
104    common::check("npv", asset_call.npv(), vanilla.npv() + k_cash.npv(), 1e-10);
105    common::check("delta", asset_call.delta(), vanilla.delta() + k_cash.delta(), 1e-10);
106    common::check("gamma", asset_call.gamma(), vanilla.gamma() + k_cash.gamma(), 1e-10);
107    common::check("vega", asset_call.vega(), vanilla.vega() + k_cash.vega(), 1e-10);
108    common::check("theta", asset_call.theta(), vanilla.theta() + k_cash.theta(), 1e-10);
109    common::check("rho", asset_call.rho(), vanilla.rho() + k_cash.rho(), 1e-10);
110    common::note("both sides are implemented independently, so this is a real cross-check");
111
112    common::section("Digital risk: delta and gamma explode near the strike at expiry");
113    common::table_header();
114    for years in [1.0, 0.25, 0.05, 0.01] {
115        common::row(
116            &format!("cash-or-nothing call, T={years}y"),
117            &base()
118                .years_to_maturity(years)
119                .binary(PutOrCall::Call, BinaryType::CashOrNothing, CASH)
120                .engine(Engine::BlackScholes)
121                .build(),
122        );
123    }
124
125    digital_greek_surfaces();
126    println!();
127}
128
129/// The digital's Greeks are the standout case for visualizing (lack of)
130/// smoothness: as maturity shrinks the delta spikes into a tall bump at the
131/// strike and gamma flips sign right across it. Saved as self-contained
132/// interactive HTML to `runs/binary_option/`.
133fn digital_greek_surfaces() {
134    use common::plot3d::{greek_surface, linspace, save_surface_html, Labels};
135    use rustyqlib::equity::vanila_option::EquityOption;
136
137    common::section("Digital Greek surfaces over (moneyness, maturity) -> runs/binary_option/*.html");
138
139    // tighter moneyness band and shorter maturities: that is where the
140    // digital's delta/gamma structure lives
141    let moneyness = linspace(0.8, 1.2, 80);
142    let mats = linspace(0.02, 1.0, 60);
143
144    let greek = |select: fn(&EquityOption) -> f64| {
145        move |m: f64, years: f64| -> f64 {
146            let option = base()
147                .spot(m * STRIKE)
148                .years_to_maturity(years)
149                .binary(PutOrCall::Call, BinaryType::CashOrNothing, CASH)
150                .engine(Engine::BlackScholes)
151                .build();
152            select(&option)
153        }
154    };
155
156    for (name, file, select) in [
157        ("Delta", "delta", (|o: &EquityOption| o.delta()) as fn(&EquityOption) -> f64),
158        ("Gamma", "gamma", |o: &EquityOption| o.gamma()),
159    ] {
160        let surface = greek_surface(&moneyness, &mats, greek(select));
161        save_surface_html(
162            &surface,
163            &format!("runs/binary_option/{file}_surface.html"),
164            &Labels {
165                title: &format!("Cash digital call {name} (K=100) — note the near-expiry spike"),
166                x: "moneyness (S/K)",
167                y: "maturity (y)",
168                z: name,
169            },
170        );
171    }
172    common::note("contrast with the vanilla surfaces: the digital is far from smooth near the strike");
173}