Options Pricing with Black-Scholes and Gradient Ascent from scratch
January 20, 2024 / 7 min read
Last Updated: January 20, 2024As professor Richard Berner once said, "all models are wrong, but some are useful." In financial economics, we have a plethora of models to choose from, each with its own assumptions and limitations.
Last time, we did credit forecasting with the Nelson-Siegel model. This time, we will delve into the world of options pricing. In this post, we will explore the famous Black-Scholes option pricing model to estimate the price of a call option on Apple stock.
Along the way, we will cover the application of the following topics:
- Black-Scholes Options Pricing
- Maximum Likelihood Estimation via Gradient Ascent
- Closed-Form Estimation of Volatility & Fisher Information Matrix
Apple stock was selling at 177.70 per share at the close of 10/31/2023. We will use
1. Retrieve Historical Data
1library('quantmod')2library('maxLik')34ticker <- 'AAPL'5start_date <- as.Date("2023-03-01")6end_date <- as.Date("2023-10-31")7getSymbols(ticker, src = "yahoo", from = start_date, to = end_date)8apple_data <- as.data.frame(AAPL)
2. Parametric Estimation
2.1 Log-Likelihood Function
Let
Then for any
For
Taking the natural logarithm of the likelihood function, we convert the product into a sum:
In
1# negative log-likelihood function2log_likelihood <- function(params, log_returns, delta) {3alpha <- params[1]4sigma <- params[2]5n <- length(log_returns)67if(sigma <= 0) return(-Inf) # Ensure sigma is positive89# Calculate the log likelihood10ll <- -n/2 * log(2 * pi * sigma^2 * delta) - 1/(2 * sigma^2 * delta) * sum((log_returns - alpha * delta)^2)11return(ll)12}
2.2 Gradient Ascent and Maximum Likelihood
We apply the gradient ascent algorithm to maximize the log-likelihood function by iteratively moving towards the maximum value of the function. The gradient
1gradient <- function(params, log_returns, delta) {2alpha <- params[1]3sigma <- params[2]4n <- length(log_returns)56dL_dalpha <- 1/(sigma^2) * sum(log_returns - alpha * delta)7dL_dsigma <- -n/sigma + 1/(delta * sigma^3) * sum((log_returns - alpha * delta)^2)8return(c(dL_dalpha, dL_dsigma))9}
We then apply optimization with initial parameters guesses and proceed with small increments (determined by our learning rate) for large iterations or when the model converges:
Gradient Ascent
1# Gradient ascent function (this is gradient ascent for log-likelihood maximization)2gradient_ascent <- function(log_returns, delta, max_iter = 1000000, learning_rate = 1e-6, tolerance = 0.0001) {3# Initial guess4params <- c(alpha = mean(log_returns), sigma = sd(log_returns))5n <- length(log_returns)6cost_history <- numeric(max_iter) # To track cost history for debugging78for(i in 1:max_iter) {9grad <- gradient(params, log_returns, delta)1011# Update parameters proposal (gradient ascent so we ADD the gradient)12new_params <- params + learning_rate * grad1314# Ensure sigma is not non-positive after the update15if (new_params[2] <= 0) {16new_params[2] <- max(params[2], 1e-8) # Revert sigma to its previous value or set to a small positive value17}1819params <- new_params2021# Calculate the likelihood (for monitoring purposes, since we maximize it)22likelihood <- log_likelihood(params, log_returns, delta)2324# Check for convergence25if (all(abs(grad) < tolerance)) {26cat("Convergence achieved after", i, "iterations.\n")27break28}29}3031return(params) # Return parameters and cost history32}3334# Run the gradient ascent35estimated_params <- gradient_ascent(log_returns, delta)36# Print the results37print(estimated_params)
2.3 Closed-Form Results
We check for the robustness of the gradient ascent by comparing the results against the closed-form estimates:
And the results match:
1# Calculate alpha_hat and sigma_hat using closed form solutions for verification2alphahat <- 1/(length(log_returns)*delta) * sum(log_returns)3sigmahat <- sqrt(1/(length(log_returns)*delta) * sum((log_returns - alphahat*delta)^2))45cat("Closed form alpha_hat:", alphahat, "\n")6cat("Closed form sigma_hat:", sigmahat, "\n")789cat("Predicted alpha:", estimated_params[1], "\n")10cat("Predicted sigma:", estimated_params[2], "\n")
Where we find closed form
2.4 Fisher Information Matrix and Variance of the Estimator
To obtain the asymptotic variance of
Hessian matrix and positive log-likelihood
1# Define the Hessian matrix for the positive log-likelihood2hessian_matrix <- function(params, log_returns, delta) {3alpha <- params[1]4sigma <- params[2]5n <- length(log_returns)67# The elements of the Hessian matrix8d2L_dalpha2 <- -n * delta/ (sigma^2)9d2L_dalphadsigma <- -2*(sigma^-3) * sum((log_returns - alpha*delta))10d2L_dsigma2 <- -3/delta * sum((log_returns - alpha * delta)^2) * sigma^(-4) + n*sigma^(-2)1112# Construct the Hessian matrix13hessian <- matrix(c(d2L_dalpha2, d2L_dalphadsigma,14d2L_dalphadsigma, d2L_dsigma2), nrow = 2)15return(hessian)16}1718# Calculate the Hessian at the estimated parameters19obs_hessian <- hessian_matrix(estimated_params, log_returns, delta)2021# Invert the Hessian to get the variance-covariance matrix22var_cov_matrix <- solve(-obs_hessian) # solve() is the base R method for matrix inversion2324# Extract the variance of sigma from the matrix25var_sigma <- var_cov_matrix[2, 2] # The variance of sigma is in the second row, second column2627# Print the estimated variance of sigma28print('Variance of sigma:')29print(var_sigma)
1"Variance of sigma: 5.633948e-06"
3. Call Option Price
Finally, from Black-Scholes, we can find the European style AAPL call Option to be:
Black-Scholes Call Option Pricing
1# Given values2P_t <- 177.70 # Current price of the stock3K <- 175 # Strike price4r <- 0.05 # Risk-free interest rate (annual)5sigma <- estimated_params[2] # Volatility from the given 'estimated_params'67# Time to expiration (1 month assumed as 1/12 of a year)8T_t <- 21/252910# Black-Scholes formula components11h1 <- (log(P_t/K) + (r + sigma^2/2) * T_t) / (sigma * sqrt(T_t))12h2 <- h1 - sigma * sqrt(T_t)1314# Using pnorm for the cumulative distribution function of the standard normal distribution15N_d1 <- pnorm(h1)16N_d2 <- pnorm(h2)1718# Black-Scholes call option price formula19call_option_price <- P_t * N_d1 - K * exp(-r * T_t) * N_d22021# Output the result22cat(call_option_price)2324cat("Option premium per share:", call_option_price,)25cat("Option price:", call_option_price * 100,)
1"Option premium per share: 3.485288"2"Option price: 348.5288"
Have a wonderful day.
– Frank
Estimating the price of a call option on Apple stock using the Black-Scholes model and maximum likelihood estimation