Pricing Options Using Monte Carlo Simulation¶
We will be valuing Asian & lookback options. Both of these are path-dependent exotic options. However, they differ in that Asian option value depends on the average price over a given period, whereas lookback options depend on the minimum price or maximum price.
We will use the Monte Carlo method to price these options; this will entail a three-step process:
Step 1: Firstly, we will simulate a number of price paths for the asset underlying the options.
The Euler scheme or Euler-Maruyama method is a numerical technique used to approximate the solution of stochastic differential equations (SDEs). It can be used to simulate the dynamics of asset prices when these prices follow a process that can be described by an SDE (e.g., Geometric Brownian Motion as in our current case). To outline the Euler scheme in general:
Consider an SDE in the general form
$$ dS_{t} = a(S_{t}, t)dt + b(S_{t}, t)dW_{t} $$where $a(S_{t}, t)$ is the drift term, i.e., the deterministic protion, $b(S_{t}, t)$ is the volatility or diffusion term, i.e., the stochastic portion, & $W_{t}$ is a Wiener process, i.e., Brownian motion.
Using the Euler scheme, we can approximate the value of $S_{t}$ over small time intervals $\delta t$. At each time step $t$, the value of the process $S_{t}$ is updated:
$$S_{t + \delta t} = S_{t} + a(S_{t}, t) \delta t + b(S_{t}, t)\delta W$$where $\delta t$ is the time increment, $\delta W = W(t + \delta t) - W(t)$ represents the increment of the Wiener process and is normally distributed, i.e., $\delta W$ ~ $N(0, \sqrt{\delta t})$
In the context of this Monte Carlo simulation for pricing Asian and Lookback options, we model the stock price as following our SDE for Geometric Brownian motion under the risk-neutral measure given as:
$$ dS_t = r S_t \, dt + \sigma S_t \, dW_t^\mathbb{Q} $$where $r$ is the risk-free rate, $\sigma$ is the volatility, and $W_t^\mathbb{Q}$ is a Wiener process under the risk-neutral measure.
We can observe that compared to our SDE for GBM under real-world probability $\mathbb{P}$, under the risk-neutral $\mathbb{Q}$, we replace our drift term $\mu$ with our risk-free rate $r$. This reflects the fact that in this risk-neutral world, investors are indifferent to risk, and assets are expected to grow at the risk-free rate, i.e., investors expect no excess return beyond the risk free rate. An important property here is that the discounted stock price, $S_t e^{-rt}$, follows a martingale. The martingale property states that the expected value tomorrow is exactly the value today or in mathematical terms: $$ \mathbb{E}^\mathbb{Q} \left[ S_T \mid \mathcal{F}_t \right] = S_t $$
Since the discounted stock price follows a martingale, this means that, under the risk-neutral measure, the expected future value of the discounted stock price is equal to its current value:
$$ \mathbb{E}^\mathbb{Q} \left[ S_T e^{-rT} \mid \mathcal{F}_t \right] = S_t e^{-rt} $$Under the risk-neutral measure, the expected return of the stock is the risk-free rate. This ensures that there exist no risk-free profits, i.e., no arbitrage opportunities.
Applying Euler discretization to our SDE for GBM, the updated asset price at time $t + \delta t$ is computed using the following formula:
$$S_{t + \delta t} = S_{t} * (1 + r \delta t + \sigma \sqrt{\delta} t w_{t})$$Or equivalently in exponential (and more numerically stable) form,
$$ S_{t+\delta t} = S_{t} \exp\left((r - \frac{1}{2}\sigma^{2})\delta t + \sigma \sqrt{\delta t} w_{t}\right) $$Here, $w_{t}$ ~ $N(0,1)$ is a standard normal random variable representing the stochastic component of the price path, $\delta t$ is the time interval, $0 < \delta t \leq T$, and $0 < t \leq T$
After we simulate a large number of price paths, we proceed to Step 2. This step will entail calculating the option payoff for each path. This is where the process for our two types of options (Asian & lookback) will differ. Some things to note:
Our strike prices can be fixed or floating.
We can sample discretely or continuously.
Within Asian options, our average price can be geometric or arithmetic.
Asian Options Payoff¶
The payoff of an Asian option is dependent on the average price of the underlying over a given period; this average can be arithmetic or geometric.
1. Fixed Strike Asian Option¶
The strike price is fixed, meaning that its value $K$ remains regardless of the average price of the underlying.
Arithmetic Average¶
Call Payoff: $$ \max\left( \frac{1}{n} \sum_{i=1}^{n} S_i - K, 0 \right) $$
Put Payoff: $$ \max\left( K - \frac{1}{n} \sum_{i=1}^{n} S_i, 0 \right) $$
Geometric Average¶
Call Payoff: $$ \max\left( \left( \prod_{i=1}^{n} S_i \right)^{\frac{1}{n}} - K, 0 \right) $$
Put Payoff: $$ \max\left( K - \left( \prod_{i=1}^{n} S_i \right)^{\frac{1}{n}}, 0 \right)
$$
2. Floating Strike Asian Option¶
The strike price is floating, meaning that the strike price becomes the average of the underlying over the time horixon of the option.
Arithmetic Average¶
Call Payoff $$ \max \left( S_{T} - \frac{1}{n} \sum_{i=1}^{n} S_i, 0 \right) $$
Put Payoff $$ \max \left( \frac{1}{n} \sum_{i=1}^{n} S_i - S_{T}, 0 \right) $$
Geometric Average¶
Call Payoff $$ \max \left( S_{T} - \left( \prod_{i=1}^{n} S_i \right)^{\frac{1}{n}}, 0 \right) $$
Put Payoff $$ \max \left( \left( \prod_{i=1}^{n} S_i \right)^{\frac{1}{n}} - S_{T}, 0 \right) $$
Lookback Options Payoff¶
Holders of lookback options can essentially 'look back' over the lifetime of the option and choose the most favorable price for exercise. Similarly to Asian options, the strike prices of lookback options can be fixed or floating.
1. Fixed Strike Lookback Option¶
When the strike price is fixed, the value $K$ remains and is compared to the underlying's minimum or maximum price over the lifetime of the option to determine the payoff.
Call Payoff (looks at the highest asset price): $$ \max\left( S_{\max} - K, 0 \right) $$
Put Payoff (looks at the lowest asset price): $$ \max\left( K - S_{\min}, 0 \right) $$
2. Floating Strike Lookback Option¶
When the strike price is floating, it becomes the best (maximum or minimum) price of the asset over the course of the option's lifetime and is compared to the asset price at expiry to determine the payoff.
Call Payoff (strike price is the minimum price): $$ \max\left( S_T - S_{\min}, 0 \right) $$
Put Payoff (strike price is the maximum price): $$ \max\left( S_{\max} - S_T, 0 \right) $$
Sampling: Discrete vs. Continuous¶
Discrete Sampling: The average or extrema (maximum/minimum) is calculated based on specific points in time (e.g., daily, weekly, or monthly).
- The formulas above with summation symbols $ \sum_{i=1}^{n} S_i $ or product symbols $ \prod_{i=1}^{n} S_i $ are examples of discrete sampling.
Continuous Sampling: The average or extrema is calculated continuously over the option’s life. The mathematical representation for continuous sampling involves integrating the price function $S(t)$.
- Arithmetic Average (continuous): $$ \frac{1}{T} \int_0^T S(t) dt $$
- Geometric Average (continuous): $$ \exp\left( \frac{1}{T} \int_0^T \ln(S(t)) dt \right) $$
Within a Monte Carlo framework, we can 'mimic' or approximate a continuous sampling value by using a very large number of very small time steps in our simulation. Essentially, we will be sampling discretely but at a very high frequency to approximate our continuously sampled extrema and averages.
Finally, we move on the Step 3. Here we will find the average payoff for each of our option types. We will then use our discount factor $ e^{-r(T-t)} $ to present value our expected payoff. This will give use the expected value of the discounted payoff under the risk-neutral density Q. This can be written as:
$$ V(S, t) = e^{-r(T-t)} E^{Q} [Payoff(S_{T})] $$We will use the following initial conditions:
- S0 = 100
- K = 100
- time to expiry, (T - t) = 1 year
- volatility $\sigma$ = 20%
- constant risk-free interest rate, r = 5%
- number of simulations = 10,000
- number of timesteps = 252
import warnings
warnings.filterwarnings('ignore')
from pydantic import BaseModel, Field, ValidationError, validator
from typing import List, Tuple
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
pd.set_option('display.max_rows', 300)
class MonteCarloOptionPricing(BaseModel):
"""Monte Carlo Option Pricing Engine"""
S0: float = Field(..., gt=0, description="Initial stock price")
K: float = Field(..., gt=0, alias="strike", description="Strike price")
r: float = Field(..., ge=0, le=1, alias="rate", description="Risk-free rate")
sigma: float = Field(..., gt=0, le=1, description="Volatility")
T: float = Field(..., gt=0, le=10, description="Time to expiration in years")
N: int = Field(..., gt=0, alias="nsim", description="Number of simulations")
ts: int = Field(252, gt=0, alias="timesteps", description="Number of timesteps")
class Config:
populate_by_name = True
@validator('N')
def check_N(cls, v):
if v > 1000000:
raise ValueError("Number of simulations should not exceed 1,000,000")
return v
@property
def df(self) -> float:
"""Discount factor"""
return np.exp(-self.r * self.T)
@property
def pseudorandomnumber(self) -> np.ndarray:
return np.random.standard_normal(self.N)
@property
def simulatepath(self) -> np.ndarray:
"""Simulate asset price paths"""
np.random.seed(13)
dt = self.T / self.ts
paths = np.zeros((self.N, self.ts + 1))
paths[:, 0] = self.S0
for t in range(1, self.ts + 1):
w = self.pseudorandomnumber
paths[:, t] = paths[:, t - 1] * np.exp((self.r - 0.5 * self.sigma**2) * dt + self.sigma * np.sqrt(dt) * w)
return paths
def price_asian_option(self, option_style: str, strike_type: str, avg_type: str) -> float:
"""Price Asian option with different styles and types"""
paths = self.simulatepath
if avg_type == 'arithmetic':
avg_price = np.mean(paths, axis=1)
elif avg_type == 'geometric':
avg_price = np.exp(np.mean(np.log(np.maximum(paths, 1e-10)), axis=1))
if strike_type == 'fixed':
if option_style == 'call':
payoff = np.maximum(avg_price - self.K, 0)
elif option_style == 'put':
payoff = np.maximum(self.K - avg_price, 0)
elif strike_type == 'floating':
if option_style == 'call':
payoff = np.maximum(paths[:, -1] - avg_price, 0)
elif option_style == 'put':
payoff = np.maximum(avg_price - paths[:, -1], 0)
expected_payoff = np.mean(payoff)
discounted_payoff = self.df * expected_payoff
return discounted_payoff
def price_lookback_option(self, option_style: str, strike_type: str) -> float:
"""Price lookback option with different styles and types"""
paths = self.simulatepath
max_price = np.max(paths, axis=1)
min_price = np.min(paths, axis=1)
if strike_type == 'fixed':
if option_style == 'call':
payoff = np.maximum(max_price - self.K, 0)
elif option_style == 'put':
payoff = np.maximum(self.K - min_price, 0)
elif strike_type == 'floating':
if option_style == 'call':
payoff = np.maximum(paths[:, -1] - min_price, 0)
elif option_style == 'put':
payoff = np.maximum(max_price - paths[:, -1], 0)
expected_payoff = np.mean(payoff)
discounted_payoff = self.df * expected_payoff
return discounted_payoff
def price_euro_option(self, option_style: str) -> float:
"""Calculate European option payoff"""
paths = self.simulatepath
if option_style == 'call':
payoff = np.maximum(0, paths[:, -1] - self.K)
elif option_style == 'put':
payoff = np.maximum(0, self.K - paths[:, -1])
expected_payoff = np.mean(payoff)
discounted_payoff = self.df * expected_payoff
return discounted_payoff
def delta(self, option_style: str, h: float = 1e-2) -> float:
"""Estimate the Delta of an option via finite difference."""
up = self.copy(update={'S0': self.S0 + h})
down = self.copy(update={'S0': self.S0 - h})
price_up = up.price_euro_option(option_style)
price_down = down.price_euro_option(option_style)
return (price_up - price_down) / (2 * h)
def gamma(self, option_style: str, h: float = 1e-2) -> float:
up = self.copy(update={'S0': self.S0 + h})
down = self.copy(update={'S0': self.S0 - h})
center = self.price_euro_option(option_style)
price_up = up.price_euro_option(option_style)
price_down = down.price_euro_option(option_style)
return (price_up - 2 * center + price_down) / (h ** 2)
def vega(self, option_style: str, h: float = 1e-2) -> float:
up = self.copy(update={'sigma': self.sigma + h})
down = self.copy(update={'sigma': self.sigma - h})
price_up = up.price_euro_option(option_style)
price_down = down.price_euro_option(option_style)
return (price_up - price_down) / (2 * h)
def rho(self, option_style: str, h: float = 1e-4) -> float:
up = self.copy(update={'r': self.r + h})
down = self.copy(update={'r': self.r - h})
price_up = up.price_euro_option(option_style)
price_down = down.price_euro_option(option_style)
return (price_up - price_down) / (2 * h)
def theta(self, option_style: str, h: float = 1/252) -> float:
shorter = self.copy(update={'T': self.T - h})
price_now = self.price_euro_option(option_style)
price_shorter = shorter.price_euro_option(option_style)
return (price_shorter - price_now) / h # Daily theta
def compute_var_cvar(self, alpha: float = 0.05) -> Tuple[float, float]:
"""Estimate Value at Risk (VaR) and Conditional VaR (CVaR)."""
paths = self.simulatepath
terminal_returns = (paths[:, -1] - self.S0) / self.S0
sorted_returns = np.sort(terminal_returns)
var = -np.percentile(sorted_returns, alpha * 100)
cvar = -sorted_returns[sorted_returns <= -var].mean()
return var, cvar
asset_one = MonteCarloOptionPricing(
S0=100,
K=100,
r=0.05,
sigma=0.2,
T=1,
N=10000,
ts=252
)
simulated_paths = asset_one.simulatepath
plt.figure(figsize=(10, 6))
for i in range(20):
plt.plot(simulated_paths[i], lw=1.5)
plt.title("Simulated Asset Price Paths, Showing 20 of 10,000")
plt.xlabel("Time Steps")
plt.ylabel("Asset Price")
plt.show()
Above, we have generated 10,000 potential price paths for asset one over a one year time horizon. However, now we must check that our simulations meet the necessary properties to proceed with pricing our options. Namely, we will check that our random numbers, log returns, and log terminal prices are normally distributed.
We want the randomness in our simulation to be modeled by normally distributed random numbers to correctly reflect the stochastic portion of GBM, i.e., the Wiener process.
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as stats
random_numbers = asset_one.pseudorandomnumber
fig, axs = plt.subplots(1, 2, figsize=(14, 6))
axs[0].hist(random_numbers, bins=50, density=True, alpha=0.6, color='g', label='Random Numbers')
mu, std = np.mean(random_numbers), np.std(random_numbers)
xmin, xmax = axs[0].get_xlim()
x = np.linspace(xmin, xmax, 100)
p = stats.norm.pdf(x, mu, std)
axs[0].plot(x, p, 'k', linewidth=2, label='Normal Distribution')
axs[0].set_title('Histogram with Normal Distribution')
axs[0].set_xlabel('Random Numbers')
axs[0].set_ylabel('Density')
axs[0].legend()
stats.probplot(random_numbers, dist="norm", plot=axs[1])
axs[1].get_lines()[1].set_color('r')
axs[1].set_title("Q-Q Plot")
plt.tight_layout()
plt.show()
stats.shapiro(random_numbers)
ShapiroResult(statistic=0.9998131103071157, pvalue=0.5764212353248366)
Our p-value is greater than 0.05. This means we accept the null hypothesis, i.e., the distribution of our random numbers does not significanttly differ from the normal distribution.
We will also confirm that our log returns are normally distributed, as assumed by GBM, to ensure that our simulation if behaving according to GBM. Similarly, we will examine the distribution of the logs of stock prices at expiry as GBM assumes stock prices are lognormally distributed.
import matplotlib.pyplot as plt
import seaborn as sns
terminal_prices = simulated_paths[:, -1]
log_terminal_prices = np.log(terminal_prices)
log_returns = np.log(terminal_prices/100)
plt.figure(figsize=(10, 6))
sns.histplot(log_returns.flatten(), bins=50, kde=True, color='blue')
plt.title('Histogram of Log Returns')
plt.xlabel('Log Returns')
plt.ylabel('Frequency')
plt.show()
plt.figure(figsize=(10, 6))
sns.histplot(log_terminal_prices, bins=50, kde=True, color='green')
plt.title('Histogram of Log Terminal Prices')
plt.xlabel('Log Terminal Prices')
plt.ylabel('Frequency')
plt.show()
import scipy.stats as stats
plt.figure(figsize=(6, 6))
stats.probplot(log_returns.flatten(), dist="norm", plot=plt)
plt.title('Q-Q Plot of Log Returns')
plt.show()
plt.figure(figsize=(6, 6))
stats.probplot(log_terminal_prices, dist="norm", plot=plt)
plt.title('Q-Q Plot of Log Terminal Prices')
plt.show()
stats.shapiro(log_terminal_prices)
ShapiroResult(statistic=0.999873964351679, pvalue=0.893872075295794)
stats.shapiro(log_returns)
ShapiroResult(statistic=0.999873964351679, pvalue=0.8938720752957932)
Inspection of our quantile-quantile plots and histograms indicates that our log prices and log returns are normally distributed. This is confirmed by our Shapiro-Wilkes tests which both yielded insignificant results.
Our random numbers, log returns, and log terminal prices are all normally distributed. This provides strong evidence that our simulated price paths are consistent with the assumptions of GBM under the risk-neutral measure. Since these verifications align with GBM, we can conclude that our simulated paths follow the correct SDE and proceed to pricing our options.
# Price different types of options
asian_call_fixed_arith_price = asset_one.price_asian_option(option_style='call', strike_type='fixed', avg_type='arithmetic')
asian_put_fixed_arith_price = asset_one.price_asian_option(option_style='put', strike_type='fixed', avg_type='arithmetic')
asian_call_float_arith_price = asset_one.price_asian_option(option_style='call', strike_type='floating', avg_type='arithmetic')
asian_put_float_arith_price = asset_one.price_asian_option(option_style='put', strike_type='floating', avg_type='arithmetic')
asian_call_fixed_geo_price = asset_one.price_asian_option(option_style='call', strike_type='fixed', avg_type='geometric')
asian_put_fixed_geo_price = asset_one.price_asian_option(option_style='put', strike_type='fixed', avg_type='geometric')
asian_call_float_geo_price = asset_one.price_asian_option(option_style='call', strike_type='floating', avg_type='geometric')
asian_put_float_geo_price = asset_one.price_asian_option(option_style='put', strike_type='floating', avg_type='geometric')
lookback_call_fixed_price = asset_one.price_lookback_option(option_style='call', strike_type='fixed')
lookback_put_fixed_price = asset_one.price_lookback_option(option_style='put', strike_type='fixed')
lookback_call_float_price = asset_one.price_lookback_option(option_style='call', strike_type='floating')
lookback_put_float_price = asset_one.price_lookback_option(option_style='put', strike_type='floating')
# Print out the prices for comparison
print(f"Asian Call Price (Arithmetic Average, Fixed Strike): {asian_call_fixed_arith_price}")
print(f"Asian Put Price (Arithmetic Average, Fixed Strike): {asian_put_fixed_arith_price}")
print(f"Asian Call Price (Arithmetic Average, Floating Strike): {asian_call_float_arith_price}")
print(f"Asian Put Price (Arithmetic Average, Floating Strike): {asian_put_float_arith_price}")
print(f"Asian Call Price (Geometric Average, Fixed Strike): {asian_call_fixed_geo_price}")
print(f"Asian Put Price (Geometric Average, Fixed Strike): {asian_put_fixed_geo_price}")
print(f"Asian Call Price (Geometric Average, Floating Strike): {asian_call_float_geo_price}")
print(f"Asian Put Price (Geometric Average, Floating Strike): {asian_put_float_geo_price}")
print(f"Lookback Call Price (Fixed Strike): {lookback_call_fixed_price}")
print(f"Lookback Put Price (Fixed Strike): {lookback_put_fixed_price}")
print(f"Lookback Call Price (Floating Strike): {lookback_call_float_price}")
print(f"Lookback Put Price (Floating Strike): {lookback_put_float_price}")
Asian Call Price (Arithmetic Average, Fixed Strike): 5.651214937346537 Asian Put Price (Arithmetic Average, Fixed Strike): 3.404484389899415 Asian Call Price (Arithmetic Average, Floating Strike): 5.90412340816781 Asian Put Price (Arithmetic Average, Floating Strike): 3.3965398420616078 Asian Call Price (Geometric Average, Fixed Strike): 5.438963057390688 Asian Put Price (Geometric Average, Fixed Strike): 3.524839581129086 Asian Call Price (Geometric Average, Floating Strike): 6.109420330449768 Asian Put Price (Geometric Average, Floating Strike): 3.269229693158047 Lookback Call Price (Fixed Strike): 18.165327302993553 Lookback Put Price (Fixed Strike): 11.828083702631323 Lookback Call Price (Floating Strike): 16.582397816184645 Lookback Put Price (Floating Strike): 13.411013189440228
With our initial conditions...
Option Type | Price |
---|---|
Asian call (arithmetic average, fixed strike) | 5.65 |
Asian put (arithmetic average, fixed strike) | 3.40 |
Asian call (arithmetic average, floating strike) | 5.90 |
Asian put (arithmetic average, floating strike) | 3.40 |
Asian call (geometric average, fixed strike) | 5.44 |
Asian put (geometric average, fixed strike) | 3.52 |
Asian call (geometric average, floating strike) | 6.11 |
Asian put (geometric average, floating strike) | 3.27 |
Lookback call (fixed strike) | 18.17 |
Lookback put (fixed strike) | 11.83 |
Lookback call (floating strike) | 16.58 |
Lookback put (floating strike) | 13.41 |
We can compare these prices to a European option with the same parameters.
euro_call = asset_one.price_euro_option(option_style='call')
euro_put = asset_one.price_euro_option(option_style='put')
print(f'The price of a European call with these same conditions is {euro_call}; the price of a European put with these same conditions is {euro_put}')
The price of a European call with these same conditions is 10.384383297788135; the price of a European put with these same conditions is 5.63006918423481
By comparing the prices of European options with Asian and lookback options on the same asset, we can see that Asian options are generally less expensive as compared to European options. Lookback options are somewhat more expensive compared to European options.
We can also create an input function to allow the user to price different types of Asian & lookback options with different parameters using Monte Carlo simulation.
def main():
print("Monte Carlo Option Pricing")
try:
S0 = float(input("Enter the initial stock price (S0): "))
K = float(input("Enter the strike price (K): "))
r = float(input("Enter the risk-free interest rate (r): "))
sigma = float(input("Enter the volatility (sigma): "))
T = float(input("Enter the time to maturity (T in years): "))
N = int(input("Enter the number of simulations: "))
ts = int(input("Enter the number of timesteps: "))
except ValueError:
print("Invalid input! Please enter numerical values.")
return
# Initialize MonteCarloOptionPricing
try:
paths = MonteCarloOptionPricing(S0=S0, K=K, r=r, sigma=sigma, T=T, N=N, ts=ts)
except Exception as e:
print(f"Error during Monte Carlo simulation: {e}")
return
# Option Type Input
option_type = input("Enter option type (asian/lookback): ").lower()
if option_type == 'asian':
option_style = input("Enter the option style (call/put): ").lower()
strike_type = input("Enter the strike type (fixed/floating): ").lower()
avg_type = input("Enter the averaging type (arithmetic/geometric): ").lower()
try:
option_price = paths.price_asian_option(option_style, strike_type, avg_type)
print(f"The price of the Asian {option_style} option is: {option_price:.2f}")
except Exception as e:
print(f"Error pricing Asian option: {e}")
elif option_type == 'lookback':
option_style = input("Enter the option style (call/put): ").lower()
strike_type = input("Enter the strike type (fixed/floating): ").lower()
try:
option_price = paths.price_lookback_option(option_style, strike_type)
print(f"The price of the Lookback {option_style} option is: {option_price:.2f}")
except Exception as e:
print(f"Error pricing Lookback option: {e}")
else:
print("Invalid option type selected!")
# Run the main function
main()
Monte Carlo Option Pricing Enter the initial stock price (S0): 100 Enter the strike price (K): 100 Enter the risk-free interest rate (r): .05 Enter the volatility (sigma): .2 Enter the time to maturity (T in years): 1 Enter the number of simulations: 10000 Enter the number of timesteps: 252 Enter option type (asian/lookback): lookback Enter the option style (call/put): call Enter the strike type (fixed/floating): fixed The price of the Lookback call option is: 18.17
We can evaluate how the estimated price of an Asian option converges as the number of paths simulated increases.
At this point, it may be useful to calculate our theoretical Black Scholes price, which can be found for our geometric, fixed strike Asian options, and compare this to our values found through Monte Carlo simulation. We will use Kemna & Vorst's (1990) closed form solution for geometric, fixed strike Asian options given by:
$$ C = Se^{(b - r)(T - t)}N(d_{1}) - Ee^{-r(T-t)}N(d_{2}) $$$$ P = Ee^{-r(T-t)}N(-d_{2})-Se^{(b-r)(T-t)}N(-d_1) $$where, $$ d_{1} = \frac{\log(S/E) + (b + 0.5 \sigma_{adj}^{2})T}{\sigma_{adj} \sqrt{T}} $$
$$ d_{1} = \frac{\log(S/E) + (b - 0.5 \sigma_{adj}^{2})T}{\sigma_{adj} \sqrt{T}} = d_{1} - \sigma_{adj} \sqrt{T} $$Our adjusted volatility, $\sigma_{adj} = \frac{\sigma}{\sqrt{3}}$ and our adjusted dividend yield $b = \frac{1}{2} (r - q - \frac{\sigma^{2}}{6})$. Note that in this case we will set $q$ (our dividend yield) equal to 0.
import numpy as np
from scipy.stats import norm
def bs_asian_option(option_style, S0, E, r, sigma, T):
adj_sigma = sigma / (np.sqrt(3))
b = 0.5 * (r - (sigma**2 / 6))
denominator = adj_sigma * np.sqrt(T)
d1_numerator = np.log(S0 / E) + (b + 0.5 * adj_sigma**2) * T
d1 = d1_numerator / denominator
d2 = d1 - denominator
if option_style == 'call':
price = (S0 * np.exp((b - r) * T) * norm.cdf(d1)) - (E * np.exp(-r * T) * norm.cdf(d2))
elif option_style == 'put':
price = (E * np.exp(-r * T) * norm.cdf(-d2)) - (S0 * np.exp((b - r) * T) * norm.cdf(-d1))
return price
theoretical_asian_call = bs_asian_option('call', 100, 100, .05, .2, 1)
theoretical_asian_put = bs_asian_option('put', 100, 100, .05, .2, 1)
print(f"Geometric Asian Call Price: {theoretical_asian_call}")
print(f"Geometric Asian Put Price: {theoretical_asian_put}")
Geometric Asian Call Price: 5.546818633789201 Geometric Asian Put Price: 3.46333194773856
sim_counts = [1000, 5000, 10000, 50000, 100000]
option_prices_sims_call = []
option_prices_sims_put = []
for N in sim_counts:
asset = MonteCarloOptionPricing(
S0=100,
K=100,
r=0.05,
sigma=.2,
T=1,
N=N,
ts=252
)
call_price = asset.price_asian_option(option_style='call', strike_type='fixed', avg_type='geometric')
option_prices_sims_call.append(call_price)
put_price = asset.price_asian_option(option_style='put', strike_type='fixed', avg_type='geometric')
option_prices_sims_put.append(put_price)
print(option_prices_sims_call)
[5.908338361625674, 5.464611367792476, 5.438963057390688, 5.527920952029866, 5.531302596312566]
print(option_prices_sims_put)
[3.3354839192510224, 3.520859043649533, 3.524839581129086, 3.450915200530052, 3.4620180891103303]
Option Type / No. Simulations | 1000 | 5000 | 10,000 | 50,000 | 100,000 | Theoretical |
---|---|---|---|---|---|---|
Call | 5.9083 | 5.4646 | 5.4390 | 5.5279 | 5.5313 | 5.5468 |
Percent difference | 6.52% | 1.48% | 1.94% | 0.34% | 0.28% | -- |
Put | 3.3355 | 3.5209 | 3.5248 | 3.4509 | 3.4620 | 3.4633 |
Percent difference | 3.69% | 1.66% | 1.78% | 0.36% | 0.04% | -- |
We can notice that as we increase our number of simulations, the price found via Monte Carlo simulation nears the theoretical price for our fixed strike, geometric Asian options. At about 50,000 simulations, the difference in our simulated prices from the Black Scholes theoretical prices for both calls and puts becomes marginal (<0.5%). Beyond this point, the improved accuracy from simulating more paths is very minute and may not be 'worth' the additional computational cost.
Number of timesteps¶
We will also examine how increasing our decreasing our number of timesteps impacts the convergence of our simulated prices with our theoretical prices. One would hypothesize that as the number of timesteps increases, the Monte Carlo simulated price will approach the theoretical Black Scholes price. This is because the Black Scholes model assumes continuous price sampling, and increasing the number of timesteps in the simulation more closely approximates this continuous sampling.
ts_counts = [126, 252, 504, 1008, 2016] #sample every other trading day, 1x trading day, 2x a trading day, 4x a trading day, & 8x a trading day
ts_test_call = []
ts_test_put = []
for ts in ts_counts:
asset = MonteCarloOptionPricing(
S0=100,
K=100,
r=0.05,
sigma=.2,
T=1,
N=10000,
ts=ts
)
call_price = asset.price_asian_option(option_style='call', strike_type='fixed', avg_type='geometric')
ts_test_call.append(call_price)
put_price = asset.price_asian_option(option_style='put', strike_type='fixed', avg_type='geometric')
ts_test_put.append(put_price)
print(ts_test_call)
[5.5070042264251935, 5.438963057390688, 5.474316553441298, 5.5591462372337, 5.579036943597684]
print(ts_test_put)
[3.5245744723410084, 3.524839581129086, 3.4645007961544283, 3.49260045628149, 3.500892210447794]
Option Type/No. Timesteps | 126 | 252 | 504 | 1008 | 2016 | Theoretical |
---|---|---|---|---|---|---|
Call | 5.5070 | 5.4390 | 5.4743 | 5.5591 | 5.5790 | 5.5468 |
Percent difference | 0.72% | 1.94% | 1.32% | 0.22% | 0.58% | -- |
Put | 3.5246 | 3.5248 | 3.4645 | 3.4926 | 3.5009 | 3.4633 |
Percent difference | 1.77% | 1.78% | 0.03% | 0.85% | 1.09% | -- |
Though there are some exceptions, there is a general trend observed that as the number of timesteps increases, the Monte Carlo simulated option prices converge closer to the theoretical values. For the call option, the percent difference decreases from 1.94% at 252 timesteps to 0.22% at 1008 timesteps. The put option converges more quickly, with a minimum percent difference of 0.03% at 504 timesteps. Slight fluctuations in the percent difference at higher timesteps may be due to random sampling variability inherent in the simulation. Overall, using 504–1008 timesteps appears to strike a good balance between accuracy and computational efficiency.
Greeks Estimation¶
delta = asset_one.delta(option_style='call')
gamma = asset_one.gamma(option_style='call')
vega = asset_one.vega(option_style='call')
rho = asset_one.rho(option_style='call')
theta = asset_one.theta(option_style='call')
print(f'Delta is {delta}; gamma is {gamma}; vega is {vega}; rho is {rho}; theta is {theta}')
Delta is 0.630279344660245; gamma is 0.0050296326037369; vega is 37.369851971882625; rho is 52.643551242228526; theta is -6.373918327820739
Explanation of Option Greeks¶
Delta: Measures the sensitivity of the option's price to a \$1 change in the price of the underlying asset. A delta of 0.63 implies the option price increases by approximately \$0.63 for every \$1 increase in the underlying asset price.
Gamma: Measures the rate of change of delta with respect to the underlying asset price. Gamma helps assess the stability of delta; higher gamma indicates more sensitivity and potential need for rehedging.
Vega: Measures the sensitivity of the option price to changes in the volatility of the underlying asset. A vega of 37.37 means the option price increases by \$37.37 for each 1-point increase in implied volatility.
Theta: Represents the time decay of the option's value. A theta of -6.37 suggests the option loses \$6.37 in value each day, all else equal.
Rho: Measures sensitivity to the risk-free rate. A rho of 52.64 means the option price increases by \$52.64 for each 1% increase in the interest rate.
VaR & Conditional VaR¶
asset_one.compute_var_cvar(alpha=0.05)
(0.2577391403715915, 0.3158434285492034)
Value at Risk & Conditional Value at Risk Explanation¶
Value at Risk (VaR) is a risk metric estimating the maximum expected loss over a specified time horizon at a given confidence level. Our VaR of 0.2577 indicates that, with 95% confidence, the option will not degrade in value by more than 25.77% over the next year; there is a 5% chance it could lose more than that.
Conditional Value at Risk (CVaR) or Expected Shortfall estimates the expected loss given the loss exceeds the VaR threshold. In this case, the CVaR is about 31.58%, indicating the average loss in the worst 5% of scenarios.
How does volatility impact option price?¶
lowvolasset = MonteCarloOptionPricing(
S0=100,
K=100,
r=0.05,
sigma=.1,
T=1,
N=10000,
ts=252
)
lowvolsimulation = lowvolasset.simulatepath
lowvol_asian_call_fixed_arith = lowvolasset.price_asian_option(option_style='call', strike_type='fixed', avg_type='arithmetic')
lowvol_asian_put_fixed_arith = lowvolasset.price_asian_option(option_style='put', strike_type='fixed', avg_type='arithmetic')
lowvol_asian_call_float_arith = lowvolasset.price_asian_option(option_style='call', strike_type='floating', avg_type='arithmetic')
lowvol_asian_put_float_arith = lowvolasset.price_asian_option(option_style='put', strike_type='floating', avg_type='arithmetic')
lowvol_asian_call_fixed_geo = lowvolasset.price_asian_option(option_style='call', strike_type='fixed', avg_type='geometric')
lowvol_asian_put_fixed_geo = lowvolasset.price_asian_option(option_style='put', strike_type='fixed', avg_type='geometric')
lowvol_asian_call_float_geo = lowvolasset.price_asian_option(option_style='call', strike_type='floating', avg_type='geometric')
lowvol_asian_put_float_geo = lowvolasset.price_asian_option(option_style='put', strike_type='floating', avg_type='geometric')
lowvol_lookback_call_fixed = lowvolasset.price_lookback_option(option_style='call', strike_type='fixed')
lowvol_lookback_put_fixed = lowvolasset.price_lookback_option(option_style='put', strike_type='fixed')
lowvol_lookback_call_float = lowvolasset.price_lookback_option(option_style='call', strike_type='floating')
lowvol_lookback_put_float = lowvolasset.price_lookback_option(option_style='put', strike_type='floating')
highvolasset = MonteCarloOptionPricing(
S0=100,
K=100,
r=0.05,
sigma=.5,
T=1,
N=10000,
ts=252
)
highvolsimulation = highvolasset.simulatepath
highvol_asian_call_fixed_arith = highvolasset.price_asian_option(option_style='call', strike_type='fixed', avg_type='arithmetic')
highvol_asian_put_fixed_arith = highvolasset.price_asian_option(option_style='put', strike_type='fixed', avg_type='arithmetic')
highvol_asian_call_float_arith = highvolasset.price_asian_option(option_style='call', strike_type='floating', avg_type='arithmetic')
highvol_asian_put_float_arith = highvolasset.price_asian_option(option_style='put', strike_type='floating', avg_type='arithmetic')
highvol_asian_call_fixed_geo = highvolasset.price_asian_option(option_style='call', strike_type='fixed', avg_type='geometric')
highvol_asian_put_fixed_geo = highvolasset.price_asian_option(option_style='put', strike_type='fixed', avg_type='geometric')
highvol_asian_call_float_geo = highvolasset.price_asian_option(option_style='call', strike_type='floating', avg_type='geometric')
highvol_asian_put_float_geo = highvolasset.price_asian_option(option_style='put', strike_type='floating', avg_type='geometric')
highvol_lookback_call_fixed = highvolasset.price_lookback_option(option_style='call', strike_type='fixed')
highvol_lookback_put_fixed = highvolasset.price_lookback_option(option_style='put', strike_type='fixed')
highvol_lookback_call_float = highvolasset.price_lookback_option(option_style='call', strike_type='floating')
highvol_lookback_put_float = highvolasset.price_lookback_option(option_style='put', strike_type='floating')
plt.figure(figsize=(10, 6))
for i in range(20):
plt.plot(lowvolsimulation[i], lw=1.5)
plt.title("Simulated Low Vol Asset Price Paths, Showing 20 of 10,000")
plt.xlabel("Time Steps")
plt.ylabel("Asset Price")
plt.show()
plt.figure(figsize=(10, 6))
for i in range(20):
plt.plot(highvolsimulation[i], lw=1.5)
plt.title("Simulated High Vol Asset Price Paths, Showing 20 of 10,000")
plt.xlabel("Time Steps")
plt.ylabel("Asset Price")
plt.show()
Notice in the above graphs that the simulated price paths for the more volatile asset express more fluctuation from the original stock price as compared to the less volatile asset. The more volatile asset ranges in price from ~25 to ~225, whereas the less volatile asset ranges from ~85 to ~125. How does this difference impact option price?
#Option prices on a low vol asset
print(f"Asian Call Price (Arithmetic Average, Fixed Strike): {lowvol_asian_call_fixed_arith}")
print(f"Asian Put Price (Arithmetic Average, Fixed Strike): {lowvol_asian_put_fixed_arith}")
print(f"Asian Call Price (Arithmetic Average, Floating Strike): {lowvol_asian_call_float_arith}")
print(f"Asian Put Price (Arithmetic Average, Floating Strike): {lowvol_asian_put_float_arith}")
print(f"Asian Call Price (Geometric Average, Fixed Strike): {lowvol_asian_call_fixed_geo}")
print(f"Asian Put Price (Geometric Average, Fixed Strike): {lowvol_asian_put_fixed_geo}")
print(f"Asian Call Price (Geometric Average, Floating Strike): {lowvol_asian_call_float_geo}")
print(f"Asian Put Price (Geometric Average, Floating Strike): {lowvol_asian_put_float_geo}")
print(f"Lookback Call Price (Fixed Strike): {lowvol_lookback_call_fixed}")
print(f"Lookback Put Price (Fixed Strike): {lowvol_lookback_put_fixed}")
print(f"Lookback Call Price (Floating Strike): {lowvol_lookback_call_float}")
print(f"Lookback Put Price (Floating Strike): {lowvol_lookback_put_float}")
Asian Call Price (Arithmetic Average, Fixed Strike): 3.5784189188777886 Asian Put Price (Arithmetic Average, Fixed Strike): 1.245419495216398 Asian Call Price (Arithmetic Average, Floating Strike): 3.7214086025755586 Asian Put Price (Arithmetic Average, Floating Strike): 1.2389991003761671 Asian Call Price (Geometric Average, Fixed Strike): 3.5107201470312397 Asian Put Price (Geometric Average, Fixed Strike): 1.2683043001569196 Asian Call Price (Geometric Average, Floating Strike): 3.7884326568700892 Asian Put Price (Geometric Average, Floating Strike): 1.2154395778836278 Lookback Call Price (Fixed Strike): 10.317669243558964 Lookback Put Price (Fixed Strike): 5.139174029449475 Lookback Call Price (Floating Strike): 9.954582955310258 Lookback Put Price (Floating Strike): 5.502260317698182
#Option prices on a high vol asset
print(f"Asian Call Price (Arithmetic Average, Fixed Strike): {highvol_asian_call_fixed_arith}")
print(f"Asian Put Price (Arithmetic Average, Fixed Strike): {highvol_asian_put_fixed_arith}")
print(f"Asian Call Price (Arithmetic Average, Floating Strike): {highvol_asian_call_float_arith}")
print(f"Asian Put Price (Arithmetic Average, Floating Strike): {highvol_asian_put_float_arith}")
print(f"Asian Call Price (Geometric Average, Fixed Strike): {highvol_asian_call_fixed_geo}")
print(f"Asian Put Price (Geometric Average, Fixed Strike): {highvol_asian_put_fixed_geo}")
print(f"Asian Call Price (Geometric Average, Floating Strike): {highvol_asian_call_float_geo}")
print(f"Asian Put Price (Geometric Average, Floating Strike): {highvol_asian_put_float_geo}")
print(f"Lookback Call Price (Fixed Strike): {highvol_lookback_call_fixed}")
print(f"Lookback Put Price (Fixed Strike): {highvol_lookback_put_fixed}")
print(f"Lookback Call Price (Floating Strike): {highvol_lookback_call_float}")
print(f"Lookback Put Price (Floating Strike): {highvol_lookback_put_float}")
Asian Call Price (Arithmetic Average, Fixed Strike): 12.049002697555537 Asian Put Price (Arithmetic Average, Fixed Strike): 10.056576415386877 Asian Call Price (Arithmetic Average, Floating Strike): 12.624543335637133 Asian Put Price (Arithmetic Average, Floating Strike): 10.024975525385797 Asian Call Price (Geometric Average, Fixed Strike): 10.888358643097245 Asian Put Price (Geometric Average, Fixed Strike): 10.909194109433825 Asian Call Price (Geometric Average, Floating Strike): 13.686690658749443 Asian Put Price (Geometric Average, Floating Strike): 9.073861099992865 Lookback Call Price (Fixed Strike): 44.822043880905525 Lookback Put Price (Fixed Strike): 29.887528445049146 Lookback Call Price (Floating Strike): 34.479522537469144 Lookback Put Price (Floating Strike): 40.23004978848552
Option Type | Price @ 10% vol | Price @ 20% vol | Price @ 50% vol |
---|---|---|---|
Asian call (arithmetic average, fixed strike) | 3.58 | 5.65 | 12.05 |
Asian put (arithmetic average, fixed strike) | 1.25 | 3.40 | 10.06 |
Asian call (arithmetic average, floating strike) | 3.72 | 5.90 | 12.62 |
Asian put (arithmetic average, floating strike) | 1.24 | 3.40 | 10.02 |
Asian call (geometric average, fixed strike) | 3.51 | 5.44 | 10.89 |
Asian put (geometric average, fixed strike) | 1.27 | 3.52 | 10.91 |
Asian call (geometric average, floating strike) | 3.79 | 6.11 | 13.69 |
Asian put (geometric average, floating strike) | 1.22 | 3.27 | 9.07 |
Lookback call (fixed strike) | 10.32 | 18.17 | 44.82 |
Lookback put (fixed strike) | 5.14 | 11.83 | 29.89 |
Lookback call (floating strike) | 9.95 | 16.58 | 34.48 |
Lookback put (floating strike) | 5.50 | 13.41 | 40.23 |
volatility_list = np.linspace(0.1, 1, 10)
option_prices_vol = []
for sigma in volatility_list:
asset = MonteCarloOptionPricing(
S0=100,
K=100,
r=0.05,
sigma=sigma,
T=1,
N=10000,
ts=252
)
price = asset.price_asian_option(option_style='put', strike_type='floating', avg_type='geometric')
option_prices_vol.append(price)
plt.figure(figsize=(10, 6))
plt.plot(volatility_list, option_prices_vol, marker='o', label='Option Price')
plt.xlabel('Volatility')
plt.ylabel('Option Price')
plt.title('Option Price vs Volatility for Asian Put (Floating, Geometric)')
plt.grid(True)
plt.legend()
plt.show()
In general, options placed on more volatile assets are more expensive; this relationship between option price and volatility is linear. A higher volatility boosts the chance that the underlying will rise well above (for a call) or well below (for a put) the strike price. This corresponds to greater potential profits for the holder of the option, making the option more valuable.
How does time to expiry impact option price¶
short_asset = MonteCarloOptionPricing(
S0=100,
K=100,
r=0.05,
sigma=.2,
T=.5,
N=10000,
ts=252
)
short_simulation = short_asset.simulatepath
short_asian_call_fixed_arith = short_asset.price_asian_option(option_style='call', strike_type='fixed', avg_type='arithmetic')
short_asian_put_fixed_arith = short_asset.price_asian_option(option_style='put', strike_type='fixed', avg_type='arithmetic')
short_asian_call_float_arith = short_asset.price_asian_option(option_style='call', strike_type='floating', avg_type='arithmetic')
short_asian_put_float_arith = short_asset.price_asian_option(option_style='put', strike_type='floating', avg_type='arithmetic')
short_asian_call_fixed_geo = short_asset.price_asian_option(option_style='call', strike_type='fixed', avg_type='geometric')
short_asian_put_fixed_geo = short_asset.price_asian_option(option_style='put', strike_type='fixed', avg_type='geometric')
short_asian_call_float_geo = short_asset.price_asian_option(option_style='call', strike_type='floating', avg_type='geometric')
short_asian_put_float_geo = short_asset.price_asian_option(option_style='put', strike_type='floating', avg_type='geometric')
short_lookback_call_fixed = short_asset.price_lookback_option(option_style='call', strike_type='fixed')
short_lookback_put_fixed = short_asset.price_lookback_option(option_style='put', strike_type='fixed')
short_lookback_call_float = short_asset.price_lookback_option(option_style='call', strike_type='floating')
short_lookback_put_float = short_asset.price_lookback_option(option_style='put', strike_type='floating')
long_asset = MonteCarloOptionPricing(
S0=100,
K=100,
r=0.05,
sigma=.2,
T=2,
N=10000,
ts=252
)
long_simulation = long_asset.simulatepath
long_asian_call_fixed_arith = long_asset.price_asian_option(option_style='call', strike_type='fixed', avg_type='arithmetic')
long_asian_put_fixed_arith = long_asset.price_asian_option(option_style='put', strike_type='fixed', avg_type='arithmetic')
long_asian_call_float_arith = long_asset.price_asian_option(option_style='call', strike_type='floating', avg_type='arithmetic')
long_asian_put_float_arith = long_asset.price_asian_option(option_style='put', strike_type='floating', avg_type='arithmetic')
long_asian_call_fixed_geo = long_asset.price_asian_option(option_style='call', strike_type='fixed', avg_type='geometric')
long_asian_put_fixed_geo = long_asset.price_asian_option(option_style='put', strike_type='fixed', avg_type='geometric')
long_asian_call_float_geo = long_asset.price_asian_option(option_style='call', strike_type='floating', avg_type='geometric')
long_asian_put_float_geo = long_asset.price_asian_option(option_style='put', strike_type='floating', avg_type='geometric')
long_lookback_call_fixed = long_asset.price_lookback_option(option_style='call', strike_type='fixed')
long_lookback_put_fixed = long_asset.price_lookback_option(option_style='put', strike_type='fixed')
long_lookback_call_float = long_asset.price_lookback_option(option_style='call', strike_type='floating')
long_lookback_put_float = long_asset.price_lookback_option(option_style='put', strike_type='floating')
#Prices of options with shorter lifespan
print(f"Asian Call Price (Arithmetic Average, Fixed Strike): {short_asian_call_fixed_arith}")
print(f"Asian Put Price (Arithmetic Average, Fixed Strike): {short_asian_put_fixed_arith}")
print(f"Asian Call Price (Arithmetic Average, Floating Strike): {short_asian_call_float_arith}")
print(f"Asian Put Price (Arithmetic Average, Floating Strike): {short_asian_put_float_arith}")
print(f"Asian Call Price (Geometric Average, Fixed Strike): {short_asian_call_fixed_geo}")
print(f"Asian Put Price (Geometric Average, Fixed Strike): {short_asian_put_fixed_geo}")
print(f"Asian Call Price (Geometric Average, Floating Strike): {short_asian_call_float_geo}")
print(f"Asian Put Price (Geometric Average, Floating Strike): {short_asian_put_float_geo}")
print(f"Lookback Call Price (Fixed Strike): {short_lookback_call_fixed}")
print(f"Lookback Put Price (Fixed Strike): {short_lookback_put_fixed}")
print(f"Lookback Call Price (Floating Strike): {short_lookback_call_float}")
print(f"Lookback Put Price (Floating Strike): {short_lookback_put_float}")
Asian Call Price (Arithmetic Average, Fixed Strike): 3.777355581897197 Asian Put Price (Arithmetic Average, Fixed Strike): 2.669914296383169 Asian Call Price (Arithmetic Average, Floating Strike): 3.9167635163712253 Asian Put Price (Arithmetic Average, Floating Strike): 2.6423206337462903 Asian Call Price (Geometric Average, Fixed Strike): 3.6784279638337685 Asian Put Price (Geometric Average, Fixed Strike): 2.737172640756545 Asian Call Price (Geometric Average, Floating Strike): 4.0135710382042005 Asian Put Price (Geometric Average, Floating Strike): 2.572942193142461 Lookback Call Price (Fixed Strike): 12.258386741360638 Lookback Put Price (Fixed Strike): 9.101225772729514 Lookback Call Price (Floating Strike): 11.483109940868477 Lookback Put Price (Floating Strike): 9.876502573221677
#Prices of options with longer lifespan
print(f"Asian Call Price (Arithmetic Average, Fixed Strike): {long_asian_call_fixed_arith}")
print(f"Asian Put Price (Arithmetic Average, Fixed Strike): {long_asian_put_fixed_arith}")
print(f"Asian Call Price (Arithmetic Average, Floating Strike): {long_asian_call_float_arith}")
print(f"Asian Put Price (Arithmetic Average, Floating Strike): {long_asian_put_float_arith}")
print(f"Asian Call Price (Geometric Average, Fixed Strike): {long_asian_call_fixed_geo}")
print(f"Asian Put Price (Geometric Average, Fixed Strike): {long_asian_put_fixed_geo}")
print(f"Asian Call Price (Geometric Average, Floating Strike): {long_asian_call_float_geo}")
print(f"Asian Put Price (Geometric Average, Floating Strike): {long_asian_put_float_geo}")
print(f"Lookback Call Price (Fixed Strike): {long_lookback_call_fixed}")
print(f"Lookback Put Price (Fixed Strike): {long_lookback_put_fixed}")
print(f"Lookback Call Price (Floating Strike): {long_lookback_call_float}")
print(f"Lookback Put Price (Floating Strike): {long_lookback_put_float}")
Asian Call Price (Arithmetic Average, Fixed Strike): 8.53859542813407 Asian Put Price (Arithmetic Average, Fixed Strike): 4.098699658848972 Asian Call Price (Arithmetic Average, Floating Strike): 9.062545526375983 Asian Put Price (Arithmetic Average, Floating Strike): 4.157853745278628 Asian Call Price (Geometric Average, Fixed Strike): 8.076394426867983 Asian Put Price (Geometric Average, Fixed Strike): 4.302843290292338 Asian Call Price (Geometric Average, Floating Strike): 9.50590439136386 Asian Put Price (Geometric Average, Floating Strike): 3.9348679775570528 Lookback Call Price (Fixed Strike): 27.168698619148415 Lookback Put Price (Fixed Strike): 14.673342333019425 Lookback Call Price (Floating Strike): 24.017929883401873 Lookback Put Price (Floating Strike): 17.82411106876596
Option Type | Price (6 months) | Price (1 year) | Price (2 years) |
---|---|---|---|
Asian call (arithmetic average, fixed strike) | 3.78 | 5.65 | 8.54 |
Asian put (arithmetic average, fixed strike) | 2.67 | 3.40 | 4.10 |
Asian call (arithmetic average, floating strike) | 3.92 | 5.90 | 9.06 |
Asian put (arithmetic average, floating strike) | 2.64 | 3.40 | 4.16 |
Asian call (geometric average, fixed strike) | 3.68 | 5.44 | 8.08 |
Asian put (geometric average, fixed strike) | 2.74 | 3.52 | 4.30 |
Asian call (geometric average, floating strike) | 4.01 | 6.11 | 9.51 |
Asian put (geometric average, floating strike) | 2.57 | 3.27 | 3.93 |
Lookback call (fixed strike) | 12.26 | 18.17 | 27.17 |
Lookback put (fixed strike) | 9.10 | 11.83 | 14.67 |
Lookback call (floating strike) | 11.48 | 16.58 | 24.02 |
Lookback put (floating strike) | 9.88 | 13.41 | 17.82 |
time_horizons = [.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5]
option_prices_time = []
for T in time_horizons:
asset = MonteCarloOptionPricing(
S0=100,
K=100,
r=0.05,
sigma=0.2,
T=T,
N=10000,
ts=252
)
price = asset.price_asian_option(option_style='call', strike_type='fixed', avg_type='arithmetic')
option_prices_time.append(price)
plt.figure(figsize=(10, 6))
plt.plot(time_horizons, option_prices_time, marker='o', label='Option Price')
plt.xlabel('Time Horizon')
plt.ylabel('Option Price')
plt.title('Option Price vs Time Horizon for Asian Option (Fixed, Arithmetic)')
plt.grid(True)
plt.legend()
plt.show()
Options with a longer time horizon are, in general, more expensive. With a longer period, the underlying has more time to fluctuate in price, thus increasing the chance that the option will be profitable, i.e., that the stock underlying will rise to above the strike for a call or fall below the strike for a put.
How does risk-free rate impact option price¶
lowRFasset = MonteCarloOptionPricing(
S0=100,
K=100,
r=0.02,
sigma=.2,
T=1,
N=10000,
ts=252
)
lowRFsimulation = lowRFasset.simulatepath
lowRF_asian_call_fixed_arith = lowRFasset.price_asian_option(option_style='call', strike_type='fixed', avg_type='arithmetic')
lowRF_asian_put_fixed_arith = lowRFasset.price_asian_option(option_style='put', strike_type='fixed', avg_type='arithmetic')
lowRF_asian_call_float_arith = lowRFasset.price_asian_option(option_style='call', strike_type='floating', avg_type='arithmetic')
lowRF_asian_put_float_arith = lowRFasset.price_asian_option(option_style='put', strike_type='floating', avg_type='arithmetic')
lowRF_asian_call_fixed_geo = lowRFasset.price_asian_option(option_style='call', strike_type='fixed', avg_type='geometric')
lowRF_asian_put_fixed_geo = lowRFasset.price_asian_option(option_style='put', strike_type='fixed', avg_type='geometric')
lowRF_asian_call_float_geo = lowRFasset.price_asian_option(option_style='call', strike_type='floating', avg_type='geometric')
lowRF_asian_put_float_geo = lowRFasset.price_asian_option(option_style='put', strike_type='floating', avg_type='geometric')
lowRF_lookback_call_fixed = lowRFasset.price_lookback_option(option_style='call', strike_type='fixed')
lowRF_lookback_put_fixed = lowRFasset.price_lookback_option(option_style='put', strike_type='fixed')
lowRF_lookback_call_float = lowRFasset.price_lookback_option(option_style='call', strike_type='floating')
lowRF_lookback_put_float = lowRFasset.price_lookback_option(option_style='put', strike_type='floating')
highRFasset = MonteCarloOptionPricing(
S0=100,
K=100,
r=0.07,
sigma=.2,
T=1,
N=10000,
ts=252
)
highRFsimulation = highRFasset.simulatepath
highRF_asian_call_fixed_arith = highRFasset.price_asian_option(option_style='call', strike_type='fixed', avg_type='arithmetic')
highRF_asian_put_fixed_arith = highRFasset.price_asian_option(option_style='put', strike_type='fixed', avg_type='arithmetic')
highRF_asian_call_float_arith = highRFasset.price_asian_option(option_style='call', strike_type='floating', avg_type='arithmetic')
highRF_asian_put_float_arith = highRFasset.price_asian_option(option_style='put', strike_type='floating', avg_type='arithmetic')
highRF_asian_call_fixed_geo = highRFasset.price_asian_option(option_style='call', strike_type='fixed', avg_type='geometric')
highRF_asian_put_fixed_geo = highRFasset.price_asian_option(option_style='put', strike_type='fixed', avg_type='geometric')
highRF_asian_call_float_geo = highRFasset.price_asian_option(option_style='call', strike_type='floating', avg_type='geometric')
highRF_asian_put_float_geo = highRFasset.price_asian_option(option_style='put', strike_type='floating', avg_type='geometric')
highRF_lookback_call_fixed = highRFasset.price_lookback_option(option_style='call', strike_type='fixed')
highRF_lookback_put_fixed = highRFasset.price_lookback_option(option_style='put', strike_type='fixed')
highRF_lookback_call_float = highRFasset.price_lookback_option(option_style='call', strike_type='floating')
highRF_lookback_put_float = highRFasset.price_lookback_option(option_style='put', strike_type='floating')
#Prices of options with lower risk-free rate
print(f"Asian Call Price (Arithmetic Average, Fixed Strike): {lowRF_asian_call_fixed_arith}")
print(f"Asian Put Price (Arithmetic Average, Fixed Strike): {lowRF_asian_put_fixed_arith}")
print(f"Asian Call Price (Arithmetic Average, Floating Strike): {lowRF_asian_call_float_arith}")
print(f"Asian Put Price (Arithmetic Average, Floating Strike): {lowRF_asian_put_float_arith}")
print(f"Asian Call Price (Geometric Average, Fixed Strike): {lowRF_asian_call_fixed_geo}")
print(f"Asian Put Price (Geometric Average, Fixed Strike): {lowRF_asian_put_fixed_geo}")
print(f"Asian Call Price (Geometric Average, Floating Strike): {lowRF_asian_call_float_geo}")
print(f"Asian Put Price (Geometric Average, Floating Strike): {lowRF_asian_put_float_geo}")
print(f"Lookback Call Price (Fixed Strike): {lowRF_lookback_call_fixed}")
print(f"Lookback Put Price (Fixed Strike): {lowRF_lookback_put_fixed}")
print(f"Lookback Call Price (Floating Strike): {lowRF_lookback_call_float}")
print(f"Lookback Put Price (Floating Strike): {lowRF_lookback_put_float}")
Asian Call Price (Arithmetic Average, Fixed Strike): 4.946115183777702 Asian Put Price (Arithmetic Average, Fixed Strike): 4.132558768126911 Asian Call Price (Arithmetic Average, Floating Strike): 5.129618384797261 Asian Put Price (Arithmetic Average, Floating Strike): 4.08578556749886 Asian Call Price (Geometric Average, Fixed Strike): 4.7619535029137285 Asian Put Price (Geometric Average, Fixed Strike): 4.278227459791804 Asian Call Price (Geometric Average, Floating Strike): 5.307785685682356 Asian Put Price (Geometric Average, Floating Strike): 3.934122495855089 Lookback Call Price (Fixed Strike): 16.826294946959777 Lookback Put Price (Fixed Strike): 13.36106381821911 Lookback Call Price (Floating Strike): 15.2184530511683 Lookback Put Price (Floating Strike): 14.968905714010589
#Prices of options with higher risk-free rate
print(f"Asian Call Price (Arithmetic Average, Fixed Strike): {highRF_asian_call_fixed_arith}")
print(f"Asian Put Price (Arithmetic Average, Fixed Strike): {highRF_asian_put_fixed_arith}")
print(f"Asian Call Price (Arithmetic Average, Floating Strike): {highRF_asian_call_float_arith}")
print(f"Asian Put Price (Arithmetic Average, Floating Strike): {highRF_asian_put_float_arith}")
print(f"Asian Call Price (Geometric Average, Fixed Strike): {highRF_asian_call_fixed_geo}")
print(f"Asian Put Price (Geometric Average, Fixed Strike): {highRF_asian_put_fixed_geo}")
print(f"Asian Call Price (Geometric Average, Floating Strike): {highRF_asian_call_float_geo}")
print(f"Asian Put Price (Geometric Average, Floating Strike): {highRF_asian_put_float_geo}")
print(f"Lookback Call Price (Fixed Strike): {highRF_lookback_call_fixed}")
print(f"Lookback Put Price (Fixed Strike): {highRF_lookback_put_fixed}")
print(f"Lookback Call Price (Floating Strike): {highRF_lookback_call_float}")
print(f"Lookback Put Price (Floating Strike): {highRF_lookback_put_float}")
Asian Call Price (Arithmetic Average, Fixed Strike): 6.145485487084342 Asian Put Price (Arithmetic Average, Fixed Strike): 2.9749145364875886 Asian Call Price (Arithmetic Average, Floating Strike): 6.451722525777454 Asian Put Price (Arithmetic Average, Floating Strike): 2.984418903344315 Asian Call Price (Geometric Average, Fixed Strike): 5.913138055439528 Asian Put Price (Geometric Average, Fixed Strike): 3.0809890581613053 Asian Call Price (Geometric Average, Floating Strike): 6.677898307200043 Asian Put Price (Geometric Average, Floating Strike): 2.8721727314483756 Lookback Call Price (Fixed Strike): 19.09641888755899 Lookback Put Price (Fixed Strike): 10.895895950696035 Lookback Call Price (Floating Strike): 17.53377052372593 Lookback Put Price (Floating Strike): 12.458544314529098
Option Type | Price (RF rate .02) | Price (RF rate .05) | Price (RF rate .07) |
---|---|---|---|
Asian call (arithmetic average, fixed strike) | 4.95 | 5.65 | 6.15 |
Asian put (arithmetic average, fixed strike) | 4.13 | 3.40 | 2.97 |
Asian call (arithmetic average, floating strike) | 5.13 | 5.90 | 6.45 |
Asian put (arithmetic average, floating strike) | 4.09 | 3.40 | 2.98 |
Asian call (geometric average, fixed strike) | 4.76 | 5.44 | 5.91 |
Asian put (geometric average, fixed strike) | 4.28 | 3.52 | 3.08 |
Asian call (geometric average, floating strike) | 5.31 | 6.11 | 6.68 |
Asian put (geometric average, floating strike) | 3.93 | 3.27 | 2.87 |
Lookback call (fixed strike) | 16.83 | 18.17 | 19.10 |
Lookback put (fixed strike) | 13.36 | 11.83 | 10.90 |
Lookback call (floating strike) | 15.22 | 16.58 | 17.53 |
Lookback put (floating strike) | 14.97 | 13.41 | 12.46 |
rf_list = np.linspace(0.1, 1, 10)
option_prices_rfcall = []
for r in rf_list:
asset = MonteCarloOptionPricing(
S0=100,
K=100,
r=r,
sigma=.2,
T=1,
N=10000,
ts=252
)
price = asset.price_asian_option(option_style='call', strike_type='fixed', avg_type='arithmetic')
option_prices_rfcall.append(price)
plt.figure(figsize=(10, 6))
plt.plot(rf_list, option_prices_rfcall, marker='o', label='Option Price')
plt.xlabel('Risk Free Rate')
plt.ylabel('Option Price')
plt.title('Option Price vs Risk Free Rate for Asian Call Option (Fixed, Arithmetic)')
plt.grid(True)
plt.legend()
plt.show()
option_prices_rfput = []
for r in rf_list:
asset = MonteCarloOptionPricing(
S0=100,
K=100,
r=r,
sigma=.2,
T=1,
N=10000,
ts=252
)
price = asset.price_asian_option(option_style='put', strike_type='fixed', avg_type='arithmetic')
option_prices_rfput.append(price)
plt.figure(figsize=(10, 6))
plt.plot(rf_list, option_prices_rfput, marker='o', label='Option Price')
plt.xlabel('Risk Free Rate')
plt.ylabel('Option Price')
plt.title('Option Price vs Risk Free Rate for Asian Put Option (Fixed, Arithmetic)')
plt.grid(True)
plt.legend()
plt.show()
For call options, higher risk-free rates increase price; the converse is true for put options. Both options demonstrate non-linear behavior with respect to the risk-free rate. This analysis suggests that in an environment with a higher risk-free rate, call options become significantly more valuable, whereas put options lose much of their value.
Conclusions¶
We can draw certain conclusions from the above observations:
Asian options are generally cheaper, while lookback options tend to be more expensive than plain vanilla options, assuming identical parameters. Asian Options average the underlying asset's price over time, reducing the impact of extreme price movements, leading to lower option prices. Lookback options, however, allow the option holder to select the optimal price over a given time period, making them more valuable relative to plain vanilla options.
As the number of simulations increases, the Monte Carlo estimated price converges toward the theoretical price. However, beyond a large number of simulations (~10,000-50,000), the improvement in accuracy becomes marginal, with diminishing returns from additional simulations, i.e., after certain point, the reduction in estimation error from adding more simulated price paths become negligible. This would indicate that beyond a certain number of simulations, the improvement in accuracy from simulating more paths would not be worth the additional computational time or effort of doing so.
Increasing the frequency of sampling within each path brings the Monte Carlo simulated price closer to the theoretical value. More frequent sampling captures price movements more accurately and approximates the continuous sampling assumed by the Black Scholes model, thus reducing discretization error and leading to a more precise estimate of the option price.
Options on more volatile assets are more expensive due to the greater likelihood that the underlying asset's price will move into a highly profitable range. Similarly, options with longer maturities tend to be more expensive as the extended time horizon increases the potential for significant price changes.
Higher risk-free rates increase the price of call options but reduce the price of put options. A higher risk-free rate reduces the present value of the strike price payment and increases the expected future price of the underlying asset. This benefits call options by increasing the likelihood of a higher payoff, while it reduces the value of put options, as the expected future price of the underlying is less likely to fall below the strike price.
References¶
Haug, E. (1997). The Complete Guide to Option Pricing Formulas. McGraw Hill.
Jackel, P. (2002). Monte Carlo Methods in Finance. John Wiley & Sons.
Kemna, & Vorst, (1990). Pricing options based on asset price averages
Singaravelu, K. (2024). Monte Carlo Option Pricing. [Python Lab].
Wilmott, P. (2007). Paul Wilmott Introduces Quantitative Finance. John Wiley & Sons.
Wilmott, P. (2006). Paul Wilmott on Quantitative Finance. John Wiley & Sons.