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