Options Pricing: Delta Hedging¶

This notebook implements:

  • Delta hedging with detailed P&L attribution
  • Greeks calculations (Delta, Gamma, Theta) for hedging
  • Hedging error analysis
  • Impact of rebalancing frequency on hedging performance
In [1]:
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm

Black-Scholes Functions¶

In [2]:
def black_scholes_d1(S, K, r, d, sigma, T):
    """Calculate d1 parameter for Black-Scholes"""
    if T <= 0:
        return 0.0
    return (np.log(S/K) + (r - d + sigma**2 / 2) * T) / (sigma * np.sqrt(T))

def black_scholes_d2(S, K, r, d, sigma, T):
    """Calculate d2 parameter for Black-Scholes"""
    if T <= 0:
        return 0.0
    d1 = black_scholes_d1(S, K, r, d, sigma, T)
    return d1 - sigma * np.sqrt(T)

def black_scholes_call(S, K, r, d, sigma, T):
    """Black-Scholes European call option price"""
    if T <= 0:
        return max(S - K, 0)
    d1 = black_scholes_d1(S, K, r, d, sigma, T)
    d2 = black_scholes_d2(S, K, r, d, sigma, T)
    return S * np.exp(-d * T) * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)

def black_scholes_put(S, K, r, d, sigma, T):
    """Black-Scholes European put option price"""
    if T <= 0:
        return max(K - S, 0)
    d1 = black_scholes_d1(S, K, r, d, sigma, T)
    d2 = black_scholes_d2(S, K, r, d, sigma, T)
    return K * np.exp(-r * T) * norm.cdf(-d2) - S * np.exp(-d * T) * norm.cdf(-d1)

Greeks Calculations¶

In [3]:
def calculate_delta_call(S, K, r, d, sigma, T):
    """Delta for call option - hedge ratio"""
    if T <= 0:
        return 1.0 if S > K else 0.0
    d1 = black_scholes_d1(S, K, r, d, sigma, T)
    return np.exp(-d * T) * norm.cdf(d1)

def calculate_delta_put(S, K, r, d, sigma, T):
    """Delta for put option - hedge ratio"""
    if T <= 0:
        return -1.0 if S < K else 0.0
    d1 = black_scholes_d1(S, K, r, d, sigma, T)
    return np.exp(-d * T) * (norm.cdf(d1) - 1)

def calculate_gamma(S, K, r, d, sigma, T):
    """Gamma - rate of change of delta (hedging error sensitivity)"""
    if T <= 0:
        return 0.0
    d1 = black_scholes_d1(S, K, r, d, sigma, T)
    return np.exp(-d * T) * norm.pdf(d1) / (S * sigma * np.sqrt(T))

def calculate_theta_call(S, K, r, d, sigma, T):
    """Theta for call option - time decay component"""
    if T <= 0:
        return 0.0
    d1 = black_scholes_d1(S, K, r, d, sigma, T)
    d2 = black_scholes_d2(S, K, r, d, sigma, T)
    term1 = -S * np.exp(-d * T) * norm.pdf(d1) * sigma / (2 * np.sqrt(T))
    term2 = d * S * np.exp(-d * T) * norm.cdf(d1)
    term3 = -r * K * np.exp(-r * T) * norm.cdf(d2)
    return term1 + term2 + term3

def calculate_theta_put(S, K, r, d, sigma, T):
    """Theta for put option - time decay component"""
    if T <= 0:
        return 0.0
    d1 = black_scholes_d1(S, K, r, d, sigma, T)
    d2 = black_scholes_d2(S, K, r, d, sigma, T)
    term1 = -S * np.exp(-d * T) * norm.pdf(d1) * sigma / (2 * np.sqrt(T))
    term2 = -d * S * np.exp(-d * T) * norm.cdf(-d1)
    term3 = r * K * np.exp(-r * T) * norm.cdf(-d2)
    return term1 + term2 + term3

Delta Hedging with P&L Attribution¶

