Skip to main content

EquityOptionBuilder

Struct EquityOptionBuilder 

Source
pub struct EquityOptionBuilder { /* private fields */ }

Implementations§

Source§

impl EquityOptionBuilder

Source

pub fn new() -> Self

Examples found in repository?
examples/dividends_and_borrow.rs (line 29)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("CARRY")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .valuation_date(asof())
36        .maturity_date(expiry())
37}
More examples
Hide additional examples
examples/barrier_option.rs (line 29)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("BARRIER")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .dividend_yield(DIV)
36        .valuation_date(asof())
37        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
38}
examples/forward_start_option.rs (line 25)
24fn base() -> EquityOptionBuilder {
25    EquityOptionBuilder::new()
26        .symbol("FWDSTART")
27        .spot(SPOT)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
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}
examples/asian_option.rs (line 24)
23fn base() -> EquityOptionBuilder {
24    EquityOptionBuilder::new()
25        .symbol("ASIAN")
26        .spot(SPOT)
27        .strike(STRIKE)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
examples/binary_option.rs (line 22)
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}
Source

pub fn symbol(self, symbol: &str) -> Self

Examples found in repository?
examples/dividends_and_borrow.rs (line 30)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("CARRY")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .valuation_date(asof())
36        .maturity_date(expiry())
37}
More examples
Hide additional examples
examples/barrier_option.rs (line 30)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("BARRIER")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .dividend_yield(DIV)
36        .valuation_date(asof())
37        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
38}
examples/forward_start_option.rs (line 26)
24fn base() -> EquityOptionBuilder {
25    EquityOptionBuilder::new()
26        .symbol("FWDSTART")
27        .spot(SPOT)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
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}
examples/asian_option.rs (line 25)
23fn base() -> EquityOptionBuilder {
24    EquityOptionBuilder::new()
25        .symbol("ASIAN")
26        .spot(SPOT)
27        .strike(STRIKE)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
examples/binary_option.rs (line 23)
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}
Source

pub fn spot(self, spot: f64) -> Self

Examples found in repository?
examples/dividends_and_borrow.rs (line 31)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("CARRY")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .valuation_date(asof())
36        .maturity_date(expiry())
37}
More examples
Hide additional examples
examples/barrier_option.rs (line 31)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("BARRIER")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .dividend_yield(DIV)
36        .valuation_date(asof())
37        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
38}
examples/forward_start_option.rs (line 27)
24fn base() -> EquityOptionBuilder {
25    EquityOptionBuilder::new()
26        .symbol("FWDSTART")
27        .spot(SPOT)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
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/asian_option.rs (line 26)
23fn base() -> EquityOptionBuilder {
24    EquityOptionBuilder::new()
25        .symbol("ASIAN")
26        .spot(SPOT)
27        .strike(STRIKE)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
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}
Source

pub fn strike(self, strike: f64) -> Self

Examples found in repository?
examples/dividends_and_borrow.rs (line 32)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("CARRY")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .valuation_date(asof())
36        .maturity_date(expiry())
37}
More examples
Hide additional examples
examples/barrier_option.rs (line 32)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("BARRIER")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .dividend_yield(DIV)
36        .valuation_date(asof())
37        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
38}
examples/asian_option.rs (line 27)
23fn base() -> EquityOptionBuilder {
24    EquityOptionBuilder::new()
25        .symbol("ASIAN")
26        .spot(SPOT)
27        .strike(STRIKE)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
examples/binary_option.rs (line 25)
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}
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}
Source

pub fn flat_vol(self, vol: f64) -> Self

Examples found in repository?
examples/dividends_and_borrow.rs (line 33)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("CARRY")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .valuation_date(asof())
36        .maturity_date(expiry())
37}
More examples
Hide additional examples
examples/barrier_option.rs (line 33)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("BARRIER")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .dividend_yield(DIV)
36        .valuation_date(asof())
37        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
38}
examples/forward_start_option.rs (line 28)
24fn base() -> EquityOptionBuilder {
25    EquityOptionBuilder::new()
26        .symbol("FWDSTART")
27        .spot(SPOT)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
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}
examples/asian_option.rs (line 28)
23fn base() -> EquityOptionBuilder {
24    EquityOptionBuilder::new()
25        .symbol("ASIAN")
26        .spot(SPOT)
27        .strike(STRIKE)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
examples/binary_option.rs (line 26)
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}
Source

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%)", &note(AUTOCALL, PROTECTION, COUPON).build());
75    common::row(
76        "Local vol (skewed surface)",
77        &note(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        &note(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        &note(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"), &note(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            &note(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            &note(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
Hide additional 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(&quotes).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}
Source

pub fn flat_rate(self, rate: f64) -> Self

Examples found in repository?
examples/dividends_and_borrow.rs (line 34)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("CARRY")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .valuation_date(asof())
36        .maturity_date(expiry())
37}
More examples
Hide additional examples
examples/barrier_option.rs (line 34)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("BARRIER")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .dividend_yield(DIV)
36        .valuation_date(asof())
37        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
38}
examples/forward_start_option.rs (line 29)
24fn base() -> EquityOptionBuilder {
25    EquityOptionBuilder::new()
26        .symbol("FWDSTART")
27        .spot(SPOT)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
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}
examples/asian_option.rs (line 29)
23fn base() -> EquityOptionBuilder {
24    EquityOptionBuilder::new()
25        .symbol("ASIAN")
26        .spot(SPOT)
27        .strike(STRIKE)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
examples/binary_option.rs (line 27)
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}
Source

pub fn discount_curve(self, curve: YieldCurve) -> Self

Source

pub fn dividend_yield(self, q: f64) -> Self

Examples found in repository?
examples/barrier_option.rs (line 35)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("BARRIER")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .dividend_yield(DIV)
36        .valuation_date(asof())
37        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
38}
More examples
Hide additional examples
examples/forward_start_option.rs (line 30)
24fn base() -> EquityOptionBuilder {
25    EquityOptionBuilder::new()
26        .symbol("FWDSTART")
27        .spot(SPOT)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
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%)", &note(AUTOCALL, PROTECTION, COUPON).build());
75    common::row(
76        "Local vol (skewed surface)",
77        &note(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        &note(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        &note(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"), &note(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            &note(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            &note(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/asian_option.rs (line 30)
23fn base() -> EquityOptionBuilder {
24    EquityOptionBuilder::new()
25        .symbol("ASIAN")
26        .spot(SPOT)
27        .strike(STRIKE)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
examples/binary_option.rs (line 28)
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}
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}
Source

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}
Source

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}
Source

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}
Source

