Q1. Price a European call We are given the following inputs:

library(NMOF)

S <- 810
X <- 800
sigma <- 0.20
v <- sigma^2
r <- 0.05
q <- 0.01
tau <- 0.5

call_nmof <- vanillaOptionEuropean(
  S = S,
  X = X,
  tau = tau,
  r = r,
  q = q,
  v = v,
  type = "call"
)

call_nmof
## $value
## [1] 58.73271
## 
## $delta
## [1] 0.6148191
## 
## $gamma
## [1] 0.003312828
## 
## $theta
## [1] -60.45444
## 
## $vega
## [1] 217.3547
## 
## $rho
## [1] 219.6354
## 
## $rhoDiv
## [1] -249.0018
## 
## $DvegaDspot
## [1] -0.3008419
## 
## $DvegaDvol
## [1] 51.68801

The European 6-month call option premium computed using vanillaOptionEuropean() is shown above.

Q2. Compute the same call premium directly by the BSM formula

Using the Black-Scholes-Merton formula with continuous dividend yield, the European call premium is:

\[ C = S e^{-q\tau}N(d_1) - X e^{-r\tau}N(d_2) \]

where

\[ d_1 = \frac{\ln(S/X) + \left(r - q + \frac{1}{2}\sigma^2\right)\tau}{\sigma\sqrt{\tau}} \]

and

\[ d_2 = d_1 - \sigma\sqrt{\tau} \]

The continuous dividend yield \(q\) enters the formula in two places. First, it discounts the stock term through \(S e^{-q\tau}\). Second, it reduces the growth term inside \(d_1\) from \(r\) to \(r-q\). A positive dividend yield lowers the call price because the call holder does not receive the dividends paid by the underlying asset.

d1 <- (log(S / X) + (r - q + 0.5 * sigma^2) * tau) / (sigma * sqrt(tau))
d2 <- d1 - sigma * sqrt(tau)

call_bsm <- S * exp(-q * tau) * pnorm(d1) -
  X * exp(-r * tau) * pnorm(d2)

call_bsm
## [1] 58.73271

The directly computed BSM call premium matches the result from vanillaOptionEuropean() in Q1!

Q3. Use put-call parity to price the put with the same strike

For European options with continuous dividend yield, put-call parity is:

\[ C - P = S e^{-q\tau} - X e^{-r\tau} \]

Rearranging for the put price:

\[ P = C - S e^{-q\tau} + X e^{-r\tau} \]

put_parity <- call_bsm - S * exp(-q * tau) + X * exp(-r * tau)

put_nmof_result <- vanillaOptionEuropean(
  S = S,
  X = X,
  tau = tau,
  r = r,
  q = q,
  v = sigma^2,
  type = "put"
)

put_nmof <- put_nmof_result$value

q3_results <- data.frame(
  Put_Parity = put_parity,
  Put_NMOF = put_nmof,
  Difference = put_parity - put_nmof
)

q3_results
##   Put_Parity Put_NMOF Difference
## 1   33.02053 33.02053          0

The put price from put-call parity matches the put price computed directly using vanillaOptionEuropean(). This verifies that the call and put prices are consistent with put-call parity under continuous dividend yield.

Q4. Compute implied volatilities of the 1425 put

For the dates from 2/26/2007 through 3/5/2007, I compute the implied volatility of the P1425 option using vanillaOptionImpliedVol() from NMOF.

Because this dataset comes from the S&P 500 futures option case study, I set the continuous dividend yield equal to the risk-free rate, so q = r.

library(NMOF)
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(knitr)

# Read the options price data
prices_data <- read.csv("OptionsPrices.csv")

# Convert the Date column into R's Date format
prices_data$Date <- as.Date(prices_data$Date, format = "%m/%d/%Y")

# Keep only the required sample window: 2/26/2007 through 3/5/2007
filtered_data <- prices_data %>%
  filter(
    Date >= as.Date("2007-02-26"),
    Date <= as.Date("2007-03-05")
  ) %>%
  arrange(Date)

# Strike price for the P1425 option
strike_p1425 <- 1425

# Preallocate an empty column for implied volatility
filtered_data$ImpliedVol <- NA

# Compute implied volatility for the 1425 put on each date
# Since these are options on futures, set q = r
for (i in 1:nrow(filtered_data)) {
  filtered_data$ImpliedVol[i] <- vanillaOptionImpliedVol(
    exercise = "european",
    price = filtered_data$P1425[i],
    S = filtered_data$UndPr[i],
    X = strike_p1425,
    tau = filtered_data$Expiry[i] / 365,
    r = filtered_data$OptRate[i],
    q = filtered_data$OptRate[i],
    type = "put"
  )
}

# Create a clean output table
iv_table <- filtered_data %>%
  select(Date, Expiry, OptRate, UndPr, P1425, ImpliedVol)

