Coverage Report

Created: 2026-06-08 06:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/quantlib/ql/time/daycounters/actualactual.cpp
Line
Count
Source
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3
/*
4
 Copyright (C) 2000, 2001, 2002, 2003 RiskMap srl
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
 <https://www.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/time/daycounters/actualactual.hpp>
21
#include <algorithm>
22
#include <cmath>
23
24
namespace QuantLib {
25
26
    namespace {
27
28
        // the template argument works around passing a protected type
29
30
        template <class T>
31
        Integer findCouponsPerYear(const T& impl,
32
0
                                   Date refStart, Date refEnd) {
33
            // This will only work for day counts longer than 15 days.
34
0
            auto months = (Integer)std::lround(12 * Real(impl.dayCount(refStart, refEnd)) / 365.0);
35
0
            return (Integer)std::lround(12.0 / Real(months));
36
0
        }
37
38
        std::vector<Date> getListOfPeriodDatesIncludingQuasiPayments(
39
0
                                                   const Schedule& schedule) {
40
            // Process the schedule into an array of dates.
41
0
            Date issueDate = schedule.date(0);
42
0
            std::vector<Date> newDates = schedule.dates();
43
44
0
            if (!schedule.hasIsRegular() || !schedule.isRegular(1))
45
0
            {
46
0
                Date firstCoupon = schedule.date(1);
47
48
0
                Date notionalFirstCoupon =
49
0
                    schedule.calendar().advance(firstCoupon,
50
0
                        -schedule.tenor(),
51
0
                        schedule.businessDayConvention(),
52
0
                        schedule.endOfMonth());
53
54
0
                newDates[0] = notionalFirstCoupon;
55
56
                //long first coupon
57
0
                if (notionalFirstCoupon > issueDate) {
58
0
                    Date priorNotionalCoupon =
59
0
                        schedule.calendar().advance(notionalFirstCoupon,
60
0
                                                    -schedule.tenor(),
61
0
                                                    schedule.businessDayConvention(),
62
0
                                                    schedule.endOfMonth());
63
0
                    newDates.insert(newDates.begin(),
64
0
                                    priorNotionalCoupon); //insert as the first element?
65
0
                }
66
0
            }
67
68
0
            if (!schedule.hasIsRegular() || !schedule.isRegular(schedule.size() - 1))
69
0
            {
70
0
                Date notionalLastCoupon =
71
0
                    schedule.calendar().advance(schedule.date(schedule.size() - 2),
72
0
                        schedule.tenor(),
73
0
                        schedule.businessDayConvention(),
74
0
                        schedule.endOfMonth());
75
76
0
                newDates[schedule.size() - 1] = notionalLastCoupon;
77
78
0
                if (notionalLastCoupon < schedule.endDate())
79
0
                {
80
0
                    Date nextNotionalCoupon =
81
0
                        schedule.calendar().advance(notionalLastCoupon,
82
0
                                                    schedule.tenor(),
83
0
                                                    schedule.businessDayConvention(),
84
0
                                                    schedule.endOfMonth());
85
0
                    newDates.push_back(nextNotionalCoupon);
86
0
                }
87
0
            }
88
89
0
            return newDates;
90
0
        }
91
92
        template <class T>
93
        Time yearFractionWithReferenceDates(const T& impl,
94
                                            const Date& d1, const Date& d2,
95
0
                                            const Date& d3, const Date& d4) {
96
0
            QL_REQUIRE(d1 <= d2,
97
0
                       "This function is only correct if d1 <= d2\n"
98
0
                       "d1: " << d1 << " d2: " << d2);
99
100
0
            Real referenceDayCount = Real(impl.dayCount(d3, d4));
101
            //guess how many coupon periods per year:
102
0
            Integer couponsPerYear;
103
0
            if (referenceDayCount < 16) {
104
0
                couponsPerYear = 1;
105
0
                referenceDayCount = impl.dayCount(d1, d1 + 1 * Years);
106
0
            }
107
0
            else {
108
0
                couponsPerYear = findCouponsPerYear(impl, d3, d4);
109
0
            }
110
0
            return Real(impl.dayCount(d1, d2)) / (referenceDayCount*couponsPerYear);
111
0
        }
112
113
    }
114
115
    ext::shared_ptr<DayCounter::Impl>
116
88.2k
    ActualActual::implementation(ActualActual::Convention c, Schedule schedule) {
117
88.2k
        switch (c) {
118
77.7k
          case ISMA:
119
77.7k
          case Bond:
120
77.7k
            if (!schedule.empty())
121
0
                return ext::make_shared<ISMA_Impl>(std::move(schedule));
122
77.7k
            else
123
77.7k
                return ext::make_shared<Old_ISMA_Impl>();
124
5.26k
          case ISDA:
125
5.26k
          case Historical:
126
5.26k
          case Actual365:
127
5.26k
            return ext::make_shared<ISDA_Impl>();
128
5.26k
          case AFB:
129
5.26k
          case Euro:
130
5.26k
            return ext::make_shared<AFB_Impl>();
131
0
          default:
132
0
            QL_FAIL("unknown act/act convention");
133
88.2k
        }
134
88.2k
    }
135
136
137
    Time ActualActual::ISMA_Impl::yearFraction(const Date& d1,
138
                                               const Date& d2,
139
                                               const Date& d3,
140
0
                                               const Date& d4) const {
141
0
        if (d1 == d2) {
142
0
            return 0.0;
143
0
        } else if (d2 < d1) {
144
0
            return -yearFraction(d2, d1, d3, d4);
145
0
        }
146
147
0
        std::vector<Date> couponDates =
148
0
            getListOfPeriodDatesIncludingQuasiPayments(schedule_);
149
150
0
        Date firstDate = *std::min_element(couponDates.begin(), couponDates.end());
151
0
        Date lastDate = *std::max_element(couponDates.begin(), couponDates.end());
152
153
0
        QL_REQUIRE(d1 >= firstDate && d2 <= lastDate, "Dates out of range of schedule: "
154
0
                       << "date 1: " << d1 << ", date 2: " << d2 << ", first date: "
155
0
                       << firstDate << ", last date: " << lastDate);
156
157
0
        Real yearFractionSum = 0.0;
158
0
        for (Size i = 0; i < couponDates.size() - 1; i++) {
159
0
            Date startReferencePeriod = couponDates[i];
160
0
            Date endReferencePeriod = couponDates[i + 1];
161
0
            if (d1 < endReferencePeriod && d2 > startReferencePeriod) {
162
0
                yearFractionSum +=
163
0
                    yearFractionWithReferenceDates(*this,
164
0
                                                   std::max(d1, startReferencePeriod),
165
0
                                                   std::min(d2, endReferencePeriod),
166
0
                                                   startReferencePeriod,
167
0
                                                   endReferencePeriod);
168
0
            }
169
0
        }
170
0
        return yearFractionSum;
171
0
    }
172
173
174
    Time ActualActual::Old_ISMA_Impl::yearFraction(const Date& d1,
175
                                                   const Date& d2,
176
                                                   const Date& d3,
177
26.0M
                                                   const Date& d4) const {
178
26.0M
        if (d1 == d2)
179
0
            return 0.0;
180
181
26.0M
        if (d1 > d2)
182
0
            return -yearFraction(d2,d1,d3,d4);
183
184
        // when the reference period is not specified, try taking
185
        // it equal to (d1,d2)
186
26.0M
        Date refPeriodStart = (d3 != Date() ? d3 : d1);
187
26.0M
        Date refPeriodEnd = (d4 != Date() ? d4 : d2);
188
189
26.0M
        QL_REQUIRE(refPeriodEnd > refPeriodStart && refPeriodEnd > d1,
190
26.0M
                   "invalid reference period: "
191
26.0M
                   << "date 1: " << d1
192
26.0M
                   << ", date 2: " << d2
193
26.0M
                   << ", reference period start: " << refPeriodStart
194
26.0M
                   << ", reference period end: " << refPeriodEnd);
195
196
        // estimate roughly the length in months of a period
197
26.0M
        auto months = (Integer)std::lround(12 * Real(refPeriodEnd - refPeriodStart) / 365);
198
199
        // for short periods...
200
26.0M
        if (months == 0) {
201
            // ...take the reference period as 1 year from d1
202
6
            refPeriodStart = d1;
203
6
            refPeriodEnd = d1 + 1*Years;
204
6
            months = 12;
205
6
        }
206
207
26.0M
        Time period = Real(months)/12.0;
208
209
26.0M
        if (d2 <= refPeriodEnd) {
210
            // here refPeriodEnd is a future (notional?) payment date
211
26.0M
            if (d1 >= refPeriodStart) {
212
                // here refPeriodStart is the last (maybe notional)
213
                // payment date.
214
                // refPeriodStart <= d1 <= d2 <= refPeriodEnd
215
                // [maybe the equality should be enforced, since
216
                // refPeriodStart < d1 <= d2 < refPeriodEnd
217
                // could give wrong results] ???
218
26.0M
                return period*Real(daysBetween(d1,d2)) /
219
26.0M
                    daysBetween(refPeriodStart,refPeriodEnd);
220
26.0M
            } else {
221
                // here refPeriodStart is the next (maybe notional)
222
                // payment date and refPeriodEnd is the second next
223
                // (maybe notional) payment date.
224
                // d1 < refPeriodStart < refPeriodEnd
225
                // AND d2 <= refPeriodEnd
226
                // this case is long first coupon
227
228
                // the last notional payment date
229
0
                Date previousRef = refPeriodStart - months*Months;
230
231
0
                if (d2 > refPeriodStart)
232
0
                    return yearFraction(d1, refPeriodStart, previousRef,
233
0
                                        refPeriodStart) +
234
0
                        yearFraction(refPeriodStart, d2, refPeriodStart,
235
0
                                     refPeriodEnd);
236
0
                else
237
0
                    return yearFraction(d1,d2,previousRef,refPeriodStart);
238
0
            }
239
26.0M
        } else {
240
            // here refPeriodEnd is the last (notional?) payment date
241
            // d1 < refPeriodEnd < d2 AND refPeriodStart < refPeriodEnd
242
1
            QL_REQUIRE(refPeriodStart<=d1,
243
1
                       "invalid dates: "
244
1
                       "d1 < refPeriodStart < refPeriodEnd < d2");
245
            // now it is: refPeriodStart <= d1 < refPeriodEnd < d2
246
247
            // the part from d1 to refPeriodEnd
248
1
            Time sum = yearFraction(d1, refPeriodEnd,
249
1
                                    refPeriodStart, refPeriodEnd);
250
251
            // the part from refPeriodEnd to d2
252
            // count how many regular periods are in [refPeriodEnd, d2],
253
            // then add the remaining time
254
1
            Integer i=0;
255
1
            Date newRefStart, newRefEnd;
256
1
            for (;;) {
257
0
                newRefStart = refPeriodEnd + (months*i)*Months;
258
0
                newRefEnd = refPeriodEnd + (months*(i+1))*Months;
259
0
                if (d2 < newRefEnd) {
260
0
                    break;
261
0
                } else {
262
0
                    sum += period;
263
0
                    i++;
264
0
                }
265
0
            }
266
1
            sum += yearFraction(newRefStart,d2,newRefStart,newRefEnd);
267
1
            return sum;
268
1
        }
269
26.0M
    }
270
271
272
    Time ActualActual::ISDA_Impl::yearFraction(const Date& d1,
273
                                               const Date& d2,
274
                                               const Date&,
275
99
                                               const Date&) const {
276
99
        if (d1 == d2)
277
0
            return 0.0;
278
279
99
        if (d1 > d2)
280
0
            return -yearFraction(d2,d1,Date(),Date());
281
282
99
        Integer y1 = d1.year(), y2 = d2.year();
283
99
        Real dib1 = (Date::isLeap(y1) ? 366.0 : 365.0),
284
99
             dib2 = (Date::isLeap(y2) ? 366.0 : 365.0);
285
286
99
        Time sum = y2 - y1 - 1;
287
99
        sum += daysBetween(d1, Date(1,January,y1+1))/dib1;
288
99
        sum += daysBetween(Date(1,January,y2),d2)/dib2;
289
99
        return sum;
290
99
    }
291
292
293
    Time ActualActual::AFB_Impl::yearFraction(const Date& d1,
294
                                              const Date& d2,
295
                                              const Date&,
296
101
                                              const Date&) const {
297
101
        if (d1 == d2)
298
0
            return 0.0;
299
300
101
        if (d1 > d2)
301
0
            return -yearFraction(d2,d1,Date(),Date());
302
303
101
        Date newD2=d2, temp=d2;
304
101
        Time sum = 0.0;
305
7.87k
        while (temp > d1) {
306
7.77k
            temp = newD2 - 1*Years;
307
7.77k
            if (temp.dayOfMonth()==28 && temp.month()==2
308
432
                && Date::isLeap(temp.year())) {
309
107
                temp += 1;
310
107
            }
311
7.77k
            if (temp>=d1) {
312
7.67k
                sum += 1.0;
313
7.67k
                newD2 = temp;
314
7.67k
            }
315
7.77k
        }
316
317
101
        Real den = 365.0;
318
319
101
        if (Date::isLeap(newD2.year())) {
320
31
            temp = Date(29, February, newD2.year());
321
31
            if (newD2>temp && d1<=temp)
322
14
                den += 1.0;
323
70
        } else if (Date::isLeap(d1.year())) {
324
23
            temp = Date(29, February, d1.year());
325
23
            if (newD2>temp && d1<=temp)
326
4
                den += 1.0;
327
23
        }
328
329
101
        return sum+daysBetween(d1, newD2)/den;
330
101
    }
331
332
}