Skip to content
All library documents

Pricing and Simulating Interest Rate Swaps with Hull–White

Article Quant Q&A · Author: Desi_Quant

Summary

The document describes pricing a plain vanilla interest rate swap from a discount curve in QuantLib and simulating short-rate paths with the one-factor Hull–White model. It includes a detailed setup for fixed and floating legs, historical index fixings, a discounting engine, and simulated rate paths whose sample variance and mean are compared with model expressions.

For swap-value simulation, the answer presents two routes. One is to model the swap rate and annuity directly, using a Gaussian short-rate model approximation discussed in a cited reference; the answer does not establish that this method yields a direct Hull–White simulation. The other is to rebuild the implied discount curve at each simulated time step, update the floating-rate index, and reprice the swap. The author characterizes that route as computationally expensive and points to existing C++ work as a possible implementation example. The material is an exploratory code example, not a validated pricing study, and it does not resolve implementation details or compare accuracy and cost quantitatively.

Key ideas

  • A swap’s value can be represented as its annuity multiplied by the difference between the swap rate and fixed coupon.
  • Directly simulating both the swap rate and annuity under Hull–White is raised as a possibility but not demonstrated.
  • An alternative is to rebuild a discount curve at each simulated step and reprice the swap.
  • Pathwise curve construction and repricing may be computationally costly.
  • The code illustrates rate-path moments but does not report a validation of simulated swap values.

Tags

Full text
# discount curve


# Is it possible to price a plain vanilla interest rate swap in Python and simulate the price using Hull White 1 Factor Model simultaneously?












I am trying to price plain vanilla interest rate swap (IRS) using QuantLib. What I am trying to do is to generate a path of simulated IRS price by simulating the interest rates using HW 1 Factor model. Here is what I have accomplished so far:

