Example 2: Black-Scholes Tutorial
As a slightly more complex example, we’ll implement the Black-Scholes formula for European option pricing and compute its derivatives using AADC. We’ll then compare these derivatives against finite difference approximations to validate our implementation.
Mathematical Background
Let \(S(t)\) denote the stock price at time \(t\) and \(K\) be the strike price. The payoff to the option holder at time \(T\) is:
\[(S(T)-K)^{+}=\max \{0, S(T)-K\}\]
To get the present value, we multiply by the discount factor \(e^{-rT}\), where \(r\) is the continuously compounded interest rate. The expected present value is:
\[f(S(0), K, r, \sigma, T)=\mathbb{E}[e^{-rT}(S(T)-K)^{+}]\]
where \(\mathbb{E}\) denotes expectation and the distribution of \(S(T)\) follows the Black-Scholes stochastic differential equation:
\[\frac{dS(t)}{S(t)}=r dt + \sigma d W(t)\]
with \(W\) a standard Brownian motion and \(\sigma\) the volatility parameter.
The Black-Scholes formula for a European call option is:
\[f(S(0), K, r, \sigma, T)= S(0) \Phi(d_1) - e^{-rT}K\Phi(d_2)\]
where:
- \(d_1 = \frac{\log(S_0/K) + (r + \sigma^2/2)T}{\sigma\sqrt{T}}\)
- \(d_2 = d_1 - \sigma\sqrt{T}\)
- \(\Phi\) is the standard normal cumulative distribution function
Implementation
Black-Scholes Function Template
We implement the Black-Scholes formula as a template function that works with both native double and active idouble types:
template<typename mdouble>
mdouble BlackScholes(
mdouble S0, mdouble K, mdouble r, mdouble vol, mdouble T
) {
mdouble d1 = (std::log(S0/K) + T*(r + 0.5 * vol * vol)) / (vol * std::sqrt(T));
mdouble d2 = d1 - vol * std::sqrt(T);
return S0 * std::cdf_normal(d1) - std::exp(-r*T) * K * std::cdf_normal(d2);
}Key Points:
- Uses
std::cdf_normal()for the cumulative normal distribution (available in AADC) - Template design allows the same code to work for both derivative computation and validation
- Mathematical operations are automatically tracked when using
idouble
Finite Difference Validation
For validation, we implement a finite difference approximation:
template<typename mdouble>
mdouble BlackScholesFD(
mdouble S0, mdouble K, mdouble r, mdouble vol, mdouble T, mdouble h
) {
return (BlackScholes(S0 + h, K, r, vol, T) - BlackScholes(S0, K, r, vol, T)) / h;
}This computes the derivative with respect to \(S_0\) using a simple forward difference scheme.
Complete Example
void exampleBlackScholes()
{
typedef __m256d mmType;
aadc::AADCFunctions<mmType> aadc_kernel;
// Declare variables using AADC active types
idouble S0, K, r, vol, T, BS;
// Initialize with base case values
S0 = 100.0; // Current stock price
K = 110.0; // Strike price
r = 0.1; // Interest rate
vol = 0.25; // Volatility
T = 1.0; // Time to maturity
// Start recording computational graph
aadc_kernel.startRecording();
// Mark input variables for differentiation
aadc::AADCArgument S0_arg(S0.markAsInput());
aadc::AADCArgument K_arg(K.markAsInput());
aadc::AADCArgument r_arg(r.markAsInput());
aadc::AADCArgument vol_arg(vol.markAsInput());
aadc::AADCArgument T_arg(T.markAsInput());
// Execute Black-Scholes calculation
BS = BlackScholes(S0, K, r, vol, T);
// Mark output variable
aadc::AADCResult BS_res(BS.markAsOutput());
// Stop recording and compile
aadc_kernel.stopRecording(); // Create workspace for vectorized computations
std::shared_ptr<aadc::AADCWorkSpace<mmType>> ws(aadc_kernel.createWorkSpace());
// Set input values for 4 different scenarios simultaneously
ws->setVal(S0_arg, 100.0); // Same S0 for all scenarios
ws->setVal(K_arg, 110.0); // Same strike for all scenarios
ws->setVal(r_arg, _mm256_set_pd(0.1, 0.1, 0.01, 0.1)); // Different rates
ws->setVal(vol_arg, _mm256_set_pd(0.5, 0.5, 0.25, 0.25)); // Different volatilities
ws->setVal(T_arg, _mm256_set_pd(2.0, 1.0, 1.0, 1.0)); // Different maturities
// Execute forward pass (compute option values)
aadc_kernel.forward(*ws);
// Set up for derivative computation
ws->setDiff(BS_res, 1.0); // Unit adjoint seed
// Execute reverse pass (compute all derivatives)
aadc_kernel.reverse(*ws); // Print computed results and derivatives
for (int i = 0; i < 4; ++i) {
std::cout << " BS(" << ws->valp(S0_arg)[0]
<< "," << ws->valp(K_arg)[i]
<< "," << ws->valp(r_arg)[i]
<< "," << ws->valp(vol_arg)[i]
<< "," << ws->valp(T_arg)[i]
<< ")=" << ws->valp(BS_res)[i]
<< ". dBS/dS0=" << ws->diffp(S0_arg)[i]
<< "; dBS/dK=" << ws->diffp(K_arg)[i]
<< "; dBS/dr=" << ws->diffp(r_arg)[i]
<< std::endl;
}
// Validate against finite differences (h = 1e-6)
std::cout << "Finite Difference Validation (dBS/dS0):" << std::endl;
std::cout << "FD(100,110,0.1,0.25,1)=" <<
BlackScholesFD(100.0, 110.0, 0.1, 0.25, 1.0, 0.000001) << std::endl;
std::cout << "FD(100,110,0.01,0.25,1)=" <<
BlackScholesFD(100.0, 110.0, 0.01, 0.25, 1.0, 0.000001) << std::endl;
std::cout << "FD(100,110,0.1,0.5,1)=" <<
BlackScholesFD(100.0, 110.0, 0.1, 0.5, 1.0, 0.000001) << std::endl;
std::cout << "FD(100,110,0.1,0.5,2)=" <<
BlackScholesFD(100.0, 110.0, 0.1, 0.5, 2.0, 0.000001) << std::endl;
}Expected Output
BS(100,110,0.1,0.25,1)=10.1601. dBS/dS0=0.557155; dBS/dK=-0.41414; dBS/dr=45.5554
BS(100,110,0.01,0.25,1)=6.53345. dBS/dS0=0.4144; dBS/dK=-0.317332; dBS/dr=34.9066
BS(100,110,0.1,0.5,1)=19.9299. dBS/dS0=0.602329; dBS/dK=-0.366391; dBS/dr=40.303
BS(100,110,0.1,0.5,2)=18.3297. dBS/dS0=0.626731; dBS/dK=-0.403122; dBS/dr=83.2899
Finite Difference Validation (dBS/dS0):
FD(100,110,0.1,0.25,1)=0.557155
FD(100,110,0.01,0.25,1)=0.4144
FD(100,110,0.1,0.5,1)=0.602329
FD(100,110,0.1,0.5,2)=0.626731
Key Observations
Greeks Computation
The derivatives computed are the standard option “Greeks”:
- Delta (∂BS/∂S₀): Sensitivity to stock price changes
- Rho (∂BS/∂r): Sensitivity to interest rate changes
- Derivative w.r.t. Strike: Sensitivity to strike price (not a standard Greek)
Validation Results
The automatic differentiation results match the finite difference approximations to high precision, confirming the correctness of our implementation.
Vectorized Efficiency
By computing 4 scenarios simultaneously, we demonstrate AADC’s vectorization capabilities: - Single recording captures the computational graph - Forward pass evaluates all scenarios in parallel - Reverse pass computes all derivatives simultaneously
Next Steps
In the next example, we’ll implement a Monte Carlo simulation to estimate the same option value and compare the results with this analytical approach, demonstrating AADC’s effectiveness for stochastic computations.
See Also
- Monte Carlo Tutorial - Stochastic pricing methods
- idouble Class Reference - Active type documentation
- Mathematical Functions - Available mathematical operations