# Cross-currency swaps

Welcome back.

From time to time, a question on the QuantLib mailing list or, as in this case, on the Quantitative Finance StackExchange causes me to answer with a notebook. Who knows, there might be a second cookbook in there somewhere. Anyway, this time the question was: the library has cross-currency swap rate helpers, but what about the instrument itself?

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.

## Cross-currency swaps

At this time, there’s no instrument class in the library modeling cross-currency swaps. However, it’s possible to calculate their value by working with cashflows. Here is a short example.

```
import QuantLib as ql
import pandas as pd
today = ql.Date(27, ql.October, 2021)
ql.Settings.instance().evaluationDate = today
bps = 1e-4
```

## Sample data

For the purposes of this notebook, we’ll use mock rates. The data frame below holds made-up zero rates for SOFR, USD LIBOR, ESTR and Euribor at a number of nodes…

```
sample_rates = pd.DataFrame(
[
(ql.Date(27, 10, 2021), 0.0229, 0.0893, -0.5490, -0.4869),
(ql.Date(27, 1, 2022), 0.0645, 0.1059, -0.5584, -0.5057),
(ql.Date(27, 4, 2022), 0.0414, 0.1602, -0.5480, -0.5236),
(ql.Date(27, 10, 2022), 0.1630, 0.2601, -0.5656, -0.5030),
(ql.Date(27, 10, 2023), 0.4639, 0.6281, -0.4365, -0.3468),
(ql.Date(27, 10, 2024), 0.7187, 0.9270, -0.3500, -0.2490),
(ql.Date(27, 10, 2025), 0.9056, 1.1257, -0.3041, -0.1590),
(ql.Date(27, 10, 2026), 1.0673, 1.2821, -0.2340, -0.0732),
(ql.Date(27, 10, 2027), 1.1615, 1.3978, -0.1690, -0.0331),
(ql.Date(27, 10, 2028), 1.2326, 1.4643, -0.1041, 0.0346),
(ql.Date(27, 10, 2029), 1.3050, 1.5589, -0.0070, 0.1263),
(ql.Date(27, 10, 2030), 1.3584, 1.5986, 0.0272, 0.1832),
(ql.Date(27, 10, 2031), 1.4023, 1.6488, 0.0744, 0.2599),
(ql.Date(27, 10, 2036), 1.5657, 1.8136, 0.3011, 0.4406),
(ql.Date(27, 10, 2041), 1.6191, 1.8749, 0.3882, 0.5331),
(ql.Date(27, 10, 2046), 1.6199, 1.8701, 0.3762, 0.5225),
(ql.Date(27, 10, 2051), 1.6208, 1.8496, 0.3401, 0.4926),
],
columns=["date", "SOFR", "USDLibor3M", "EUR-USD-discount", "Euribor3M"],
)
```

…and this helper function uses them to create an interest-rate curve. Depending on the context, curves will be used for either forecasting or discounting.

```
def sample_curve(tag):
curve = ql.ZeroCurve(
sample_rates["date"], sample_rates[tag] / 100, ql.Actual365Fixed()
)
return ql.YieldTermStructureHandle(curve)
```

## Const-notional cross-currency swaps

The first kind of cross-currency swaps we’ll model is less common but simpler. The notionals are exchanged at the beginning (where they have the same value, given the exchange rate at that time) and again at the end. There is no rebalancing during the life of the swap. All the coupons in either leg have the same notional in the leg’s payment currency.

In this example we’ll model a 5-years swap paying quarterly coupons, based on 3M Euribor on one leg and 3M USD Libor on the other. We start by creating the indexes and their forecasting curves.

```
euribor3M_curve = sample_curve("Euribor3M")
euribor3M = ql.Euribor(ql.Period(3, ql.Months), euribor3M_curve)
```

```
usdlibor3M_curve = sample_curve("USDLibor3M")
usdlibor3M = ql.USDLibor(ql.Period(3, ql.Months), usdlibor3M_curve)
```