```
import prettytable as pt
import matplotlib.pyplot as plt
import numpy as np

calendar = ql.TARGET()
evaluationDate = ql.Date(15, ql.April, 2019)
ql.Settings.instance().evaluationDate = evaluationDate
# discount curve

curveDates = [
    ql.Date(15, ql.April, 2019), ql.Date(23, ql.April, 2019), ql.Date(16, ql.May, 2019), ql.Date(16, ql.July, 2019),
    ql.Date(16, ql.October, 2019), ql.Date(16, ql.April, 2020), ql.Date(16, ql.October, 2020), ql.Date(16, ql.April, 2021),
    ql.Date(19, ql.April, 2022), ql.Date(17, ql.April, 2023), ql.Date(16, ql.April, 2024), ql.Date(16, ql.April, 2025),
    ql.Date(16, ql.April, 2026), ql.Date(16, ql.April, 2027), ql.Date(18, ql.April, 2028), ql.Date(16, ql.April, 2029),
    ql.Date(16, ql.April, 2030), ql.Date(16, ql.April, 2031), ql.Date(17, ql.April, 2034), ql.Date(18, ql.April, 2039),
    ql.Date(19, ql.April, 2044), ql.Date(20, ql.April, 2049), ql.Date(16, ql.April, 2054), ql.Date(16, ql.April, 2059),
    ql.Date(16, ql.April, 2064), ql.Date(16, ql.April, 2069)]

discountFactors = [
    1.0, 1.0000735, 1.0003059, 1.0007842, 1.0011807, 1.0023373, 1.0033115,
    1.0039976, 1.0039393, 1.0015958, 0.9972325, 0.9907452, 0.9820912, 0.9715859,
    0.9591332, 0.9455427, 0.9311096, 0.9161298, 0.8705738, 0.8017461, 0.7464983,
    0.7010373, 0.6626670, 0.6289098, 0.5974307, 0.5684840]

discountCurve = ql.DiscountCurve(
    curveDates,
    discountFactors,
    ql.Actual360(),  # 
    calendar)

discountCurveHandle = ql.YieldTermStructureHandle(discountCurve)
euriborIndex = ql.Euribor6M(discountCurveHandle)

# add fixing dates and rates for floating leg

unusedRate = 0.0  # not used in pricing
#euribor-6M at 2019-01-17
rate20190117 = -0.00236  

euriborIndex.addFixing(fixingDate=ql.Date(17, ql.January, 2007), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.July, 2007), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.January, 2008), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.July, 2008), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(15, ql.January, 2009), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(16, ql.July, 2009), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(15, ql.January, 2010), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(15, ql.July, 2010), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.January, 2011), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(15, ql.July, 2011), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.January, 2012), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.July, 2012), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.January, 2013), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.July, 2013), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(16, ql.January, 2014), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.July, 2014), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(15, ql.January, 2015), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(16, ql.July, 2015), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(15, ql.January, 2016), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(15, ql.July, 2016), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.January, 2017), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.July, 2017), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.January, 2018), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.July, 2018), fixing=unusedRate)
euriborIndex.addFixing(fixingDate=ql.Date(17, ql.January, 2019), fixing=rate20190117)

# swap contract
nominal = 36200000.0
spread = 0.009
swapType = ql.VanillaSwap.Receiver
lengthInYears = 25
effectiveDate = ql.Date(19, ql.January, 2007)
terminationDate = effectiveDate + ql.Period(lengthInYears, ql.Years)

# fixed leg

fixedLegFrequency = ql.Period(ql.Semiannual)
fixedLegConvention = ql.ModifiedFollowing
fixedLegDayCounter = ql.Thirty360(ql.Thirty360.USA)
fixedDateGeneration = ql.DateGeneration.Forward
fixedRate = 0.045

fixedSchedule = ql.Schedule(
    effectiveDate,
    terminationDate,
    fixedLegFrequency,
    calendar,
    fixedLegConvention,
    fixedLegConvention,
    fixedDateGeneration,
    False)

# floating leg

floatingLegFrequency = ql.Period(ql.Semiannual)
floatingLegConvention = ql.ModifiedFollowing
floatingLegDayCounter = ql.Actual360()
floatingDateGeneration = ql.DateGeneration.Forward

floatSchedule = ql.Schedule(
    effectiveDate,
    terminationDate,
    floatingLegFrequency,
    calendar,
    floatingLegConvention,
    floatingLegConvention,
    floatingDateGeneration,
    False)

spot25YearSwap = ql.VanillaSwap(
    swapType,
    nominal,
    fixedSchedule,
    fixedRate,
    fixedLegDayCounter,
    floatSchedule,
    euriborIndex,
    spread,
    floatingLegDayCounter)

swapEngine = ql.DiscountingSwapEngine(discountCurveHandle)
spot25YearSwap.setPricingEngine(swapEngine)

fixedNpv = 0.0
floatingNpv = 0.0

for cf in spot25YearSwap.fixedLeg():
    if cf.date() > evaluationDate:
        fixedNpv = fixedNpv + discountCurveHandle.discount(cf.date()) * cf.amount()
        print('Fixed Leg Cash Flows (no nominal):',fixedNpv)

fixedNpv = fixedNpv + discountCurveHandle.discount(spot25YearSwap.fixedLeg()[-1].date()) * nominal

for cf in spot25YearSwap.floatingLeg():
    if cf.date() > evaluationDate:
        floatingNpv = floatingNpv + discountCurveHandle.discount(cf.date()) * cf.amount()
        print('Floating Leg Cash Flows (no nominal):', floatingNpv)

floatingNpv = floatingNpv + discountCurveHandle.discount(spot25YearSwap.floatingLeg()[-1].date()) * nominal
print('Sum of Fixed Leg Cash Flows (no nominal):',fixedNpv)
print('Sum of Floating Leg Cash Flows (no nominal):', floatingNpv)
print('Price of the Swap is ',fixedNpv - floatingNpv)

###################################################Simulating Short Rates##################################################
sigma = 0.1
a = 0.03
timestep = 360
length = 15 # in years
forward_rate = 0.05
day_count = ql.Thirty360()
todays_date = ql.Date(15, 4, 2019)
ql.Settings.instance().evaluationDate = todays_date

#spot_curve = ql.FlatForward(todays_date, ql.QuoteHandle(ql.SimpleQuote(forward_rate)), day_count)
#spot_curve_handle = ql.YieldTermStructureHandle(spot_curve)

hw_process = ql.HullWhiteProcess(discountCurveHandle, a, sigma)
rng = ql.GaussianRandomSequenceGenerator(ql.UniformRandomSequenceGenerator(timestep, ql.UniformRandomGenerator()))
seq = ql.GaussianPathGenerator(hw_process, length, timestep, rng, False)

def generate_paths(num_paths, timestep):
    arr = np.zeros((num_paths, timestep+1))
    for i in range(num_paths):
        sample_path = seq.next()
        path = sample_path.value()
        time = [path.time(j) for j in range(len(path))]
        value = [path[j] for j in range(len(path))]
        arr[i, :] = np.array(value)
    return np.array(time), arr

num_paths = 1
time, paths = generate_paths(num_paths, timestep)
for i in range(num_paths):
    plt.plot(time, paths[i, :], lw=0.8, alpha=0.6)
plt.title("Hull-White Short Rate Simulation")
plt.show()

num_paths = 1000
time, paths = generate_paths(num_paths, timestep)

vol = [np.var(paths[:, i]) for i in range(timestep+1)]
plt.plot(time, vol, "r-.", lw=3, alpha=0.6)
plt.plot(time,sigma*sigma/(2*a)*(1.0-np.exp(-2.0*a*np.array(time))), "b-", lw=2, alpha=0.5)
plt.title("Variance of Short Rates")

def alpha(forward, sigma, a, t):
    return forward + 0.5* np.power(sigma/a*(1.0 - np.exp(-a*t)), 2)

avg = [np.mean(paths[:, i]) for i in range(timestep+1)]
plt.plot(time, avg, "r-.", lw=3, alpha=0.6)
plt.plot(time,alpha(forward_rate, sigma, a, time), "b-", lw=2, alpha=0.6)
plt.title("Mean of Short Rates")
```

## Answer by mmencke (score 2)

https://quant.stackexchange.com/a/67765

For a plain vanilla swap we can define the annuity $$A(t)\equiv\sum_{i=0}^{N-1} \tau_i P(t,T_{i+1})$$ and write the swap rate as $$S(t)\equiv\frac{P(t,T_0)-P(t,T_N)}{A(t)}$$ such that the swap price can be written as $$V_{swap}(t)=A(t)(S(t)-c)$$ where $c$ is the fixed rate coupon. It would be great if we were able to simulate the swap rate (and annuity) directly, but I am not quite sure whether this is possible in the Hull-White model. On pages 422 to 423 in Andersen & Piterbarg (2010) there is an approximation for the distribution of the swap rate for a general Gaussian one-factor short rate model and how this can be used for pricing swaptions. Maybe the results can be used for simulating the swap rate directly, but I am not sure. Be aware that the specification is a bit more general with Hull-White being a special case.

Another possibility would be to construct the implied discount curve on each step of the path, update the interest rate index and re-price the swap. This is however very computationally expensive and not very elegant. I have made some C++ code here that does this to price Credit Value Adjustment, which you can use for inspiration if you choose to pursue this option.

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.