Asset swaps

import QuantLib as ql
import pandas as pd
today = ql.Date(20, ql.May, 2021)
ql.Settings.instance().evaluationDate = today

The AssetSwap class builds a swap that exchanges the coupons from a given bond for floating-rate coupons.

Let’s take a fixed-rate bond as an example:

schedule = ql.Schedule(
    ql.Date(8, ql.February, 2020),
    ql.Date(8, ql.February, 2025),
    ql.Period(6, ql.Months),
    ql.TARGET(),
    ql.Following,
    ql.Following,
    ql.DateGeneration.Backward,
    False,
)
settlementDays = 3
faceAmount = 100
coupons = [0.03]
paymentDayCounter = ql.Thirty360(ql.Thirty360.BondBasis)

bond = ql.FixedRateBond(
    settlementDays, faceAmount, schedule, coupons, paymentDayCounter
)

Besides the bond, the AssetSwap constructor takes the floating-rate index used to fix the exchanged coupons…

forecast_curve = ql.RelinkableYieldTermStructureHandle(
    ql.FlatForward(today, 0.01, ql.Actual360())
)
index = ql.Euribor6M(forecast_curve)

…and other parameters: a spread over the floating rate, the bond price, the schedule for the floating-rate coupons (which is optional: if we pass an empty one, the swap will use the same as the bond), the day-count convention for the floating-rate coupons, and a couple of flags specifying the kind of swap we’re creating.

spread = 0.0050
bond_price = 103.0

pay_fixed = True
par_asset_swap = False

swap = ql.AssetSwap(
    pay_fixed,
    bond,
    bond_price,
    index,
    spread,
    ql.Schedule(),
    index.dayCounter(),
    par_asset_swap,
)

When par_asset_swap = False, the swap creates floating-rate coupons paid on a notional equal to the bond price. As for bonds, it’s possible to extract the coupons and retrieve information on each one:

def print_coupon_info(cashflows):
    data = []
    for cf in cashflows:
        c = ql.as_coupon(cf)
        if c is not None:
            data.append((c.date(), c.rate(), c.nominal(), c.amount()))
        else:
            data.append((cf.date(), None, None, cf.amount()))

    return pd.DataFrame(
        data, columns=["date", "rate", "notional", "amount"]
    ).style.format(
        {"amount": "{:.2f}", "notional": "{:.2f}", "rate": "{:.2%}"}
    )
print_coupon_info(bond.cashflows())
  date rate notional amount
0 August 10th, 2020 3.00% 100.00 1.50
1 February 8th, 2021 3.00% 100.00 1.48
2 August 9th, 2021 3.00% 100.00 1.51
3 February 8th, 2022 3.00% 100.00 1.49
4 August 8th, 2022 3.00% 100.00 1.50
5 February 8th, 2023 3.00% 100.00 1.50
6 August 8th, 2023 3.00% 100.00 1.50
7 February 8th, 2024 3.00% 100.00 1.50
8 August 8th, 2024 3.00% 100.00 1.50
9 February 10th, 2025 3.00% 100.00 1.52
10 February 10th, 2025 nan% nan 100.00
print_coupon_info(swap.leg(0))
  date rate notional amount
0 August 9th, 2021 3.00% 100.00 1.51
1 February 8th, 2022 3.00% 100.00 1.49
2 August 8th, 2022 3.00% 100.00 1.50
3 February 8th, 2023 3.00% 100.00 1.50
4 August 8th, 2023 3.00% 100.00 1.50
5 February 8th, 2024 3.00% 100.00 1.50
6 August 8th, 2024 3.00% 100.00 1.50
7 February 10th, 2025 3.00% 100.00 1.52
8 February 10th, 2025 nan% nan 100.00
print_coupon_info(swap.leg(1))
  date rate notional amount
0 August 10th, 2021 1.50% 103.89 0.33
1 February 10th, 2022 1.50% 103.89 0.80
2 August 10th, 2022 1.50% 103.89 0.78
3 February 10th, 2023 1.50% 103.89 0.80
4 August 10th, 2023 1.50% 103.89 0.78
5 February 12th, 2024 1.50% 103.89 0.81
6 August 12th, 2024 1.50% 103.89 0.79
7 February 10th, 2025 1.50% 103.89 0.79
8 February 10th, 2025 nan% nan 103.89

