6
$\begingroup$

I'm learning QuantLib-Python and trying to replicate an Interest Rate Swap valuation on a custom curve constructed by passing lists of dates and discounting factors. Please see my code below

import QuantLib as ql

today = ql.Date(31, ql.May, 2023)
ql.Settings.instance().setEvaluationDate(today)

dates = [ql.Date(31, ql.May, 2023), ql.Date(28, ql.August, 2023), ql.Date(27, ql.November, 2023), 
                 ql.Date(26, ql.February, 2024), ql.Date(27, ql.May, 2024), ql.Date(26, ql.May, 2025), ql.Date(26, ql.May, 2026)]

dfs = [1.00, 0.980626530240871, 0.961865563098355, 0.942995352345942, 0.923889577251464, 0.845638911735274, 0.770640534908645]

dayCounter = ql.ActualActual(ql.ActualActual.ISDA)
calendar = ql.UnitedStates()

curve = ql.DiscountCurve(dates, dfs, dayCounter, calendar)
curveHandle = ql.YieldTermStructureHandle(curve)

start = today
maturity = calendar.advance(start, ql.Period('1Y'))
fix_schedule = ql.MakeSchedule(start, maturity, ql.Period('1Y'))
float_schedule = ql.MakeSchedule(start, maturity, ql.Period('3M'))

customIndex = ql.IborIndex('index', ql.Period('3M'), 0, ql.USDCurrency(), ql.UnitedStates(), ql.ModifiedFollowing, True, dayCounter, curveHandle)
customIndex.addFixing(ql.Date(30, ql.May, 2023), 0.075)

notional = 100000000
swap = ql.VanillaSwap(ql.VanillaSwap.Payer, notional, fix_schedule, 0.0833, dayCounter, 
                     float_schedule, customIndex, 0, dayCounter)
swap_engine = ql.DiscountingSwapEngine(curveHandle)
swap.setPricingEngine(swap_engine)

print(swap.NPV())

There is a possibility to print out undiscounted fixed and floating leg cashflows by

print("Net Present Value: {0}".format(swap.NPV()))
print()
print("Fixed leg cashflows:")
for i, cf in enumerate(swap.leg(0)):
    print("%2d    %-18s  %10.2f"%(i+1, cf.date(), cf.amount()))
print()
print("Floating leg cashflows:")
for i, cf in enumerate(swap.leg(1)):
    print("%2d    %-18s  %10.2f"%(i+1, cf.date(), cf.amount()))

The Net Present Value of a swap should be given by the difference of discounted floating and fixed leg cashflows. I tried to replicate calculations manually via

fwd1 = curve.forwardRate(ql.Date(31, 5, 2023), ql.Date(31, 8, 2023), dayCounter, ql.Continuous).rate()
df1 = curve.discount(ql.Date(31, 8, 2023))
tau1 = dayCounter.yearFraction(ql.Date(31, 5, 2023), ql.Date(31, 8, 2023))
cashflow1 = df1 * notional * fwd1 * tau1
print(cashflow1)
print()

fwd2 = curve.forwardRate(ql.Date(1, 9, 2023), ql.Date(30, 11, 2023), dayCounter, ql.Simple).rate()
df2 = curve.discount(ql.Date(30, 11, 2023))
tau2 = dayCounter.yearFraction(ql.Date(1, 9, 2023), ql.Date(30, 11, 2023))
cashflow2 = df2 * notional * fwd2 * tau2
print(cashflow2)
print()

fwd3 = curve.forwardRate(ql.Date(1, 12, 2023), ql.Date(29, 2, 2024), dayCounter, ql.Simple).rate()
df3 = curve.discount(ql.Date(29, 2, 2024))
tau3 = dayCounter.yearFraction(ql.Date(1, 12, 2023), ql.Date(29, 2, 2024))
cashflow3 = df3 * notional * fwd3 * tau3
print(cashflow3)
print()

fwd4 = curve.forwardRate(ql.Date(1, 3, 2024), ql.Date(31, 5, 2024), dayCounter, ql.Simple).rate()
df4 = curve.discount(ql.Date(31, 5, 2024))
tau4 = dayCounter.yearFraction(ql.Date(1, 3, 2024), ql.Date(31, 5, 2024))
cashflow4 = df4 * notional * fwd4 * tau4
print(cashflow4)
print()

floatCashFlow = cashflow1 + cashflow2 + cashflow3 + cashflow4
print(floatCashFlow)
print()

df = df4
fixRate = 0.0833
tau = dayCounter.yearFraction(ql.Date(31, 5, 2023), ql.Date(31, 5, 2024))
fixCashFlow = df * notional * fixRate * tau
print(fixCashFlow)
print()

valueSwap = floatCashFlow - fixCashFlow
print(valueSwap)

and got a staggeringly different NPV. Can anyone show me how to precisely arrive at a built-in NPV valuation using built-in methods for calling discounting factors and forward rates? I suppose the difference is hidden somewhere in payment schedules but I failed to match the numbers by playing around with dates.

$\endgroup$

2 Answers 2

3
$\begingroup$

1/ the cf.amount() attribute returns undiscounted cashflows not PVd ones

2/ your curve.forwardRate() call is returning a continuously compounded zero rate, use customIndex(fixing date) to get the relevant ibor fwd

$\endgroup$
3
  • $\begingroup$ 1. I'm well aware of it. It was mentioned in my question that these cashflows are undiscounted. 2. customIndex(fixing date) returns error IborIndex object is not callable. I tried customIndex.forecastFixing(fixing date) as well but it says that IborIndex object has no attribute forecastFixing. $\endgroup$
    – Hasek
    Commented Jun 2, 2023 at 14:06
  • 2
    $\begingroup$ Try customIndex.fixing(fixing date) $\endgroup$
    – user35980
    Commented Jun 2, 2023 at 14:12
  • $\begingroup$ customIndex.fixing(fixing date) works. I was able to replicate undiscounted floating and fixed cashflows and thus NPV. Thanks! $\endgroup$
    – Hasek
    Commented Jun 2, 2023 at 14:32
1
$\begingroup$

As a base for comparison you can also try rateslib

from rateslib import Curve, IRS
from pandas import Series

curve = Curve(
    nodes={
        dt(2023, 5, 31): 1.00,
        dt(2023, 8, 28): 0.980626530240871,
        dt(2023, 11, 27): 0.961865563098355,
        dt(2024, 2, 26): 0.942995352345942,
        dt(2024, 5, 27): 0.923889577251464,
        dt(2025, 5, 26): 0.845638911735274,
        dt(2026, 5, 26): 0.770640534908645,
    },
    interpolation = "log_linear",
    calendar="nyc",
    convention="act360",
)

irs = IRS(
    effective=dt(2023, 5, 31),
    termination="1Y",
    calendar="nyc",
    frequency="A",
    convention="act360",
    modifier="MF",
    notional=100e6,
    fixed_rate=8.33,
    leg2_frequency="Q",
    leg2_fixing_method="ibor",
    leg2_method_param=1,
    leg2_fixings=Series([7.5], index=[dt(2023, 5, 30)])
)

enter image description here

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.