import yfinance as yf
import pandas as pd
import numpy as np
from scipy.optimize import minimize_RISK_FREE_RATE = 0.03# Download AAPL stock data and options data
ticker = "AAPL"
stock = yf.Ticker(ticker)
# Define the expiration date for the options (example: closest expiry date)
expiry_date = stock.options[0]
# Get the options chain for the given expiry date
options_chain = stock.option_chain(expiry_date)
# Filter out deep out-of-the-money (OTM) options
# For example, if AAPL is trading at $150, consider calls with strike > $180 and puts with strike < $120
spot_price = stock.history(period="1d")["Close"].iloc[-1]
deep_otm_calls = options_chain.calls[options_chain.calls["strike"] > spot_price * 1.2]
deep_otm_puts = options_chain.puts[options_chain.puts["strike"] < spot_price * 0.8]
# Combine and preview the data
deep_otm_options = pd.concat([deep_otm_calls, deep_otm_puts])
deep_otm_options["expiry"] = expiry_datedef simulate_merton_jump(
init_price: float,
expiry: int,
mu: float,
sigma: float,
lambda_jump: float,
jump_mean: float,
jump_vol: float,
num_paths: int = 100,
num_steps: int = 252,
) -> np.ndarray:
"""
Simulate Merton jump diffusion paths using Monte Carlo simulation.
For instance, 30 days (physical date) rather than trading days to expiry, the T = 30/365.
Args:
S0: initial stock price.
T: time to maturity.
mu: drift term.
sigma: diffusion term.
lambda_jump: jump intensity, the average number of jumps per year.
jump_mean: mean of jump size distribution.
jump_std: standard deviation of jump size distribution.
num_paths: number of simulated paths.
num_steps: number of time steps, default to number of trading days in a year.
"""
dt = expiry / num_steps
paths = np.zeros(shape=(num_paths, num_steps + 1))
paths[:, 0] = init_price # Initial stock price for all paths
for t in range(1, num_steps + 1):
W = np.random.normal(loc=0, scale=1, size=num_paths) # Standard Wiener process
paths[:, t] = paths[:, t - 1] * np.exp(
(mu - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * W
)
num_jumps = np.random.poisson(lam=lambda_jump * dt, size=num_paths)
jump_sizes = np.exp(jump_mean + jump_vol * np.random.normal(0, 1, num_paths))
paths[:, t] *= np.where(num_jumps > 0, jump_sizes, 1)
return pathsdef merton_pricing(
*,
S0: float,
K: float,
T: int,
r: float,
mu: float,
sigma: float,
lambda_jump: float,
jump_mean: float,
jump_vol: float,
num_paths,
option_type: str,
) -> float:
"""
Price European options under Merton jump diffusion model using simulated path.
First generate simulated paths using the Merton jump diffusion model,
then calculate the option price using the risk-neutral pricing formula.
Args:
S0: initial stock price.
K: strike price.
T: time to maturity.
r: risk-free rate.
mu: drift term.
sigma: diffusion term.
lambda_jump: jump intensity, the average number of jumps per year.
jump_mean: mean of jump size distribution.
jump_std: standard deviation of jump size distribution.
num_paths: number of simulated paths.
option_type: "call" or "put".
Returns:
BS option price.
"""
paths = simulate_merton_jump(
init_price=S0,
expiry=T,
mu=mu,
sigma=sigma,
lambda_jump=lambda_jump,
jump_mean=jump_mean,
jump_vol=jump_vol,
num_paths=num_paths,
)
terminal_prices = paths[:, -1] # Extract terminal prices
if option_type == "call":
payoffs = np.maximum(terminal_prices - K, 0)
else:
payoffs = np.maximum(K - terminal_prices, 0)
return np.exp(-r * T) * np.mean(payoffs) # risk neutral pricingdef objective_func(params: list, market_data, init_value, risk_free_rate) -> float:
"""
Objective function to minimize the sum of squared
errors between market prices and model prices.
Args:
params: model parameters, [mu, sigma, lambda_jump, jump_mean, jump_std].
market_data: option chain data.
init_value: initial stock price.
risk_free_rate: risk-free rate.
Returns:
Sum of squared errors.
"""
mu, sigma, lambda_jump, jump_mean, jump_std = params
error = 0
for _, row in market_data.iterrows():
strike = row["strike"]
expiry = (pd.to_datetime(row["expiry"]) - pd.Timestamp.now()).days / 365
market_price = (row["bid"] + row["ask"]) / 2
model_price = merton_pricing(
S0=init_value,
K=strike,
T=expiry,
r=risk_free_rate,
mu=mu,
sigma=sigma,
lambda_jump=lambda_jump,
jump_mean=jump_mean,
jump_vol=jump_std,
num_paths=100,
option_type="call",
)
error += (market_price - model_price) ** 2
return error# Initial guesses for parameters
initial_params = [
0.05, # mu
0.2, # sigma
0.1, # lambda_jump
-0.1, # jump_mean
0.1, # jump_std
]
# Calibrate using deep OTM options
result = minimize(
objective_func,
initial_params,
args=(deep_otm_options, spot_price, _RISK_FREE_RATE),
method="Nelder-Mead",
)
calibrated_params = result.x
print("Calibrated parameters:", calibrated_params)Calibrated parameters: [ 0.05200766 0.20268906 0.100778 -0.10256474 0.09463834]