Asset swaps
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.