Use QuantLib Python to calculate roll-down of a swap

I would want to use QuantLib Python to calculate 6-month roll-down of a 5-year swap.

I believe that the calculation I need to do is as follows:

$Rolldown=r_{0,5Y}-r_{0,4.5Y}$

Where $r_{0,5Y}$ is the 5Y spot rate and $r_{0,4.5Y}$ is the 4Y spot rate.

You can find the definition of roll-down in the following links:

https://e-markets.nordea.com/api/research/attachment/2796

http://www.gioa.us/presentations/2012/4-Rajappa.pdf

Please let me know whether I got my equation correct.

Assuming that my equation is correct, the following is how I tried to calculate roll-down of the swap using QuantLib Python:

from QuantLib import *

# global data
calendar = TARGET()
todaysDate = Date(6,November,2001);
Settings.instance().evaluationDate = todaysDate
settlementDate = Date(8,November,2001);

# market quotes
deposits = { (1,Weeks): 0.0382,
(1,Months): 0.0372,
(3,Months): 0.0363,
(6,Months): 0.0353,
(9,Months): 0.0348,
(1,Years): 0.0345 }

swaps = { (2,Years): 0.037125,
(3,Years): 0.0398,
(5,Years): 0.0443,
(10,Years): 0.05165,
(15,Years): 0.055175 }

# convert them to Quote objects
for n,unit in deposits.keys():
deposits[(n,unit)] = SimpleQuote(deposits[(n,unit)])
for n,unit in swaps.keys():
swaps[(n,unit)] = SimpleQuote(swaps[(n,unit)])

# build rate helpers

dayCounter = Actual360()
settlementDays = 2
depositHelpers = [ DepositRateHelper(QuoteHandle(deposits[(n,unit)]),
Period(n,unit), settlementDays,
calendar, ModifiedFollowing,
False, dayCounter)
for n, unit in [(1,Weeks),(1,Months),(3,Months),
(6,Months),(9,Months),(1,Years)] ]

fixedLegFrequency = Annual
fixedLegTenor = Period(1,Years)
fixedLegDayCounter = Thirty360()
floatingLegFrequency = Semiannual
floatingLegTenor = Period(6,Months)
swapHelpers = [ SwapRateHelper(QuoteHandle(swaps[(n,unit)]),
Period(n,unit), calendar,
fixedLegDayCounter, Euribor6M())
for n, unit in swaps.keys() ]

# term structure handles

# term-structure construction

helpers = depositHelpers + swapHelpers
depoSwapCurve = PiecewiseFlatForward(settlementDate, helpers, Actual360())

swapEngine = DiscountingSwapEngine(discountTermStructure)

# 5Y Swap

nominal = 1000000

fixedLegFrequency = Annual
fixedLegDayCounter = Thirty360()
fixedRate = 0.04

floatingLegFrequency = Semiannual
fixingDays = 2
index = Euribor6M(forecastTermStructure)
floatingLegDayCounter = index.dayCounter()

fixedSchedule1 = Schedule(settlementDate, maturity1,
fixedLegTenor, calendar,
DateGeneration.Forward, False)
floatingSchedule1 = Schedule(settlementDate, maturity1,
floatingLegTenor, calendar,
DateGeneration.Forward, False)

fixedSchedule1, fixedRate, fixedLegDayCounter,
floatingLegDayCounter)
spot1.setPricingEngine(swapEngine)

# 4.5Y Swap

rolldown_period = Period(6, Months)

fixedSchedule2 = Schedule(settlementDate, maturity2,
fixedLegTenor, calendar,
DateGeneration.Forward, False)
floatingSchedule2 = Schedule(settlementDate, maturity2,
floatingLegTenor, calendar,
DateGeneration.Forward, False)

fixedSchedule2, fixedRate, fixedLegDayCounter,
floatingLegDayCounter)
spot2.setPricingEngine(swapEngine)

# price on two different evaluation dates

spot5Y = spot1.fairRate()
spot4Y6M = spot2.fairRate()

print('5Y spot rate')
print(spot5Y)
print('4.5Y spot rate')
print(spot4Y6M)
print('6 month roll down')
print(spot5Y - spot4Y6M)


Am I correct in how to use QuantLib Python to calculate 6-month roll-down of a 5-year swap? Is it necessary to create 2 swap objects like what I have done in order to calculate swap roll-down?

No, I think you're getting a wrong figure. The general idea might work, but the construction of the second swap must be changed. (Or you can use just one swap; I'll get to that later.)

As I read the documentation you linked, the idea of the second swap would be to model the first swap after 6 months have passed. However, let's look at the two swaps. I'll define a helper function to look at their coupons:

def show_cashflows(leg):
for c in leg:
print '%20s | %s | %.4f%%' % (c.date(), c.amount(),
as_coupon(c).rate()*100)


This is what the fixed leg of the 5-years swap pays...

show_cashflows(spot1.fixedLeg())

November 8th, 2002 | 40000.0 | 4.0000%
November 10th, 2003 | 40000.0 | 4.0000%
November 8th, 2004 | 40000.0 | 4.0000%
November 8th, 2005 | 40000.0 | 4.0000%
November 8th, 2006 | 40000.0 | 4.0000%


