Case Study: Hedging A Long-Only SPX Portfolio With Costless Collars#
import warnings
from pathlib import Path
import numpy as np
import pandas as pd
import plotly.io as pio
import pull_options_data
import spx_hedging_functions
from settings import config
pd.set_option("display.max_columns", None)
pio.templates.default = "plotly_white"
warnings.filterwarnings("ignore")
# Load environment variables
DATA_DIR = Path(config("DATA_DIR"))
OUTPUT_DIR = Path(config("OUTPUT_DIR"))
WRDS_USERNAME = config("WRDS_USERNAME")
START_DATE = config("START_DATE")
END_DATE = config("END_DATE")
asof_date = "2023-08-02"
interpolation_args = {"method": "cubic", "order": 3}
# enter the range of costless collar to construct
d = 0.80
contracts_to_display = 15
r = 0.045
N = 1000
days_in_year = 365.0
strike_range = (1000, 6000)
expiry_range = (0.01, 2.0)
dt = 1 / days_in_year
option_chain = pull_options_data.load_spx_option_data(
asof_date=asof_date, start_date=START_DATE, end_date=END_DATE
)
# do we need to check if forward prices for calls and puts are the same? no, because the sql_query() function is pulling the forward price from a single other dataset and joining tables
# Build market predictions
delta_map, prediction_range = spx_hedging_functions.build_market_predictions(
option_chain=option_chain,
asof_date=asof_date,
contracts_to_display=contracts_to_display,
interpolation_args=interpolation_args,
)
# Display prediction range table
prediction_range_reformat = prediction_range.copy()
prediction_range_reformat.index = prediction_range_reformat.index.strftime("%Y-%m-%d")
prediction_range_reformat.index.name = "Expiration Date"
prediction_range_reformat = prediction_range_reformat.T
prediction_range_reformat = prediction_range_reformat.iloc[:, :contracts_to_display]
prediction_range_reformat = prediction_range_reformat.style.format("{:.2f}")
prediction_range_reformat
Expiration Date | 2023-08-03 | 2023-08-04 | 2023-08-07 | 2023-08-08 | 2023-08-09 | 2023-08-10 | 2023-08-11 | 2023-08-14 | 2023-08-15 | 2023-08-16 | 2023-08-17 | 2023-08-18 | 2023-08-21 | 2023-08-22 | 2023-08-23 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
80% Below | 4545.00 | 4560.00 | 4560.00 | 4565.00 | 4570.00 | 4580.00 | 4585.00 | 4595.00 | 4600.00 | 4605.00 | 4605.00 | 4610.00 | 4620.00 | 4625.00 | 4625.00 |
50/50 | 4515.00 | 4515.00 | 4515.00 | 4520.00 | 4520.00 | 4520.00 | 4520.00 | 4520.00 | 4520.00 | 4525.00 | 4525.00 | 4525.00 | 4525.00 | 4525.00 | 4530.00 |
80% Above | 4475.00 | 4460.00 | 4455.00 | 4450.00 | 4440.00 | 4430.00 | 4425.00 | 4420.00 | 4415.00 | 4410.00 | 4405.00 | 4400.00 | 4395.00 | 4395.00 | 4390.00 |
Fwd Prices 2023-08-02 | 4513.99 | 4514.59 | 4516.40 | 4517.01 | 4517.61 | 4518.21 | 4518.81 | 4520.64 | 4521.25 | 4521.86 | 4522.47 | 4523.09 | 4524.94 | 4525.56 | 4526.18 |
# Create pareto chart visualization
pareto_data = spx_hedging_functions.build_pareto_chart(
ticker="SPX",
asof_date=asof_date,
prediction_range=prediction_range,
delta_map=delta_map,
contracts_to_display=contracts_to_display,
)
# Display pareto chart
pareto_data['figure'].show()
# Display prediction range table
pareto_data['prediction_range'].style.format("{:.2f}")
Expiration Date | 2023-08-03 | 2023-08-04 | 2023-08-07 | 2023-08-08 | 2023-08-09 | 2023-08-10 | 2023-08-11 | 2023-08-14 | 2023-08-15 | 2023-08-16 | 2023-08-17 | 2023-08-18 | 2023-08-21 | 2023-08-22 | 2023-08-23 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
80% Below | 4545.00 | 4560.00 | 4560.00 | 4565.00 | 4570.00 | 4580.00 | 4585.00 | 4595.00 | 4600.00 | 4605.00 | 4605.00 | 4610.00 | 4620.00 | 4625.00 | 4625.00 |
50/50 | 4515.00 | 4515.00 | 4515.00 | 4520.00 | 4520.00 | 4520.00 | 4520.00 | 4520.00 | 4520.00 | 4525.00 | 4525.00 | 4525.00 | 4525.00 | 4525.00 | 4530.00 |
80% Above | 4475.00 | 4460.00 | 4455.00 | 4450.00 | 4440.00 | 4430.00 | 4425.00 | 4420.00 | 4415.00 | 4410.00 | 4405.00 | 4400.00 | 4395.00 | 4395.00 | 4390.00 |
Fwd Prices 2023-08-02 | 4513.99 | 4514.59 | 4516.40 | 4517.01 | 4517.61 | 4518.21 | 4518.81 | 4520.64 | 4521.25 | 4521.86 | 4522.47 | 4523.09 | 4524.94 | 4525.56 | 4526.18 |
# Create delta heatmap visualization
delta_heatmap = spx_hedging_functions.plot_delta_heatmap(
ticker="SPX",
asof_date=asof_date,
delta_map=delta_map,
contracts_to_display=contracts_to_display,
heatmap_step=3,
)
Expiration Date | 2023-08-03 | 2023-08-04 | 2023-08-07 | 2023-08-08 | 2023-08-09 | 2023-08-10 | 2023-08-11 | 2023-08-14 | 2023-08-15 | 2023-08-16 | 2023-08-17 | 2023-08-18 | 2023-08-21 | 2023-08-22 | 2023-08-23 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Price >= | |||||||||||||||
$4,615.00 | 1% | 2% | 4% | 7% | 8% | 11% | 7% | 12% | 14% | 16% | 17% | 19% | 21% | 22% | 23% |
$4,600.00 | 1% | 4% | 8% | 10% | 12% | 10% | 14% | 17% | 19% | 21% | 22% | 24% | 26% | 27% | 28% |
$4,585.00 | 2% | 8% | 12% | 9% | 13% | 17% | 20% | 23% | 25% | 26% | 28% | 29% | 30% | 31% | 32% |
$4,570.00 | 5% | 14% | 13% | 18% | 21% | 25% | 27% | 29% | 31% | 32% | 33% | 34% | 36% | 36% | 37% |
$4,555.00 | 12% | 14% | 24% | 27% | 29% | 32% | 34% | 36% | 37% | 38% | 39% | 39% | 41% | 41% | 42% |
$4,540.00 | 24% | 29% | 34% | 36% | 38% | 40% | 41% | 42% | 43% | 44% | 44% | 45% | 46% | 46% | 46% |
$4,525.00 | 36% | 42% | 45% | 46% | 46% | 47% | 48% | 49% | 49% | 49% | 50% | 50% | 51% | 51% | 51% |
$4,510.00 | 55% | 54% | 54% | 54% | 54% | 54% | 54% | 55% | 55% | 55% | 55% | 55% | 55% | 55% | 55% |
$4,495.00 | 69% | 64% | 63% | 62% | 62% | 61% | 60% | 60% | 60% | 60% | 59% | 59% | 59% | 59% | 59% |
$4,480.00 | 79% | 72% | 71% | 69% | 68% | 66% | 65% | 65% | 65% | 64% | 64% | 64% | 64% | 63% | 63% |
$4,465.00 | 85% | 79% | 77% | 75% | 73% | 71% | 70% | 70% | 69% | 68% | 68% | 67% | 67% | 67% | 66% |
$4,450.00 | 88% | 83% | 81% | 79% | 78% | 76% | 74% | 74% | 73% | 72% | 71% | 71% | 71% | 70% | 70% |
$4,435.00 | 90% | 87% | 85% | 83% | 82% | 79% | 78% | 77% | 76% | 75% | 75% | 74% | 74% | 73% | 73% |
$4,420.00 | 91% | 89% | 88% | 86% | 85% | 82% | 81% | 80% | 79% | 78% | 77% | 77% | 76% | 76% | 75% |
$4,405.00 | 92% | 91% | 90% | 88% | 87% | 85% | 83% | 83% | 82% | 81% | 80% | 79% | 79% | 78% | 78% |
$4,390.00 | 93% | 92% | 91% | 90% | 89% | 87% | 86% | 85% | 84% | 83% | 82% | 81% | 81% | 80% | 80% |
$4,375.00 | 94% | 93% | 92% | 91% | 91% | 88% | 87% | 87% | 86% | 85% | 84% | 83% | 83% | 82% | 82% |
$4,360.00 | 94% | 93% | 93% | 92% | 92% | 90% | 89% | 88% | 87% | 87% | 86% | 85% | 85% | 84% | 83% |
$4,345.00 | 94% | 94% | 94% | 93% | 93% | 91% | 90% | 90% | 89% | 88% | 87% | 87% | 86% | 85% | 85% |
$4,330.00 | 95% | 94% | 94% | 94% | 93% | 92% | 91% | 91% | 90% | 89% | 88% | 88% | 87% | 87% | 86% |
$4,315.00 | 95% | 95% | 95% | 94% | 94% | 93% | 92% | 92% | 91% | 90% | 90% | 89% | 89% | 88% | 88% |
$4,300.00 | 95% | 95% | 95% | 95% | 94% | 93% | 93% | 92% | 92% | 91% | 90% | 90% | 90% | 89% | 89% |
$4,285.00 | 96% | 95% | 95% | 95% | 95% | 94% | 93% | 93% | 93% | 92% | 91% | 91% | 91% | 90% | 90% |
$4,270.00 | 96% | 96% | 96% | 95% | 95% | 94% | 94% | 94% | 93% | 93% | 92% | 92% | 91% | 91% | 90% |
$4,255.00 | 96% | 96% | 96% | 96% | 95% | 94% | 94% | 94% | 94% | 93% | 93% | 92% | 92% | 92% | 91% |
$4,240.00 | 96% | 96% | 96% | 96% | 96% | 95% | 95% | 95% | 94% | 94% | 93% | 93% | 93% | 92% | 92% |
$4,225.00 | 96% | 96% | 96% | 96% | 96% | 95% | 95% | 95% | 95% | 94% | 94% | 93% | 93% | 93% | 93% |
$4,210.00 | 96% | 96% | 97% | 96% | 96% | 95% | 95% | 95% | 95% | 95% | 94% | 94% | 94% | 93% | 93% |
$4,195.00 | 97% | 96% | 97% | 97% | 96% | 96% | 95% | 96% | 95% | 95% | 95% | 94% | 94% | 94% | 94% |
$4,180.00 | 97% | 97% | 97% | 97% | 96% | 96% | 96% | 96% | 96% | 95% | 95% | 95% | 95% | 94% | 94% |
$4,165.00 | 97% | 97% | 97% | 97% | 97% | 96% | 96% | 96% | 96% | 96% | 95% | 95% | 95% | 95% | 94% |
$4,150.00 | 97% | 97% | 97% | 97% | 97% | 96% | 96% | 96% | 96% | 96% | 95% | 95% | 95% | 95% | 95% |
$4,135.00 | 97% | 97% | 97% | 97% | 97% | 96% | 96% | 96% | 96% | 96% | 96% | 96% | 96% | 95% | 95% |
$4,120.00 | 97% | 97% | 97% | 97% | 97% | 96% | 96% | 97% | 96% | 96% | 96% | 96% | 96% | 96% | 95% |
$4,100.00 | 97% | 97% | 97% | 97% | 97% | 97% | 97% | 97% | 97% | 97% | 96% | 96% | 96% | 96% | 96% |
$4,075.00 | 97% | 97% | 98% | 97% | 97% | 97% | 97% | 97% | 97% | 97% | 96% | 96% | 96% | 96% | 96% |
$4,050.00 | 97% | 97% | 98% | 98% | 98% | 97% | 97% | 97% | 97% | 97% | 97% | 97% | 97% | 97% | 96% |
$4,025.00 | 98% | 98% | 98% | 98% | 98% | 97% | 97% | 97% | 97% | 97% | 97% | 97% | 97% | 97% | 97% |
$4,000.00 | 98% | 98% | 98% | 98% | 98% | 97% | 97% | 98% | 97% | 97% | 97% | 97% | 97% | 97% | 97% |
$3,975.00 | 98% | 98% | 98% | 98% | 98% | 97% | 97% | 98% | 98% | 98% | 97% | 97% | 97% | 97% | 97% |
$3,950.00 | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 97% | 97% | 98% | 97% | 97% |
$3,925.00 | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% |
$3,900.00 | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% |
$3,875.00 | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% |
$3,850.00 | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% |
$3,825.00 | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% |
$3,800.00 | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% |
$3,775.00 | 98% | 98% | 99% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% | 98% |
$3,750.00 | 98% | 98% | 99% | 99% | 99% | 98% | 98% | 98% | 99% | 99% | 98% | 98% | 99% | 98% | 98% |
$3,725.00 | 98% | 98% | 99% | 99% | 99% | 98% | 98% | 98% | 99% | 99% | 98% | 98% | 99% | 99% | 99% |
$3,700.00 | 98% | 98% | 99% | 99% | 99% | 98% | 98% | 98% | 99% | 99% | 99% | 99% | 99% | 99% | 99% |
$3,675.00 | 98% | 99% | 99% | 99% | 99% | 98% | 98% | 99% | 99% | 99% | 99% | 98% | 99% | 99% | 99% |
$3,650.00 | 98% | 99% | 99% | 99% | 99% | 99% | 98% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% |
$3,625.00 | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% |
$3,600.00 | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% |
$3,575.00 | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% |
$3,550.00 | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% |
$3,525.00 | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% |
$3,500.00 | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% |
$3,450.00 | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% |
$3,375.00 | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% |
$3,300.00 | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% |
$3,225.00 | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% | 99% |
# get the forward prices
fwd_prices = spx_hedging_functions.get_fwd_prices(asof_date, option_chain, contracts_to_display)
iv_surface, price_surface = spx_hedging_functions.build_iv_and_price_surface(
option_chain,
fwd_prices,
asof_date,
strike_range,
expiry_range,
smoothing=True,
smoothing_params={"sigma": 2, "clip_bounds": (0.05, 0.75)},
)
spx_hedging_functions.plot_vol_price_charts(
iv_surface=iv_surface,
price_surface=price_surface,
strike_range=[np.floor(iv_surface.index.min()), np.ceil(iv_surface.index.max())],
expiry_range=[iv_surface.columns.min(), iv_surface.columns.max()],
iv_surface_title="SPX Implied Volatility",
price_surface_title="Price Surface as of " + asof_date,
)
default_percentiles = np.unique(
np.round(np.sort(np.append(np.arange(0.1, 1.0, 0.1), np.array([d, 1 - d]))), 3)
)
print(f"Constructing a {d:.0%} / {(1 - d):.0%} collar...")
Constructing a 80% / 20% collar...
# build the delta map
delta_map = spx_hedging_functions.build_delta_map(
option_chain,
asof_date,
contracts_to_display,
show_detail=False,
weighted=False,
interpolate_missing=True,
interpolation_args=interpolation_args,
)
# simulate a range of futures, including the collar strike %iles
futures_data = spx_hedging_functions.simulate_futures(
asof_date, fwd_prices, iv_surface, r, N, dt, default_percentiles, random_seed=123
)
# Display simulation results
futures_data['figure'].show()
futures_data['display_data'].T.style.format("{:.2f}")
2023-08-07 | 2023-08-08 | 2023-08-09 | 2023-08-10 | 2023-08-11 | 2023-08-14 | 2023-08-15 | 2023-08-16 | 2023-08-17 | 2023-08-18 | 2023-08-21 | 2023-08-22 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
10% | 4449.50 | 4450.67 | 4438.45 | 4428.40 | 4421.81 | 4413.16 | 4403.29 | 4400.49 | 4400.64 | 4390.03 | 4385.70 | 4379.96 |
20% | 4474.00 | 4475.09 | 4467.02 | 4465.16 | 4458.16 | 4453.21 | 4451.22 | 4453.14 | 4453.47 | 4443.70 | 4443.11 | 4438.91 |
30% | 4492.66 | 4493.71 | 4490.18 | 4487.57 | 4486.09 | 4484.65 | 4485.24 | 4485.20 | 4485.94 | 4484.93 | 4485.61 | 4481.93 |
40% | 4506.44 | 4507.57 | 4509.32 | 4506.90 | 4512.25 | 4511.35 | 4515.92 | 4514.30 | 4515.03 | 4514.33 | 4519.59 | 4514.84 |
50% | 4521.48 | 4521.89 | 4528.31 | 4525.12 | 4530.28 | 4532.85 | 4539.55 | 4538.36 | 4539.39 | 4541.23 | 4549.80 | 4551.05 |
60% | 4537.04 | 4537.69 | 4544.16 | 4543.29 | 4548.85 | 4559.11 | 4564.88 | 4566.87 | 4567.77 | 4570.09 | 4576.72 | 4578.88 |
70% | 4554.38 | 4554.53 | 4561.46 | 4566.46 | 4570.92 | 4583.42 | 4590.72 | 4596.62 | 4597.01 | 4604.42 | 4613.82 | 4611.78 |
80% | 4572.37 | 4572.06 | 4585.45 | 4594.78 | 4596.27 | 4611.94 | 4623.78 | 4631.76 | 4632.83 | 4640.59 | 4654.08 | 4652.53 |
90% | 4599.37 | 4598.36 | 4613.98 | 4619.32 | 4627.56 | 4648.92 | 4664.74 | 4675.08 | 4676.60 | 4682.99 | 4703.57 | 4709.62 |
Strip 2023-08-02 | 4516.40 | 4517.01 | 4517.61 | 4518.21 | 4518.81 | 4520.64 | 4521.25 | 4521.86 | 4522.47 | 4523.09 | 4524.94 | 4525.56 |
simulated_futures = futures_data['simulated_futures']
delta_map.columns = spx_hedging_functions.dates_to_time_remaining(delta_map.columns, asof_date)
delta_map = delta_map.loc[:, simulated_futures.index]
# Build collar strikes data and visualization
collar_data = spx_hedging_functions.build_collar_strikes(
d,
delta_map,
simulated_futures=simulated_futures,
contracts_to_display=contracts_to_display,
asof_date=asof_date,
option_chain=option_chain,
)
# Display the figure
collar_data['figure'].show()
# Display formatted collar strikes table
display_collar_strikes = collar_data['collar_strikes'].copy()
display_collar_strikes.index = display_collar_strikes.index.strftime("%Y-%m-%d")
display_collar_strikes.T.style.format("{:.2f}")
2023-08-07 | 2023-08-08 | 2023-08-09 | 2023-08-10 | 2023-08-11 | 2023-08-14 | 2023-08-15 | 2023-08-16 | 2023-08-17 | 2023-08-18 | 2023-08-21 | 2023-08-22 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
80% Above | 4455.00 | 4450.00 | 4440.00 | 4430.00 | 4425.00 | 4420.00 | 4415.00 | 4410.00 | 4405.00 | 4400.00 | 4395.00 | 4395.00 |
80% Below | 4560.00 | 4565.00 | 4570.00 | 4580.00 | 4585.00 | 4595.00 | 4600.00 | 4605.00 | 4605.00 | 4610.00 | 4620.00 | 4625.00 |
Sim. 80% Below | 4572.37 | 4572.06 | 4585.45 | 4594.78 | 4596.27 | 4611.94 | 4623.78 | 4631.76 | 4632.83 | 4640.59 | 4654.08 | 4652.53 |
Sim. 80% Above | 4474.00 | 4475.09 | 4467.02 | 4465.16 | 4458.16 | 4453.21 | 4451.22 | 4453.14 | 4453.47 | 4443.70 | 4443.11 | 4438.91 |
Strip 2023-08-02 | 4516.40 | 4517.01 | 4517.61 | 4518.21 | 4518.81 | 4520.64 | 4521.25 | 4521.86 | 4522.47 | 4523.09 | 4524.94 | 4525.56 |
Costless Ceiling | 4750.00 | 4630.00 | 4615.00 | 4620.00 | 4620.00 | 4625.00 | 4620.00 | 4620.00 | 4620.00 | 4620.00 | 4620.00 | 4620.00 |
# Display net cost
print(f"Net cost to hedge this set of collars: ${collar_data['net_cost']:.4f} per unit")
Net cost to hedge this set of collars: $-0.3750 per unit