kable(iv_table, digits = 4)
Date Expiry OptRate UndPr P1425 ImpliedVol
2007-02-26 298 0.0518 1492.0 41.8862 0.1376
2007-02-27 297 0.0518 1432.4 69.6728 0.1485
2007-02-28 296 0.0518 1446.3 59.5717 0.1402
2007-03-01 295 0.0518 1441.9 61.2374 0.1401
2007-03-02 294 0.0518 1422.3 71.0377 0.1427
2007-03-05 291 0.0518 1408.3 79.1396 0.1464

The implied volatility of the 1425 put increased sharply from 13.76% on 2/26/2007 to 14.85% on 2/27/2007, which coincided with a large drop in the underlying futures price. After that, the implied volatility stayed around 14.0% to 14.6% during the sample window. This suggests that the market-implied uncertainty for the 1425 put rose when the underlying futures price fell.

Q5. Compute option premium and Greeks on the first date

On the first date in the sample window, 2/26/2007, I compute the premium and Greeks of the 1425 put using vanillaOptionEuropean(). Since this is an option on futures, I set q = r.

# Select the first date in the sample window
first_day <- filtered_data[1, ]

# Compute the option premium and Greeks for the 1425 put
put_1425_first_day <- vanillaOptionEuropean(
  type = "put",
  S = first_day$UndPr,
  X = strike_p1425,
  tau = first_day$Expiry / 365,
  r = first_day$OptRate,
  q = first_day$OptRate,
  v = first_day$ImpliedVol^2,
  greeks = TRUE
)

# Create a clean table of the required outputs
q5_table <- data.frame(
  value = put_1425_first_day$value,
  delta = put_1425_first_day$delta,
  gamma = put_1425_first_day$gamma,
  vega = put_1425_first_day$vega,
  theta = put_1425_first_day$theta,
  rho = put_1425_first_day$rho
)

kable(q5_table, digits = 4)
value delta gamma vega theta rho
41.8779 -0.3192 0.0019 469.697 -37.4212 -423.0347

The model premium is close to the observed market price of the P1425 option on 2/26/2007 because the implied volatility was chosen to match the market premium. The Delta is negative because this is a put option, so the option value tends to increase when the underlying futures price decreases. Gamma is positive, meaning the option has convexity with respect to the underlying price. Vega is positive, so the put value increases when implied volatility rises. Theta is negative, showing that the option loses value as time passes, holding other factors fixed. Rho is negative for this put, meaning the option value decreases when the interest rate increases.

Q6. Write the option P&L decomposition formula

Using only the Delta, Gamma, and Vega terms, the short-horizon option P&L decomposition formula is:

\[ \Delta V \approx \Delta \cdot \Delta S + \frac{1}{2}\Gamma(\Delta S)^2 + \nu \cdot \Delta \sigma \]

where \(\Delta V\) is the estimated change in the option premium, \(\Delta S\) is the change in the underlying futures price, and \(\Delta \sigma\) is the change in implied volatility.

The Delta term captures the first-order effect of a change in the underlying futures price. The Gamma term captures the curvature effect, meaning it adjusts the estimate when the underlying price movement is large. The Vega term captures the effect of changes in implied volatility. Since this is only a local Taylor approximation, it works best for small day-to-day changes and may be less accurate when the underlying price or implied volatility changes sharply.

Q7. Compute daily P&L decomposition for the 1425 put

For each day from 2/27/2007 through 3/5/2007, I compute the actual P&L and the estimated P&L. The estimated P&L uses the Greeks from the previous day to explain the change in option premium from the previous day to the current day.

The full decomposition is:

\[ O_{t+\Delta t} - O_t \approx \Delta(O_t)(S_{t+\Delta t} - S_t) + \frac{1}{2}\Gamma(O_t)(S_{t+\Delta t} - S_t)^2 + \nu(O_t)(\sigma_{t+\Delta t} - \sigma_t) + \Theta(O_t)\frac{\Delta t}{365} + \rho(O_t)(r_{t+\Delta t} - r_t) \]

# Compute value and Greeks for each date in the filtered sample window
filtered_data$value <- NA
filtered_data$delta <- NA
filtered_data$gamma <- NA
filtered_data$vega <- NA
filtered_data$theta <- NA
filtered_data$rho <- NA

for (i in 1:nrow(filtered_data)) {
  
  greek_result <- vanillaOptionEuropean(
    type = "put",
    S = filtered_data$UndPr[i],
    X = strike_p1425,
    tau = filtered_data$Expiry[i] / 365,
    r = filtered_data$OptRate[i],
    q = filtered_data$OptRate[i],
    v = filtered_data$ImpliedVol[i]^2,
    greeks = TRUE
  )
  
  filtered_data$value[i] <- greek_result$value
  filtered_data$delta[i] <- greek_result$delta
  filtered_data$gamma[i] <- greek_result$gamma
  filtered_data$vega[i] <- greek_result$vega
  filtered_data$theta[i] <- greek_result$theta
  filtered_data$rho[i] <- greek_result$rho
}

# Preallocate columns for the P&L decomposition
filtered_data$actual_pnl <- NA
filtered_data$delta_pnl <- NA
filtered_data$gamma_pnl <- NA
filtered_data$vega_pnl <- NA
filtered_data$theta_pnl <- NA
filtered_data$rho_pnl <- NA
filtered_data$estimated_pnl <- NA