In [4]:
def delta_hedging_simulation(S0, K, r, d, sigma, T, num_steps, option_type='call', 
                                      transaction_cost=0.0, seed=None):
    """
    Delta hedging simulation with P&L attribution
    
    Parameters:
    -----------
    S0 : float
        Initial stock price
    K : float
        Strike price
    r : float
        Risk-free rate
    d : float
        Dividend yield
    sigma : float
        Volatility
    T : float
        Time to maturity (years)
    num_steps : int
        Number of rebalancing steps
    option_type : str
        'call' or 'put'
    transaction_cost : float
        Transaction cost as a percentage of stock value
    seed : int
        Random seed for reproducibility
    
    Returns:
    --------
    results : dict
        Dictionary containing all hedging results and P&L components
    """
    if seed is not None:
        np.random.seed(seed)
    
    dt = T / num_steps
    
    # Initialize arrays
    stock_path = np.zeros(num_steps + 1)
    delta_path = np.zeros(num_steps + 1)
    gamma_path = np.zeros(num_steps + 1)
    theta_path = np.zeros(num_steps + 1)
    option_value_path = np.zeros(num_steps + 1)
    hedge_portfolio = np.zeros(num_steps + 1)
    cash_account = np.zeros(num_steps + 1)
    time_path = np.zeros(num_steps + 1)
    transaction_costs = np.zeros(num_steps + 1)
    
    # P&L attribution arrays
    delta_pnl = np.zeros(num_steps + 1)
    gamma_pnl = np.zeros(num_steps + 1)
    theta_pnl = np.zeros(num_steps + 1)
    
    # Initial values
    stock_path[0] = S0
    time_path[0] = 0
    
    if option_type == 'call':
        option_value_path[0] = black_scholes_call(S0, K, r, d, sigma, T)
        delta_path[0] = calculate_delta_call(S0, K, r, d, sigma, T)
        theta_path[0] = calculate_theta_call(S0, K, r, d, sigma, T)
    else:
        option_value_path[0] = black_scholes_put(S0, K, r, d, sigma, T)
        delta_path[0] = calculate_delta_put(S0, K, r, d, sigma, T)
        theta_path[0] = calculate_theta_put(S0, K, r, d, sigma, T)
    
    gamma_path[0] = calculate_gamma(S0, K, r, d, sigma, T)
    
    # Initial hedge: short option, long delta shares
    cash_account[0] = option_value_path[0] - delta_path[0] * S0
    hedge_portfolio[0] = 0.0  # Self-financing at start
    
    # Simulate stock path using GBM
    for t in range(1, num_steps + 1):
        Z = np.random.standard_normal()
        stock_path[t] = stock_path[t - 1] * np.exp((r - d - 0.5 * sigma**2) * dt + 
                                                     sigma * np.sqrt(dt) * Z)
        time_path[t] = t * dt
    
    # Rebalance hedge portfolio
    for t in range(1, num_steps + 1):
        time_remaining = T - time_path[t]
        
        # Calculate new Greeks and option value
        if time_remaining > 1e-10:
            if option_type == 'call':
                option_value_path[t] = black_scholes_call(stock_path[t], K, r, d, sigma, time_remaining)
                new_delta = calculate_delta_call(stock_path[t], K, r, d, sigma, time_remaining)
                theta_path[t] = calculate_theta_call(stock_path[t], K, r, d, sigma, time_remaining)
            else:
                option_value_path[t] = black_scholes_put(stock_path[t], K, r, d, sigma, time_remaining)
                new_delta = calculate_delta_put(stock_path[t], K, r, d, sigma, time_remaining)
                theta_path[t] = calculate_theta_put(stock_path[t], K, r, d, sigma, time_remaining)
            
            gamma_path[t] = calculate_gamma(stock_path[t], K, r, d, sigma, time_remaining)
        else:
            # At maturity
            if option_type == 'call':
                option_value_path[t] = max(stock_path[t] - K, 0)
            else:
                option_value_path[t] = max(K - stock_path[t], 0)
            new_delta = 1.0 if (option_type == 'call' and stock_path[t] > K) else \
                       (-1.0 if (option_type == 'put' and stock_path[t] < K) else 0.0)
            gamma_path[t] = 0.0
            theta_path[t] = 0.0
        
        # Calculate P&L components
        dS = stock_path[t] - stock_path[t - 1]
        
        # Delta P&L (first-order: linear in stock price change)
        delta_pnl[t] = delta_path[t - 1] * dS
        
        # Gamma P&L (second-order: quadratic in stock price change)
        gamma_pnl[t] = 0.5 * gamma_path[t - 1] * dS**2
        
        # Theta P&L (time decay)
        theta_pnl[t] = theta_path[t - 1] * dt
        
        # Update cash account with interest
        cash_account[t] = cash_account[t - 1] * np.exp(r * dt)
        
        # Rebalancing transaction
        delta_change = new_delta - delta_path[t - 1]
        rebalance_cost = abs(delta_change) * stock_path[t] * transaction_cost
        transaction_costs[t] = rebalance_cost
        
        # Update cash account for stock purchase/sale
        cash_account[t] -= delta_change * stock_path[t] + rebalance_cost
        
        # Update delta
        delta_path[t] = new_delta
        
        # Calculate total portfolio value (stocks + cash - option)
        hedge_portfolio[t] = delta_path[t] * stock_path[t] + cash_account[t] - option_value_path[t]
    
    # Final P&L
    final_payoff = option_value_path[-1]
    total_pnl = hedge_portfolio[-1]
    
    # Compile results
    results = {
        'total_pnl': total_pnl,
        'stock_path': stock_path,
        'delta_path': delta_path,
        'gamma_path': gamma_path,
        'theta_path': theta_path,
        'option_value_path': option_value_path,
        'hedge_portfolio': hedge_portfolio,
        'cash_account': cash_account,
        'time_path': time_path,
        'delta_pnl': delta_pnl,
        'gamma_pnl': gamma_pnl,
        'theta_pnl': theta_pnl,
        'transaction_costs': transaction_costs,
        'total_transaction_cost': np.sum(transaction_costs),
        'final_option_value': final_payoff
    }
    
    return results

