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()
../_images/notebooks_Delta_Hedging_6_0.png

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()
../_images/notebooks_Delta_Hedging_9_0.png

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
[ ]:

[ ]: