Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
blackmultilegoptionengine.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2024 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/averagebmacoupon.hpp>
27#include <ql/cashflows/fixedratecoupon.hpp>
28#include <ql/cashflows/iborcoupon.hpp>
29#include <ql/pricingengines/blackformula.hpp>
30
31#include <boost/algorithm/string/join.hpp>
32
33namespace QuantExt {
34
35BlackMultiLegOptionEngineBase::BlackMultiLegOptionEngineBase(const Handle<YieldTermStructure>& discountCurve,
36 const Handle<SwaptionVolatilityStructure>& volatility)
37 : discountCurve_(discountCurve), volatility_(volatility) {}
38
39bool BlackMultiLegOptionEngineBase::instrumentIsHandled(const MultiLegOption& m, std::vector<std::string>& messages) {
40 return instrumentIsHandled(m.legs(), m.payer(), m.currency(), m.exercise(), m.settlementType(),
41 m.settlementMethod(), messages);
42}
43
44bool BlackMultiLegOptionEngineBase::instrumentIsHandled(const std::vector<Leg>& legs, const std::vector<bool>& payer,
45 const std::vector<Currency>& currency,
46 const QuantLib::ext::shared_ptr<Exercise>& exercise,
47 const Settlement::Type& settlementType,
48 const Settlement::Method& settlementMethod,
49 std::vector<std::string>& messages) {
50
51 bool isHandled = true;
52
53 // is there a unique pay currency and all interest rate indices are in this same currency?
54
55 for (Size i = 1; i < currency.size(); ++i) {
56 if (currency[0] != currency[i]) {
57 messages.push_back("NumericLgmMultilegOptionEngine: can only handle single currency underlyings, got " +
58 currency[0].code() + " on leg #1 and " + currency[i].code() + " on leg #" +
59 std::to_string(i + 1));
60 isHandled = false;
61 }
62 }
63
64 for (Size i = 0; i < legs.size(); ++i) {
65 for (Size j = 0; j < legs[i].size(); ++j) {
66 if (auto cpn = QuantLib::ext::dynamic_pointer_cast<FloatingRateCoupon>(legs[i][j])) {
67 if (cpn->index()->currency() != currency[0]) {
68 messages.push_back("NumericLgmMultilegOptionEngine: can only handle indices (" +
69 cpn->index()->name() + ") with same currency as unqiue pay currency (" +
70 currency[0].code());
71 }
72 }
73 }
74 }
75
76 // check coupon types
77
78 for (Size i = 0; i < legs.size(); ++i) {
79 for (Size j = 0; j < legs[i].size(); ++j) {
80 if (auto c = QuantLib::ext::dynamic_pointer_cast<Coupon>(legs[i][j])) {
81 if (!(QuantLib::ext::dynamic_pointer_cast<IborCoupon>(c) || QuantLib::ext::dynamic_pointer_cast<FixedRateCoupon>(c) ||
82 QuantLib::ext::dynamic_pointer_cast<QuantExt::OvernightIndexedCoupon>(c) ||
83 QuantLib::ext::dynamic_pointer_cast<QuantExt::AverageONIndexedCoupon>(c) ||
84 QuantLib::ext::dynamic_pointer_cast<QuantLib::AverageBMACoupon>(c) ||
85 QuantLib::ext::dynamic_pointer_cast<QuantExt::SubPeriodsCoupon1>(c))) {
86 messages.push_back(
87 "BlackMultilegOptionEngine: coupon type not handled, supported coupon types: Fix, "
88 "Ibor, ON comp, ON avg, BMA/SIFMA, subperiod. leg = " +
89 std::to_string(i) + " cf = " + std::to_string(j));
90 isHandled = false;
91 }
92 }
93 }
94 }
95
96 // check exercise type
97
98 isHandled = isHandled && (exercise == nullptr || exercise->type() == Exercise::European);
99
100 return isHandled;
101}
102
104
105 std::vector<std::string> messages;
107 "BlackMultiLegOptionEngineBase::calculate(): instrument is not handled: "
108 << boost::algorithm::join(messages, ", "));
109
110 // handle empty exercise
111
112 if (exercise_ == nullptr) {
113 npv_ = 0.0;
114 for (Size i = 0; i < legs_.size(); ++i) {
115 for (Size j = 0; j < legs_[i].size(); ++j) {
116 npv_ += legs_[i][j]->amount() * discountCurve_->discount(legs_[i][j]->date());
117 }
118 }
120 return;
121 }
122
123 // set exercise date and calculate exercise time on vol day counter
124
125 QL_REQUIRE(exercise_->dates().size() == 1,
126 "BlackMultiLegOptionEngineBase: expected 1 exercise dates, got " << exercise_->dates().size());
127 Date exerciseDate = exercise_->date(0);
128 Real exerciseTime = volatility_->timeFromReference(exerciseDate);
129
130 // filter on future coupons and store the necessary data,
131 // also determine fixed day count (one of the fixed cpn), earliest and latest accrual dates
132
133 struct CfInfo {
134 Real fixedRate = Null<Real>(); // for fixed cpn
135 Real floatingSpread = Null<Real>(); // for float cpn
136 Real floatingGearing = Null<Real>(); // for float cpn
137 Real nominal = Null<Real>(); // for all cpn
138 Real accrual = Null<Real>(); // for all cpn
139 Real amount = Null<Real>(); // for all cf
140 Real discountFactor = Null<Real>(); // for all cf
141 Date payDate = Null<Date>(); // for all cf
142 };
143
144 std::vector<CfInfo> cfInfo;
145
146 DayCounter fixedDayCount;
147 Date earliestAccrualStartDate = Date::maxDate();
148 Date latestAccrualEndDate = Date::minDate();
149
150 Size numFixed = 0, numFloat = 0;
151
152 for (Size i = 0; i < legs_.size(); ++i) {
153 for (Size j = 0; j < legs_[i].size(); ++j) {
154 Real payer = payer_[i] ? -1.0 : 1.0;
155 if (auto c = QuantLib::ext::dynamic_pointer_cast<Coupon>(legs_[i][j])) {
156 auto fixedCoupon = QuantLib::ext::dynamic_pointer_cast<FixedRateCoupon>(c);
157 if (fixedCoupon)
158 fixedDayCount = c->dayCounter();
159 if (c->accrualStartDate() >= exerciseDate) {
160 cfInfo.push_back(CfInfo());
161 earliestAccrualStartDate = std::min(earliestAccrualStartDate, c->accrualStartDate());
162 latestAccrualEndDate = std::max(latestAccrualEndDate, c->accrualEndDate());
163 if (fixedCoupon) {
164 cfInfo.back().fixedRate = fixedCoupon->rate();
165 cfInfo.back().nominal = c->nominal() * payer;
166 ++numFixed;
167 } else if (auto f = QuantLib::ext::dynamic_pointer_cast<FloatingRateCoupon>(c)) {
168 cfInfo.back().floatingSpread = f->spread();
169 cfInfo.back().floatingGearing = f->gearing();
170 cfInfo.back().nominal = f->nominal() * payer;
171 ++numFloat;
172 }
173 cfInfo.back().accrual = c->accrualPeriod();
174 cfInfo.back().discountFactor = discountCurve_->discount(c->date());
175 cfInfo.back().amount = c->amount() * payer;
176 cfInfo.back().payDate = c->date();
177 }
178 } else if (legs_[i][j]->date() > exerciseDate) {
179 cfInfo.push_back(CfInfo());
180 cfInfo.back().discountFactor = discountCurve_->discount(legs_[i][j]->date());
181 cfInfo.back().amount = legs_[i][j]->amount() * payer;
182 cfInfo.back().payDate = legs_[i][j]->date();
183 }
184 }
185 }
186
187 // if there are future floating coupons, but no fixed, we add a dummy fixed with 0 fixed rate to
188 // ensure we can calculate the ATM rate
189 if (numFloat > 0 && numFixed == 0) {
190 for (auto const& c : cfInfo) {
191 if (c.floatingSpread != Null<Real>()) {
192 cfInfo.push_back(CfInfo());
193 cfInfo.back().fixedRate = 0.0;
194 cfInfo.back().nominal = c.nominal * -1; // assume opposite direction
195 cfInfo.back().accrual = c.accrual;
196 cfInfo.back().discountFactor = c.discountFactor;
197 cfInfo.back().amount = 0.0;
198 cfInfo.back().payDate = c.payDate;
199 }
200 }
201 }
202
203 QL_REQUIRE(!fixedDayCount.empty(), "BlackMultiLegOptionEngineBase::calculate(): could not determine fixed day "
204 "counter - it appears no fixed coupons are present in the underlying.");
205 QL_REQUIRE(earliestAccrualStartDate != Date::maxDate() && latestAccrualEndDate != Date::minDate(),
206 "BlackMultiLegOptionEngineBase::calculate(): could not determine earliest / latest accrual start / end "
207 "dates. No coupons seems to be present in the underlying.");
208
209 // add the rebate (if any) as a fixed cashflow to the exercise-into underlying
210
211 if (auto rebatedExercise = QuantLib::ext::dynamic_pointer_cast<QuantExt::RebatedExercise>(exercise_)) {
212 cfInfo.push_back(CfInfo());
213 cfInfo.back().amount = rebatedExercise->rebate(0);
214 cfInfo.back().discountFactor = discountCurve_->discount(rebatedExercise->rebatePaymentDate(0));
215 }
216
217 // determine the pv-weighted fixed rate, the fixed bps, the fixed npv
218
219 Real weightedFixedRateNom = 0.0, weightedFixedRateDenom = 0.0;
220 Real fixedBps = 0.0, fixedNpv = 0.0;
221 for (auto const& c : cfInfo) {
222 if (c.fixedRate != Null<Real>()) {
223 Real w = c.nominal * c.accrual * c.discountFactor;
224 weightedFixedRateNom += c.fixedRate * w;
225 weightedFixedRateDenom += w;
226 fixedBps += w;
227 fixedNpv += c.amount * c.discountFactor;
228 }
229 }
230 Real weightedFixedRate = weightedFixedRateNom / weightedFixedRateDenom;
231
232 // determine the pv-weighted floating margin, the floating bps, the floating npv
233
234 Real weightedFloatingSpreadNom = 0.0, weightedFloatingSpreadDenom = 0.0;
235 Real floatingBps = 0.0, floatingNpv = 0.0;
236 for (auto const& c : cfInfo) {
237 if (c.floatingSpread != Null<Real>()) {
238 Real w = c.nominal * c.accrual * c.discountFactor;
239 weightedFloatingSpreadNom += c.floatingSpread * w;
240 weightedFloatingSpreadDenom += w;
241 floatingBps += w;
242 floatingNpv += c.amount * c.discountFactor;
243 }
244 }
245 Real weightedFloatingSpread = weightedFloatingSpreadNom / weightedFloatingSpreadDenom;
246
247 // determine the simple cf npv
248
249 Real simpleCfNpv = 0.0;
250 for (auto const& c : cfInfo) {
251 if (c.fixedRate == Null<Real>() && c.floatingSpread == Null<Real>()) {
252 simpleCfNpv += c.amount * c.discountFactor;
253 }
254 }
255
256 // determine the fair swap rate
257
258 Real atmForward = -floatingNpv / fixedBps;
259
260 // determine the spread correction
261
262 Real spreadCorrection = weightedFloatingSpread * std::abs(floatingBps / fixedBps);
263
264 Real effectiveAtmForward = atmForward - spreadCorrection;
265 Real effectiveFixedRate = weightedFixedRate - spreadCorrection;
266
267 // incorporate the simple cashflows (including the rebate)
268
269 Real simpleCfCorrection = 0.0;
270 for (auto const& c : cfInfo) {
271 if (c.fixedRate == Null<Real>() && c.floatingSpread == Null<Real>()) {
272 simpleCfCorrection += c.amount * c.discountFactor / fixedBps;
273 }
274 }
275
276 effectiveFixedRate += simpleCfCorrection;
277
278 // determine the annuity
279
280 Real annuity = 0.0;
281 if (settlementType_ == Settlement::Physical ||
282 (settlementType_ == Settlement::Cash && settlementMethod_ == Settlement::CollateralizedCashPrice)) {
283 annuity = std::abs(fixedBps);
284 } else if (settlementType_ == Settlement::Cash && settlementMethod_ == Settlement::ParYieldCurve) {
285 // we assume that the cash settlement date is equal to the swap start date
286 InterestRate sr(effectiveAtmForward, fixedDayCount, Compounded, Annual);
287 for (auto const& c : cfInfo) {
288 if (c.fixedRate != Null<Real>()) {
289 annuity += std::abs(c.nominal) * c.accrual *
290 sr.discountFactor(std::min(earliestAccrualStartDate, c.payDate), c.payDate);
291 }
292 }
293 annuity *= discountCurve_->discount(earliestAccrualStartDate);
294 } else {
295 QL_FAIL("BlackMultiLegOptionEngine: invalid (settlementType, settlementMethod) pair");
296 }
297
298 // determine the swap length
299
300 Real swapLength = volatility_->swapLength(earliestAccrualStartDate, latestAccrualEndDate);
301
302 // swapLength is rounded to whole months. To ensure we can read a variance
303 // and a shift from volatility_ we floor swapLength at 1/12 here therefore.
304 swapLength = std::max(swapLength, 1.0 / 12.0);
305
306 Real variance = volatility_->blackVariance(exerciseDate, swapLength, effectiveFixedRate);
307 Real displacement =
308 volatility_->volatilityType() == ShiftedLognormal ? volatility_->shift(exerciseDate, swapLength) : 0.0;
309
310 Real stdDev = std::sqrt(variance);
311 Real delta = 0.0, vega = 0.0;
312 Option::Type optionType = fixedBps > 0 ? Option::Put : Option::Call;
313
314 if (close_enough(annuity, 0.0)) {
315 npv_ = 0.0;
316 } else if (volatility_->volatilityType() == ShiftedLognormal) {
317 npv_ = blackFormula(optionType, effectiveFixedRate, effectiveAtmForward, stdDev, annuity, displacement);
318 vega = std::sqrt(exerciseTime) *
319 blackFormulaStdDevDerivative(effectiveFixedRate, effectiveAtmForward, stdDev, annuity, displacement);
320 delta = blackFormulaForwardDerivative(optionType, effectiveFixedRate, effectiveAtmForward, stdDev, annuity,
321 displacement);
322 } else {
323 npv_ = bachelierBlackFormula(optionType, effectiveFixedRate, effectiveAtmForward, stdDev, annuity);
324 vega = std::sqrt(exerciseTime) *
325 bachelierBlackFormulaStdDevDerivative(effectiveFixedRate, effectiveAtmForward, stdDev, annuity);
326 delta = bachelierBlackFormulaForwardDerivative(optionType, effectiveFixedRate, effectiveAtmForward, stdDev,
327 annuity);
328 }
329
330 underlyingNpv_ = fixedNpv + floatingNpv + simpleCfNpv;
331
332 additionalResults_["spreadCorrection"] = spreadCorrection;
333 additionalResults_["simpleCashflowCorrection"] = simpleCfCorrection;
334 additionalResults_["strike"] = effectiveFixedRate;
335 additionalResults_["atmForward"] = effectiveAtmForward;
336 additionalResults_["underlyingNPV"] = underlyingNpv_;
337 additionalResults_["annuity"] = annuity;
338 additionalResults_["timeToExpiry"] = exerciseTime;
339 additionalResults_["impliedVolatility"] = Real(stdDev / std::sqrt(exerciseTime));
340 additionalResults_["swapLength"] = swapLength;
341 additionalResults_["stdDev"] = stdDev;
342 additionalResults_["delta"] = delta;
343 additionalResults_["vega"] = vega;
344 additionalResults_["fixedNpv"] = fixedNpv;
345 additionalResults_["fixedBps"] = fixedBps;
346 additionalResults_["weightedFixedRate"] = weightedFixedRate;
347 additionalResults_["floatingNpv"] = floatingNpv;
348 additionalResults_["floatingBps"] = floatingBps;
349 additionalResults_["weightedFloatingSpread"] = weightedFloatingSpread;
350 additionalResults_["simpleCfNpv"] = simpleCfNpv;
351}
352
353BlackMultiLegOptionEngine::BlackMultiLegOptionEngine(const Handle<YieldTermStructure>& discountCurve,
354 const Handle<SwaptionVolatilityStructure>& volatility)
355 : BlackMultiLegOptionEngineBase(discountCurve, volatility) {
356 registerWith(discountCurve_);
357 registerWith(volatility_);
358}
359
361 legs_ = arguments_.legs;
362 payer_ = arguments_.payer;
363 currency_ = arguments_.currency;
364 exercise_ = arguments_.exercise;
365 settlementType_ = arguments_.settlementType;
366 settlementMethod_ = arguments_.settlementMethod;
367
369
370 results_.value = npv_;
371 results_.underlyingNpv = underlyingNpv_;
372 results_.additionalResults = additionalResults_;
373 results_.additionalResults["underlyingNpv"] = underlyingNpv_;
374}
375
377 const Handle<YieldTermStructure>& discountCurve, const Handle<SwaptionVolatilityStructure>& volatility)
378 : BlackMultiLegOptionEngineBase(discountCurve, volatility) {
379 registerWith(discountCurve_);
380 registerWith(volatility_);
381}
382
384 legs_ = arguments_.legs;
385 payer_.resize(arguments_.payer.size());
386 for (Size i = 0; i < arguments_.payer.size(); ++i) {
387 payer_[i] = QuantLib::close_enough(arguments_.payer[i], -1.0);
388 }
389 currency_ = std::vector<Currency>(legs_.size(), arguments_.swap->iborIndex()->currency());
390 exercise_ = arguments_.exercise;
391 settlementType_ = arguments_.settlementType;
392 settlementMethod_ = arguments_.settlementMethod;
393
395
396 results_.value = npv_;
397 results_.additionalResults = additionalResults_;
398 results_.additionalResults["underlyingNpv"] = underlyingNpv_;
399}
400
402 const Handle<YieldTermStructure>& discountCurve, const Handle<SwaptionVolatilityStructure>& volatility)
403 : BlackMultiLegOptionEngineBase(discountCurve, volatility) {
404 registerWith(discountCurve_);
405 registerWith(volatility_);
406}
407
409 legs_ = arguments_.legs;
410 payer_.resize(arguments_.payer.size());
411 for (Size i = 0; i < arguments_.payer.size(); ++i) {
412 payer_[i] = QuantLib::close_enough(arguments_.payer[i], -1.0);
413 }
414 currency_ = std::vector<Currency>(legs_.size(), arguments_.swap->iborIndex()->currency());
415 exercise_ = arguments_.exercise;
416 settlementType_ = arguments_.settlementType;
417 settlementMethod_ = arguments_.settlementMethod;
418
420
421 results_.value = npv_;
422 results_.additionalResults = additionalResults_;
423 results_.additionalResults["underlyingNpv"] = underlyingNpv_;
424}
425
426} // namespace QuantExt
coupon paying the weighted average of the daily overnight rate
Simple Black European swaption engine.
const Instrument::results * results_
Definition: cdsoption.cpp:81
BlackMultiLegOptionEngineBase(const Handle< YieldTermStructure > &discountCurve, const Handle< SwaptionVolatilityStructure > &volatility)
QuantLib::ext::shared_ptr< Exercise > exercise_
std::map< std::string, boost::any > additionalResults_
Handle< SwaptionVolatilityStructure > volatility_
static bool instrumentIsHandled(const MultiLegOption &m, std::vector< std::string > &messages)
BlackMultiLegOptionEngine(const Handle< YieldTermStructure > &discountCurve, const Handle< SwaptionVolatilityStructure > &volatility)
BlackNonstandardSwaptionFromMultilegOptionEngine(const Handle< YieldTermStructure > &discountCurve, const Handle< SwaptionVolatilityStructure > &volatility)
BlackSwaptionFromMultilegOptionEngine(const Handle< YieldTermStructure > &discountCurve, const Handle< SwaptionVolatilityStructure > &volatility)
const std::vector< Leg > & legs() const
const Settlement::Type settlementType() const
const Settlement::Method settlementMethod() const
const QuantLib::ext::shared_ptr< Exercise > exercise() const
const std::vector< Currency > & currency() const
const std::vector< bool > & payer() const
Filter close_enough(const RandomVariable &x, const RandomVariable &y)
RandomVariable variance(const RandomVariable &r)
coupon paying the compounded daily overnight rate, copy of QL class, added includeSpread flag
more flexible version of ql class
Coupon with a number of sub-periods.
Swap::arguments * arguments_