Instruction, data and Guideline from the professor:
This project assignment is designed to verify the put-call parity of
crude oil options on crude oil futures contracts, which are traded on
Chicago Mercantile Exchange (CME), from 12/1/2020 throughout
3/10/2021,
for example. You can select different time periods of data. But, the
time span for the data is no less than 40 trading days in general.
The contract specifications can be found at the website of CME. (FYI:
The futures prices are available at the link
http:/www.cmegroup.com/trading/energy/crude-oil/light-sweet-crude.html
The call and put option prices are available at the link
http://www.cmegroup.com/trading/energy/crude-oil/light-sweet-crude_quotes_globex_options.html.)
To verify the put-call parity,
Through D2L, you can find the data
(“WTI_Data.xlsx”) including futures prices and options
prices on a daily basis.
The last prices are associated with different contract maturities given
in the first row of the spreadsheet “Futures”.
The “WTI Crude Future Mar21”, for example, list future prices at the
maturity of March 2021, which are used in this assignment.
You are required to use three different contract maturities.
Similarly, the “last” call and put prices are used in this
assignment. To ensure that you use the correct prices, there are a few
points that you should note: A spreadsheet “Calls-Apr”, for example,
include call option prices for
The interest rates uses US treasury bill interest rates with a
maturity of 1, 2, 3, … months, depending on the maturities for your
derivatives maturities. If an interest rate with a given maturity is not
listed, 9-month rates for example, you can use 6-month and 12-month
interest rates to interpolate.
You are required to create a spreadsheet that contains date,
futures price, call price, put price, strike price, expiration, interest
rate, verify the parity, calculate C-P (difference between call and put
prices).
You are required to summarize the results presented in (4) in the
following perspective: (a) potential arbitrage opportunities; (b)
strategies exploiting arbitrage opportunities; (c) whether your
strategies are practical and why?
Submit a report including (5) along with (4).
Implementing Put-Call Parity
European Option:
The put-call parity relationship for options on futures is given by:
\(C - P = (F-K) e^{rT}\)
Where:
C = Call option price
P = Put option price
F = Futures price
K = Strike price
r = Risk-free interest rate (continuously compounded)
T = Time to maturity (in years)
Simulate Arbitrage Trades
When LHS>RHS:
Buy the put, sell the call, and buy the underlying asset.
When RHS>LHS:
Sell the put, buy the call, and short the underlying asset.
American Option:
For American options, especially those on dividend-paying stocks, the
relationship is adjusted to account for potential early exercise and
dividends.
The adjusted inequality for American options is:
\[S-K \le C-P \le S-K e^{-rT}\]
This inequality indicates that the difference between the call and
put prices for American options lies between \(S-K\) and \(S-K
e^{-rT}\).
The exact relationship depends on factors like dividends and the
likelihood of early exercise.
Where:
C = Call option price
P = Put option price
S = Futures price
K = Strike price
r = Risk-free interest rate (continuously compounded)
T = Time to maturity (in years)
This approach allows us to assess whether the observed prices of American options fall within the expected theoretical bounds, considering the possibility of early exercise and other factors.
Additional Considerations for European
Options:
Monitor Transaction Costs
Tools and Techniques:
- Spreadsheets or Software: Automate calculations of parity
equations.
- Quantitative Models: Use programming languages like Python or R to
scan for opportunities in large datasets.
- Trading Platforms: Real-time alerts based on preset arbitrage
conditions.
By systematically checking these conditions, you can identify and potentially exploit arbitrage opportunities effectively.
Additional Considerations for American Options:
Dividends: If the underlying asset pays dividends, the present
value of expected dividends should be subtracted from the spot price in
the Upper_Bound calculation to accurately reflect the asset’s
value.
Market Conditions: The actual prices of American options may
deviate from these theoretical bounds due to market conditions,
liquidity, and other factors.
By implementing these adjustments, our analysis will better reflect the pricing dynamics of American options, acknowledging the complexities introduced by their early exercise feature.
Verify the put-call parity
This Report is to verify the put-call parity for crude oil options on
futures contracts traded on the Chicago Mercantile Exchange (CME) from
12/1/2020 through 3/10/2021.
For American-style options, the put-call parity
relationship differs slightly because of the possibility of early
exercise.
However, at maturity, early exercise becomes irrelevant
as the options are exercised (or not) based solely on their intrinsic
value. Thus, the relationship simplifies.
Why This Works
Because of, Early Exercise is Irrelevant at Maturity:
At maturity, the options are either exercised or
expire worthless.
The potential for early exercise (a defining feature of American
options) no longer applies.
So, On Maturity,
we are dealing with Intrinsic Value Only. So,
Futures Price (F) converges with the spot price (S)
\[ C = max(0, F-K) \] \[ P=max(0, K - F) \]
Practical Considerations While the formula remains the same as for European options:
\[C - P = F - K\]
Data Preparation
Futures Prices:
We extracted daily futures prices for three different contract
maturities (e.g., March 2021, April 2021, June 2021) from the “Futures”
sheet in “WTI_Data-02.xlsx”.
At first, we manually clean the data in Excel then calculate the
put-call parity using R language.
Options Prices:
Also extracted daily call and put option prices corresponding to the
same maturities and matching strike prices from the respective sheets
(e.g., “Calls_Puts_Apr” for April 2021).
The analysis covers the following expirations and strike prices:
# Read data from the Excel file
data_apr21 <- read_excel("WTI_Data-02.xlsx", sheet = "Calls_Puts_Apr")
# data_apr21$Date <- as.Date(data_apr21$Date, format = "%m/%d/%Y")
data_apr21$futures_apr <- as.numeric(data_apr21$futures_apr)
data_apr21$cp64 <- as.numeric(data_apr21$cp64)
data_apr21$pp64 <- as.numeric(data_apr21$pp64)
data_apr21$cp64.5 <- as.numeric(data_apr21$cp64.5)
data_apr21$pp64.5 <- as.numeric(data_apr21$pp64.5)
data_apr21$cp65 <- as.numeric(data_apr21$cp65)
data_apr21$pp65 <- as.numeric(data_apr21$pp65)
# Strike Price: USD 64
# Calculate parity deviation for American-style options
data_apr21 <- data_apr21 %>%
mutate(deviation64 = (cp64 - pp64) - (futures_apr - 64))
# Strike Price: USD 64.5
data_apr21 <- data_apr21 %>%
mutate(deviation64.5 = (cp64.5 - pp64.5) - (futures_apr - 64.5))
# Strike Price: USD 65
data_apr21 <- data_apr21 %>%
mutate(deviation65 = (cp65 - pp65) - (futures_apr - 65))
# Define transaction cost threshold
transaction_cost <- 0.05 # We can adjust as per market norms
# Identify arbitrage opportunities
data_apr21 <- data_apr21 %>%
mutate(arbitrage64 = abs(deviation64) > transaction_cost) %>%
mutate(arbitrage64.5 = abs(deviation64.5) > transaction_cost) %>%
mutate(arbitrage65 = abs(deviation65) > transaction_cost)
data_apr21 %>%
head(10) %>%
kable(
format = "html",
caption = "The data April 21 (Maturity) with arbitrage opportunities"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE,
position = "center"
) %>%
row_spec(0, background = "#D3D3D3", bold = TRUE)| Date | futures_apr | cp64 | pp64 | cp64.5 | pp64.5 | cp65 | pp65 | deviation64 | deviation64.5 | deviation65 | arbitrage64 | arbitrage64.5 | arbitrage65 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3/16/2021 | 64.37 | 0.85 | 0.41 | 0.67 | 0.63 | 0.49 | 1.01 | 0.07 | 0.17 | 0.11 | TRUE | TRUE | TRUE |
| 3/15/2021 | 65.39 | 1.69 | 0.30 | 1.32 | 0.43 | 0.99 | 0.60 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
| 3/12/2021 | 65.61 | 2.00 | 0.39 | 1.64 | 0.53 | 1.32 | 0.71 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
| 3/11/2021 | 66.02 | 2.46 | 0.44 | 2.09 | 0.57 | 1.75 | 0.73 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
| 3/10/2021 | 64.44 | 1.54 | 1.10 | 1.28 | 1.34 | 1.05 | 1.61 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
| 3/9/2021 | 64.01 | 1.45 | 1.44 | 1.21 | 1.70 | 1.01 | 2.00 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
| 3/8/2021 | 65.05 | 2.12 | 1.07 | 1.83 | 1.28 | 1.57 | 1.52 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
| 3/5/2021 | 66.09 | 2.89 | 0.80 | 2.55 | 0.96 | 2.23 | 1.14 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
| 3/4/2021 | 63.83 | 1.61 | 1.78 | 1.37 | 2.04 | 1.17 | 2.34 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
| 3/3/2021 | 61.28 | 0.79 | 3.51 | 0.66 | 3.88 | 0.55 | 4.27 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
# Combine data for all strikes into a single data frame
deviation_data_apr21 <- data.frame(
Strike = rep(c(64, 64.5, 65), each = nrow(data_apr21)),
ParityDeviation = c(data_apr21$deviation64, data_apr21$deviation64.5,
data_apr21$deviation65))
# Plot deviations
ggplot(deviation_data_apr21, aes(x = Strike, y = ParityDeviation)) +
geom_boxplot() +
geom_hline(yintercept = c(-transaction_cost, transaction_cost), color = "red", linetype = "dashed") +
labs(title = "Put-Call Parity Deviations April 21", y = "Parity Deviation", x = "Strike Price")# Filter out rows with NA values in Parity Deviation
filtered_data <- deviation_data_apr21 %>%
filter(!is.na(ParityDeviation))
# 1. Line Plot - Parity Deviation Trends by Strike Price
ggplot(filtered_data, aes(x = Strike, y = ParityDeviation)) +
geom_line(color = "blue", size = 1) +
geom_point(color = "darkblue", size = 2) +
labs(
title = "Parity Deviation Trends by Strike Price",
x = "Strike Price",
y = "Parity Deviation"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 12, face = "bold"),
axis.title = element_text(size = 10),
axis.text = element_text(size = 10)
)# 2. Histogram - Distribution of Parity Deviations
ggplot(filtered_data, aes(x = ParityDeviation)) +
geom_histogram(binwidth = 0.02, fill = "blue", color = "black", alpha = 0.7) +
labs(
title = "Distribution of Parity Deviations",
x = "Parity Deviation",
y = "Frequency"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 12, face = "bold"),
axis.title = element_text(size = 10),
axis.text = element_text(size = 10)
)# 3. Boxplot - Parity Deviations by Strike Price
ggplot(filtered_data, aes(x = factor(Strike), y = ParityDeviation)) +
geom_boxplot(fill = "lightblue", color = "darkblue", outlier.color = "red", outlier.size = 2) +
labs(
title = "Boxplot of Parity Deviations by Strike Price",
x = "Strike Price",
y = "Parity Deviation"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 12, face = "bold"),
axis.title = element_text(size = 10),
axis.text = element_text(size = 10)
)Key Observations:
Distribution of Deviations:
Most deviations are close to zero, suggesting that the observed
market prices align well with theoretical expectations.
Minimal deviations from the parity equation imply that the market
efficiently prices options, leaving limited opportunities for
arbitrage.
Strike Price Behavior:
The dataset includes multiple strike prices, allowing for a
comparative analysis of deviations across various levels.
The magnitude and variability of deviations appear similar across strike
prices, indicating consistent market dynamics.
Implications for Arbitrage:
Minor deviations within a narrow range suggest that transaction
costs, bid-ask spreads, or other trading frictions likely outweigh
potential arbitrage profits.
Observations align with the expectation of market efficiency,
particularly in actively traded instruments like crude oil
options.
Conclusion:
The analysis highlights that deviations from put-call parity are minimal
and consistent across strike prices. This confirms that the crude oil
options market at the Chicago Mercantile Exchange is highly
efficient.
Opportunities for arbitrage are limited and likely offset by
transaction costs. This reinforces the theoretical robustness of
put-call parity and the practicality of its application in financial
markets.
# Read data from the Excel file
data_jun21 <- read_excel("WTI_Data-02.xlsx", sheet = "Calls_Puts_Jun")
# data_jun21$Date <- as.Date(data_jun21$Date, format = "%m/%d/%Y")
data_jun21$futures_jun <- as.numeric(data_jun21$futures_jun)
data_jun21$cp63 <- as.numeric(data_jun21$cp63)
data_jun21$pp63 <- as.numeric(data_jun21$pp63)
data_jun21$cp64 <- as.numeric(data_jun21$cp64)
data_jun21$pp64 <- as.numeric(data_jun21$pp64)
data_jun21$cp64.5 <- as.numeric(data_jun21$cp64.5)
data_jun21$pp64.5 <- as.numeric(data_jun21$pp64.5)
data_jun21$cp65 <- as.numeric(data_jun21$cp65)
data_jun21$pp65 <- as.numeric(data_jun21$pp65)
data_jun21$cp65.5 <- as.numeric(data_jun21$cp65.5)
data_jun21$pp65.5 <- as.numeric(data_jun21$pp65.5)
data_jun21$cp66 <- as.numeric(data_jun21$cp66)
data_jun21$pp66 <- as.numeric(data_jun21$pp66)
# Strike Price: USD 63
# Calculate parity deviation for American-style options
data_jun21 <- data_jun21 %>%
mutate(deviation63 = (cp63 - pp63) - (futures_jun - 63))
# Strike Price: USD 63.5
data_jun21 <- data_jun21 %>%
mutate(deviation63.5 = (cp63.5 - pp63.5) - (futures_jun - 63.5))
# Strike Price: USD 64
data_jun21 <- data_jun21 %>%
mutate(deviation64 = (cp64 - pp64) - (futures_jun - 64))
# Define transaction cost threshold
transaction_cost <- 0.05 # Adjust as per market norms
# Identify arbitrage opportunities
data_jun21 <- data_jun21 %>%
mutate(arbitrage63 = abs(deviation63) > transaction_cost) %>%
mutate(arbitrage63.5 = abs(deviation63.5) > transaction_cost) %>%
mutate(arbitrage64 = abs(deviation64) > transaction_cost)
data_jun21 %>%
head(10) %>%
kable(
format = "html",
caption = "The data June 21 (Maturity) with arbitrage opportunities"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE,
position = "center"
) %>%
row_spec(0, background = "#D3D3D3", bold = TRUE)| Date | futures_jun | cp63 | pp63 | cp63.5 | pp63.5 | cp64 | pp64 | cp64.5 | pp64.5 | cp65 | pp65 | cp65.5 | pp65.5 | cp66 | pp66 | deviation63 | deviation63.5 | deviation64 | arbitrage63 | arbitrage63.5 | arbitrage64 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3/16/2021 | 64.32 | 4.82 | 3.14 | 4.53 | 3.35 | 4.25 | 3.57 | 3.99 | 3.81 | 3.74 | 4.06 | 3.50 | 4.32 | 3.26 | 4.58 | 0.36 | 0.36 | 0.36 | TRUE | TRUE | TRUE |
| 3/15/2021 | 65.20 | 5.16 | 2.96 | 4.87 | 3.17 | 4.58 | 3.38 | 4.31 | 3.61 | 4.05 | 3.85 | 3.80 | 4.10 | 3.56 | 4.36 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
| 3/12/2021 | 65.36 | 5.25 | 2.89 | 4.95 | 3.09 | 4.66 | 3.30 | 4.39 | 3.53 | 4.13 | 3.77 | 3.88 | 4.02 | 3.63 | 4.27 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
| 3/11/2021 | 65.66 | 5.50 | 2.84 | 5.20 | 3.04 | 4.91 | 3.25 | 4.62 | 3.46 | 4.35 | 3.69 | 4.09 | 3.93 | 3.85 | 4.19 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
| 3/10/2021 | 64.09 | 4.47 | 3.38 | 4.20 | 3.61 | 3.94 | 3.85 | 3.70 | 4.11 | 3.46 | 4.37 | 3.24 | 4.65 | 3.02 | 4.93 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
| 3/9/2021 | 63.69 | 4.30 | 3.61 | 4.04 | 3.85 | 3.79 | 4.10 | 3.55 | 4.36 | 3.32 | 4.63 | 3.10 | 4.91 | 2.90 | 5.21 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
| 3/8/2021 | 64.52 | 4.74 | 3.22 | 4.46 | 3.44 | 4.20 | 3.68 | 3.94 | 3.92 | 3.69 | 4.17 | 3.46 | 4.44 | 3.23 | 4.71 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
| 3/5/2021 | 65.45 | 5.41 | 2.96 | 5.11 | 3.16 | 4.82 | 3.37 | 4.54 | 3.59 | 4.28 | 3.83 | 4.02 | 4.07 | 3.77 | 4.32 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
| 3/4/2021 | 63.15 | 3.93 | 3.78 | 3.67 | 4.02 | 3.43 | 4.28 | 3.20 | 4.55 | 2.98 | 4.83 | 2.77 | 5.12 | 2.57 | 5.42 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
| 3/3/2021 | 60.69 | 2.79 | 5.10 | 2.59 | 5.40 | 2.41 | 5.72 | 2.23 | 6.04 | 2.07 | 6.38 | 1.92 | NA | 1.77 | 7.08 | 0.00 | 0.00 | 0.00 | FALSE | FALSE | FALSE |
# Combine data for all strikes into a single data frame
deviation_data_jun21 <- data.frame(
Strike = rep(c(63, 63.5, 64), each = nrow(data_jun21)),
ParityDeviation = c(data_jun21$deviation63, data_jun21$deviation63.5,
data_jun21$deviation64))
# Plot deviations
ggplot(deviation_data_jun21, aes(x = Strike, y = ParityDeviation)) +
geom_boxplot() +
geom_hline(yintercept = c(-transaction_cost, transaction_cost), color = "red", linetype = "dashed") +
labs(title = "Put-Call Parity Deviations June 21", y = "Parity Deviation", x = "Strike Price")# Filter out rows with NA values in ParityDeviation
filtered_data <- deviation_data_jun21 %>%
filter(!is.na(ParityDeviation))
# 1. Line Plot - Parity Deviation Trends by Strike Price
ggplot(filtered_data, aes(x = Strike, y = ParityDeviation)) +
geom_line(color = "blue", size = 1) +
geom_point(color = "darkblue", size = 2) +
labs(
title = "Parity Deviation Trends by Strike Price",
x = "Strike Price",
y = "Parity Deviation"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 12, face = "bold"),
axis.title = element_text(size = 10),
axis.text = element_text(size = 10)
)# 2. Histogram - Distribution of Parity Deviations
ggplot(filtered_data, aes(x = ParityDeviation)) +
geom_histogram(binwidth = 0.02, fill = "blue", color = "black", alpha = 0.7) +
labs(
title = "Distribution of Parity Deviations",
x = "Parity Deviation",
y = "Frequency"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 12, face = "bold"),
axis.title = element_text(size = 10),
axis.text = element_text(size = 10)
)# 3. Boxplot - Parity Deviations by Strike Price
ggplot(filtered_data, aes(x = factor(Strike), y = ParityDeviation)) +
geom_boxplot(fill = "lightblue", color = "darkblue", outlier.color = "red", outlier.size = 2) +
labs(
title = "Boxplot of Parity Deviations by Strike Price",
x = "Strike Price",
y = "Parity Deviation"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 12, face = "bold"),
axis.title = element_text(size = 10),
axis.text = element_text(size = 10)
)# Read data from the Excel file
data_jul21 <- read_excel("WTI_Data-02.xlsx", sheet = "Calls_Puts_Jul")
# data_jul21$Date <- as.Date(data_jul21$Date, format = "%m/%d/%Y")
data_jul21$futures_jul <- as.numeric(data_jul21$futures_jul)
data_jul21$cp63 <- as.numeric(data_jul21$cp63)
data_jul21$pp63 <- as.numeric(data_jul21$pp63)
data_jul21$cp63.5 <- as.numeric(data_jul21$cp63.5)
data_jul21$pp63.5 <- as.numeric(data_jul21$pp63.5)
data_jul21$cp64 <- as.numeric(data_jul21$cp64)
data_jul21$pp64 <- as.numeric(data_jul21$pp64)
data_jul21$cp64.5 <- as.numeric(data_jul21$cp64.5)
data_jul21$pp64.5 <- as.numeric(data_jul21$pp64.5)
data_jul21$cp65 <- as.numeric(data_jul21$cp65)
data_jul21$pp65 <- as.numeric(data_jul21$pp65)
data_jul21$cp65.5 <- as.numeric(data_jul21$cp65.5)
data_jul21$pp65.5 <- as.numeric(data_jul21$pp65.5)
data_jul21$cp66 <- as.numeric(data_jul21$cp66)
data_jul21$pp66 <- as.numeric(data_jul21$pp66)
data_jul21 <- data_jul21 %>%
mutate(deviation63 = (cp63 - pp63) - (futures_jul - 63))
# Strike Price: USD 63
# Calculate parity deviation for American-style options
data_jul21 <- data_jul21 %>%
mutate(deviation63 = (cp63 - pp63) - (futures_jul - 63))
# Strike Price: USD 63.5
data_jul21 <- data_jul21 %>%
mutate(deviation63.5 = (cp63.5 - pp63.5) - (futures_jul - 63.5))
# Strike Price: USD 64
data_jul21 <- data_jul21 %>%
mutate(deviation64 = (cp64 - pp64) - (futures_jul - 64))
# Strike Price: USD 64.5
data_jul21 <- data_jul21 %>%
mutate(deviation64.5 = (cp64.5 - pp64.5) - (futures_jul - 64.5))
# Strike Price: USD 65
data_jul21 <- data_jul21 %>%
mutate(deviation65 = (cp65 - pp65) - (futures_jul - 65))
# Define transaction cost threshold
transaction_cost <- 0.05 # We can adjust as per market norms
# Identify arbitrage opportunities
data_jul21 <- data_jul21 %>%
mutate(arbitrage63 = abs(deviation63) > transaction_cost) %>%
mutate(arbitrage63.5 = abs(deviation63.5) > transaction_cost) %>%
mutate(arbitrage64 = abs(deviation64) > transaction_cost)%>%
mutate(arbitrage64.5 = abs(deviation64.5) > transaction_cost)%>%
mutate(arbitrage65 = abs(deviation65) > transaction_cost)
data_jul21 %>%
head(10) %>%
kable(
format = "html",
caption = "The data july 21 (Maturity) with arbitrage opportunities"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE,
position = "center"
) %>%
row_spec(0, background = "#D3D3D3", bold = TRUE)| Date | futures_jul | cp63 | pp63 | cp63.5 | pp63.5 | cp64 | pp64 | cp64.5 | pp64.5 | cp65 | pp65 | cp65.5 | pp65.5 | cp66 | pp66 | deviation63 | deviation63.5 | deviation64 | deviation64.5 | deviation65 | arbitrage63 | arbitrage63.5 | arbitrage64 | arbitrage64.5 | arbitrage65 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3/15/2021 | 63.87 | 5.68 | 3.97 | 5.41 | 4.20 | 5.14 | 4.43 | 4.88 | 4.67 | 4.63 | 4.92 | 4.39 | 5.18 | 4.15 | 5.44 | 0.84 | 0.84 | 0.84 | 0.84 | 0.84 | TRUE | TRUE | TRUE | TRUE | TRUE |
| 3/12/2021 | 64.71 | 5.72 | 3.88 | 5.44 | 4.10 | 5.17 | 4.33 | 4.91 | 4.57 | 4.66 | 4.82 | 4.42 | 5.08 | 4.18 | 5.34 | 0.13 | 0.13 | 0.13 | 0.13 | 0.13 | TRUE | TRUE | TRUE | TRUE | TRUE |
| 3/11/2021 | 64.84 | 5.91 | 3.80 | 5.63 | 4.02 | 5.35 | 4.24 | 5.08 | 4.48 | 4.83 | 4.72 | 4.58 | 4.97 | 4.34 | 5.23 | 0.27 | 0.27 | 0.27 | 0.26 | 0.27 | TRUE | TRUE | TRUE | TRUE | TRUE |
| 3/10/2021 | 65.11 | 4.91 | 4.33 | 4.65 | 4.57 | 4.40 | 4.82 | 4.16 | 5.08 | 3.94 | 5.36 | 3.72 | 5.64 | 3.51 | 5.93 | -1.53 | -1.53 | -1.53 | -1.53 | -1.53 | TRUE | TRUE | TRUE | TRUE | TRUE |
| 3/9/2021 | 63.58 | 4.74 | 4.54 | 4.49 | 4.79 | 4.25 | 5.05 | 4.02 | 5.32 | 3.79 | 5.59 | 3.58 | 5.88 | 3.38 | 6.18 | -0.38 | -0.38 | -0.38 | -0.38 | -0.38 | TRUE | TRUE | TRUE | TRUE | TRUE |
| 3/8/2021 | 63.20 | 5.09 | 4.15 | 4.82 | 4.38 | 4.56 | 4.62 | 4.31 | 4.87 | 4.07 | 5.13 | 3.84 | 5.40 | 3.62 | 5.68 | 0.74 | 0.74 | 0.74 | 0.74 | 0.74 | TRUE | TRUE | TRUE | TRUE | TRUE |
| 3/5/2021 | 63.94 | 5.69 | 3.88 | 5.41 | 4.10 | 5.13 | 4.32 | 4.87 | 4.56 | 4.61 | 4.80 | 4.36 | 5.05 | 4.12 | 5.31 | 0.87 | 0.87 | 0.87 | 0.87 | 0.87 | TRUE | TRUE | TRUE | TRUE | TRUE |
| 3/4/2021 | 64.81 | 4.23 | 4.71 | 3.99 | 4.97 | 3.76 | 5.24 | 3.54 | 5.52 | 3.32 | 5.80 | 3.12 | 6.10 | 2.92 | 6.40 | -2.29 | -2.29 | -2.29 | -2.29 | -2.29 | TRUE | TRUE | TRUE | TRUE | TRUE |
| 3/3/2021 | 62.52 | 3.23 | 6.08 | 3.03 | 6.38 | 2.85 | 6.70 | 2.67 | 7.02 | 2.51 | 7.36 | 2.35 | 7.70 | 2.20 | 8.05 | -2.37 | -2.37 | -2.37 | -2.37 | -2.37 | TRUE | TRUE | TRUE | TRUE | TRUE |
| 3/2/2021 | 60.15 | 2.62 | 6.97 | 2.46 | 7.30 | 2.29 | 7.64 | 2.14 | 7.99 | 2.00 | 8.35 | 1.87 | 8.71 | 1.74 | 9.09 | -1.50 | -1.49 | -1.50 | -1.50 | -1.50 | TRUE | TRUE | TRUE | TRUE | TRUE |
# Combine data for all strikes into a single data frame
deviation_data_jul21 <- data.frame(
Strike = rep(c(63, 63.5, 64, 64.5, 65), each = nrow(data_jul21)),
ParityDeviation = c(data_jul21$deviation63, data_jul21$deviation63.5,
data_jul21$deviation64, data_jul21$deviation64.5,
data_jul21$deviation65)
)
# Plot deviations
ggplot(deviation_data_jul21, aes(x = Strike, y = ParityDeviation)) +
geom_boxplot() +
geom_hline(yintercept = c(-transaction_cost, transaction_cost), color = "red", linetype = "dashed") +
labs(title = "Put-Call Parity Deviations July 21", y = "Parity Deviation", x = "Strike Price")# Filter out rows with NA values in ParityDeviation
filtered_data <- deviation_data_jul21 %>%
filter(!is.na(ParityDeviation))
# 1. Line Plot - Parity Deviation Trends by Strike Price
ggplot(filtered_data, aes(x = Strike, y = ParityDeviation)) +
geom_line(color = "blue", size = 1) +
geom_point(color = "darkblue", size = 2) +
labs(
title = "Parity Deviation Trends by Strike Price",
x = "Strike Price",
y = "Parity Deviation"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 12, face = "bold"),
axis.title = element_text(size = 10),
axis.text = element_text(size = 10)
)# 2. Histogram - Distribution of Parity Deviations
ggplot(filtered_data, aes(x = ParityDeviation)) +
geom_histogram(binwidth = 0.02, fill = "blue", color = "black", alpha = 0.7) +
labs(
title = "Distribution of Parity Deviations",
x = "Parity Deviation",
y = "Frequency"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 12, face = "bold"),
axis.title = element_text(size = 10),
axis.text = element_text(size = 10)
)# 3. Boxplot - Parity Deviations by Strike Price
ggplot(filtered_data, aes(x = factor(Strike), y = ParityDeviation)) +
geom_boxplot(fill = "lightblue", color = "darkblue", outlier.color = "red", outlier.size = 2) +
labs(
title = "Boxplot of Parity Deviations by Strike Price",
x = "Strike Price",
y = "Parity Deviation"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 12, face = "bold"),
axis.title = element_text(size = 10),
axis.text = element_text(size = 10)
)Conclusion from Results
By analyzing the flagged arbitrage opportunities and
deviations:
If all deviations are within ±0.05, the market is
arbitrage-free at the specified transaction cost.
If some deviations exceed ±0.05, those specific rows
indicate potential arbitrage opportunities that can be exploited.
We Found 1 Arbitrage opportunity for the expiration April
21 & June 21. And several
arbitrage opportunities for July 21.
Mohammad Hossein
Ardestani
MQIM 3768677
Faculty of Management
University of New Brunswick
mhossein.ardestani@unb.ca
Md Mahmudul
Hasan
MQIM 3760573
Faculty of Management
University of New Brunswick
mahmudul.hasan@unb.ca