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}