Call option valuation: Black-Scholes vs. Monte Carlo

In this post, we will tackle the valuation of call and put options using two methods: a closed-form formula and a stochastic model. Options are a type of financial instrument that give their owner the right, but not the obligation, to buy or sell an asset at a predetermined price within a certain period. A call option allows the holder to purchase the asset at a specific price (known as the strike price) before the option expires, while a put option grants the holder the right to sell the asset at the strike price before expiry. Call options are commonly used to bet on an asset's price increase, whereas put options are often used to hedge against price declines or speculate on downward movements.

The valuation of options can be quite complex because it involves forecasting how the price of the underlying asset might change over time. Several factors influence an option's value, including the current price of the asset, the strike price, time remaining until expiry, interest rates, and the volatility of the asset's price.

We'll use two popular methods to assess the value of both call and put options: the Black-Scholes closed-form formula and a stochastic model. After calculating the option values using each approach, we'll compare the results to see if they align or if they produce differing valuations.


List of content:

  1. Option attributes
  2. Black-Scholes formula
  3. Monte Carlo simulation
  4. Comparison

Option attributes

For our exercise, we'll be valuing European call and put options. European options can only be exercised at their expiration date, unlike American options, which can be exercised at any point before expiration. A call option gives the holder the right to purchase the underlying asset at a set price (the strike price) when the option expires, while a put option grants the right to sell the asset at the strike price upon expiration.

Here are the attributes of the options we'll be working with:

  • Current price (\( S_{0} \)) - The asset's current market price, which we'll set at EUR 100. This price serves as the starting point for evaluating how the options' values might change.
  • Strike price (\( X \)) - The price at which the holder of the option can buy (call) or sell (put) the asset when the option expires. We'll use a strike price of EUR 100, meaning both options will be "at the money" if the asset's price remains unchanged over the year.
  • Time to expiration (\( T \)) - This is the duration until the options expire, which we'll set to 1 year. Options with longer times to expiration generally have higher value, as there is more time for the asset price to change.
  • Risk-free interest rate (\( r \)) - This is the theoretical rate of return on a completely risk-free investment, such as government bonds. For this exercise, we'll assume a 5% risk-free interest rate.
  • Volatility of the stock (\( \sigma \)) - This measures the asset's price fluctuations over time. In this case, we'll use a volatility of 0.2, or 20%. Higher volatility means a greater chance for the asset price to swing, which affects the options' potential values.

With these attributes defined, we'll now proceed to valuing the options. In the next section, we'll explore how to calculate their prices using the Black-Scholes formula, a widely used model in finance for evaluating options.

Black-Scholes formula

Our first approach to valuing options is to use the Black-Scholes formula, a closed-form solution that provides a theoretical price for European call and put options. This formula, introduced by Fischer Black and Myron Scholes in the early 1970s, is widely used in finance due to its simplicity and effectiveness. It calculates the price of an option based on five inputs: the current price of the asset, the strike price, the risk-free interest rate, the time to expiration, and the asset's volatility.

The Black-Scholes formulas for the prices of European call and put options are:

\[ C = S_{0} \cdot \mathcal{N}(d1) - X \cdot e^{-r \cdot T} \cdot \mathcal{N}(d2) \] \[ P = X \cdot e^{-r \cdot T} \cdot \mathcal{N}(-d2) - S_{0} \cdot \mathcal{N}(-d1) \]

where:

\[ d1 = \frac{\ln(\frac{S_{0}}{X}) + (r + \frac{\sigma^2}{2} \cdot T)}{\sigma \cdot \sqrt{T}} \] \[ d2 = d1 - \sigma \cdot \sqrt{T} \]

In these formulas:

  • \( C \) - the call option price,
  • \( P \) - the put option price,
  • \( S_{0} \) - the current price of the asset,
  • \( X \) - the strike price,
  • \( T \) - the time to expiration in years,
  • \( r \) - the risk-free interest rate,
  • \( \sigma \) - the volatility of the asset's price.

\( \mathcal{N}(d1) \) and \( \mathcal{N}(d2) \) represent the cumulative distribution function values for a standard normal distribution at \( d1 \) and \( d2 \). The formula captures the probabilities of the option expiring "in the money" and adjusts the price for time value and risk.

The Python code below calculates both call and put option prices using the Black-Scholes formula:

import numpy as np
from scipy.stats import norm

np.random.seed(1)

# Parameters
S0 = 100
X = 100
r = 0.05
T = 1
sigma = 0.2

