Coverage Report

Created: 2026-08-14 07:10

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/quantlib/ql/pricingengines/futures/discountingperpetualfuturesengine.cpp
Line
Count
Source
1
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3
/*
4
 Copyright (C) 2025 Hiroto Ogawa
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/math/integrals/trapezoidintegral.hpp>
21
#include <ql/math/interpolations/backwardflatinterpolation.hpp>
22
#include <ql/math/interpolations/cubicinterpolation.hpp>
23
#include <ql/math/interpolations/linearinterpolation.hpp>
24
#include <ql/pricingengines/futures/discountingperpetualfuturesengine.hpp>
25
#include <ql/time/daycounters/yearfractiontodate.hpp>
26
#include <ql/settings.hpp>
27
28
namespace QuantLib {
29
30
    DiscountingPerpetualFuturesEngine::DiscountingPerpetualFuturesEngine(
31
        const Handle<YieldTermStructure>& domesticDiscountCurve,
32
        const Handle<YieldTermStructure>& foreignDiscountCurve,
33
        const Handle<Quote>& assetSpot,
34
        const std::vector<Time>& fundingTimes,
35
        const std::vector<Rate>& fundingRates,
36
        const std::vector<Spread>& interestRateDiffs,
37
        const DiscountingPerpetualFuturesEngine::InterpolationType fundingInterpType,
38
        const Real maxT)
39
0
    : domesticDiscountCurve_(domesticDiscountCurve),
40
0
      foreignDiscountCurve_(foreignDiscountCurve), assetSpot_(assetSpot),
41
0
      fundingTimes_(fundingTimes), fundingRates_(fundingRates),
42
0
      interestRateDiffs_(interestRateDiffs), fundingInterpType_(fundingInterpType), maxT_(maxT) {
43
0
        registerWith(domesticDiscountCurve_);
44
0
        registerWith(foreignDiscountCurve_);
45
0
        registerWith(assetSpot_);
46
0
        QL_REQUIRE(!fundingTimes_.empty(), "fundingTimes is empty");
47
0
        QL_REQUIRE(!fundingRates_.empty(), "fundingRates is empty");
48
0
        QL_REQUIRE(!interestRateDiffs_.empty(), "interestRateDiffs is empty");
49
0
        QL_REQUIRE(fundingTimes_.size() == fundingRates_.size(),
50
0
                   "fundingTimes and fundingRates must have the same size.");
51
0
        QL_REQUIRE(fundingTimes_.size() == interestRateDiffs_.size(),
52
0
                   "fundingTimes and interestRateDiffs must have the same size.");
53
0
    }
54
55
0
    void DiscountingPerpetualFuturesEngine::calculate() const {
56
0
        QL_REQUIRE(!domesticDiscountCurve_.empty(),
57
0
                   "domestic discounting term structure handle is empty");
58
0
        QL_REQUIRE(!foreignDiscountCurve_.empty(),
59
0
                   "foreign discounting term structure handle is empty");
60
0
        QL_REQUIRE(!assetSpot_.empty(), "asset spot handle is empty");
61
62
0
        results_.value = 0.0;
63
0
        results_.errorEstimate = Null<Real>();
64
65
0
        QL_REQUIRE(
66
0
            arguments_.payoffType == PerpetualFutures::Linear ||
67
0
                arguments_.payoffType == PerpetualFutures::Inverse,
68
0
            "Only Linear and Inverse payoffs are supported in DiscountingPerpetualFuturesEngine");
69
70
        // Linear payoff <--> Inverse payoff:
71
        // 1. exchange domestic and foreign curves
72
        // 2. future price: f <--> 1/f
73
0
        auto effDomCurve = arguments_.payoffType == PerpetualFutures::Linear ?
74
0
                               domesticDiscountCurve_ : foreignDiscountCurve_;
75
0
        auto effForCurve = arguments_.payoffType == PerpetualFutures::Linear ?
76
0
                               foreignDiscountCurve_ : domesticDiscountCurve_;
77
78
0
        Period fundingFreq = arguments_.fundingFrequency;
79
0
        Date refDate = Settings::instance().evaluationDate();
80
0
        DayCounter dc = arguments_.dc;
81
0
        Calendar cal = arguments_.cal;
82
        
83
0
        Interpolation fundingRateInterp =
84
0
            DiscountingPerpetualFuturesEngine::selectInterpolation(fundingTimes_, fundingRates_);
85
0
        fundingRateInterp.enableExtrapolation();
86
0
        QL_REQUIRE(fundingRateInterp(fundingRateInterp.xMax()) > 0,
87
0
                   "fundingRate at max time is negative. Because the last funding rate is "
88
0
                   "flatly extrapolated, integral diverges.");
89
0
        Interpolation interestRateDiffInterp =
90
0
            DiscountingPerpetualFuturesEngine::selectInterpolation(fundingTimes_,
91
0
                                                                   interestRateDiffs_);
92
0
        interestRateDiffInterp.enableExtrapolation();
93
94
0
        Real factor = 0.;
95
0
        if (fundingFreq.length() > 0) { 
96
            // discrete-time case
97
0
            std::vector<Real> timeGrid;
98
0
            Real tGrid = 0.;
99
0
            while (tGrid < maxT_) {
100
0
                timeGrid.push_back(tGrid);
101
0
                Real tUnit = 0.;
102
0
                Date date = yearFractionToDate(dc, refDate, tGrid);
103
0
                Real daysInYear = dc.dayCount(Date(1, January, date.year()), Date(1, January, date.year()+1));
104
0
                switch (fundingFreq.units()) { 
105
0
                    case Years:
106
0
                        tGrid += fundingFreq.length();
107
0
                        break;
108
0
                    case Months:
109
0
                        tUnit = 1. / 12.;
110
0
                        tGrid += tUnit * fundingFreq.length();
111
0
                        break;
112
0
                    case Weeks:
113
0
                    case Days:
114
0
                        tGrid = dc.yearFraction(refDate, cal.advance(date, fundingFreq));
115
0
                        break;
116
0
                    case Hours:
117
0
                        tUnit = 1. / daysInYear / 24.;
118
0
                        tGrid += tUnit * fundingFreq.length();
119
0
                        break;
120
0
                    case Minutes:
121
0
                        tUnit = 1. / daysInYear / 24. / 60.;
122
0
                        tGrid += tUnit * fundingFreq.length();
123
0
                        break;
124
0
                    case Seconds:
125
0
                        tUnit = 1. / daysInYear / 24. / 60. / 60.;
126
0
                        tGrid += tUnit * fundingFreq.length();
127
0
                        break;
128
0
                    case Milliseconds:
129
0
                        tUnit = 1. / daysInYear / 24. / 60. / 60. / 1000.;
130
0
                        tGrid += tUnit * fundingFreq.length();
131
0
                        break;
132
0
                    case Microseconds:
133
0
                        tUnit = 1. / daysInYear / 24. / 60. / 60. / 1000. / 1000.;
134
0
                        tGrid += tUnit * fundingFreq.length();
135
0
                        break;
136
0
                    default:
137
0
                        QL_FAIL("Unknown unit in fundingFrequency");
138
0
                }
139
0
            }
140
0
            std::vector<Rate> fundingRateGrid(timeGrid.size());
141
0
            std::vector<Spread> interestRateDiffGrid(timeGrid.size());
142
0
            for (Size i = 0; i < timeGrid.size(); ++i) {
143
0
                Real time = timeGrid[i];
144
0
                fundingRateGrid[i] = fundingRateInterp(time);
145
0
                interestRateDiffGrid[i] = interestRateDiffInterp(time);
146
0
            }
147
            
148
0
            if (arguments_.fundingType == PerpetualFutures::FundingWithCurrentSpot) {
149
0
                Real ratio = 1.;
150
0
                Size i;
151
0
                for (i = 0; i < timeGrid.size() - 1; ++i) {
152
0
                    Real time = timeGrid[i];
153
0
                    Real nextTime = timeGrid[i + 1];
154
0
                    ratio = effForCurve->discount(nextTime) / effForCurve->discount(time)
155
0
                          / effDomCurve->discount(nextTime) * effDomCurve->discount(time);
156
0
                    fundingRateGrid[i] *= ratio;
157
0
                    interestRateDiffGrid[i] *= ratio;
158
0
                }
159
                // for i = timeGrid.size() - 1
160
0
                fundingRateGrid[i] *= ratio;
161
0
                interestRateDiffGrid[i] *= ratio;
162
0
            }
163
0
            auto productIRDiff = [timeGrid, fundingRateGrid](Size i) {
164
0
                Real ret = 1.;
165
0
                for (Size j = 0; j <= i; ++j) {
166
0
                    ret /= 1. + fundingRateGrid[j];
167
0
                }
168
0
                return ret;
169
0
            };
170
0
            Real sum = 0.;
171
0
            std::vector<Real> df_dom, df_for;
172
0
            for (Size i = 0; i < timeGrid.size() - 1; ++i) {
173
0
                Real time = timeGrid[i];
174
0
                sum += productIRDiff(i) * (fundingRateGrid[i] - interestRateDiffGrid[i])
175
0
                       * effForCurve->discount(time) / effDomCurve->discount(time);
176
0
                df_dom.push_back(effDomCurve->discount(time));
177
0
                df_for.push_back(effForCurve->discount(time));
178
0
            }
179
0
            Size iLast = timeGrid.size() - 1;
180
0
            Real timeLast = timeGrid[iLast];
181
0
            Real productIRDiffLast = productIRDiff(iLast);
182
0
            Real fundingRateGridLast = fundingRateGrid[iLast];
183
0
            Real interestRateDiffGridLast = interestRateDiffGrid[iLast];
184
185
0
            Real domRateLast =
186
0
                effDomCurve->forwardRate(timeLast, timeLast, Continuous, NoFrequency).rate();
187
0
            Real forRateLast =
188
0
                effForCurve->forwardRate(timeLast, timeLast, Continuous, NoFrequency).rate();
189
190
            // for t > maxT_, assume flat extrapolation on all rates
191
0
            Real lastTerm = productIRDiffLast
192
0
                             * (fundingRateGridLast - interestRateDiffGridLast)
193
0
                             * effForCurve->discount(timeLast) / effDomCurve->discount(timeLast);
194
0
            Real timeStep = (timeGrid.back() - timeGrid.front()) / (timeGrid.size() - 1);
195
0
            Real ratio =
196
0
                1. / (1. + fundingRateGridLast) * exp(-timeStep * (forRateLast - domRateLast));
197
0
            sum += lastTerm / (1. - ratio);
198
0
            factor = sum;
199
200
0
        } else {
201
            // continuous-time case
202
0
            TrapezoidIntegral<Default> integrator(1.e-6, 30);
203
0
            Real fundingRateXMax = fundingRateInterp.xMax();
204
0
            auto expIRDiff = [&fundingRateInterp, &integrator, fundingRateXMax](Real s) -> Real {
205
0
                if (s < fundingRateXMax) {
206
0
                    return exp(-integrator(fundingRateInterp, 0., s));
207
0
                } else {
208
0
                    return exp(-integrator(fundingRateInterp, 0., fundingRateXMax) -
209
0
                               fundingRateInterp(fundingRateXMax) * (s - fundingRateXMax));
210
0
                }
211
0
            };
212
213
0
            auto timeIntegrand = [fundingRateInterp, interestRateDiffInterp, integrator, expIRDiff,
214
0
                                  effDomCurve, effForCurve](Real s) -> Real {
215
0
                return (fundingRateInterp(s) - interestRateDiffInterp(s)) * expIRDiff(s)
216
0
                       * effForCurve->discount(s) / effDomCurve->discount(s);
217
0
            };
218
0
            factor = integrator(timeIntegrand, 0., maxT_);
219
220
            // for t > maxT_, assume flat extrapolaiton on all rates
221
0
            Real fundingRateLast = fundingRateInterp(maxT_);
222
0
            Real interestRateDiffLast = interestRateDiffInterp(maxT_);
223
0
            Real expIRDiff_last = expIRDiff(maxT_);
224
0
            Real domRateLast =
225
0
                effDomCurve->forwardRate(maxT_, maxT_, Continuous, NoFrequency).rate();
226
0
            Real forRateLast =
227
0
                effForCurve->forwardRate(maxT_, maxT_, Continuous, NoFrequency).rate();
228
0
            Real ratio = fundingRateLast + forRateLast - domRateLast;
229
0
            factor += (fundingRateLast - interestRateDiffLast) * expIRDiff_last *
230
0
                      effForCurve->discount(maxT_) / effDomCurve->discount(maxT_) / ratio;
231
0
        }
232
233
0
        if (arguments_.payoffType == PerpetualFutures::Linear) {
234
0
            results_.value = assetSpot_->value() * factor;
235
0
        } else {
236
0
            results_.value = assetSpot_->value() / factor;
237
0
        }
238
0
    }
239
240
    Interpolation
241
    DiscountingPerpetualFuturesEngine::selectInterpolation(const std::vector<Time>& times,
242
0
                                                           const std::vector<Real>& values) const {
243
0
        Interpolation interpolator;
244
0
        switch (fundingInterpType_) {
245
0
            case Linear:
246
0
                interpolator = LinearInterpolation(times.begin(), times.end(), values.begin());
247
0
                break;
248
0
            case PiecewiseConstant:
249
0
                interpolator =
250
0
                    BackwardFlatInterpolation(times.begin(), times.end(), values.begin());
251
0
                break;
252
0
            case CubicSpline:
253
0
                interpolator = CubicNaturalSpline(times.begin(), times.end(), values.begin());
254
0
                break;
255
0
            default:
256
0
                QL_FAIL("Unknown interpolation type");
257
0
        }
258
0
        return interpolator;
259
0
    }
260
261
}