Skip to main content

vanilla_option/
vanilla_option.rs

1//! Vanilla European and American options across every pricing engine.
2//!
3//! Run with:  cargo run --release --example vanilla_option
4
5mod common;
6
7use chrono::NaiveDate;
8use rustyqlib::core::trade::PutOrCall;
9use rustyqlib::core::traits::Instrument;
10use rustyqlib::equity::blackscholes::bs_price;
11use rustyqlib::equity::builder::EquityOptionBuilder;
12use rustyqlib::equity::montecarlo::{McModel, Sampler};
13use rustyqlib::equity::utils::Engine;
14use rustyqlib::equity::vanila_option::EquityOption;
15
16const SPOT: f64 = 100.0;
17const STRIKE: f64 = 100.0;
18const VOL: f64 = 0.30;
19const RATE: f64 = 0.05;
20const DIV: f64 = 0.02;
21
22fn asof() -> NaiveDate {
23    NaiveDate::from_ymd_opt(2026, 1, 1).unwrap()
24}
25
26fn base(put_or_call: PutOrCall) -> EquityOptionBuilder {
27    EquityOptionBuilder::new()
28        .symbol("VANILLA")
29        .spot(SPOT)
30        .strike(STRIKE)
31        .flat_vol(VOL)
32        .flat_rate(RATE)
33        .dividend_yield(DIV)
34        .valuation_date(asof())
35        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
36        .vanilla(put_or_call)
37}
38
39fn priced(builder: EquityOptionBuilder, engine: Engine) -> EquityOption {
40    builder.engine(engine).build()
41}
42
43fn main() {
44    common::title("VANILLA OPTION — S=100 K=100 sigma=30% r=5% q=2% T=1y");
45
46    for pc in [PutOrCall::Call, PutOrCall::Put] {
47        common::section(&format!("European {pc:?}"));
48        common::table_header();
49        common::row("Analytical (Black-Scholes)", &priced(base(pc), Engine::BlackScholes));
50        // common::row("Binomial (1000 steps)", &priced(base(pc), Engine::Binomial));
51        // common::row("Finite difference (400x400)", &priced(base(pc), Engine::FiniteDifference));
52        // common::row("Monte Carlo (Sobol, 100k)", &priced(base(pc), Engine::MonteCarlo));
53        // common::row(
54        //     "Monte Carlo (pseudo, 100k)",
55        //     &base(pc)
56        //         .engine(Engine::MonteCarlo)
57        //         .mc_config({
58        //             let mut c = rustyqlib::equity::montecarlo::MonteCarloConfig::default();
59        //             c.sampler = Sampler::PseudoRandom;
60        //             c
61        //         })
62        //         .build(),
63        // );
64    }
65
66    // common::section("American put (early exercise premium)");
67    // common::table_header();
68    // let european_put = priced(base(PutOrCall::Put), Engine::BlackScholes).npv();
69    // common::row(
70    //     "Analytical (rejects American)",
71    //     &base(PutOrCall::Put).american().vanilla(PutOrCall::Put).engine(Engine::BlackScholes).build(),
72    // );
73    // common::row(
74    //     "Binomial",
75    //     &base(PutOrCall::Put).american().vanilla(PutOrCall::Put).engine(Engine::Binomial).build(),
76    // );
77    // common::row(
78    //     "Finite difference (Brennan-Schwartz)",
79    //     &base(PutOrCall::Put)
80    //         .american()
81    //         .vanilla(PutOrCall::Put)
82    //         .engine(Engine::FiniteDifference)
83    //         .build(),
84    // );
85    // common::row(
86    //     "Monte Carlo (Longstaff-Schwartz)",
87    //     &base(PutOrCall::Put)
88    //         .american()
89    //         .vanilla(PutOrCall::Put)
90    //         .engine(Engine::MonteCarlo)
91    //         .paths(50_000)
92    //         .build(),
93    // );
94    // common::note(&format!("European put for reference: {european_put:.6}"));
95    //common::note("FD and MC report true American Greeks (grid / LSMC repricing);");
96    //common::note("the tree falls back to analytic European Greeks — note the delta gap.");
97
98    //common::section("Model comparison (same flat 30% vol)");
99    //common::table_header();
100    //common::row("GBM", &priced(base(PutOrCall::Call), Engine::MonteCarlo));
101    //common::row(
102    //    "Local vol (flat surface)",
103    //    &base(PutOrCall::Call)
104    //        .engine(Engine::MonteCarlo)
105    //        .model(McModel::LocalVol)
106    //        .paths(50_000)
107    //        .build(),
108    //);
109    // common::row(
110    //     "Heston (vol-of-vol -> 0)",
111    //     &base(PutOrCall::Call)
112    //         .engine(Engine::MonteCarlo)
113    //         .heston(rustyqlib::equity::heston::HestonParams {
114    //             v0: VOL * VOL,
115    //             kappa: 1.0,
116    //             theta: VOL * VOL,
117    //             vol_of_vol: 1e-3,
118    //             rho: 0.0,
119    //         })
120    //         .paths(50_000)
121    //         .build(),
122    // );
123    //common::note("all three must agree: flat surface and zero vol-of-vol are Black-Scholes");
124
125    // common::section("Identities");
126    // let call = priced(base(PutOrCall::Call), Engine::BlackScholes);
127    // let put = priced(base(PutOrCall::Put), Engine::BlackScholes);
128    // let parity = SPOT * (-DIV * 1.0_f64).exp() - STRIKE * (-RATE * 1.0_f64).exp();
129    // common::check("put-call parity: C - P", call.npv() - put.npv(), parity, 1e-10);
130    // common::check(
131    //     "closed form vs bs_price()",
132    //     call.npv(),
133    //     bs_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, PutOrCall::Call),
134    //     1e-12,
135    // );
136    // common::check(
137    //     "delta_call - delta_put = e^{-qT}",
138    //     call.delta() - put.delta(),
139    //     (-DIV * 1.0_f64).exp(),
140    //     1e-10,
141    // );
142
143    // common::section("Implied volatility round trip");
144    // let mut iv_option = priced(base(PutOrCall::Call), Engine::BlackScholes);
145    // let market_price = iv_option.npv();
146    // let recovered = iv_option.imp_vol(market_price);
147    // common::check("implied vol recovers input", recovered, VOL, 1e-10);
148    //
149    // common::section("Greeks vs bump-and-reprice (finite difference of the closed form)");
150    // let h = 0.01;
151    // let up = base(PutOrCall::Call).spot(SPOT + h).engine(Engine::BlackScholes).build();
152    // let dn = base(PutOrCall::Call).spot(SPOT - h).engine(Engine::BlackScholes).build();
153    // common::check("delta", call.delta(), (up.npv() - dn.npv()) / (2.0 * h), 1e-6);
154    // common::check(
155    //     "gamma",
156    //     call.gamma(),
157    //     (up.npv() - 2.0 * call.npv() + dn.npv()) / (h * h),
158    //     1e-4,
159    // );
160
161    greek_surfaces();
162    println!();
163}
164
165/// Save interactive 3D surfaces of the Greeks over (moneyness, maturity) so
166/// their shape and smoothness can be inspected. Written as self-contained
167/// HTML to `runs/vanilla_option/`.
168fn greek_surfaces() {
169    use common::plot3d::{greek_surface, linspace, save_surface_html, Labels};
170
171    common::section("Greek surfaces over (moneyness, maturity) -> runs/vanilla_option/*.html");
172
173    // x = moneyness S/K (0.4 .. 1.6, i.e. spot 40..160 for K=100);
174    // y = maturity 0.05..2.0y (short-dated ATM is where the structure lives)
175    let moneyness = linspace(0.6, 1.4, 72);
176    let mats = linspace(0.05, 1.0, 56);
177
178    // a call priced analytically at (moneyness, maturity); spot = m * K
179    let greek = |select: fn(&EquityOption) -> f64| {
180        move |m: f64, years: f64| -> f64 {
181            let option = EquityOptionBuilder::new()
182                .spot(m * STRIKE)
183                .strike(STRIKE)
184                .flat_vol(VOL)
185                .flat_rate(RATE)
186                .dividend_yield(DIV)
187                .valuation_date(asof())
188                .years_to_maturity(years)
189                .vanilla(PutOrCall::Call)
190                .engine(Engine::BlackScholes)
191                .build();
192            select(&option)
193        }
194    };
195
196    for (name, file, select) in [
197        ("Delta", "delta", (|o: &EquityOption| o.delta()) as fn(&EquityOption) -> f64),
198        ("Gamma", "gamma", |o: &EquityOption| o.gamma()),
199        ("Vega", "vega", |o: &EquityOption| o.vega()),
200        ("Theta", "theta", |o: &EquityOption| o.theta()),
201    ] {
202        let surface = greek_surface(&moneyness, &mats, greek(select));
203        save_surface_html(
204            &surface,
205            &format!("runs/vanilla_option/{file}_surface.html"),
206            &Labels {
207                title: &format!("Vanilla call {name} (K=100, sigma=30%, r=5%, q=2%)"),
208                x: "moneyness (S/K)",
209                y: "maturity (y)",
210                z: name,
211            },
212        );
213    }
214    common::note("open the HTML in a browser to rotate, zoom and hover the surfaces");
215}