Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
representativeswaption.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2019 Quaternion Risk Management Ltd
3 All rights reserved.
4
5 This file is part of ORE, a free-software/open-source library
6 for transparent pricing and risk analysis - http://opensourcerisk.org
7
8 ORE is free software: you can redistribute it and/or modify it
9 under the terms of the Modified BSD License. You should have received a
10 copy of the license along with this program.
11 The license is also available online at <http://opensourcerisk.org>
12
13 This program is distributed on the basis that it will form a useful
14 contribution to risk analytics and model standardisation, but WITHOUT
15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
17*/
18
20
25
26#include <ql/cashflows/cashflows.hpp>
27#include <ql/cashflows/couponpricer.hpp>
28#include <ql/cashflows/fixedratecoupon.hpp>
29#include <ql/cashflows/iborcoupon.hpp>
30#include <ql/cashflows/simplecashflow.hpp>
31#include <ql/exercise.hpp>
32#include <ql/instruments/makevanillaswap.hpp>
33#include <ql/math/optimization/costfunction.hpp>
34#include <ql/math/optimization/levenbergmarquardt.hpp>
35#include <ql/pricingengines/swap/discountingswapengine.hpp>
36#include <ql/termstructures/yield/flatforward.hpp>
37#include <ql/time/daycounters/actualactual.hpp>
38
39namespace QuantExt {
40
42 const std::vector<Leg>& underlying, const std::vector<bool>& isPayer,
43 const QuantLib::ext::shared_ptr<SwapIndex>& swapIndexBase, const bool useUnderlyingIborIndex,
44 const Handle<YieldTermStructure>& discountCurve, const Real reversion, const Real volatility, const Real flatRate)
45 : underlying_(underlying), isPayer_(isPayer), swapIndexBase_(swapIndexBase),
46 useUnderlyingIborIndex_(useUnderlyingIborIndex), discountCurve_(discountCurve), reversion_(reversion),
47 volatility_(volatility), flatRate_(flatRate) {
48
49 // set up flat curve, if we want that
50 Handle<YieldTermStructure> flatCurve;
51 if (flatRate != Null<Real>()) {
52 flatCurve =
53 Handle<YieldTermStructure>(QuantLib::ext::make_shared<FlatForward>(0, NullCalendar(), flatRate_, ActualActual(ActualActual::ISDA)));
54 }
55
56 // determine last cashflow date of underlying
57 Date maturityDate = Date::minDate();
58 for (auto const& l : underlying)
59 for (auto const& c : l)
60 maturityDate = std::max(c->date(), maturityDate);
61 QL_REQUIRE(maturityDate > discountCurve->referenceDate(), "underlying maturity ("
62 << maturityDate << ") must be gt reference date ("
63 << discountCurve_->referenceDate() << ")");
64
65 // set up model
66 model_ = QuantLib::ext::make_shared<LGM>(QuantLib::ext::make_shared<IrLgm1fPiecewiseConstantHullWhiteAdaptor>(
67 swapIndexBase_->currency(), flatCurve.empty() ? discountCurve_ : flatCurve, Array(), Array(1, volatility_),
68 Array(), Array(1, reversion_)));
69
70 // build underlying leg with its ibor / ois coupons linked to model forward curves
71 QuantLib::ext::shared_ptr<IborIndex> modelIborIndexToUse, iborIndexToUse;
72 for (Size l = 0; l < underlying_.size(); ++l) {
73 for (auto const& c : underlying_[l]) {
74 if (auto i = QuantLib::ext::dynamic_pointer_cast<IborCoupon>(c)) {
75 // standard ibor coupon
76 QL_REQUIRE(!i->isInArrears(), "RepresentativeSwaptionMatcher: can not handle in arrears fixing");
77 QuantLib::ext::shared_ptr<LgmImpliedYtsFwdFwdCorrected> y;
78 auto fc = modelForwardCurves_.find(i->iborIndex()->name());
79 if (fc != modelForwardCurves_.end())
80 y = fc->second;
81 else {
82 y = QuantLib::ext::make_shared<LgmImpliedYtsFwdFwdCorrected>(
83 model_, flatCurve.empty() ? i->iborIndex()->forwardingTermStructure() : flatCurve);
84 modelForwardCurves_[i->iborIndex()->name()] = y;
85 }
86 auto iborIndexLinkedToModelCurve = i->iborIndex()->clone(Handle<YieldTermStructure>(y));
87 auto tmp = QuantLib::ext::make_shared<IborCoupon>(
88 i->date(), i->nominal(), i->accrualStartDate(), i->accrualEndDate(), i->fixingDays(),
89 iborIndexLinkedToModelCurve, i->gearing(), i->spread(), i->referencePeriodStart(),
90 i->referencePeriodEnd(), i->dayCounter(), false);
91 tmp->setPricer(QuantLib::ext::make_shared<BlackIborCouponPricer>());
92 modelLinkedUnderlying_.push_back(tmp);
93 if (modelIborIndexToUse == nullptr) {
94 modelIborIndexToUse = iborIndexLinkedToModelCurve;
95 iborIndexToUse = i->iborIndex();
96 }
97 } else if (auto o = QuantLib::ext::dynamic_pointer_cast<QuantExt::OvernightIndexedCoupon>(c)) {
98 auto onIndex = o->overnightIndex();
99 QL_REQUIRE(onIndex, "internal error: could not cast o->index() to overnightIndex");
100 QuantLib::ext::shared_ptr<LgmImpliedYtsFwdFwdCorrected> y;
101 auto fc = modelForwardCurves_.find(onIndex->name());
102 if (fc != modelForwardCurves_.end())
103 y = fc->second;
104 else {
105 y = QuantLib::ext::make_shared<LgmImpliedYtsFwdFwdCorrected>(
106 model_, flatCurve.empty() ? onIndex->forwardingTermStructure() : flatCurve);
107 modelForwardCurves_[onIndex->name()] = y;
108 }
109 auto onIndexLinkedToModelCurve =
110 QuantLib::ext::dynamic_pointer_cast<OvernightIndex>(onIndex->clone(Handle<YieldTermStructure>(y)));
111 QL_REQUIRE(onIndexLinkedToModelCurve,
112 "internal error: could not cast onIndex->clone() to OvernightIndex");
113 auto tmp = QuantLib::ext::make_shared<QuantExt::OvernightIndexedCoupon>(
114 o->date(), o->nominal(), o->accrualStartDate(), o->accrualEndDate(), onIndexLinkedToModelCurve,
115 o->gearing(), o->spread(), o->referencePeriodStart(), o->referencePeriodEnd(), o->dayCounter(),
116 false, o->includeSpread(), o->lookback(), o->rateCutoff(), o->fixingDays(),
117 o->rateComputationStartDate(), o->rateComputationEndDate());
118 tmp->setPricer(QuantLib::ext::make_shared<OvernightIndexedCouponPricer>());
119 modelLinkedUnderlying_.push_back(tmp);
120 if (modelIborIndexToUse == nullptr) {
121 modelIborIndexToUse = onIndexLinkedToModelCurve;
122 iborIndexToUse = o->overnightIndex();
123 }
124 } else if (auto o = QuantLib::ext::dynamic_pointer_cast<QuantExt::AverageONIndexedCoupon>(c)) {
125 auto onIndex = o->overnightIndex();
126 QL_REQUIRE(onIndex, "internal error: could not cast o->index() to overnightIndex");
127 QuantLib::ext::shared_ptr<LgmImpliedYtsFwdFwdCorrected> y;
128 auto fc = modelForwardCurves_.find(onIndex->name());
129 if (fc != modelForwardCurves_.end())
130 y = fc->second;
131 else {
132 y = QuantLib::ext::make_shared<LgmImpliedYtsFwdFwdCorrected>(
133 model_, flatCurve.empty() ? onIndex->forwardingTermStructure() : flatCurve);
134 modelForwardCurves_[onIndex->name()] = y;
135 }
136 auto onIndexLinkedToModelCurve =
137 QuantLib::ext::dynamic_pointer_cast<OvernightIndex>(onIndex->clone(Handle<YieldTermStructure>(y)));
138 QL_REQUIRE(onIndexLinkedToModelCurve,
139 "internal error: could not cast onIndex->clone() to OvernightIndex");
140 auto tmp = QuantLib::ext::make_shared<QuantExt::AverageONIndexedCoupon>(
141 o->date(), o->nominal(), o->accrualStartDate(), o->accrualEndDate(), onIndexLinkedToModelCurve,
142 o->gearing(), o->spread(), o->rateCutoff(), o->dayCounter(), o->lookback(), o->fixingDays(),
143 o->rateComputationStartDate(), o->rateComputationEndDate());
144 tmp->setPricer(QuantLib::ext::make_shared<AverageONIndexedCouponPricer>());
145 modelLinkedUnderlying_.push_back(tmp);
146 if (modelIborIndexToUse == nullptr) {
147 modelIborIndexToUse = onIndexLinkedToModelCurve;
148 iborIndexToUse = o->overnightIndex();
149 }
150 } else if (QuantLib::ext::dynamic_pointer_cast<FixedRateCoupon>(c) ||
151 QuantLib::ext::dynamic_pointer_cast<SimpleCashFlow>(c)) {
152 // fixed coupon or simple cashflow
153 modelLinkedUnderlying_.push_back(c);
154 } else {
155 QL_FAIL("RepresentativeSwaptionMatcher: unsupported coupon type");
156 }
157 modelLinkedUnderlyingIsPayer_.push_back(isPayer[l]);
158 }
159 }
160
161 // build model linked discounting curve
163 QuantLib::ext::make_shared<LgmImpliedYtsFwdFwdCorrected>(model_, flatCurve.empty() ? discountCurve_ : flatCurve);
164
165 // identify the ibor index to use for the matching
166 if (modelIborIndexToUse == nullptr || !useUnderlyingIborIndex_) {
167 auto fc = modelForwardCurves_.find(swapIndexBase_->iborIndex()->name());
168 if (fc != modelForwardCurves_.end())
169 modelIborIndexToUse = swapIndexBase_->iborIndex()->clone(Handle<YieldTermStructure>(fc->second));
170 else
171 modelIborIndexToUse = swapIndexBase_->iborIndex()->clone(
172 Handle<YieldTermStructure>(QuantLib::ext::make_shared<LgmImpliedYtsFwdFwdCorrected>(
173 model_, flatCurve.empty() ? swapIndexBase_->iborIndex()->forwardingTermStructure() : flatCurve)));
174 iborIndexToUse = swapIndexBase_->iborIndex();
175 }
176
177 // build model linked swap index base
179 QuantLib::ext::dynamic_pointer_cast<LgmImpliedYtsFwdFwdCorrected>(*modelIborIndexToUse->forwardingTermStructure());
181 "internal error: could not cast modelIborIndexToUse->forwardingTermStructure() to "
182 "LgmImpliedYtsFwdFwdCorrected");
183 modelSwapIndexDiscountCurve_ = QuantLib::ext::make_shared<LgmImpliedYtsFwdFwdCorrected>(
184 model_, flatCurve.empty() ? (swapIndexBase_->discountingTermStructure().empty()
185 ? modelIborIndexToUse->forwardingTermStructure()
186 : swapIndexBase_->discountingTermStructure())
187 : flatCurve);
188
189 // create the final swap index base to use, i.e. the one with replaced ibor index, if desired
190 swapIndexBaseFinal_ = QuantLib::ext::make_shared<SwapIndex>(
191 swapIndexBase_->familyName(), swapIndexBase_->tenor(), swapIndexBase_->fixingDays(), swapIndexBase_->currency(),
192 swapIndexBase_->fixingCalendar(), swapIndexBase_->fixedLegTenor(), swapIndexBase_->fixedLegConvention(),
193 swapIndexBase_->dayCounter(), iborIndexToUse, Handle<YieldTermStructure>(modelSwapIndexDiscountCurve_));
194
195 // clone the swap index base using the model fwd and dsc curves and replacing the ibor tenor, if that applies
196 modelSwapIndexBase_ = QuantLib::ext::make_shared<SwapIndex>(
197 swapIndexBase_->familyName(), swapIndexBase_->tenor(), swapIndexBase_->fixingDays(), swapIndexBase_->currency(),
198 swapIndexBase_->fixingCalendar(), swapIndexBase_->fixedLegTenor(), swapIndexBase_->fixedLegConvention(),
199 swapIndexBase_->dayCounter(), modelIborIndexToUse, Handle<YieldTermStructure>(modelSwapIndexDiscountCurve_));
200}
201
202namespace {
203bool includeCashflow(const QuantLib::ext::shared_ptr<CashFlow>& f, const Date& exerciseDate,
205 switch (criterion) {
207 if (auto c = QuantLib::ext::dynamic_pointer_cast<Coupon>(f))
208 return c->accrualStartDate() >= exerciseDate;
210 return f->date() > exerciseDate;
211 default:
212 QL_FAIL("unknown inclusiong criterion");
213 }
214}
215} // namespace
216
217QuantLib::ext::shared_ptr<Swaption> RepresentativeSwaptionMatcher::representativeSwaption(Date exerciseDate,
218 const InclusionCriterion criterion) {
219
220 QL_REQUIRE(exerciseDate > discountCurve_->referenceDate(),
221 "exerciseDate (" << exerciseDate << ") must be greater than reference date ("
222 << discountCurve_->referenceDate() << ")");
223
224 // shift size for derivative computation
225
226 constexpr static Real h = 1.0E-4;
227
228 // build leg containing all coupons with pay date > exerciseDate
229
230 Date today = discountCurve_->referenceDate();
231 Leg effectiveLeg;
232 Real additionalDeterministicNpv = 0.0;
233 std::vector<bool> effectiveIsPayer;
234 for (Size c = 0; c < modelLinkedUnderlying_.size(); ++c) {
235 QuantLib::ext::shared_ptr<CashFlow> cf = modelLinkedUnderlying_[c];
236 if (!includeCashflow(cf, exerciseDate, criterion))
237 continue;
238 if (auto i = QuantLib::ext::dynamic_pointer_cast<IborCoupon>(cf)) {
239 if (today <= i->fixingDate() && i->fixingDate() < exerciseDate) {
240
241 // an ibor coupon with today <= fixing date < exerciseDate is modified such that the fixing date is on
242 // (or due to holiday adjustments shortly after) the exercise date; the nominal is adjusted such that
243 // the effective accrual time remains the same
244 // if on the other hand the fixing date is in the past, the historic fixing is required
245
246 Calendar cal = i->iborIndex()->fixingCalendar();
247 Date newAccrualStart = cal.advance(cal.adjust(exerciseDate), i->fixingDays() * Days);
248 Date newAccrualEnd = std::max(i->accrualEndDate(), newAccrualStart + 1);
249 Real newAccrualTime = i->dayCounter().yearFraction(newAccrualStart, newAccrualEnd);
250 Real oldAccrualTime = i->dayCounter().yearFraction(i->accrualStartDate(), i->accrualEndDate());
251 auto tmp = QuantLib::ext::make_shared<IborCoupon>(
252 i->date(), i->nominal() * oldAccrualTime / newAccrualTime, newAccrualStart, newAccrualEnd,
253 i->fixingDays(), i->iborIndex(), i->gearing(), i->spread(), i->referencePeriodStart(),
254 i->referencePeriodEnd(), i->dayCounter(), false);
255 tmp->setPricer(QuantLib::ext::make_shared<BlackIborCouponPricer>());
256 effectiveLeg.push_back(tmp);
257 effectiveIsPayer.push_back(modelLinkedUnderlyingIsPayer_[c]);
258 } else {
259 // leave the ibor coupon as is
260 effectiveLeg.push_back(cf);
261 effectiveIsPayer.push_back(modelLinkedUnderlyingIsPayer_[c]);
262 }
263 } else if (auto o = QuantLib::ext::dynamic_pointer_cast<OvernightIndexedCoupon>(cf)) {
264
265 if (o->fixingDates().empty())
266 continue;
267
268 // For an OIS coupon, we keep the original coupon, if the first fixing date >= exercise date
269
270 if (o->fixingDates().front() >= exerciseDate) {
271 o->setPricer(QuantLib::ext::make_shared<QuantExt::OvernightIndexedCouponPricer>());
272 effectiveLeg.push_back(o);
273 effectiveIsPayer.push_back(modelLinkedUnderlyingIsPayer_[c]);
274 } else {
275
276 // For an OIS coupon with first exercise date <= exercise date, represent
277 // a) fixing dates < today via a fixed cashflow
278 // b) fixing dates >= today via a float cashflow
279 // For b) keep fixing dates >= exerciseDate only, but at least one fixing date and scale the result
280 // to the full rate period associated to b). Furthermore, the accrual period will be the same as
281 // the rate computation (value dates) period
282
283 Real accrualToRatePeriodRatio = 1.0;
284 if (o->rateComputationStartDate() != Null<Date>() && o->rateComputationEndDate() != Null<Date>()) {
285 accrualToRatePeriodRatio =
286 o->dayCounter().yearFraction(o->accrualStartDate(), o->accrualEndDate()) /
287 o->dayCounter().yearFraction(o->rateComputationStartDate(), o->rateComputationEndDate());
288 }
289 Date firstFixingDate = o->fixingDates().front();
290 if (firstFixingDate < today) {
291 Date firstValueDate = valueDate(firstFixingDate, o);
292 Date lastFixingDateBeforeToday =
293 *std::next(std::lower_bound(o->fixingDates().begin(), o->fixingDates().end(), today), -1);
294 Date lastValueDateBeforeToday = valueDate(lastFixingDateBeforeToday, o);
295 if (lastValueDateBeforeToday > firstValueDate) {
296 auto tmp = QuantLib::ext::make_shared<OvernightIndexedCoupon>(
297 o->date(), o->nominal() * accrualToRatePeriodRatio, firstValueDate,
298 lastValueDateBeforeToday, o->overnightIndex(), o->gearing(), o->spread(),
299 o->referencePeriodStart(), o->referencePeriodEnd(), o->dayCounter(), false,
300 o->includeSpread(), 0 * Days, o->rateCutoff(), o->fixingDays());
301 tmp->setPricer(QuantLib::ext::make_shared<QuantExt::OvernightIndexedCouponPricer>());
302 additionalDeterministicNpv += discountCurve_->discount(tmp->date()) * tmp->amount();
303 }
304 }
305 if (o->fixingDates().back() >= today) {
306 Date firstFixingDateGeqToday =
307 *std::lower_bound(o->fixingDates().begin(), o->fixingDates().end(), today);
308 Date firstValueDateGeqToday = valueDate(firstFixingDateGeqToday, o);
309 Date lastValueDate = valueDate(o->fixingDates().back(), o);
310 Date startDate = o->index()->fixingCalendar().adjust(exerciseDate);
311 Date endDate = std::max(lastValueDate, startDate + 1);
312 Real factor = o->dayCounter().yearFraction(firstValueDateGeqToday, lastValueDate) /
313 o->dayCounter().yearFraction(startDate, endDate);
314 auto tmp = QuantLib::ext::make_shared<OvernightIndexedCoupon>(
315 o->date(), o->nominal() * accrualToRatePeriodRatio * factor, startDate, endDate,
316 o->overnightIndex(), o->gearing(), o->spread(), o->referencePeriodStart(),
317 o->referencePeriodEnd(), o->dayCounter(), false, o->includeSpread(), 0 * Days, o->rateCutoff(),
318 o->fixingDays());
319 tmp->setPricer(QuantLib::ext::make_shared<OvernightIndexedCouponPricer>());
320 effectiveLeg.push_back(tmp);
321 effectiveIsPayer.push_back(modelLinkedUnderlyingIsPayer_[c]);
322 }
323 }
324 } else if (auto o = QuantLib::ext::dynamic_pointer_cast<AverageONIndexedCoupon>(cf)) {
325
326 if (o->fixingDates().empty())
327 continue;
328
329 if (o->fixingDates().front() >= exerciseDate) {
330 o->setPricer(QuantLib::ext::make_shared<QuantExt::AverageONIndexedCouponPricer>());
331 effectiveLeg.push_back(o);
332 effectiveIsPayer.push_back(modelLinkedUnderlyingIsPayer_[c]);
333 } else {
334
335 Real accrualToRatePeriodRatio = 1.0;
336 if (o->rateComputationStartDate() != Null<Date>() && o->rateComputationEndDate() != Null<Date>()) {
337 accrualToRatePeriodRatio =
338 o->dayCounter().yearFraction(o->accrualStartDate(), o->accrualEndDate()) /
339 o->dayCounter().yearFraction(o->rateComputationStartDate(), o->rateComputationEndDate());
340 }
341 Date firstFixingDate = o->fixingDates().front();
342 if (firstFixingDate < today) {
343 Date firstValueDate = valueDate(firstFixingDate, o);
344 Date lastFixingDateBeforeToday =
345 *std::next(std::lower_bound(o->fixingDates().begin(), o->fixingDates().end(), today), -1);
346 Date lastValueDateBeforeToday = valueDate(lastFixingDateBeforeToday, o);
347 if (lastValueDateBeforeToday > firstValueDate) {
348 auto tmp = QuantLib::ext::make_shared<AverageONIndexedCoupon>(
349 o->date(), o->nominal() * accrualToRatePeriodRatio, firstValueDate,
350 lastValueDateBeforeToday, o->overnightIndex(), o->gearing(), o->spread(), o->rateCutoff(),
351 o->dayCounter(), 0 * Days, o->fixingDays());
352 tmp->setPricer(QuantLib::ext::make_shared<QuantExt::AverageONIndexedCouponPricer>());
353 additionalDeterministicNpv += discountCurve_->discount(tmp->date()) * tmp->amount();
354 }
355 }
356 if (o->fixingDates().back() >= today) {
357 Date firstFixingDateGeqToday =
358 *std::lower_bound(o->fixingDates().begin(), o->fixingDates().end(), today);
359 Date firstValueDateGeqToday = valueDate(firstFixingDateGeqToday, o);
360 Date lastValueDate = valueDate(o->fixingDates().back(), o);
361 Date startDate = o->index()->fixingCalendar().adjust(exerciseDate);
362 Date endDate = std::max(lastValueDate, startDate + 1);
363 Real factor = o->dayCounter().yearFraction(firstValueDateGeqToday, lastValueDate) /
364 o->dayCounter().yearFraction(startDate, endDate);
365 auto tmp = QuantLib::ext::make_shared<AverageONIndexedCoupon>(
366 o->date(), o->nominal() * accrualToRatePeriodRatio * factor, startDate, endDate,
367 o->overnightIndex(), o->gearing(), o->spread(), o->rateCutoff(), o->dayCounter(), 0 * Days,
368 o->fixingDays());
369 tmp->setPricer(QuantLib::ext::make_shared<AverageONIndexedCouponPricer>());
370 effectiveLeg.push_back(tmp);
371 effectiveIsPayer.push_back(modelLinkedUnderlyingIsPayer_[c]);
372 }
373 }
374 } else if (QuantLib::ext::dynamic_pointer_cast<FixedRateCoupon>(cf) != nullptr ||
375 QuantLib::ext::dynamic_pointer_cast<SimpleCashFlow>(cf) != nullptr) {
376 effectiveLeg.push_back(cf);
377 effectiveIsPayer.push_back(modelLinkedUnderlyingIsPayer_[c]);
378 } else {
379 QL_FAIL("internal error: coupon type in modelLinkedUnderlying_ not supported in representativeSwaption()");
380 }
381 }
382
383 if (effectiveLeg.empty())
384 return QuantLib::ext::shared_ptr<Swaption>();
385
386 // adjust exercise date to a valid fixing date, otherwise MakeVanillaSwap below may fail
387 exerciseDate = swapIndexBase_->fixingCalendar().adjust(exerciseDate);
388
389 // compute exercise time (the dc of the discount curve defines the date => time mapping by convention)
390 Real t_ex = discountCurve_->timeFromReference(exerciseDate);
391
392 // use T = t_ex - forward measure for all calculations instead of original LGM measure
393 model_->parametrization()->shift() = -model_->parametrization()->H(t_ex);
394
395 // initial guess for strike = nominal weighted fixed rate of coupons
396 Real nominalSum = 0.0, nominalSumAbs = 0.0, strikeGuess = 0.0;
397 Size nCpns = 0;
398 for (Size c = 0; c < modelLinkedUnderlying_.size(); ++c) {
399 if (auto f = QuantLib::ext::dynamic_pointer_cast<FixedRateCoupon>(modelLinkedUnderlying_[c])) {
400 strikeGuess += f->rate() * std::abs(f->nominal());
401 nominalSum += f->nominal() * (modelLinkedUnderlyingIsPayer_[c] ? -1.0 : 1.0);
402 nominalSumAbs += std::abs(f->nominal());
403 nCpns++;
404 }
405 }
406 Real nominalGuess = nominalSum / static_cast<Real>(nCpns);
407 if (close_enough(nominalSumAbs, 0.0))
408 strikeGuess = 0.01; // default guess
409 else
410 strikeGuess /= nominalSumAbs;
411
412 // initial guess for maturity = maturity of last cashflow
413 Real maturityGuess = ActualActual(ActualActual::ISDA).yearFraction(exerciseDate, CashFlows::maturityDate(modelLinkedUnderlying_));
414
415 // target function, input components are nominal, strike, maturity, output rel. error in npv, delta, gamma
416 struct Matcher : public CostFunction {
417 QuantLib::ext::shared_ptr<VanillaSwap> underlyingSwap(const QuantLib::ext::shared_ptr<SwapIndex> swapIndexBase,
418 const Period& maturity) const {
419 // same as in SwapIndex::underlyingSwap() to make sure we are consistent
420 return MakeVanillaSwap(maturity, swapIndexBase->iborIndex(), 0.0)
421 .withEffectiveDate(swapIndexBase->valueDate(exerciseDate))
422 .withFixedLegCalendar(swapIndexBase->fixingCalendar())
423 .withFixedLegDayCount(swapIndexBase->dayCounter())
424 .withFixedLegTenor(swapIndexBase->fixedLegTenor())
425 .withFixedLegConvention(swapIndexBase->fixedLegConvention())
426 .withFixedLegTerminationDateConvention(swapIndexBase->fixedLegConvention())
427 .receiveFixed(true)
428 .withNominal(1.0);
429 }
430 void setState(const Real state) const {
431 for (auto const& c : modelCurves)
432 c->state(state);
433 }
434 std::tuple<Size, Real> periodFromTime(Real t) const {
435 t *= 12.0;
436 Size months = static_cast<Size>(std::floor(t));
437 Real alpha = t - static_cast<Real>(months);
438 return std::make_tuple(months, alpha);
439 }
440 Array values(const Array& x) const override {
441 Real maturityTime = std::min(x[2] * x[2], maxMaturityTime);
442 // bracket continuous maturity between two discrete tenors rounded to whole months
443 // this is essential to provide a smooth target function
444 Size months;
445 Real alpha;
446 std::tie(months, alpha) = periodFromTime(maturityTime);
447 RawResult rawResult;
448 // do we have a cached result?
449 auto res = cachedRawResults.find(months);
450 if (res != cachedRawResults.end()) {
451 rawResult = res->second;
452 } else {
453 Period lowerMaturity = months * Months;
454 Period upperMaturity = lowerMaturity + 1 * Months;
455 // generate candidate underlying and compute npv for states 0,+h,-h with chosen stepsize
456 QuantLib::ext::shared_ptr<VanillaSwap> underlyingLower, underlyingUpper;
457 if (lowerMaturity > 0 * Months)
458 underlyingLower = underlyingSwap(modelSwapIndexBase, lowerMaturity);
459 underlyingUpper = underlyingSwap(modelSwapIndexBase, upperMaturity);
460 if (underlyingLower)
461 underlyingLower->setPricingEngine(engine);
462 underlyingUpper->setPricingEngine(engine);
463 setState(0.0);
464 rawResult.npv0_l = underlyingLower ? underlyingLower->NPV() : 0.0;
465 rawResult.bps0_l = underlyingLower ? underlyingLower->fixedLegBPS() * 1.0E4 : 0.0;
466 rawResult.npv0_u = underlyingUpper->NPV();
467 rawResult.bps0_u = underlyingUpper->fixedLegBPS() * 1.0E4;
468 setState(h);
469 rawResult.npvu_l = underlyingLower ? underlyingLower->NPV() : 0.0;
470 rawResult.bpsu_l = underlyingLower ? underlyingLower->fixedLegBPS() * 1.0E4 : 0.0;
471 rawResult.npvu_u = underlyingUpper->NPV();
472 rawResult.bpsu_u = underlyingUpper->fixedLegBPS() * 1.0E4;
473 setState(-h);
474 rawResult.npvd_l = underlyingLower ? underlyingLower->NPV() : 0.0;
475 rawResult.bpsd_l = underlyingLower ? underlyingLower->fixedLegBPS() * 1.0E4 : 0.0;
476 rawResult.npvd_u = underlyingUpper->NPV();
477 rawResult.bpsd_u = underlyingUpper->fixedLegBPS() * 1.0E4;
478 cachedRawResults[months] = rawResult;
479 }
480 // compute npv of lower and upper underlying
481 Real v0_l = x[0] * (rawResult.npv0_l + rawResult.bps0_l * x[1]);
482 Real v0_u = x[0] * (rawResult.npv0_u + rawResult.bps0_u * x[1]);
483 Real vu_l = x[0] * (rawResult.npvu_l + rawResult.bpsu_l * x[1]);
484 Real vu_u = x[0] * (rawResult.npvu_u + rawResult.bpsu_u * x[1]);
485 Real vd_l = x[0] * (rawResult.npvd_l + rawResult.bpsd_l * x[1]);
486 Real vd_u = x[0] * (rawResult.npvd_u + rawResult.bpsd_u * x[1]);
487 // compute interpolated npv
488 Real v0 = v0_l * (1.0 - alpha) + v0_u * alpha;
489 Real vu = vu_l * (1.0 - alpha) + vu_u * alpha;
490 Real vd = vd_l * (1.0 - alpha) + vd_u * alpha;
491 // compute delta and gamma w.r.t. model state
492 Real delta = (vu - vd) / (2.0 * h);
493 Real gamma = (vu + vd) / (h * h);
494 // return target function
495 Array target{(v0 - npv_target) / delta_target, (delta - delta_target) / delta_target,
496 (gamma - gamma_target) / gamma_target};
497 return target;
498 }
499 // to be provided
500 Real h;
501 Date exerciseDate;
502 Real maxMaturityTime;
503 QuantLib::ext::shared_ptr<SwapIndex> modelSwapIndexBase;
504 QuantLib::ext::shared_ptr<PricingEngine> engine;
505 std::set<QuantLib::ext::shared_ptr<LgmImpliedYtsFwdFwdCorrected>> modelCurves;
506 Real npv_target, delta_target, gamma_target;
507 // internal
508 struct RawResult {
509 double npv0_l, npvu_l, npvd_l, bps0_l, bpsu_l, bpsd_l;
510 double npv0_u, npvu_u, npvd_u, bps0_u, bpsu_u, bpsd_u;
511 };
512 mutable std::map<Size, RawResult> cachedRawResults;
513 };
514
515 // init target function
516 Matcher matcher;
517 matcher.h = h;
518 // limit max maturity time such that we are safe anyway
519 matcher.maxMaturityTime = discountCurve_->dayCounter().yearFraction(exerciseDate, Date::maxDate() - 365);
520 matcher.exerciseDate = exerciseDate;
521 matcher.modelSwapIndexBase = modelSwapIndexBase_;
522 matcher.engine = QuantLib::ext::make_shared<DiscountingSwapEngine>(Handle<YieldTermStructure>(modelDiscountCurve_), false,
523 exerciseDate, exerciseDate);
524 for (auto const& c : modelForwardCurves_)
525 matcher.modelCurves.insert(c.second);
526 matcher.modelCurves.insert(modelDiscountCurve_);
527 matcher.modelCurves.insert(modelSwapIndexForwardCurve_);
528 matcher.modelCurves.insert(modelSwapIndexDiscountCurve_);
529
530 // set reference date in model curves
531 for (auto const& c : matcher.modelCurves)
532 c->referenceDate(exerciseDate);
533
534 // compute exotic underlying npv, delta, gamma and set as target
535 Leg rec, pay;
536 for (Size c = 0; c < effectiveLeg.size(); ++c) {
537 if (effectiveIsPayer[c])
538 pay.push_back(effectiveLeg[c]);
539 else
540 rec.push_back(effectiveLeg[c]);
541 }
542 Swap exotic(pay, rec);
543 exotic.setPricingEngine(matcher.engine);
544 Real v0 = exotic.NPV();
545 matcher.setState(h);
546 Real vu = exotic.NPV();
547 matcher.setState(-h);
548 Real vd = exotic.NPV();
549 matcher.npv_target = v0 + additionalDeterministicNpv;
550 matcher.delta_target = (vu - vd) / (2.0 * h);
551 matcher.gamma_target = (vu + vd) / (h * h);
552
553 // set up optimizer and run it
554 NoConstraint constraint;
555 Array guess{nominalGuess, strikeGuess, std::sqrt(maturityGuess)};
556 Problem problem(matcher, constraint, guess);
557 LevenbergMarquardt opt;
558 EndCriteria ec(1000, 20, 1E-8, 1E-8, 1E-8);
559 opt.minimize(problem, ec);
560
561 // extract result and return it
562 Array x = problem.currentValue();
563 Real nominal = x[0];
564 Real strike = x[1];
565 Size months;
566 Real alpha;
567 std::tie(months, alpha) = matcher.periodFromTime(std::min(x[2] * x[2], matcher.maxMaturityTime));
568 Size nMonths = std::max<Size>(1, (months + static_cast<Size>(std::round(alpha))));
569 Period maturity = nMonths * Months;
570 // rescale notional to adjust for the difference between the calibrated maturity and the actual maturity we set
571 nominal *= x[2] * x[2] * 12.0 / static_cast<Real>(nMonths);
572 QuantLib::ext::shared_ptr<VanillaSwap> underlying =
573 MakeVanillaSwap(maturity, swapIndexBaseFinal_->iborIndex(), strike)
574 .withEffectiveDate(swapIndexBaseFinal_->valueDate(exerciseDate))
575 .withFixedLegCalendar(swapIndexBaseFinal_->fixingCalendar())
576 .withFixedLegDayCount(swapIndexBaseFinal_->dayCounter())
577 .withFixedLegTenor(swapIndexBaseFinal_->fixedLegTenor())
578 .withFixedLegConvention(swapIndexBaseFinal_->fixedLegConvention())
579 .withFixedLegTerminationDateConvention(swapIndexBaseFinal_->fixedLegConvention())
580 .receiveFixed(nominal > 0.0)
581 .withNominal(std::abs(nominal));
582 underlying->setPricingEngine(QuantLib::ext::make_shared<DiscountingSwapEngine>(discountCurve_));
583 return QuantLib::ext::make_shared<Swaption>(underlying, QuantLib::ext::make_shared<EuropeanExercise>(exerciseDate));
584}
585
586QuantLib::Date
587RepresentativeSwaptionMatcher::valueDate(const QuantLib::Date& fixingDate,
588 const QuantLib::ext::shared_ptr<QuantLib::FloatingRateCoupon>& cpn) const {
589 return cpn->index()->fixingCalendar().advance(fixingDate, cpn->fixingDays(), Days, Following);
590}
591} // namespace QuantExt
coupon paying the weighted average of the daily overnight rate
Pricer for average overnight indexed coupons.
QuantLib::ext::shared_ptr< SwapIndex > modelSwapIndexBase_
const Handle< YieldTermStructure > discountCurve_
QuantLib::ext::shared_ptr< Swaption > representativeSwaption(Date exerciseDate, const InclusionCriterion criterion=InclusionCriterion::AccrualStartGeqExercise)
QuantLib::ext::shared_ptr< SwapIndex > swapIndexBaseFinal_
QuantLib::ext::shared_ptr< LgmImpliedYtsFwdFwdCorrected > modelSwapIndexDiscountCurve_
QuantLib::ext::shared_ptr< LgmImpliedYtsFwdFwdCorrected > modelDiscountCurve_
QuantLib::ext::shared_ptr< LGM > model_
std::map< std::string, QuantLib::ext::shared_ptr< LgmImpliedYtsFwdFwdCorrected > > modelForwardCurves_
const QuantLib::ext::shared_ptr< SwapIndex > swapIndexBase_
QuantLib::ext::shared_ptr< LgmImpliedYtsFwdFwdCorrected > modelSwapIndexForwardCurve_
RepresentativeSwaptionMatcher(const std::vector< Leg > &underlying, const std::vector< bool > &isPayer, const QuantLib::ext::shared_ptr< SwapIndex > &standardSwapIndexBase, const bool useUnderlyingIborIndex, const Handle< YieldTermStructure > &discountCurve, const Real reversion, const Real volatility=0.0050, const Real flatRate=Null< Real >())
QuantLib::Date valueDate(const QuantLib::Date &fixingDate, const QuantLib::ext::shared_ptr< QuantLib::FloatingRateCoupon > &cpn) const
adaptor to emulate piecewise constant Hull White parameters
Filter close_enough(const RandomVariable &x, const RandomVariable &y)
coupon paying the compounded daily overnight rate, copy of QL class, added includeSpread flag
representative swaption matcher