Monte Carlo Simulation for Derivative Pricing › Path-Dependent Options

Michael BrenndoerferDecember 2, 202511 min read
Quantitative FinanceAll chapters
Part 6 of 15Numerical Methods for Derivative Pricing
Chapter 34Monte Carlo Simulation for Derivative Pricing › Path-Dependent Options
Reading section 6 of 13 · Path-Dependent Options · 11 minSection 6 of 13 · 11 min

Book contents

Section 6 of 13 · 11 min read

Monte Carlo Simulation for Derivative Pricing › Path-Dependent OptionsOne section · Sections in this chapter6 / 13 ↓
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

The real power of Monte Carlo emerges when pricing path-dependent derivatives. These are options whose payoff depends on the terminal price STS_T and on the entire trajectory of the underlying asset over its lifetime. For such options, no simple closed-form solutions typically exist, and Monte Carlo becomes indispensable. We now examine three important classes of path-dependent options: Asian options, barrier options, and lookback options.

Asian Options

Asian options have payoffs based on the average price of the underlying over some specified period. This averaging feature makes Asian options popular in commodity markets, where buyers and sellers often care about average prices over a month or quarter rather than spot prices on a single day. An Asian call with arithmetic averaging pays the following at expiration:

Payoff=max⁡(1M∑i=1MSti−K,0)\text{Payoff} = \max\left(\frac{1}{M}\sum_{i=1}^{M} S_{t_i} - K, 0\right)

where:

  • Payoff\text{Payoff}: cash flow received at expiration
  • MM: number of averaging points
  • StiS_{t_i}: asset price at observation time tit_i
  • KK: strike price

The payoff structure compares the arithmetic average of prices observed at MM discrete times against the strike price. If the average exceeds the strike, you receive the difference; otherwise, the option expires worthless. This averaging creates a significant mathematical challenge: because the arithmetic average of lognormal random variables is not itself lognormal, there is no closed-form solution analogous to Black-Scholes. The distribution of the average is complex and does not yield to analytical techniques. Monte Carlo is therefore the standard approach for pricing arithmetic Asian options.