For each of the currencies, we create a corresponding sequence of cashflows, including the notional exchanges (which, unlike for vanilla swaps, don’t cancel out—at least at maturity). The reference notional will be in dollars, converted in EUR at the present exchange rate (also made up). Just for kicks, we’ll also add a spread to the USD leg.

```
notional = 1_000_000
fx_0 = 0.85
spread = 50.0 * bps
```

As I mentioned, the swaps make quarterly payments. The corresponding schedule starts spot and ends in five years.

```
calendar = ql.UnitedStates(ql.UnitedStates.FederalReserve)
start_date = calendar.advance(today, ql.Period(2, ql.Days))
end_date = calendar.advance(start_date, ql.Period(5, ql.Years))
tenor = ql.Period(3, ql.Months)
rule = ql.DateGeneration.Forward
convention = ql.Following
end_of_month = False
schedule = ql.Schedule(
start_date,
end_date,
tenor,
calendar,
convention,
convention,
rule,
end_of_month,
)
```

Since the notionals don’t change, the two legs are similar: an initial lending of notional, the interest payments received according to the schedule and the index fixings, and the final payment of the notional.

```
usd_leg = (
(ql.SimpleCashFlow(-notional, schedule[0]),)
+ ql.IborLeg(
nominals=[notional],
schedule=schedule,
index=usdlibor3M,
spreads=[spread],
)
+ (ql.SimpleCashFlow(notional, schedule[-1]),)
)
```

For the EUR leg, of course, we’ll have to convert the notional in the proper currency.

```
eur_leg = (
(ql.SimpleCashFlow(-notional * fx_0, schedule[0]),)
+ ql.IborLeg(nominals=[notional * fx_0], schedule=schedule, index=euribor3M)
+ (ql.SimpleCashFlow(notional * fx_0, schedule[-1]),)
)
```

Now, we can get the NPV of each leg in its own currency by discounting them with the corresponding curve. For the USD leg, that would be the SOFR curve. For the EUR leg, ideally, a discount curve bootstrapped on cross-currency instruments so that it can capture the basis.

```
sofr_curve = sample_curve("SOFR")
usd_npv = ql.CashFlows.npv(usd_leg, sofr_curve, True)
usd_npv
```

```
35427.65532574046
```

```
eurusd_curve = sample_curve("EUR-USD-discount")
eur_npv = ql.CashFlows.npv(eur_leg, eurusd_curve, True)
eur_npv
```

```
6908.723028828739
```

And of course, we can convert the NPV of the EUR leg in USD:

```
ql.CashFlows.npv(eur_leg, eurusd_curve, True) / fx_0
```

```
8127.90944568087
```

We can also look at each cashflow by means of a short(ish) helper function:

```
def cashflow_data(leg):
data = []
for cf in sorted(leg, key=lambda c: c.date()):
coupon = ql.as_floating_rate_coupon(cf)
if coupon is None:
data.append((cf.date(), None, None, cf.amount()))
else:
data.append(
(
coupon.date(),
coupon.nominal(),
coupon.rate(),
coupon.amount(),
)
)
return pd.DataFrame(
data, columns=["date", "nominal", "rate", "amount"]
).style.format({"amount": "{:.2f}", "nominal": "{:.2f}", "rate": "{:.2%}"})
```

Here are the cashflows in USD…

```
cashflow_data(usd_leg)
```

date | nominal | rate | amount | |
---|---|---|---|---|

0 | October 29th, 2021 | nan | nan% | -1000000.00 |

1 | January 31st, 2022 | 1000000.00 | 0.61% | 1585.56 |

2 | April 29th, 2022 | 1000000.00 | 0.72% | 1750.57 |

3 | July 29th, 2022 | 1000000.00 | 0.81% | 2040.59 |

4 | October 31st, 2022 | 1000000.00 | 0.91% | 2386.92 |

5 | January 30th, 2023 | 1000000.00 | 1.22% | 3080.34 |

