Skip to content

feat(greeks): Black-76 Greeks (delta/gamma/vega/theta/rho)#402

Merged
joaquinbejar merged 1 commit into
mainfrom
pricing/black-76-greeks
Apr 27, 2026
Merged

feat(greeks): Black-76 Greeks (delta/gamma/vega/theta/rho)#402
joaquinbejar merged 1 commit into
mainfrom
pricing/black-76-greeks

Conversation

@joaquinbejar
Copy link
Copy Markdown
Owner

Summary

Closed-form Greeks for the Black-76 (Black 1976) pricing model, completing the futures/forwards story started in #398. Closes #400.

Black-76 prices options on futures, forwards, swaptions, caps/floors and commodity futures — anywhere the underlying is a forward price F with the cost of carry already baked in. The Greeks share a single e^(-rT) discount factor across both legs and ignore dividend_yield (because F is carry-adjusted).

Public surface

New module src/greeks/black_76.rs:

  • pub fn delta_b76(option: &Options) -> Result<Decimal, GreeksError>
  • pub fn gamma_b76(option: &Options) -> Result<Decimal, GreeksError>
  • pub fn vega_b76(option: &Options) -> Result<Decimal, GreeksError> — per 1 % vol
  • pub fn theta_b76(option: &Options) -> Result<Decimal, GreeksError> — per calendar day
  • pub fn rho_b76(option: &Options) -> Result<Decimal, GreeksError> — per 1 % rate
  • pub trait Black76Greeks mirroring the existing Black76 pricing trait

calculate_d_values_black_76 was promoted from pub(crate) to pub so callers and the new module can share the helper.

Conventions match the BSM Greeks module:

  • All math is Decimal end-to-end via d_mul/d_sub/d_div.
  • Quantity scales linearly; Side::Short flips the delta sign only (gamma/vega/theta/rho stay sign-agnostic, same as greeks::gamma/vega/theta/rho).
  • Vega ÷ 100 (per 1 percentage point of vol).
  • Theta ÷ 365 (per calendar day).
  • Rho ÷ 100 (per 1 percentage point of rate).
  • tracing::instrument on every entry point.

Formulas

The implementation follows Hull, Options, Futures and Other Derivatives (10th ed., Ch. 18) rigorously. The issue's formula for theta omitted the + r·F·e^(-rT)·N(d1) term and the formula for rho was the BSM-style two-leg expression rather than the simpler Black-76 form. The complete Hull forms are:

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

Γ      = e^(-rT) · n(d1) / (F · σ · √T)

ν      = F · e^(-rT) · n(d1) · √T

Θ_call = -F·e^(-rT)·n(d1)·σ/(2√T) + r·F·e^(-rT)·N(d1) - r·K·e^(-rT)·N(d2)
Θ_put  = -F·e^(-rT)·n(d1)·σ/(2√T) - r·F·e^(-rT)·N(-d1) + r·K·e^(-rT)·N(-d2)

ρ      = -T · price_b76     (the only place r appears in Black-76 is e^(-rT))

The ρ = -T · price identity is asserted analytically by tests for both call and put.

Tests (21, all passing)

#[cfg(test)] mod tests in src/greeks/black_76.rs:

  • Delta range and identity:
    • Δ_call ∈ (0,1), Δ_put ∈ (-1,0) across moneyness
    • Δ_call − Δ_put = e^(-rT) to 1e-9
  • Positivity / symmetry:
    • Γ > 0, ν > 0 for both call and put across strikes
    • Γ_call = Γ_put, ν_call = ν_put (Black-76 invariant)
  • Theta sanity:
    • long ATM call and put both decay (Θ < 0)
  • Rho identity:
    • ρ = -T · price / 100 for both call and put
  • BSM cross-check under the S = F·e^(-rT), q = 0 transform (1e-9):
    • Δ_b76 = e^(-rT) · Δ_bsm
    • ν_b76 = ν_bsm
    • Γ_b76 = e^(-2rT) · Γ_bsm
  • Hull reference (Ch. 18 ATM call, F=K=20, r=0.09, T≈1/3, σ=0.25): Δ ≈ 0.5132
  • Error paths:
    • zero volatility on every Greek → GreeksError
    • American / Bermuda / exotic → GreeksError::Pricing(UnsupportedOptionType)
  • Trait Black76Greeks round-trips against the free functions
  • Side::Short negates delta
  • Quantity scales linearly

