Basis swaps and FX
A basis swap between two Euribor tenors, an FX forward, and a curve implied by FX swap points, all on the stable API; then, experimental, a curve from cross-currency basis swaps. The market is flat: €STR at 2.80%, dollars at 4.00%, and EUR/USD at 1.0850.
A basis swap
Euribor 3M plus a spread against Euribor 6M, each forecast on its own curve and both discounted on €STR, as a
FloatFloatSwap. A spread is linear in the 3M leg's coupons, so the leg's BPS gives the fair spread:
var euribor3M = new Euribor3M(new FlatForward(context.EvaluationDate, 0.0295, new Actual365Fixed()));
var euribor6M = new Euribor6M(new FlatForward(context.EvaluationDate, 0.0305, new Actual365Fixed()));
Calendar calendar = new TARGET();
Date start = calendar.Advance(context.EvaluationDate, 2, TimeUnit.Days);
Date end = calendar.Advance(start, 5, TimeUnit.Years, BusinessDayConvention.ModifiedFollowing);
Schedule Quarterly() => new MakeSchedule().From(start).To(end).WithTenor(euribor3M.Tenor).WithCalendar(calendar)
.WithConvention(BusinessDayConvention.ModifiedFollowing).ToSchedule();
Schedule Semiannual() => new MakeSchedule().From(start).To(end).WithTenor(euribor6M.Tenor).WithCalendar(calendar)
.WithConvention(BusinessDayConvention.ModifiedFollowing).ToSchedule();
FloatFloatSwap Swap(double spread) => new(SwapType.Payer, 10e6, 10e6, Quarterly(), euribor3M, new Actual360(),
Semiannual(), euribor6M, new Actual360(), context, spread1: spread);
SwapResults flat = DiscountingSwapEngine.Calculate(Swap(0.0), context, EurDiscount);
// A spread of one basis point on the 3M leg moves that leg's NPV by its BPS.
double fairSpread = -flat.NPV!.Value / flat.LegBPS[0]!.Value * 1e-4;
SwapResults atFair = DiscountingSwapEngine.Calculate(Swap(fairSpread), context, EurDiscount);
| 3M + spread against 6M | Spread | NPV |
|---|---|---|
| Flat | 0.00 bp | 47,324.97 |
| At the fair spread | 10.0320 bp | 0.00 |
An FX forward
Euros sold for dollars in a year, valued from the two discount curves and spot; struck at its fair rate, it is worth nothing:
Date maturity = new TARGET().Advance(context.EvaluationDate, 1, TimeUnit.Years);
var probe = new FxForward(10e6, new EURCurrency(), new USDCurrency(), Spot, maturity, true);
double fair = DiscountingFxForwardEngine.Calculate(probe, context, EurDiscount, UsdDiscount, Spot).FairForwardRate!.Value;
var forward = new FxForward(10e6, new EURCurrency(), new USDCurrency(), fair, maturity, true);
FxForwardResults result = DiscountingFxForwardEngine.Calculate(forward, context, EurDiscount, UsdDiscount, Spot);
| One-year EUR/USD forward | Value |
|---|---|
| Spot | 1.0850 |
| Fair forward rate | 1.098026 |
| NPV of €10 million sold forward at that rate, in dollars | 0.00 |
A curve from FX swap points
A euro curve, collateralized in dollars, bootstrapped from EUR/USD swap points with FxSwapRateHelper:
(Period Tenor, double Points)[] quotes =
[
(new Period(1, TimeUnit.Months), 0.0010), (new Period(3, TimeUnit.Months), 0.0031),
(new Period(6, TimeUnit.Months), 0.0062), (new Period(1, TimeUnit.Years), 0.0125),
];
var helpers = quotes.Select(q => new FxSwapRateHelper(q.Points, Spot, q.Tenor, 2, new TARGET(),
BusinessDayConvention.Following, false, false, UsdDiscount, context)).ToList();
var eurCurve = new PiecewiseYieldCurve<Discount, LogLinear, IterativeBootstrap>(
context.EvaluationDate, helpers, new Actual365Fixed(), context);
The curve gives back each quote:
| Tenor | Swap points | Implied by the curve |
|---|---|---|
| 1M | 10.00 | 10.00 |
| 3M | 31.00 | 31.00 |
| 6M | 62.00 | 62.00 |
| 1Y | 125.00 | 125.00 |
Experimental: a curve from cross-currency basis swaps
The cross-currency basis-swap helpers are experimental, as QuantLib's are: their members can change in any release.
Using them needs the opt-in, NQL0001 in C# (here a #pragma in the sample's own file) and
import nanoql.experimental in Python (concepts). A euro curve bootstrapped from EUR/USD basis
swaps on €STR and SOFR, collateralized in dollars:
var estr = new Estr(BasisAndFx.EurDiscount);
var sofr = new Sofr(BasisAndFx.UsdDiscount);
(int Years, double Basis)[] quotes = [(1, -0.0008), (2, -0.0010), (3, -0.0012), (5, -0.0013)];
var helpers = quotes.Select(q => new ConstNotionalCrossCurrencyBasisSwapRateHelper(
q.Basis, new Period(q.Years, TimeUnit.Years), 2, new TARGET(), BusinessDayConvention.ModifiedFollowing, false,
estr, sofr, BasisAndFx.UsdDiscount, isFxBaseCurrencyCollateralCurrency: false,
isBasisOnFxBaseCurrencyLeg: true, context, paymentFrequency: Frequency.Quarterly)).ToList();
var eurCurve = new PiecewiseYieldCurve<Discount, LogLinear, IterativeBootstrap>(
context.EvaluationDate, helpers, new Actual365Fixed(), context);
| Tenor | Quoted basis | Implied by the curve |
|---|---|---|
| 1Y | -8.00 bp | -8.00 bp |
| 2Y | -10.00 bp | -10.00 bp |
| 3Y | -12.00 bp | -12.00 bp |
| 5Y | -13.00 bp | -13.00 bp |