# black-76
[](https://crates.io/crates/black-76)
[](https://docs.rs/black-76)
[](#license)
Black-76 closed-form pricing, Greeks, and implied-volatility solver for
European options on **forwards and futures**. Synchronous `f64` math, no
allocations on the hot path, no logging dependency.
The Black-76 model (Fischer Black, *The Pricing of Commodity Contracts*,
Journal of Financial Economics, 1976) prices options whose underlying is a
forward contract rather than a spot asset. Typical applications:
Eurodollar futures options, FX forward options, commodity-futures options,
and crypto perpetual-futures options.
## Features
- Closed-form **call / put prices**, **vega**, and **intrinsic value**.
- First-order **Greeks**: delta, gamma, vega (per 1%), theta (per day), rho (per 1%).
- **Implied-volatility solver**: Newton-Raphson with a Brent's-method
fallback when vega is too small (deep OTM / near-expiry). Convergence is
decided in **volatility space**, so it holds at any forward scale.
- Explicit **convergence contract**: a [`SolverResult`] whose `iv` is `NaN`
(and `converged = false`) on **every** non-converged path, plus a
[`SolverStatus`] enum giving the exact reason (near-expiry, below intrinsic,
non-positive price, no root in range, not identifiable, max iterations).
No silently clamped boundary values pretending to be solutions.
- Optional **per-expiry vol smile** (`vol-surface`) with linear
interpolation and flat extrapolation.
- Optional **risk-neutral probability extraction** (`digital`) via
call-spread replication and `N(d2)` with skew adjustment.
Single runtime dependency: `statrs` (for the standard normal CDF / PDF).
No `chrono`, no `tracing`, no async runtime, no `serde` unless you opt in.
## Quick start
```toml
[dependencies]
black-76 = "0.1"
```
```rust
use black_76::{call_price, solve_iv, SolverConfig};
// Price an ATM call: F=100, K=100, T=1 year, sigma=20%, r=0
let c = call_price(100.0, 100.0, 1.0, 0.20, 0.0);
assert!((c - 7.9656).abs() < 1e-3);
// Recover the IV from a market price
let cfg = SolverConfig::default();
let result = solve_iv(7.9656, 100.0, 100.0, 1.0, 0.0, true, &cfg);
assert!(result.converged);
assert!((result.iv - 0.20).abs() < 1e-4);
```
### Typo-resistant inputs
Positional `f64` arguments are easy to mis-order. [`BlackInputs`] and
[`IvQuery`] name every field (and are `const`-constructible):
```rust
use black_76::{BlackInputs, IvQuery, SolverConfig};
let inputs = BlackInputs::new(100.0, 100.0, 1.0, 0.20, 0.0); // f, k, t, sigma, r
let c = inputs.call_price();
let g = inputs.greeks(true); // delta, gamma, vega, theta, rho
let iv = IvQuery::new(c, 100.0, 100.0, 1.0, 0.0, true)
.solve(&SolverConfig::default());
assert!(iv.converged);
```
### Convergence checking is mandatory
```rust
use black_76::{solve_iv, SolverConfig};
let cfg = SolverConfig::default();
let result = solve_iv(/* implausibly high market price */ 1_000.0,
100.0, 100.0, 1.0, 0.0, true, &cfg);
if result.converged {
println!("IV = {}", result.iv);
} else {
eprintln!("no IV ({:?}); result.iv is NaN", result.status);
}
```
Skipping the `converged` check is a bug: `iv` is `f64::NAN` on every
non-converged path, and [`SolverStatus`] tells you which one.
## Feature flags
| *(none)* | yes | core pricing, Greeks, IV solver |
| `serde` | no | `Serialize` / `Deserialize` derives on public types |
| `vol-surface` | no | per-expiry [`VolSmile`] with interpolation |
| `digital` | no | risk-neutral probability extraction (requires `vol-surface`) |
```toml
black-76 = { version = "0.1", features = ["vol-surface", "digital", "serde"] }
```
## Examples
Six runnable examples under `examples/`:
```bash
cargo run --example atm_price
cargo run --example solve_iv
cargo run --example put_call_parity
cargo run --example greeks
cargo run --example vol_smile --features vol-surface
cargo run --example digital_prob --features digital
```
## Benchmarks
Two Criterion benches under `benches/`:
```bash
cargo bench --bench pricing
cargo bench --bench iv_solver
```
Indicative single-thread timings (Criterion median, `--release`, AMD Ryzen 7
PRO 8840U). Treat them as ballpark rather than a guarantee:
| `d1_d2` | ~20 ns |
| `call_price` (ATM) | ~68 ns |
| `put_price` (ATM) | ~63 ns |
| `vega` (ATM) | ~41 ns |
| `solve_iv` (Newton path) | ~320 ns |
| `solve_iv` (Brent fallback, deep OTM) | ~3300 ns |
## Conventions
- **Vega** is reported per 1% absolute change in IV (trader convention),
i.e. the raw `dC/dsigma` divided by 100.
- **Theta** is reported per calendar day with `year = 365.25` days.
- **Time** is in years.
- **Rate** is continuous compounding.
- All numerics are `f64`. The crate exposes no `Decimal`-typed APIs.
## Comparison with alternatives
| Language | pure Rust | Python + C ext | C++ via FFI | Rust |
| Runtime deps | `statrs` only | NumPy/SciPy | QuantLib | varies |
| Async / runtime | none | n/a | n/a | none |
| Model focus | Black-76 (forwards/futures) | Black / BS / -76 | everything | mostly Black-Scholes |
| IV solver | Newton + Brent, **typed `SolverStatus`** | Newton / `lets_be_rational` | various | varies |
| Non-convergence | `iv = NaN` + status enum | exceptions | exceptions | varies by crate |
| Greeks | delta, gamma, vega, theta, rho | full | full | crate-dependent |
| Smile / digitals | optional (`vol-surface`, `digital`) | n/a | full surfaces | n/a |
| `forbid(unsafe_code)` | yes | n/a | no (FFI) | varies |
| MSRV / semver | 1.85, `#[non_exhaustive]` API | n/a | n/a | varies |
The table is positioning, not a precise feature audit; check each project's
current release for specifics.
`black-76` is deliberately narrow: a synchronous, dependency-light
forward-options pricer with a typed convergence contract, not a full
derivatives library. Reach for QuantLib when you need exotic products or full
curve/surface machinery.
## Background
This began as the pricing core of a crypto-options arbitrage system, where I
needed Black-76 prices, Greeks, and an implied-vol solver that were
allocation-free on the hot path and explicit about non-convergence: near
expiry, deep out-of-the-money, or at exchange-scale forwards (BTC ~100k) where
an absolute price tolerance stops being meaningful. I pulled the pure-math
modules out of that project and generalized them into this standalone `f64`
crate.
## MSRV
Rust **1.85** (Edition 2024).
## References
- Hull, *Options, Futures, and Other Derivatives*, 10th ed., §17.
- Haug, *The Complete Guide to Option Pricing Formulas*, 2nd ed., §1.1.
- Brent, *Algorithms for Minimization without Derivatives*, 1973, Ch. 4.
- Brenner & Subrahmanyam, *A Simple Formula to Compute the Implied
Standard Deviation*, Financial Analysts Journal, 1988.
## License
Dual-licensed under either of
- Apache License, Version 2.0 ([`LICENSE-APACHE`](LICENSE-APACHE) or
<http://www.apache.org/licenses/LICENSE-2.0>)
- MIT License ([`LICENSE-MIT`](LICENSE-MIT) or
<http://opensource.org/licenses/MIT>)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in the work by you, as defined in the Apache-2.0
license, shall be dual-licensed as above, without any additional terms or
conditions.
[`SolverResult`]: https://docs.rs/black-76/latest/black_76/struct.SolverResult.html
[`SolverStatus`]: https://docs.rs/black-76/latest/black_76/enum.SolverStatus.html
[`BlackInputs`]: https://docs.rs/black-76/latest/black_76/struct.BlackInputs.html
[`IvQuery`]: https://docs.rs/black-76/latest/black_76/struct.IvQuery.html
[`VolSmile`]: https://docs.rs/black-76/latest/black_76/vol_surface/struct.VolSmile.html