Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
blackindexcdsoptionengine.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2021 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
21#include <ql/exercise.hpp>
22#include <ql/pricingengines/blackformula.hpp>
23#include <ql/pricingengines/credit/isdacdsengine.hpp>
24#include <ql/pricingengines/credit/midpointcdsengine.hpp>
25#include <ql/termstructures/credit/flathazardrate.hpp>
26#include <ql/termstructures/yield/flatforward.hpp>
27#include <ql/time/daycounters/actual360.hpp>
29
30#include <numeric>
31
32using namespace QuantLib;
33using std::string;
34using std::vector;
35
36namespace QuantExt {
37
39 // Calculate option value depending on strike type.
40 if (arguments_.strikeType == CdsOption::Spread)
42 else
44}
45
47
48 const Date& exerciseDate = arguments_.exercise->dates().front();
49 Real exerciseTime = volatility_->timeFromReference(exerciseDate);
50 const auto& cds = *arguments_.swap;
51 const Real& strike = arguments_.strike;
52 results_.additionalResults["strikeSpread"] = strike;
53 Real runningSpread = cds.runningSpread();
54 results_.additionalResults["runningSpread"] = runningSpread;
55
56 DiscountFactor discTradeCollToExercise = discountTradeCollateral_->discount(exerciseDate);
57 DiscountFactor discSwapCurrToExercise = discountSwapCurrency_->discount(exerciseDate);
58 results_.additionalResults["discountToExerciseTradeCollateral"] = discTradeCollToExercise;
59 results_.additionalResults["discountToExerciseSwapCurrency"] = discSwapCurrToExercise;
60
61 // Calculate the risky annuity
62 Real rpv01 = std::abs(cds.couponLegNPV() + cds.accrualRebateNPV()) / (cds.notional() * cds.runningSpread());
63 results_.additionalResults["riskyAnnuity"] = rpv01;
64 QL_REQUIRE(cds.notional() > 0.0 || close_enough(cds.notional(), 0.0),
65 "BlackIndexCdsOptionEngine: notional must not be negative (" << cds.notional() << ")");
66 QL_REQUIRE(rpv01 > 0.0, "BlackIndexCdsOptionEngine: risky annuity must be positive (couponLegNPV="
67 << cds.couponLegNPV() << ", accrualRebateNPV=" << cds.accrualRebateNPV()
68 << ", notional=" << cds.notional() << ", runningSpread=" << cds.runningSpread() << ")");
69
70 Real fairSpread = cds.fairSpreadClean();
71 results_.additionalResults["forwardSpread"] = fairSpread;
72
73 // FEP adjusted forward spread. F^{Adjusted} in O'Kane 2008, Section 11.7. F' in ICE paper (notation is poor).
74 Real Fp = fairSpread + fep * (Settlement::Cash ? discSwapCurrToExercise : discTradeCollToExercise) / rpv01 /
75 discTradeCollToExercise / cds.notional();
76 results_.additionalResults["fepAdjustedForwardSpread"] = Fp;
77
78 // Adjusted strike spread. K' in O'Kane 2008, Section 11.7. K' in ICE paper (notation is poor).
79 Real Kp = close_enough(strike, 0.0)
80 ? 0.0
81 : runningSpread + arguments_.tradeDateNtl / cds.notional() * forwardRiskyAnnuityStrike() *
82 (strike - runningSpread) *
83 (Settlement::Cash ? discSwapCurrToExercise : discTradeCollToExercise) / rpv01;
84 results_.additionalResults["adjustedStrikeSpread"] = Kp;
85
86 // Read the volatility from the volatility surface
87 Real volatility = volatility_->volatility(exerciseDate, QuantExt::periodToTime(arguments_.indexTerm), strike,
89 Real stdDev = volatility * std::sqrt(exerciseTime);
90 results_.additionalResults["volatility"] = volatility;
91 results_.additionalResults["standardDeviation"] = stdDev;
92
93 // Option type
94 Option::Type callPut = cds.side() == Protection::Buyer ? Option::Call : Option::Put;
95 results_.additionalResults["callPut"] = callPut == Option::Call ? string("Call") : string("Put");
96
97 // NPV. Add the relevant notionals to the additional results also.
98 results_.additionalResults["valuationDateNotional"] = cds.notional();
99 results_.additionalResults["tradeDateNotional"] = arguments_.tradeDateNtl;
100
101 // Check the forward before plugging it into the black formula
102 QL_REQUIRE(Fp > 0.0 || close_enough(stdDev, 0.0),
103 "BlackIndexCdsOptionEngine: FEP adjusted forward spread ("
104 << Fp << ") is not positive, can not calculate a reasonable option price");
105 // The strike spread might get negative through the adjustment above, but economically the strike is
106 // floored at 0.0, so we ensure this here. This lets us compute the black formula as well in all cases.
107 Kp = std::max(Kp, 0.0);
108
109 results_.value = discTradeCollToExercise / (Settlement::Cash ? discSwapCurrToExercise : discTradeCollToExercise) *
110 rpv01 * cds.notional() * blackFormula(callPut, Kp, Fp, stdDev, 1.0);
111}
112
114
115 // Underlying index CDS.
116 const auto& cds = *arguments_.swap;
117
118 // Add some additional entries to additional results.
119 results_.additionalResults["strikePrice"] = arguments_.strike;
120
121 const Real& tradeDateNtl = arguments_.tradeDateNtl;
122 results_.additionalResults["valuationDateNotional"] = cds.notional();
123 results_.additionalResults["tradeDateNotional"] = tradeDateNtl;
124
125 // effective strike (strike is expressed w.r.t. trade date notional by market convention)
126 Real effStrike = 1.0 - tradeDateNtl / cds.notional() * (1.0 - arguments_.strike);
127 results_.additionalResults["strikePriceDefaultAdjusted"] = effStrike;
128
129 // Discount factor to exercise
130 const Date& exerciseDate = arguments_.exercise->dates().front();
131 Real exerciseTime = volatility_->timeFromReference(exerciseDate);
132 DiscountFactor discTradeCollToExercise = discountTradeCollateral_->discount(exerciseDate);
133 DiscountFactor discSwapCurrToExercise = discountSwapCurrency_->discount(exerciseDate);
134 results_.additionalResults["discountToExerciseTradeCollateral"] = discTradeCollToExercise;
135 results_.additionalResults["discountToExerciseSwapCurrency"] = discSwapCurrToExercise;
136
137 // NPV from buyer's perspective gives upfront, as of valuation date, with correct sign.
138 Real npv = cds.side() == Protection::Buyer ? cds.NPV() : -cds.NPV();
139 results_.additionalResults["upfront"] =
140 npv * (arguments_.settlementType == Settlement::Cash ? discTradeCollToExercise / discSwapCurrToExercise : 1.0);
141
142 Real forwardPrice =
143 1 - npv / cds.notional() / (Settlement::Cash ? discSwapCurrToExercise : discTradeCollToExercise);
144 results_.additionalResults["forwardPrice"] = forwardPrice;
145
146 // Front end protection adjusted forward price.
147 Real Fp = forwardPrice - fep / cds.notional() / discTradeCollToExercise;
148 results_.additionalResults["fepAdjustedForwardPrice"] = Fp;
149
150 // Read the volatility from the volatility surface
151 Real volatility = volatility_->volatility(exerciseDate, QuantExt::periodToTime(arguments_.indexTerm), effStrike,
153 Real stdDev = volatility * std::sqrt(exerciseTime);
154 results_.additionalResults["volatility"] = volatility;
155 results_.additionalResults["standardDeviation"] = stdDev;
156
157 // If protection buyer, put on price.
158 Option::Type cp = cds.side() == Protection::Buyer ? Option::Put : Option::Call;
159 results_.additionalResults["callPut"] = cp == Option::Put ? string("Put") : string("Call");
160
161 // Check the inputs to the black formula before applying it
162 QL_REQUIRE(Fp > 0.0 || close_enough(stdDev, 0.0),
163 "BlackIndexCdsOptionEngine: FEP adjusted forward price ("
164 << Fp << ") is not positive, can not calculate a reasonable option price");
165 QL_REQUIRE(effStrike > 0 || close_enough(effStrike, 0.0),
166 "BlackIndexCdsOptionEngine: Effective Strike price ("
167 << effStrike << ") is not positive, can not calculate a reasonable option price");
168
169 results_.value = cds.notional() * blackFormula(cp, effStrike, Fp, stdDev, discTradeCollToExercise);
170}
171
173
174 // Underlying index CDS.
175 const auto& cds = *arguments_.swap;
176
177 // This method returns RPV01(0; t_e, T, K) / SP(t_e; K). This is the quantity in formula 11.9 of O'Kane 2008.
178 // There is a slight modification in that we divide by the survival probability to t_E using the flat curve at
179 // the strike spread that we create here.
180
181 // Standard index CDS schedule.
182 Schedule schedule = MakeSchedule()
183 .from(cds.protectionStartDate())
184 .to(cds.maturity())
185 .withCalendar(WeekendsOnly())
186 .withFrequency(Quarterly)
187 .withConvention(Following)
188 .withTerminationDateConvention(Unadjusted)
189 .withRule(DateGeneration::CDS2015);
190
191 // Derive hazard rate curve from a single forward starting CDS matching the characteristics of underlying index
192 // CDS with a running spread equal to the strike.
193 const Real& strike = arguments_.strike;
194 Real accuracy = 1e-8;
195
196 auto strikeCds = QuantLib::ext::make_shared<CreditDefaultSwap>(
197 Protection::Buyer, 1 / accuracy, strike, schedule, Following, Actual360(), cds.settlesAccrual(),
198 cds.protectionPaymentTime(), cds.protectionStartDate(), QuantLib::ext::shared_ptr<Claim>(), Actual360(true),
199 true, cds.tradeDate(), cds.cashSettlementDays());
200 // dummy engine
201 strikeCds->setPricingEngine(QuantLib::ext::make_shared<MidPointCdsEngine>(
202 Handle<DefaultProbabilityTermStructure>(
203 QuantLib::ext::make_shared<FlatHazardRate>(0, NullCalendar(), 0.0, Actual365Fixed())),
204 0.0, Handle<YieldTermStructure>(QuantLib::ext::make_shared<FlatForward>(0, NullCalendar(), 0.0, Actual365Fixed()))));
205
206 Real hazardRate;
207 try {
208 hazardRate =
209 strikeCds->impliedHazardRate(0.0, discountSwapCurrency_, Actual365Fixed(), indexRecovery_, accuracy);
210 } catch (const std::exception& e) {
211 QL_FAIL("can not imply fair hazard rate for CDS at option strike "
212 << strike << ". Is the strike correct? Exception: " << e.what());
213 }
214
215 Handle<DefaultProbabilityTermStructure> dph(
216 QuantLib::ext::make_shared<FlatHazardRate>(discountSwapCurrency_->referenceDate(), hazardRate, Actual365Fixed()));
217
218 // Calculate the forward risky strike annuity.
219 strikeCds->setPricingEngine(
220 QuantLib::ext::make_shared<QuantExt::MidPointCdsEngine>(dph, indexRecovery_, discountSwapCurrency_));
221 Real rpv01_K = std::abs(strikeCds->couponLegNPV() + strikeCds->accrualRebateNPV()) /
222 (strikeCds->notional() * strikeCds->runningSpread());
223 results_.additionalResults["riskyAnnuityStrike"] = rpv01_K;
224 QL_REQUIRE(rpv01_K > 0.0, "BlackIndexCdsOptionEngine: strike based risky annuity must be positive.");
225
226 // Survival to exercise
227 const Date& exerciseDate = arguments_.exercise->dates().front();
228 Probability spToExercise = dph->survivalProbability(exerciseDate);
229 Real discToExercise = discountSwapCurrency_->discount(exerciseDate);
230 results_.additionalResults["strikeBasedSurvivalToExercise"] = spToExercise;
231
232 // Forward risky annuity strike (divides out the survival probability and discount to exercise)
233 Real rpv01_K_fwd = rpv01_K / spToExercise / discToExercise;
234 results_.additionalResults["forwardRiskyAnnuityStrike"] = rpv01_K_fwd;
235
236 return rpv01_K_fwd;
237}
238
239} // namespace QuantExt
Black index credit default swap option engine.
const Instrument::results * results_
Definition: cdsoption.cpp:81
void spreadStrikeCalculate(QuantLib::Real fep) const
void doCalc() const override
Engine specific calculation.
void priceStrikeCalculate(QuantLib::Real fep) const
QuantLib::Handle< QuantLib::YieldTermStructure > discountTradeCollateral_
QuantLib::Real fep() const
Calculate the discounted value of the front end protection.
const QuantLib::Handle< QuantExt::CreditVolCurve > volatility() const
QuantLib::Real indexRecovery_
Assumed index recovery used in the flat strike spread curve calculation if provided.
QuantLib::Handle< QuantLib::YieldTermStructure > discountSwapCurrency_
QuantLib::Handle< QuantExt::CreditVolCurve > volatility_
Filter close_enough(const RandomVariable &x, const RandomVariable &y)
Real periodToTime(const Period &p)
Definition: time.cpp:37
Swap::arguments * arguments_
time related utilities.