1mod common;
8
9use chrono::NaiveDate;
10use rustyqlib::core::trade::PutOrCall;
11use rustyqlib::core::traits::Instrument;
12use rustyqlib::equity::builder::EquityOptionBuilder;
13use rustyqlib::equity::forward_start_option::forward_start_price;
14use rustyqlib::equity::heston::HestonParams;
15use rustyqlib::equity::montecarlo::McModel;
16use rustyqlib::equity::utils::Engine;
17
18const SPOT: f64 = 100.0;
19const VOL: f64 = 0.30;
20const RATE: f64 = 0.05;
21const DIV: f64 = 0.02;
22const START: f64 = 0.5; fn 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}
34
35fn heston_params(vol_of_vol: f64, rho: f64) -> HestonParams {
36 HestonParams { v0: VOL * VOL, kappa: 2.0, theta: VOL * VOL, vol_of_vol, rho }
37}
38
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}