pub struct EquityOptionBuilder { /* private fields */ }Implementations§
Source§impl EquityOptionBuilder
impl EquityOptionBuilder
Sourcepub fn new() -> Self
pub fn new() -> Self
Examples found in repository?
More examples
examples/autocallable_option.rs (line 33)
32fn base() -> EquityOptionBuilder {
33 EquityOptionBuilder::new()
34 .symbol("ATHENA")
35 .spot(SPOT)
36 .flat_vol(VOL)
37 .flat_rate(RATE)
38 .dividend_yield(DIV)
39 .valuation_date(asof())
40 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
41 .engine(Engine::MonteCarlo)
42 .paths(50_000)
43}Additional examples can be found in:
Sourcepub fn symbol(self, symbol: &str) -> Self
pub fn symbol(self, symbol: &str) -> Self
Examples found in repository?
More examples
examples/autocallable_option.rs (line 34)
32fn base() -> EquityOptionBuilder {
33 EquityOptionBuilder::new()
34 .symbol("ATHENA")
35 .spot(SPOT)
36 .flat_vol(VOL)
37 .flat_rate(RATE)
38 .dividend_yield(DIV)
39 .valuation_date(asof())
40 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
41 .engine(Engine::MonteCarlo)
42 .paths(50_000)
43}Additional examples can be found in:
Sourcepub fn spot(self, spot: f64) -> Self
pub fn spot(self, spot: f64) -> Self
Examples found in repository?
More examples
examples/autocallable_option.rs (line 35)
32fn base() -> EquityOptionBuilder {
33 EquityOptionBuilder::new()
34 .symbol("ATHENA")
35 .spot(SPOT)
36 .flat_vol(VOL)
37 .flat_rate(RATE)
38 .dividend_yield(DIV)
39 .valuation_date(asof())
40 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
41 .engine(Engine::MonteCarlo)
42 .paths(50_000)
43}examples/binary_option.rs (line 24)
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}Additional examples can be found in:
Sourcepub fn strike(self, strike: f64) -> Self
pub fn strike(self, strike: f64) -> Self
Examples found in repository?
More examples
examples/vanilla_option.rs (line 30)
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}examples/heston_option.rs (line 31)
27fn base() -> EquityOptionBuilder {
28 EquityOptionBuilder::new()
29 .symbol("HESTON")
30 .spot(SPOT)
31 .strike(STRIKE)
32 .flat_vol(0.30) // only used as the vega-bump reference
33 .flat_rate(RATE)
34 .dividend_yield(DIV)
35 .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
36 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
37 .heston(params())
38}Additional examples can be found in:
Sourcepub fn flat_vol(self, vol: f64) -> Self
pub fn flat_vol(self, vol: f64) -> Self
Examples found in repository?
More examples
examples/autocallable_option.rs (line 36)
32fn base() -> EquityOptionBuilder {
33 EquityOptionBuilder::new()
34 .symbol("ATHENA")
35 .spot(SPOT)
36 .flat_vol(VOL)
37 .flat_rate(RATE)
38 .dividend_yield(DIV)
39 .valuation_date(asof())
40 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
41 .engine(Engine::MonteCarlo)
42 .paths(50_000)
43}Additional examples can be found in:
Sourcepub fn vol_surface(self, surface: VolSurface) -> Self
pub fn vol_surface(self, surface: VolSurface) -> Self
Examples found in repository?
examples/autocallable_option.rs (line 78)
65fn main() {
66 common::title(&format!(
67 "AUTOCALLABLE NOTE — N={NOTIONAL} autocall={AUTOCALL} protection={PROTECTION} coupon={COUPON}/period, {OBSERVATIONS} observations, T=1y"
68 ));
69 common::note("pays N + m*coupon if S >= autocall barrier at observation m;");
70 common::note("otherwise N at maturity, or N*S_T/S_0 if the protection barrier was breached.");
71
72 common::section("Model comparison");
73 common::table_header();
74 common::row("GBM (flat 30%)", ¬e(AUTOCALL, PROTECTION, COUPON).build());
75 common::row(
76 "Local vol (skewed surface)",
77 ¬e(AUTOCALL, PROTECTION, COUPON)
78 .vol_surface(skewed_surface())
79 .model(McModel::LocalVol)
80 .build(),
81 );
82 common::row(
83 "Heston (vol-of-vol=0.4, rho=-0.7)",
84 ¬e(AUTOCALL, PROTECTION, COUPON)
85 .heston(HestonParams {
86 v0: VOL * VOL,
87 kappa: 2.0,
88 theta: VOL * VOL,
89 vol_of_vol: 0.4,
90 rho: -0.7,
91 })
92 .build(),
93 );
94 common::row(
95 "Analytical (unsupported)",
96 ¬e(AUTOCALL, PROTECTION, COUPON).engine(Engine::BlackScholes).build(),
97 );
98 common::note("skew/stoch-vol raise the knock-in probability, lowering the note value");
99
100 common::section("Structure sensitivity (GBM)");
101 common::table_header();
102 for coupon in [0.0, 3.0, 6.0, 9.0] {
103 common::row(&format!("coupon = {coupon}/period"), ¬e(AUTOCALL, PROTECTION, coupon).build());
104 }
105 for protection in [50.0, 60.0, 70.0, 80.0] {
106 common::row(
107 &format!("protection barrier = {protection}"),
108 ¬e(AUTOCALL, protection, COUPON).build(),
109 );
110 }
111 for autocall in [95.0, 100.0, 105.0, 110.0] {
112 common::row(
113 &format!("autocall barrier = {autocall}"),
114 ¬e(autocall, PROTECTION, COUPON).build(),
115 );
116 }
117
118 common::section("Observation frequency (GBM)");
119 common::table_header();
120 for obs in [1usize, 2, 4, 12] {
121 common::row(
122 &format!("{obs} observations"),
123 &base().autocallable(AUTOCALL, PROTECTION, COUPON, obs, NOTIONAL).build(),
124 );
125 }
126
127 common::section("Degenerate cases (exact identities)");
128 let always_calls = base()
129 .autocallable(1e-9, 50.0, COUPON, OBSERVATIONS, NOTIONAL)
130 .build()
131 .npv();
132 common::check(
133 "barrier at 0 -> called at t1 with 1 coupon",
134 always_calls,
135 (NOTIONAL + COUPON) * (-RATE * 0.25_f64).exp(),
136 1e-8,
137 );
138 let never_calls = base()
139 .autocallable(1e12, 1e-9, COUPON, OBSERVATIONS, NOTIONAL)
140 .build()
141 .npv();
142 common::check(
143 "unreachable barriers -> zero-coupon bond",
144 never_calls,
145 NOTIONAL * (-RATE * 1.0_f64).exp(),
146 1e-8,
147 );
148 let full_downside = base()
149 .autocallable(1e12, 1e12, 0.0, OBSERVATIONS, NOTIONAL)
150 .dividend_yield(0.0)
151 .build()
152 .npv();
153 common::check(
154 "always knocked in, no coupon -> discounted forward",
155 full_downside,
156 NOTIONAL,
157 0.3,
158 );
159 println!();
160}More examples
examples/barrier_option.rs (line 160)
40fn main() {
41 common::title("BARRIER OPTIONS — S=100 K=100 sigma=30% r=5% q=2% T=1y");
42
43 common::section("All eight types, analytic (Reiner-Rubinstein)");
44 common::table_header();
45 for (dir, knock, pc, level) in [
46 (BarrierDirection::Down, KnockType::In, PutOrCall::Call, 90.0),
47 (BarrierDirection::Down, KnockType::Out, PutOrCall::Call, 90.0),
48 (BarrierDirection::Down, KnockType::In, PutOrCall::Put, 90.0),
49 (BarrierDirection::Down, KnockType::Out, PutOrCall::Put, 90.0),
50 (BarrierDirection::Up, KnockType::In, PutOrCall::Call, 120.0),
51 (BarrierDirection::Up, KnockType::Out, PutOrCall::Call, 120.0),
52 (BarrierDirection::Up, KnockType::In, PutOrCall::Put, 120.0),
53 (BarrierDirection::Up, KnockType::Out, PutOrCall::Put, 120.0),
54 ] {
55 common::row(
56 &format!("{dir:?}-and-{knock:?} {pc:?} H={level}"),
57 &base().barrier(pc, dir, knock, level).engine(Engine::BlackScholes).build(),
58 );
59 }
60
61 common::section("Engine comparison: down-and-out call, H=90");
62 common::table_header();
63 for (label, engine) in [
64 ("Analytical (Reiner-Rubinstein)", Engine::BlackScholes),
65 ("Finite difference (absorbing)", Engine::FiniteDifference),
66 ("Monte Carlo (Brownian bridge)", Engine::MonteCarlo),
67 ("Binomial (unsupported)", Engine::Binomial),
68 ] {
69 common::row(
70 label,
71 &base()
72 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 90.0)
73 .engine(engine)
74 .build(),
75 );
76 }
77 common::note("MC applies a bridge crossing correction, so monitoring is effectively continuous");
78
79 common::section("In-out parity: KI + KO = vanilla");
80 let vanilla = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build();
81 for level in [80.0, 90.0, 99.0] {
82 let ki = base()
83 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::In, level)
84 .engine(Engine::BlackScholes)
85 .build();
86 let ko = base()
87 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, level)
88 .engine(Engine::BlackScholes)
89 .build();
90 common::check(
91 &format!("H={level}: KI + KO"),
92 ki.npv() + ko.npv(),
93 vanilla.npv(),
94 1e-10,
95 );
96 }
97
98 common::section("Limits");
99 common::check(
100 "far barrier: KO call -> vanilla",
101 barrier_price(SPOT, STRIKE, 1e-4, RATE, DIV, VOL, 1.0, BarrierDirection::Down, KnockType::Out, PutOrCall::Call),
102 vanilla.npv(),
103 1e-9,
104 );
105 common::check(
106 "up-and-out call with K >= H is worthless",
107 barrier_price(SPOT, 110.0, 105.0, RATE, DIV, VOL, 1.0, BarrierDirection::Up, KnockType::Out, PutOrCall::Call),
108 0.0,
109 1e-12,
110 );
111 common::check(
112 "spot at barrier: KO = 0",
113 base()
114 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, SPOT)
115 .engine(Engine::BlackScholes)
116 .build()
117 .npv(),
118 0.0,
119 1e-12,
120 );
121
122 common::section("Barrier level sweep: down-and-out call");
123 common::table_header();
124 for level in [50.0, 70.0, 85.0, 95.0, 99.0] {
125 common::row(
126 &format!("H={level}"),
127 &base()
128 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, level)
129 .engine(Engine::BlackScholes)
130 .build(),
131 );
132 }
133 common::note("value decreases as the barrier approaches spot; delta can exceed 1 near it");
134
135 common::section("Smile matters: down-and-out call under local vol");
136 let skewed = VolSurface::from_strike_grid(
137 &[Tenor::YearFraction(0.5), Tenor::YearFraction(1.0), Tenor::YearFraction(2.0)],
138 &[70.0, 85.0, 100.0, 115.0, 130.0],
139 &[
140 vec![0.38, 0.34, 0.30, 0.28, 0.27],
141 vec![0.37, 0.34, 0.30, 0.29, 0.28],
142 vec![0.36, 0.33, 0.30, 0.29, 0.28],
143 ],
144 asof(),
145 DayCountConvention::Act365,
146 )
147 .unwrap();
148 common::table_header();
149 common::row(
150 "GBM (flat 30%)",
151 &base()
152 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 90.0)
153 .engine(Engine::MonteCarlo)
154 .paths(50_000)
155 .build(),
156 );
157 common::row(
158 "Local vol (skewed surface)",
159 &base()
160 .vol_surface(skewed)
161 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 90.0)
162 .engine(Engine::MonteCarlo)
163 .model(McModel::LocalVol)
164 .paths(50_000)
165 .build(),
166 );
167 common::note("downside skew raises the knock-out probability, lowering the price");
168 println!();
169}examples/local_vol_calibration.rs (line 109)
34fn main() {
35 common::title("LOCAL VOLATILITY — quotes -> implied surface -> Dupire -> reprice");
36
37 let maturities = [
38 (NaiveDate::from_ymd_opt(2026, 7, 2).unwrap(), 0.23),
39 (NaiveDate::from_ymd_opt(2027, 1, 1).unwrap(), 0.25),
40 ];
41
42 common::section("Step 1: generate market quotes from a known skew");
43 println!(" sigma(K, T) = base(T) - 0.001 * (K - 100)");
44 let mut quotes = Vec::new();
45 for (maturity, base_vol) in maturities {
46 let t = (maturity - asof()).num_days() as f64 / 365.0;
47 for i in 0..13 {
48 let strike = 70.0 + 5.0 * i as f64;
49 let vol = true_vol(strike, base_vol);
50 let price = bs_price(SPOT, strike, RATE, 0.0, vol, t, PutOrCall::Call);
51 let mut option = EquityOptionBuilder::new()
52 .spot(SPOT)
53 .strike(strike)
54 .flat_vol(0.2) // placeholder: the solve does not use it
55 .flat_rate(RATE)
56 .valuation_date(asof())
57 .maturity_date(maturity)
58 .vanilla(PutOrCall::Call)
59 .build();
60 option.base.current_price = Quote::new(price);
61 quotes.push(Box::new(option));
62 }
63 }
64 println!(" {} quotes across {} expiries", quotes.len(), maturities.len());
65
66 common::section("Step 2: back out implied vols and build the surface");
67 let surface = build_implied_vol_surface("es).expect("calibration failed");
68 println!("{surface}");
69
70 common::section("Step 3: check the surface recovers the input smile");
71 for (t, base_vol) in [(182.0 / 365.0, 0.23), (1.0, 0.25)] {
72 for strike in [70.0, 85.0, 100.0, 115.0, 130.0] {
73 let recovered = surface.vol(strike, SPOT, t);
74 common::check(
75 &format!("T={t:.3} K={strike}"),
76 recovered,
77 true_vol(strike, base_vol),
78 1e-6,
79 );
80 }
81 }
82
83 common::section("Step 4: Dupire local volatility from that surface");
84 let curve =
85 YieldCurve::flat(RATE, asof(), DayCountConvention::Act365, Compounding::Continuous).unwrap();
86 let lv = LocalVol::new(&surface, &curve, SPOT, 0.0, 0.0);
87 println!(" {:>8} {:>12} {:>12} {:>12}", "level", "t=0.25", "t=0.50", "t=1.00");
88 for level in [70.0, 85.0, 100.0, 115.0, 130.0] {
89 println!(
90 " {level:>8.1} {:>12.4} {:>12.4} {:>12.4}",
91 lv.vol(level, 0.25),
92 lv.vol(level, 0.50),
93 lv.vol(level, 1.00)
94 );
95 }
96 common::note("local vol is steeper in strike than implied vol (the 'twice the slope' rule)");
97 common::note("the far wings are noisy: Dupire takes numerical derivatives of a");
98 common::note("piecewise-linear surface with flat extrapolation — trust the interior.");
99
100 common::section("Step 5: reprice the calibrating vanillas through local vol MC");
101 common::table_header();
102 for strike in [90.0, 100.0, 110.0] {
103 let expected = bs_price(SPOT, strike, RATE, 0.0, true_vol(strike, 0.25), 1.0, PutOrCall::Call);
104 common::row(
105 &format!("local vol MC, K={strike}"),
106 &EquityOptionBuilder::new()
107 .spot(SPOT)
108 .strike(strike)
109 .vol_surface(surface.clone())
110 .flat_rate(RATE)
111 .valuation_date(asof())
112 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
113 .vanilla(PutOrCall::Call)
114 .engine(Engine::MonteCarlo)
115 .model(McModel::LocalVol)
116 .paths(50_000)
117 .build(),
118 );
119 println!("{:<34} {expected:>12.6} <- Black-Scholes target at the quoted smile vol", "");
120 }
121
122 common::section("Local vol on the finite difference engine (no sampling noise)");
123 common::table_header();
124 for strike in [90.0, 100.0, 110.0] {
125 common::row(
126 &format!("local vol FD, K={strike}"),
127 &EquityOptionBuilder::new()
128 .spot(SPOT)
129 .strike(strike)
130 .vol_surface(surface.clone())
131 .flat_rate(RATE)
132 .valuation_date(asof())
133 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
134 .vanilla(PutOrCall::Call)
135 .engine(Engine::FiniteDifference)
136 .model(McModel::LocalVol)
137 .build(),
138 );
139 }
140
141 common::section("Sanity: a flat surface must give flat local vol");
142 let flat = VolSurface::flat(0.25, asof(), DayCountConvention::Act365).unwrap();
143 let flat_lv = LocalVol::new(&flat, &curve, SPOT, 0.0, 0.0);
144 for (level, t) in [(70.0, 0.25), (100.0, 1.0), (130.0, 2.0)] {
145 common::check(&format!("sigma_loc({level}, {t})"), flat_lv.vol(level, t), 0.25, 1e-6);
146 }
147
148 common::section("Term structure: local vol is the forward variance");
149 let term = VolSurface::from_strike_smiles(
150 &[Tenor::YearFraction(0.5), Tenor::YearFraction(1.0)],
151 &[vec![(100.0, 0.20)], vec![(100.0, 0.25)]],
152 asof(),
153 DayCountConvention::Act365,
154 )
155 .unwrap();
156 let term_lv = LocalVol::new(&term, &curve, SPOT, 0.0, 0.0);
157 // (0.25^2 * 1 - 0.20^2 * 0.5) / 0.5 = 0.085
158 common::check(
159 "sigma_loc between pillars = sqrt(fwd variance)",
160 term_lv.vol(100.0, 0.75),
161 0.085_f64.sqrt(),
162 1e-3,
163 );
164 println!();
165}Sourcepub fn flat_rate(self, rate: f64) -> Self
pub fn flat_rate(self, rate: f64) -> Self
Examples found in repository?
More examples
examples/autocallable_option.rs (line 37)
32fn base() -> EquityOptionBuilder {
33 EquityOptionBuilder::new()
34 .symbol("ATHENA")
35 .spot(SPOT)
36 .flat_vol(VOL)
37 .flat_rate(RATE)
38 .dividend_yield(DIV)
39 .valuation_date(asof())
40 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
41 .engine(Engine::MonteCarlo)
42 .paths(50_000)
43}Additional examples can be found in:
pub fn discount_curve(self, curve: YieldCurve) -> Self
Sourcepub fn dividend_yield(self, q: f64) -> Self
pub fn dividend_yield(self, q: f64) -> Self
Examples found in repository?
More examples
examples/autocallable_option.rs (line 38)
32fn base() -> EquityOptionBuilder {
33 EquityOptionBuilder::new()
34 .symbol("ATHENA")
35 .spot(SPOT)
36 .flat_vol(VOL)
37 .flat_rate(RATE)
38 .dividend_yield(DIV)
39 .valuation_date(asof())
40 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
41 .engine(Engine::MonteCarlo)
42 .paths(50_000)
43}
44
45fn note(autocall: f64, protection: f64, coupon: f64) -> EquityOptionBuilder {
46 base().autocallable(autocall, protection, coupon, OBSERVATIONS, NOTIONAL)
47}
48
49/// Downward-skewed surface: the shape that actually drives these notes.
50fn skewed_surface() -> VolSurface {
51 VolSurface::from_strike_grid(
52 &[Tenor::YearFraction(0.25), Tenor::YearFraction(0.5), Tenor::YearFraction(1.0)],
53 &[60.0, 70.0, 85.0, 100.0, 115.0, 130.0],
54 &[
55 vec![0.42, 0.38, 0.33, 0.29, 0.27, 0.26],
56 vec![0.41, 0.37, 0.33, 0.30, 0.28, 0.27],
57 vec![0.40, 0.37, 0.33, 0.30, 0.29, 0.28],
58 ],
59 asof(),
60 DayCountConvention::Act365,
61 )
62 .unwrap()
63}
64
65fn main() {
66 common::title(&format!(
67 "AUTOCALLABLE NOTE — N={NOTIONAL} autocall={AUTOCALL} protection={PROTECTION} coupon={COUPON}/period, {OBSERVATIONS} observations, T=1y"
68 ));
69 common::note("pays N + m*coupon if S >= autocall barrier at observation m;");
70 common::note("otherwise N at maturity, or N*S_T/S_0 if the protection barrier was breached.");
71
72 common::section("Model comparison");
73 common::table_header();
74 common::row("GBM (flat 30%)", ¬e(AUTOCALL, PROTECTION, COUPON).build());
75 common::row(
76 "Local vol (skewed surface)",
77 ¬e(AUTOCALL, PROTECTION, COUPON)
78 .vol_surface(skewed_surface())
79 .model(McModel::LocalVol)
80 .build(),
81 );
82 common::row(
83 "Heston (vol-of-vol=0.4, rho=-0.7)",
84 ¬e(AUTOCALL, PROTECTION, COUPON)
85 .heston(HestonParams {
86 v0: VOL * VOL,
87 kappa: 2.0,
88 theta: VOL * VOL,
89 vol_of_vol: 0.4,
90 rho: -0.7,
91 })
92 .build(),
93 );
94 common::row(
95 "Analytical (unsupported)",
96 ¬e(AUTOCALL, PROTECTION, COUPON).engine(Engine::BlackScholes).build(),
97 );
98 common::note("skew/stoch-vol raise the knock-in probability, lowering the note value");
99
100 common::section("Structure sensitivity (GBM)");
101 common::table_header();
102 for coupon in [0.0, 3.0, 6.0, 9.0] {
103 common::row(&format!("coupon = {coupon}/period"), ¬e(AUTOCALL, PROTECTION, coupon).build());
104 }
105 for protection in [50.0, 60.0, 70.0, 80.0] {
106 common::row(
107 &format!("protection barrier = {protection}"),
108 ¬e(AUTOCALL, protection, COUPON).build(),
109 );
110 }
111 for autocall in [95.0, 100.0, 105.0, 110.0] {
112 common::row(
113 &format!("autocall barrier = {autocall}"),
114 ¬e(autocall, PROTECTION, COUPON).build(),
115 );
116 }
117
118 common::section("Observation frequency (GBM)");
119 common::table_header();
120 for obs in [1usize, 2, 4, 12] {
121 common::row(
122 &format!("{obs} observations"),
123 &base().autocallable(AUTOCALL, PROTECTION, COUPON, obs, NOTIONAL).build(),
124 );
125 }
126
127 common::section("Degenerate cases (exact identities)");
128 let always_calls = base()
129 .autocallable(1e-9, 50.0, COUPON, OBSERVATIONS, NOTIONAL)
130 .build()
131 .npv();
132 common::check(
133 "barrier at 0 -> called at t1 with 1 coupon",
134 always_calls,
135 (NOTIONAL + COUPON) * (-RATE * 0.25_f64).exp(),
136 1e-8,
137 );
138 let never_calls = base()
139 .autocallable(1e12, 1e-9, COUPON, OBSERVATIONS, NOTIONAL)
140 .build()
141 .npv();
142 common::check(
143 "unreachable barriers -> zero-coupon bond",
144 never_calls,
145 NOTIONAL * (-RATE * 1.0_f64).exp(),
146 1e-8,
147 );
148 let full_downside = base()
149 .autocallable(1e12, 1e12, 0.0, OBSERVATIONS, NOTIONAL)
150 .dividend_yield(0.0)
151 .build()
152 .npv();
153 common::check(
154 "always knocked in, no coupon -> discounted forward",
155 full_downside,
156 NOTIONAL,
157 0.3,
158 );
159 println!();
160}examples/vanilla_option.rs (line 33)
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}Additional examples can be found in:
Sourcepub fn borrow_cost(self, b: f64) -> Self
pub fn borrow_cost(self, b: f64) -> Self
Continuous stock borrow (repo) cost; part of the carry.
Examples found in repository?
examples/dividends_and_borrow.rs (line 46)
39fn main() {
40 common::title("DIVIDENDS AND BORROW COST — S=100 K=100 sigma=30% r=5% T=1y");
41
42 common::section("Continuous carry: dividend yield and borrow cost are interchangeable");
43 common::table_header();
44 common::row("no carry", &base().vanilla(PutOrCall::Call).build());
45 common::row("q = 4%", &base().dividend_yield(0.04).vanilla(PutOrCall::Call).build());
46 common::row("borrow = 4%", &base().borrow_cost(0.04).vanilla(PutOrCall::Call).build());
47 common::row(
48 "q = 1% + borrow = 3%",
49 &base().dividend_yield(0.01).borrow_cost(0.03).vanilla(PutOrCall::Call).build(),
50 );
51 common::note("carry_yield() = dividend_yield + borrow_cost enters every formula as 'q'");
52
53 let q_only = base().dividend_yield(0.04).vanilla(PutOrCall::Call).build();
54 let split = base().dividend_yield(0.01).borrow_cost(0.03).vanilla(PutOrCall::Call).build();
55 common::check("q=4% vs q=1%+b=3%", split.npv(), q_only.npv(), 1e-12);
56
57 common::section("Hard-to-borrow names: high borrow cost lowers the forward");
58 common::table_header();
59 for b in [0.0, 0.02, 0.05, 0.15] {
60 let option = base().borrow_cost(b).vanilla(PutOrCall::Call).build();
61 common::row(&format!("borrow = {:.0}%", b * 100.0), &option);
62 }
63 let hard = base().borrow_cost(0.15).vanilla(PutOrCall::Call).build();
64 println!(
65 " forward with 15% borrow: {:.4} (vs spot {SPOT})",
66 hard.base.forward_price()
67 );
68
69 common::section("Discrete cash dividends: 2 x 1.50 over the year");
70 let with_divs = |b: EquityOptionBuilder| {
71 b.cash_dividend(NaiveDate::from_ymd_opt(2026, 4, 1).unwrap(), 1.5)
72 .cash_dividend(NaiveDate::from_ymd_opt(2026, 10, 1).unwrap(), 1.5)
73 };
74 let analytic = with_divs(base()).vanilla(PutOrCall::Call).build();
75 println!(
76 " spot {SPOT} - PV(dividends) {:.6} = escrowed spot {:.6}",
77 analytic.base.pv_cash_dividends(),
78 analytic.base.effective_spot()
79 );
80 common::table_header();
81 common::row("Analytical (escrowed model)", &analytic);
82 common::row(
83 "Binomial (escrowed)",
84 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::Binomial).build(),
85 );
86 common::row(
87 "Finite difference (jump model)",
88 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::FiniteDifference).build(),
89 );
90 common::row(
91 "Monte Carlo terminal (escrowed)",
92 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::MonteCarlo).build(),
93 );
94 common::row(
95 "Monte Carlo path-wise (jump model)",
96 &with_divs(base())
97 .vanilla(PutOrCall::Call)
98 .engine(Engine::MonteCarlo)
99 .mc_time_steps(200)
100 .paths(50_000)
101 .build(),
102 );
103 common::note("escrowed: lognormal on S - PV(divs); jump: dividends subtracted at each ex-date");
104 common::note("the two models differ slightly by construction — that gap is expected, not a bug");
105
106 common::check(
107 "escrowed analytic == BS on the escrowed spot",
108 analytic.npv(),
109 bs_price(analytic.base.effective_spot(), STRIKE, RATE, 0.0, VOL, 1.0, PutOrCall::Call),
110 1e-10,
111 );
112
113 common::section("Where the jump model matters: American exercise and barriers");
114 common::table_header();
115 common::row(
116 "American put, FD (jumps)",
117 &with_divs(base())
118 .american()
119 .vanilla(PutOrCall::Put)
120 .engine(Engine::FiniteDifference)
121 .build(),
122 );
123 common::row(
124 "American put, no dividends",
125 &base().american().vanilla(PutOrCall::Put).engine(Engine::FiniteDifference).build(),
126 );
127 common::row(
128 "Down-and-out call H=85, MC (jumps)",
129 &with_divs(base())
130 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
131 .engine(Engine::MonteCarlo)
132 .paths(50_000)
133 .build(),
134 );
135 common::row(
136 "Down-and-out call H=85, no dividends",
137 &base()
138 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
139 .engine(Engine::MonteCarlo)
140 .paths(50_000)
141 .build(),
142 );
143 common::note("dividend drops push the path toward a down barrier and change exercise timing");
144
145 common::section("Put-call parity with full carry");
146 let call = with_divs(base()).borrow_cost(0.02).vanilla(PutOrCall::Call).build();
147 let put = with_divs(base()).borrow_cost(0.02).vanilla(PutOrCall::Put).build();
148 let parity = call.base.effective_spot() * (-call.base.carry_yield() * 1.0_f64).exp()
149 - STRIKE * (-RATE * 1.0_f64).exp();
150 common::check("C - P = S_eff e^{-(q+b)T} - K e^{-rT}", call.npv() - put.npv(), parity, 1e-10);
151 println!();
152}Sourcepub fn cash_dividend(self, date: NaiveDate, amount: f64) -> Self
pub fn cash_dividend(self, date: NaiveDate, amount: f64) -> Self
Examples found in repository?
examples/dividends_and_borrow.rs (line 71)
39fn main() {
40 common::title("DIVIDENDS AND BORROW COST — S=100 K=100 sigma=30% r=5% T=1y");
41
42 common::section("Continuous carry: dividend yield and borrow cost are interchangeable");
43 common::table_header();
44 common::row("no carry", &base().vanilla(PutOrCall::Call).build());
45 common::row("q = 4%", &base().dividend_yield(0.04).vanilla(PutOrCall::Call).build());
46 common::row("borrow = 4%", &base().borrow_cost(0.04).vanilla(PutOrCall::Call).build());
47 common::row(
48 "q = 1% + borrow = 3%",
49 &base().dividend_yield(0.01).borrow_cost(0.03).vanilla(PutOrCall::Call).build(),
50 );
51 common::note("carry_yield() = dividend_yield + borrow_cost enters every formula as 'q'");
52
53 let q_only = base().dividend_yield(0.04).vanilla(PutOrCall::Call).build();
54 let split = base().dividend_yield(0.01).borrow_cost(0.03).vanilla(PutOrCall::Call).build();
55 common::check("q=4% vs q=1%+b=3%", split.npv(), q_only.npv(), 1e-12);
56
57 common::section("Hard-to-borrow names: high borrow cost lowers the forward");
58 common::table_header();
59 for b in [0.0, 0.02, 0.05, 0.15] {
60 let option = base().borrow_cost(b).vanilla(PutOrCall::Call).build();
61 common::row(&format!("borrow = {:.0}%", b * 100.0), &option);
62 }
63 let hard = base().borrow_cost(0.15).vanilla(PutOrCall::Call).build();
64 println!(
65 " forward with 15% borrow: {:.4} (vs spot {SPOT})",
66 hard.base.forward_price()
67 );
68
69 common::section("Discrete cash dividends: 2 x 1.50 over the year");
70 let with_divs = |b: EquityOptionBuilder| {
71 b.cash_dividend(NaiveDate::from_ymd_opt(2026, 4, 1).unwrap(), 1.5)
72 .cash_dividend(NaiveDate::from_ymd_opt(2026, 10, 1).unwrap(), 1.5)
73 };
74 let analytic = with_divs(base()).vanilla(PutOrCall::Call).build();
75 println!(
76 " spot {SPOT} - PV(dividends) {:.6} = escrowed spot {:.6}",
77 analytic.base.pv_cash_dividends(),
78 analytic.base.effective_spot()
79 );
80 common::table_header();
81 common::row("Analytical (escrowed model)", &analytic);
82 common::row(
83 "Binomial (escrowed)",
84 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::Binomial).build(),
85 );
86 common::row(
87 "Finite difference (jump model)",
88 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::FiniteDifference).build(),
89 );
90 common::row(
91 "Monte Carlo terminal (escrowed)",
92 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::MonteCarlo).build(),
93 );
94 common::row(
95 "Monte Carlo path-wise (jump model)",
96 &with_divs(base())
97 .vanilla(PutOrCall::Call)
98 .engine(Engine::MonteCarlo)
99 .mc_time_steps(200)
100 .paths(50_000)
101 .build(),
102 );
103 common::note("escrowed: lognormal on S - PV(divs); jump: dividends subtracted at each ex-date");
104 common::note("the two models differ slightly by construction — that gap is expected, not a bug");
105
106 common::check(
107 "escrowed analytic == BS on the escrowed spot",
108 analytic.npv(),
109 bs_price(analytic.base.effective_spot(), STRIKE, RATE, 0.0, VOL, 1.0, PutOrCall::Call),
110 1e-10,
111 );
112
113 common::section("Where the jump model matters: American exercise and barriers");
114 common::table_header();
115 common::row(
116 "American put, FD (jumps)",
117 &with_divs(base())
118 .american()
119 .vanilla(PutOrCall::Put)
120 .engine(Engine::FiniteDifference)
121 .build(),
122 );
123 common::row(
124 "American put, no dividends",
125 &base().american().vanilla(PutOrCall::Put).engine(Engine::FiniteDifference).build(),
126 );
127 common::row(
128 "Down-and-out call H=85, MC (jumps)",
129 &with_divs(base())
130 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
131 .engine(Engine::MonteCarlo)
132 .paths(50_000)
133 .build(),
134 );
135 common::row(
136 "Down-and-out call H=85, no dividends",
137 &base()
138 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
139 .engine(Engine::MonteCarlo)
140 .paths(50_000)
141 .build(),
142 );
143 common::note("dividend drops push the path toward a down barrier and change exercise timing");
144
145 common::section("Put-call parity with full carry");
146 let call = with_divs(base()).borrow_cost(0.02).vanilla(PutOrCall::Call).build();
147 let put = with_divs(base()).borrow_cost(0.02).vanilla(PutOrCall::Put).build();
148 let parity = call.base.effective_spot() * (-call.base.carry_yield() * 1.0_f64).exp()
149 - STRIKE * (-RATE * 1.0_f64).exp();
150 common::check("C - P = S_eff e^{-(q+b)T} - K e^{-rT}", call.npv() - put.npv(), parity, 1e-10);
151 println!();
152}Sourcepub fn on_future(self, settlement: FuturesSettlement) -> Self
pub fn on_future(self, settlement: FuturesSettlement) -> Self
Price the option on a future with Black-76: spot is then the
futures price F. European vanilla, Analytical engine only.
Examples found in repository?
examples/futures_option.rs (line 33)
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}Sourcepub fn valuation_date(self, date: NaiveDate) -> Self
pub fn valuation_date(self, date: NaiveDate) -> Self
Examples found in repository?
More examples
examples/autocallable_option.rs (line 39)
32fn base() -> EquityOptionBuilder {
33 EquityOptionBuilder::new()
34 .symbol("ATHENA")
35 .spot(SPOT)
36 .flat_vol(VOL)
37 .flat_rate(RATE)
38 .dividend_yield(DIV)
39 .valuation_date(asof())
40 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
41 .engine(Engine::MonteCarlo)
42 .paths(50_000)
43}Additional examples can be found in:
Sourcepub fn maturity_date(self, date: NaiveDate) -> Self
pub fn maturity_date(self, date: NaiveDate) -> Self
Examples found in repository?
More examples
examples/autocallable_option.rs (line 40)
32fn base() -> EquityOptionBuilder {
33 EquityOptionBuilder::new()
34 .symbol("ATHENA")
35 .spot(SPOT)
36 .flat_vol(VOL)
37 .flat_rate(RATE)
38 .dividend_yield(DIV)
39 .valuation_date(asof())
40 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
41 .engine(Engine::MonteCarlo)
42 .paths(50_000)
43}Additional examples can be found in:
Sourcepub fn years_to_maturity(self, years: f64) -> Self
pub fn years_to_maturity(self, years: f64) -> Self
Convenience for examples: maturity = valuation + years * 365 days.
Examples found in repository?
examples/vanilla_option.rs (line 188)
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}More examples
examples/binary_option.rs (line 118)
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}Sourcepub fn american(self) -> Self
pub fn american(self) -> Self
Examples found in repository?
examples/dividends_and_borrow.rs (line 118)
39fn main() {
40 common::title("DIVIDENDS AND BORROW COST — S=100 K=100 sigma=30% r=5% T=1y");
41
42 common::section("Continuous carry: dividend yield and borrow cost are interchangeable");
43 common::table_header();
44 common::row("no carry", &base().vanilla(PutOrCall::Call).build());
45 common::row("q = 4%", &base().dividend_yield(0.04).vanilla(PutOrCall::Call).build());
46 common::row("borrow = 4%", &base().borrow_cost(0.04).vanilla(PutOrCall::Call).build());
47 common::row(
48 "q = 1% + borrow = 3%",
49 &base().dividend_yield(0.01).borrow_cost(0.03).vanilla(PutOrCall::Call).build(),
50 );
51 common::note("carry_yield() = dividend_yield + borrow_cost enters every formula as 'q'");
52
53 let q_only = base().dividend_yield(0.04).vanilla(PutOrCall::Call).build();
54 let split = base().dividend_yield(0.01).borrow_cost(0.03).vanilla(PutOrCall::Call).build();
55 common::check("q=4% vs q=1%+b=3%", split.npv(), q_only.npv(), 1e-12);
56
57 common::section("Hard-to-borrow names: high borrow cost lowers the forward");
58 common::table_header();
59 for b in [0.0, 0.02, 0.05, 0.15] {
60 let option = base().borrow_cost(b).vanilla(PutOrCall::Call).build();
61 common::row(&format!("borrow = {:.0}%", b * 100.0), &option);
62 }
63 let hard = base().borrow_cost(0.15).vanilla(PutOrCall::Call).build();
64 println!(
65 " forward with 15% borrow: {:.4} (vs spot {SPOT})",
66 hard.base.forward_price()
67 );
68
69 common::section("Discrete cash dividends: 2 x 1.50 over the year");
70 let with_divs = |b: EquityOptionBuilder| {
71 b.cash_dividend(NaiveDate::from_ymd_opt(2026, 4, 1).unwrap(), 1.5)
72 .cash_dividend(NaiveDate::from_ymd_opt(2026, 10, 1).unwrap(), 1.5)
73 };
74 let analytic = with_divs(base()).vanilla(PutOrCall::Call).build();
75 println!(
76 " spot {SPOT} - PV(dividends) {:.6} = escrowed spot {:.6}",
77 analytic.base.pv_cash_dividends(),
78 analytic.base.effective_spot()
79 );
80 common::table_header();
81 common::row("Analytical (escrowed model)", &analytic);
82 common::row(
83 "Binomial (escrowed)",
84 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::Binomial).build(),
85 );
86 common::row(
87 "Finite difference (jump model)",
88 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::FiniteDifference).build(),
89 );
90 common::row(
91 "Monte Carlo terminal (escrowed)",
92 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::MonteCarlo).build(),
93 );
94 common::row(
95 "Monte Carlo path-wise (jump model)",
96 &with_divs(base())
97 .vanilla(PutOrCall::Call)
98 .engine(Engine::MonteCarlo)
99 .mc_time_steps(200)
100 .paths(50_000)
101 .build(),
102 );
103 common::note("escrowed: lognormal on S - PV(divs); jump: dividends subtracted at each ex-date");
104 common::note("the two models differ slightly by construction — that gap is expected, not a bug");
105
106 common::check(
107 "escrowed analytic == BS on the escrowed spot",
108 analytic.npv(),
109 bs_price(analytic.base.effective_spot(), STRIKE, RATE, 0.0, VOL, 1.0, PutOrCall::Call),
110 1e-10,
111 );
112
113 common::section("Where the jump model matters: American exercise and barriers");
114 common::table_header();
115 common::row(
116 "American put, FD (jumps)",
117 &with_divs(base())
118 .american()
119 .vanilla(PutOrCall::Put)
120 .engine(Engine::FiniteDifference)
121 .build(),
122 );
123 common::row(
124 "American put, no dividends",
125 &base().american().vanilla(PutOrCall::Put).engine(Engine::FiniteDifference).build(),
126 );
127 common::row(
128 "Down-and-out call H=85, MC (jumps)",
129 &with_divs(base())
130 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
131 .engine(Engine::MonteCarlo)
132 .paths(50_000)
133 .build(),
134 );
135 common::row(
136 "Down-and-out call H=85, no dividends",
137 &base()
138 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
139 .engine(Engine::MonteCarlo)
140 .paths(50_000)
141 .build(),
142 );
143 common::note("dividend drops push the path toward a down barrier and change exercise timing");
144
145 common::section("Put-call parity with full carry");
146 let call = with_divs(base()).borrow_cost(0.02).vanilla(PutOrCall::Call).build();
147 let put = with_divs(base()).borrow_cost(0.02).vanilla(PutOrCall::Put).build();
148 let parity = call.base.effective_spot() * (-call.base.carry_yield() * 1.0_f64).exp()
149 - STRIKE * (-RATE * 1.0_f64).exp();
150 common::check("C - P = S_eff e^{-(q+b)T} - K e^{-rT}", call.npv() - put.npv(), parity, 1e-10);
151 println!();
152}pub fn exercise_style(self, style: ContractStyle) -> Self
pub fn payoff(self, payoff: Box<dyn Payoff>) -> Self
Sourcepub fn vanilla(self, put_or_call: PutOrCall) -> Self
pub fn vanilla(self, put_or_call: PutOrCall) -> Self
Examples found in repository?
examples/vanilla_option.rs (line 36)
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}More examples
examples/futures_option.rs (line 32)
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}examples/forward_start_option.rs (line 122)
39fn main() {
40 common::title("FORWARD-START OPTION — S=100, strike = 1.0 x S(0.5y), T=1y, sigma=30%");
41
42 common::section("Black-Scholes: analytic vs Monte Carlo");
43 common::table_header();
44 common::row(
45 "Analytical (Rubinstein)",
46 &base()
47 .forward_start(PutOrCall::Call, 1.0, START)
48 .engine(Engine::BlackScholes)
49 .build(),
50 );
51 common::row(
52 "Monte Carlo (GBM)",
53 &base()
54 .forward_start(PutOrCall::Call, 1.0, START)
55 .engine(Engine::MonteCarlo)
56 .paths(100_000)
57 .build(),
58 );
59 common::row(
60 "Finite difference (unsupported)",
61 &base()
62 .forward_start(PutOrCall::Call, 1.0, START)
63 .engine(Engine::FiniteDifference)
64 .build(),
65 );
66
67 common::section("Forward smile: Heston vs Black-Scholes");
68 common::table_header();
69 let bs = base()
70 .forward_start(PutOrCall::Call, 1.0, START)
71 .engine(Engine::BlackScholes)
72 .build()
73 .npv();
74 common::row(
75 "Heston vol-of-vol=0.001 (-> BS)",
76 &base()
77 .forward_start(PutOrCall::Call, 1.0, START)
78 .engine(Engine::MonteCarlo)
79 .heston(heston_params(1e-3, 0.0))
80 .paths(50_000)
81 .build(),
82 );
83 for (vov, rho) in [(0.2, -0.7), (0.4, -0.7), (0.6, -0.7), (0.4, 0.0)] {
84 common::row(
85 &format!("Heston vol-of-vol={vov}, rho={rho}"),
86 &base()
87 .forward_start(PutOrCall::Call, 1.0, START)
88 .engine(Engine::MonteCarlo)
89 .heston(heston_params(vov, rho))
90 .paths(50_000)
91 .build(),
92 );
93 }
94 common::note(&format!("Black-Scholes reference: {bs:.6}"));
95 common::note("the gap is the forward-smile effect — the reason to price these on a stoch-vol model");
96
97 common::section("Strike fraction sweep (analytic)");
98 common::table_header();
99 for k in [0.9, 0.95, 1.0, 1.05, 1.1] {
100 common::row(
101 &format!("strike = {k} x S(t_f), call"),
102 &base().forward_start(PutOrCall::Call, k, START).engine(Engine::BlackScholes).build(),
103 );
104 }
105
106 common::section("Fixing date sweep (analytic, ATM)");
107 common::table_header();
108 for start in [0.1, 0.25, 0.5, 0.75, 0.9] {
109 common::row(
110 &format!("fixing at {:.0}% of life", start * 100.0),
111 &base().forward_start(PutOrCall::Call, 1.0, start).engine(Engine::BlackScholes).build(),
112 );
113 }
114 common::note("later fixing leaves less time to expiry, so the option is worth less");
115
116 common::section("Identities");
117 common::check(
118 "immediate fixing -> vanilla struck at S0",
119 forward_start_price(SPOT, 1.0, RATE, DIV, VOL, 1e-6, 1.0, PutOrCall::Call),
120 base()
121 .strike(SPOT)
122 .vanilla(PutOrCall::Call)
123 .engine(Engine::BlackScholes)
124 .build()
125 .npv(),
126 1e-3,
127 );
128 let p100 = forward_start_price(100.0, 1.0, RATE, DIV, VOL, 0.5, 1.0, PutOrCall::Call);
129 let p200 = forward_start_price(200.0, 1.0, RATE, DIV, VOL, 0.5, 1.0, PutOrCall::Call);
130 common::check("homogeneity: price(2S) = 2 price(S)", p200, 2.0 * p100, 1e-12);
131 let fs = base()
132 .forward_start(PutOrCall::Call, 1.0, START)
133 .engine(Engine::BlackScholes)
134 .build();
135 common::check("delta = price / spot (homogeneity)", fs.delta(), fs.npv() / SPOT, 1e-6);
136 println!();
137}examples/binary_option.rs (line 99)
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}examples/asian_option.rs (line 116)
35fn main() {
36 common::title("ASIAN OPTIONS — S=100 K=100 sigma=30% r=5% q=2% T=1y");
37
38 common::section("Fixed strike (average price) call");
39 common::table_header();
40 common::row(
41 "Geometric, analytic (exact)",
42 &base()
43 .asian(PutOrCall::Call, AveragingType::Geometric, AsianStrikeType::FixedStrike)
44 .engine(Engine::BlackScholes)
45 .build(),
46 );
47 common::row(
48 "Geometric, Monte Carlo",
49 &base()
50 .asian(PutOrCall::Call, AveragingType::Geometric, AsianStrikeType::FixedStrike)
51 .engine(Engine::MonteCarlo)
52 .paths(50_000)
53 .build(),
54 );
55 common::row(
56 "Arithmetic, Turnbull-Wakeman",
57 &base()
58 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
59 .engine(Engine::BlackScholes)
60 .build(),
61 );
62 common::row(
63 "Arithmetic, MC + geometric CV",
64 &base()
65 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
66 .engine(Engine::MonteCarlo)
67 .paths(50_000)
68 .build(),
69 );
70
71 common::section("Control variate effect (same path count)");
72 common::table_header();
73 let with_cv = base()
74 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
75 .engine(Engine::MonteCarlo)
76 .paths(20_000)
77 .build();
78 let without_cv = base()
79 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
80 .engine(Engine::MonteCarlo)
81 .paths(20_000)
82 .mc_config({
83 // Euler stepping disables the control variate precondition
84 let mut c = rustyqlib::equity::montecarlo::MonteCarloConfig::default();
85 c.paths = 20_000;
86 c.scheme = DiscretizationScheme::Euler;
87 c.time_steps = 100;
88 c
89 })
90 .build();
91 common::row("with geometric control variate", &with_cv);
92 common::row("without (Euler path route)", &without_cv);
93 common::note("compare the std err column: the CV collapses the variance");
94
95 common::section("Floating strike (average strike)");
96 common::table_header();
97 for pc in [PutOrCall::Call, PutOrCall::Put] {
98 common::row(
99 &format!("Monte Carlo, {pc:?}"),
100 &base()
101 .asian(pc, AveragingType::Arithmetic, AsianStrikeType::FloatingStrike)
102 .engine(Engine::MonteCarlo)
103 .paths(50_000)
104 .build(),
105 );
106 common::row(
107 &format!("Analytic (unsupported), {pc:?}"),
108 &base()
109 .asian(pc, AveragingType::Arithmetic, AsianStrikeType::FloatingStrike)
110 .engine(Engine::BlackScholes)
111 .build(),
112 );
113 }
114
115 common::section("Orderings and limits");
116 let vanilla = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build().npv();
117 let geo = geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, None, PutOrCall::Call);
118 let arith = turnbull_wakeman_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, PutOrCall::Call);
119 println!(" geometric {geo:.6} < arithmetic {arith:.6} < vanilla {vanilla:.6}");
120 common::note("AM-GM: the arithmetic average dominates the geometric one");
121 common::note("averaging reduces effective volatility (sigma^2 T / 3), so both sit below vanilla");
122 common::check(
123 "discrete geometric (n=1e5) -> continuous",
124 geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, Some(100_000), PutOrCall::Call),
125 geo,
126 1e-3,
127 );
128
129 common::section("Averaging frequency (geometric, exact)");
130 for n in [4usize, 12, 52, 252] {
131 let price =
132 geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, Some(n), PutOrCall::Call);
133 println!(" {n:>4} fixings: {price:.6}");
134 }
135 println!();
136}examples/dividends_and_borrow.rs (line 44)
39fn main() {
40 common::title("DIVIDENDS AND BORROW COST — S=100 K=100 sigma=30% r=5% T=1y");
41
42 common::section("Continuous carry: dividend yield and borrow cost are interchangeable");
43 common::table_header();
44 common::row("no carry", &base().vanilla(PutOrCall::Call).build());
45 common::row("q = 4%", &base().dividend_yield(0.04).vanilla(PutOrCall::Call).build());
46 common::row("borrow = 4%", &base().borrow_cost(0.04).vanilla(PutOrCall::Call).build());
47 common::row(
48 "q = 1% + borrow = 3%",
49 &base().dividend_yield(0.01).borrow_cost(0.03).vanilla(PutOrCall::Call).build(),
50 );
51 common::note("carry_yield() = dividend_yield + borrow_cost enters every formula as 'q'");
52
53 let q_only = base().dividend_yield(0.04).vanilla(PutOrCall::Call).build();
54 let split = base().dividend_yield(0.01).borrow_cost(0.03).vanilla(PutOrCall::Call).build();
55 common::check("q=4% vs q=1%+b=3%", split.npv(), q_only.npv(), 1e-12);
56
57 common::section("Hard-to-borrow names: high borrow cost lowers the forward");
58 common::table_header();
59 for b in [0.0, 0.02, 0.05, 0.15] {
60 let option = base().borrow_cost(b).vanilla(PutOrCall::Call).build();
61 common::row(&format!("borrow = {:.0}%", b * 100.0), &option);
62 }
63 let hard = base().borrow_cost(0.15).vanilla(PutOrCall::Call).build();
64 println!(
65 " forward with 15% borrow: {:.4} (vs spot {SPOT})",
66 hard.base.forward_price()
67 );
68
69 common::section("Discrete cash dividends: 2 x 1.50 over the year");
70 let with_divs = |b: EquityOptionBuilder| {
71 b.cash_dividend(NaiveDate::from_ymd_opt(2026, 4, 1).unwrap(), 1.5)
72 .cash_dividend(NaiveDate::from_ymd_opt(2026, 10, 1).unwrap(), 1.5)
73 };
74 let analytic = with_divs(base()).vanilla(PutOrCall::Call).build();
75 println!(
76 " spot {SPOT} - PV(dividends) {:.6} = escrowed spot {:.6}",
77 analytic.base.pv_cash_dividends(),
78 analytic.base.effective_spot()
79 );
80 common::table_header();
81 common::row("Analytical (escrowed model)", &analytic);
82 common::row(
83 "Binomial (escrowed)",
84 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::Binomial).build(),
85 );
86 common::row(
87 "Finite difference (jump model)",
88 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::FiniteDifference).build(),
89 );
90 common::row(
91 "Monte Carlo terminal (escrowed)",
92 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::MonteCarlo).build(),
93 );
94 common::row(
95 "Monte Carlo path-wise (jump model)",
96 &with_divs(base())
97 .vanilla(PutOrCall::Call)
98 .engine(Engine::MonteCarlo)
99 .mc_time_steps(200)
100 .paths(50_000)
101 .build(),
102 );
103 common::note("escrowed: lognormal on S - PV(divs); jump: dividends subtracted at each ex-date");
104 common::note("the two models differ slightly by construction — that gap is expected, not a bug");
105
106 common::check(
107 "escrowed analytic == BS on the escrowed spot",
108 analytic.npv(),
109 bs_price(analytic.base.effective_spot(), STRIKE, RATE, 0.0, VOL, 1.0, PutOrCall::Call),
110 1e-10,
111 );
112
113 common::section("Where the jump model matters: American exercise and barriers");
114 common::table_header();
115 common::row(
116 "American put, FD (jumps)",
117 &with_divs(base())
118 .american()
119 .vanilla(PutOrCall::Put)
120 .engine(Engine::FiniteDifference)
121 .build(),
122 );
123 common::row(
124 "American put, no dividends",
125 &base().american().vanilla(PutOrCall::Put).engine(Engine::FiniteDifference).build(),
126 );
127 common::row(
128 "Down-and-out call H=85, MC (jumps)",
129 &with_divs(base())
130 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
131 .engine(Engine::MonteCarlo)
132 .paths(50_000)
133 .build(),
134 );
135 common::row(
136 "Down-and-out call H=85, no dividends",
137 &base()
138 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
139 .engine(Engine::MonteCarlo)
140 .paths(50_000)
141 .build(),
142 );
143 common::note("dividend drops push the path toward a down barrier and change exercise timing");
144
145 common::section("Put-call parity with full carry");
146 let call = with_divs(base()).borrow_cost(0.02).vanilla(PutOrCall::Call).build();
147 let put = with_divs(base()).borrow_cost(0.02).vanilla(PutOrCall::Put).build();
148 let parity = call.base.effective_spot() * (-call.base.carry_yield() * 1.0_f64).exp()
149 - STRIKE * (-RATE * 1.0_f64).exp();
150 common::check("C - P = S_eff e^{-(q+b)T} - K e^{-rT}", call.npv() - put.npv(), parity, 1e-10);
151 println!();
152}Additional examples can be found in:
Sourcepub fn binary(
self,
put_or_call: PutOrCall,
binary_type: BinaryType,
cash: f64,
) -> Self
pub fn binary( self, put_or_call: PutOrCall, binary_type: BinaryType, cash: f64, ) -> Self
Examples found in repository?
examples/binary_option.rs (line 49)
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}More examples
examples/heston_option.rs (line 74)
40fn main() {
41 let p = params();
42 common::title(&format!(
43 "HESTON — v0={} kappa={} theta={} vol-of-vol={} rho={}",
44 p.v0, p.kappa, p.theta, p.vol_of_vol, p.rho
45 ));
46 common::note(&format!(
47 "Feller condition 2*kappa*theta >= vol_of_vol^2: {}",
48 if p.feller_condition_holds() { "holds" } else { "VIOLATED (variance can touch zero)" }
49 ));
50
51 common::section("Vanilla: semi-analytic vs Monte Carlo");
52 common::table_header();
53 for pc in [PutOrCall::Call, PutOrCall::Put] {
54 common::row(
55 &format!("Analytical (char. function), {pc:?}"),
56 &base().vanilla(pc).engine(Engine::BlackScholes).build(),
57 );
58 common::row(
59 &format!("Monte Carlo (full-trunc Euler), {pc:?}"),
60 &base().vanilla(pc).engine(Engine::MonteCarlo).paths(100_000).build(),
61 );
62 }
63 common::row(
64 "Finite difference (unsupported)",
65 &base().vanilla(PutOrCall::Call).engine(Engine::FiniteDifference).build(),
66 );
67 common::note("MC vega/theta bump sqrt(v0) and sqrt(theta) in parallel");
68
69 common::section("Binaries under Heston");
70 common::table_header();
71 common::row(
72 "Cash-or-nothing call (analytic)",
73 &base()
74 .binary(PutOrCall::Call, BinaryType::CashOrNothing, 1.0)
75 .engine(Engine::BlackScholes)
76 .build(),
77 );
78 common::row(
79 "Cash-or-nothing call (MC)",
80 &base()
81 .binary(PutOrCall::Call, BinaryType::CashOrNothing, 1.0)
82 .engine(Engine::MonteCarlo)
83 .paths(100_000)
84 .build(),
85 );
86 common::row(
87 "Asset-or-nothing call (analytic)",
88 &base()
89 .binary(PutOrCall::Call, BinaryType::AssetOrNothing, 0.0)
90 .engine(Engine::BlackScholes)
91 .build(),
92 );
93
94 common::section("Path-dependent payoffs (Monte Carlo only)");
95 common::table_header();
96 common::row(
97 "Down-and-out call H=85",
98 &base()
99 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
100 .engine(Engine::MonteCarlo)
101 .paths(50_000)
102 .build(),
103 );
104 common::row(
105 "Down-and-in put H=85",
106 &base()
107 .barrier(PutOrCall::Put, BarrierDirection::Down, KnockType::In, 85.0)
108 .engine(Engine::MonteCarlo)
109 .paths(50_000)
110 .build(),
111 );
112
113 common::section("Identities");
114 let call = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build();
115 let put = base().vanilla(PutOrCall::Put).engine(Engine::BlackScholes).build();
116 let parity = SPOT * (-DIV * 1.0_f64).exp() - STRIKE * (-RATE * 1.0_f64).exp();
117 common::check("put-call parity", call.npv() - put.npv(), parity, 1e-10);
118 let asset = base()
119 .binary(PutOrCall::Call, BinaryType::AssetOrNothing, 0.0)
120 .engine(Engine::BlackScholes)
121 .build();
122 let k_cash = base()
123 .binary(PutOrCall::Call, BinaryType::CashOrNothing, STRIKE)
124 .engine(Engine::BlackScholes)
125 .build();
126 common::check("vanilla = asset digital - K cash digitals", call.npv(), asset.npv() - k_cash.npv(), 1e-10);
127 common::check(
128 "vol-of-vol -> 0 degenerates to Black-Scholes",
129 heston_price(
130 SPOT,
131 STRIKE,
132 RATE,
133 DIV,
134 1.0,
135 &HestonParams { vol_of_vol: 1e-4, ..params() },
136 PutOrCall::Call,
137 ),
138 bs_price(SPOT, STRIKE, RATE, DIV, p.v0.sqrt(), 1.0, PutOrCall::Call),
139 1e-4,
140 );
141
142 common::section("The Heston smile (implied vol backed out of Heston prices)");
143 println!(" {:>8} {:>14} {:>14}", "strike", "heston price", "implied vol");
144 for k in [70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0] {
145 let price = heston_price(SPOT, k, RATE, DIV, 1.0, &p, PutOrCall::Call);
146 let iv = implied_vol_from_price(SPOT, k, RATE, DIV, 1.0, price, PutOrCall::Call)
147 .unwrap_or(f64::NAN);
148 println!(" {k:>8.1} {price:>14.6} {:>13.4}%", iv * 100.0);
149 }
150 common::note("rho < 0 tilts the smile: low strikes carry higher implied vol");
151
152 common::section("Correlation and vol-of-vol control the smile shape");
153 println!(" {:>6} {:>8} {:>12} {:>12} {:>12}", "rho", "vol-of-vol", "iv(80)", "iv(100)", "iv(120)");
154 for (rho, vov) in [(-0.7, 0.4), (0.0, 0.4), (0.7, 0.4), (-0.7, 0.1), (-0.7, 0.8)] {
155 let hp = HestonParams { rho, vol_of_vol: vov, ..params() };
156 let iv = |k: f64| {
157 let price = heston_price(SPOT, k, RATE, DIV, 1.0, &hp, PutOrCall::Call);
158 implied_vol_from_price(SPOT, k, RATE, DIV, 1.0, price, PutOrCall::Call)
159 .unwrap_or(f64::NAN)
160 * 100.0
161 };
162 println!(" {rho:>6.1} {vov:>10.1} {:>11.3}% {:>11.3}% {:>11.3}%", iv(80.0), iv(100.0), iv(120.0));
163 }
164 common::note("rho controls the skew (tilt); vol-of-vol controls the smile (curvature)");
165 println!();
166}Sourcepub fn barrier(
self,
put_or_call: PutOrCall,
direction: BarrierDirection,
knock: KnockType,
barrier: f64,
) -> Self
pub fn barrier( self, put_or_call: PutOrCall, direction: BarrierDirection, knock: KnockType, barrier: f64, ) -> Self
Examples found in repository?
examples/dividends_and_borrow.rs (line 130)
39fn main() {
40 common::title("DIVIDENDS AND BORROW COST — S=100 K=100 sigma=30% r=5% T=1y");
41
42 common::section("Continuous carry: dividend yield and borrow cost are interchangeable");
43 common::table_header();
44 common::row("no carry", &base().vanilla(PutOrCall::Call).build());
45 common::row("q = 4%", &base().dividend_yield(0.04).vanilla(PutOrCall::Call).build());
46 common::row("borrow = 4%", &base().borrow_cost(0.04).vanilla(PutOrCall::Call).build());
47 common::row(
48 "q = 1% + borrow = 3%",
49 &base().dividend_yield(0.01).borrow_cost(0.03).vanilla(PutOrCall::Call).build(),
50 );
51 common::note("carry_yield() = dividend_yield + borrow_cost enters every formula as 'q'");
52
53 let q_only = base().dividend_yield(0.04).vanilla(PutOrCall::Call).build();
54 let split = base().dividend_yield(0.01).borrow_cost(0.03).vanilla(PutOrCall::Call).build();
55 common::check("q=4% vs q=1%+b=3%", split.npv(), q_only.npv(), 1e-12);
56
57 common::section("Hard-to-borrow names: high borrow cost lowers the forward");
58 common::table_header();
59 for b in [0.0, 0.02, 0.05, 0.15] {
60 let option = base().borrow_cost(b).vanilla(PutOrCall::Call).build();
61 common::row(&format!("borrow = {:.0}%", b * 100.0), &option);
62 }
63 let hard = base().borrow_cost(0.15).vanilla(PutOrCall::Call).build();
64 println!(
65 " forward with 15% borrow: {:.4} (vs spot {SPOT})",
66 hard.base.forward_price()
67 );
68
69 common::section("Discrete cash dividends: 2 x 1.50 over the year");
70 let with_divs = |b: EquityOptionBuilder| {
71 b.cash_dividend(NaiveDate::from_ymd_opt(2026, 4, 1).unwrap(), 1.5)
72 .cash_dividend(NaiveDate::from_ymd_opt(2026, 10, 1).unwrap(), 1.5)
73 };
74 let analytic = with_divs(base()).vanilla(PutOrCall::Call).build();
75 println!(
76 " spot {SPOT} - PV(dividends) {:.6} = escrowed spot {:.6}",
77 analytic.base.pv_cash_dividends(),
78 analytic.base.effective_spot()
79 );
80 common::table_header();
81 common::row("Analytical (escrowed model)", &analytic);
82 common::row(
83 "Binomial (escrowed)",
84 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::Binomial).build(),
85 );
86 common::row(
87 "Finite difference (jump model)",
88 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::FiniteDifference).build(),
89 );
90 common::row(
91 "Monte Carlo terminal (escrowed)",
92 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::MonteCarlo).build(),
93 );
94 common::row(
95 "Monte Carlo path-wise (jump model)",
96 &with_divs(base())
97 .vanilla(PutOrCall::Call)
98 .engine(Engine::MonteCarlo)
99 .mc_time_steps(200)
100 .paths(50_000)
101 .build(),
102 );
103 common::note("escrowed: lognormal on S - PV(divs); jump: dividends subtracted at each ex-date");
104 common::note("the two models differ slightly by construction — that gap is expected, not a bug");
105
106 common::check(
107 "escrowed analytic == BS on the escrowed spot",
108 analytic.npv(),
109 bs_price(analytic.base.effective_spot(), STRIKE, RATE, 0.0, VOL, 1.0, PutOrCall::Call),
110 1e-10,
111 );
112
113 common::section("Where the jump model matters: American exercise and barriers");
114 common::table_header();
115 common::row(
116 "American put, FD (jumps)",
117 &with_divs(base())
118 .american()
119 .vanilla(PutOrCall::Put)
120 .engine(Engine::FiniteDifference)
121 .build(),
122 );
123 common::row(
124 "American put, no dividends",
125 &base().american().vanilla(PutOrCall::Put).engine(Engine::FiniteDifference).build(),
126 );
127 common::row(
128 "Down-and-out call H=85, MC (jumps)",
129 &with_divs(base())
130 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
131 .engine(Engine::MonteCarlo)
132 .paths(50_000)
133 .build(),
134 );
135 common::row(
136 "Down-and-out call H=85, no dividends",
137 &base()
138 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
139 .engine(Engine::MonteCarlo)
140 .paths(50_000)
141 .build(),
142 );
143 common::note("dividend drops push the path toward a down barrier and change exercise timing");
144
145 common::section("Put-call parity with full carry");
146 let call = with_divs(base()).borrow_cost(0.02).vanilla(PutOrCall::Call).build();
147 let put = with_divs(base()).borrow_cost(0.02).vanilla(PutOrCall::Put).build();
148 let parity = call.base.effective_spot() * (-call.base.carry_yield() * 1.0_f64).exp()
149 - STRIKE * (-RATE * 1.0_f64).exp();
150 common::check("C - P = S_eff e^{-(q+b)T} - K e^{-rT}", call.npv() - put.npv(), parity, 1e-10);
151 println!();
152}More examples
examples/barrier_option.rs (line 57)
40fn main() {
41 common::title("BARRIER OPTIONS — S=100 K=100 sigma=30% r=5% q=2% T=1y");
42
43 common::section("All eight types, analytic (Reiner-Rubinstein)");
44 common::table_header();
45 for (dir, knock, pc, level) in [
46 (BarrierDirection::Down, KnockType::In, PutOrCall::Call, 90.0),
47 (BarrierDirection::Down, KnockType::Out, PutOrCall::Call, 90.0),
48 (BarrierDirection::Down, KnockType::In, PutOrCall::Put, 90.0),
49 (BarrierDirection::Down, KnockType::Out, PutOrCall::Put, 90.0),
50 (BarrierDirection::Up, KnockType::In, PutOrCall::Call, 120.0),
51 (BarrierDirection::Up, KnockType::Out, PutOrCall::Call, 120.0),
52 (BarrierDirection::Up, KnockType::In, PutOrCall::Put, 120.0),
53 (BarrierDirection::Up, KnockType::Out, PutOrCall::Put, 120.0),
54 ] {
55 common::row(
56 &format!("{dir:?}-and-{knock:?} {pc:?} H={level}"),
57 &base().barrier(pc, dir, knock, level).engine(Engine::BlackScholes).build(),
58 );
59 }
60
61 common::section("Engine comparison: down-and-out call, H=90");
62 common::table_header();
63 for (label, engine) in [
64 ("Analytical (Reiner-Rubinstein)", Engine::BlackScholes),
65 ("Finite difference (absorbing)", Engine::FiniteDifference),
66 ("Monte Carlo (Brownian bridge)", Engine::MonteCarlo),
67 ("Binomial (unsupported)", Engine::Binomial),
68 ] {
69 common::row(
70 label,
71 &base()
72 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 90.0)
73 .engine(engine)
74 .build(),
75 );
76 }
77 common::note("MC applies a bridge crossing correction, so monitoring is effectively continuous");
78
79 common::section("In-out parity: KI + KO = vanilla");
80 let vanilla = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build();
81 for level in [80.0, 90.0, 99.0] {
82 let ki = base()
83 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::In, level)
84 .engine(Engine::BlackScholes)
85 .build();
86 let ko = base()
87 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, level)
88 .engine(Engine::BlackScholes)
89 .build();
90 common::check(
91 &format!("H={level}: KI + KO"),
92 ki.npv() + ko.npv(),
93 vanilla.npv(),
94 1e-10,
95 );
96 }
97
98 common::section("Limits");
99 common::check(
100 "far barrier: KO call -> vanilla",
101 barrier_price(SPOT, STRIKE, 1e-4, RATE, DIV, VOL, 1.0, BarrierDirection::Down, KnockType::Out, PutOrCall::Call),
102 vanilla.npv(),
103 1e-9,
104 );
105 common::check(
106 "up-and-out call with K >= H is worthless",
107 barrier_price(SPOT, 110.0, 105.0, RATE, DIV, VOL, 1.0, BarrierDirection::Up, KnockType::Out, PutOrCall::Call),
108 0.0,
109 1e-12,
110 );
111 common::check(
112 "spot at barrier: KO = 0",
113 base()
114 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, SPOT)
115 .engine(Engine::BlackScholes)
116 .build()
117 .npv(),
118 0.0,
119 1e-12,
120 );
121
122 common::section("Barrier level sweep: down-and-out call");
123 common::table_header();
124 for level in [50.0, 70.0, 85.0, 95.0, 99.0] {
125 common::row(
126 &format!("H={level}"),
127 &base()
128 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, level)
129 .engine(Engine::BlackScholes)
130 .build(),
131 );
132 }
133 common::note("value decreases as the barrier approaches spot; delta can exceed 1 near it");
134
135 common::section("Smile matters: down-and-out call under local vol");
136 let skewed = VolSurface::from_strike_grid(
137 &[Tenor::YearFraction(0.5), Tenor::YearFraction(1.0), Tenor::YearFraction(2.0)],
138 &[70.0, 85.0, 100.0, 115.0, 130.0],
139 &[
140 vec![0.38, 0.34, 0.30, 0.28, 0.27],
141 vec![0.37, 0.34, 0.30, 0.29, 0.28],
142 vec![0.36, 0.33, 0.30, 0.29, 0.28],
143 ],
144 asof(),
145 DayCountConvention::Act365,
146 )
147 .unwrap();
148 common::table_header();
149 common::row(
150 "GBM (flat 30%)",
151 &base()
152 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 90.0)
153 .engine(Engine::MonteCarlo)
154 .paths(50_000)
155 .build(),
156 );
157 common::row(
158 "Local vol (skewed surface)",
159 &base()
160 .vol_surface(skewed)
161 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 90.0)
162 .engine(Engine::MonteCarlo)
163 .model(McModel::LocalVol)
164 .paths(50_000)
165 .build(),
166 );
167 common::note("downside skew raises the knock-out probability, lowering the price");
168 println!();
169}examples/heston_option.rs (line 99)
40fn main() {
41 let p = params();
42 common::title(&format!(
43 "HESTON — v0={} kappa={} theta={} vol-of-vol={} rho={}",
44 p.v0, p.kappa, p.theta, p.vol_of_vol, p.rho
45 ));
46 common::note(&format!(
47 "Feller condition 2*kappa*theta >= vol_of_vol^2: {}",
48 if p.feller_condition_holds() { "holds" } else { "VIOLATED (variance can touch zero)" }
49 ));
50
51 common::section("Vanilla: semi-analytic vs Monte Carlo");
52 common::table_header();
53 for pc in [PutOrCall::Call, PutOrCall::Put] {
54 common::row(
55 &format!("Analytical (char. function), {pc:?}"),
56 &base().vanilla(pc).engine(Engine::BlackScholes).build(),
57 );
58 common::row(
59 &format!("Monte Carlo (full-trunc Euler), {pc:?}"),
60 &base().vanilla(pc).engine(Engine::MonteCarlo).paths(100_000).build(),
61 );
62 }
63 common::row(
64 "Finite difference (unsupported)",
65 &base().vanilla(PutOrCall::Call).engine(Engine::FiniteDifference).build(),
66 );
67 common::note("MC vega/theta bump sqrt(v0) and sqrt(theta) in parallel");
68
69 common::section("Binaries under Heston");
70 common::table_header();
71 common::row(
72 "Cash-or-nothing call (analytic)",
73 &base()
74 .binary(PutOrCall::Call, BinaryType::CashOrNothing, 1.0)
75 .engine(Engine::BlackScholes)
76 .build(),
77 );
78 common::row(
79 "Cash-or-nothing call (MC)",
80 &base()
81 .binary(PutOrCall::Call, BinaryType::CashOrNothing, 1.0)
82 .engine(Engine::MonteCarlo)
83 .paths(100_000)
84 .build(),
85 );
86 common::row(
87 "Asset-or-nothing call (analytic)",
88 &base()
89 .binary(PutOrCall::Call, BinaryType::AssetOrNothing, 0.0)
90 .engine(Engine::BlackScholes)
91 .build(),
92 );
93
94 common::section("Path-dependent payoffs (Monte Carlo only)");
95 common::table_header();
96 common::row(
97 "Down-and-out call H=85",
98 &base()
99 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
100 .engine(Engine::MonteCarlo)
101 .paths(50_000)
102 .build(),
103 );
104 common::row(
105 "Down-and-in put H=85",
106 &base()
107 .barrier(PutOrCall::Put, BarrierDirection::Down, KnockType::In, 85.0)
108 .engine(Engine::MonteCarlo)
109 .paths(50_000)
110 .build(),
111 );
112
113 common::section("Identities");
114 let call = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build();
115 let put = base().vanilla(PutOrCall::Put).engine(Engine::BlackScholes).build();
116 let parity = SPOT * (-DIV * 1.0_f64).exp() - STRIKE * (-RATE * 1.0_f64).exp();
117 common::check("put-call parity", call.npv() - put.npv(), parity, 1e-10);
118 let asset = base()
119 .binary(PutOrCall::Call, BinaryType::AssetOrNothing, 0.0)
120 .engine(Engine::BlackScholes)
121 .build();
122 let k_cash = base()
123 .binary(PutOrCall::Call, BinaryType::CashOrNothing, STRIKE)
124 .engine(Engine::BlackScholes)
125 .build();
126 common::check("vanilla = asset digital - K cash digitals", call.npv(), asset.npv() - k_cash.npv(), 1e-10);
127 common::check(
128 "vol-of-vol -> 0 degenerates to Black-Scholes",
129 heston_price(
130 SPOT,
131 STRIKE,
132 RATE,
133 DIV,
134 1.0,
135 &HestonParams { vol_of_vol: 1e-4, ..params() },
136 PutOrCall::Call,
137 ),
138 bs_price(SPOT, STRIKE, RATE, DIV, p.v0.sqrt(), 1.0, PutOrCall::Call),
139 1e-4,
140 );
141
142 common::section("The Heston smile (implied vol backed out of Heston prices)");
143 println!(" {:>8} {:>14} {:>14}", "strike", "heston price", "implied vol");
144 for k in [70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0] {
145 let price = heston_price(SPOT, k, RATE, DIV, 1.0, &p, PutOrCall::Call);
146 let iv = implied_vol_from_price(SPOT, k, RATE, DIV, 1.0, price, PutOrCall::Call)
147 .unwrap_or(f64::NAN);
148 println!(" {k:>8.1} {price:>14.6} {:>13.4}%", iv * 100.0);
149 }
150 common::note("rho < 0 tilts the smile: low strikes carry higher implied vol");
151
152 common::section("Correlation and vol-of-vol control the smile shape");
153 println!(" {:>6} {:>8} {:>12} {:>12} {:>12}", "rho", "vol-of-vol", "iv(80)", "iv(100)", "iv(120)");
154 for (rho, vov) in [(-0.7, 0.4), (0.0, 0.4), (0.7, 0.4), (-0.7, 0.1), (-0.7, 0.8)] {
155 let hp = HestonParams { rho, vol_of_vol: vov, ..params() };
156 let iv = |k: f64| {
157 let price = heston_price(SPOT, k, RATE, DIV, 1.0, &hp, PutOrCall::Call);
158 implied_vol_from_price(SPOT, k, RATE, DIV, 1.0, price, PutOrCall::Call)
159 .unwrap_or(f64::NAN)
160 * 100.0
161 };
162 println!(" {rho:>6.1} {vov:>10.1} {:>11.3}% {:>11.3}% {:>11.3}%", iv(80.0), iv(100.0), iv(120.0));
163 }
164 common::note("rho controls the skew (tilt); vol-of-vol controls the smile (curvature)");
165 println!();
166}Sourcepub fn asian(
self,
put_or_call: PutOrCall,
averaging: AveragingType,
strike_type: AsianStrikeType,
) -> Self
pub fn asian( self, put_or_call: PutOrCall, averaging: AveragingType, strike_type: AsianStrikeType, ) -> Self
Examples found in repository?
examples/asian_option.rs (line 43)
35fn main() {
36 common::title("ASIAN OPTIONS — S=100 K=100 sigma=30% r=5% q=2% T=1y");
37
38 common::section("Fixed strike (average price) call");
39 common::table_header();
40 common::row(
41 "Geometric, analytic (exact)",
42 &base()
43 .asian(PutOrCall::Call, AveragingType::Geometric, AsianStrikeType::FixedStrike)
44 .engine(Engine::BlackScholes)
45 .build(),
46 );
47 common::row(
48 "Geometric, Monte Carlo",
49 &base()
50 .asian(PutOrCall::Call, AveragingType::Geometric, AsianStrikeType::FixedStrike)
51 .engine(Engine::MonteCarlo)
52 .paths(50_000)
53 .build(),
54 );
55 common::row(
56 "Arithmetic, Turnbull-Wakeman",
57 &base()
58 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
59 .engine(Engine::BlackScholes)
60 .build(),
61 );
62 common::row(
63 "Arithmetic, MC + geometric CV",
64 &base()
65 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
66 .engine(Engine::MonteCarlo)
67 .paths(50_000)
68 .build(),
69 );
70
71 common::section("Control variate effect (same path count)");
72 common::table_header();
73 let with_cv = base()
74 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
75 .engine(Engine::MonteCarlo)
76 .paths(20_000)
77 .build();
78 let without_cv = base()
79 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
80 .engine(Engine::MonteCarlo)
81 .paths(20_000)
82 .mc_config({
83 // Euler stepping disables the control variate precondition
84 let mut c = rustyqlib::equity::montecarlo::MonteCarloConfig::default();
85 c.paths = 20_000;
86 c.scheme = DiscretizationScheme::Euler;
87 c.time_steps = 100;
88 c
89 })
90 .build();
91 common::row("with geometric control variate", &with_cv);
92 common::row("without (Euler path route)", &without_cv);
93 common::note("compare the std err column: the CV collapses the variance");
94
95 common::section("Floating strike (average strike)");
96 common::table_header();
97 for pc in [PutOrCall::Call, PutOrCall::Put] {
98 common::row(
99 &format!("Monte Carlo, {pc:?}"),
100 &base()
101 .asian(pc, AveragingType::Arithmetic, AsianStrikeType::FloatingStrike)
102 .engine(Engine::MonteCarlo)
103 .paths(50_000)
104 .build(),
105 );
106 common::row(
107 &format!("Analytic (unsupported), {pc:?}"),
108 &base()
109 .asian(pc, AveragingType::Arithmetic, AsianStrikeType::FloatingStrike)
110 .engine(Engine::BlackScholes)
111 .build(),
112 );
113 }
114
115 common::section("Orderings and limits");
116 let vanilla = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build().npv();
117 let geo = geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, None, PutOrCall::Call);
118 let arith = turnbull_wakeman_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, PutOrCall::Call);
119 println!(" geometric {geo:.6} < arithmetic {arith:.6} < vanilla {vanilla:.6}");
120 common::note("AM-GM: the arithmetic average dominates the geometric one");
121 common::note("averaging reduces effective volatility (sigma^2 T / 3), so both sit below vanilla");
122 common::check(
123 "discrete geometric (n=1e5) -> continuous",
124 geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, Some(100_000), PutOrCall::Call),
125 geo,
126 1e-3,
127 );
128
129 common::section("Averaging frequency (geometric, exact)");
130 for n in [4usize, 12, 52, 252] {
131 let price =
132 geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, Some(n), PutOrCall::Call);
133 println!(" {n:>4} fixings: {price:.6}");
134 }
135 println!();
136}Sourcepub fn forward_start(
self,
put_or_call: PutOrCall,
strike_fraction: f64,
start_fraction: f64,
) -> Self
pub fn forward_start( self, put_or_call: PutOrCall, strike_fraction: f64, start_fraction: f64, ) -> Self
start_fraction is the strike-fixing time as a fraction of the
option’s life, in (0, 1).
Examples found in repository?
examples/forward_start_option.rs (line 47)
39fn main() {
40 common::title("FORWARD-START OPTION — S=100, strike = 1.0 x S(0.5y), T=1y, sigma=30%");
41
42 common::section("Black-Scholes: analytic vs Monte Carlo");
43 common::table_header();
44 common::row(
45 "Analytical (Rubinstein)",
46 &base()
47 .forward_start(PutOrCall::Call, 1.0, START)
48 .engine(Engine::BlackScholes)
49 .build(),
50 );
51 common::row(
52 "Monte Carlo (GBM)",
53 &base()
54 .forward_start(PutOrCall::Call, 1.0, START)
55 .engine(Engine::MonteCarlo)
56 .paths(100_000)
57 .build(),
58 );
59 common::row(
60 "Finite difference (unsupported)",
61 &base()
62 .forward_start(PutOrCall::Call, 1.0, START)
63 .engine(Engine::FiniteDifference)
64 .build(),
65 );
66
67 common::section("Forward smile: Heston vs Black-Scholes");
68 common::table_header();
69 let bs = base()
70 .forward_start(PutOrCall::Call, 1.0, START)
71 .engine(Engine::BlackScholes)
72 .build()
73 .npv();
74 common::row(
75 "Heston vol-of-vol=0.001 (-> BS)",
76 &base()
77 .forward_start(PutOrCall::Call, 1.0, START)
78 .engine(Engine::MonteCarlo)
79 .heston(heston_params(1e-3, 0.0))
80 .paths(50_000)
81 .build(),
82 );
83 for (vov, rho) in [(0.2, -0.7), (0.4, -0.7), (0.6, -0.7), (0.4, 0.0)] {
84 common::row(
85 &format!("Heston vol-of-vol={vov}, rho={rho}"),
86 &base()
87 .forward_start(PutOrCall::Call, 1.0, START)
88 .engine(Engine::MonteCarlo)
89 .heston(heston_params(vov, rho))
90 .paths(50_000)
91 .build(),
92 );
93 }
94 common::note(&format!("Black-Scholes reference: {bs:.6}"));
95 common::note("the gap is the forward-smile effect — the reason to price these on a stoch-vol model");
96
97 common::section("Strike fraction sweep (analytic)");
98 common::table_header();
99 for k in [0.9, 0.95, 1.0, 1.05, 1.1] {
100 common::row(
101 &format!("strike = {k} x S(t_f), call"),
102 &base().forward_start(PutOrCall::Call, k, START).engine(Engine::BlackScholes).build(),
103 );
104 }
105
106 common::section("Fixing date sweep (analytic, ATM)");
107 common::table_header();
108 for start in [0.1, 0.25, 0.5, 0.75, 0.9] {
109 common::row(
110 &format!("fixing at {:.0}% of life", start * 100.0),
111 &base().forward_start(PutOrCall::Call, 1.0, start).engine(Engine::BlackScholes).build(),
112 );
113 }
114 common::note("later fixing leaves less time to expiry, so the option is worth less");
115
116 common::section("Identities");
117 common::check(
118 "immediate fixing -> vanilla struck at S0",
119 forward_start_price(SPOT, 1.0, RATE, DIV, VOL, 1e-6, 1.0, PutOrCall::Call),
120 base()
121 .strike(SPOT)
122 .vanilla(PutOrCall::Call)
123 .engine(Engine::BlackScholes)
124 .build()
125 .npv(),
126 1e-3,
127 );
128 let p100 = forward_start_price(100.0, 1.0, RATE, DIV, VOL, 0.5, 1.0, PutOrCall::Call);
129 let p200 = forward_start_price(200.0, 1.0, RATE, DIV, VOL, 0.5, 1.0, PutOrCall::Call);
130 common::check("homogeneity: price(2S) = 2 price(S)", p200, 2.0 * p100, 1e-12);
131 let fs = base()
132 .forward_start(PutOrCall::Call, 1.0, START)
133 .engine(Engine::BlackScholes)
134 .build();
135 common::check("delta = price / spot (homogeneity)", fs.delta(), fs.npv() / SPOT, 1e-6);
136 println!();
137}Sourcepub fn autocallable(
self,
autocall_barrier: f64,
protection_barrier: f64,
coupon: f64,
observations: usize,
notional: f64,
) -> Self
pub fn autocallable( self, autocall_barrier: f64, protection_barrier: f64, coupon: f64, observations: usize, notional: f64, ) -> Self
Examples found in repository?
examples/autocallable_option.rs (line 46)
45fn note(autocall: f64, protection: f64, coupon: f64) -> EquityOptionBuilder {
46 base().autocallable(autocall, protection, coupon, OBSERVATIONS, NOTIONAL)
47}
48
49/// Downward-skewed surface: the shape that actually drives these notes.
50fn skewed_surface() -> VolSurface {
51 VolSurface::from_strike_grid(
52 &[Tenor::YearFraction(0.25), Tenor::YearFraction(0.5), Tenor::YearFraction(1.0)],
53 &[60.0, 70.0, 85.0, 100.0, 115.0, 130.0],
54 &[
55 vec![0.42, 0.38, 0.33, 0.29, 0.27, 0.26],
56 vec![0.41, 0.37, 0.33, 0.30, 0.28, 0.27],
57 vec![0.40, 0.37, 0.33, 0.30, 0.29, 0.28],
58 ],
59 asof(),
60 DayCountConvention::Act365,
61 )
62 .unwrap()
63}
64
65fn main() {
66 common::title(&format!(
67 "AUTOCALLABLE NOTE — N={NOTIONAL} autocall={AUTOCALL} protection={PROTECTION} coupon={COUPON}/period, {OBSERVATIONS} observations, T=1y"
68 ));
69 common::note("pays N + m*coupon if S >= autocall barrier at observation m;");
70 common::note("otherwise N at maturity, or N*S_T/S_0 if the protection barrier was breached.");
71
72 common::section("Model comparison");
73 common::table_header();
74 common::row("GBM (flat 30%)", ¬e(AUTOCALL, PROTECTION, COUPON).build());
75 common::row(
76 "Local vol (skewed surface)",
77 ¬e(AUTOCALL, PROTECTION, COUPON)
78 .vol_surface(skewed_surface())
79 .model(McModel::LocalVol)
80 .build(),
81 );
82 common::row(
83 "Heston (vol-of-vol=0.4, rho=-0.7)",
84 ¬e(AUTOCALL, PROTECTION, COUPON)
85 .heston(HestonParams {
86 v0: VOL * VOL,
87 kappa: 2.0,
88 theta: VOL * VOL,
89 vol_of_vol: 0.4,
90 rho: -0.7,
91 })
92 .build(),
93 );
94 common::row(
95 "Analytical (unsupported)",
96 ¬e(AUTOCALL, PROTECTION, COUPON).engine(Engine::BlackScholes).build(),
97 );
98 common::note("skew/stoch-vol raise the knock-in probability, lowering the note value");
99
100 common::section("Structure sensitivity (GBM)");
101 common::table_header();
102 for coupon in [0.0, 3.0, 6.0, 9.0] {
103 common::row(&format!("coupon = {coupon}/period"), ¬e(AUTOCALL, PROTECTION, coupon).build());
104 }
105 for protection in [50.0, 60.0, 70.0, 80.0] {
106 common::row(
107 &format!("protection barrier = {protection}"),
108 ¬e(AUTOCALL, protection, COUPON).build(),
109 );
110 }
111 for autocall in [95.0, 100.0, 105.0, 110.0] {
112 common::row(
113 &format!("autocall barrier = {autocall}"),
114 ¬e(autocall, PROTECTION, COUPON).build(),
115 );
116 }
117
118 common::section("Observation frequency (GBM)");
119 common::table_header();
120 for obs in [1usize, 2, 4, 12] {
121 common::row(
122 &format!("{obs} observations"),
123 &base().autocallable(AUTOCALL, PROTECTION, COUPON, obs, NOTIONAL).build(),
124 );
125 }
126
127 common::section("Degenerate cases (exact identities)");
128 let always_calls = base()
129 .autocallable(1e-9, 50.0, COUPON, OBSERVATIONS, NOTIONAL)
130 .build()
131 .npv();
132 common::check(
133 "barrier at 0 -> called at t1 with 1 coupon",
134 always_calls,
135 (NOTIONAL + COUPON) * (-RATE * 0.25_f64).exp(),
136 1e-8,
137 );
138 let never_calls = base()
139 .autocallable(1e12, 1e-9, COUPON, OBSERVATIONS, NOTIONAL)
140 .build()
141 .npv();
142 common::check(
143 "unreachable barriers -> zero-coupon bond",
144 never_calls,
145 NOTIONAL * (-RATE * 1.0_f64).exp(),
146 1e-8,
147 );
148 let full_downside = base()
149 .autocallable(1e12, 1e12, 0.0, OBSERVATIONS, NOTIONAL)
150 .dividend_yield(0.0)
151 .build()
152 .npv();
153 common::check(
154 "always knocked in, no coupon -> discounted forward",
155 full_downside,
156 NOTIONAL,
157 0.3,
158 );
159 println!();
160}Sourcepub fn engine(self, engine: Engine) -> Self
pub fn engine(self, engine: Engine) -> Self
Examples found in repository?
examples/vanilla_option.rs (line 40)
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}More examples
examples/autocallable_option.rs (line 41)
32fn base() -> EquityOptionBuilder {
33 EquityOptionBuilder::new()
34 .symbol("ATHENA")
35 .spot(SPOT)
36 .flat_vol(VOL)
37 .flat_rate(RATE)
38 .dividend_yield(DIV)
39 .valuation_date(asof())
40 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
41 .engine(Engine::MonteCarlo)
42 .paths(50_000)
43}
44
45fn note(autocall: f64, protection: f64, coupon: f64) -> EquityOptionBuilder {
46 base().autocallable(autocall, protection, coupon, OBSERVATIONS, NOTIONAL)
47}
48
49/// Downward-skewed surface: the shape that actually drives these notes.
50fn skewed_surface() -> VolSurface {
51 VolSurface::from_strike_grid(
52 &[Tenor::YearFraction(0.25), Tenor::YearFraction(0.5), Tenor::YearFraction(1.0)],
53 &[60.0, 70.0, 85.0, 100.0, 115.0, 130.0],
54 &[
55 vec![0.42, 0.38, 0.33, 0.29, 0.27, 0.26],
56 vec![0.41, 0.37, 0.33, 0.30, 0.28, 0.27],
57 vec![0.40, 0.37, 0.33, 0.30, 0.29, 0.28],
58 ],
59 asof(),
60 DayCountConvention::Act365,
61 )
62 .unwrap()
63}
64
65fn main() {
66 common::title(&format!(
67 "AUTOCALLABLE NOTE — N={NOTIONAL} autocall={AUTOCALL} protection={PROTECTION} coupon={COUPON}/period, {OBSERVATIONS} observations, T=1y"
68 ));
69 common::note("pays N + m*coupon if S >= autocall barrier at observation m;");
70 common::note("otherwise N at maturity, or N*S_T/S_0 if the protection barrier was breached.");
71
72 common::section("Model comparison");
73 common::table_header();
74 common::row("GBM (flat 30%)", ¬e(AUTOCALL, PROTECTION, COUPON).build());
75 common::row(
76 "Local vol (skewed surface)",
77 ¬e(AUTOCALL, PROTECTION, COUPON)
78 .vol_surface(skewed_surface())
79 .model(McModel::LocalVol)
80 .build(),
81 );
82 common::row(
83 "Heston (vol-of-vol=0.4, rho=-0.7)",
84 ¬e(AUTOCALL, PROTECTION, COUPON)
85 .heston(HestonParams {
86 v0: VOL * VOL,
87 kappa: 2.0,
88 theta: VOL * VOL,
89 vol_of_vol: 0.4,
90 rho: -0.7,
91 })
92 .build(),
93 );
94 common::row(
95 "Analytical (unsupported)",
96 ¬e(AUTOCALL, PROTECTION, COUPON).engine(Engine::BlackScholes).build(),
97 );
98 common::note("skew/stoch-vol raise the knock-in probability, lowering the note value");
99
100 common::section("Structure sensitivity (GBM)");
101 common::table_header();
102 for coupon in [0.0, 3.0, 6.0, 9.0] {
103 common::row(&format!("coupon = {coupon}/period"), ¬e(AUTOCALL, PROTECTION, coupon).build());
104 }
105 for protection in [50.0, 60.0, 70.0, 80.0] {
106 common::row(
107 &format!("protection barrier = {protection}"),
108 ¬e(AUTOCALL, protection, COUPON).build(),
109 );
110 }
111 for autocall in [95.0, 100.0, 105.0, 110.0] {
112 common::row(
113 &format!("autocall barrier = {autocall}"),
114 ¬e(autocall, PROTECTION, COUPON).build(),
115 );
116 }
117
118 common::section("Observation frequency (GBM)");
119 common::table_header();
120 for obs in [1usize, 2, 4, 12] {
121 common::row(
122 &format!("{obs} observations"),
123 &base().autocallable(AUTOCALL, PROTECTION, COUPON, obs, NOTIONAL).build(),
124 );
125 }
126
127 common::section("Degenerate cases (exact identities)");
128 let always_calls = base()
129 .autocallable(1e-9, 50.0, COUPON, OBSERVATIONS, NOTIONAL)
130 .build()
131 .npv();
132 common::check(
133 "barrier at 0 -> called at t1 with 1 coupon",
134 always_calls,
135 (NOTIONAL + COUPON) * (-RATE * 0.25_f64).exp(),
136 1e-8,
137 );
138 let never_calls = base()
139 .autocallable(1e12, 1e-9, COUPON, OBSERVATIONS, NOTIONAL)
140 .build()
141 .npv();
142 common::check(
143 "unreachable barriers -> zero-coupon bond",
144 never_calls,
145 NOTIONAL * (-RATE * 1.0_f64).exp(),
146 1e-8,
147 );
148 let full_downside = base()
149 .autocallable(1e12, 1e12, 0.0, OBSERVATIONS, NOTIONAL)
150 .dividend_yield(0.0)
151 .build()
152 .npv();
153 common::check(
154 "always knocked in, no coupon -> discounted forward",
155 full_downside,
156 NOTIONAL,
157 0.3,
158 );
159 println!();
160}examples/futures_option.rs (line 34)
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}examples/forward_start_option.rs (line 48)
39fn main() {
40 common::title("FORWARD-START OPTION — S=100, strike = 1.0 x S(0.5y), T=1y, sigma=30%");
41
42 common::section("Black-Scholes: analytic vs Monte Carlo");
43 common::table_header();
44 common::row(
45 "Analytical (Rubinstein)",
46 &base()
47 .forward_start(PutOrCall::Call, 1.0, START)
48 .engine(Engine::BlackScholes)
49 .build(),
50 );
51 common::row(
52 "Monte Carlo (GBM)",
53 &base()
54 .forward_start(PutOrCall::Call, 1.0, START)
55 .engine(Engine::MonteCarlo)
56 .paths(100_000)
57 .build(),
58 );
59 common::row(
60 "Finite difference (unsupported)",
61 &base()
62 .forward_start(PutOrCall::Call, 1.0, START)
63 .engine(Engine::FiniteDifference)
64 .build(),
65 );
66
67 common::section("Forward smile: Heston vs Black-Scholes");
68 common::table_header();
69 let bs = base()
70 .forward_start(PutOrCall::Call, 1.0, START)
71 .engine(Engine::BlackScholes)
72 .build()
73 .npv();
74 common::row(
75 "Heston vol-of-vol=0.001 (-> BS)",
76 &base()
77 .forward_start(PutOrCall::Call, 1.0, START)
78 .engine(Engine::MonteCarlo)
79 .heston(heston_params(1e-3, 0.0))
80 .paths(50_000)
81 .build(),
82 );
83 for (vov, rho) in [(0.2, -0.7), (0.4, -0.7), (0.6, -0.7), (0.4, 0.0)] {
84 common::row(
85 &format!("Heston vol-of-vol={vov}, rho={rho}"),
86 &base()
87 .forward_start(PutOrCall::Call, 1.0, START)
88 .engine(Engine::MonteCarlo)
89 .heston(heston_params(vov, rho))
90 .paths(50_000)
91 .build(),
92 );
93 }
94 common::note(&format!("Black-Scholes reference: {bs:.6}"));
95 common::note("the gap is the forward-smile effect — the reason to price these on a stoch-vol model");
96
97 common::section("Strike fraction sweep (analytic)");
98 common::table_header();
99 for k in [0.9, 0.95, 1.0, 1.05, 1.1] {
100 common::row(
101 &format!("strike = {k} x S(t_f), call"),
102 &base().forward_start(PutOrCall::Call, k, START).engine(Engine::BlackScholes).build(),
103 );
104 }
105
106 common::section("Fixing date sweep (analytic, ATM)");
107 common::table_header();
108 for start in [0.1, 0.25, 0.5, 0.75, 0.9] {
109 common::row(
110 &format!("fixing at {:.0}% of life", start * 100.0),
111 &base().forward_start(PutOrCall::Call, 1.0, start).engine(Engine::BlackScholes).build(),
112 );
113 }
114 common::note("later fixing leaves less time to expiry, so the option is worth less");
115
116 common::section("Identities");
117 common::check(
118 "immediate fixing -> vanilla struck at S0",
119 forward_start_price(SPOT, 1.0, RATE, DIV, VOL, 1e-6, 1.0, PutOrCall::Call),
120 base()
121 .strike(SPOT)
122 .vanilla(PutOrCall::Call)
123 .engine(Engine::BlackScholes)
124 .build()
125 .npv(),
126 1e-3,
127 );
128 let p100 = forward_start_price(100.0, 1.0, RATE, DIV, VOL, 0.5, 1.0, PutOrCall::Call);
129 let p200 = forward_start_price(200.0, 1.0, RATE, DIV, VOL, 0.5, 1.0, PutOrCall::Call);
130 common::check("homogeneity: price(2S) = 2 price(S)", p200, 2.0 * p100, 1e-12);
131 let fs = base()
132 .forward_start(PutOrCall::Call, 1.0, START)
133 .engine(Engine::BlackScholes)
134 .build();
135 common::check("delta = price / spot (homogeneity)", fs.delta(), fs.npv() / SPOT, 1e-6);
136 println!();
137}examples/binary_option.rs (line 49)
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}examples/asian_option.rs (line 44)
35fn main() {
36 common::title("ASIAN OPTIONS — S=100 K=100 sigma=30% r=5% q=2% T=1y");
37
38 common::section("Fixed strike (average price) call");
39 common::table_header();
40 common::row(
41 "Geometric, analytic (exact)",
42 &base()
43 .asian(PutOrCall::Call, AveragingType::Geometric, AsianStrikeType::FixedStrike)
44 .engine(Engine::BlackScholes)
45 .build(),
46 );
47 common::row(
48 "Geometric, Monte Carlo",
49 &base()
50 .asian(PutOrCall::Call, AveragingType::Geometric, AsianStrikeType::FixedStrike)
51 .engine(Engine::MonteCarlo)
52 .paths(50_000)
53 .build(),
54 );
55 common::row(
56 "Arithmetic, Turnbull-Wakeman",
57 &base()
58 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
59 .engine(Engine::BlackScholes)
60 .build(),
61 );
62 common::row(
63 "Arithmetic, MC + geometric CV",
64 &base()
65 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
66 .engine(Engine::MonteCarlo)
67 .paths(50_000)
68 .build(),
69 );
70
71 common::section("Control variate effect (same path count)");
72 common::table_header();
73 let with_cv = base()
74 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
75 .engine(Engine::MonteCarlo)
76 .paths(20_000)
77 .build();
78 let without_cv = base()
79 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
80 .engine(Engine::MonteCarlo)
81 .paths(20_000)
82 .mc_config({
83 // Euler stepping disables the control variate precondition
84 let mut c = rustyqlib::equity::montecarlo::MonteCarloConfig::default();
85 c.paths = 20_000;
86 c.scheme = DiscretizationScheme::Euler;
87 c.time_steps = 100;
88 c
89 })
90 .build();
91 common::row("with geometric control variate", &with_cv);
92 common::row("without (Euler path route)", &without_cv);
93 common::note("compare the std err column: the CV collapses the variance");
94
95 common::section("Floating strike (average strike)");
96 common::table_header();
97 for pc in [PutOrCall::Call, PutOrCall::Put] {
98 common::row(
99 &format!("Monte Carlo, {pc:?}"),
100 &base()
101 .asian(pc, AveragingType::Arithmetic, AsianStrikeType::FloatingStrike)
102 .engine(Engine::MonteCarlo)
103 .paths(50_000)
104 .build(),
105 );
106 common::row(
107 &format!("Analytic (unsupported), {pc:?}"),
108 &base()
109 .asian(pc, AveragingType::Arithmetic, AsianStrikeType::FloatingStrike)
110 .engine(Engine::BlackScholes)
111 .build(),
112 );
113 }
114
115 common::section("Orderings and limits");
116 let vanilla = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build().npv();
117 let geo = geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, None, PutOrCall::Call);
118 let arith = turnbull_wakeman_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, PutOrCall::Call);
119 println!(" geometric {geo:.6} < arithmetic {arith:.6} < vanilla {vanilla:.6}");
120 common::note("AM-GM: the arithmetic average dominates the geometric one");
121 common::note("averaging reduces effective volatility (sigma^2 T / 3), so both sit below vanilla");
122 common::check(
123 "discrete geometric (n=1e5) -> continuous",
124 geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, Some(100_000), PutOrCall::Call),
125 geo,
126 1e-3,
127 );
128
129 common::section("Averaging frequency (geometric, exact)");
130 for n in [4usize, 12, 52, 252] {
131 let price =
132 geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, Some(n), PutOrCall::Call);
133 println!(" {n:>4} fixings: {price:.6}");
134 }
135 println!();
136}Additional examples can be found in:
Sourcepub fn model(self, model: McModel) -> Self
pub fn model(self, model: McModel) -> Self
Examples found in repository?
examples/autocallable_option.rs (line 79)
65fn main() {
66 common::title(&format!(
67 "AUTOCALLABLE NOTE — N={NOTIONAL} autocall={AUTOCALL} protection={PROTECTION} coupon={COUPON}/period, {OBSERVATIONS} observations, T=1y"
68 ));
69 common::note("pays N + m*coupon if S >= autocall barrier at observation m;");
70 common::note("otherwise N at maturity, or N*S_T/S_0 if the protection barrier was breached.");
71
72 common::section("Model comparison");
73 common::table_header();
74 common::row("GBM (flat 30%)", ¬e(AUTOCALL, PROTECTION, COUPON).build());
75 common::row(
76 "Local vol (skewed surface)",
77 ¬e(AUTOCALL, PROTECTION, COUPON)
78 .vol_surface(skewed_surface())
79 .model(McModel::LocalVol)
80 .build(),
81 );
82 common::row(
83 "Heston (vol-of-vol=0.4, rho=-0.7)",
84 ¬e(AUTOCALL, PROTECTION, COUPON)
85 .heston(HestonParams {
86 v0: VOL * VOL,
87 kappa: 2.0,
88 theta: VOL * VOL,
89 vol_of_vol: 0.4,
90 rho: -0.7,
91 })
92 .build(),
93 );
94 common::row(
95 "Analytical (unsupported)",
96 ¬e(AUTOCALL, PROTECTION, COUPON).engine(Engine::BlackScholes).build(),
97 );
98 common::note("skew/stoch-vol raise the knock-in probability, lowering the note value");
99
100 common::section("Structure sensitivity (GBM)");
101 common::table_header();
102 for coupon in [0.0, 3.0, 6.0, 9.0] {
103 common::row(&format!("coupon = {coupon}/period"), ¬e(AUTOCALL, PROTECTION, coupon).build());
104 }
105 for protection in [50.0, 60.0, 70.0, 80.0] {
106 common::row(
107 &format!("protection barrier = {protection}"),
108 ¬e(AUTOCALL, protection, COUPON).build(),
109 );
110 }
111 for autocall in [95.0, 100.0, 105.0, 110.0] {
112 common::row(
113 &format!("autocall barrier = {autocall}"),
114 ¬e(autocall, PROTECTION, COUPON).build(),
115 );
116 }
117
118 common::section("Observation frequency (GBM)");
119 common::table_header();
120 for obs in [1usize, 2, 4, 12] {
121 common::row(
122 &format!("{obs} observations"),
123 &base().autocallable(AUTOCALL, PROTECTION, COUPON, obs, NOTIONAL).build(),
124 );
125 }
126
127 common::section("Degenerate cases (exact identities)");
128 let always_calls = base()
129 .autocallable(1e-9, 50.0, COUPON, OBSERVATIONS, NOTIONAL)
130 .build()
131 .npv();
132 common::check(
133 "barrier at 0 -> called at t1 with 1 coupon",
134 always_calls,
135 (NOTIONAL + COUPON) * (-RATE * 0.25_f64).exp(),
136 1e-8,
137 );
138 let never_calls = base()
139 .autocallable(1e12, 1e-9, COUPON, OBSERVATIONS, NOTIONAL)
140 .build()
141 .npv();
142 common::check(
143 "unreachable barriers -> zero-coupon bond",
144 never_calls,
145 NOTIONAL * (-RATE * 1.0_f64).exp(),
146 1e-8,
147 );
148 let full_downside = base()
149 .autocallable(1e12, 1e12, 0.0, OBSERVATIONS, NOTIONAL)
150 .dividend_yield(0.0)
151 .build()
152 .npv();
153 common::check(
154 "always knocked in, no coupon -> discounted forward",
155 full_downside,
156 NOTIONAL,
157 0.3,
158 );
159 println!();
160}More examples
examples/barrier_option.rs (line 163)
40fn main() {
41 common::title("BARRIER OPTIONS — S=100 K=100 sigma=30% r=5% q=2% T=1y");
42
43 common::section("All eight types, analytic (Reiner-Rubinstein)");
44 common::table_header();
45 for (dir, knock, pc, level) in [
46 (BarrierDirection::Down, KnockType::In, PutOrCall::Call, 90.0),
47 (BarrierDirection::Down, KnockType::Out, PutOrCall::Call, 90.0),
48 (BarrierDirection::Down, KnockType::In, PutOrCall::Put, 90.0),
49 (BarrierDirection::Down, KnockType::Out, PutOrCall::Put, 90.0),
50 (BarrierDirection::Up, KnockType::In, PutOrCall::Call, 120.0),
51 (BarrierDirection::Up, KnockType::Out, PutOrCall::Call, 120.0),
52 (BarrierDirection::Up, KnockType::In, PutOrCall::Put, 120.0),
53 (BarrierDirection::Up, KnockType::Out, PutOrCall::Put, 120.0),
54 ] {
55 common::row(
56 &format!("{dir:?}-and-{knock:?} {pc:?} H={level}"),
57 &base().barrier(pc, dir, knock, level).engine(Engine::BlackScholes).build(),
58 );
59 }
60
61 common::section("Engine comparison: down-and-out call, H=90");
62 common::table_header();
63 for (label, engine) in [
64 ("Analytical (Reiner-Rubinstein)", Engine::BlackScholes),
65 ("Finite difference (absorbing)", Engine::FiniteDifference),
66 ("Monte Carlo (Brownian bridge)", Engine::MonteCarlo),
67 ("Binomial (unsupported)", Engine::Binomial),
68 ] {
69 common::row(
70 label,
71 &base()
72 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 90.0)
73 .engine(engine)
74 .build(),
75 );
76 }
77 common::note("MC applies a bridge crossing correction, so monitoring is effectively continuous");
78
79 common::section("In-out parity: KI + KO = vanilla");
80 let vanilla = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build();
81 for level in [80.0, 90.0, 99.0] {
82 let ki = base()
83 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::In, level)
84 .engine(Engine::BlackScholes)
85 .build();
86 let ko = base()
87 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, level)
88 .engine(Engine::BlackScholes)
89 .build();
90 common::check(
91 &format!("H={level}: KI + KO"),
92 ki.npv() + ko.npv(),
93 vanilla.npv(),
94 1e-10,
95 );
96 }
97
98 common::section("Limits");
99 common::check(
100 "far barrier: KO call -> vanilla",
101 barrier_price(SPOT, STRIKE, 1e-4, RATE, DIV, VOL, 1.0, BarrierDirection::Down, KnockType::Out, PutOrCall::Call),
102 vanilla.npv(),
103 1e-9,
104 );
105 common::check(
106 "up-and-out call with K >= H is worthless",
107 barrier_price(SPOT, 110.0, 105.0, RATE, DIV, VOL, 1.0, BarrierDirection::Up, KnockType::Out, PutOrCall::Call),
108 0.0,
109 1e-12,
110 );
111 common::check(
112 "spot at barrier: KO = 0",
113 base()
114 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, SPOT)
115 .engine(Engine::BlackScholes)
116 .build()
117 .npv(),
118 0.0,
119 1e-12,
120 );
121
122 common::section("Barrier level sweep: down-and-out call");
123 common::table_header();
124 for level in [50.0, 70.0, 85.0, 95.0, 99.0] {
125 common::row(
126 &format!("H={level}"),
127 &base()
128 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, level)
129 .engine(Engine::BlackScholes)
130 .build(),
131 );
132 }
133 common::note("value decreases as the barrier approaches spot; delta can exceed 1 near it");
134
135 common::section("Smile matters: down-and-out call under local vol");
136 let skewed = VolSurface::from_strike_grid(
137 &[Tenor::YearFraction(0.5), Tenor::YearFraction(1.0), Tenor::YearFraction(2.0)],
138 &[70.0, 85.0, 100.0, 115.0, 130.0],
139 &[
140 vec![0.38, 0.34, 0.30, 0.28, 0.27],
141 vec![0.37, 0.34, 0.30, 0.29, 0.28],
142 vec![0.36, 0.33, 0.30, 0.29, 0.28],
143 ],
144 asof(),
145 DayCountConvention::Act365,
146 )
147 .unwrap();
148 common::table_header();
149 common::row(
150 "GBM (flat 30%)",
151 &base()
152 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 90.0)
153 .engine(Engine::MonteCarlo)
154 .paths(50_000)
155 .build(),
156 );
157 common::row(
158 "Local vol (skewed surface)",
159 &base()
160 .vol_surface(skewed)
161 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 90.0)
162 .engine(Engine::MonteCarlo)
163 .model(McModel::LocalVol)
164 .paths(50_000)
165 .build(),
166 );
167 common::note("downside skew raises the knock-out probability, lowering the price");
168 println!();
169}examples/local_vol_calibration.rs (line 115)
34fn main() {
35 common::title("LOCAL VOLATILITY — quotes -> implied surface -> Dupire -> reprice");
36
37 let maturities = [
38 (NaiveDate::from_ymd_opt(2026, 7, 2).unwrap(), 0.23),
39 (NaiveDate::from_ymd_opt(2027, 1, 1).unwrap(), 0.25),
40 ];
41
42 common::section("Step 1: generate market quotes from a known skew");
43 println!(" sigma(K, T) = base(T) - 0.001 * (K - 100)");
44 let mut quotes = Vec::new();
45 for (maturity, base_vol) in maturities {
46 let t = (maturity - asof()).num_days() as f64 / 365.0;
47 for i in 0..13 {
48 let strike = 70.0 + 5.0 * i as f64;
49 let vol = true_vol(strike, base_vol);
50 let price = bs_price(SPOT, strike, RATE, 0.0, vol, t, PutOrCall::Call);
51 let mut option = EquityOptionBuilder::new()
52 .spot(SPOT)
53 .strike(strike)
54 .flat_vol(0.2) // placeholder: the solve does not use it
55 .flat_rate(RATE)
56 .valuation_date(asof())
57 .maturity_date(maturity)
58 .vanilla(PutOrCall::Call)
59 .build();
60 option.base.current_price = Quote::new(price);
61 quotes.push(Box::new(option));
62 }
63 }
64 println!(" {} quotes across {} expiries", quotes.len(), maturities.len());
65
66 common::section("Step 2: back out implied vols and build the surface");
67 let surface = build_implied_vol_surface("es).expect("calibration failed");
68 println!("{surface}");
69
70 common::section("Step 3: check the surface recovers the input smile");
71 for (t, base_vol) in [(182.0 / 365.0, 0.23), (1.0, 0.25)] {
72 for strike in [70.0, 85.0, 100.0, 115.0, 130.0] {
73 let recovered = surface.vol(strike, SPOT, t);
74 common::check(
75 &format!("T={t:.3} K={strike}"),
76 recovered,
77 true_vol(strike, base_vol),
78 1e-6,
79 );
80 }
81 }
82
83 common::section("Step 4: Dupire local volatility from that surface");
84 let curve =
85 YieldCurve::flat(RATE, asof(), DayCountConvention::Act365, Compounding::Continuous).unwrap();
86 let lv = LocalVol::new(&surface, &curve, SPOT, 0.0, 0.0);
87 println!(" {:>8} {:>12} {:>12} {:>12}", "level", "t=0.25", "t=0.50", "t=1.00");
88 for level in [70.0, 85.0, 100.0, 115.0, 130.0] {
89 println!(
90 " {level:>8.1} {:>12.4} {:>12.4} {:>12.4}",
91 lv.vol(level, 0.25),
92 lv.vol(level, 0.50),
93 lv.vol(level, 1.00)
94 );
95 }
96 common::note("local vol is steeper in strike than implied vol (the 'twice the slope' rule)");
97 common::note("the far wings are noisy: Dupire takes numerical derivatives of a");
98 common::note("piecewise-linear surface with flat extrapolation — trust the interior.");
99
100 common::section("Step 5: reprice the calibrating vanillas through local vol MC");
101 common::table_header();
102 for strike in [90.0, 100.0, 110.0] {
103 let expected = bs_price(SPOT, strike, RATE, 0.0, true_vol(strike, 0.25), 1.0, PutOrCall::Call);
104 common::row(
105 &format!("local vol MC, K={strike}"),
106 &EquityOptionBuilder::new()
107 .spot(SPOT)
108 .strike(strike)
109 .vol_surface(surface.clone())
110 .flat_rate(RATE)
111 .valuation_date(asof())
112 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
113 .vanilla(PutOrCall::Call)
114 .engine(Engine::MonteCarlo)
115 .model(McModel::LocalVol)
116 .paths(50_000)
117 .build(),
118 );
119 println!("{:<34} {expected:>12.6} <- Black-Scholes target at the quoted smile vol", "");
120 }
121
122 common::section("Local vol on the finite difference engine (no sampling noise)");
123 common::table_header();
124 for strike in [90.0, 100.0, 110.0] {
125 common::row(
126 &format!("local vol FD, K={strike}"),
127 &EquityOptionBuilder::new()
128 .spot(SPOT)
129 .strike(strike)
130 .vol_surface(surface.clone())
131 .flat_rate(RATE)
132 .valuation_date(asof())
133 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
134 .vanilla(PutOrCall::Call)
135 .engine(Engine::FiniteDifference)
136 .model(McModel::LocalVol)
137 .build(),
138 );
139 }
140
141 common::section("Sanity: a flat surface must give flat local vol");
142 let flat = VolSurface::flat(0.25, asof(), DayCountConvention::Act365).unwrap();
143 let flat_lv = LocalVol::new(&flat, &curve, SPOT, 0.0, 0.0);
144 for (level, t) in [(70.0, 0.25), (100.0, 1.0), (130.0, 2.0)] {
145 common::check(&format!("sigma_loc({level}, {t})"), flat_lv.vol(level, t), 0.25, 1e-6);
146 }
147
148 common::section("Term structure: local vol is the forward variance");
149 let term = VolSurface::from_strike_smiles(
150 &[Tenor::YearFraction(0.5), Tenor::YearFraction(1.0)],
151 &[vec![(100.0, 0.20)], vec![(100.0, 0.25)]],
152 asof(),
153 DayCountConvention::Act365,
154 )
155 .unwrap();
156 let term_lv = LocalVol::new(&term, &curve, SPOT, 0.0, 0.0);
157 // (0.25^2 * 1 - 0.20^2 * 0.5) / 0.5 = 0.085
158 common::check(
159 "sigma_loc between pillars = sqrt(fwd variance)",
160 term_lv.vol(100.0, 0.75),
161 0.085_f64.sqrt(),
162 1e-3,
163 );
164 println!();
165}Sourcepub fn heston(self, params: HestonParams) -> Self
pub fn heston(self, params: HestonParams) -> Self
Examples found in repository?
examples/heston_option.rs (line 37)
27fn base() -> EquityOptionBuilder {
28 EquityOptionBuilder::new()
29 .symbol("HESTON")
30 .spot(SPOT)
31 .strike(STRIKE)
32 .flat_vol(0.30) // only used as the vega-bump reference
33 .flat_rate(RATE)
34 .dividend_yield(DIV)
35 .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
36 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
37 .heston(params())
38}More examples
examples/autocallable_option.rs (lines 85-91)
65fn main() {
66 common::title(&format!(
67 "AUTOCALLABLE NOTE — N={NOTIONAL} autocall={AUTOCALL} protection={PROTECTION} coupon={COUPON}/period, {OBSERVATIONS} observations, T=1y"
68 ));
69 common::note("pays N + m*coupon if S >= autocall barrier at observation m;");
70 common::note("otherwise N at maturity, or N*S_T/S_0 if the protection barrier was breached.");
71
72 common::section("Model comparison");
73 common::table_header();
74 common::row("GBM (flat 30%)", ¬e(AUTOCALL, PROTECTION, COUPON).build());
75 common::row(
76 "Local vol (skewed surface)",
77 ¬e(AUTOCALL, PROTECTION, COUPON)
78 .vol_surface(skewed_surface())
79 .model(McModel::LocalVol)
80 .build(),
81 );
82 common::row(
83 "Heston (vol-of-vol=0.4, rho=-0.7)",
84 ¬e(AUTOCALL, PROTECTION, COUPON)
85 .heston(HestonParams {
86 v0: VOL * VOL,
87 kappa: 2.0,
88 theta: VOL * VOL,
89 vol_of_vol: 0.4,
90 rho: -0.7,
91 })
92 .build(),
93 );
94 common::row(
95 "Analytical (unsupported)",
96 ¬e(AUTOCALL, PROTECTION, COUPON).engine(Engine::BlackScholes).build(),
97 );
98 common::note("skew/stoch-vol raise the knock-in probability, lowering the note value");
99
100 common::section("Structure sensitivity (GBM)");
101 common::table_header();
102 for coupon in [0.0, 3.0, 6.0, 9.0] {
103 common::row(&format!("coupon = {coupon}/period"), ¬e(AUTOCALL, PROTECTION, coupon).build());
104 }
105 for protection in [50.0, 60.0, 70.0, 80.0] {
106 common::row(
107 &format!("protection barrier = {protection}"),
108 ¬e(AUTOCALL, protection, COUPON).build(),
109 );
110 }
111 for autocall in [95.0, 100.0, 105.0, 110.0] {
112 common::row(
113 &format!("autocall barrier = {autocall}"),
114 ¬e(autocall, PROTECTION, COUPON).build(),
115 );
116 }
117
118 common::section("Observation frequency (GBM)");
119 common::table_header();
120 for obs in [1usize, 2, 4, 12] {
121 common::row(
122 &format!("{obs} observations"),
123 &base().autocallable(AUTOCALL, PROTECTION, COUPON, obs, NOTIONAL).build(),
124 );
125 }
126
127 common::section("Degenerate cases (exact identities)");
128 let always_calls = base()
129 .autocallable(1e-9, 50.0, COUPON, OBSERVATIONS, NOTIONAL)
130 .build()
131 .npv();
132 common::check(
133 "barrier at 0 -> called at t1 with 1 coupon",
134 always_calls,
135 (NOTIONAL + COUPON) * (-RATE * 0.25_f64).exp(),
136 1e-8,
137 );
138 let never_calls = base()
139 .autocallable(1e12, 1e-9, COUPON, OBSERVATIONS, NOTIONAL)
140 .build()
141 .npv();
142 common::check(
143 "unreachable barriers -> zero-coupon bond",
144 never_calls,
145 NOTIONAL * (-RATE * 1.0_f64).exp(),
146 1e-8,
147 );
148 let full_downside = base()
149 .autocallable(1e12, 1e12, 0.0, OBSERVATIONS, NOTIONAL)
150 .dividend_yield(0.0)
151 .build()
152 .npv();
153 common::check(
154 "always knocked in, no coupon -> discounted forward",
155 full_downside,
156 NOTIONAL,
157 0.3,
158 );
159 println!();
160}examples/forward_start_option.rs (line 79)
39fn main() {
40 common::title("FORWARD-START OPTION — S=100, strike = 1.0 x S(0.5y), T=1y, sigma=30%");
41
42 common::section("Black-Scholes: analytic vs Monte Carlo");
43 common::table_header();
44 common::row(
45 "Analytical (Rubinstein)",
46 &base()
47 .forward_start(PutOrCall::Call, 1.0, START)
48 .engine(Engine::BlackScholes)
49 .build(),
50 );
51 common::row(
52 "Monte Carlo (GBM)",
53 &base()
54 .forward_start(PutOrCall::Call, 1.0, START)
55 .engine(Engine::MonteCarlo)
56 .paths(100_000)
57 .build(),
58 );
59 common::row(
60 "Finite difference (unsupported)",
61 &base()
62 .forward_start(PutOrCall::Call, 1.0, START)
63 .engine(Engine::FiniteDifference)
64 .build(),
65 );
66
67 common::section("Forward smile: Heston vs Black-Scholes");
68 common::table_header();
69 let bs = base()
70 .forward_start(PutOrCall::Call, 1.0, START)
71 .engine(Engine::BlackScholes)
72 .build()
73 .npv();
74 common::row(
75 "Heston vol-of-vol=0.001 (-> BS)",
76 &base()
77 .forward_start(PutOrCall::Call, 1.0, START)
78 .engine(Engine::MonteCarlo)
79 .heston(heston_params(1e-3, 0.0))
80 .paths(50_000)
81 .build(),
82 );
83 for (vov, rho) in [(0.2, -0.7), (0.4, -0.7), (0.6, -0.7), (0.4, 0.0)] {
84 common::row(
85 &format!("Heston vol-of-vol={vov}, rho={rho}"),
86 &base()
87 .forward_start(PutOrCall::Call, 1.0, START)
88 .engine(Engine::MonteCarlo)
89 .heston(heston_params(vov, rho))
90 .paths(50_000)
91 .build(),
92 );
93 }
94 common::note(&format!("Black-Scholes reference: {bs:.6}"));
95 common::note("the gap is the forward-smile effect — the reason to price these on a stoch-vol model");
96
97 common::section("Strike fraction sweep (analytic)");
98 common::table_header();
99 for k in [0.9, 0.95, 1.0, 1.05, 1.1] {
100 common::row(
101 &format!("strike = {k} x S(t_f), call"),
102 &base().forward_start(PutOrCall::Call, k, START).engine(Engine::BlackScholes).build(),
103 );
104 }
105
106 common::section("Fixing date sweep (analytic, ATM)");
107 common::table_header();
108 for start in [0.1, 0.25, 0.5, 0.75, 0.9] {
109 common::row(
110 &format!("fixing at {:.0}% of life", start * 100.0),
111 &base().forward_start(PutOrCall::Call, 1.0, start).engine(Engine::BlackScholes).build(),
112 );
113 }
114 common::note("later fixing leaves less time to expiry, so the option is worth less");
115
116 common::section("Identities");
117 common::check(
118 "immediate fixing -> vanilla struck at S0",
119 forward_start_price(SPOT, 1.0, RATE, DIV, VOL, 1e-6, 1.0, PutOrCall::Call),
120 base()
121 .strike(SPOT)
122 .vanilla(PutOrCall::Call)
123 .engine(Engine::BlackScholes)
124 .build()
125 .npv(),
126 1e-3,
127 );
128 let p100 = forward_start_price(100.0, 1.0, RATE, DIV, VOL, 0.5, 1.0, PutOrCall::Call);
129 let p200 = forward_start_price(200.0, 1.0, RATE, DIV, VOL, 0.5, 1.0, PutOrCall::Call);
130 common::check("homogeneity: price(2S) = 2 price(S)", p200, 2.0 * p100, 1e-12);
131 let fs = base()
132 .forward_start(PutOrCall::Call, 1.0, START)
133 .engine(Engine::BlackScholes)
134 .build();
135 common::check("delta = price / spot (homogeneity)", fs.delta(), fs.npv() / SPOT, 1e-6);
136 println!();
137}Sourcepub fn mc_config(self, cfg: MonteCarloConfig) -> Self
pub fn mc_config(self, cfg: MonteCarloConfig) -> Self
Examples found in repository?
examples/asian_option.rs (lines 82-89)
35fn main() {
36 common::title("ASIAN OPTIONS — S=100 K=100 sigma=30% r=5% q=2% T=1y");
37
38 common::section("Fixed strike (average price) call");
39 common::table_header();
40 common::row(
41 "Geometric, analytic (exact)",
42 &base()
43 .asian(PutOrCall::Call, AveragingType::Geometric, AsianStrikeType::FixedStrike)
44 .engine(Engine::BlackScholes)
45 .build(),
46 );
47 common::row(
48 "Geometric, Monte Carlo",
49 &base()
50 .asian(PutOrCall::Call, AveragingType::Geometric, AsianStrikeType::FixedStrike)
51 .engine(Engine::MonteCarlo)
52 .paths(50_000)
53 .build(),
54 );
55 common::row(
56 "Arithmetic, Turnbull-Wakeman",
57 &base()
58 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
59 .engine(Engine::BlackScholes)
60 .build(),
61 );
62 common::row(
63 "Arithmetic, MC + geometric CV",
64 &base()
65 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
66 .engine(Engine::MonteCarlo)
67 .paths(50_000)
68 .build(),
69 );
70
71 common::section("Control variate effect (same path count)");
72 common::table_header();
73 let with_cv = base()
74 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
75 .engine(Engine::MonteCarlo)
76 .paths(20_000)
77 .build();
78 let without_cv = base()
79 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
80 .engine(Engine::MonteCarlo)
81 .paths(20_000)
82 .mc_config({
83 // Euler stepping disables the control variate precondition
84 let mut c = rustyqlib::equity::montecarlo::MonteCarloConfig::default();
85 c.paths = 20_000;
86 c.scheme = DiscretizationScheme::Euler;
87 c.time_steps = 100;
88 c
89 })
90 .build();
91 common::row("with geometric control variate", &with_cv);
92 common::row("without (Euler path route)", &without_cv);
93 common::note("compare the std err column: the CV collapses the variance");
94
95 common::section("Floating strike (average strike)");
96 common::table_header();
97 for pc in [PutOrCall::Call, PutOrCall::Put] {
98 common::row(
99 &format!("Monte Carlo, {pc:?}"),
100 &base()
101 .asian(pc, AveragingType::Arithmetic, AsianStrikeType::FloatingStrike)
102 .engine(Engine::MonteCarlo)
103 .paths(50_000)
104 .build(),
105 );
106 common::row(
107 &format!("Analytic (unsupported), {pc:?}"),
108 &base()
109 .asian(pc, AveragingType::Arithmetic, AsianStrikeType::FloatingStrike)
110 .engine(Engine::BlackScholes)
111 .build(),
112 );
113 }
114
115 common::section("Orderings and limits");
116 let vanilla = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build().npv();
117 let geo = geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, None, PutOrCall::Call);
118 let arith = turnbull_wakeman_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, PutOrCall::Call);
119 println!(" geometric {geo:.6} < arithmetic {arith:.6} < vanilla {vanilla:.6}");
120 common::note("AM-GM: the arithmetic average dominates the geometric one");
121 common::note("averaging reduces effective volatility (sigma^2 T / 3), so both sit below vanilla");
122 common::check(
123 "discrete geometric (n=1e5) -> continuous",
124 geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, Some(100_000), PutOrCall::Call),
125 geo,
126 1e-3,
127 );
128
129 common::section("Averaging frequency (geometric, exact)");
130 for n in [4usize, 12, 52, 252] {
131 let price =
132 geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, Some(n), PutOrCall::Call);
133 println!(" {n:>4} fixings: {price:.6}");
134 }
135 println!();
136}Sourcepub fn paths(self, paths: usize) -> Self
pub fn paths(self, paths: usize) -> Self
Examples found in repository?
examples/autocallable_option.rs (line 42)
32fn base() -> EquityOptionBuilder {
33 EquityOptionBuilder::new()
34 .symbol("ATHENA")
35 .spot(SPOT)
36 .flat_vol(VOL)
37 .flat_rate(RATE)
38 .dividend_yield(DIV)
39 .valuation_date(asof())
40 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
41 .engine(Engine::MonteCarlo)
42 .paths(50_000)
43}More examples
examples/forward_start_option.rs (line 56)
39fn main() {
40 common::title("FORWARD-START OPTION — S=100, strike = 1.0 x S(0.5y), T=1y, sigma=30%");
41
42 common::section("Black-Scholes: analytic vs Monte Carlo");
43 common::table_header();
44 common::row(
45 "Analytical (Rubinstein)",
46 &base()
47 .forward_start(PutOrCall::Call, 1.0, START)
48 .engine(Engine::BlackScholes)
49 .build(),
50 );
51 common::row(
52 "Monte Carlo (GBM)",
53 &base()
54 .forward_start(PutOrCall::Call, 1.0, START)
55 .engine(Engine::MonteCarlo)
56 .paths(100_000)
57 .build(),
58 );
59 common::row(
60 "Finite difference (unsupported)",
61 &base()
62 .forward_start(PutOrCall::Call, 1.0, START)
63 .engine(Engine::FiniteDifference)
64 .build(),
65 );
66
67 common::section("Forward smile: Heston vs Black-Scholes");
68 common::table_header();
69 let bs = base()
70 .forward_start(PutOrCall::Call, 1.0, START)
71 .engine(Engine::BlackScholes)
72 .build()
73 .npv();
74 common::row(
75 "Heston vol-of-vol=0.001 (-> BS)",
76 &base()
77 .forward_start(PutOrCall::Call, 1.0, START)
78 .engine(Engine::MonteCarlo)
79 .heston(heston_params(1e-3, 0.0))
80 .paths(50_000)
81 .build(),
82 );
83 for (vov, rho) in [(0.2, -0.7), (0.4, -0.7), (0.6, -0.7), (0.4, 0.0)] {
84 common::row(
85 &format!("Heston vol-of-vol={vov}, rho={rho}"),
86 &base()
87 .forward_start(PutOrCall::Call, 1.0, START)
88 .engine(Engine::MonteCarlo)
89 .heston(heston_params(vov, rho))
90 .paths(50_000)
91 .build(),
92 );
93 }
94 common::note(&format!("Black-Scholes reference: {bs:.6}"));
95 common::note("the gap is the forward-smile effect — the reason to price these on a stoch-vol model");
96
97 common::section("Strike fraction sweep (analytic)");
98 common::table_header();
99 for k in [0.9, 0.95, 1.0, 1.05, 1.1] {
100 common::row(
101 &format!("strike = {k} x S(t_f), call"),
102 &base().forward_start(PutOrCall::Call, k, START).engine(Engine::BlackScholes).build(),
103 );
104 }
105
106 common::section("Fixing date sweep (analytic, ATM)");
107 common::table_header();
108 for start in [0.1, 0.25, 0.5, 0.75, 0.9] {
109 common::row(
110 &format!("fixing at {:.0}% of life", start * 100.0),
111 &base().forward_start(PutOrCall::Call, 1.0, start).engine(Engine::BlackScholes).build(),
112 );
113 }
114 common::note("later fixing leaves less time to expiry, so the option is worth less");
115
116 common::section("Identities");
117 common::check(
118 "immediate fixing -> vanilla struck at S0",
119 forward_start_price(SPOT, 1.0, RATE, DIV, VOL, 1e-6, 1.0, PutOrCall::Call),
120 base()
121 .strike(SPOT)
122 .vanilla(PutOrCall::Call)
123 .engine(Engine::BlackScholes)
124 .build()
125 .npv(),
126 1e-3,
127 );
128 let p100 = forward_start_price(100.0, 1.0, RATE, DIV, VOL, 0.5, 1.0, PutOrCall::Call);
129 let p200 = forward_start_price(200.0, 1.0, RATE, DIV, VOL, 0.5, 1.0, PutOrCall::Call);
130 common::check("homogeneity: price(2S) = 2 price(S)", p200, 2.0 * p100, 1e-12);
131 let fs = base()
132 .forward_start(PutOrCall::Call, 1.0, START)
133 .engine(Engine::BlackScholes)
134 .build();
135 common::check("delta = price / spot (homogeneity)", fs.delta(), fs.npv() / SPOT, 1e-6);
136 println!();
137}examples/asian_option.rs (line 52)
35fn main() {
36 common::title("ASIAN OPTIONS — S=100 K=100 sigma=30% r=5% q=2% T=1y");
37
38 common::section("Fixed strike (average price) call");
39 common::table_header();
40 common::row(
41 "Geometric, analytic (exact)",
42 &base()
43 .asian(PutOrCall::Call, AveragingType::Geometric, AsianStrikeType::FixedStrike)
44 .engine(Engine::BlackScholes)
45 .build(),
46 );
47 common::row(
48 "Geometric, Monte Carlo",
49 &base()
50 .asian(PutOrCall::Call, AveragingType::Geometric, AsianStrikeType::FixedStrike)
51 .engine(Engine::MonteCarlo)
52 .paths(50_000)
53 .build(),
54 );
55 common::row(
56 "Arithmetic, Turnbull-Wakeman",
57 &base()
58 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
59 .engine(Engine::BlackScholes)
60 .build(),
61 );
62 common::row(
63 "Arithmetic, MC + geometric CV",
64 &base()
65 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
66 .engine(Engine::MonteCarlo)
67 .paths(50_000)
68 .build(),
69 );
70
71 common::section("Control variate effect (same path count)");
72 common::table_header();
73 let with_cv = base()
74 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
75 .engine(Engine::MonteCarlo)
76 .paths(20_000)
77 .build();
78 let without_cv = base()
79 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
80 .engine(Engine::MonteCarlo)
81 .paths(20_000)
82 .mc_config({
83 // Euler stepping disables the control variate precondition
84 let mut c = rustyqlib::equity::montecarlo::MonteCarloConfig::default();
85 c.paths = 20_000;
86 c.scheme = DiscretizationScheme::Euler;
87 c.time_steps = 100;
88 c
89 })
90 .build();
91 common::row("with geometric control variate", &with_cv);
92 common::row("without (Euler path route)", &without_cv);
93 common::note("compare the std err column: the CV collapses the variance");
94
95 common::section("Floating strike (average strike)");
96 common::table_header();
97 for pc in [PutOrCall::Call, PutOrCall::Put] {
98 common::row(
99 &format!("Monte Carlo, {pc:?}"),
100 &base()
101 .asian(pc, AveragingType::Arithmetic, AsianStrikeType::FloatingStrike)
102 .engine(Engine::MonteCarlo)
103 .paths(50_000)
104 .build(),
105 );
106 common::row(
107 &format!("Analytic (unsupported), {pc:?}"),
108 &base()
109 .asian(pc, AveragingType::Arithmetic, AsianStrikeType::FloatingStrike)
110 .engine(Engine::BlackScholes)
111 .build(),
112 );
113 }
114
115 common::section("Orderings and limits");
116 let vanilla = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build().npv();
117 let geo = geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, None, PutOrCall::Call);
118 let arith = turnbull_wakeman_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, PutOrCall::Call);
119 println!(" geometric {geo:.6} < arithmetic {arith:.6} < vanilla {vanilla:.6}");
120 common::note("AM-GM: the arithmetic average dominates the geometric one");
121 common::note("averaging reduces effective volatility (sigma^2 T / 3), so both sit below vanilla");
122 common::check(
123 "discrete geometric (n=1e5) -> continuous",
124 geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, Some(100_000), PutOrCall::Call),
125 geo,
126 1e-3,
127 );
128
129 common::section("Averaging frequency (geometric, exact)");
130 for n in [4usize, 12, 52, 252] {
131 let price =
132 geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, Some(n), PutOrCall::Call);
133 println!(" {n:>4} fixings: {price:.6}");
134 }
135 println!();
136}examples/dividends_and_borrow.rs (line 100)
39fn main() {
40 common::title("DIVIDENDS AND BORROW COST — S=100 K=100 sigma=30% r=5% T=1y");
41
42 common::section("Continuous carry: dividend yield and borrow cost are interchangeable");
43 common::table_header();
44 common::row("no carry", &base().vanilla(PutOrCall::Call).build());
45 common::row("q = 4%", &base().dividend_yield(0.04).vanilla(PutOrCall::Call).build());
46 common::row("borrow = 4%", &base().borrow_cost(0.04).vanilla(PutOrCall::Call).build());
47 common::row(
48 "q = 1% + borrow = 3%",
49 &base().dividend_yield(0.01).borrow_cost(0.03).vanilla(PutOrCall::Call).build(),
50 );
51 common::note("carry_yield() = dividend_yield + borrow_cost enters every formula as 'q'");
52
53 let q_only = base().dividend_yield(0.04).vanilla(PutOrCall::Call).build();
54 let split = base().dividend_yield(0.01).borrow_cost(0.03).vanilla(PutOrCall::Call).build();
55 common::check("q=4% vs q=1%+b=3%", split.npv(), q_only.npv(), 1e-12);
56
57 common::section("Hard-to-borrow names: high borrow cost lowers the forward");
58 common::table_header();
59 for b in [0.0, 0.02, 0.05, 0.15] {
60 let option = base().borrow_cost(b).vanilla(PutOrCall::Call).build();
61 common::row(&format!("borrow = {:.0}%", b * 100.0), &option);
62 }
63 let hard = base().borrow_cost(0.15).vanilla(PutOrCall::Call).build();
64 println!(
65 " forward with 15% borrow: {:.4} (vs spot {SPOT})",
66 hard.base.forward_price()
67 );
68
69 common::section("Discrete cash dividends: 2 x 1.50 over the year");
70 let with_divs = |b: EquityOptionBuilder| {
71 b.cash_dividend(NaiveDate::from_ymd_opt(2026, 4, 1).unwrap(), 1.5)
72 .cash_dividend(NaiveDate::from_ymd_opt(2026, 10, 1).unwrap(), 1.5)
73 };
74 let analytic = with_divs(base()).vanilla(PutOrCall::Call).build();
75 println!(
76 " spot {SPOT} - PV(dividends) {:.6} = escrowed spot {:.6}",
77 analytic.base.pv_cash_dividends(),
78 analytic.base.effective_spot()
79 );
80 common::table_header();
81 common::row("Analytical (escrowed model)", &analytic);
82 common::row(
83 "Binomial (escrowed)",
84 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::Binomial).build(),
85 );
86 common::row(
87 "Finite difference (jump model)",
88 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::FiniteDifference).build(),
89 );
90 common::row(
91 "Monte Carlo terminal (escrowed)",
92 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::MonteCarlo).build(),
93 );
94 common::row(
95 "Monte Carlo path-wise (jump model)",
96 &with_divs(base())
97 .vanilla(PutOrCall::Call)
98 .engine(Engine::MonteCarlo)
99 .mc_time_steps(200)
100 .paths(50_000)
101 .build(),
102 );
103 common::note("escrowed: lognormal on S - PV(divs); jump: dividends subtracted at each ex-date");
104 common::note("the two models differ slightly by construction — that gap is expected, not a bug");
105
106 common::check(
107 "escrowed analytic == BS on the escrowed spot",
108 analytic.npv(),
109 bs_price(analytic.base.effective_spot(), STRIKE, RATE, 0.0, VOL, 1.0, PutOrCall::Call),
110 1e-10,
111 );
112
113 common::section("Where the jump model matters: American exercise and barriers");
114 common::table_header();
115 common::row(
116 "American put, FD (jumps)",
117 &with_divs(base())
118 .american()
119 .vanilla(PutOrCall::Put)
120 .engine(Engine::FiniteDifference)
121 .build(),
122 );
123 common::row(
124 "American put, no dividends",
125 &base().american().vanilla(PutOrCall::Put).engine(Engine::FiniteDifference).build(),
126 );
127 common::row(
128 "Down-and-out call H=85, MC (jumps)",
129 &with_divs(base())
130 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
131 .engine(Engine::MonteCarlo)
132 .paths(50_000)
133 .build(),
134 );
135 common::row(
136 "Down-and-out call H=85, no dividends",
137 &base()
138 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
139 .engine(Engine::MonteCarlo)
140 .paths(50_000)
141 .build(),
142 );
143 common::note("dividend drops push the path toward a down barrier and change exercise timing");
144
145 common::section("Put-call parity with full carry");
146 let call = with_divs(base()).borrow_cost(0.02).vanilla(PutOrCall::Call).build();
147 let put = with_divs(base()).borrow_cost(0.02).vanilla(PutOrCall::Put).build();
148 let parity = call.base.effective_spot() * (-call.base.carry_yield() * 1.0_f64).exp()
149 - STRIKE * (-RATE * 1.0_f64).exp();
150 common::check("C - P = S_eff e^{-(q+b)T} - K e^{-rT}", call.npv() - put.npv(), parity, 1e-10);
151 println!();
152}examples/barrier_option.rs (line 154)
40fn main() {
41 common::title("BARRIER OPTIONS — S=100 K=100 sigma=30% r=5% q=2% T=1y");
42
43 common::section("All eight types, analytic (Reiner-Rubinstein)");
44 common::table_header();
45 for (dir, knock, pc, level) in [
46 (BarrierDirection::Down, KnockType::In, PutOrCall::Call, 90.0),
47 (BarrierDirection::Down, KnockType::Out, PutOrCall::Call, 90.0),
48 (BarrierDirection::Down, KnockType::In, PutOrCall::Put, 90.0),
49 (BarrierDirection::Down, KnockType::Out, PutOrCall::Put, 90.0),
50 (BarrierDirection::Up, KnockType::In, PutOrCall::Call, 120.0),
51 (BarrierDirection::Up, KnockType::Out, PutOrCall::Call, 120.0),
52 (BarrierDirection::Up, KnockType::In, PutOrCall::Put, 120.0),
53 (BarrierDirection::Up, KnockType::Out, PutOrCall::Put, 120.0),
54 ] {
55 common::row(
56 &format!("{dir:?}-and-{knock:?} {pc:?} H={level}"),
57 &base().barrier(pc, dir, knock, level).engine(Engine::BlackScholes).build(),
58 );
59 }
60
61 common::section("Engine comparison: down-and-out call, H=90");
62 common::table_header();
63 for (label, engine) in [
64 ("Analytical (Reiner-Rubinstein)", Engine::BlackScholes),
65 ("Finite difference (absorbing)", Engine::FiniteDifference),
66 ("Monte Carlo (Brownian bridge)", Engine::MonteCarlo),
67 ("Binomial (unsupported)", Engine::Binomial),
68 ] {
69 common::row(
70 label,
71 &base()
72 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 90.0)
73 .engine(engine)
74 .build(),
75 );
76 }
77 common::note("MC applies a bridge crossing correction, so monitoring is effectively continuous");
78
79 common::section("In-out parity: KI + KO = vanilla");
80 let vanilla = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build();
81 for level in [80.0, 90.0, 99.0] {
82 let ki = base()
83 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::In, level)
84 .engine(Engine::BlackScholes)
85 .build();
86 let ko = base()
87 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, level)
88 .engine(Engine::BlackScholes)
89 .build();
90 common::check(
91 &format!("H={level}: KI + KO"),
92 ki.npv() + ko.npv(),
93 vanilla.npv(),
94 1e-10,
95 );
96 }
97
98 common::section("Limits");
99 common::check(
100 "far barrier: KO call -> vanilla",
101 barrier_price(SPOT, STRIKE, 1e-4, RATE, DIV, VOL, 1.0, BarrierDirection::Down, KnockType::Out, PutOrCall::Call),
102 vanilla.npv(),
103 1e-9,
104 );
105 common::check(
106 "up-and-out call with K >= H is worthless",
107 barrier_price(SPOT, 110.0, 105.0, RATE, DIV, VOL, 1.0, BarrierDirection::Up, KnockType::Out, PutOrCall::Call),
108 0.0,
109 1e-12,
110 );
111 common::check(
112 "spot at barrier: KO = 0",
113 base()
114 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, SPOT)
115 .engine(Engine::BlackScholes)
116 .build()
117 .npv(),
118 0.0,
119 1e-12,
120 );
121
122 common::section("Barrier level sweep: down-and-out call");
123 common::table_header();
124 for level in [50.0, 70.0, 85.0, 95.0, 99.0] {
125 common::row(
126 &format!("H={level}"),
127 &base()
128 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, level)
129 .engine(Engine::BlackScholes)
130 .build(),
131 );
132 }
133 common::note("value decreases as the barrier approaches spot; delta can exceed 1 near it");
134
135 common::section("Smile matters: down-and-out call under local vol");
136 let skewed = VolSurface::from_strike_grid(
137 &[Tenor::YearFraction(0.5), Tenor::YearFraction(1.0), Tenor::YearFraction(2.0)],
138 &[70.0, 85.0, 100.0, 115.0, 130.0],
139 &[
140 vec![0.38, 0.34, 0.30, 0.28, 0.27],
141 vec![0.37, 0.34, 0.30, 0.29, 0.28],
142 vec![0.36, 0.33, 0.30, 0.29, 0.28],
143 ],
144 asof(),
145 DayCountConvention::Act365,
146 )
147 .unwrap();
148 common::table_header();
149 common::row(
150 "GBM (flat 30%)",
151 &base()
152 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 90.0)
153 .engine(Engine::MonteCarlo)
154 .paths(50_000)
155 .build(),
156 );
157 common::row(
158 "Local vol (skewed surface)",
159 &base()
160 .vol_surface(skewed)
161 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 90.0)
162 .engine(Engine::MonteCarlo)
163 .model(McModel::LocalVol)
164 .paths(50_000)
165 .build(),
166 );
167 common::note("downside skew raises the knock-out probability, lowering the price");
168 println!();
169}examples/heston_option.rs (line 60)
40fn main() {
41 let p = params();
42 common::title(&format!(
43 "HESTON — v0={} kappa={} theta={} vol-of-vol={} rho={}",
44 p.v0, p.kappa, p.theta, p.vol_of_vol, p.rho
45 ));
46 common::note(&format!(
47 "Feller condition 2*kappa*theta >= vol_of_vol^2: {}",
48 if p.feller_condition_holds() { "holds" } else { "VIOLATED (variance can touch zero)" }
49 ));
50
51 common::section("Vanilla: semi-analytic vs Monte Carlo");
52 common::table_header();
53 for pc in [PutOrCall::Call, PutOrCall::Put] {
54 common::row(
55 &format!("Analytical (char. function), {pc:?}"),
56 &base().vanilla(pc).engine(Engine::BlackScholes).build(),
57 );
58 common::row(
59 &format!("Monte Carlo (full-trunc Euler), {pc:?}"),
60 &base().vanilla(pc).engine(Engine::MonteCarlo).paths(100_000).build(),
61 );
62 }
63 common::row(
64 "Finite difference (unsupported)",
65 &base().vanilla(PutOrCall::Call).engine(Engine::FiniteDifference).build(),
66 );
67 common::note("MC vega/theta bump sqrt(v0) and sqrt(theta) in parallel");
68
69 common::section("Binaries under Heston");
70 common::table_header();
71 common::row(
72 "Cash-or-nothing call (analytic)",
73 &base()
74 .binary(PutOrCall::Call, BinaryType::CashOrNothing, 1.0)
75 .engine(Engine::BlackScholes)
76 .build(),
77 );
78 common::row(
79 "Cash-or-nothing call (MC)",
80 &base()
81 .binary(PutOrCall::Call, BinaryType::CashOrNothing, 1.0)
82 .engine(Engine::MonteCarlo)
83 .paths(100_000)
84 .build(),
85 );
86 common::row(
87 "Asset-or-nothing call (analytic)",
88 &base()
89 .binary(PutOrCall::Call, BinaryType::AssetOrNothing, 0.0)
90 .engine(Engine::BlackScholes)
91 .build(),
92 );
93
94 common::section("Path-dependent payoffs (Monte Carlo only)");
95 common::table_header();
96 common::row(
97 "Down-and-out call H=85",
98 &base()
99 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
100 .engine(Engine::MonteCarlo)
101 .paths(50_000)
102 .build(),
103 );
104 common::row(
105 "Down-and-in put H=85",
106 &base()
107 .barrier(PutOrCall::Put, BarrierDirection::Down, KnockType::In, 85.0)
108 .engine(Engine::MonteCarlo)
109 .paths(50_000)
110 .build(),
111 );
112
113 common::section("Identities");
114 let call = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build();
115 let put = base().vanilla(PutOrCall::Put).engine(Engine::BlackScholes).build();
116 let parity = SPOT * (-DIV * 1.0_f64).exp() - STRIKE * (-RATE * 1.0_f64).exp();
117 common::check("put-call parity", call.npv() - put.npv(), parity, 1e-10);
118 let asset = base()
119 .binary(PutOrCall::Call, BinaryType::AssetOrNothing, 0.0)
120 .engine(Engine::BlackScholes)
121 .build();
122 let k_cash = base()
123 .binary(PutOrCall::Call, BinaryType::CashOrNothing, STRIKE)
124 .engine(Engine::BlackScholes)
125 .build();
126 common::check("vanilla = asset digital - K cash digitals", call.npv(), asset.npv() - k_cash.npv(), 1e-10);
127 common::check(
128 "vol-of-vol -> 0 degenerates to Black-Scholes",
129 heston_price(
130 SPOT,
131 STRIKE,
132 RATE,
133 DIV,
134 1.0,
135 &HestonParams { vol_of_vol: 1e-4, ..params() },
136 PutOrCall::Call,
137 ),
138 bs_price(SPOT, STRIKE, RATE, DIV, p.v0.sqrt(), 1.0, PutOrCall::Call),
139 1e-4,
140 );
141
142 common::section("The Heston smile (implied vol backed out of Heston prices)");
143 println!(" {:>8} {:>14} {:>14}", "strike", "heston price", "implied vol");
144 for k in [70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0] {
145 let price = heston_price(SPOT, k, RATE, DIV, 1.0, &p, PutOrCall::Call);
146 let iv = implied_vol_from_price(SPOT, k, RATE, DIV, 1.0, price, PutOrCall::Call)
147 .unwrap_or(f64::NAN);
148 println!(" {k:>8.1} {price:>14.6} {:>13.4}%", iv * 100.0);
149 }
150 common::note("rho < 0 tilts the smile: low strikes carry higher implied vol");
151
152 common::section("Correlation and vol-of-vol control the smile shape");
153 println!(" {:>6} {:>8} {:>12} {:>12} {:>12}", "rho", "vol-of-vol", "iv(80)", "iv(100)", "iv(120)");
154 for (rho, vov) in [(-0.7, 0.4), (0.0, 0.4), (0.7, 0.4), (-0.7, 0.1), (-0.7, 0.8)] {
155 let hp = HestonParams { rho, vol_of_vol: vov, ..params() };
156 let iv = |k: f64| {
157 let price = heston_price(SPOT, k, RATE, DIV, 1.0, &hp, PutOrCall::Call);
158 implied_vol_from_price(SPOT, k, RATE, DIV, 1.0, price, PutOrCall::Call)
159 .unwrap_or(f64::NAN)
160 * 100.0
161 };
162 println!(" {rho:>6.1} {vov:>10.1} {:>11.3}% {:>11.3}% {:>11.3}%", iv(80.0), iv(100.0), iv(120.0));
163 }
164 common::note("rho controls the skew (tilt); vol-of-vol controls the smile (curvature)");
165 println!();
166}Additional examples can be found in:
Sourcepub fn mc_time_steps(self, steps: usize) -> Self
pub fn mc_time_steps(self, steps: usize) -> Self
Examples found in repository?
examples/dividends_and_borrow.rs (line 99)
39fn main() {
40 common::title("DIVIDENDS AND BORROW COST — S=100 K=100 sigma=30% r=5% T=1y");
41
42 common::section("Continuous carry: dividend yield and borrow cost are interchangeable");
43 common::table_header();
44 common::row("no carry", &base().vanilla(PutOrCall::Call).build());
45 common::row("q = 4%", &base().dividend_yield(0.04).vanilla(PutOrCall::Call).build());
46 common::row("borrow = 4%", &base().borrow_cost(0.04).vanilla(PutOrCall::Call).build());
47 common::row(
48 "q = 1% + borrow = 3%",
49 &base().dividend_yield(0.01).borrow_cost(0.03).vanilla(PutOrCall::Call).build(),
50 );
51 common::note("carry_yield() = dividend_yield + borrow_cost enters every formula as 'q'");
52
53 let q_only = base().dividend_yield(0.04).vanilla(PutOrCall::Call).build();
54 let split = base().dividend_yield(0.01).borrow_cost(0.03).vanilla(PutOrCall::Call).build();
55 common::check("q=4% vs q=1%+b=3%", split.npv(), q_only.npv(), 1e-12);
56
57 common::section("Hard-to-borrow names: high borrow cost lowers the forward");
58 common::table_header();
59 for b in [0.0, 0.02, 0.05, 0.15] {
60 let option = base().borrow_cost(b).vanilla(PutOrCall::Call).build();
61 common::row(&format!("borrow = {:.0}%", b * 100.0), &option);
62 }
63 let hard = base().borrow_cost(0.15).vanilla(PutOrCall::Call).build();
64 println!(
65 " forward with 15% borrow: {:.4} (vs spot {SPOT})",
66 hard.base.forward_price()
67 );
68
69 common::section("Discrete cash dividends: 2 x 1.50 over the year");
70 let with_divs = |b: EquityOptionBuilder| {
71 b.cash_dividend(NaiveDate::from_ymd_opt(2026, 4, 1).unwrap(), 1.5)
72 .cash_dividend(NaiveDate::from_ymd_opt(2026, 10, 1).unwrap(), 1.5)
73 };
74 let analytic = with_divs(base()).vanilla(PutOrCall::Call).build();
75 println!(
76 " spot {SPOT} - PV(dividends) {:.6} = escrowed spot {:.6}",
77 analytic.base.pv_cash_dividends(),
78 analytic.base.effective_spot()
79 );
80 common::table_header();
81 common::row("Analytical (escrowed model)", &analytic);
82 common::row(
83 "Binomial (escrowed)",
84 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::Binomial).build(),
85 );
86 common::row(
87 "Finite difference (jump model)",
88 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::FiniteDifference).build(),
89 );
90 common::row(
91 "Monte Carlo terminal (escrowed)",
92 &with_divs(base()).vanilla(PutOrCall::Call).engine(Engine::MonteCarlo).build(),
93 );
94 common::row(
95 "Monte Carlo path-wise (jump model)",
96 &with_divs(base())
97 .vanilla(PutOrCall::Call)
98 .engine(Engine::MonteCarlo)
99 .mc_time_steps(200)
100 .paths(50_000)
101 .build(),
102 );
103 common::note("escrowed: lognormal on S - PV(divs); jump: dividends subtracted at each ex-date");
104 common::note("the two models differ slightly by construction — that gap is expected, not a bug");
105
106 common::check(
107 "escrowed analytic == BS on the escrowed spot",
108 analytic.npv(),
109 bs_price(analytic.base.effective_spot(), STRIKE, RATE, 0.0, VOL, 1.0, PutOrCall::Call),
110 1e-10,
111 );
112
113 common::section("Where the jump model matters: American exercise and barriers");
114 common::table_header();
115 common::row(
116 "American put, FD (jumps)",
117 &with_divs(base())
118 .american()
119 .vanilla(PutOrCall::Put)
120 .engine(Engine::FiniteDifference)
121 .build(),
122 );
123 common::row(
124 "American put, no dividends",
125 &base().american().vanilla(PutOrCall::Put).engine(Engine::FiniteDifference).build(),
126 );
127 common::row(
128 "Down-and-out call H=85, MC (jumps)",
129 &with_divs(base())
130 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
131 .engine(Engine::MonteCarlo)
132 .paths(50_000)
133 .build(),
134 );
135 common::row(
136 "Down-and-out call H=85, no dividends",
137 &base()
138 .barrier(PutOrCall::Call, BarrierDirection::Down, KnockType::Out, 85.0)
139 .engine(Engine::MonteCarlo)
140 .paths(50_000)
141 .build(),
142 );
143 common::note("dividend drops push the path toward a down barrier and change exercise timing");
144
145 common::section("Put-call parity with full carry");
146 let call = with_divs(base()).borrow_cost(0.02).vanilla(PutOrCall::Call).build();
147 let put = with_divs(base()).borrow_cost(0.02).vanilla(PutOrCall::Put).build();
148 let parity = call.base.effective_spot() * (-call.base.carry_yield() * 1.0_f64).exp()
149 - STRIKE * (-RATE * 1.0_f64).exp();
150 common::check("C - P = S_eff e^{-(q+b)T} - K e^{-rT}", call.npv() - put.npv(), parity, 1e-10);
151 println!();
152}pub fn seed(self, seed: u64) -> Self
pub fn fd_config(self, cfg: FdConfig) -> Self
pub fn fd_grid(self, spot_steps: usize, time_steps: usize) -> Self
Sourcepub fn build(self) -> EquityOption
pub fn build(self) -> EquityOption
Examples found in repository?
examples/vanilla_option.rs (line 40)
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}More examples
examples/futures_option.rs (line 35)
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}examples/autocallable_option.rs (line 74)
65fn main() {
66 common::title(&format!(
67 "AUTOCALLABLE NOTE — N={NOTIONAL} autocall={AUTOCALL} protection={PROTECTION} coupon={COUPON}/period, {OBSERVATIONS} observations, T=1y"
68 ));
69 common::note("pays N + m*coupon if S >= autocall barrier at observation m;");
70 common::note("otherwise N at maturity, or N*S_T/S_0 if the protection barrier was breached.");
71
72 common::section("Model comparison");
73 common::table_header();
74 common::row("GBM (flat 30%)", ¬e(AUTOCALL, PROTECTION, COUPON).build());
75 common::row(
76 "Local vol (skewed surface)",
77 ¬e(AUTOCALL, PROTECTION, COUPON)
78 .vol_surface(skewed_surface())
79 .model(McModel::LocalVol)
80 .build(),
81 );
82 common::row(
83 "Heston (vol-of-vol=0.4, rho=-0.7)",
84 ¬e(AUTOCALL, PROTECTION, COUPON)
85 .heston(HestonParams {
86 v0: VOL * VOL,
87 kappa: 2.0,
88 theta: VOL * VOL,
89 vol_of_vol: 0.4,
90 rho: -0.7,
91 })
92 .build(),
93 );
94 common::row(
95 "Analytical (unsupported)",
96 ¬e(AUTOCALL, PROTECTION, COUPON).engine(Engine::BlackScholes).build(),
97 );
98 common::note("skew/stoch-vol raise the knock-in probability, lowering the note value");
99
100 common::section("Structure sensitivity (GBM)");
101 common::table_header();
102 for coupon in [0.0, 3.0, 6.0, 9.0] {
103 common::row(&format!("coupon = {coupon}/period"), ¬e(AUTOCALL, PROTECTION, coupon).build());
104 }
105 for protection in [50.0, 60.0, 70.0, 80.0] {
106 common::row(
107 &format!("protection barrier = {protection}"),
108 ¬e(AUTOCALL, protection, COUPON).build(),
109 );
110 }
111 for autocall in [95.0, 100.0, 105.0, 110.0] {
112 common::row(
113 &format!("autocall barrier = {autocall}"),
114 ¬e(autocall, PROTECTION, COUPON).build(),
115 );
116 }
117
118 common::section("Observation frequency (GBM)");
119 common::table_header();
120 for obs in [1usize, 2, 4, 12] {
121 common::row(
122 &format!("{obs} observations"),
123 &base().autocallable(AUTOCALL, PROTECTION, COUPON, obs, NOTIONAL).build(),
124 );
125 }
126
127 common::section("Degenerate cases (exact identities)");
128 let always_calls = base()
129 .autocallable(1e-9, 50.0, COUPON, OBSERVATIONS, NOTIONAL)
130 .build()
131 .npv();
132 common::check(
133 "barrier at 0 -> called at t1 with 1 coupon",
134 always_calls,
135 (NOTIONAL + COUPON) * (-RATE * 0.25_f64).exp(),
136 1e-8,
137 );
138 let never_calls = base()
139 .autocallable(1e12, 1e-9, COUPON, OBSERVATIONS, NOTIONAL)
140 .build()
141 .npv();
142 common::check(
143 "unreachable barriers -> zero-coupon bond",
144 never_calls,
145 NOTIONAL * (-RATE * 1.0_f64).exp(),
146 1e-8,
147 );
148 let full_downside = base()
149 .autocallable(1e12, 1e12, 0.0, OBSERVATIONS, NOTIONAL)
150 .dividend_yield(0.0)
151 .build()
152 .npv();
153 common::check(
154 "always knocked in, no coupon -> discounted forward",
155 full_downside,
156 NOTIONAL,
157 0.3,
158 );
159 println!();
160}examples/forward_start_option.rs (line 49)
39fn main() {
40 common::title("FORWARD-START OPTION — S=100, strike = 1.0 x S(0.5y), T=1y, sigma=30%");
41
42 common::section("Black-Scholes: analytic vs Monte Carlo");
43 common::table_header();
44 common::row(
45 "Analytical (Rubinstein)",
46 &base()
47 .forward_start(PutOrCall::Call, 1.0, START)
48 .engine(Engine::BlackScholes)
49 .build(),
50 );
51 common::row(
52 "Monte Carlo (GBM)",
53 &base()
54 .forward_start(PutOrCall::Call, 1.0, START)
55 .engine(Engine::MonteCarlo)
56 .paths(100_000)
57 .build(),
58 );
59 common::row(
60 "Finite difference (unsupported)",
61 &base()
62 .forward_start(PutOrCall::Call, 1.0, START)
63 .engine(Engine::FiniteDifference)
64 .build(),
65 );
66
67 common::section("Forward smile: Heston vs Black-Scholes");
68 common::table_header();
69 let bs = base()
70 .forward_start(PutOrCall::Call, 1.0, START)
71 .engine(Engine::BlackScholes)
72 .build()
73 .npv();
74 common::row(
75 "Heston vol-of-vol=0.001 (-> BS)",
76 &base()
77 .forward_start(PutOrCall::Call, 1.0, START)
78 .engine(Engine::MonteCarlo)
79 .heston(heston_params(1e-3, 0.0))
80 .paths(50_000)
81 .build(),
82 );
83 for (vov, rho) in [(0.2, -0.7), (0.4, -0.7), (0.6, -0.7), (0.4, 0.0)] {
84 common::row(
85 &format!("Heston vol-of-vol={vov}, rho={rho}"),
86 &base()
87 .forward_start(PutOrCall::Call, 1.0, START)
88 .engine(Engine::MonteCarlo)
89 .heston(heston_params(vov, rho))
90 .paths(50_000)
91 .build(),
92 );
93 }
94 common::note(&format!("Black-Scholes reference: {bs:.6}"));
95 common::note("the gap is the forward-smile effect — the reason to price these on a stoch-vol model");
96
97 common::section("Strike fraction sweep (analytic)");
98 common::table_header();
99 for k in [0.9, 0.95, 1.0, 1.05, 1.1] {
100 common::row(
101 &format!("strike = {k} x S(t_f), call"),
102 &base().forward_start(PutOrCall::Call, k, START).engine(Engine::BlackScholes).build(),
103 );
104 }
105
106 common::section("Fixing date sweep (analytic, ATM)");
107 common::table_header();
108 for start in [0.1, 0.25, 0.5, 0.75, 0.9] {
109 common::row(
110 &format!("fixing at {:.0}% of life", start * 100.0),
111 &base().forward_start(PutOrCall::Call, 1.0, start).engine(Engine::BlackScholes).build(),
112 );
113 }
114 common::note("later fixing leaves less time to expiry, so the option is worth less");
115
116 common::section("Identities");
117 common::check(
118 "immediate fixing -> vanilla struck at S0",
119 forward_start_price(SPOT, 1.0, RATE, DIV, VOL, 1e-6, 1.0, PutOrCall::Call),
120 base()
121 .strike(SPOT)
122 .vanilla(PutOrCall::Call)
123 .engine(Engine::BlackScholes)
124 .build()
125 .npv(),
126 1e-3,
127 );
128 let p100 = forward_start_price(100.0, 1.0, RATE, DIV, VOL, 0.5, 1.0, PutOrCall::Call);
129 let p200 = forward_start_price(200.0, 1.0, RATE, DIV, VOL, 0.5, 1.0, PutOrCall::Call);
130 common::check("homogeneity: price(2S) = 2 price(S)", p200, 2.0 * p100, 1e-12);
131 let fs = base()
132 .forward_start(PutOrCall::Call, 1.0, START)
133 .engine(Engine::BlackScholes)
134 .build();
135 common::check("delta = price / spot (homogeneity)", fs.delta(), fs.npv() / SPOT, 1e-6);
136 println!();
137}examples/binary_option.rs (line 49)
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}examples/asian_option.rs (line 45)
35fn main() {
36 common::title("ASIAN OPTIONS — S=100 K=100 sigma=30% r=5% q=2% T=1y");
37
38 common::section("Fixed strike (average price) call");
39 common::table_header();
40 common::row(
41 "Geometric, analytic (exact)",
42 &base()
43 .asian(PutOrCall::Call, AveragingType::Geometric, AsianStrikeType::FixedStrike)
44 .engine(Engine::BlackScholes)
45 .build(),
46 );
47 common::row(
48 "Geometric, Monte Carlo",
49 &base()
50 .asian(PutOrCall::Call, AveragingType::Geometric, AsianStrikeType::FixedStrike)
51 .engine(Engine::MonteCarlo)
52 .paths(50_000)
53 .build(),
54 );
55 common::row(
56 "Arithmetic, Turnbull-Wakeman",
57 &base()
58 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
59 .engine(Engine::BlackScholes)
60 .build(),
61 );
62 common::row(
63 "Arithmetic, MC + geometric CV",
64 &base()
65 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
66 .engine(Engine::MonteCarlo)
67 .paths(50_000)
68 .build(),
69 );
70
71 common::section("Control variate effect (same path count)");
72 common::table_header();
73 let with_cv = base()
74 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
75 .engine(Engine::MonteCarlo)
76 .paths(20_000)
77 .build();
78 let without_cv = base()
79 .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
80 .engine(Engine::MonteCarlo)
81 .paths(20_000)
82 .mc_config({
83 // Euler stepping disables the control variate precondition
84 let mut c = rustyqlib::equity::montecarlo::MonteCarloConfig::default();
85 c.paths = 20_000;
86 c.scheme = DiscretizationScheme::Euler;
87 c.time_steps = 100;
88 c
89 })
90 .build();
91 common::row("with geometric control variate", &with_cv);
92 common::row("without (Euler path route)", &without_cv);
93 common::note("compare the std err column: the CV collapses the variance");
94
95 common::section("Floating strike (average strike)");
96 common::table_header();
97 for pc in [PutOrCall::Call, PutOrCall::Put] {
98 common::row(
99 &format!("Monte Carlo, {pc:?}"),
100 &base()
101 .asian(pc, AveragingType::Arithmetic, AsianStrikeType::FloatingStrike)
102 .engine(Engine::MonteCarlo)
103 .paths(50_000)
104 .build(),
105 );
106 common::row(
107 &format!("Analytic (unsupported), {pc:?}"),
108 &base()
109 .asian(pc, AveragingType::Arithmetic, AsianStrikeType::FloatingStrike)
110 .engine(Engine::BlackScholes)
111 .build(),
112 );
113 }
114
115 common::section("Orderings and limits");
116 let vanilla = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build().npv();
117 let geo = geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, None, PutOrCall::Call);
118 let arith = turnbull_wakeman_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, PutOrCall::Call);
119 println!(" geometric {geo:.6} < arithmetic {arith:.6} < vanilla {vanilla:.6}");
120 common::note("AM-GM: the arithmetic average dominates the geometric one");
121 common::note("averaging reduces effective volatility (sigma^2 T / 3), so both sit below vanilla");
122 common::check(
123 "discrete geometric (n=1e5) -> continuous",
124 geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, Some(100_000), PutOrCall::Call),
125 geo,
126 1e-3,
127 );
128
129 common::section("Averaging frequency (geometric, exact)");
130 for n in [4usize, 12, 52, 252] {
131 let price =
132 geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, Some(n), PutOrCall::Call);
133 println!(" {n:>4} fixings: {price:.6}");
134 }
135 println!();
136}Additional examples can be found in:
Trait Implementations§
Auto Trait Implementations§
impl !RefUnwindSafe for EquityOptionBuilder
impl !UnwindSafe for EquityOptionBuilder
impl Freeze for EquityOptionBuilder
impl Send for EquityOptionBuilder
impl Sync for EquityOptionBuilder
impl Unpin for EquityOptionBuilder
impl UnsafeUnpin for EquityOptionBuilder
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more