Case Study: Hedging A Long-Only SPX Portfolio With Costless Collars

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