...and here is the floating leg:

show_cashflows(spot1.floatingLeg())

May 8th, 2002 | 17748.0555555 | 3.5300%
November 8th, 2002 | 16930.6254304 | 3.3125%
May 8th, 2003 | 19219.7194265 | 3.8227%
November 10th, 2003 | 19755.8616671 | 3.8237%
May 10th, 2004 | 22498.6599297 | 4.4503%
November 8th, 2004 | 22498.6599297 | 4.4503%
May 9th, 2005 | 25550.9745581 | 5.0540%
November 8th, 2005 | 25693.1528497 | 5.0544%
May 8th, 2006 | 25408.8159749 | 5.0537%
November 8th, 2006 | 25835.3508522 | 5.0547%


Your second swap should give you the same coupons, as seen from an evaluation date 6 months later. The floating leg looks the same, except that one coupon is gone:

show_cashflows(spot2.floatingLeg())

May 8th, 2002 | 17748.0555555 | 3.5300%
November 8th, 2002 | 16930.6254304 | 3.3125%
May 8th, 2003 | 19219.7194265 | 3.8227%
November 10th, 2003 | 19755.8616671 | 3.8237%
May 10th, 2004 | 22498.6599297 | 4.4503%
November 8th, 2004 | 22498.6599297 | 4.4503%
May 9th, 2005 | 25550.9745581 | 5.0540%
November 8th, 2005 | 25693.1528497 | 5.0544%
May 8th, 2006 | 25408.8159749 | 5.0537%


(It looks like the last coupon is gone, not the first; but that's because you're assuming that the curve stays the same and shifts in time, and therefore the fixings of the second coupon in six months will equal the fixing of the first coupon now.)

However, here's what the fixed leg looks like:

show_cashflows(spot2.fixedLeg())

November 8th, 2002 | 40000.0 | 4.0000%
November 10th, 2003 | 40000.0 | 4.0000%
November 8th, 2004 | 40000.0 | 4.0000%
November 8th, 2005 | 40000.0 | 4.0000%
May 8th, 2006 | 20000.0 | 4.0000%


Since you created a 4.5 years schedule, the last coupon is shorter and is missing half the amount. This, of course, causes the fair rate to be off.

print spot2.fairRate()

0.0434536470094


You have a couple of ways to fix this.

First, instead of creating a 4.5 years swap, you can create a 5-years swap starting 6 months in the past:

issue2 = calendar.advance(settlementDate, -rolldown_period)

fixedSchedule2 = Schedule(issue2, maturity2,
fixedLegTenor, calendar,
DateGeneration.Forward, False)
floatingSchedule2 = Schedule(issue2, maturity2,
floatingLegTenor, calendar,
DateGeneration.Forward, False)

fixedSchedule2, fixedRate, fixedLegDayCounter,
floatingLegDayCounter)
swap3.setPricingEngine(swapEngine)


Now the fixed leg pays the correct amounts...

show_cashflows(swap3.fixedLeg())

May 8th, 2002 | 40000.0 | 4.0000%
May 8th, 2003 | 40000.0 | 4.0000%
May 10th, 2004 | 40000.0 | 4.0000%
May 9th, 2005 | 40000.0 | 4.0000%
May 8th, 2006 | 40000.0 | 4.0000%


...and the fair rate changes accordingly.

print swap3.fairRate()

0.0387677146311


The second way to fix it is, as you suggested, to use a single swap.

You can build the term structure so that it moves with the evaluation date; instead of passing the reference date explicitly, tell the curve that the reference date should be two business days after the evaluation date.

depoSwapCurve = PiecewiseFlatForward(2, TARGET(), helpers, Actual360())


The rest of the initialization stays the same. With this setup, you can simply ask the fair rate of the swap spot1 at two different evaluation dates:

Settings.instance().evaluationDate = todaysDate
print spot1.fairRate()

0.0443

print spot1.fairRate()

0.038847797035


(The fair rate after 6 months is similar, but not quite the same as the one calculated in the first way. It's probably due to the curve being a bit different due; the tenors of the deposits used for bootstrapping might equal a different number of days depending on the spot date.)

Don't use the Nordea piece - it's not correct. Let me explain why;

If rolldown is the assumption that the IRS curve remains static through time then for a 5Y IRS you have two components; the first fixing, i.e. 0.5Y part which is known, and the 0.5Y4.5Y fwd IRS, which is floating and yet to fix.

If, through rolldown, you assume that the 0.5Y4.5Y IRS will converge to the current 0Y4.5Y IRS then that is what you should be subtracting in order to obtain the number of basis points that are applicable to the portion of risk that exists for the extent of the 0.5Y4.5Y swap.

I don't know why carry and rolldown are so poorly and universally misunderstood in finance but many research pieces from banks are wrong or, if not wrong, poorly conceived and overly complicated. If you want to read accurate details for IRSs consider "pricing and trading interest rate derivatives: a pratical guide to swaps" by JHM Darbyshire, and also take a look at these other answers...

Carry calculation on an interest rate swap

Question on pure carry for two bonds

question regarding carry & roll of a bond