Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
blackbondoptionengine.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
22
23#include <ql/cashflows/cashflows.hpp>
24#include <ql/exercise.hpp>
25#include <ql/pricingengines/blackformula.hpp>
26#include <ql/quote.hpp>
27#include <ql/termstructures/yield/impliedtermstructure.hpp>
28#include <ql/termstructures/yieldtermstructure.hpp>
29#include <ql/time/calendars/nullcalendar.hpp>
30
31namespace QuantExt {
32
33BlackBondOptionEngine::BlackBondOptionEngine(const Handle<YieldTermStructure>& discountCurve,
34 const Handle<SwaptionVolatilityStructure>& volatility,
35 const Handle<YieldTermStructure>& underlyingReferenceCurve,
36 const Handle<DefaultProbabilityTermStructure>& defaultCurve,
37 const Handle<Quote>& recoveryRate, const Handle<Quote>& securitySpread,
38 Period timestepPeriod)
39 : discountCurve_(discountCurve), volatility_(volatility), underlyingReferenceCurve_(underlyingReferenceCurve),
40 defaultCurve_(defaultCurve), recoveryRate_(recoveryRate), securitySpread_(securitySpread),
41 timestepPeriod_(timestepPeriod) {
42 registerWith(discountCurve_);
43 registerWith(volatility_);
44 registerWith(underlyingReferenceCurve_);
45 registerWith(defaultCurve_);
46 registerWith(recoveryRate_);
47 registerWith(securitySpread_);
48}
49
51
52 QL_REQUIRE(!discountCurve_.empty(), "BlackBondOptionEngine::calculate(): empty discount curve");
53
54 QL_REQUIRE(arguments_.putCallSchedule.size() == 1, "BlackBondOptionEngine: can only handle European options");
55 Date exerciseDate = arguments_.putCallSchedule.front()->date();
56
57 QL_REQUIRE(!underlyingReferenceCurve_.empty(), "BlackBondOptionEngine::calculate(): empty reference curve");
58
59 auto fwdBondEngine = QuantLib::ext::make_shared<DiscountingRiskyBondEngine>(
61 auto bondNpvResults = fwdBondEngine->calculateNpv(exerciseDate, arguments_.underlying->settlementDate(exerciseDate),
62 arguments_.underlying->cashflows());
63
64 for (auto& cfRes : bondNpvResults.cashflowResults) {
65 cfRes.legNumber = 0;
66 cfRes.type = "Underlying_Bond__" + cfRes.type;
67 }
68
69 Real fwdNpv = bondNpvResults.npv;
70
71
72 Real knockOutProbability = defaultCurve_.empty() ? 0.0 : 1.0 - defaultCurve_->survivalProbability(exerciseDate);
73
74 // adjust forward if option does not knock out (option is on the recovery value if bond defaults before expiry)
75 if (!arguments_.knocksOutOnDefault) {
76 fwdNpv = (1.0 - knockOutProbability) * fwdNpv + knockOutProbability *
77 (recoveryRate_.empty() ? 0.0 : recoveryRate_->value()) *
78 arguments_.underlying->notional(exerciseDate);
79 }
80
81 // hard code yield compounding convention to annual
82 Rate fwdYtm = CashFlows::yield(arguments_.underlying->cashflows(), fwdNpv, volatility_->dayCounter(), Compounded,
83 Annual, false, exerciseDate, exerciseDate);
84 InterestRate fwdRate(fwdYtm, volatility_->dayCounter(), Compounded, Annual);
85 Time fwdDur = CashFlows::duration(arguments_.underlying->cashflows(), fwdRate, Duration::Modified, false,
86 exerciseDate, exerciseDate);
87
88 QL_REQUIRE(arguments_.putCallSchedule.size() == 1, "BlackBondOptionEngine: only European bond options allowed");
89
90 // read atm yield vol
91 Real underlyingLength = volatility_->swapLength(exerciseDate, arguments_.underlying->cashflows().back()->date());
92 Volatility yieldVol = volatility_->volatility(exerciseDate, underlyingLength, fwdYtm);
93
94 // compute price vol from yield vol
95 Volatility fwdPriceVol;
96 Real shift = 0.0;
97 if (volatility_->volatilityType() == VolatilityType::Normal)
98 fwdPriceVol = yieldVol * fwdDur;
99 else {
100 if (close_enough(volatility_->shift(exerciseDate, underlyingLength), 0.0)) {
101 QL_REQUIRE(fwdYtm > 0, "BlackBondOptionEngine: input yield vols are lognormal, but yield is not positive ("
102 << fwdYtm << ")");
103 fwdPriceVol = yieldVol * fwdDur * fwdYtm;
104 } else {
105 shift = volatility_->shift(exerciseDate, underlyingLength);
106 QL_REQUIRE(fwdYtm > -shift, "BlackBondOptionEngine: input yield vols are shifted lognormal "
107 << shift << ", but yield (" << fwdYtm << ") is not greater than -shift ("
108 << -shift);
109 fwdPriceVol = yieldVol * fwdDur * (fwdYtm + shift);
110 }
111 }
112
113 QL_REQUIRE(fwdPriceVol >= 0.0, "BlackBondOptionEngine: negative forward price vol ("
114 << fwdPriceVol << "), yieldVol=" << yieldVol << ", fwdDur=" << fwdDur
115 << ", fwdYtm=" << fwdYtm << ", shift="
116 << (volatility_->volatilityType() == VolatilityType::Normal
117 ? 0.0
118 : volatility_->shift(exerciseDate, underlyingLength)));
119
120 // strike could be a price or yield
121 Real cashStrike;
122 if (arguments_.putCallSchedule.front()->isBondPrice()) {
123 // adjust cashCashStrike if given as clean
124 // note that the accruedAmount is on the basis of notional 100, as cleanPrice and dirtyPrice
125 cashStrike = arguments_.putCallSchedule.front()->price().amount();
126 if (arguments_.putCallSchedule.front()->price().type() == QuantLib::Bond::Price::Clean)
127 cashStrike += arguments_.underlying->accruedAmount(exerciseDate) / 100;
128 } else {
129 // for a yield get the strike using npv calculation, yield should always be dirty price
130 // so no adjustment needed
131 InterestRate yield = arguments_.putCallSchedule.front()->yield();
132 cashStrike = CashFlows::npv(arguments_.underlying->cashflows(), yield, false, exerciseDate, exerciseDate);
133 }
134
135 Real optionValue = blackFormula(
136 arguments_.putCallSchedule[0]->type() == Callability::Call ? Option::Call : Option::Put, cashStrike, fwdNpv,
137 fwdPriceVol * std::sqrt(volatility_->timeFromReference(exerciseDate)), discountCurve_->discount(exerciseDate));
138
139 // correct for knock out probability
140 if (arguments_.knocksOutOnDefault && !defaultCurve_.empty())
141 optionValue *= 1.0 - knockOutProbability;
142
143 CashFlowResults optionFlow;
144 optionFlow.payDate = exerciseDate;
145 optionFlow.legNumber = 1;
146 optionFlow.type = "ExpectedOptionPayoff";
147 optionFlow.amount = optionValue / discountCurve_->discount(exerciseDate);
148 optionFlow.discountFactor = discountCurve_->discount(exerciseDate);
149 optionFlow.presentValue = optionValue;
150
151 bondNpvResults.cashflowResults.push_back(optionFlow);
152
153 results_.additionalResults["knockOutProbability"] = knockOutProbability;
154 results_.additionalResults["cashFlowResults"] = bondNpvResults.cashflowResults;
155 results_.additionalResults["CashStrike"] = cashStrike;
156 results_.additionalResults["FwdCashPrice"] = fwdNpv;
157 results_.additionalResults["PriceVol"] = fwdPriceVol;
158 results_.additionalResults["timeToExpiry"] = volatility_->timeFromReference(exerciseDate);
159 results_.additionalResults["optionValue"] = optionValue;
160 results_.additionalResults["yieldVol"] = yieldVol;
161 results_.additionalResults["yieldVolShift"] = shift;
162 results_.additionalResults["fwdDuration"] = fwdDur;
163 results_.additionalResults["fwdYieldToMaturity"] = fwdYtm;
164
165 results_.additionalResults["AccruedAtExercise"] = arguments_.underlying->accruedAmount(exerciseDate)/100;
166 // results_.additionalResults["CleanBondPrice"] = arguments_.underlying->cleanPrice();
167 // results_.additionalResults["DirtyBondPrice"] = arguments_.underlying->dirtyPrice();
168 if (!arguments_.knocksOutOnDefault) {
169 results_.additionalResults["ExpectedBondRecovery"] = knockOutProbability *
170 (recoveryRate_.empty() ? 0.0 : recoveryRate_->value()) *
171 arguments_.underlying->notional(exerciseDate);
172 }
173 results_.value = optionValue;
174}
175
176} // namespace QuantExt
Black bond option engine.
const Instrument::results * results_
Definition: cdsoption.cpp:81
Handle< YieldTermStructure > discountCurve_
BlackBondOptionEngine(const Handle< YieldTermStructure > &discountCurve, const Handle< SwaptionVolatilityStructure > &volatility, const Handle< YieldTermStructure > &underlyingReferenceCurve, const Handle< DefaultProbabilityTermStructure > &defaultCurve=Handle< DefaultProbabilityTermStructure >(), const Handle< Quote > &recoveryRate=Handle< Quote >(), const Handle< Quote > &securitySpread=Handle< Quote >(), Period timestepPeriod=1 *Months)
volatility is the quoted fwd yield volatility, not price vol
Handle< SwaptionVolatilityStructure > volatility_
Handle< DefaultProbabilityTermStructure > defaultCurve_
Handle< YieldTermStructure > underlyingReferenceCurve_
Filter close_enough(const RandomVariable &x, const RandomVariable &y)
Swap::arguments * arguments_