/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 | | } |