Discrete Hedging Error Analysis¶

In [5]:
def analyze_hedging_error(S0, K, r, d, sigma, T, rebalance_frequencies, num_simulations=100, 
                         option_type='call', seed=None):
    """
    Analyze hedging error as a function of rebalancing frequency
    
    Parameters:
    -----------
    rebalance_frequencies : list
        List of number of rebalancing steps to test
    num_simulations : int
        Number of Monte Carlo simulations per frequency
    
    Returns:
    --------
    results : dict
        Dictionary with mean and std of P&L for each frequency
    """
    results = {
        'frequencies': rebalance_frequencies,
        'mean_pnl': [],
        'std_pnl': [],
        'mean_abs_error': [],
        'rmse': []
    }
    
    for freq in rebalance_frequencies:
        pnls = []
        for sim in range(num_simulations):
            sim_seed = seed + sim if seed is not None else None
            hedge_results = delta_hedging_simulation(
                S0, K, r, d, sigma, T, freq, option_type, seed=sim_seed
            )
            pnls.append(hedge_results['total_pnl'])
        
        results['mean_pnl'].append(np.mean(pnls))
        results['std_pnl'].append(np.std(pnls))
        results['mean_abs_error'].append(np.mean(np.abs(pnls)))
        results['rmse'].append(np.sqrt(np.mean(np.array(pnls)**2)))
    
    return results

Parameters Setup¶

In [6]:
# Base Parameters
S0 = 100.0      # Initial stock price
K = 100.0       # Strike price
r = 0.05        # Risk-free rate
d = 0.02        # Dividend yield
sigma = 0.2     # Volatility
T = 1.0         # Time to maturity
option_type = 'call'

Single Delta Hedging Simulation with P&L Attribution¶

In [7]:
print("="*70)
print("DELTA HEDGING SIMULATION - DETAILED ANALYSIS")
print("="*70)

num_hedge_steps = 50
hedge_results = delta_hedging_simulation(
    S0, K, r, d, sigma, T, num_hedge_steps, option_type, 
    transaction_cost=0.001, seed=42
)

print(f"\nHedging Parameters:")
print(f"  Rebalancing Steps:  {num_hedge_steps}")
print(f"  Transaction Cost:   0.1%")
print(f"  Initial Stock:      ${S0:.2f}")
print(f"  Final Stock:        ${hedge_results['stock_path'][-1]:.2f}")