6 | May 1st, 2023 | 1000000.00 | 1.40% | 3541.30 |

7 | July 31st, 2023 | 1000000.00 | 1.58% | 3999.86 |

8 | October 30th, 2023 | 1000000.00 | 1.76% | 4444.64 |

9 | January 29th, 2024 | 1000000.00 | 1.79% | 4518.96 |

10 | April 29th, 2024 | 1000000.00 | 1.93% | 4890.80 |

11 | July 29th, 2024 | 1000000.00 | 2.08% | 5262.77 |

12 | October 29th, 2024 | 1000000.00 | 2.22% | 5682.53 |

13 | January 29th, 2025 | 1000000.00 | 2.06% | 5257.82 |

14 | April 29th, 2025 | 1000000.00 | 2.16% | 5388.64 |

15 | July 29th, 2025 | 1000000.00 | 2.25% | 5695.32 |

16 | October 29th, 2025 | 1000000.00 | 2.35% | 6000.94 |

17 | January 29th, 2026 | 1000000.00 | 2.27% | 5807.00 |

18 | April 29th, 2026 | 1000000.00 | 2.35% | 5873.71 |

19 | July 29th, 2026 | 1000000.00 | 2.43% | 6133.38 |

20 | October 29th, 2026 | 1000000.00 | 2.50% | 6388.32 |

21 | October 29th, 2026 | nan | nan% | 1000000.00 |

…and here are those in EUR.

```
cashflow_data(eur_leg)
```

date | nominal | rate | amount | |
---|---|---|---|---|

0 | October 29th, 2021 | nan | nan% | -850000.00 |

1 | January 31st, 2022 | 850000.00 | -0.50% | -1108.91 |

2 | April 29th, 2022 | 850000.00 | -0.53% | -1109.57 |

3 | July 29th, 2022 | 850000.00 | -0.49% | -1042.88 |

4 | October 31st, 2022 | 850000.00 | -0.46% | -1020.88 |

5 | January 30th, 2023 | 850000.00 | -0.30% | -644.90 |

6 | May 1st, 2023 | 850000.00 | -0.22% | -479.05 |

7 | July 31st, 2023 | 850000.00 | -0.15% | -314.08 |

8 | October 30th, 2023 | 850000.00 | -0.07% | -158.20 |

9 | January 29th, 2024 | 850000.00 | -0.12% | -266.58 |

10 | April 29th, 2024 | 850000.00 | -0.08% | -163.55 |

11 | July 29th, 2024 | 850000.00 | -0.03% | -60.50 |

12 | October 29th, 2024 | 850000.00 | 0.02% | 42.55 |

13 | January 29th, 2025 | 850000.00 | 0.04% | 96.24 |

14 | April 29th, 2025 | 850000.00 | 0.09% | 188.22 |

15 | July 29th, 2025 | 850000.00 | 0.13% | 284.92 |

16 | October 29th, 2025 | 850000.00 | 0.18% | 383.98 |

17 | January 29th, 2026 | 850000.00 | 0.20% | 443.61 |

18 | April 29th, 2026 | 850000.00 | 0.25% | 523.68 |

19 | July 29th, 2026 | 850000.00 | 0.29% | 619.73 |

20 | October 29th, 2026 | 850000.00 | 0.33% | 708.11 |

21 | October 29th, 2026 | nan | nan% | 850000.00 |

To get the NPV of the swap, we add those of the two legs after converting the EUR leg back to dollars:

```
NPV = usd_npv - eur_npv / fx_0
NPV
```

```
27299.74588005959
```

## Mark-to-market cross-currency swaps

In this more common kind of cross-currency swaps, the notionals are rebalanced at each coupon date so that their value remain the same (according, of course, to the value of the FX rate at each coupon start).

In order to model this rebalancing feature we will need to estimate the FX rates in the future, the corresponding notionals in Euro for the floating cashflows, and the amounts exchanged due to rebalancing.

