Skip to content

Commit f3ac6cd

Browse files
authored
feat(greeks): Garman-Kohlhagen FX Greeks (delta/gamma/vega/theta/rho_d/rho_f) (#403)
* feat(greeks): add Garman-Kohlhagen FX Greeks for #401 Closed-form Greeks (delta, gamma, vega, theta, ρ_d, ρ_f) for the Garman–Kohlhagen (1983) FX option pricing model. Closes #401. Garman–Kohlhagen prices European options on a foreign-exchange spot rate `S` quoted as domestic per unit of foreign, with the foreign currency earning interest at rate `r_f`. Structurally GK ≡ BSM with the continuous dividend yield `q = r_f`, and FX options carry two rate sensitivities (domestic and foreign) instead of one. Public surface (`src/greeks/garman_kohlhagen.rs`): - `delta_gk`, `gamma_gk`, `vega_gk`, `theta_gk`, `rho_domestic_gk`, `rho_foreign_gk` - `GarmanKohlhagenGreeks` trait mirroring the `GarmanKohlhagen` pricing trait — implementors provide `get_option(&self)` and inherit default delegations to the free functions above. FX field mapping: `risk_free_rate → r_d`, `dividend_yield → r_f`, `underlying_price → S`. Greek units mirror the BSM module: vega per 1 % vol, theta per calendar day (annual ÷ 365), domestic and foreign rho per 1 % rate. Quantity scales linearly; `Side::Short` flips the delta sign only (consistent with `greeks::delta`). The implementation uses the carry-adjusted form of `d1`/`d2` directly (`b = r_d − r_f`) rather than delegating to the existing BSM Greeks. The existing BSM Greeks compute `d1` from `risk_free_rate` only and then multiply by `e^(-qT)` — a mismatch that produces incorrect deltas and thetas when `dividend_yield ≠ 0`. The pricing kernels are unaffected (they go through `calculate_d_values`, which does include `−q` in the drift). Fixing the BSM Greeks is tracked separately; for GK we needed correct FX numbers, hence the standalone implementation. Tests (18, all passing): - Delta range: `Δ_call ∈ (0, e^(-r_f·T))`, `Δ_put ∈ (-e^(-r_f·T), 0)` - Spot delta-parity: `Δ_call − Δ_put = e^(-r_f·T)` to 1e-9 - `Γ > 0`, `ν > 0` for both call and put across moneyness - `Γ_call = Γ_put`, `ν_call = ν_put` (Black-Scholes invariant) - Rho signs: long call → +ρ_d, -ρ_f; long put → -ρ_d, +ρ_f - BSM equivalence at `q = 0` (the only case where buggy BSM agrees) - Theta vs numerical price-difference (5e-3 tolerance, 1-day bump) - FX call-delta reference (S=0.98, K=1.00, r_d=5 %, r_f=4 %, T=4/12, σ=10 %): Δ ≈ 0.3909 to 1e-3 - Zero volatility on every Greek → error - American / Bermuda / exotic → `GreeksError::Pricing(UnsupportedOptionType)` - Trait `GarmanKohlhagenGreeks` round-trips against the free functions - `Side::Short` negates delta - Quantity scales linearly Example: `examples/examples_pricing/src/bin/garman_kohlhagen_greeks.rs` walks ATM / ITM / OTM calls and puts on a 6-month EUR/USD scenario, demonstrates the spot delta-parity identity (zero residual) and trait usage on a wrapper type. * Simplify Monte Carlo simulator data structure for improved rendering performance. * address review: handle T=0 + use checked d_add in theta - Added time_to_expiry helper that mirrors BSM Greeks: Greeks at expiration return discrete values (delta = ±1/0 by intrinsic state, gamma/vega/theta/ρ_d/ρ_f = 0) instead of erroring out from d1/d2. - Replaced raw `+` in theta_gk with checked d_add to keep arithmetic consistent with the rest of the crate (tracked in src/model/decimal.rs). - New tests cover T=0 across all six Greeks, including ITM/OTM and short side. 23 GK Greek tests pass. Addresses Copilot review on PR #403.
1 parent b9623a1 commit f3ac6cd

7 files changed

Lines changed: 991 additions & 6 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "optionstratlib"
3-
version = "0.17.2"
3+
version = "0.17.3"
44
edition = "2024"
55
authors = ["Joaquin Bejar <jb@taunais.com>"]
66
description = "OptionStratLib is a comprehensive Rust library for options trading and strategy development across multiple asset classes."

Draws/Simulation/simulator_test_montecarlo.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

examples/examples_pricing/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ path = "src/bin/black_76_greeks.rs"
2323
[[bin]]
2424
name = "garman_kohlhagen"
2525
path = "src/bin/garman_kohlhagen.rs"
26+
27+
[[bin]]
28+
name = "garman_kohlhagen_greeks"
29+
path = "src/bin/garman_kohlhagen_greeks.rs"
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/******************************************************************************
2+
Author: Joaquín Béjar García
3+
Email: jb@taunais.com
4+
Date: 2026-04-27
5+
******************************************************************************/
6+
7+
//! Garman–Kohlhagen Greeks example — EUR/USD scenario.
8+
//!
9+
//! Computes spot delta, gamma, vega, theta, domestic rho, and foreign rho
10+
//! for ATM, ITM, and OTM calls and puts on a 6-month EUR/USD option, then
11+
//! demonstrates spot delta-parity (`Δ_call − Δ_put = e^(-r_f·T)`) and the
12+
//! `GarmanKohlhagenGreeks` trait.
13+
14+
use optionstratlib::error::greeks::GreeksError;
15+
use optionstratlib::greeks::{
16+
GarmanKohlhagenGreeks, delta_gk, gamma_gk, rho_domestic_gk, rho_foreign_gk, theta_gk, vega_gk,
17+
};
18+
use optionstratlib::model::types::{OptionStyle, OptionType, Side};
19+
use optionstratlib::pricing::garman_kohlhagen;
20+
use optionstratlib::{ExpirationDate, Options};
21+
use positive::{Positive, pos_or_panic};
22+
use rust_decimal::MathematicalOps;
23+
use rust_decimal_macros::dec;
24+
use tracing::info;
25+
26+
fn build_option(s: f64, k: f64, style: OptionStyle) -> Options {
27+
Options::new(
28+
OptionType::European,
29+
Side::Long,
30+
"EURUSD".to_string(),
31+
pos_or_panic!(k),
32+
ExpirationDate::Days(pos_or_panic!(180.0)),
33+
pos_or_panic!(0.10),
34+
Positive::ONE,
35+
pos_or_panic!(s),
36+
dec!(0.045),
37+
style,
38+
pos_or_panic!(0.025),
39+
None,
40+
)
41+
}
42+
43+
fn print_greeks(label: &str, opt: &Options) -> Result<(), Box<dyn std::error::Error>> {
44+
let price = garman_kohlhagen(opt)?;
45+
let delta = delta_gk(opt)?;
46+
let gamma = gamma_gk(opt)?;
47+
let vega = vega_gk(opt)?;
48+
let theta = theta_gk(opt)?;
49+
let rho_d = rho_domestic_gk(opt)?;
50+
let rho_f = rho_foreign_gk(opt)?;
51+
info!(
52+
"{label:<6} S={s:>6} K={k:>6} | price={price:>9.6} Δ={delta:>9.6} Γ={gamma:>9.4} ν={vega:>9.6} Θ={theta:>10.6} ρd={rho_d:>9.6} ρf={rho_f:>9.6}",
53+
label = label,
54+
s = opt.underlying_price.to_dec(),
55+
k = opt.strike_price.to_dec(),
56+
price = price,
57+
delta = delta,
58+
gamma = gamma,
59+
vega = vega,
60+
theta = theta,
61+
rho_d = rho_d,
62+
rho_f = rho_f,
63+
);
64+
Ok(())
65+
}
66+
67+
fn main() -> Result<(), Box<dyn std::error::Error>> {
68+
tracing_subscriber::fmt()
69+
.with_env_filter("info")
70+
.without_time()
71+
.init();
72+
73+
info!("=== Garman–Kohlhagen Greeks (6-month EUR/USD, σ=10%, r_d=4.5%, r_f=2.5%) ===");
74+
info!("");
75+
info!("Calls");
76+
print_greeks("OTM", &build_option(1.05, 1.10, OptionStyle::Call))?;
77+
print_greeks("ATM", &build_option(1.10, 1.10, OptionStyle::Call))?;
78+
print_greeks("ITM", &build_option(1.15, 1.10, OptionStyle::Call))?;
79+
info!("");
80+
info!("Puts");
81+
print_greeks("ITM", &build_option(1.05, 1.10, OptionStyle::Put))?;
82+
print_greeks("ATM", &build_option(1.10, 1.10, OptionStyle::Put))?;
83+
print_greeks("OTM", &build_option(1.15, 1.10, OptionStyle::Put))?;
84+
85+
// ---- Spot delta-parity --------------------------------------------
86+
let call = build_option(1.10, 1.10, OptionStyle::Call);
87+
let put = build_option(1.10, 1.10, OptionStyle::Put);
88+
let dc = delta_gk(&call)?;
89+
let dp = delta_gk(&put)?;
90+
let years = call.expiration_date.get_years()?.to_dec();
91+
let df_rf = (-call.dividend_yield.to_dec() * years).exp();
92+
let diff = dc - dp;
93+
info!("");
94+
info!(
95+
"Spot delta-parity: Δ_call − Δ_put = {diff:.10}, e^(-r_f·T) = {df:.10}, |err| = {err:.2e}",
96+
diff = diff,
97+
df = df_rf,
98+
err = (diff - df_rf).abs(),
99+
);
100+
101+
// ---- Trait usage --------------------------------------------------
102+
struct FxQuote(Options);
103+
impl GarmanKohlhagenGreeks for FxQuote {
104+
fn get_option(&self) -> Result<&Options, GreeksError> {
105+
Ok(&self.0)
106+
}
107+
}
108+
let q = FxQuote(build_option(1.10, 1.10, OptionStyle::Call));
109+
info!("");
110+
info!(
111+
"Trait usage: Δ={delta} ρd={rho_d} ρf={rho_f}",
112+
delta = q.delta_gk()?,
113+
rho_d = q.rho_domestic_gk()?,
114+
rho_f = q.rho_foreign_gk()?,
115+
);
116+
117+
Ok(())
118+
}

0 commit comments

Comments
 (0)