print(f"\nP&L Attribution:")
print(f"  Total P&L:          ${hedge_results['total_pnl']:.6f}")
print(f"  Delta P&L:          ${np.sum(hedge_results['delta_pnl']):.6f}")
print(f"  Gamma P&L:          ${np.sum(hedge_results['gamma_pnl']):.6f}")
print(f"  Theta P&L:          ${np.sum(hedge_results['theta_pnl']):.6f}")
print(f"  Transaction Costs:  ${hedge_results['total_transaction_cost']:.6f}")
print(f"  Final Option Value: ${hedge_results['final_option_value']:.6f}")
print()
======================================================================
DELTA HEDGING SIMULATION - DETAILED ANALYSIS
======================================================================

Hedging Parameters:
  Rebalancing Steps:  50
  Transaction Cost:   0.1%
  Initial Stock:      $100.00
  Final Stock:        $73.43

P&L Attribution:
  Total P&L:          $-0.687547
  Delta P&L:          $-8.198098
  Gamma P&L:          $2.669855
  Theta P&L:          $-3.786448
  Transaction Costs:  $0.161856
  Final Option Value: $0.000000

Visualization: Single Delta Hedging Path¶

In [8]:
fig, axes = plt.subplots(2, 2, figsize=(15, 10))

# Stock Price Path
axes[0, 0].plot(hedge_results['time_path'], hedge_results['stock_path'], 'b-', linewidth=2)
axes[0, 0].axhline(y=K, color='r', linestyle='--', label=f'Strike K={K}')
axes[0, 0].set_xlabel('Time (years)', fontsize=11)
axes[0, 0].set_ylabel('Stock Price ($)', fontsize=11)
axes[0, 0].set_title('Stock Price Path', fontsize=12, fontweight='bold')
axes[0, 0].legend(fontsize=10)
axes[0, 0].grid(True, alpha=0.3)

# Delta Path
axes[0, 1].plot(hedge_results['time_path'], hedge_results['delta_path'], 'g-', linewidth=2)
axes[0, 1].set_xlabel('Time (years)', fontsize=11)
axes[0, 1].set_ylabel('Delta', fontsize=11)
axes[0, 1].set_title('Delta Hedge Ratio Over Time', fontsize=12, fontweight='bold')
axes[0, 1].grid(True, alpha=0.3)

# Cumulative P&L Components
cumulative_delta_pnl = np.cumsum(hedge_results['delta_pnl'])
cumulative_gamma_pnl = np.cumsum(hedge_results['gamma_pnl'])
cumulative_theta_pnl = np.cumsum(hedge_results['theta_pnl'])

axes[1, 0].plot(hedge_results['time_path'], cumulative_delta_pnl, 'b-', 
                label='Delta P&L', linewidth=2)
axes[1, 0].plot(hedge_results['time_path'], cumulative_gamma_pnl, 'r-', 
                label='Gamma P&L', linewidth=2)
axes[1, 0].plot(hedge_results['time_path'], cumulative_theta_pnl, 'g-', 
                label='Theta P&L', linewidth=2)
axes[1, 0].axhline(y=0, color='k', linestyle='-', linewidth=1, alpha=0.3)
axes[1, 0].set_xlabel('Time (years)', fontsize=11)
axes[1, 0].set_ylabel('Cumulative P&L ($)', fontsize=11)
axes[1, 0].set_title('P&L Attribution Components', fontsize=12, fontweight='bold')
axes[1, 0].legend(fontsize=10)
axes[1, 0].grid(True, alpha=0.3)

# Portfolio Value
axes[1, 1].plot(hedge_results['time_path'], hedge_results['hedge_portfolio'], 
                'purple', linewidth=2, label='Portfolio Value')
axes[1, 1].axhline(y=0, color='k', linestyle='--', linewidth=1, alpha=0.5)
axes[1, 1].set_xlabel('Time (years)', fontsize=11)
axes[1, 1].set_ylabel('Portfolio Value ($)', fontsize=11)
axes[1, 1].set_title('Hedge Portfolio Value', fontsize=12, fontweight='bold')
axes[1, 1].legend(fontsize=10)
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
No description has been provided for this image

Multiple Delta Hedging Simulations¶

In [9]:
print("="*70)
print("MULTIPLE DELTA HEDGING SIMULATIONS")
print("="*70)