# Use previous-day Greeks to explain the option P&L from previous day to current day
for (i in 2:nrow(filtered_data)) {
  
  # Changes from previous day to current day
  dS <- filtered_data$UndPr[i] - filtered_data$UndPr[i - 1]
  dVol <- filtered_data$ImpliedVol[i] - filtered_data$ImpliedVol[i - 1]
  dr <- filtered_data$OptRate[i] - filtered_data$OptRate[i - 1]
  dt_days <- filtered_data$Expiry[i - 1] - filtered_data$Expiry[i]
  
  # Actual P&L using observed market premium
  filtered_data$actual_pnl[i] <- filtered_data$P1425[i] - filtered_data$P1425[i - 1]
  
  # Estimated P&L buckets using previous-day Greeks
  filtered_data$delta_pnl[i] <- filtered_data$delta[i - 1] * dS
  filtered_data$gamma_pnl[i] <- 0.5 * filtered_data$gamma[i - 1] * dS^2
  filtered_data$vega_pnl[i] <- filtered_data$vega[i - 1] * dVol
  filtered_data$theta_pnl[i] <- filtered_data$theta[i - 1] * dt_days / 365
  filtered_data$rho_pnl[i] <- filtered_data$rho[i - 1] * dr
  
  # Total estimated P&L
  filtered_data$estimated_pnl[i] <-
    filtered_data$delta_pnl[i] +
    filtered_data$gamma_pnl[i] +
    filtered_data$vega_pnl[i] +
    filtered_data$theta_pnl[i] +
    filtered_data$rho_pnl[i]
}

# Create clean Q7 output table
q7_table <- filtered_data %>%
  filter(Date > as.Date("2007-02-26")) %>%
  select(
    Date,
    actual_pnl,
    estimated_pnl,
    delta_pnl,
    gamma_pnl,
    vega_pnl,
    theta_pnl,
    rho_pnl
  )

kable(q7_table, digits = 4)
Date actual_pnl estimated_pnl delta_pnl gamma_pnl vega_pnl theta_pnl rho_pnl
2007-02-27 27.7866 27.3817 19.0252 3.3350 5.1241 -0.1025 0
2007-02-28 -10.1011 -10.1087 -6.1025 0.1914 -4.0846 -0.1130 0
2007-03-01 1.6658 1.6657 1.8071 0.0199 -0.0537 -0.1077 0
2007-03-02 9.8002 9.8005 8.2288 0.3996 1.2796 -0.1077 0
2007-03-05 8.1019 8.1205 6.4502 0.2056 1.7897 -0.3250 0

The actual P&L is the observed change in the market premium of the 1425 put from one trading day to the next. The estimated P&L decomposes this change into Delta, Gamma, Vega, Theta, and Rho components. For each day, I use the previous day’s Greeks to explain the next day’s change, because those Greeks represent the option’s sensitivities at the beginning of the holding period.

Q8. Compare actual and estimated P&L

The table below compares the actual P&L with the estimated P&L and the main Greek components: Delta P&L, Gamma P&L, and Vega P&L.

q8_table <- q7_table %>%
  select(
    Date,
    actual_pnl,
    estimated_pnl,
    delta_pnl,
    gamma_pnl,
    vega_pnl
  ) %>%
  mutate(Date = as.character(Date)) %>%
  rename(
    `Actual P&L` = actual_pnl,
    `Estimated P&L` = estimated_pnl,
    `Delta P&L` = delta_pnl,
    `Gamma P&L` = gamma_pnl,
    `Vega P&L` = vega_pnl
  )

kable(q8_table, digits = 4)
Date Actual P&L Estimated P&L Delta P&L Gamma P&L Vega P&L
2007-02-27 27.7866 27.3817 19.0252 3.3350 5.1241
2007-02-28 -10.1011 -10.1087 -6.1025 0.1914 -4.0846
2007-03-01 1.6658 1.6657 1.8071 0.0199 -0.0537
2007-03-02 9.8002 9.8005 8.2288 0.3996 1.2796
2007-03-05 8.1019 8.1205 6.4502 0.2056 1.7897

The estimated P&L is very close to the actual P&L for most days in this sample window. The approximation works especially well when the daily changes in the underlying futures price and implied volatility are relatively small, such as from 2/28/2007 to 3/1/2007. The largest move occurs from 2/26/2007 to 2/27/2007, when the actual P&L is about 27.79 and the estimated P&L is about 27.38. Even on this large-move day, the approximation is still fairly close, mainly because Delta, Gamma, and Vega together explain most of the put price increase.

The approximation may break down when the underlying futures price or implied volatility changes sharply, because the decomposition is based on a local Taylor approximation using the previous day’s Greeks. It also does not fully capture higher-order effects, such as changes in Delta, Gamma, or Vega themselves over the day. In this sample, Rho does not contribute because the interest rate stays constant at 0.0518, while Theta is small and negative because one trading day of time decay slightly reduces the option value.