FastMM documentation
FastMM C++ API 0.6.0
Public headers, tier 1 and tier 2
Loading...
Searching...
No Matches
black76.hpp
Go to the documentation of this file.
1#pragma once
2// Black-76 option pricing, greeks and implied volatility (options on a forward / futures price).
3//
4// d1 = (ln(F / K) + sigma^2 T / 2) / (sigma sqrt(T)), d2 = d1 - sigma sqrt(T), D = e^{-rT}
5// call = D (F N(d1) - K N(d2)), put = D (K N(-d2) - F N(-d1))
6//
7// Conventions (per unit of the underlying; multiply by position size and contract multiplier):
8// delta = dV/dF call D N(d1), put -D N(-d1)
9// gamma = d2V/dF2 D n(d1) / (F sigma sqrt(T))
10// vega = dV/dsigma D F n(d1) sqrt(T) (per 1.00 of vol: /100 for a vol point)
11// theta = dV/dt -D F n(d1) sigma / (2 sqrt(T)) + r V (per year of calendar time)
12// rho = dV/dr -T V (Black-76: the forward does not move)
13// Reference: F. Black, "The pricing of commodity contracts", J. Financial Economics 3 (1976);
14// E. G. Haug, "The Complete Guide to Option Pricing Formulas" (2nd ed.), Black-76 example
15// F = K = 19, T = 0.75, r = 0.10, sigma = 0.28 -> call = put = 1.7011
16// (tests/core/black76_test.cpp).
17//
18// Deribit (ticker channel, verified against a recorded testnet ticker) reports the same model
19// with r = interest_rate (0): delta = D N(d1), gamma per USD, vega / 100 (per vol point), theta /
20// 365 (per day) in USD, and rho as the spot-model K T N(d2) / 100 rather than Black-76 rho.
21// Inverse (coin-margined) options are quoted in the base coin: price_coin = V / F, and their
22// delta in coin terms is delta - price_coin (the premium itself moves with F).
23//
24// Everything is noexcept, allocation-free and uses only <cmath>; the implied-volatility solver is
25// a bracketed Newton iteration with bisection fallback and a hard iteration bound.
26#include <algorithm>
27#include <cmath>
28#include <cstdint>
29#include <numbers>
30
31namespace fastmm::options {
32
33inline constexpr double kSecondsPerYear = 365.0 * 86400.0; // ACT/365, as Deribit's theta
34inline constexpr double kMinVol = 1e-4; // 0.01 % annualised
35inline constexpr double kMaxVol = 10.0; // 1000 % annualised
37enum class CallPut : std::uint8_t { Call = 0, Put = 1 };
38
39// Standard normal density and distribution. erfc keeps the far tail accurate (no 1 - N(x)).
40[[nodiscard]] inline double norm_pdf(double x) noexcept {
41 return 0.398942280401432677939946 * std::exp(-0.5 * x * x); // 1 / sqrt(2 pi)
43[[nodiscard]] inline double norm_cdf(double x) noexcept {
44 return 0.5 * std::erfc(-x * 0.707106781186547524400844); // 1 / sqrt(2)
46
47// Year fraction between two epoch-ns timestamps (ACT/365); negative once expired.
48[[nodiscard]] constexpr double year_fraction(std::int64_t expiry_ns, std::int64_t now_ns) noexcept {
49 return static_cast<double>(expiry_ns - now_ns) / 1e9 / kSecondsPerYear;
51
52struct Greeks {
53 double price = 0.0;
54 double delta = 0.0;
55 double gamma = 0.0;
56 double vega = 0.0;
57 double theta = 0.0;
58 double rho = 0.0;
59};
61// Price and greeks. Degenerate inputs (T <= 0 or sigma <= 0) return the discounted intrinsic
62// value with a step delta and zero gamma/vega/theta; F <= 0 or K <= 0 return all zeros.
63[[nodiscard]] inline Greeks black76(CallPut cp,
64 double forward,
65 double strike,
66 double t_years,
67 double sigma,
68 double rate = 0.0) noexcept {
69 Greeks g;
70 if (!(forward > 0.0) || !(strike > 0.0)) return g;
71 const double df = std::exp(-rate * (t_years > 0.0 ? t_years : 0.0));
72 const bool call = cp == CallPut::Call;
73 if (!(t_years > 0.0) || !(sigma > 0.0)) {
74 const double intrinsic = call ? forward - strike : strike - forward;
75 if (intrinsic > 0.0) {
76 g.price = df * intrinsic;
77 g.delta = call ? df : -df;
78 }
79 return g;
80 }
81 const double sqrt_t = std::sqrt(t_years);
82 const double sd = sigma * sqrt_t;
83 const double d1 = (std::log(forward / strike) + 0.5 * sd * sd) / sd;
84 const double d2 = d1 - sd;
85 const double pdf = norm_pdf(d1);
86 if (call) {
87 g.price = df * (forward * norm_cdf(d1) - strike * norm_cdf(d2));
88 g.delta = df * norm_cdf(d1);
89 } else {
90 g.price = df * (strike * norm_cdf(-d2) - forward * norm_cdf(-d1));
91 g.delta = -df * norm_cdf(-d1);
92 }
93 g.gamma = df * pdf / (forward * sd);
94 g.vega = df * forward * pdf * sqrt_t;
95 g.theta = -df * forward * pdf * sigma / (2.0 * sqrt_t) + rate * g.price;
96 g.rho = -t_years * g.price;
97 return g;
98}
99
100[[nodiscard]] inline double black76_price(CallPut cp,
101 double forward,
102 double strike,
103 double t_years,
104 double sigma,
105 double rate = 0.0) noexcept {
106 return black76(cp, forward, strike, t_years, sigma, rate).price;
107}
108
109enum class IvStatus : std::uint8_t {
110 Ok = 0,
111 AtIntrinsic = 1, // price at (or within tolerance of) the no-arbitrage lower bound: vol 0
112 OutOfBounds = 2, // below intrinsic, or above what kMaxVol can produce
113 BadInput = 3, // non-positive forward/strike/time or non-finite price
114 NoConvergence = 4, // iteration bound hit; vol is the best bracket midpoint
115};
116
117struct IvResult {
118 double vol = 0.0;
120 int iterations = 0;
121 [[nodiscard]] bool ok() const noexcept { return status == IvStatus::Ok; }
124// Implied volatility of a Black-76 price. `price_tol` is the absolute price tolerance (same unit
125// as `price`); iterations never exceed `max_iterations`.
126[[nodiscard]] inline IvResult implied_vol(CallPut cp,
127 double price,
128 double forward,
129 double strike,
130 double t_years,
131 double rate = 0.0,
132 double price_tol = 1e-12,
133 int max_iterations = 100) noexcept {
134 IvResult r;
135 if (!(forward > 0.0) || !(strike > 0.0) || !(t_years > 0.0) || !std::isfinite(price) ||
136 !std::isfinite(rate))
137 return r;
138 const bool call = cp == CallPut::Call;
139 // Work undiscounted: the bounds are then [max(F-K, 0), F] for calls, [max(K-F, 0), K] for puts.
140 const double p = price * std::exp(rate * t_years);
141 const double intrinsic = call ? std::max(forward - strike, 0.0) : std::max(strike - forward, 0.0);
142 const double upper = call ? forward : strike;
143 const double tol = price_tol > 0.0 ? price_tol : 1e-12;
144 if (p < intrinsic - tol || p >= upper) {
145 r.status = IvStatus::OutOfBounds;
146 return r;
147 }
148 double lo = kMinVol;
149 double hi = kMaxVol;
150 if (p <= black76_price(cp, forward, strike, t_years, lo) + tol) {
151 r.status = IvStatus::AtIntrinsic;
152 return r;
153 }
154 if (p > black76_price(cp, forward, strike, t_years, hi)) {
155 r.status = IvStatus::OutOfBounds;
156 return r;
157 }
158 // Brenner-Subrahmanyam start (exact at the money), kept inside the bracket.
159 double sigma =
160 std::sqrt(2.0 * std::numbers::pi / t_years) * (p - intrinsic) / (0.5 * (forward + strike));
161 if (!(sigma > lo && sigma < hi)) sigma = 0.5; // NOLINT(readability-simplify-boolean-expr): NaN
162 const double sqrt_t = std::sqrt(t_years);
163 for (int i = 1; i <= max_iterations; ++i) {
164 r.iterations = i;
165 const Greeks g = black76(cp, forward, strike, t_years, sigma);
166 const double diff = g.price - p;
167 if (std::fabs(diff) <= tol) {
168 r.vol = sigma;
169 r.status = IvStatus::Ok;
170 return r;
171 }
172 if (diff > 0.0) {
173 hi = sigma;
174 } else {
175 lo = sigma;
176 }
177 if (hi - lo <= 1e-15 * hi) {
178 r.vol = 0.5 * (lo + hi);
179 r.status = IvStatus::Ok;
180 return r;
181 }
182 const double vega =
183 forward * norm_pdf(std::log(forward / strike) / (sigma * sqrt_t) + 0.5 * sigma * sqrt_t) *
184 sqrt_t;
185 double next = vega > 1e-300 ? sigma - diff / vega : 0.0;
186 // Newton left the bracket (or produced NaN): bisect.
187 if (!(next > lo && next < hi))
188 next = 0.5 * (lo + hi); // NOLINT(readability-simplify-boolean-expr)
189 sigma = next;
190 }
191 r.vol = 0.5 * (lo + hi);
192 r.status = IvStatus::NoConvergence;
193 return r;
194}
195
196// ---- inverse (coin-margined) conversions ----------------------------------------------------
197
198// Quote-currency value -> price in the base coin (Deribit BTC options: USD / underlying price).
199[[nodiscard]] inline double to_coin_price(double quote_value, double forward) noexcept {
200 return forward > 0.0 ? quote_value / forward : 0.0;
201}
202// Delta of a coin-quoted option in coin units. With V_coin = V / F, the coin value of the
203// position changes by F * dV_coin/dF = dV/dF - V / F = delta - V_coin per unit move of F.
204[[nodiscard]] constexpr double coin_delta(double delta, double coin_price) noexcept {
205 return delta - coin_price;
206}
207
208} // namespace fastmm::options
Definition black76.hpp:33
Greeks black76(CallPut cp, double forward, double strike, double t_years, double sigma, double rate=0.0) noexcept
Price and greeks.
Definition black76.hpp:65
double to_coin_price(double quote_value, double forward) noexcept
Quote-currency value -> price in the base coin (Deribit BTC options: USD / underlying price).
Definition black76.hpp:200
constexpr double year_fraction(std::int64_t expiry_ns, std::int64_t now_ns) noexcept
Year fraction between two epoch-ns timestamps (ACT/365); negative once expired.
Definition black76.hpp:50
constexpr double kMinVol
0.01 % annualised
Definition black76.hpp:36
double norm_pdf(double x) noexcept
Standard normal density and distribution. erfc keeps the far tail accurate (no 1 - N(x)).
Definition black76.hpp:42
IvResult implied_vol(CallPut cp, double price, double forward, double strike, double t_years, double rate=0.0, double price_tol=1e-12, int max_iterations=100) noexcept
Implied volatility of a Black-76 price.
Definition black76.hpp:128
constexpr double coin_delta(double delta, double coin_price) noexcept
Delta of a coin-quoted option in coin units.
Definition black76.hpp:205
constexpr double kMaxVol
1000 % annualised
Definition black76.hpp:37
double black76_price(CallPut cp, double forward, double strike, double t_years, double sigma, double rate=0.0) noexcept
Definition black76.hpp:102
CallPut
Definition black76.hpp:39
double norm_cdf(double x) noexcept
Definition black76.hpp:45
constexpr double kSecondsPerYear
ACT/365, as Deribit's theta.
Definition black76.hpp:35
IvStatus
Definition black76.hpp:111
@ AtIntrinsic
price at (or within tolerance of) the no-arbitrage lower bound: vol 0
@ OutOfBounds
below intrinsic, or above what kMaxVol can produce
@ NoConvergence
iteration bound hit; vol is the best bracket midpoint
@ BadInput
non-positive forward/strike/time or non-finite price
Definition black76.hpp:54
double price
Definition black76.hpp:55
double rho
Definition black76.hpp:60
double theta
Definition black76.hpp:59
double vega
Definition black76.hpp:58
double gamma
Definition black76.hpp:57
double delta
Definition black76.hpp:56
Definition black76.hpp:119
bool ok() const noexcept
Definition black76.hpp:123
IvStatus status
Definition black76.hpp:121
int iterations
Definition black76.hpp:122
double vol
Definition black76.hpp:120