The future FX rates can be forecast from the discount curves, since they model the cost of money. We’ll write a convenience function to extract it at any given date:

```
def FX(date):
return fx_0 * sofr_curve.discount(date) / eurusd_curve.discount(date)
```

The notionals at the start of each coupon can now be calculated from the FX rates:

```
start_dates = list(schedule)[:-1]
notionals = [notional * FX(d) for d in start_dates]
```

Given the notionals, we can also calculate the rebalancing cashflows:

```
rebalancing_cashflows = []
for i in range(len(notionals) - 1):
rebalancing_cashflows.append(
ql.SimpleCashFlow(notionals[i] - notionals[i + 1], schedule[i + 1])
)
```

Finally, we can create the two legs and price them. The USD leg is as before, since its notional doesn’t change:

```
usd_leg = (
(ql.SimpleCashFlow(-notional, schedule[0]),)
+ ql.IborLeg(
nominals=[notional],
schedule=schedule,
index=usdlibor3M,
spreads=[spread],
)
+ (ql.SimpleCashFlow(notional, schedule[-1]),)
)
```

```
cashflow_data(usd_leg)
```

date | nominal | rate | amount | |
---|---|---|---|---|

0 | October 29th, 2021 | nan | nan% | -1000000.00 |

1 | January 31st, 2022 | 1000000.00 | 0.61% | 1585.56 |

2 | April 29th, 2022 | 1000000.00 | 0.72% | 1750.57 |

3 | July 29th, 2022 | 1000000.00 | 0.81% | 2040.59 |

4 | October 31st, 2022 | 1000000.00 | 0.91% | 2386.92 |

5 | January 30th, 2023 | 1000000.00 | 1.22% | 3080.34 |

6 | May 1st, 2023 | 1000000.00 | 1.40% | 3541.30 |

7 | July 31st, 2023 | 1000000.00 | 1.58% | 3999.86 |

8 | October 30th, 2023 | 1000000.00 | 1.76% | 4444.64 |

9 | January 29th, 2024 | 1000000.00 | 1.79% | 4518.96 |

10 | April 29th, 2024 | 1000000.00 | 1.93% | 4890.80 |

11 | July 29th, 2024 | 1000000.00 | 2.08% | 5262.77 |

12 | October 29th, 2024 | 1000000.00 | 2.22% | 5682.53 |

13 | January 29th, 2025 | 1000000.00 | 2.06% | 5257.82 |

14 | April 29th, 2025 | 1000000.00 | 2.16% | 5388.64 |

15 | July 29th, 2025 | 1000000.00 | 2.25% | 5695.32 |

16 | October 29th, 2025 | 1000000.00 | 2.35% | 6000.94 |

17 | January 29th, 2026 | 1000000.00 | 2.27% | 5807.00 |

18 | April 29th, 2026 | 1000000.00 | 2.35% | 5873.71 |

19 | July 29th, 2026 | 1000000.00 | 2.43% | 6133.38 |

20 | October 29th, 2026 | 1000000.00 | 2.50% | 6388.32 |

21 | October 29th, 2026 | nan | nan% | 1000000.00 |

The EUR leg, instead, changes notionals and thus it includes the rebalancing payments:

```
eur_leg = (
[ql.SimpleCashFlow(-notionals[0], schedule[0])]
+ list(ql.IborLeg(nominals=notionals, schedule=schedule, index=euribor3M))
+ rebalancing_cashflows
+ [ql.SimpleCashFlow(notionals[-1], schedule[-1])]
)
```

```
cashflow_data(eur_leg)
```

date | nominal | rate | amount | |
---|---|---|---|---|

0 | October 29th, 2021 | nan | nan% | -849973.31 |

1 | January 31st, 2022 | 849973.31 | -0.50% | -1108.87 |

2 | January 31st, 2022 | nan | nan% | 1361.41 |

3 | April 29th, 2022 | 848611.90 | -0.53% | -1107.76 |

