Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
equityforwardcurvestripper.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
19#include <ql/instruments/impliedvolatility.hpp>
20#include <ql/instruments/vanillaoption.hpp>
21#include <ql/math/solvers1d/brent.hpp>
22#include <ql/pricingengines/blackformula.hpp>
23#include <ql/processes/blackscholesprocess.hpp>
24#include <ql/termstructures/volatility/equityfx/blackconstantvol.hpp>
25#include <ql/termstructures/yield/flatforward.hpp>
28
29using std::set;
30using std::vector;
31using namespace QuantLib;
32
33namespace {
34
35class PriceError {
36public:
37 PriceError(const VanillaOption& option, SimpleQuote& vol, Real targetValue);
38 Real operator()(Volatility x) const;
39
40private:
41 const VanillaOption& option_;
42 SimpleQuote& vol_;
43 Real targetValue_;
44};
45
46PriceError::PriceError(const VanillaOption& option, SimpleQuote& vol, Real targetValue)
47 : option_(option), vol_(vol), targetValue_(targetValue){};
48
49Real PriceError::operator()(Volatility x) const {
50 vol_.setValue(x);
51 Real npv;
52 // Barone Adesi Whaley fails for very small variance, so wrap in a try/catch
53 try {
54 npv = option_.NPV();
55 } catch (...) {
56 npv = 0.0;
57 }
58 return npv - targetValue_;
59}
60
61} // namespace
62
63namespace QuantExt {
64
65EquityForwardCurveStripper::EquityForwardCurveStripper(const QuantLib::ext::shared_ptr<OptionPriceSurface>& callSurface,
66 const QuantLib::ext::shared_ptr<OptionPriceSurface>& putSurface,
67 Handle<YieldTermStructure>& forecastCurve,
68 Handle<QuantLib::Quote>& equitySpot, Exercise::Type type)
69 : callSurface_(callSurface), putSurface_(putSurface), forecastCurve_(forecastCurve), equitySpot_(equitySpot),
70 type_(type), forwards_(callSurface_->expiries().size()) {
71
72 // the call and put surfaces should have the same expiries/strikes/reference date/day counters, some checks to
73 // ensure this
74 QL_REQUIRE(callSurface_->strikes() == putSurface_->strikes(),
75 "Mismatch between Call and Put strikes in EquityForwardCurveStripper");
76 QL_REQUIRE(callSurface_->expiries() == putSurface_->expiries(),
77 "Mismatch between Call and Put expiries in EquityForwardCurveStripper");
78 QL_REQUIRE(callSurface_->referenceDate() == putSurface_->referenceDate(),
79 "Mismatch between Call and Put reference dates in EquityForwardCurveStripper");
80 QL_REQUIRE(callSurface_->dayCounter() == putSurface_->dayCounter(),
81 "Mismatch between Call and Put day counters in EquityForwardCurveStripper");
82
83 // register with all market data
84 registerWith(callSurface);
85 registerWith(putSurface);
86 registerWith(forecastCurve);
87 registerWith(equitySpot);
88 registerWith(Settings::instance().evaluationDate());
89}
90
92
93 vector<vector<Real> > allStrikes = callSurface_->strikes();
94 forwards_.resize(callSurface_->expiries().size());
95
96 // at each option expiry time we calculate a forward
97 for (Size i = 0; i < expiries().size(); i++) {
98 Date expiry = expiries()[i];
99 // get the relevant strikes at this expiry
100 vector<Real> strikes = allStrikes[i];
101 QL_REQUIRE(strikes.size() > 0, "No strikes for expiry " << expiry);
102
103 // if we only have one strike we just use that to get the forward
104 if (strikes.size() == 1) {
106 continue;
107 }
108
109 // we make a first guess at the forward price
110 // strikes are ordered, lowest to highest, we take the first guess as midpoint of 2 strikes
111 // where (C-P) goes from positive to negative
112 Real forward = strikes.back();
113 for (Size k = 0; k < strikes.size(); k++) {
114 if (callSurface_->price(expiry, strikes[k]) <= putSurface_->price(expiry, strikes[k])) {
115 if (k == 0)
116 forward = strikes.front();
117 else
118 forward = (strikes[k] + strikes[k - 1]) / 2;
119 break;
120 }
121 }
122
123 // call and put surface to be used to find forward - updated for American
124 auto callSurface = callSurface_;
125 auto putSurface = putSurface_;
126
127 Size maxIter = 100;
128 Size j = 0;
129 bool isForward = false;
130 while (!isForward && j < maxIter) {
131
132 if (type_ == Exercise::American) {
133
134 // we take the strike either side of our forward guess, this is sufficient because
135 // we construct new price surfaces but only ask for the price at the forward from these
136 vector<Real> amerStrikes(2);
137 auto it_lower = std::lower_bound(strikes.begin(), strikes.end(), forward);
138 if (it_lower == strikes.end())
139 it_lower = std::prev(it_lower);
140 amerStrikes[1] = *it_lower;
141 amerStrikes[0] = (it_lower == strikes.begin()) ? *it_lower : *std::prev(it_lower);
142
143 // for American options we first get the implied vol from the American premiums
144 // we use these to construct the European prices in order to apply put call parity
145
146 // get date and daycounter from the prices surface
147 Date asof = callSurface_->referenceDate();
148 DayCounter dc = callSurface_->dayCounter();
149 Calendar cal = callSurface_->calendar();
150 Time t = dc.yearFraction(asof, expiry);
151
152 // dividend rate from S_t = S * exp((r - q) * t)
153 Real q = forecastCurve_->zeroRate(t, Continuous) - log(forward / equitySpot_->value()) / t;
154
155 // term structures needed to get implied vol
156 QuantLib::ext::shared_ptr<SimpleQuote> volQuote = QuantLib::ext::make_shared<SimpleQuote>(0.1);
157 Handle<BlackVolTermStructure> volTs(
158 QuantLib::ext::make_shared<BlackConstantVol>(asof, cal, Handle<Quote>(volQuote), dc));
159 Handle<YieldTermStructure> divTs(QuantLib::ext::make_shared<FlatForward>(asof, q, dc));
160
161 // a black scholes process
162 QuantLib::ext::shared_ptr<GeneralizedBlackScholesProcess> gbsp =
163 QuantLib::ext::make_shared<BlackScholesMertonProcess>(equitySpot_, divTs, forecastCurve_, volTs);
164 QuantLib::ext::shared_ptr<PricingEngine> engine =
165 QuantLib::ext::make_shared<QuantExt::BaroneAdesiWhaleyApproximationEngine>(gbsp);
166
167 vector<vector<Volatility> > vols(2, vector<Volatility>(amerStrikes.size()));
168 vector<Option::Type> types;
169 types.push_back(Option::Call);
170 types.push_back(Option::Put);
171
172 for (Size l = 0; l < types.size(); l++) {
173 for (Size k = 0; k < amerStrikes.size(); k++) {
174 // create an american option for current strike/expiry and type
175 QuantLib::ext::shared_ptr<StrikedTypePayoff> payoff(new PlainVanillaPayoff(types[l], amerStrikes[k]));
176 QuantLib::ext::shared_ptr<Exercise> exercise = QuantLib::ext::make_shared<AmericanExercise>(expiry);
177 VanillaOption option(payoff, exercise);
178 option.setPricingEngine(engine);
179
180 // option.setPricingEngine(engine);
181 Real targetPrice = types[l] == Option::Call ? callSurface_->price(expiry, amerStrikes[k])
182 : putSurface_->price(expiry, amerStrikes[k]);
183
184 // calculate the implied volatility using a solver
185 try {
186 PriceError f(option, *volQuote, targetPrice);
187 Brent solver;
188 solver.setMaxEvaluations(100);
189 solver.setLowerBound(0.0001);
190 vols[l][k] = solver.solve(f, 0.0001, 0.2, 0.01);
191 } catch (...) {
192 vols[l][k] = 0.0;
193 }
194 }
195 }
196
197 vector<Real> newStrikes;
198 vector<Date> dates;
199 vector<Real> callPremiums, putPremiums;
200
201 for (Size k = 0; k < amerStrikes.size(); k++) {
202 if (vols[0][k] != 0.0 && vols[1][k] != 0.0) {
203 // get the european option prices for each strike
204 Real call = blackFormula(Option::Call, amerStrikes[k], forward, vols[0][k] * sqrt(t),
205 forecastCurve_->discount(t));
206 Real put = blackFormula(Option::Put, amerStrikes[k], forward, vols[1][k] * sqrt(t),
207 forecastCurve_->discount(t));
208
209 if (call != 0.0 && put != 0.0) {
210 newStrikes.push_back(amerStrikes[k]);
211 dates.push_back(expiry);
212 callPremiums.push_back(call);
213 putPremiums.push_back(put);
214 }
215 }
216 }
217 // throw away any strikes where the vol is zero for either put or call
218 // must have at least one new strike otherwise continue with current price surfaces
219 if (newStrikes.size() > 0) {
220 // build call/put price surfaces with the new European prices
221 callSurface = QuantLib::ext::make_shared<OptionPriceSurface>(asof, dates, newStrikes, callPremiums, dc);
222 putSurface = QuantLib::ext::make_shared<OptionPriceSurface>(asof, dates, newStrikes, putPremiums, dc);
223 }
224 }
225
226 Real newForward = 0.0;
227 // if our guess is below the first strike or after the last strike we just take the relevant strike
228 if (forward <= strikes.front()) {
229 newForward = forwardFromPutCallParity(expiry, strikes.front(), *callSurface, *putSurface);
230 // if forward is still less than first strike we accept this
231 isForward = newForward <= strikes.front();
232 } else if (forward >= strikes.back()) {
233 newForward = forwardFromPutCallParity(expiry, strikes.back(), *callSurface, *putSurface);
234 // if forward is still greater than last strike we accept this
235 isForward = newForward >= strikes.back();
236 } else {
237 newForward = forwardFromPutCallParity(expiry, forward, *callSurface, *putSurface);
238
239 // check - has it moved by less that 0.1%
240 isForward = fabs((newForward - forward) / forward) < 0.001;
241 }
242 forward = newForward;
243 j++;
244 }
245 forwards_[i] = forward;
246 }
247}
248
250 const OptionPriceSurface& putSurface) const {
251 Real C = callSurface.price(d, strike);
252 Real P = putSurface.price(d, strike);
253 Real D = forecastCurve_->discount(d);
254
255 return strike + (C - P) / D;
256}
257
258const vector<Date> EquityForwardCurveStripper::expiries() const {
259 calculate();
260 return callSurface_->expiries();
261}
262
263const vector<Real> EquityForwardCurveStripper::forwards() const {
264 calculate();
265 return forwards_;
266}
267
268} // namespace QuantExt
Barone-Adesi and Whaley approximation engine.
Real targetValue_
Definition: cdsoption.cpp:79
QuantLib::ext::shared_ptr< SimpleQuote > vol_
Definition: cdsoption.cpp:80
QuantLib::Handle< QuantLib::YieldTermStructure > forecastCurve_
const QuantLib::ext::shared_ptr< OptionPriceSurface > callSurface_
const QuantLib::ext::shared_ptr< OptionPriceSurface > putSurface_
QuantLib::Real forwardFromPutCallParity(QuantLib::Date d, QuantLib::Real call, const OptionPriceSurface &callSurface, const OptionPriceSurface &putSurface) const
const std::vector< QuantLib::Date > expiries() const
return the expiries
QuantLib::Handle< QuantLib::Quote > equitySpot_
EquityForwardCurveStripper(const QuantLib::ext::shared_ptr< OptionPriceSurface > &callSurface, const QuantLib::ext::shared_ptr< OptionPriceSurface > &putSurface, QuantLib::Handle< QuantLib::YieldTermStructure > &forecastCurve, QuantLib::Handle< QuantLib::Quote > &equitySpot, QuantLib::Exercise::Type type=QuantLib::Exercise::European)
std::vector< QuantLib::Real > forwards_
store the stripped forward rates
const std::vector< QuantLib::Real > forwards() const
return the stripped forwards
QuantLib::Real price(QuantLib::Time t, QuantLib::Real strike) const
Imply equity forwards from option put/call parity.
const P2_< E1, E2 > P(const E1 &e1, const E2 &e2)
RandomVariable sqrt(RandomVariable x)
CompiledFormula log(CompiledFormula x)
vector< Real > strikes