Coverage Report

Created: 2025-12-08 06:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/quantlib/ql/experimental/callablebonds/discretizedcallablefixedratebond.cpp
Line
Count
Source
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3
/*
4
 Copyright (C) 2008 Allen Kuo
5
 Copyright (C) 2021, 2022 Ralf Konrad Eckel
6
7
 This file is part of QuantLib, a free-software/open-source library
8
 for financial quantitative analysts and developers - http://quantlib.org/
9
10
 QuantLib is free software: you can redistribute it and/or modify it
11
 under the terms of the QuantLib license.  You should have received a
12
 copy of the license along with this program; if not, please email
13
 <quantlib-dev@lists.sf.net>. The license is also available online at
14
 <https://www.quantlib.org/license.shtml>.
15
16
 This program is distributed in the hope that it will be useful, but WITHOUT
17
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18
 FOR A PARTICULAR PURPOSE.  See the license for more details.
19
*/
20
21
#include <ql/experimental/callablebonds/discretizedcallablefixedratebond.hpp>
22
23
namespace QuantLib {
24
25
    namespace {
26
27
0
        bool withinNextWeek(Time t1, Time t2) {
28
0
            static const Time dt = 1.0 / 52;
29
0
            return t1 <= t2 && t2 <= t1 + dt;
30
0
        }
31
32
    }
33
34
35
    DiscretizedCallableFixedRateBond::DiscretizedCallableFixedRateBond(
36
        const CallableBond::arguments& args, const Handle<YieldTermStructure>& termStructure)
37
0
    : arguments_(args), adjustedCallabilityPrices_(args.callabilityPrices) {
38
39
0
        auto dayCounter = termStructure->dayCounter();
40
0
        auto referenceDate = termStructure->referenceDate();
41
42
0
        redemptionTime_ = dayCounter.yearFraction(referenceDate, args.redemptionDate);
43
44
        /* By default the coupon adjustment should take place in
45
         * DiscretizedCallableFixedRateBond::postAdjustValuesImpl(). */
46
0
        couponAdjustments_ =
47
0
            std::vector<CouponAdjustment>(args.couponDates.size(), CouponAdjustment::post);
48
49
0
        couponTimes_.resize(args.couponDates.size());
50
0
        for (Size i = 0; i < couponTimes_.size(); ++i) {
51
0
            couponTimes_[i] = dayCounter.yearFraction(referenceDate, args.couponDates[i]);
52
0
        }
53
54
0
        callabilityTimes_.resize(args.callabilityDates.size());
55
0
        for (Size i = 0; i < callabilityTimes_.size(); ++i) {
56
0
            const Date callabilityDate = args.callabilityDates[i];
57
0
            Time callabilityTime = dayCounter.yearFraction(referenceDate, args.callabilityDates[i]);
58
59
            // To avoid mispricing, we snap exercise dates to the closest coupon date.
60
0
            for (Size j = 0; j < couponTimes_.size(); j++) {
61
0
                const Time couponTime = couponTimes_[j];
62
0
                const Date couponDate = args.couponDates[j];
63
64
0
                if (withinNextWeek(callabilityTime, couponTime) && callabilityDate < couponDate) {
65
                    // Snap the exercise date.
66
0
                    callabilityTime = couponTime;
67
68
                    /* The order of events must be changed here. In
69
                     * DiscretizedCallableFixedRateBond::postAdjustValuesImpl() the callability is
70
                     * done before adding of the coupon. However from the
71
                     * DiscretizedAsset::rollback(Time to) perspective the coupon must be added
72
                     * before the callability as it is later in time. */
73
0
                    couponAdjustments_[j] = CouponAdjustment::pre;
74
75
                    /* We snapped the callabilityTime so we need to take into account the missing
76
                     * discount factor including any possible spread e.g. set in the OAS
77
                     * calculation. */
78
0
                    auto spread  = arguments_.spread;
79
0
                    auto calcDiscountFactorInclSpread = [&termStructure, spread](Date date) {
80
0
                        auto time = termStructure->timeFromReference(date);
81
0
                        auto zeroRateInclSpread =
82
0
                            termStructure->zeroRate(date, termStructure->dayCounter(), Continuous,
83
0
                                                    NoFrequency) +
84
0
                            spread;
85
0
                        auto df = std::exp(-zeroRateInclSpread * time);
86
0
                        return df;
87
0
                    };
88
89
0
                    auto dfTillCallDate = calcDiscountFactorInclSpread(callabilityDate);
90
0
                    auto dfTillCouponDate = calcDiscountFactorInclSpread(couponDate);
91
0
                    adjustedCallabilityPrices_[i] *= dfTillCallDate / dfTillCouponDate;
92
93
0
                    break;
94
0
                }
95
0
            }
96
97
0
            adjustedCallabilityPrices_[i] *= arguments_.faceAmount / 100.0;
98
0
            callabilityTimes_[i] = callabilityTime;
99
0
        }
100
0
    }
101
102
103
0
    void DiscretizedCallableFixedRateBond::reset(Size size) {
104
0
        values_ = Array(size, arguments_.redemption);
105
0
        adjustValues();
106
0
    }
107
108
109
0
    std::vector<Time> DiscretizedCallableFixedRateBond::mandatoryTimes() const {
110
0
        std::vector<Time> times;
111
0
        Time t;
112
0
        Size i;
113
114
0
        t = redemptionTime_;
115
0
        if (t >= 0.0) {
116
0
            times.push_back(t);
117
0
        }
118
119
0
        for (i = 0; i < couponTimes_.size(); i++) {
120
0
            t = couponTimes_[i];
121
0
            if (t >= 0.0) {
122
0
                times.push_back(t);
123
0
            }
124
0
        }
125
126
0
        for (i = 0; i < callabilityTimes_.size(); i++) {
127
0
            t = callabilityTimes_[i];
128
0
            if (t >= 0.0) {
129
0
                times.push_back(t);
130
0
            }
131
0
        }
132
133
0
        return times;
134
0
    }
135
136
137
0
    void DiscretizedCallableFixedRateBond::preAdjustValuesImpl() {
138
0
        for (Size i = 0; i < couponTimes_.size(); i++) {
139
0
            if (couponAdjustments_[i] == CouponAdjustment::pre) {
140
0
                Time t = couponTimes_[i];
141
0
                if (t >= 0.0 && isOnTime(t)) {
142
0
                    addCoupon(i);
143
0
                }
144
0
            }
145
0
        }
146
0
    }
147
148
149
0
    void DiscretizedCallableFixedRateBond::postAdjustValuesImpl() {
150
0
        for (Size i = 0; i < callabilityTimes_.size(); i++) {
151
0
            Time t = callabilityTimes_[i];
152
0
            if (t >= 0.0 && isOnTime(t)) {
153
0
                applyCallability(i);
154
0
            }
155
0
        }
156
0
        for (Size i = 0; i < couponTimes_.size(); i++) {
157
0
            if (couponAdjustments_[i] == CouponAdjustment::post) {
158
0
                Time t = couponTimes_[i];
159
0
                if (t >= 0.0 && isOnTime(t)) {
160
                    /* Exercise and coupon date matches. */
161
0
                    addCoupon(i);
162
0
                }
163
0
            }
164
0
        }
165
0
    }
166
167
168
0
    void DiscretizedCallableFixedRateBond::applyCallability(Size i) {
169
0
        Size j;
170
0
        switch (arguments_.putCallSchedule[i]->type()) {
171
0
            case Callability::Call:
172
0
                for (j = 0; j < values_.size(); j++) {
173
0
                    values_[j] = std::min(adjustedCallabilityPrices_[i], values_[j]);
174
0
                }
175
0
                break;
176
0
            case Callability::Put:
177
0
                for (j = 0; j < values_.size(); j++) {
178
0
                    values_[j] = std::max(values_[j], adjustedCallabilityPrices_[i]);
179
0
                }
180
0
                break;
181
0
            default:
182
0
                QL_FAIL("unknown callability type");
183
0
        }
184
0
    }
185
186
187
0
    void DiscretizedCallableFixedRateBond::addCoupon(Size i) {
188
0
        values_ += arguments_.couponAmounts[i];
189
0
    }
190
191
}