4 | April 29th, 2022 | nan | nan% | 1140.19 |

5 | July 29th, 2022 | 847471.71 | -0.49% | -1039.78 |

6 | July 29th, 2022 | nan | nan% | 1688.83 |

7 | October 31st, 2022 | 845782.88 | -0.46% | -1015.82 |

8 | October 31st, 2022 | nan | nan% | 2036.91 |

9 | January 30th, 2023 | 843745.97 | -0.30% | -640.16 |

10 | January 30th, 2023 | nan | nan% | 1989.74 |

11 | May 1st, 2023 | 841756.23 | -0.22% | -474.40 |

12 | May 1st, 2023 | nan | nan% | 2164.38 |

13 | July 31st, 2023 | 839591.85 | -0.15% | -310.23 |

14 | July 31st, 2023 | nan | nan% | 2337.65 |

15 | October 30th, 2023 | 837254.19 | -0.07% | -155.83 |

16 | October 30th, 2023 | nan | nan% | 2508.90 |

17 | January 29th, 2024 | 834745.29 | -0.12% | -261.80 |

18 | January 29th, 2024 | nan | nan% | 2661.04 |

19 | April 29th, 2024 | 832084.25 | -0.08% | -160.10 |

20 | April 29th, 2024 | nan | nan% | 2825.60 |

21 | July 29th, 2024 | 829258.65 | -0.03% | -59.02 |

22 | July 29th, 2024 | nan | nan% | 2988.43 |

23 | October 29th, 2024 | 826270.22 | 0.02% | 41.36 |

24 | October 29th, 2024 | nan | nan% | 3181.28 |

25 | January 29th, 2025 | 823088.95 | 0.04% | 93.19 |

26 | January 29th, 2025 | nan | nan% | 3166.37 |

27 | April 29th, 2025 | 819922.58 | 0.09% | 181.56 |

28 | April 29th, 2025 | nan | nan% | 3227.34 |

29 | July 29th, 2025 | 816695.24 | 0.13% | 273.75 |

30 | July 29th, 2025 | nan | nan% | 3392.07 |

31 | October 29th, 2025 | 813303.17 | 0.18% | 367.40 |

32 | October 29th, 2025 | nan | nan% | 3550.54 |

33 | January 29th, 2026 | 809752.63 | 0.20% | 422.60 |

34 | January 29th, 2026 | nan | nan% | 3259.96 |

35 | April 29th, 2026 | 806492.67 | 0.25% | 496.88 |

36 | April 29th, 2026 | nan | nan% | 3266.85 |

37 | July 29th, 2026 | 803225.82 | 0.29% | 585.63 |

38 | July 29th, 2026 | nan | nan% | 3380.28 |

39 | October 29th, 2026 | 799845.53 | 0.33% | 666.33 |

40 | October 29th, 2026 | nan | nan% | 799845.53 |

Finally, as in the previous case, we can calculate the NPV of each leg and of the swap:

```
usd_npv = ql.CashFlows.npv(usd_leg, sofr_curve, True)
usd_npv
```

```
35427.65532574046
```

```
eur_npv = ql.CashFlows.npv(eur_leg, eurusd_curve, True)
eur_npv
```

```
6678.7560644123005
```

```
NPV = usd_npv - eur_npv / fx_0
NPV
```

```
27570.29524996128
```

## Future developments

The above calculations work but are not very well integrated with the rest of the library; if you’re familiar with QuantLib, you know that usually instruments can monitor market quotes and recalculate automatically when they change. This is not possible with the above; it will be up to you to recreate the cashflows when the FX rate or the curves are updated. A cross-currency swap modeled as an instrument would be way more convenient.

The reason we don’t have such an instrument yet is that, unfortunately, the calculations above are a bit awkward to encapsulate in an instrument class. The constant-notional case is easy enough, but the mark-to-market case requires changing the notionals of the coupons when market quotes change, which is something our current classes were not prepared for. We’ll have to think about it and try to add the required functionality in future releases.