Skip to content

feat(greeks): Garman-Kohlhagen FX Greeks (delta/gamma/vega/theta/rho_d/rho_f)#403

Merged
joaquinbejar merged 3 commits intomainfrom
pricing/garman-kohlhagen-greeks
Apr 27, 2026
Merged

feat(greeks): Garman-Kohlhagen FX Greeks (delta/gamma/vega/theta/rho_d/rho_f)#403
joaquinbejar merged 3 commits intomainfrom
pricing/garman-kohlhagen-greeks

Conversation

@joaquinbejar
Copy link
Copy Markdown
Owner

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 rate r_f. Structurally GK ≡ BSM with q = 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 % vol
  • pub fn theta_gk(option: &Options) -> Result<Decimal, GreeksError> — per calendar day
  • pub fn rho_domestic_gk(option: &Options) -> Result<Decimal, GreeksError> — per 1 % r_d
  • pub fn rho_foreign_gk(option: &Options) -> Result<Decimal, GreeksError> — per 1 % r_f
  • pub trait GarmanKohlhagenGreeks mirroring the existing GarmanKohlhagen pricing trait

FX 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::Short flips the delta sign only (consistent with greeks::delta).

Formulas

d1 = [ln(S/K) + (r_d − r_f + σ²/2)·T] / (σ·√T)
d2 = d1 − σ·√T

Δ_call = e^(-r_f·T) · N(d1)
Δ_put  = -e^(-r_f·T) · N(-d1)

Γ      = e^(-r_f·T) · n(d1) / (S · σ · √T)

ν      = S · e^(-r_f·T) · n(d1) · √T

Θ_call = -S·e^(-r_f T)·n(d1)·σ/(2√T) − r_d·K·e^(-r_d T)·N(d2)  + r_f·S·e^(-r_f T)·N(d1)
Θ_put  = -S·e^(-r_f T)·n(d1)·σ/(2√T) + r_d·K·e^(-r_d T)·N(-d2) − r_f·S·e^(-r_f T)·N(-d1)

ρ_d_call =  K · T · e^(-r_d·T) · N(d2)
ρ_d_put  = -K · T · e^(-r_d·T) · N(-d2)

ρ_f_call = -S · T · e^(-r_f·T) · N(d1)
ρ_f_put  =  S · T · e^(-r_f·T) · N(-d1)

Note on BSM Greeks

This module implements the carry-adjusted Garman–Kohlhagen formulas directly using the b = r_d − r_f cost-of-carry term in d1/d2. It does not delegate to the existing BSM Greeks (crate::greeks::delta/gamma/...) because those compute d1 from risk_free_rate only and then multiply the result by e^(-qT) — a mismatch between the d-values and the discount factor that yields incorrect numbers when dividend_yield ≠ 0. The pricing kernels are unaffected (they go through calculate_d_values, which does include −q in 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 tests in src/greeks/garman_kohlhagen.rs:

  • 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
  • Positivity / symmetry:
    • Γ > 0, ν > 0 across moneyness for both call and put
    • Γ_call = Γ_put, ν_call = ν_put (Black–Scholes invariant)
  • Rho signs: long call → +ρ_d, -ρ_f; long put → -ρ_d, +ρ_f
  • BSM equivalence at q = 0 (bit-exact) — the only case where the buggy BSM Greeks agree
  • Theta vs numerical price difference (1-day bump, 5e-3 tolerance)
  • 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
  • Error paths:
    • zero volatility on every Greek → GreeksError
    • 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 — 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)

  • Delta monotonicity (call ∈ (0, e^(-r_f·T)), put ∈ (-e^(-r_f·T), 0)) and parity
  • Gamma > 0
  • Vega > 0
  • Rho signs (long call: +ρ_d, -ρ_f; long put: -ρ_d, +ρ_f)
  • Hull / Wystup-style FX reference values to 1e-3
  • Cross-check vs BSM Greeks at q = 0 (bit-exact). Note: the issue's "1e-9 cross-check at q = r_f" can only hold once the existing BSM Greeks are corrected; the discrepancy is documented above and limited to that one criterion.
  • Zero volatility → error
  • American / Bermuda / exotic → UnsupportedOptionType
  • Theta vs numerical differentiation
  • cargo clippy --all-targets --all-features --workspace -- -D warnings clean
  • cargo fmt --all --check clean
  • cargo test --lib --all-features clean (3819 passed)
  • cargo build --release clean
  • All pub items documented

Test plan

  • cargo test --lib --all-features — 3819 passed
  • cargo clippy --all-targets --all-features --workspace -- -D warnings — clean
  • cargo fmt --all --check — clean
  • cargo build --release — clean
  • cargo run --bin garman_kohlhagen_greeks -p examples_pricing — identity holds to zero
  • CI green

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.
@joaquinbejar joaquinbejar force-pushed the pricing/garman-kohlhagen-greeks branch from 9f9e28c to d5b6f04 Compare April 27, 2026 11:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.rs with GK-specific closed-form implementations and a GarmanKohlhagenGreeks trait.
  • 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.

Comment thread src/greeks/garman_kohlhagen.rs
Comment thread src/greeks/garman_kohlhagen.rs
- 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.
@joaquinbejar
Copy link
Copy Markdown
Owner Author

Thanks for the review. Both inline comments addressed in d5b7c2e — added BSM-style T==0 short-circuiting and switched the theta path to d_add for consistent checked arithmetic.

@joaquinbejar
Copy link
Copy Markdown
Owner Author

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.

@joaquinbejar joaquinbejar merged commit f3ac6cd into main Apr 27, 2026
12 checks passed
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 27, 2026

Codecov Report

❌ Patch coverage is 96.47059% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/greeks/garman_kohlhagen.rs 96.47% 6 Missing ⚠️
Files with missing lines Coverage Δ
src/lib.rs 100.00% <ø> (ø)
src/greeks/garman_kohlhagen.rs 96.47% <96.47%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Greeks for Garman-Kohlhagen FX pricing model (pricing/greeks)

2 participants