Coverage Report

Created: 2025-11-04 06:12

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/quantlib/ql/cashflows/overnightindexedcouponpricer.cpp
Line
Count
Source
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3
/*
4
 Copyright (C) 2009 Roland Lichters
5
 Copyright (C) 2009 Ferdinando Ametrano
6
 Copyright (C) 2014 Peter Caspers
7
 Copyright (C) 2016 Stefano Fondi
8
 Copyright (C) 2017 Joseph Jeisman
9
 Copyright (C) 2017 Fabrice Lecuyer
10
11
 This file is part of QuantLib, a free-software/open-source library
12
 for financial quantitative analysts and developers - http://quantlib.org/
13
14
 QuantLib is free software: you can redistribute it and/or modify it
15
 under the terms of the QuantLib license.  You should have received a
16
 copy of the license along with this program; if not, please email
17
 <quantlib-dev@lists.sf.net>. The license is also available online at
18
 <https://www.quantlib.org/license.shtml>.
19
20
 This program is distributed in the hope that it will be useful, but WITHOUT
21
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
22
 FOR A PARTICULAR PURPOSE.  See the license for more details.
23
*/
24
25
#include <ql/cashflows/overnightindexedcouponpricer.hpp>
26
27
namespace QuantLib {
28
29
    namespace {
30
31
        Size determineNumberOfFixings(const std::vector<Date>& interestDates,
32
                                      const Date& date,
33
0
                                      bool applyObservationShift) {
34
0
            Size n = std::lower_bound(interestDates.begin(), interestDates.end(), date) -
35
0
                     interestDates.begin();
36
            // When using the observation shift, it may happen that
37
            // that the end of accrual period will fall later than the last
38
            // interest date. In which case, n will be equal to the number of
39
            // interest dates, while we know that the number of fixing dates is
40
            // always one less than the number of interest dates.
41
0
            return n == interestDates.size() && applyObservationShift ? n - 1 : n;
42
0
        }
43
    }
44
45
0
    void CompoundingOvernightIndexedCouponPricer::initialize(const FloatingRateCoupon& coupon) {
46
0
        coupon_ = dynamic_cast<const OvernightIndexedCoupon*>(&coupon);
47
0
        QL_ENSURE(coupon_, "wrong coupon type");
48
0
    }
49
50
0
    Rate CompoundingOvernightIndexedCouponPricer::swapletRate() const {
51
0
        return averageRate(coupon_->accrualEndDate());
52
0
    }
53
54
0
    Rate CompoundingOvernightIndexedCouponPricer::averageRate(const Date& date) const {
55
0
        const Date today = Settings::instance().evaluationDate();
56
57
0
        const ext::shared_ptr<OvernightIndex> index =
58
0
            ext::dynamic_pointer_cast<OvernightIndex>(coupon_->index());
59
0
        const auto& pastFixings = index->timeSeries();
60
61
0
        const auto& fixingDates = coupon_->fixingDates();
62
0
        const auto& valueDates = coupon_->valueDates();
63
0
        const auto& interestDates = coupon_->interestDates();
64
0
        const auto& dt = coupon_->dt();
65
0
        const bool applyObservationShift = coupon_->applyObservationShift();
66
67
0
        Size i = 0;
68
0
        const Size n = determineNumberOfFixings(interestDates, date, applyObservationShift);
69
70
0
        Real compoundFactor = 1.0;
71
72
        // already fixed part
73
0
        while (i < n && fixingDates[i] < today) {
74
            // rate must have been fixed
75
0
            const Rate fixing = pastFixings[fixingDates[i]];
76
0
            QL_REQUIRE(fixing != Null<Real>(),
77
0
                       "Missing " << index->name() << " fixing for " << fixingDates[i]);
78
0
            Time span = (date >= interestDates[i + 1] ?
79
0
                             dt[i] :
80
0
                             index->dayCounter().yearFraction(interestDates[i], date));
81
0
            compoundFactor *= (1.0 + fixing * span);
82
0
            ++i;
83
0
        }
84
85
        // today is a border case
86
0
        if (i < n && fixingDates[i] == today) {
87
            // might have been fixed
88
0
            try {
89
0
                Rate fixing = pastFixings[fixingDates[i]];
90
0
                if (fixing != Null<Real>()) {
91
0
                    Time span = (date >= interestDates[i + 1] ?
92
0
                                     dt[i] :
93
0
                                     index->dayCounter().yearFraction(interestDates[i], date));
94
0
                    compoundFactor *= (1.0 + fixing * span);
95
0
                    ++i;
96
0
                } else {
97
0
                    ; // fall through and forecast
98
0
                }
99
0
            } catch (Error&) {
100
0
                ; // fall through and forecast
101
0
            }
102
0
        }
103
104
        // forward part using telescopic property in order
105
        // to avoid the evaluation of multiple forward fixings
106
        // where possible.
107
0
        if (i < n) {
108
0
            const Handle<YieldTermStructure> curve = index->forwardingTermStructure();
109
0
            QL_REQUIRE(!curve.empty(),
110
0
                       "null term structure set to this instance of " << index->name());
111
112
0
            const auto effectiveRate = [&index, &fixingDates, &date, &interestDates,
113
0
                                        &dt](Size position) {
114
0
                Rate fixing = index->fixing(fixingDates[position]);
115
0
                Time span = (date >= interestDates[position + 1] ?
116
0
                                 dt[position] :
117
0
                                 index->dayCounter().yearFraction(interestDates[position], date));
118
0
                return span * fixing;
119
0
            };
120
121
0
            if (!coupon_->canApplyTelescopicFormula()) {
122
                // With lookback applied, the telescopic formula cannot be used,
123
                // we need to project each fixing in the coupon.
124
                // Only in one particular case when observation shift is used and
125
                // no intrinsic index fixing delay is applied, the telescopic formula
126
                // holds, because regardless of the fixing delay in the coupon,
127
                // in such configuration value dates will be equal to interest dates.
128
                // A potential lockout, which may occur in tandem with a lookback
129
                // setting, will be handled automatically based on fixing dates.
130
                // Same applies to a case when accrual calculation date does or
131
                // does not occur on an interest date.
132
0
                while (i < n) {
133
0
                    compoundFactor *= (1.0 + effectiveRate(i));
134
0
                    ++i;
135
0
                }
136
0
            } else {
137
                // No lookback, we can partially apply the telescopic formula.
138
                // But we need to make a correction for a potential lockout.
139
0
                const Size nLockout = n - coupon_->lockoutDays();
140
0
                const bool isLockoutApplied = coupon_->lockoutDays() > 0;
141
142
                // Lockout could already start at or before i.
143
                // In such case the ratio of discount factors will be equal to 1.
144
0
                const DiscountFactor startDiscount =
145
0
                    curve->discount(valueDates[std::min<Size>(nLockout, i)]);
146
0
                if (interestDates[n] == date || isLockoutApplied) {
147
                    // telescopic formula up to potential lockout dates.
148
0
                    const DiscountFactor endDiscount =
149
0
                        curve->discount(valueDates[std::min<Size>(nLockout, n)]);
150
0
                    compoundFactor *= startDiscount / endDiscount;
151
                    // For the lockout periods the telescopic formula does not apply.
152
                    // The value dates (at which the projection is calculated) correspond
153
                    // to the locked-out fixing, while the interest dates (at which the
154
                    // interest over that fixing is accrued) are not fixed at lockout,
155
                    // hence they do not cancel out.
156
0
                    i = std::max(nLockout, i);
157
158
                    // With no lockout, the loop is skipped because i = n.
159
0
                    while (i < n) {
160
0
                        compoundFactor *= (1.0 + effectiveRate(i));
161
0
                        ++i;
162
0
                    }
163
0
                } else {
164
                    // No lockout and date is different than last interest date.
165
                    // The last fixing is not used for its full period (the date is between
166
                    // its start and end date).  We can use the telescopic formula until the
167
                    // previous date, then we'll add the missing bit.
168
0
                    const DiscountFactor endDiscount = curve->discount(valueDates[n - 1]);
169
0
                    compoundFactor *= startDiscount / endDiscount;
170
0
                    compoundFactor *= (1.0 + effectiveRate(n - 1));
171
0
                }
172
0
            }
173
0
        }
174
175
0
        const Rate rate = (compoundFactor - 1.0) / coupon_->accruedPeriod(date);
176
0
        return coupon_->gearing() * rate + coupon_->spread();
177
0
    }
178
179
    void
180
0
    ArithmeticAveragedOvernightIndexedCouponPricer::initialize(const FloatingRateCoupon& coupon) {
181
0
        coupon_ = dynamic_cast<const OvernightIndexedCoupon*>(&coupon);
182
0
        QL_ENSURE(coupon_, "wrong coupon type");
183
0
    }
184
185
0
    Rate ArithmeticAveragedOvernightIndexedCouponPricer::swapletRate() const {
186
187
0
        ext::shared_ptr<OvernightIndex> index =
188
0
            ext::dynamic_pointer_cast<OvernightIndex>(coupon_->index());
189
190
0
        const auto& fixingDates = coupon_->fixingDates();
191
0
        const auto& dt = coupon_->dt();
192
193
0
        Size n = dt.size(), i = 0;
194
195
0
        Real accumulatedRate = 0.0;
196
197
0
        const auto& pastFixings = index->timeSeries();
198
199
        // already fixed part
200
0
        Date today = Settings::instance().evaluationDate();
201
0
        while (i < n && fixingDates[i] < today) {
202
            // rate must have been fixed
203
0
            Rate pastFixing = pastFixings[fixingDates[i]];
204
0
            QL_REQUIRE(pastFixing != Null<Real>(),
205
0
                       "Missing " << index->name() << " fixing for " << fixingDates[i]);
206
0
            accumulatedRate += pastFixing * dt[i];
207
0
            ++i;
208
0
        }
209
210
        // today is a border case
211
0
        if (i < n && fixingDates[i] == today) {
212
            // might have been fixed
213
0
            try {
214
0
                Rate pastFixing = pastFixings[fixingDates[i]];
215
0
                if (pastFixing != Null<Real>()) {
216
0
                    accumulatedRate += pastFixing * dt[i];
217
0
                    ++i;
218
0
                } else {
219
0
                    ; // fall through and forecast
220
0
                }
221
0
            } catch (Error&) {
222
0
                ; // fall through and forecast
223
0
            }
224
0
        }
225
226
        /* forward part using telescopic property in order
227
        to avoid the evaluation of multiple forward fixings
228
        (approximation proposed by Katsumi Takada)*/
229
0
        if (byApprox_ && i < n) {
230
0
            Handle<YieldTermStructure> curve = index->forwardingTermStructure();
231
0
            QL_REQUIRE(!curve.empty(),
232
0
                       "null term structure set to this instance of " << index->name());
233
234
0
            const auto& dates = coupon_->valueDates();
235
0
            DiscountFactor startDiscount = curve->discount(dates[i]);
236
0
            DiscountFactor endDiscount = curve->discount(dates[n]);
237
238
0
            accumulatedRate +=
239
0
                log(startDiscount / endDiscount) -
240
0
                convAdj1(curve->timeFromReference(dates[i]), curve->timeFromReference(dates[n])) -
241
0
                convAdj2(curve->timeFromReference(dates[i]), curve->timeFromReference(dates[n]));
242
0
        }
243
        // otherwise
244
0
        else if (i < n) {
245
0
            Handle<YieldTermStructure> curve = index->forwardingTermStructure();
246
0
            QL_REQUIRE(!curve.empty(),
247
0
                       "null term structure set to this instance of " << index->name());
248
249
0
            const auto& dates = coupon_->valueDates();
250
0
            Time te = curve->timeFromReference(dates[n]);
251
0
            while (i < n) {
252
                // forcast fixing
253
0
                Rate forecastFixing = index->fixing(fixingDates[i]);
254
0
                Time ti1 = curve->timeFromReference(dates[i]);
255
0
                Time ti2 = curve->timeFromReference(dates[i + 1]);
256
                /*convexity adjustment due to payment dalay of each
257
                overnight fixing, supposing an Hull-White short rate model*/
258
0
                Real convAdj = exp(
259
0
                    0.5 * pow(vol_, 2.0) / pow(mrs_, 3.0) * (exp(2 * mrs_ * ti1) - 1) *
260
0
                    (exp(-mrs_ * ti2) - exp(-mrs_ * te)) * (exp(-mrs_ * ti2) - exp(-mrs_ * ti1)));
261
0
                accumulatedRate += convAdj * (1 + forecastFixing * dt[i]) - 1;
262
0
                ++i;
263
0
            }
264
0
        }
265
266
0
        Rate rate = accumulatedRate / coupon_->accrualPeriod();
267
0
        return coupon_->gearing() * rate + coupon_->spread();
268
0
    }
269
270
0
    Real ArithmeticAveragedOvernightIndexedCouponPricer::convAdj1(Time ts, Time te) const {
271
0
        return vol_ * vol_ / (4.0 * pow(mrs_, 3.0)) * (1.0 - exp(-2.0 * mrs_ * ts)) *
272
0
               pow((1.0 - exp(-mrs_ * (te - ts))), 2.0);
273
0
    }
274
275
0
    Real ArithmeticAveragedOvernightIndexedCouponPricer::convAdj2(Time ts, Time te) const {
276
0
        return vol_ * vol_ / (2.0 * pow(mrs_, 2.0)) *
277
0
               ((te - ts) - pow(1.0 - exp(-mrs_ * (te - ts)), 2.0) / mrs_ -
278
0
                (1.0 - exp(-2.0 * mrs_ * (te - ts))) / (2.0 * mrs_));
279
0
    }
280
}