num_hedge_sims = 100
hedge_steps = 50
hedge_pnls = []
gamma_pnls = []
theta_pnls = []
transaction_cost_list = []

for i in range(num_hedge_sims):
    results = delta_hedging_simulation(
        S0, K, r, d, sigma, T, hedge_steps, option_type, 
        transaction_cost=0.001, seed=i
    )
    hedge_pnls.append(results['total_pnl'])
    gamma_pnls.append(np.sum(results['gamma_pnl']))
    theta_pnls.append(np.sum(results['theta_pnl']))
    transaction_cost_list.append(results['total_transaction_cost'])

hedge_pnls = np.array(hedge_pnls)
gamma_pnls = np.array(gamma_pnls)
theta_pnls = np.array(theta_pnls)
transaction_cost_list = np.array(transaction_cost_list)

print(f"\nResults from {num_hedge_sims} simulations ({hedge_steps} rebalancing steps):")
print(f"  Mean Total P&L:        ${np.mean(hedge_pnls):.6f}")
print(f"  Std Dev P&L:           ${np.std(hedge_pnls):.6f}")
print(f"  Min P&L:               ${np.min(hedge_pnls):.6f}")
print(f"  Max P&L:               ${np.max(hedge_pnls):.6f}")
print(f"  Mean Gamma P&L:        ${np.mean(gamma_pnls):.6f}")
print(f"  Mean Theta P&L:        ${np.mean(theta_pnls):.6f}")
print(f"  Mean Transaction Cost: ${np.mean(transaction_cost_list):.6f}")
print()
======================================================================
MULTIPLE DELTA HEDGING SIMULATIONS
======================================================================

Results from 100 simulations (50 rebalancing steps):
  Mean Total P&L:        $-1.326962
  Std Dev P&L:           $1.012520
  Min P&L:               $-3.966965
  Max P&L:               $0.927677
  Mean Gamma P&L:        $3.707305
  Mean Theta P&L:        $-5.002525
  Mean Transaction Cost: $0.214621

Visualization: Multiple Delta Hedging Simulations¶

In [10]:
fig, axes = plt.subplots(2, 2, figsize=(15, 10))

# P&L Distribution
axes[0, 0].hist(hedge_pnls, bins=30, edgecolor='black', alpha=0.7, color='skyblue')
axes[0, 0].axvline(x=np.mean(hedge_pnls), color='r', linestyle='--', linewidth=2, 
                   label=f'Mean: ${np.mean(hedge_pnls):.4f}')
axes[0, 0].axvline(x=0, color='k', linestyle='-', linewidth=1, alpha=0.3)
axes[0, 0].set_xlabel('Total P&L ($)', fontsize=11)
axes[0, 0].set_ylabel('Frequency', fontsize=11)
axes[0, 0].set_title(f'Distribution of Hedge P&L\n({num_hedge_sims} simulations)', 
                     fontsize=12, fontweight='bold')
axes[0, 0].legend(fontsize=10)
axes[0, 0].grid(True, alpha=0.3)

# P&L Over Simulations
axes[0, 1].plot(range(1, num_hedge_sims + 1), hedge_pnls, 'bo-', alpha=0.6, markersize=4)
axes[0, 1].axhline(y=np.mean(hedge_pnls), color='r', linestyle='--', linewidth=2, 
                   label=f'Mean: ${np.mean(hedge_pnls):.4f}')
axes[0, 1].axhline(y=0, color='k', linestyle='-', linewidth=1, alpha=0.3)
axes[0, 1].set_xlabel('Simulation Number', fontsize=11)
axes[0, 1].set_ylabel('Total P&L ($)', fontsize=11)
axes[0, 1].set_title('Hedge P&L Across Simulations', fontsize=12, fontweight='bold')
axes[0, 1].legend(fontsize=10)
axes[0, 1].grid(True, alpha=0.3)

# Gamma P&L vs Total P&L
axes[1, 0].scatter(gamma_pnls, hedge_pnls, alpha=0.6, s=50)
axes[1, 0].axhline(y=0, color='k', linestyle='-', linewidth=1, alpha=0.3)
axes[1, 0].axvline(x=0, color='k', linestyle='-', linewidth=1, alpha=0.3)
axes[1, 0].set_xlabel('Gamma P&L ($)', fontsize=11)
axes[1, 0].set_ylabel('Total P&L ($)', fontsize=11)
axes[1, 0].set_title('Gamma P&L vs Total P&L', fontsize=12, fontweight='bold')
axes[1, 0].grid(True, alpha=0.3)

