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