Skip to content

Commit b9623a1

Browse files
authored
feat(greeks): add Black-76 Greeks (delta/gamma/vega/theta/rho) for #400 (#402)
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.
1 parent 6cc3226 commit b9623a1

5 files changed

Lines changed: 866 additions & 5 deletions

File tree

examples/examples_pricing/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] }
1616
name = "black_76"
1717
path = "src/bin/black_76.rs"
1818

19+
[[bin]]
20+
name = "black_76_greeks"
21+
path = "src/bin/black_76_greeks.rs"
22+
1923
[[bin]]
2024
name = "garman_kohlhagen"
2125
path = "src/bin/garman_kohlhagen.rs"
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/******************************************************************************
2+
Author: Joaquín Béjar García
3+
Email: jb@taunais.com
4+
Date: 2026-04-27
5+
******************************************************************************/
6+
7+
//! Black-76 Greeks example — commodity futures scenario.
8+
//!
9+
//! Computes delta, gamma, vega, theta, and rho for ATM, ITM, and OTM calls and
10+
//! puts on a 6-month crude-oil futures contract. Prints the values together
11+
//! with the Hull-style sanity identity `Δ_call − Δ_put = e^(-rT)` and shows
12+
//! both free-function and trait-based usage.
13+
14+
use optionstratlib::error::greeks::GreeksError;
15+
use optionstratlib::greeks::{Black76Greeks, delta_b76, gamma_b76, rho_b76, theta_b76, vega_b76};
16+
use optionstratlib::model::types::{OptionStyle, OptionType, Side};
17+
use optionstratlib::pricing::black_76;
18+
use optionstratlib::{ExpirationDate, Options};
19+
use positive::{Positive, pos_or_panic};
20+
use rust_decimal::{Decimal, MathematicalOps};
21+
use rust_decimal_macros::dec;
22+
use tracing::info;
23+
24+
fn build_option(f: f64, k: f64, style: OptionStyle) -> Options {
25+
Options::new(
26+
OptionType::European,
27+
Side::Long,
28+
"CL".to_string(),
29+
pos_or_panic!(k),
30+
ExpirationDate::Days(pos_or_panic!(180.0)),
31+
pos_or_panic!(0.30),
32+
Positive::ONE,
33+
pos_or_panic!(f),
34+
dec!(0.045),
35+
style,
36+
pos_or_panic!(0.0),
37+
None,
38+
)
39+
}
40+
41+
fn print_greeks(label: &str, opt: &Options) -> Result<(), Box<dyn std::error::Error>> {
42+
let price = black_76(opt)?;
43+
let delta = delta_b76(opt)?;
44+
let gamma = gamma_b76(opt)?;
45+
let vega = vega_b76(opt)?;
46+
let theta = theta_b76(opt)?;
47+
let rho = rho_b76(opt)?;
48+
info!(
49+
"{label:<6} F={F:>6} K={K:>6} | price={price:>9.4} Δ={delta:>8.4} Γ={gamma:>9.6} ν={vega:>8.4} Θ={theta:>9.4} ρ={rho:>9.4}",
50+
label = label,
51+
F = opt.underlying_price.to_dec(),
52+
K = opt.strike_price.to_dec(),
53+
price = price,
54+
delta = delta,
55+
gamma = gamma,
56+
vega = vega,
57+
theta = theta,
58+
rho = rho,
59+
);
60+
Ok(())
61+
}
62+
63+
fn main() -> Result<(), Box<dyn std::error::Error>> {
64+
tracing_subscriber::fmt()
65+
.with_env_filter("info")
66+
.without_time()
67+
.init();
68+
69+
info!("=== Black-76 Greeks (6-month CL futures, σ=30%, r=4.5%) ===");
70+
info!("");
71+
info!("Calls");
72+
print_greeks("OTM", &build_option(70.0, 80.0, OptionStyle::Call))?;
73+
print_greeks("ATM", &build_option(75.0, 75.0, OptionStyle::Call))?;
74+
print_greeks("ITM", &build_option(80.0, 70.0, OptionStyle::Call))?;
75+
info!("");
76+
info!("Puts");
77+
print_greeks("ITM", &build_option(70.0, 80.0, OptionStyle::Put))?;
78+
print_greeks("ATM", &build_option(75.0, 75.0, OptionStyle::Put))?;
79+
print_greeks("OTM", &build_option(80.0, 70.0, OptionStyle::Put))?;
80+
81+
// ---- Identity: Δ_call − Δ_put = e^(-rT) at any strike --------------
82+
info!("");
83+
let call = build_option(75.0, 75.0, OptionStyle::Call);
84+
let put = build_option(75.0, 75.0, OptionStyle::Put);
85+
let dc = delta_b76(&call)?;
86+
let dp = delta_b76(&put)?;
87+
let years = call.expiration_date.get_years()?.to_dec();
88+
let df = (-call.risk_free_rate * years).exp();
89+
let diff = dc - dp;
90+
info!(
91+
"Identity check: Δ_call − Δ_put = {diff:.10}, e^(-rT) = {df:.10}, |err| = {err:.2e}",
92+
diff = diff,
93+
df = df,
94+
err = (diff - df).abs(),
95+
);
96+
97+
// ---- Trait usage ----------------------------------------------------
98+
struct FutureContract(Options);
99+
impl Black76Greeks for FutureContract {
100+
fn get_option(&self) -> Result<&Options, GreeksError> {
101+
Ok(&self.0)
102+
}
103+
}
104+
let contract = FutureContract(build_option(75.0, 75.0, OptionStyle::Call));
105+
info!("");
106+
info!(
107+
"Trait usage on wrapper type: Δ={delta} Γ={gamma}",
108+
delta = contract.delta_b76()?,
109+
gamma = contract.gamma_b76()?,
110+
);
111+
112+
// Suppress unused-import warning on Decimal in case formatting changes.
113+
let _: Decimal = df;
114+
Ok(())
115+
}

0 commit comments

Comments
 (0)