# Component Contributions
components = ['Gamma P&L', 'Theta P&L', 'Trans. Cost']
values = [np.mean(gamma_pnls), np.mean(theta_pnls), -np.mean(transaction_cost_list)]
colors = ['green' if v > 0 else 'red' for v in values]

axes[1, 1].bar(components, values, color=colors, alpha=0.7, edgecolor='black')
axes[1, 1].axhline(y=0, color='k', linestyle='-', linewidth=1)
axes[1, 1].set_ylabel('Average Contribution ($)', fontsize=11)
axes[1, 1].set_title('Mean P&L Components', fontsize=12, fontweight='bold')
axes[1, 1].grid(True, alpha=0.3, axis='y')

# Add value labels on bars
for i, v in enumerate(values):
    axes[1, 1].text(i, v, f'${v:.4f}', ha='center', 
                    va='bottom' if v > 0 else 'top', fontsize=10, fontweight='bold')

plt.tight_layout()
plt.show()
No description has been provided for this image

Hedging Error vs Rebalancing Frequency¶

In [11]:
print("="*70)
print("HEDGING ERROR ANALYSIS - REBALANCING FREQUENCY")
print("="*70)

rebalance_freqs = [10, 25, 50, 100, 250, 500]
num_sims_freq = 50

error_analysis = analyze_hedging_error(
    S0, K, r, d, sigma, T, rebalance_freqs, num_sims_freq, option_type, seed=42
)

print(f"\nRebalancing Frequency Analysis ({num_sims_freq} simulations each):")
print("-" * 70)
print(f"{'Steps':<10} {'Mean P&L':<15} {'Std Dev':<15} {'RMSE':<15}")
print("-" * 70)
for i, freq in enumerate(error_analysis['frequencies']):
    print(f"{freq:<10} ${error_analysis['mean_pnl'][i]:<14.6f} "
          f"${error_analysis['std_pnl'][i]:<14.6f} ${error_analysis['rmse'][i]:<14.6f}")
print()
======================================================================
HEDGING ERROR ANALYSIS - REBALANCING FREQUENCY
======================================================================

Rebalancing Frequency Analysis (50 simulations each):
----------------------------------------------------------------------
Steps      Mean P&L        Std Dev         RMSE           
----------------------------------------------------------------------
10         $-1.451333      $2.103864       $2.555897      
25         $-1.251516      $1.414295       $1.888524      
50         $-1.009856      $0.803077       $1.290248      
100        $-1.048502      $0.843550       $1.345709      
250        $-1.203273      $0.702510       $1.393336      
500        $-1.277274      $0.680161       $1.447083      

Visualization: Hedging Error Analysis¶

In [12]:
fig, axes = plt.subplots(1, 2, figsize=(15, 5))

# Standard Deviation vs Frequency
axes[0].plot(error_analysis['frequencies'], error_analysis['std_pnl'], 
             'bo-', linewidth=2, markersize=8)
axes[0].set_xlabel('Number of Rebalancing Steps', fontsize=11)
axes[0].set_ylabel('Std Dev of P&L ($)', fontsize=11)
axes[0].set_title('Hedging Error vs Rebalancing Frequency', fontsize=12, fontweight='bold')
axes[0].grid(True, alpha=0.3)
axes[0].set_xscale('log')

# RMSE vs Frequency
axes[1].plot(error_analysis['frequencies'], error_analysis['rmse'], 
             'ro-', linewidth=2, markersize=8)
axes[1].set_xlabel('Number of Rebalancing Steps', fontsize=11)
axes[1].set_ylabel('RMSE of P&L ($)', fontsize=11)
axes[1].set_title('Root Mean Square Error vs Rebalancing Frequency', 
                  fontsize=12, fontweight='bold')
axes[1].grid(True, alpha=0.3)
axes[1].set_xscale('log')

plt.tight_layout()
plt.show()
No description has been provided for this image