# Calculate d1 and d2
d1 = (np.log(S0 / X) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
d2 = d1 - sigma * np.sqrt(T)

# Calculate option prices
call_price = S0 * norm.cdf(d1) - X * np.exp(-r * T) * norm.cdf(d2)
put_price = X * np.exp(-r * T) * norm.cdf(-d2) - S0 * norm.cdf(-d1)

print(f"Call option price: EUR {call_price:.2f}")
print(f"Put option price: EUR {put_price:.2f}")

# Call option price: EUR 10.45
# Put option price: EUR 5.57

This code applies the Black-Scholes formula to compute call and put option prices. Running the code gives the option prices based on the provided inputs. While this method provides a quick and efficient way to estimate option values, it relies on assumptions such as constant volatility and a single risk-free rate, which may not always reflect real-world conditions.

In the next section, we'll explore a different approach using stochastic modelling to evaluate option values.

Monte Carlo simulation

Our second approach for valuing options is to use stochastic modelling, specifically a Monte Carlo simulation. Unlike the Black-Scholes formula, which gives a single theoretical price based on fixed inputs, Monte Carlo simulations allow us to generate a wide range of possible outcomes by simulating many scenarios for the underlying asset's price.

The basic idea is to project the asset's price over the option's life, calculate the payoff for each simulated scenario, and then take the average payoff to estimate the option's value. This method can be applied to both call and put options, as the payoff structure for each option type is different: for a call option, the payoff is the maximum of zero or the difference between the asset's price and the strike price, while for a put option, the payoff is the maximum of zero or the difference between the strike price and the asset's price.

For the Monthe Carlo simulation, we will use the cashflower package. Let's create a model named monte_carlo.

from cashflower import create_model

create_model("monte_carlo")

Economic scenarios

To project the future value of the stock price, we will use a mathematical process called Geometric Brownian Motion (GBM), which is a common model for simulating asset prices. GBM assumes that the price evolves over time in a way that accounts for both predictable growth (linked to the risk-free rate) and random fluctuations (linked to volatility). This combination makes it well-suited for capturing the uncertain, often volatile nature of stock prices.

Here's the Python function to generate simulated price paths based on GBM. Let's create a directory utils inside of our model and the gbm.py script inside of it.

# utils/gbm.py
import numpy as np

def gbm(S0, r, T, sigma, n_scen=1, seed=1):
    """Geometric Brownian Motion"""
    np.random.seed(seed)
    Z = np.random.randn(n_scen)
    return S0 * np.exp((r - 0.5 * sigma**2) * T + sigma * np.sqrt(T) * Z)

In this function:

  • S0 - the current stock price,
  • r - the risk-free interest rate,
  • T - the time to expiration,
  • sigma - the stock's volatility,
  • n_scen - the number of simulated scenarios, and
  • Z - the random component, drawn from a normal distribution.

For this exercise, we'll generate 100,000 scenarios to capture a wide range of possible outcomes for the stock price at expiration. Each scenario represents one potential future price path for the asset.

Setting and input

Usually, cash flow models use monthly time steps, but since we only care about the option's value at expiration, we'll use an annual step here. In our settings file, we specify 100,000 stochastic scenarios and set the maximum calculation and output times to 1.

# settings.py
settings = {
    # ...
    "NUM_STOCHASTIC_SCENARIOS": 100_000,
    "T_MAX_CALCULATION": 1,
    "T_MAX_OUTPUT": 1,
}

The input for our model includes the option's parameters (current price, strike price, time to expiration, risk-free rate, and volatility) and the 100,000 stochastic scenarios generated by the GBM function.

# input.py
import pandas as pd
from settings import settings
from utils.gbm import gbm

n_scen = settings.get("NUM_STOCHASTIC_SCENARIOS")

# Option parameters
option = {
    "S0": 100,
    "X": 100,
    "T": 1.0,
    "r": 0.05,
    "sigma": 0.2,
}

# Stochastic scenarios for the stock price
scenarios = pd.DataFrame({
    "num": range(1, n_scen+1),
    "S1": gbm(option["S0"], option["r"], option["T"], option["sigma"], n_scen)
}).set_index("num")

assumption = {
    "option": option,
    "scenarios": scenarios,
}

Model

The model itself will have two key variables: the payoff of the option and the present value of the payoff.

  • Payoff - the option's payoff depends on whether it expires "in the money".
    • call option - if the stock price at expiration (S1) exceeds the strike price (X), the payoff is S1 - X; otherwise, it's zero.
    • put option - the payoff is X - S1 or zero.
  • Discounted payoff - to determine the current value of this future payoff, we need to discount it back to the present using the risk-free interest rate.

Here's the code implementing these steps:

# model.py
import numpy as np
from cashflower import variable
from input import assumption


@variable()
def call_payoff(t, stoch):
    option = assumption["option"]

    if t == option["T"]:
        S1 = assumption["scenarios"].loc[stoch, "S1"]
        X = option["X"]
        return max(S1 - X, 0)
    else:
        return 0


@variable()
def put_payoff(t, stoch):
    option = assumption["option"]

    if t == option["T"]:
        S1 = assumption["scenarios"].loc[stoch, "S1"]
        X = option["X"]
        return max(X - S1, 0)
    else:
        return 0


@variable()
def call_pv_payoff(t, stoch):
    option = assumption["option"]

    if t == 1:
        return call_payoff(t, stoch)
    else:
        return np.exp(-option["r"] * option["T"]) * call_payoff(t + 1, stoch)


@variable()
def put_pv_payoff(t, stoch):
    option = assumption["option"]

    if t == 1:
        return put_payoff(t, stoch)
    else:
        return np.exp(-option["r"] * option["T"]) * put_payoff(t + 1, stoch)

In this model:

  • The call_payoff and put_payoff variables check if time equals the option's expiration (T). If so, they calculate the payoff by comparing the stock price S1 to the strike price X.
  • The call_pv_payoff and put_pv_payoff variables discount the payoff to present value.

Behind the scenes stoch changes its value from 1 to 100,000. By averaging the pv_payoff across all scenarios, we'll estimate the fair value of the option. This approach captures the potential range of future prices and their associated payoffs, providing a realistic estimate based on the simulated scenarios.

# output
t  call_payoff  put_payoff  call_pv_payoff  put_pv_payoff
0         0.00        0.00           10.50           5.53
1        11.04        5.81           11.04           5.81

Monte Carlo simulations like this one are versatile and can handle complex options or situations where prices follow unpredictable paths, though they tend to be computationally intensive. In the next and final section, we'll wrap up by summarising our findings and comparing the two approaches.

Comparison

Now that we've evaluated the call and put option using both the closed-form formula and stochastic modelling, let's compare the results and consider what they reveal.

Using the Black-Scholes formula, we arrived at a price of EUR 10.45 for the call option and EUR 5.57 for the put option. This method provides a quick and precise result based on the assumptions that the stock follows a log-normal distribution, volatility remains constant and there are no dividends or transaction costs. The closed-form solution is efficient and gives a solid estimate for simpler options like this European call.

The stochastic modelling approach, which used 100,000 simulated price scenarios, resulted in a price of EUR 10.50 and EUR 5.53. By generating a large number of possible outcomes, we were able to capture a range of potential future prices and their associated payoffs. Monte Carlo simulation is particularly useful for complex options or in situations where market assumptions are less certain, allowing us to see how random fluctuations in price might affect the option's value. Here, we see that the Monte Carlo result is very close to the Black-Scholes price, confirming that both methods yield similar valuations under our set parameters.

Which method to choose?

In this example, both approaches produced similar results, which is often the case when assumptions like constant volatility and a known expiration time hold true. For simple options, the Black-Scholes formula is usually preferred for its speed and precision. However, stochastic modelling is highly valuable when flexibility is needed, such as when dealing with options that have complex payoffs, path dependencies, or other features that make analytical solutions challenging.

In summary, both methods have their strengths and the choice depends on the option's complexity and the level of accuracy required.


Which approach do you find more intuitive for valuing options: analytical formulas or simulations?

Read also:

Log in to add your comment.

Comments

admin (2024-12-06)

That's correct, crimson_phoenix. Thanks for noticing! I have updated the code.


crimson_phoenix (2024-12-06)

hey, I think that "np.random.seed(1)" is not necessary in the first code snippet


admin (2024-11-20)

Hi clara74!

`stoch` starts at 1.

That's a great question, especially considering Python uses zero-based indexing. However, in this case, `stoch` will take values from 1 to 100,000.


clara74 (2024-11-19)

Does `stoch` start from 0 or 1?


future_actuary (2024-11-14)

I’ve used the Black-Scholes formula in class, but it's helpful to see it applied with code examples here. Nice job walking through the calculations.