Delta Hedging
This notebook compares delta hedging against simulated data with constant or stochastic volatility.
[1]:
from collections import defaultdict
from dataclasses import dataclass
from datetime import date
from decimal import Decimal
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import QuantLib as ql
from yabte.backtest import (
ADFI_AVAILABLE_AT_CLOSE,
ADFI_AVAILABLE_AT_OPEN,
Book,
CashTransaction,
OHLCAsset,
OrderSizeType,
PositionalOrder,
SimpleOrder,
Strategy,
StrategyRunner,
)
from yabte.utilities.simulation.geometric_brownian_motion import gbm_simulate_paths
from yabte.utilities.simulation.heston import heston_simulate_paths
Black Scholes Asset & Simple Delta Hedge Strategy
[3]:
# TODO: track call premium mtm valuation using constant / stochastic volatility
class QlBsm:
"""Black Scholes Model Pricer"""
def __init__(
self, K: float, sigma: float, exp: date, r: float = 0, S: float | None = None
):
self.option = ql.EuropeanOption(
ql.PlainVanillaPayoff(ql.Option.Call, K),
ql.EuropeanExercise(ql.Date().from_date(exp)),
)
day_counter = ql.ActualActual(ql.ActualActual.ISDA)
calendar = ql.NullCalendar()
self.S = S = ql.SimpleQuote(S or K)
self.r = r = ql.SimpleQuote(r)
self.sigma = sigma = ql.SimpleQuote(sigma)
risk_free_curve = ql.FlatForward(0, calendar, ql.QuoteHandle(r), day_counter)
volatility = ql.BlackConstantVol(
0, calendar, ql.QuoteHandle(sigma), day_counter
)
process = ql.BlackScholesProcess(
ql.QuoteHandle(S),
ql.YieldTermStructureHandle(risk_free_curve),
ql.BlackVolTermStructureHandle(volatility),
)
engine = ql.AnalyticEuropeanEngine(process)
self.option.setPricingEngine(engine)
def calc(
self,
t: date,
S: float | None = None,
sigma: float | None = None,
r: float | None = None,
greeks: bool = False,
) -> float | tuple[float, float, float, float]:
ql.Settings.instance().evaluationDate = ql.Date().from_date(t)
if S is not None:
self.S.setValue(S)
if sigma is not None:
self.sigma.setValue(sigma)
if r is not None:
self.r.setValue(r)
if greeks:
return (
self.option.NPV(),
self.option.delta(),
self.option.gamma(),
self.option.vega(),
)
else:
return self.option.NPV()
@dataclass(kw_only=True)
class BSMOption(OHLCAsset):
"""Black Scholes Model Option"""
K: float
exp: date
r: float = 0
divr: float = 0
cp: int = 1
def data_fields(self):
dfs = super().data_fields()
dfs.append(
(
"IVol",
ADFI_AVAILABLE_AT_CLOSE | ADFI_AVAILABLE_AT_OPEN,
)
)
return dfs
def intraday_traded_price(self, asset_day_data, size) -> Decimal:
ts = asset_day_data.name
bsm_option = QlBsm(K=self.K, sigma=asset_day_data.IVol, exp=self.exp, r=self.r)
price = bsm_option.calc(t=ts, S=asset_day_data.Close)
return round(Decimal(price), self.price_round_dp)
class DeltaHedgingStrat(Strategy):
def init(self):
# capture some data for analysis
self.metrics = defaultdict(dict)
def on_open(self):
data = self.data
p = self.params
ts = self.ts
# buy option on t0
if len(data) == 1:
self.orders.append(SimpleOrder(asset_name="CO_ACME", size=1))
# buy delta hedge shares
t = (p.exp - ts).days / 100
s = data.ACME.Open.iloc[-1]
vol = data.iloc[-1].loc["ACME"].IVol
bsm_option = QlBsm(K=p.K, sigma=vol, exp=p.exp, r=p.r)
_, delta, gamma, vega = bsm_option.calc(t=ts, S=s, greeks=True)
self.orders.append(
PositionalOrder(
asset_name="ACME", size=-1 * delta, size_type=OrderSizeType.QUANTITY
)
)
self.metrics[ts]["delta"] = delta
self.metrics[ts]["gamma"] = gamma
self.metrics[ts]["vega"] = vega
self.metrics[ts]["vol"] = vol
self.metrics[ts]["price"] = s
Constant Volatility
[4]:
# gbm params
r = 0.05
vol = 0.2
s0 = 100
N = 101
T = N / 365
# simulate data
rng = np.random.default_rng(12345) # for reproducibility
ix = pd.date_range(end="20221231", periods=N, freq="D")
p = gbm_simulate_paths(S0=s0, mu=r, sigma=vol, R=1, T=T, n_steps=N, n_sims=1, rng=rng)
df = pd.DataFrame(np.c_[p[:, :, 0], p[:, :, 0]], index=ix)
df.columns = pd.MultiIndex.from_tuples((("ACME", "Open"), ("ACME", "Close")))
# add constant vol to data
df.loc[:, ("ACME", "IVol")] = vol
# assets
assets = [
OHLCAsset(name="ACME", denom="USD", quantity_round_dp=6),
BSMOption(name="CO_ACME", data_label="ACME", K=s0, exp=ix[-1], r=r),
]
# run simulation
book = Book(name="Main", cash="0", rate=0.05 / 100)
sr = StrategyRunner(
data=df,
assets=assets,
strategies=[DeltaHedgingStrat()],
books=[book],
)
srr = sr.run(
{
"r": r,
"vol": vol,
"exp": ix[-1],
"K": s0,
}
)
metrics = pd.DataFrame.from_dict(srr.strategies[0].metrics, orient="index").reindex(
srr.book_history.index
)
[5]:
fig, axs = plt.subplots(2, 3, figsize=(3 * 4, 2 * 3))
fig.suptitle("Constant Volatility")
thc = srr.transaction_history[1:].total.sum()
srr.book_history.Main.plot(ax=axs[0][0], title=f"Total Hedge Cost: {thc:.2f}")
metrics.price.plot(title="price", ax=axs[0][1])
metrics.vol.plot(title="vol", ax=axs[0][2])
metrics.delta.plot(title="delta", ax=axs[1][0])
metrics.gamma.plot(title="gamma", ax=axs[1][1])
metrics.vega.plot(title="vega", ax=axs[1][2])
fig.tight_layout()
Stochastic Volatility
[6]:
# gbm params
r = 0.05
vol = 0.2
s0 = 100
N = 101
T = N / 365
kappa = 4
theta = 0.02
v0 = 0.02
sigma = 0.9
R = 0.9
# simulate data
rng = np.random.default_rng(12345) # for reproducibility
ix = pd.date_range(end="20221231", periods=N, freq="D")
p, vol = heston_simulate_paths(
S0=s0,
v0=v0,
mu=r,
kappa=kappa,
theta=theta,
xi=sigma,
R=np.array([[1, R], [R, 1]]),
T=T,
n_steps=N,
n_sims=1,
rng=rng,
)
df = pd.DataFrame(np.c_[p[:, 0], p[:, 0]], index=ix)
df.columns = pd.MultiIndex.from_tuples((("ACME", "Open"), ("ACME", "Close")))
# add constant vol to data
df.loc[:, ("ACME", "IVol")] = vol
# assets
assets = [
OHLCAsset(name="ACME", denom="USD", quantity_round_dp=6),
BSMOption(name="CO_ACME", data_label="ACME", K=s0, exp=ix[-1], r=r),
]
# run simulation
book = Book(name="Main", cash="0", rate=0.05 / 100)
sr = StrategyRunner(
data=df,
assets=assets,
strategies=[DeltaHedgingStrat()],
books=[book],
)
srr = sr.run(
{
"r": r,
"vol": vol,
"exp": ix[-1],
"K": s0,
}
)
metrics = pd.DataFrame.from_dict(srr.strategies[0].metrics, orient="index").reindex(
srr.book_history.index
)
[7]:
fig, axs = plt.subplots(2, 3, figsize=(3 * 4, 2 * 3))
fig.suptitle("Stochastic Volatility")
thc = srr.transaction_history[1:].total.sum()
srr.book_history.Main.plot(ax=axs[0][0], title=f"Total Hedge Cost: {thc:.2f}")
metrics.price.plot(title="price", ax=axs[0][1])
metrics.vol.plot(title="vol", ax=axs[0][2])
metrics.delta.plot(title="delta", ax=axs[1][0])
metrics.gamma.plot(title="gamma", ax=axs[1][1])
metrics.vega.plot(title="vega", ax=axs[1][2])
fig.tight_layout()
Transactions
[8]:
with pd.option_context("display.max_rows", None):
display(srr.transaction_history.head(20))
| ts | total | desc | quantity | price | asset_name | order_label | book | |
|---|---|---|---|---|---|---|---|---|
| 0 | 2022-09-22 | -1.4100 | buy CO_ACME | 1.00 | 1.41 | CO_ACME | NaN | Main |
| 1 | 2022-09-22 | 90.55420000 | sell ACME | -0.905542 | 100.00 | ACME | NaN | Main |
| 2 | 2022-09-22 | 0.045 | interest payment on cash 89.14 | NaN | NaN | NaN | NaN | Main |
| 3 | 2022-09-23 | -90.72625298 | buy ACME | 0.905542 | 100.19 | ACME | NaN | Main |
| 4 | 2022-09-23 | 83.40617120 | sell ACME | -0.832480 | 100.19 | ACME | NaN | Main |
| 5 | 2022-09-23 | 0.041 | interest payment on cash 81.87 | NaN | NaN | NaN | NaN | Main |
| 6 | 2022-09-24 | -83.72251360 | buy ACME | 0.832480 | 100.57 | ACME | NaN | Main |
| 7 | 2022-09-24 | 77.19642573 | sell ACME | -0.767589 | 100.57 | ACME | NaN | Main |
| 8 | 2022-09-24 | 0.038 | interest payment on cash 75.38 | NaN | NaN | NaN | NaN | Main |
| 9 | 2022-09-25 | -77.47275777 | buy ACME | 0.767589 | 100.93 | ACME | NaN | Main |
| 10 | 2022-09-25 | 73.19413321 | sell ACME | -0.725197 | 100.93 | ACME | NaN | Main |
| 11 | 2022-09-25 | 0.036 | interest payment on cash 71.14 | NaN | NaN | NaN | NaN | Main |
| 12 | 2022-09-26 | -74.17314916 | buy ACME | 0.725197 | 102.28 | ACME | NaN | Main |
| 13 | 2022-09-26 | 74.12998700 | sell ACME | -0.724775 | 102.28 | ACME | NaN | Main |
| 14 | 2022-09-26 | 0.036 | interest payment on cash 71.14 | NaN | NaN | NaN | NaN | Main |
| 15 | 2022-09-27 | -74.78228450 | buy ACME | 0.724775 | 103.18 | ACME | NaN | Main |
| 16 | 2022-09-27 | 73.48366102 | sell ACME | -0.712189 | 103.18 | ACME | NaN | Main |
| 17 | 2022-09-27 | 0.035 | interest payment on cash 69.87 | NaN | NaN | NaN | NaN | Main |
| 18 | 2022-09-28 | -71.91684522 | buy ACME | 0.712189 | 100.98 | ACME | NaN | Main |
| 19 | 2022-09-28 | 62.26871112 | sell ACME | -0.616644 | 100.98 | ACME | NaN | Main |
[ ]:
[ ]: