Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite.
The futures convexity adjustment#
A Eurodollar futures contract is settled every day, a forward rate agreement only at the end. Gains on the futures are reinvested when rates are high and financed when they are low, so the futures rate is the risk-neutral expectation of the future rate and exceeds the forward rate. With the Ho-Lee and Hull-White models (1990) the gap has a closed form; under Ho-Lee
It is negligible for short contracts and worth tens of basis points for long ones, which is why long-dated futures quotes cannot be used as forward rates when bootstrapping.
import matplotlib.pyplot as plt
import numpy as np
from financialkit.fixed_income import NelsonSiegelCurve
from financialkit.interest_rate_models import HoLeeModel, HullWhiteModel, futures_convexity_adjustment
curve = NelsonSiegelCurve(0.05, -0.02, 0.01, 2.0)
Hull’s example#
Hull (Example 6.4): sigma = 1.2%, an 8-year contract on 3-month rates.
print(f"Ho-Lee adjustment: {1e4 * futures_convexity_adjustment(8.0, 8.25, 0.012):.1f} bp (Hull: 47.5 bp)")
Ho-Lee adjustment: 47.5 bp (Hull: 47.5 bp)
The adjustment grows with the square of maturity#
t1 = np.linspace(0.25, 10, 40)
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
for a in (0.0, 0.03, 0.1):
axes[0].plot(t1, 1e4 * futures_convexity_adjustment(t1, t1 + 0.25, 0.012, a), label="Ho-Lee" if a == 0 else f"Hull-White a = {a}")
axes[0].set_xlabel("futures expiry t1 (years)")
axes[0].set_ylabel("futures - forward (bp)")
axes[0].legend()

<matplotlib.legend.Legend object at 0x11a79cd70>
The futures rate is the risk-neutral expected rate#
Simulate the short rate to \(t_1\), compute the 3-month rate there from the model’s bond price, and average it.
expiries = np.array([1.0, 3.0, 5.0, 8.0])
for name, model, a in (("Ho-Lee", HoLeeModel(curve, 0.012), 0.0), ("Hull-White", HullWhiteModel(curve, 0.1, 0.012), 0.1)):
gap, err = [], []
for k, t in enumerate(expiries):
_, r = model.simulate(t, 8, 40_000, rng=k)
R = -np.log(model.zero_coupon_bond(t + 0.25, t=t, state=r[:, -1])) / 0.25
gap.append(R.mean() - curve.forward_rate(t, t + 0.25))
err.append(R.std() / np.sqrt(R.size))
axes[1].errorbar(expiries, 1e4 * np.array(gap), yerr=1.96e4 * np.array(err), fmt="o", capsize=3, label=f"{name} Monte Carlo")
axes[1].plot(t1, 1e4 * futures_convexity_adjustment(t1, t1 + 0.25, 0.012, a), lw=1)
axes[1].set_xlabel("futures expiry t1 (years)")
axes[1].set_ylabel("E[R] - forward (bp)")
axes[1].legend()
fig.tight_layout()
plt.show()
Total running time of the script: (0 minutes 0.072 seconds)