Coverage Report

Created: 2025-08-11 06:28

/src/quantlib/ql/experimental/termstructures/crosscurrencyratehelpers.cpp
Line
Count
Source (jump to first uncovered line)
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3
/*
4
 Copyright (C) 2021 Marcin Rybacki
5
6
 This file is part of QuantLib, a free-software/open-source library
7
 for financial quantitative analysts and developers - http://quantlib.org/
8
9
 QuantLib is free software: you can redistribute it and/or modify it
10
 under the terms of the QuantLib license.  You should have received a
11
 copy of the license along with this program; if not, please email
12
 <quantlib-dev@lists.sf.net>. The license is also available online at
13
 <http://quantlib.org/license.shtml>.
14
15
 This program is distributed in the hope that it will be useful, but WITHOUT
16
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17
 FOR A PARTICULAR PURPOSE.  See the license for more details.
18
*/
19
20
#include <ql/cashflows/overnightindexedcoupon.hpp>
21
#include <ql/cashflows/iborcoupon.hpp>
22
#include <ql/cashflows/cashflows.hpp>
23
#include <ql/cashflows/simplecashflow.hpp>
24
#include <ql/experimental/termstructures/crosscurrencyratehelpers.hpp>
25
#include <ql/utilities/null_deleter.hpp>
26
#include <utility>
27
28
namespace QuantLib {
29
30
    namespace {
31
32
        Schedule legSchedule(const Date& evaluationDate,
33
                             const Period& tenor,
34
                             const Period& frequency,
35
                             Natural fixingDays,
36
                             const Calendar& calendar,
37
                             BusinessDayConvention convention,
38
0
                             bool endOfMonth) {
39
0
            QL_REQUIRE(tenor >= frequency,
40
0
                       "XCCY instrument tenor should not be smaller than coupon frequency.");
41
42
0
            Date referenceDate = calendar.adjust(evaluationDate);
43
0
            Date earliestDate = calendar.advance(referenceDate, fixingDays * Days, convention);
44
0
            Date maturity = earliestDate + tenor;
45
0
            return MakeSchedule()
46
0
                .from(earliestDate)
47
0
                .to(maturity)
48
0
                .withTenor(frequency)
49
0
                .withCalendar(calendar)
50
0
                .withConvention(convention)
51
0
                .endOfMonth(endOfMonth)
52
0
                .backwards();
53
0
        }
54
55
        Leg buildFloatingLeg(const Date& evaluationDate,
56
                         const Period& tenor,
57
                         Natural fixingDays,
58
                         const Calendar& calendar,
59
                         BusinessDayConvention convention,
60
                         bool endOfMonth,
61
                         const ext::shared_ptr<IborIndex>& idx,
62
                         Frequency paymentFrequency,
63
0
                         Integer paymentLag) {
64
0
            auto overnightIndex = ext::dynamic_pointer_cast<OvernightIndex>(idx);
65
66
0
            Period freqPeriod;
67
0
            if (paymentFrequency == NoFrequency) {
68
0
                QL_REQUIRE(!overnightIndex, "Require payment frequency for overnight indices.");
69
0
                freqPeriod = idx->tenor();
70
0
            } else {
71
0
                freqPeriod = Period(paymentFrequency);
72
0
            }
73
74
0
            Schedule sch = legSchedule(evaluationDate, tenor, freqPeriod, fixingDays, calendar,
75
0
                                       convention, endOfMonth);
76
0
            if (overnightIndex != nullptr) {
77
0
                return OvernightLeg(sch, overnightIndex)
78
0
                    .withNotionals(1.0)
79
0
                    .withPaymentLag(paymentLag);
80
0
            }
81
0
            return IborLeg(sch, idx).withNotionals(1.0).withPaymentLag(paymentLag);
82
0
        }
83
84
        std::pair<Real, Real>
85
        npvbpsConstNotionalLeg(const Leg& iborLeg,
86
                               const Date& initialNotionalExchangeDate,
87
                               const Date& finalNotionalExchangeDate,
88
0
                               const Handle<YieldTermStructure>& discountCurveHandle) {
89
0
            const Spread basisPoint = 1.0e-4;
90
0
            Date refDt = discountCurveHandle->referenceDate();
91
0
            const YieldTermStructure& discountRef = **discountCurveHandle;
92
0
            bool includeSettleDtFlows = true;
93
0
            auto [npv, bps] = CashFlows::npvbps(iborLeg, discountRef, includeSettleDtFlows, refDt, refDt);
94
            // Include NPV of the notional exchange at start and maturity.
95
0
            npv += (-1.0) * discountRef.discount(initialNotionalExchangeDate);
96
0
            npv += discountRef.discount(finalNotionalExchangeDate);
97
0
            bps /= basisPoint;
98
0
            return { npv, bps };
99
0
        }
100
101
        class ResettingLegHelper {
102
          public:
103
            explicit ResettingLegHelper(const YieldTermStructure& discountCurve,
104
                                        const YieldTermStructure& foreignCurve)
105
0
            : discountCurve_(discountCurve), foreignCurve_(foreignCurve) {}
106
0
            DiscountFactor discount(const Date& d) const {
107
0
                return discountCurve_.discount(d);
108
0
            }
109
0
            Real notionalAdjustment(const Date& d) const {
110
0
                return foreignCurve_.discount(d) / discountCurve_.discount(d);
111
0
            }
112
113
          private:
114
            const YieldTermStructure& discountCurve_;
115
            const YieldTermStructure& foreignCurve_;
116
        };
117
118
        class ResettingLegCalculator : public AcyclicVisitor, public Visitor<Coupon> {
119
          public:
120
            explicit ResettingLegCalculator(const YieldTermStructure& discountCurve,
121
                                            const YieldTermStructure& foreignCurve,
122
                                            Integer paymentLag,
123
                                            Calendar paymentCalendar,
124
                                            BusinessDayConvention convention)
125
0
            : helper_(discountCurve, foreignCurve), paymentLag_(paymentLag),
126
0
              paymentCalendar_(std::move(paymentCalendar)), convention_(convention) {}
127
128
0
            void visit(Coupon& c) override {
129
0
                Date start = c.accrualStartDate();
130
0
                Date end = c.accrualEndDate();
131
0
                Time accrual = c.accrualPeriod();
132
0
                Real adjustedNotional = c.nominal() * helper_.notionalAdjustment(start);
133
134
0
                DiscountFactor discountStart, discountEnd;
135
136
0
                if (paymentLag_ == 0) {
137
0
                    discountStart = helper_.discount(start);
138
0
                    discountEnd = helper_.discount(end);
139
0
                } else {
140
0
                    Date paymentStart =
141
0
                        paymentCalendar_.advance(start, paymentLag_, Days, convention_);
142
0
                    Date paymentEnd = paymentCalendar_.advance(end, paymentLag_, Days, convention_);
143
0
                    discountStart = helper_.discount(paymentStart);
144
0
                    discountEnd = helper_.discount(paymentEnd);
145
0
                }
146
147
                // NPV of a resetting coupon consists of a redemption of borrowed amount occurring
148
                // at the end of the accrual period plus the accrued interest, minus the borrowed
149
                // amount at the start of the period. All amounts are corrected by an adjustment
150
                // corresponding to the implied forward exchange rate, which is estimated by
151
                // the ratio of foreign and domestic curves discount factors.
152
0
                Real npvRedeemedAmount =
153
0
                    adjustedNotional * discountEnd * (1.0 + c.rate() * accrual);
154
0
                Real npvBorrowedAmount = -adjustedNotional * discountStart;
155
156
0
                npv_ += (npvRedeemedAmount + npvBorrowedAmount);
157
0
                bps_ += adjustedNotional * discountEnd * accrual;
158
0
            }
159
0
            Real NPV() const { return npv_; }
160
0
            Real BPS() const { return bps_; }
161
162
          private:
163
            ResettingLegHelper helper_;
164
            Real npv_ = 0.0;
165
            Real bps_ = 0.0;
166
            Integer paymentLag_;
167
            Calendar paymentCalendar_;
168
            BusinessDayConvention convention_;
169
        };
170
171
        std::pair<Real, Real> npvbpsResettingLeg(const Leg& iborLeg,
172
                                                 Integer paymentLag,
173
                                                 const Calendar& paymentCalendar,
174
                                                 BusinessDayConvention convention,
175
                                                 const Handle<YieldTermStructure>& discountCurveHandle,
176
0
                                                 const Handle<YieldTermStructure>& foreignCurveHandle) {
177
0
            const YieldTermStructure& discountCurveRef = **discountCurveHandle;
178
0
            const YieldTermStructure& foreignCurveRef = **foreignCurveHandle;
179
180
0
            ResettingLegCalculator calc(discountCurveRef, foreignCurveRef, paymentLag,
181
0
                                        paymentCalendar, convention);
182
0
            for (const auto& i : iborLeg) {
183
0
                CashFlow& cf = *i;
184
0
                cf.accept(calc);
185
0
            }
186
0
            return { calc.NPV(), calc.BPS() };
187
0
        }
188
    }
189
190
    CrossCurrencyBasisSwapRateHelperBase::CrossCurrencyBasisSwapRateHelperBase(
191
        const Handle<Quote>& basis,
192
        const Period& tenor,
193
        Natural fixingDays,
194
        Calendar calendar,
195
        BusinessDayConvention convention,
196
        bool endOfMonth,
197
        ext::shared_ptr<IborIndex> baseCurrencyIndex,
198
        ext::shared_ptr<IborIndex> quoteCurrencyIndex,
199
        Handle<YieldTermStructure> collateralCurve,
200
        bool isFxBaseCurrencyCollateralCurrency,
201
        bool isBasisOnFxBaseCurrencyLeg,
202
        Frequency paymentFrequency,
203
        Integer paymentLag)
204
0
    : RelativeDateRateHelper(basis), tenor_(tenor), fixingDays_(fixingDays),
205
0
      calendar_(std::move(calendar)), convention_(convention), endOfMonth_(endOfMonth),
206
0
      baseCcyIdx_(std::move(baseCurrencyIndex)), quoteCcyIdx_(std::move(quoteCurrencyIndex)),
207
0
      collateralHandle_(std::move(collateralCurve)),
208
0
      isFxBaseCurrencyCollateralCurrency_(isFxBaseCurrencyCollateralCurrency),
209
0
      isBasisOnFxBaseCurrencyLeg_(isBasisOnFxBaseCurrencyLeg),
210
0
      paymentFrequency_(paymentFrequency), paymentLag_(paymentLag) {
211
0
        registerWith(baseCcyIdx_);
212
0
        registerWith(quoteCcyIdx_);
213
0
        registerWith(collateralHandle_);
214
0
        CrossCurrencyBasisSwapRateHelperBase::initializeDates();
215
0
    }
216
217
0
    void CrossCurrencyBasisSwapRateHelperBase::initializeDates() {
218
0
        baseCcyIborLeg_ = buildFloatingLeg(evaluationDate_, tenor_, fixingDays_, calendar_, convention_,
219
0
                                       endOfMonth_, baseCcyIdx_, paymentFrequency_, paymentLag_);
220
0
        quoteCcyIborLeg_ = buildFloatingLeg(evaluationDate_, tenor_, fixingDays_, calendar_,
221
0
                                        convention_, endOfMonth_, quoteCcyIdx_, paymentFrequency_, paymentLag_);
222
0
        earliestDate_ = std::min(CashFlows::startDate(baseCcyIborLeg_), 
223
0
                                CashFlows::startDate(quoteCcyIborLeg_));
224
0
        maturityDate_ = std::max(CashFlows::maturityDate(baseCcyIborLeg_),
225
0
                                 CashFlows::maturityDate(quoteCcyIborLeg_));
226
0
        if (paymentLag_ == 0) {
227
0
            initialNotionalExchangeDate_ = earliestDate_;
228
0
            finalNotionalExchangeDate_ = maturityDate_;
229
0
        } else {
230
0
            initialNotionalExchangeDate_ = calendar_.advance(earliestDate_, paymentLag_, Days, convention_);
231
0
            finalNotionalExchangeDate_ = calendar_.advance(maturityDate_, paymentLag_, Days, convention_);
232
0
        }
233
0
        Date lastPaymentDate = std::max(baseCcyIborLeg_.back()->date(), quoteCcyIborLeg_.back()->date());
234
0
        latestRelevantDate_ = latestDate_ = std::max(maturityDate_, lastPaymentDate);
235
0
    }
236
237
    const Handle<YieldTermStructure>&
238
0
    CrossCurrencyBasisSwapRateHelperBase::baseCcyLegDiscountHandle() const {
239
0
        QL_REQUIRE(!termStructureHandle_.empty(), "term structure not set");
240
0
        QL_REQUIRE(!collateralHandle_.empty(), "collateral term structure not set");
241
0
        return isFxBaseCurrencyCollateralCurrency_ ? collateralHandle_ : termStructureHandle_;
242
0
    }
243
244
    const Handle<YieldTermStructure>&
245
0
    CrossCurrencyBasisSwapRateHelperBase::quoteCcyLegDiscountHandle() const {
246
0
        QL_REQUIRE(!termStructureHandle_.empty(), "term structure not set");
247
0
        QL_REQUIRE(!collateralHandle_.empty(), "collateral term structure not set");
248
0
        return isFxBaseCurrencyCollateralCurrency_ ? termStructureHandle_ : collateralHandle_;
249
0
    }
250
251
0
    void CrossCurrencyBasisSwapRateHelperBase::setTermStructure(YieldTermStructure* t) {
252
        // do not set the relinkable handle as an observer -
253
        // force recalculation when needed
254
0
        bool observer = false;
255
256
0
        ext::shared_ptr<YieldTermStructure> temp(t, null_deleter());
257
0
        termStructureHandle_.linkTo(temp, observer);
258
259
0
        RelativeDateRateHelper::setTermStructure(t);
260
0
    }
261
262
    ConstNotionalCrossCurrencyBasisSwapRateHelper::ConstNotionalCrossCurrencyBasisSwapRateHelper(
263
        const Handle<Quote>& basis,
264
        const Period& tenor,
265
        Natural fixingDays,
266
        const Calendar& calendar,
267
        BusinessDayConvention convention,
268
        bool endOfMonth,
269
        const ext::shared_ptr<IborIndex>& baseCurrencyIndex,
270
        const ext::shared_ptr<IborIndex>& quoteCurrencyIndex,
271
        const Handle<YieldTermStructure>& collateralCurve,
272
        bool isFxBaseCurrencyCollateralCurrency,
273
        bool isBasisOnFxBaseCurrencyLeg,
274
        Frequency paymentFrequency,
275
        Integer paymentLag)
276
0
    : CrossCurrencyBasisSwapRateHelperBase(basis,
277
0
                                           tenor,
278
0
                                           fixingDays,
279
0
                                           calendar,
280
0
                                           convention,
281
0
                                           endOfMonth,
282
0
                                           baseCurrencyIndex,
283
0
                                           quoteCurrencyIndex,
284
0
                                           collateralCurve,
285
0
                                           isFxBaseCurrencyCollateralCurrency,
286
0
                                           isBasisOnFxBaseCurrencyLeg,
287
0
                                           paymentFrequency,
288
0
                                           paymentLag) {}
289
290
0
    Real ConstNotionalCrossCurrencyBasisSwapRateHelper::impliedQuote() const {
291
0
        auto [npvBaseCcy, bpsBaseCcy] = npvbpsConstNotionalLeg(baseCcyIborLeg_, initialNotionalExchangeDate_, finalNotionalExchangeDate_, baseCcyLegDiscountHandle());
292
0
        auto [npvQuoteCcy, bpsQuoteCcy] = npvbpsConstNotionalLeg(quoteCcyIborLeg_, initialNotionalExchangeDate_, finalNotionalExchangeDate_, quoteCcyLegDiscountHandle());
293
0
        Real bps = isBasisOnFxBaseCurrencyLeg_ ? -bpsBaseCcy : bpsQuoteCcy;
294
0
        return -(npvQuoteCcy - npvBaseCcy) / bps;
295
0
    }
296
297
0
    void ConstNotionalCrossCurrencyBasisSwapRateHelper::accept(AcyclicVisitor& v) {
298
0
        auto* v1 = dynamic_cast<Visitor<ConstNotionalCrossCurrencyBasisSwapRateHelper>*>(&v);
299
0
        if (v1 != nullptr)
300
0
            v1->visit(*this);
301
0
        else
302
0
            RateHelper::accept(v);
303
0
    }
304
305
    MtMCrossCurrencyBasisSwapRateHelper::MtMCrossCurrencyBasisSwapRateHelper(
306
        const Handle<Quote>& basis,
307
        const Period& tenor,
308
        Natural fixingDays,
309
        const Calendar& calendar,
310
        BusinessDayConvention convention,
311
        bool endOfMonth,
312
        const ext::shared_ptr<IborIndex>& baseCurrencyIndex,
313
        const ext::shared_ptr<IborIndex>& quoteCurrencyIndex,
314
        const Handle<YieldTermStructure>& collateralCurve,
315
        bool isFxBaseCurrencyCollateralCurrency,
316
        bool isBasisOnFxBaseCurrencyLeg,
317
        bool isFxBaseCurrencyLegResettable,
318
        Frequency paymentFrequency,
319
        Integer paymentLag)
320
0
    : CrossCurrencyBasisSwapRateHelperBase(basis,
321
0
                                           tenor,
322
0
                                           fixingDays,
323
0
                                           calendar,
324
0
                                           convention,
325
0
                                           endOfMonth,
326
0
                                           baseCurrencyIndex,
327
0
                                           quoteCurrencyIndex,
328
0
                                           collateralCurve,
329
0
                                           isFxBaseCurrencyCollateralCurrency,
330
0
                                           isBasisOnFxBaseCurrencyLeg,
331
0
                                           paymentFrequency,
332
0
                                           paymentLag),
333
0
      isFxBaseCurrencyLegResettable_(isFxBaseCurrencyLegResettable) {}
334
335
0
    Real MtMCrossCurrencyBasisSwapRateHelper::impliedQuote() const {
336
0
        Real npvBaseCcy = 0.0, bpsBaseCcy = 0.0;
337
0
        Real npvQuoteCcy = 0.0, bpsQuoteCcy = 0.0;
338
0
        if (isFxBaseCurrencyLegResettable_) {
339
0
            std::tie(npvBaseCcy, bpsBaseCcy) =
340
0
                npvbpsResettingLeg(baseCcyIborLeg_, paymentLag_, calendar_, convention_,
341
0
                                   baseCcyLegDiscountHandle(), quoteCcyLegDiscountHandle());
342
0
            std::tie(npvQuoteCcy, bpsQuoteCcy) =
343
0
                npvbpsConstNotionalLeg(quoteCcyIborLeg_, initialNotionalExchangeDate_,
344
0
                                       finalNotionalExchangeDate_, quoteCcyLegDiscountHandle());
345
0
        } else {
346
0
            std::tie(npvBaseCcy, bpsBaseCcy) =
347
0
                npvbpsConstNotionalLeg(baseCcyIborLeg_, initialNotionalExchangeDate_,
348
0
                                       finalNotionalExchangeDate_, baseCcyLegDiscountHandle());
349
0
            std::tie(npvQuoteCcy, bpsQuoteCcy) = npvbpsResettingLeg(
350
0
                                       quoteCcyIborLeg_, paymentLag_, calendar_, convention_,
351
0
                                       quoteCcyLegDiscountHandle(), baseCcyLegDiscountHandle());
352
0
        }
353
354
0
        Real bps = isBasisOnFxBaseCurrencyLeg_ ? -bpsBaseCcy : bpsQuoteCcy;
355
356
0
        return -(npvQuoteCcy - npvBaseCcy) / bps;
357
0
    }
358
359
0
    void MtMCrossCurrencyBasisSwapRateHelper::accept(AcyclicVisitor& v) {
360
0
        auto* v1 = dynamic_cast<Visitor<MtMCrossCurrencyBasisSwapRateHelper>*>(&v);
361
0
        if (v1 != nullptr)
362
0
            v1->visit(*this);
363
0
        else
364
0
            RateHelper::accept(v);
365
0
    }
366
}