In[11]:
Code
def monte_carlo_asian_call(S0, K, T, r, sigma, n_simulations, n_steps):
    """
    Price an arithmetic average Asian call option.

    The average is computed over n_steps equally spaced observation dates.
    """
    dt = T / n_steps

    # Initialize arrays
    payoffs = np.zeros(n_simulations)

    for i in range(n_simulations):
        # Generate path
        S = S0
        path_sum = 0

        for j in range(n_steps):
            Z = np.random.standard_normal()
            S = S * np.exp((r - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * Z)
            path_sum += S

        # Average price over the path
        average_price = path_sum / n_steps
        payoffs[i] = max(average_price - K, 0)

    discount_factor = np.exp(-r * T)
    price = discount_factor * np.mean(payoffs)
    std_error = discount_factor * np.std(payoffs) / np.sqrt(n_simulations)

    return price, std_error

The loop-based implementation above is clear and easy to understand, showing the logic step by step. However, it is inefficient because Python loops are slow. For production use, we must vectorize the computation, replacing explicit loops with array operations that NumPy can execute efficiently:

In[12]:
Code
def monte_carlo_asian_call_vectorized(
    S0, K, T, r, sigma, n_simulations, n_steps
):
    """
    Vectorized Asian call pricing for efficiency.
    """
    dt = T / n_steps

    # Generate all random increments at once: (n_simulations, n_steps)
    Z = np.random.standard_normal((n_simulations, n_steps))

    # Compute cumulative log returns
    log_increments = (r - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * Z
    log_S = np.log(S0) + np.cumsum(log_increments, axis=1)
    S_paths = np.exp(log_S)

    # Compute average price for each path
    average_prices = np.mean(S_paths, axis=1)

    # Compute payoffs
    payoffs = np.maximum(average_prices - K, 0)

    # Discount
    discount_factor = np.exp(-r * T)
    price = discount_factor * np.mean(payoffs)
    std_error = discount_factor * np.std(payoffs) / np.sqrt(n_simulations)

    return price, std_error
In[13]:
Code
np.random.seed(42)
asian_price, asian_se = monte_carlo_asian_call_vectorized(
    S0=100, K=100, T=1.0, r=0.05, sigma=0.2, n_simulations=100000, n_steps=252
)
asian_ci_width = 1.96 * asian_se
Out[14]:
Console
Asian call price: $5.7592 ± $0.0494 (95% CI)

Notice that the Asian option is cheaper than the equivalent European call. This price difference reflects a fundamental property of averaging: it reduces the effective volatility of the payoff. When you average many observations, extreme high values and extreme low values tend to cancel each other out, pulling the average toward the center of the distribution. This reduction in dispersion lowers the probability of very high payoffs, which are what make call options valuable. Asian options are therefore attractive for hedging situations where the average price is more relevant than the spot price, which is common in commodity markets where contracts reference monthly averages.

Out[15]:
Visualization
Scatter plot of terminal versus path-average prices for 2,000 simulations, with strike references at 105 dollars and a 45-degree comparison line.
Comparison of terminal price versus average price for Asian options. Each point represents one simulated path. The averaging effect reduces dispersion: the average prices (y-axis) are less spread out than terminal prices (x-axis), explaining why Asian options cost less than European options.

Barrier Options

Barrier options are contracts that are activated or extinguished when the underlying crosses a specified price level called the barrier. These options are popular because they cost less than vanilla options while still providing payoff in scenarios you consider most likely. A down-and-out call, for example, pays like a regular call at expiration unless the underlying price ever falls to or below a barrier level BB, in which case the option becomes worthless immediately and permanently:

Payoff={max⁡(ST−K,0)if min⁡0≤t≤TSt>B0otherwise\text{Payoff} = \begin{cases} \max(S_T - K, 0) & \text{if } \min_{0 \leq t \leq T} S_t > B \\ 0 & \text{otherwise} \end{cases}

where:

  • STS_T: asset price at time TT
  • KK: strike price
  • BB: barrier level
  • min⁡0≤t≤TSt\min_{0 \leq t \leq T} S_t: minimum asset price realized during the option's life

The payoff condition requires monitoring whether the minimum price observed over the option's life stays strictly above the barrier. If at any point during the option's life the price touches or crosses below the barrier, the option is "knocked out" and pays nothing, regardless of where the final price ends up. This path dependency makes Monte Carlo the natural pricing approach.

In[16]:
Code
def monte_carlo_down_and_out_call(
    S0, K, B, T, r, sigma, n_simulations, n_steps
):
    """
    Price a down-and-out call option.

    The option is knocked out if the price falls to or below barrier B.
    """
    dt = T / n_steps

    # Generate paths
    Z = np.random.standard_normal((n_simulations, n_steps))
    log_increments = (r - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * Z
    log_S = np.log(S0) + np.cumsum(log_increments, axis=1)
    S_paths = np.exp(log_S)

    # Check if barrier was breached at any point
    barrier_breached = np.any(S_paths <= B, axis=1)

    # Terminal prices
    ST = S_paths[:, -1]

    # Payoff is zero if barrier was breached
    payoffs = np.where(barrier_breached, 0, np.maximum(ST - K, 0))

    discount_factor = np.exp(-r * T)
    price = discount_factor * np.mean(payoffs)
    std_error = discount_factor * np.std(payoffs) / np.sqrt(n_simulations)

    return price, std_error
In[17]:
Code
np.random.seed(42)

# European call for comparison
euro_price, _ = monte_carlo_european_call(
    S0=100, K=100, T=1.0, r=0.05, sigma=0.2, n_simulations=100000
)

# Down-and-out with barrier at 80
barrier_price, barrier_se = monte_carlo_down_and_out_call(
    S0=100,
    K=100,
    B=80,
    T=1.0,
    r=0.05,
    sigma=0.2,
    n_simulations=100000,
    n_steps=252,
)
barrier_ci_width = 1.96 * barrier_se
barrier_discount = (euro_price - barrier_price) / euro_price * 100
Out[18]:
Console
European call price:     $10.4739
Down-and-out call price: $10.3322 ± $0.0909
Barrier discount:        1.4%

The down-and-out call trades at a substantial discount to the vanilla European call because it can be knocked out. This discount compensates you for bearing knockout risk: the possibility that a temporary price decline eliminates the option even if prices subsequently recover. Barrier options are attractive when you believe the barrier will not be breached, as they offer similar upside potential at reduced cost.

Out[19]:
Visualization
Thirty simulated stock-price paths colored by barrier outcome, with knocked-out paths in coral, surviving paths in blue, and horizontal lines at the 80-dollar barrier and 105-dollar strike.
Sample paths for a down-and-out barrier option. Red paths breach the barrier at 80 dollars and are knocked out, receiving zero payoff regardless of where they finish. Blue paths survive above the barrier and receive the standard call payoff if they end above the strike.
Barrier Monitoring Bias

Our simulation monitors the barrier only at discrete time steps. In reality, barriers are often monitored continuously. A path that crosses the barrier between observation times will not trigger knockout in our simulation, causing us to overestimate the option's value. Finer time steps reduce this bias, and there exist analytical corrections for this discretization error.

Lookback Options

Lookback options have payoffs based on the maximum or minimum price achieved during the option's life. These options give you the benefit of perfect hindsight, allowing you to exercise at the most favorable price observed. A floating-strike lookback call, for instance, pays the difference between the final price and the minimum price observed:

Payoff=ST−min⁡0≤t≤TSt\text{Payoff} = S_T - \min_{0 \leq t \leq T} S_t

where:

  • Payoff\text{Payoff}: cash flow received at expiration
  • STS_T: final asset price
  • min⁡0≤t≤TSt\min_{0 \leq t \leq T} S_t: minimum asset price realized over the life of the option

You effectively buy at the lowest price observed over the option's life and sell at the final price. This "buy low, sell at market" feature guarantees a positive payoff whenever the final price differs from the minimum, which is almost certain for any path with volatility.

In[20]:
Code
def monte_carlo_lookback_call(S0, T, r, sigma, n_simulations, n_steps):
    """
    Price a floating-strike lookback call option.

    Payoff = S_T - min(S_t) for t in [0, T]
    """
    dt = T / n_steps

    # Include initial price in paths
    Z = np.random.standard_normal((n_simulations, n_steps))
    log_increments = (r - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * Z

    # Prepend log(S0) to get full path including start
    log_S0 = np.full((n_simulations, 1), np.log(S0))
    log_S = np.hstack([log_S0, np.log(S0) + np.cumsum(log_increments, axis=1)])
    S_paths = np.exp(log_S)

    # Terminal and minimum prices
    ST = S_paths[:, -1]
    S_min = np.min(S_paths, axis=1)

    payoffs = ST - S_min

    discount_factor = np.exp(-r * T)
    price = discount_factor * np.mean(payoffs)
    std_error = discount_factor * np.std(payoffs) / np.sqrt(n_simulations)

    return price, std_error
In[21]:
Code
np.random.seed(42)
lookback_price, lookback_se = monte_carlo_lookback_call(
    S0=100, T=1.0, r=0.05, sigma=0.2, n_simulations=100000, n_steps=252
)
lookback_ci_width = 1.96 * lookback_se
Out[22]:
Console
Lookback call price: $16.5870 ± $0.0898

Lookback options are expensive because they always pay off: as long as there is any volatility in the path, the final price will differ from the minimum. You capture the full range from minimum to maximum, receiving perfect market timing ability as a feature of the contract. These options serve as theoretical upper bounds for what options could cost if you had the ability to time the market perfectly.

Key Parameters

The key parameters for path-dependent option simulations are:

  • n_steps: Number of time steps (MM). This determines the grid density for the path simulation. Higher MM reduces discretization error for continuous barriers and lookbacks.
  • B: Barrier level. The price threshold that triggers the barrier condition (knock-in or knock-out).