Example

examples/examples_pricing/src/bin/black_76_greeks.rs — 6-month crude-oil futures (σ=30 %, r=4.5 %), walks ATM / ITM / OTM calls and puts, prints all Greeks, demonstrates the Δ_call − Δ_put = e^(-rT) identity (zero residual) and trait usage on a wrapper type.

Acceptance criteria (from #400)

  • Delta monotonicity: call ∈ [0,1], put ∈ [-1,0]; Δ_call − Δ_put = e^(-rT)
  • Gamma > 0
  • Vega > 0
  • Hull reference values to 1e-3
  • Cross-check vs BSM with S = F·e^(-rT), q = 0 to 1e-9 (post-transform relationships)
  • Zero volatility → error
  • American / Bermuda / exotic → UnsupportedOptionType
  • cargo clippy --all-targets --all-features --workspace -- -D warnings clean
  • cargo fmt --all --check clean
  • cargo test and cargo test --features plotly clean (lib: 3826 / 3822 passed). The static_export pass is pre-existing red on origin/main due to missing chromium for headless render — unrelated to this PR.
  • cargo build --release clean
  • All pub items documented

Test plan

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

Closed-form Greeks for the Black-76 (Black 1976) model, complementing the
pricing kernel landed in #398. Black-76 prices options on futures, forwards,
swaptions, caps/floors, and commodity futures — anywhere the underlying is a
forward price `F` with the cost of carry already baked in. The Greeks share
the same `e^(-rT)` discount factor across both legs and ignore
`dividend_yield` (since `F` is carry-adjusted).

Public surface (`src/greeks/black_76.rs`):
- `delta_b76`, `gamma_b76`, `vega_b76`, `theta_b76`, `rho_b76`
- `Black76Greeks` trait mirroring the `Black76` pricing trait — implementors
  provide `get_option(&self)` and inherit default delegations to the free
  functions above.

Units mirror the BSM module: vega per 1 % vol, theta per calendar day
(annual ÷ 365), rho per 1 % rate (annual ÷ 100). Quantity scales linearly;
`Side::Short` flips the delta sign only (consistent with `greeks::delta`).

Promoted `calculate_d_values_black_76` from `pub(crate)` to `pub` so external
crates and the new module can share the helper.

Formulas use the complete Hull (10th ed., Ch. 18) expressions. Note that the
issue's formula for theta and rho omitted the `r·F·e^(-rT)·N(d1)` and
F-leg discount terms; this implementation follows Hull rigorously and the
relationship `ρ = -T · price` is verified analytically and by test.

Tests (21, all passing):
- Delta range: call ∈ (0,1), put ∈ (-1,0)
- Identity `Δ_call − Δ_put = e^(-rT)` to 1e-9
- Gamma > 0, Vega > 0 across multiple strikes
- Gamma and Vega are call/put symmetric
- Theta < 0 for long ATM call/put (decay)
- `ρ = -T · price / 100` analytic identity
- BSM cross-check via `S = F·e^(-rT)`, `q = 0`:
  - `Δ_b76 = e^(-rT) · Δ_bsm` to 1e-9
  - `ν_b76 = ν_bsm` to 1e-9
  - `Γ_b76 = e^(-2rT) · Γ_bsm` to 1e-9
- Hull ATM-call reference (F=K=20, r=0.09, T≈1/3, σ=0.25) → Δ ≈ 0.5132
- Zero volatility → error
- American / Bermuda / exotic → `GreeksError::Pricing(UnsupportedOptionType)`
- Trait impl, side negation, quantity scaling

Example: `examples/examples_pricing/src/bin/black_76_greeks.rs` walks ATM /
ITM / OTM calls and puts on a 6-month CL futures contract, prints all
Greeks, demonstrates the call-minus-put identity and trait usage.

Closes #400.
@joaquinbejar joaquinbejar merged commit b9623a1 into main Apr 27, 2026
13 checks passed
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 27, 2026

Codecov Report

❌ Patch coverage is 99.18699% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/greeks/black_76.rs 99.18% 1 Missing ⚠️
Files with missing lines Coverage Δ
src/greeks/utils.rs 82.60% <100.00%> (ø)
src/greeks/black_76.rs 99.18% <99.18%> (ø)

... 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 Black-76 pricing model (pricing/greeks)

1 participant