pub fn valuation_date(self, date: NaiveDate) -> Self

Examples found in repository?
examples/dividends_and_borrow.rs (line 35)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("CARRY")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .valuation_date(asof())
36        .maturity_date(expiry())
37}
More examples
Hide additional examples
examples/barrier_option.rs (line 36)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("BARRIER")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .dividend_yield(DIV)
36        .valuation_date(asof())
37        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
38}
examples/forward_start_option.rs (line 31)
24fn base() -> EquityOptionBuilder {
25    EquityOptionBuilder::new()
26        .symbol("FWDSTART")
27        .spot(SPOT)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
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}
examples/asian_option.rs (line 31)
23fn base() -> EquityOptionBuilder {
24    EquityOptionBuilder::new()
25        .symbol("ASIAN")
26        .spot(SPOT)
27        .strike(STRIKE)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
examples/binary_option.rs (line 29)
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}
Source

pub fn maturity_date(self, date: NaiveDate) -> Self

Examples found in repository?
examples/dividends_and_borrow.rs (line 36)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("CARRY")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .valuation_date(asof())
36        .maturity_date(expiry())
37}
More examples
Hide additional examples
examples/barrier_option.rs (line 37)
28fn base() -> EquityOptionBuilder {
29    EquityOptionBuilder::new()
30        .symbol("BARRIER")
31        .spot(SPOT)
32        .strike(STRIKE)
33        .flat_vol(VOL)
34        .flat_rate(RATE)
35        .dividend_yield(DIV)
36        .valuation_date(asof())
37        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
38}
examples/forward_start_option.rs (line 32)
24fn base() -> EquityOptionBuilder {
25    EquityOptionBuilder::new()
26        .symbol("FWDSTART")
27        .spot(SPOT)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
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}
examples/asian_option.rs (line 32)
23fn base() -> EquityOptionBuilder {
24    EquityOptionBuilder::new()
25        .symbol("ASIAN")
26        .spot(SPOT)
27        .strike(STRIKE)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
examples/binary_option.rs (line 30)
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}
Source

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
Hide additional 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}
Source

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}
Source

pub fn exercise_style(self, style: ContractStyle) -> Self

Source

pub fn payoff(self, payoff: Box<dyn Payoff>) -> Self

Source

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
Hide additional 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}
Source

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
Hide additional 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}
Source

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
Hide additional 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}
Source

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}
Source

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}
Source

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%)", &note(AUTOCALL, PROTECTION, COUPON).build());
75    common::row(
76        "Local vol (skewed surface)",
77        &note(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        &note(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        &note(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"), &note(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            &note(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            &note(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}
Source

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
Hide additional 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%)", &note(AUTOCALL, PROTECTION, COUPON).build());
75    common::row(
76        "Local vol (skewed surface)",
77        &note(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        &note(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        &note(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"), &note(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            &note(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            &note(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}
Source

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%)", &note(AUTOCALL, PROTECTION, COUPON).build());
75    common::row(
76        "Local vol (skewed surface)",
77        &note(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        &note(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        &note(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"), &note(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            &note(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            &note(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
Hide additional 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(&quotes).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}
Source

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
Hide additional 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%)", &note(AUTOCALL, PROTECTION, COUPON).build());
75    common::row(
76        "Local vol (skewed surface)",
77        &note(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        &note(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        &note(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"), &note(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            &note(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            &note(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}
Source

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}
Source

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
Hide additional 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}
Source

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}
Source

pub fn seed(self, seed: u64) -> Self

Source

pub fn fd_config(self, cfg: FdConfig) -> Self

Source

pub fn fd_grid(self, spot_steps: usize, time_steps: usize) -> Self

Source

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
Hide additional 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%)", &note(AUTOCALL, PROTECTION, COUPON).build());
75    common::row(
76        "Local vol (skewed surface)",
77        &note(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        &note(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        &note(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"), &note(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            &note(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            &note(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}

Trait Implementations§

Source§

impl Default for EquityOptionBuilder

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V