Welcome back.

In this post, a new notebook on a class that I haven’t seen used a lot in the wild. Enjoy!

Subscribe to my Substack to receive my posts in your inbox, or follow me on Twitter or LinkedIn if you want to be notified of new posts, or subscribe via RSS if you’re the tech type: the buttons for all that are in the footer. Also, I’m available for training, both online and (when possible) on-site: visit my Training page for more information.

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

Par asset swaps

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.011175077404665848

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.