1mod common;
7
8use chrono::NaiveDate;
9use rustyqlib::core::trade::PutOrCall;
10use rustyqlib::core::traits::Instrument;
11use rustyqlib::equity::barrier::{BarrierDirection, KnockType};
12use rustyqlib::equity::blackscholes::{bs_price, implied_vol_from_price};
13use rustyqlib::equity::builder::EquityOptionBuilder;
14use rustyqlib::equity::heston::{heston_price, HestonParams};
15use rustyqlib::equity::utils::Engine;
16use rustyqlib::equity::vanila_option::BinaryType;
17
18const SPOT: f64 = 100.0;
19const STRIKE: f64 = 100.0;
20const RATE: f64 = 0.05;
21const DIV: f64 = 0.02;
22
23fn params() -> HestonParams {
24 HestonParams { v0: 0.09, kappa: 2.0, theta: 0.09, vol_of_vol: 0.4, rho: -0.7 }
25}
26
27fn base() -> EquityOptionBuilder {
28 EquityOptionBuilder::new()
29 .symbol("HESTON")
30 .spot(SPOT)
31 .strike(STRIKE)
32 .flat_vol(0.30) .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}
39
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}