Background

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.

Load Packages

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)

Define key variables

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

Model

Define d1 and d2

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)
}

BS Price

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)
  }
}

BS Greeks

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)
}

Compute volatility based on historical returns

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))

Sample pricing and parity check

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.

Compare the model price to market quotes

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.