pub struct EquityOption {
pub base: EquityOptionBase,
pub payoff: Box<dyn Payoff>,
pub engine: Engine,
pub mc: MonteCarloConfig,
pub fd: FdConfig,
pub heston: Option<HestonParams>,
}Fields§
§base: EquityOptionBase§payoff: Box<dyn Payoff>§engine: Engine§mc: MonteCarloConfigMonte Carlo settings (paths, time steps, scheme, sampler, seed).
mc.model (GBM vs local vol) also applies to the FD engine.
fd: FdConfigFinite difference grid settings; only consulted when engine is
Engine::FiniteDifference.
heston: Option<HestonParams>Heston parameters; required when the model is Heston.
Implementations§
Source§impl EquityOption
impl EquityOption
pub fn time_to_maturity(&self) -> f64
Source§impl EquityOption
impl EquityOption
pub fn from_json(data: &EquityOptionData) -> Box<EquityOption>
Source§impl EquityOption
impl EquityOption
Sourcepub fn try_imp_vol(&self, option_price: f64) -> Result<f64, String>
pub fn try_imp_vol(&self, option_price: f64) -> Result<f64, String>
Implied Black-Scholes volatility for option_price (safeguarded
Newton with arbitrage-bound checks); does not modify the option.
Sourcepub fn imp_vol(&mut self, option_price: f64) -> f64
pub fn imp_vol(&mut self, option_price: f64) -> f64
Implied vol for option_price; leaves the option holding a flat
surface at the solved vol. Panics on arbitrage-violating prices —
use try_imp_vol to handle those gracefully.
pub fn get_imp_vol(&mut self) -> f64
Source§impl EquityOption
Greeks per engine: Monte Carlo uses bump-and-reprice with common random
numbers (supporting American via Longstaff-Schwartz repricing); the FD
engine reads delta/gamma/theta off its own grid (so American and barrier
sensitivities are engine-consistent) with vega/rho by re-solving; the
remaining engines use the analytic Black-Scholes closed forms.
impl EquityOption
Greeks per engine: Monte Carlo uses bump-and-reprice with common random numbers (supporting American via Longstaff-Schwartz repricing); the FD engine reads delta/gamma/theta off its own grid (so American and barrier sensitivities are engine-consistent) with vega/rho by re-solving; the remaining engines use the analytic Black-Scholes closed forms.
Sourcepub fn delta(&self) -> f64
pub fn delta(&self) -> f64
Examples found in repository?
39pub fn row(label: &str, option: &EquityOption) {
40 // keep the table readable: the caught panic is reported in the row
41 let hook = std::panic::take_hook();
42 std::panic::set_hook(Box::new(|_| {}));
43 let result = catch_unwind(AssertUnwindSafe(|| {
44 let (pv, std_err) = if option.engine == Engine::MonteCarlo {
45 let s = montecarlo::npv_with_stats(option);
46 (s.pv, Some(s.std_err))
47 } else {
48 (option.npv(), None)
49 };
50 (pv, option.delta(), option.gamma(), option.vega(), option.theta(), option.rho(), std_err)
51 }));
52 std::panic::set_hook(hook);
53 match result {
54 Ok((pv, delta, gamma, vega, theta, rho, std_err)) => {
55 let se = match std_err {
56 Some(v) => format!("{v:.5}"),
57 None => "-".to_string(),
58 };
59 println!(
60 "{label:<34} {pv:>12.6} {delta:>10.5} {gamma:>10.5} {vega:>9.3} {theta:>9.3} {rho:>9.3} {se:>9}"
61 );
62 }
63 Err(payload) => {
64 let msg = payload
65 .downcast_ref::<String>()
66 .cloned()
67 .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
68 .unwrap_or_else(|| "panicked".to_string());
69 let short: String = msg.split(';').next().unwrap_or(&msg).chars().take(52).collect();
70 println!("{label:<34} {:>12} ({short})", "unsupported");
71 }
72 }
73}More examples
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}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}33fn main() {
34 common::title("BINARY OPTION — S=100 K=100 sigma=30% r=5% q=2% T=1y");
35
36 for (name, binary_type, cash) in [
37 ("cash-or-nothing (1 unit)", BinaryType::CashOrNothing, CASH),
38 ("asset-or-nothing", BinaryType::AssetOrNothing, 0.0),
39 ] {
40 for pc in [PutOrCall::Call, PutOrCall::Put] {
41 common::section(&format!("{name} {pc:?}"));
42 common::table_header();
43 for (label, engine) in [
44 ("Analytical (closed form)", Engine::BlackScholes),
45 ("Binomial (1000 steps)", Engine::Binomial),
46 ("Finite difference", Engine::FiniteDifference),
47 ("Monte Carlo (Sobol, 100k)", Engine::MonteCarlo),
48 ] {
49 common::row(label, &base().binary(pc, binary_type, cash).engine(engine).build());
50 }
51 }
52 }
53 common::note("the tree oscillates on digitals: the strike falls between terminal nodes");
54
55 common::section("Cash amount scales linearly");
56 common::table_header();
57 for cash in [1.0, 100.0, 1000.0] {
58 common::row(
59 &format!("cash-or-nothing call, cash={cash}"),
60 &base()
61 .binary(PutOrCall::Call, BinaryType::CashOrNothing, cash)
62 .engine(Engine::BlackScholes)
63 .build(),
64 );
65 }
66
67 common::section("Identities");
68 let cash_call = base()
69 .binary(PutOrCall::Call, BinaryType::CashOrNothing, CASH)
70 .engine(Engine::BlackScholes)
71 .build();
72 let cash_put = base()
73 .binary(PutOrCall::Put, BinaryType::CashOrNothing, CASH)
74 .engine(Engine::BlackScholes)
75 .build();
76 common::check(
77 "cash call + cash put = e^{-rT}",
78 cash_call.npv() + cash_put.npv(),
79 (-RATE * 1.0_f64).exp(),
80 1e-12,
81 );
82
83 let asset_call = base()
84 .binary(PutOrCall::Call, BinaryType::AssetOrNothing, 0.0)
85 .engine(Engine::BlackScholes)
86 .build();
87 let asset_put = base()
88 .binary(PutOrCall::Put, BinaryType::AssetOrNothing, 0.0)
89 .engine(Engine::BlackScholes)
90 .build();
91 common::check(
92 "asset call + asset put = S e^{-qT}",
93 asset_call.npv() + asset_put.npv(),
94 SPOT * (-DIV * 1.0_f64).exp(),
95 1e-10,
96 );
97
98 common::section("Replication: asset digital = vanilla call + K cash digitals");
99 let vanilla = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build();
100 let k_cash = base()
101 .binary(PutOrCall::Call, BinaryType::CashOrNothing, STRIKE)
102 .engine(Engine::BlackScholes)
103 .build();
104 common::check("npv", asset_call.npv(), vanilla.npv() + k_cash.npv(), 1e-10);
105 common::check("delta", asset_call.delta(), vanilla.delta() + k_cash.delta(), 1e-10);
106 common::check("gamma", asset_call.gamma(), vanilla.gamma() + k_cash.gamma(), 1e-10);
107 common::check("vega", asset_call.vega(), vanilla.vega() + k_cash.vega(), 1e-10);
108 common::check("theta", asset_call.theta(), vanilla.theta() + k_cash.theta(), 1e-10);
109 common::check("rho", asset_call.rho(), vanilla.rho() + k_cash.rho(), 1e-10);
110 common::note("both sides are implemented independently, so this is a real cross-check");
111
112 common::section("Digital risk: delta and gamma explode near the strike at expiry");
113 common::table_header();
114 for years in [1.0, 0.25, 0.05, 0.01] {
115 common::row(
116 &format!("cash-or-nothing call, T={years}y"),
117 &base()
118 .years_to_maturity(years)
119 .binary(PutOrCall::Call, BinaryType::CashOrNothing, CASH)
120 .engine(Engine::BlackScholes)
121 .build(),
122 );
123 }
124
125 digital_greek_surfaces();
126 println!();
127}
128
129/// The digital's Greeks are the standout case for visualizing (lack of)
130/// smoothness: as maturity shrinks the delta spikes into a tall bump at the
131/// strike and gamma flips sign right across it. Saved as self-contained
132/// interactive HTML to `runs/binary_option/`.
133fn digital_greek_surfaces() {
134 use common::plot3d::{greek_surface, linspace, save_surface_html, Labels};
135 use rustyqlib::equity::vanila_option::EquityOption;
136
137 common::section("Digital Greek surfaces over (moneyness, maturity) -> runs/binary_option/*.html");
138
139 // tighter moneyness band and shorter maturities: that is where the
140 // digital's delta/gamma structure lives
141 let moneyness = linspace(0.8, 1.2, 80);
142 let mats = linspace(0.02, 1.0, 60);
143
144 let greek = |select: fn(&EquityOption) -> f64| {
145 move |m: f64, years: f64| -> f64 {
146 let option = base()
147 .spot(m * STRIKE)
148 .years_to_maturity(years)
149 .binary(PutOrCall::Call, BinaryType::CashOrNothing, CASH)
150 .engine(Engine::BlackScholes)
151 .build();
152 select(&option)
153 }
154 };
155
156 for (name, file, select) in [
157 ("Delta", "delta", (|o: &EquityOption| o.delta()) as fn(&EquityOption) -> f64),
158 ("Gamma", "gamma", |o: &EquityOption| o.gamma()),
159 ] {
160 let surface = greek_surface(&moneyness, &mats, greek(select));
161 save_surface_html(
162 &surface,
163 &format!("runs/binary_option/{file}_surface.html"),
164 &Labels {
165 title: &format!("Cash digital call {name} (K=100) — note the near-expiry spike"),
166 x: "moneyness (S/K)",
167 y: "maturity (y)",
168 z: name,
169 },
170 );
171 }
172 common::note("contrast with the vanilla surfaces: the digital is far from smooth near the strike");
173}Sourcepub fn gamma(&self) -> f64
pub fn gamma(&self) -> f64
Examples found in repository?
39pub fn row(label: &str, option: &EquityOption) {
40 // keep the table readable: the caught panic is reported in the row
41 let hook = std::panic::take_hook();
42 std::panic::set_hook(Box::new(|_| {}));
43 let result = catch_unwind(AssertUnwindSafe(|| {
44 let (pv, std_err) = if option.engine == Engine::MonteCarlo {
45 let s = montecarlo::npv_with_stats(option);
46 (s.pv, Some(s.std_err))
47 } else {
48 (option.npv(), None)
49 };
50 (pv, option.delta(), option.gamma(), option.vega(), option.theta(), option.rho(), std_err)
51 }));
52 std::panic::set_hook(hook);
53 match result {
54 Ok((pv, delta, gamma, vega, theta, rho, std_err)) => {
55 let se = match std_err {
56 Some(v) => format!("{v:.5}"),
57 None => "-".to_string(),
58 };
59 println!(
60 "{label:<34} {pv:>12.6} {delta:>10.5} {gamma:>10.5} {vega:>9.3} {theta:>9.3} {rho:>9.3} {se:>9}"
61 );
62 }
63 Err(payload) => {
64 let msg = payload
65 .downcast_ref::<String>()
66 .cloned()
67 .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
68 .unwrap_or_else(|| "panicked".to_string());
69 let short: String = msg.split(';').next().unwrap_or(&msg).chars().take(52).collect();
70 println!("{label:<34} {:>12} ({short})", "unsupported");
71 }
72 }
73}More examples
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}33fn main() {
34 common::title("BINARY OPTION — S=100 K=100 sigma=30% r=5% q=2% T=1y");
35
36 for (name, binary_type, cash) in [
37 ("cash-or-nothing (1 unit)", BinaryType::CashOrNothing, CASH),
38 ("asset-or-nothing", BinaryType::AssetOrNothing, 0.0),
39 ] {
40 for pc in [PutOrCall::Call, PutOrCall::Put] {
41 common::section(&format!("{name} {pc:?}"));
42 common::table_header();
43 for (label, engine) in [
44 ("Analytical (closed form)", Engine::BlackScholes),
45 ("Binomial (1000 steps)", Engine::Binomial),
46 ("Finite difference", Engine::FiniteDifference),
47 ("Monte Carlo (Sobol, 100k)", Engine::MonteCarlo),
48 ] {
49 common::row(label, &base().binary(pc, binary_type, cash).engine(engine).build());
50 }
51 }
52 }
53 common::note("the tree oscillates on digitals: the strike falls between terminal nodes");
54
55 common::section("Cash amount scales linearly");
56 common::table_header();
57 for cash in [1.0, 100.0, 1000.0] {
58 common::row(
59 &format!("cash-or-nothing call, cash={cash}"),
60 &base()
61 .binary(PutOrCall::Call, BinaryType::CashOrNothing, cash)
62 .engine(Engine::BlackScholes)
63 .build(),
64 );
65 }
66
67 common::section("Identities");
68 let cash_call = base()
69 .binary(PutOrCall::Call, BinaryType::CashOrNothing, CASH)
70 .engine(Engine::BlackScholes)
71 .build();
72 let cash_put = base()
73 .binary(PutOrCall::Put, BinaryType::CashOrNothing, CASH)
74 .engine(Engine::BlackScholes)
75 .build();
76 common::check(
77 "cash call + cash put = e^{-rT}",
78 cash_call.npv() + cash_put.npv(),
79 (-RATE * 1.0_f64).exp(),
80 1e-12,
81 );
82
83 let asset_call = base()
84 .binary(PutOrCall::Call, BinaryType::AssetOrNothing, 0.0)
85 .engine(Engine::BlackScholes)
86 .build();
87 let asset_put = base()
88 .binary(PutOrCall::Put, BinaryType::AssetOrNothing, 0.0)
89 .engine(Engine::BlackScholes)
90 .build();
91 common::check(
92 "asset call + asset put = S e^{-qT}",
93 asset_call.npv() + asset_put.npv(),
94 SPOT * (-DIV * 1.0_f64).exp(),
95 1e-10,
96 );
97
98 common::section("Replication: asset digital = vanilla call + K cash digitals");
99 let vanilla = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build();
100 let k_cash = base()
101 .binary(PutOrCall::Call, BinaryType::CashOrNothing, STRIKE)
102 .engine(Engine::BlackScholes)
103 .build();
104 common::check("npv", asset_call.npv(), vanilla.npv() + k_cash.npv(), 1e-10);
105 common::check("delta", asset_call.delta(), vanilla.delta() + k_cash.delta(), 1e-10);
106 common::check("gamma", asset_call.gamma(), vanilla.gamma() + k_cash.gamma(), 1e-10);
107 common::check("vega", asset_call.vega(), vanilla.vega() + k_cash.vega(), 1e-10);
108 common::check("theta", asset_call.theta(), vanilla.theta() + k_cash.theta(), 1e-10);
109 common::check("rho", asset_call.rho(), vanilla.rho() + k_cash.rho(), 1e-10);
110 common::note("both sides are implemented independently, so this is a real cross-check");
111
112 common::section("Digital risk: delta and gamma explode near the strike at expiry");
113 common::table_header();
114 for years in [1.0, 0.25, 0.05, 0.01] {
115 common::row(
116 &format!("cash-or-nothing call, T={years}y"),
117 &base()
118 .years_to_maturity(years)
119 .binary(PutOrCall::Call, BinaryType::CashOrNothing, CASH)
120 .engine(Engine::BlackScholes)
121 .build(),
122 );
123 }
124
125 digital_greek_surfaces();
126 println!();
127}
128
129/// The digital's Greeks are the standout case for visualizing (lack of)
130/// smoothness: as maturity shrinks the delta spikes into a tall bump at the
131/// strike and gamma flips sign right across it. Saved as self-contained
132/// interactive HTML to `runs/binary_option/`.
133fn digital_greek_surfaces() {
134 use common::plot3d::{greek_surface, linspace, save_surface_html, Labels};
135 use rustyqlib::equity::vanila_option::EquityOption;
136
137 common::section("Digital Greek surfaces over (moneyness, maturity) -> runs/binary_option/*.html");
138
139 // tighter moneyness band and shorter maturities: that is where the
140 // digital's delta/gamma structure lives
141 let moneyness = linspace(0.8, 1.2, 80);
142 let mats = linspace(0.02, 1.0, 60);
143
144 let greek = |select: fn(&EquityOption) -> f64| {
145 move |m: f64, years: f64| -> f64 {
146 let option = base()
147 .spot(m * STRIKE)
148 .years_to_maturity(years)
149 .binary(PutOrCall::Call, BinaryType::CashOrNothing, CASH)
150 .engine(Engine::BlackScholes)
151 .build();
152 select(&option)
153 }
154 };
155
156 for (name, file, select) in [
157 ("Delta", "delta", (|o: &EquityOption| o.delta()) as fn(&EquityOption) -> f64),
158 ("Gamma", "gamma", |o: &EquityOption| o.gamma()),
159 ] {
160 let surface = greek_surface(&moneyness, &mats, greek(select));
161 save_surface_html(
162 &surface,
163 &format!("runs/binary_option/{file}_surface.html"),
164 &Labels {
165 title: &format!("Cash digital call {name} (K=100) — note the near-expiry spike"),
166 x: "moneyness (S/K)",
167 y: "maturity (y)",
168 z: name,
169 },
170 );
171 }
172 common::note("contrast with the vanilla surfaces: the digital is far from smooth near the strike");
173}Sourcepub fn vega(&self) -> f64
pub fn vega(&self) -> f64
Examples found in repository?
39pub fn row(label: &str, option: &EquityOption) {
40 // keep the table readable: the caught panic is reported in the row
41 let hook = std::panic::take_hook();
42 std::panic::set_hook(Box::new(|_| {}));
43 let result = catch_unwind(AssertUnwindSafe(|| {
44 let (pv, std_err) = if option.engine == Engine::MonteCarlo {
45 let s = montecarlo::npv_with_stats(option);
46 (s.pv, Some(s.std_err))
47 } else {
48 (option.npv(), None)
49 };
50 (pv, option.delta(), option.gamma(), option.vega(), option.theta(), option.rho(), std_err)
51 }));
52 std::panic::set_hook(hook);
53 match result {
54 Ok((pv, delta, gamma, vega, theta, rho, std_err)) => {
55 let se = match std_err {
56 Some(v) => format!("{v:.5}"),
57 None => "-".to_string(),
58 };
59 println!(
60 "{label:<34} {pv:>12.6} {delta:>10.5} {gamma:>10.5} {vega:>9.3} {theta:>9.3} {rho:>9.3} {se:>9}"
61 );
62 }
63 Err(payload) => {
64 let msg = payload
65 .downcast_ref::<String>()
66 .cloned()
67 .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
68 .unwrap_or_else(|| "panicked".to_string());
69 let short: String = msg.split(';').next().unwrap_or(&msg).chars().take(52).collect();
70 println!("{label:<34} {:>12} ({short})", "unsupported");
71 }
72 }
73}More examples
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}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}Sourcepub fn theta(&self) -> f64
pub fn theta(&self) -> f64
Examples found in repository?
39pub fn row(label: &str, option: &EquityOption) {
40 // keep the table readable: the caught panic is reported in the row
41 let hook = std::panic::take_hook();
42 std::panic::set_hook(Box::new(|_| {}));
43 let result = catch_unwind(AssertUnwindSafe(|| {
44 let (pv, std_err) = if option.engine == Engine::MonteCarlo {
45 let s = montecarlo::npv_with_stats(option);
46 (s.pv, Some(s.std_err))
47 } else {
48 (option.npv(), None)
49 };
50 (pv, option.delta(), option.gamma(), option.vega(), option.theta(), option.rho(), std_err)
51 }));
52 std::panic::set_hook(hook);
53 match result {
54 Ok((pv, delta, gamma, vega, theta, rho, std_err)) => {
55 let se = match std_err {
56 Some(v) => format!("{v:.5}"),
57 None => "-".to_string(),
58 };
59 println!(
60 "{label:<34} {pv:>12.6} {delta:>10.5} {gamma:>10.5} {vega:>9.3} {theta:>9.3} {rho:>9.3} {se:>9}"
61 );
62 }
63 Err(payload) => {
64 let msg = payload
65 .downcast_ref::<String>()
66 .cloned()
67 .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
68 .unwrap_or_else(|| "panicked".to_string());
69 let short: String = msg.split(';').next().unwrap_or(&msg).chars().take(52).collect();
70 println!("{label:<34} {:>12} ({short})", "unsupported");
71 }
72 }
73}More examples
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}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}Sourcepub fn rho(&self) -> f64
pub fn rho(&self) -> f64
Examples found in repository?
39pub fn row(label: &str, option: &EquityOption) {
40 // keep the table readable: the caught panic is reported in the row
41 let hook = std::panic::take_hook();
42 std::panic::set_hook(Box::new(|_| {}));
43 let result = catch_unwind(AssertUnwindSafe(|| {
44 let (pv, std_err) = if option.engine == Engine::MonteCarlo {
45 let s = montecarlo::npv_with_stats(option);
46 (s.pv, Some(s.std_err))
47 } else {
48 (option.npv(), None)
49 };
50 (pv, option.delta(), option.gamma(), option.vega(), option.theta(), option.rho(), std_err)
51 }));
52 std::panic::set_hook(hook);
53 match result {
54 Ok((pv, delta, gamma, vega, theta, rho, std_err)) => {
55 let se = match std_err {
56 Some(v) => format!("{v:.5}"),
57 None => "-".to_string(),
58 };
59 println!(
60 "{label:<34} {pv:>12.6} {delta:>10.5} {gamma:>10.5} {vega:>9.3} {theta:>9.3} {rho:>9.3} {se:>9}"
61 );
62 }
63 Err(payload) => {
64 let msg = payload
65 .downcast_ref::<String>()
66 .cloned()
67 .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
68 .unwrap_or_else(|| "panicked".to_string());
69 let short: String = msg.split(';').next().unwrap_or(&msg).chars().take(52).collect();
70 println!("{label:<34} {:>12} ({short})", "unsupported");
71 }
72 }
73}More examples
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}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}