Black-Scholes Formula and European Option Pricing › Implementing the Black-Scholes Formula

Michael BrenndoerferNovember 28, 20256 min read
Quantitative FinanceAll chapters
Part 5 of 15Stochastic Calculus and Pricing Theory
Chapter 30Black-Scholes Formula and European Option Pricing › Implementing the Black-Scholes Formula
Reading section 5 of 11 · Implementing the Black-Scholes Formula · 6 minSection 5 of 11 · 6 min

Book contents

Section 5 of 11 · 6 min read

Black-Scholes Formula and European Option Pricing › Implementing the Black-Scholes FormulaOne section · Sections in this chapter5 / 11 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let's implement the Black-Scholes formula in Python and explore its behavior. These educational functions use scalar inputs with S,K,σ>0S,K,\sigma>0 and positive remaining time for the pre-expiry formulas, with a separate expiry-payoff branch. They assume finite, numerically usable inputs and are not general-range pricing routines. The validator explicitly rejects zero volatility, which would require a separate deterministic-limit formula. Negative remaining maturity is rejected; zero remaining maturity returns the expiry payoff.

In[5]:
Code
import numpy as np
from scipy.stats import norm


def validate_bs_inputs(S, K, T, r, sigma, option_type="call", q=0.0):
    """Validate this chapter's finite scalar educational pricing domain.

    S, K and sigma must be positive; remaining maturity T may be zero.
    Rates r and continuous yield q may have either sign.
    Zero volatility is excluded, rather than evaluated through a singular formula.
    Numeric inputs must be representable as finite Python floats.
    """
    from numbers import Real

    values = (S, K, T, r, sigma, q)
    if not all(isinstance(value, Real) for value in values):
        raise ValueError("Pricing inputs must be finite real scalars")
    try:
        S, K, T, r, sigma, q = map(float, values)
    except (OverflowError, TypeError, ValueError) as error:
        raise ValueError("Pricing inputs must be finite real scalars") from error
    if not all(np.isfinite(value) for value in (S, K, T, r, sigma, q)):
        raise ValueError("Pricing inputs must be finite real scalars")
    if S <= 0 or K <= 0 or sigma <= 0 or T < 0:
        raise ValueError("S, K and sigma must be positive; T must be nonnegative")
    if not isinstance(option_type, str) or option_type not in ("call", "put"):
        raise ValueError("option_type must be 'call' or 'put'")
    return S, K, T, r, sigma, q


def black_scholes_call(S, K, T, r, sigma):
    """
    Calculate Black-Scholes price for a European call option.

    Parameters:
        S: Current stock price
        K: Strike price
        T: Time to expiration (in years)
        r: Risk-free interest rate (annualized)
        sigma: Volatility (annualized)

    Returns:
        Call option price

    Inputs are finite real scalars with S,K,sigma>0 and T>=0; T=0
    returns the expiry payoff. The implementation assumes numerically usable
    intermediates and does not support arbitrary numerical ranges.
    """
    S, K, T, r, sigma, _ = validate_bs_inputs(S, K, T, r, sigma)
    if T == 0:
        return max(S - K, 0)  # At expiration, return intrinsic value

    d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)

    call_price = S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
    return call_price


def black_scholes_put(S, K, T, r, sigma):
    """
    Calculate a scalar European put price for finite inputs with S,K,sigma>0.
    T is nonnegative remaining maturity; T=0 returns the expiry payoff.
    This educational implementation assumes numerically usable intermediates.
    """
    S, K, T, r, sigma, _ = validate_bs_inputs(S, K, T, r, sigma, "put")
    if T == 0:
        return max(K - S, 0)

    d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)

    put_price = K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)
    return put_price

Now let's calculate option prices for a specific example:

In[6]:
Code
# Example parameters
S = 100  # Current stock price: $100
K = 105  # Strike price: $105
T = 0.5  # Time to expiration: 6 months
r = 0.05  # Risk-free rate: 5%
sigma = 0.20  # Volatility: 20%

call_price = black_scholes_call(S, K, T, r, sigma)
put_price = black_scholes_put(S, K, T, r, sigma)
Out[7]:
Console
Black-Scholes Option Pricing Example
========================================
Stock Price (S):     $100.00
Strike Price (K):    $105.00
Time to Expiry (T):  0.50 years (6 months)
Risk-free Rate (r):  5.0%
Volatility (σ):      20.0%
----------------------------------------
Call Option Price:   $4.5817
Put Option Price:    $6.9892

The call option on this slightly out-of-the-money stock has a lower value than the put, which holds intrinsic value. Let's verify put-call parity holds:

In[8]:
Code
# Verify put-call parity: C - P = S - K*exp(-rT)
lhs = call_price - put_price
rhs = S - K * np.exp(-r * T)
parity_difference = abs(lhs - rhs)
Out[9]:
Console
Put-Call Parity Verification
========================================
C - P = -2.4075
S - K*exp(-rT) = -2.4075
Difference: 0.00e+00
Put-call parity holds

At these example inputs, the implementation agrees with put-call parity to floating-point precision.

Examining the Intermediate Calculations

Understanding the intermediate values d1d_1 and d2d_2 helps build intuition for how the formula works:

In[10]:
Code
def compute_d1_d2(S, K, T, r, sigma):
    """Compute d1/d2 for finite scalar S,K,T,sigma>0 and finite r."""
    S, K, T, r, sigma, _ = validate_bs_inputs(S, K, T, r, sigma)
    if T == 0:
        raise ValueError("d1 and d2 require positive remaining maturity")
    log_moneyness = np.log(S / K)
    d1_shift = (r + 0.5 * sigma**2) * T
    diffusion = sigma * np.sqrt(T)

    d1 = (log_moneyness + d1_shift) / diffusion
    d2 = d1 - diffusion

    return d1, d2, log_moneyness, d1_shift, diffusion


d1, d2, log_moneyness, d1_shift, diffusion = compute_d1_d2(S, K, T, r, sigma)
Out[11]:
Console
Breakdown of d1 and d2 Calculations
=============================================
ln(S/K) = ln(100/105) = -0.0488
d1 numerator shift (r + σ²/2)T = 0.0350
Diffusion σ√T = 0.1414
---------------------------------------------
d1 = (ln(S/K) + shift) / diffusion = -0.0975
d2 = d1 - σ√T = -0.2389
---------------------------------------------
N(d1) = 0.4612 (probability under share measure)
N(d2) = 0.4056 (risk-neutral probability ITM)

The negative d2d_2 value tells us there's less than a 50% risk-neutral probability that this option finishes in-the-money. The value of N(d2)N(d_2) calculated above represents the precise probability of the stock price exceeding the strike at expiration under the risk-neutral measure.

Key Parameters

The key parameters for the Black-Scholes implementation are:

  • SS: Current stock price. Higher prices increase call values and decrease put values.
  • KK: Strike price. The reference price for the option's payoff.
  • TT: Time to expiration in years. Longer times generally increase option values.
  • rr: Risk-free interest rate (annualized). Higher rates favor calls over puts.
  • σ\sigma: Volatility (annualized). Controls the dispersion of terminal stock prices; with the other inputs fixed, higher volatility increases both call and put values.