When par_asset_swap = True, the floating-rate coupons are paid on a notional equal to 100 and the swap includes an upfront payment:

par_asset_swap = True

swap = ql.AssetSwap(
    pay_fixed,
    bond,
    bond_price,
    index,
    spread,
    ql.Schedule(),
    index.dayCounter(),
    par_asset_swap,
)
print_coupon_info(swap.leg(0))
  date rate notional amount
0 August 9th, 2021 3.00% 100.00 1.51
1 February 8th, 2022 3.00% 100.00 1.49
2 August 8th, 2022 3.00% 100.00 1.50
3 February 8th, 2023 3.00% 100.00 1.50
4 August 8th, 2023 3.00% 100.00 1.50
5 February 8th, 2024 3.00% 100.00 1.50
6 August 8th, 2024 3.00% 100.00 1.50
7 February 10th, 2025 3.00% 100.00 1.52
8 February 10th, 2025 nan% nan 100.00
print_coupon_info(swap.leg(1))
  date rate notional amount
0 May 25th, 2021 nan% nan 3.89
1 August 10th, 2021 1.50% 100.00 0.32
2 February 10th, 2022 1.50% 100.00 0.77
3 August 10th, 2022 1.50% 100.00 0.76
4 February 10th, 2023 1.50% 100.00 0.77
5 August 10th, 2023 1.50% 100.00 0.76
6 February 12th, 2024 1.50% 100.00 0.78
7 August 12th, 2024 1.50% 100.00 0.76
8 February 10th, 2025 1.50% 100.00 0.76
9 February 10th, 2025 nan% nan 100.00

In both cases, once we give it an discounting engine, the swap can return more information.

discount_curve = ql.YieldTermStructureHandle(
    ql.FlatForward(today, 0.02, ql.Actual360())
)
swap.setPricingEngine(ql.DiscountingSwapEngine(discount_curve))

The NPV and legNPV methods return the value of the swap or of either leg. In this case we’re paying the bond coupons, therefore the corresponding leg has a negative value.

print(swap.NPV())
print(swap.legNPV(0))
print(swap.legNPV(1))
-2.230481904331157
-104.26057030905751
102.03008840472636

It’s also possible to retrieve the spread over the floating index that would make the swap fair:

fair_spread = swap.fairSpread()
print(fair_spread)
0.01117507740466585

We can test it by re-building the swap with this spread and asking for the NPV again:

swap = ql.AssetSwap(
    pay_fixed,
    bond,
    bond_price,
    index,
    fair_spread,
    ql.Schedule(),
    index.dayCounter(),
    par_asset_swap,
)
swap.setPricingEngine(ql.DiscountingSwapEngine(discount_curve))
print(swap.NPV())
print(swap.legNPV(0))
print(swap.legNPV(1))
0.0
-104.26057030905751
104.26057030905751
print_coupon_info(swap.leg(0))
  date rate notional amount
0 August 9th, 2021 3.00% 100.00 1.51
1 February 8th, 2022 3.00% 100.00 1.49
2 August 8th, 2022 3.00% 100.00 1.50
3 February 8th, 2023 3.00% 100.00 1.50
4 August 8th, 2023 3.00% 100.00 1.50
5 February 8th, 2024 3.00% 100.00 1.50
6 August 8th, 2024 3.00% 100.00 1.50
7 February 10th, 2025 3.00% 100.00 1.52
8 February 10th, 2025 nan% nan 100.00
print_coupon_info(swap.leg(1))
  date rate notional amount
0 May 25th, 2021 nan% nan 3.89
1 August 10th, 2021 2.12% 100.00 0.45
2 February 10th, 2022 2.12% 100.00 1.08
3 August 10th, 2022 2.12% 100.00 1.07
4 February 10th, 2023 2.12% 100.00 1.08
5 August 10th, 2023 2.12% 100.00 1.07
6 February 12th, 2024 2.12% 100.00 1.10
7 August 12th, 2024 2.12% 100.00 1.07
8 February 10th, 2025 2.12% 100.00 1.07
9 February 10th, 2025 nan% nan 100.00

Asset swaps can be built based on other kinds of bonds besides fixed-rate ones; the resulting instances work the same way.