Amazon (AMZN) is one of the largest publicly traded companies in the United States, with a market capitalization of roughly $2.55 trillion as of early 2026. Its actively traded options market and lack of dividend payments make it a natural choice for exploring the Black–Scholes option pricing model.
This project asks a simple question: do Amazon call options trade at prices that make sense under Black–Scholes? Using real market data, the model prices options based on the current stock price, time to expiration, risk-free interest rates, and volatility estimated from historical returns.
The model’s output is then compared to live bid and ask quotes to see how closely theory matches reality. This comparison highlights where historical volatility performs well, where it falls short, and how market implied volatility fills the gap.
To get started, we load the R packages that will be used to retrieve market data and build the option pricing model.
# Load packages
library(quantmod)
## Loading required package: xts
## Loading required package: zoo
##
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
##
## as.Date, as.Date.numeric
## Loading required package: TTR
## Registered S3 method overwritten by 'quantmod':
## method from
## as.zoo.data.frame zoo
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.4 ✔ readr 2.1.5
## ✔ forcats 1.0.0 ✔ stringr 1.5.1
## ✔ ggplot2 3.5.1 ✔ tibble 3.2.1
## ✔ lubridate 1.9.4 ✔ tidyr 1.3.1
## ✔ purrr 1.0.4
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::first() masks xts::first()
## ✖ dplyr::lag() masks stats::lag()
## ✖ dplyr::last() masks xts::last()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(dplyr)
We define the key inputs for the pricing model by retrieving the current Amazon stock price, obtaining the risk free interest rate, and computing the time to maturity based on the option’s expiration date.
# Retrieve AMZN stock prices
getSymbols("AMZN", src = "yahoo")
## [1] "AMZN"
# Define spot price
S <- as.numeric(last(AMZN$AMZN.Close))
# Retrieve 1-year Treasury yield (risk-free rate)
getSymbols("DGS1", src = "FRED")
## [1] "DGS1"
r <- as.numeric(last(DGS1)) / 100
# Define expiration date and today (valuation as of 2/3/2026)
expir <- as.Date("2026-02-20")
today <- as.Date("2026-02-03")
# Compute time to maturity in years
T <- as.numeric(expir - S) / 365.25
This section defines \(d_1\) and \(d_2\), two key quantities in the Black–Scholes model that are used to compute call and put option prices based on the model inputs introduced above.
# Define d1 and d2
d1 <- function(S, K, r, sigma, T) {
(log(S / K) + (r + 0.5 * sigma^2) * T) / (sigma * sqrt(T))
}
d2 <- function(S, K, r, sigma, T) {
d1(S, K, r, sigma, T) - sigma * sqrt(T)
}
To bring the pieces together, we define the Black–Scholes pricing
function bs_price, which computes theoretical prices for
call and put options using the model inputs.
# Define Black-Scholes pricing function
bs_price <- function(type = c("call","put"), S, K, r, sigma, T) {
type <- match.arg(type)
d1v <- d1(S, K, r, sigma, T)
d2v <- d2(S, K, r, sigma, T)
if (type == "call") {
S * pnorm(d1v) - K * exp(-r * T) * pnorm(d2v)
} else {
K * exp(-r * T) * pnorm(-d2v) - S * pnorm(-d1v)
}
}
This section computes option sensitivities from the Black–Scholes model, including delta, gamma, and vega. These measures help interpret the risk of an option position. Delta captures sensitivity to changes in the underlying stock price, gamma measures how delta changes as the stock price moves, and vega reflects sensitivity to changes in implied volatility.
# Define key Black–Scholes Greeks
bs_greeks_min <- function(type = c("call","put"), S, K, r, sigma, T) {
type <- match.arg(type)
d1v <- d1(S, K, r, sigma, T)
delta <- if (type == "call") {
pnorm(d1v)
} else {
pnorm(d1v) - 1
}
gamma <- dnorm(d1v) / (S * sigma * sqrt(T))
vega <- S * dnorm(d1v) * sqrt(T) / 100
list(delta = delta, gamma = gamma, vega = vega)
}
To estimate volatility for the Black–Scholes model, we calculate Amazon’s historical volatility using daily log returns and annualize it using a 252 trading day convention.
pr <- Ad(AMZN)
returns <- diff(log(pr))
hist_sigma <- as.numeric(sd(returns, na.rm = TRUE) * sqrt(252))
Using the historical volatility estimate, we price an Amazon call and put option at the selected strike. We also verify the results using put–call parity, which checks for consistency in the model.
# Define strike price
K = 227.5
# Price call and put options
call_pr <- bs_price("call", S, K, r, hist_sigma, T)
put_pr <- bs_price("put", S, K, r, hist_sigma, T)
# Compute Greeks
call_greeks <- bs_greeks_min("call", S, K, r, hist_sigma, T)
put_greeks <- bs_greeks_min("put", S, K, r, hist_sigma, T)
# Results table
results_tbl <- tibble(
Option = c("Call", "Put"),
Price = c(call_pr, put_pr),
Delta = c(call_greeks$delta, put_greeks$delta),
Gamma = c(call_greeks$gamma, put_greeks$gamma),
Vega = c(call_greeks$vega, put_greeks$vega)
)
results_tbl
## # A tibble: 2 × 5
## Option Price Delta Gamma Vega
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 Call 230. 0.983 0.0000639 0.780
## 2 Put 20.3 -0.0174 0.0000639 0.780
As expected under Black–Scholes, the call and put share the same gamma and vega, while the put delta equals the call delta minus one. The larger call price reflects that the strike is below the current stock price.
# Put-call parity check
call_pr - put_pr
## [1] 210.1351
S - K * exp(-r * T)
## [1] 210.1351
Since these values are equal, this confirms that the model satisfies put–call parity, which is a required no-arbitrage condition under the Black–Scholes model.
Finally, we compare the model price to the market bid and ask quotes from Fidelity by computing the mid price and measuring the percent difference between the model and market.
# Define model prices and input quotes from Fidelty (as of 2/3/2026)
model_call <- call_pr
bid_call <- 17.65
ask_call <- 17.80
mid_call <- (bid_call + ask_call) / 2
# Calculate variance
pct_diff_call <- (model_call - mid_call) / mid_call * 100
pct_diff_call
## [1] 1200.149
model_put <- put_pr
bid_put <- 6.05
ask_put <- 6.20
mid_put <- (bid_put + ask_put) / 2
# Calculate variance
pct_diff_vs_market <- (model_put - mid_put) / mid_put * 100
pct_diff_call
## [1] 1200.149
Overall, the model prices the Amazon call option about 1.3% higher than the market mid price from Fidelity, using a valuation date of February 3, 2026, an expiration of February 20, 2026, and a strike of 227.5.
However, the same approach prices the corresponding put option approximately 65.3% lower than the observed market price. This gap reflects a limitation of using historical volatility within the Black–Scholes framework. In practice, equity options often reflect higher implied volatility on the downside, which results in market put prices that are higher than those implied by historical volatility.