feat(greeks): Garman-Kohlhagen FX Greeks (delta/gamma/vega/theta/rho_d/rho_f)#403
feat(greeks): Garman-Kohlhagen FX Greeks (delta/gamma/vega/theta/rho_d/rho_f)#403joaquinbejar merged 3 commits intomainfrom
Conversation
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.
9f9e28c to
d5b6f04
Compare
There was a problem hiding this comment.
Pull request overview
Adds closed-form FX Greeks for the Garman–Kohlhagen model (delta/gamma/vega/theta and split domestic/foreign rho), completing the FX greeks surface started by the earlier GK pricing work.
Changes:
- Introduces
src/greeks/garman_kohlhagen.rswith GK-specific closed-form implementations and aGarmanKohlhagenGreekstrait. - Re-exports the new GK Greeks from
src/greeks/mod.rs. - Adds a runnable example binary (
garman_kohlhagen_greeks) and wires it into the examples workspace.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/greeks/mod.rs |
Registers and re-exports the new GK Greeks API. |
src/greeks/garman_kohlhagen.rs |
Implements GK closed-form Greeks, plus unit tests and a trait wrapper. |
examples/examples_pricing/src/bin/garman_kohlhagen_greeks.rs |
Demonstrates pricing + GK Greeks output and delta-parity check. |
examples/examples_pricing/Cargo.toml |
Adds the new example binary target. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- 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.
|
Thanks for the review. Both inline comments addressed in d5b7c2e — added BSM-style |
|
Thanks for catching those edge cases. Fixed both the T=0 handling (now mirrors BSM behavior) and the theta calculation to use checked arithmetic. All tests pass including the new T=0 scenarios. |
Codecov Report❌ Patch coverage is
... and 1 file with indirect coverage changes 🚀 New features to boost your workflow:
|
Summary
Closed-form Greeks for the Garman–Kohlhagen (1983) FX option pricing model, completing the FX story started in #399. Closes #401.
GK prices European options on a foreign-exchange spot
S(domestic per unit of foreign), with the foreign currency earning interest at rater_f. Structurally GK ≡ BSM withq = r_f, but FX options carry two rate sensitivities (domestic and foreign) instead of one.Public surface
New module
src/greeks/garman_kohlhagen.rs:pub fn delta_gk(option: &Options) -> Result<Decimal, GreeksError>pub fn gamma_gk(option: &Options) -> Result<Decimal, GreeksError>pub fn vega_gk(option: &Options) -> Result<Decimal, GreeksError>— per 1 % volpub fn theta_gk(option: &Options) -> Result<Decimal, GreeksError>— per calendar daypub fn rho_domestic_gk(option: &Options) -> Result<Decimal, GreeksError>— per 1 %r_dpub fn rho_foreign_gk(option: &Options) -> Result<Decimal, GreeksError>— per 1 %r_fpub trait GarmanKohlhagenGreeksmirroring the existingGarmanKohlhagenpricing traitFX field mapping:
risk_free_rate → r_d,dividend_yield → r_f,underlying_price → S. Greek units mirror the BSM module: vega ÷ 100, theta ÷ 365, both rhos ÷ 100. Quantity scales linearly;Side::Shortflips the delta sign only (consistent withgreeks::delta).Formulas
Note on BSM Greeks
This module implements the carry-adjusted Garman–Kohlhagen formulas directly using the
b = r_d − r_fcost-of-carry term ind1/d2. It does not delegate to the existing BSM Greeks (crate::greeks::delta/gamma/...) because those computed1fromrisk_free_rateonly and then multiply the result bye^(-qT)— a mismatch between the d-values and the discount factor that yields incorrect numbers whendividend_yield ≠ 0. The pricing kernels are unaffected (they go throughcalculate_d_values, which does include−qin the drift). A separate issue should be filed to fix the BSM Greeks; for GK we needed correct FX numbers, hence the standalone implementation.Tests (18, all passing)
#[cfg(test)] mod testsinsrc/greeks/garman_kohlhagen.rs:Δ_call ∈ (0, e^(-r_f·T)),Δ_put ∈ (-e^(-r_f·T), 0)Δ_call − Δ_put = e^(-r_f·T)to 1e-9Γ > 0,ν > 0across moneyness for both call and putΓ_call = Γ_put,ν_call = ν_put(Black–Scholes invariant)q = 0(bit-exact) — the only case where the buggy BSM Greeks agreeGreeksErrorGreeksError::Pricing(UnsupportedOptionType)GarmanKohlhagenGreeksround-trips against the free functionsSide::Shortnegates deltaExample
examples/examples_pricing/src/bin/garman_kohlhagen_greeks.rs— 6-month EUR/USD (σ=10 %, r_d=4.5 %, r_f=2.5 %), walks ATM / ITM / OTM calls and puts, prints all Greeks, demonstratesΔ_call − Δ_put = e^(-r_f·T)(zero residual) and trait usage on a wrapper type.Acceptance criteria (from #401)
q = 0(bit-exact). Note: the issue's "1e-9 cross-check atq = r_f" can only hold once the existing BSM Greeks are corrected; the discrepancy is documented above and limited to that one criterion.UnsupportedOptionTypecargo clippy --all-targets --all-features --workspace -- -D warningscleancargo fmt --all --checkcleancargo test --lib --all-featuresclean (3819 passed)cargo build --releasecleanpubitems documentedTest plan
cargo test --lib --all-features— 3819 passedcargo clippy --all-targets --all-features --workspace -- -D warnings— cleancargo fmt --all --check— cleancargo build --release— cleancargo run --bin garman_kohlhagen_greeks -p examples_pricing— identity holds to zero