Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
carrmadanarbitragecheck.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2020 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/errors.hpp>
22#include <ql/math/comparison.hpp>
23
24#include <numeric>
25#include <sstream>
26
27namespace QuantExt {
28
29CarrMadanMarginalProbability::CarrMadanMarginalProbability(const std::vector<Real>& strikes, const Real forward,
30 const std::vector<Real>& callPrices,
31 const VolatilityType volType, const Real shift)
32 : strikes_(strikes), forward_(forward), callPrices_(callPrices), volType_(volType), shift_(shift) {
33
34 // check input
35
36 QL_REQUIRE(close_enough(shift_, 0.0) || shift_ > 0.0,
37 "CarrMadanMarginalProbability: shift (" << shift_ << ") must be non-negative");
38
39 QL_REQUIRE(strikes_.size() == callPrices_.size(), "CarrMadanMarginalProbability: strikes ("
40 << strikes_.size() << ") inconsistent to callPrices ("
41 << callPrices_.size() << ")");
42
43 QL_REQUIRE(!strikes_.empty(), "CarrMadanMarginalProbability: input moneyness is empty");
44
45 // build sort permutation for strikes
46
47 std::vector<Size> perm(strikes.size());
48 std::iota(perm.begin(), perm.end(), 0);
49 std::sort(perm.begin(), perm.end(), [this](Size a, Size b) { return strikes_[a] < strikes_[b]; });
50
51 // check strikes are different (and increasing for the found permutation)
52
53 for (Size i = 1; i < strikes_.size(); ++i) {
54 QL_REQUIRE(strikes_[perm[i]] > strikes_[perm[i - 1]] && !close_enough(strikes_[perm[i]], strikes_[perm[i - 1]]),
55 "CarrMadanMarginalProbability: duplicate strikes at "
56 << perm[i - 1] << ", " << perm[i] << ": " << strikes[perm[i - 1]] << ", " << strikes[perm[i]]);
57 }
58
59 QL_REQUIRE(volType_ == Normal || strikes_[perm[0]] > -shift_ || close_enough(strikes_[perm[0]], -shift_),
60 "CarrMadanMarginalProbability: all input strikes (" << strikes_[perm[0]] << ") plus shift (" << shift
61 << ") must be positive or zero, got "
62 << strikes_[perm[0]] + shift);
63
64 /* add strike -shift and corresponding call price (= forward + shift), if not already present
65 this is only done for ShiftedLognormal vols, not for Normal */
66 bool minusShiftStrikeAdded = false;
67 if (volType_ == ShiftedLognormal) {
68 if (!close_enough(strikes_[perm[0]], 0.0)) {
69 strikes_.push_back(-shift);
70 callPrices_.push_back(forward_);
71 perm.insert(perm.begin(), strikes_.size() - 1);
72 minusShiftStrikeAdded = true;
73 } else {
74 QL_REQUIRE(close_enough(callPrices_[perm[0]], forward_ + shift_),
75 "CarrMadanMarginalProbability: call price ("
76 << callPrices_.front() << ") for strike -shift (" << -shift_ << ") should match forward ("
77 << forward_ << ") + shift (" << shift_ << ") = " << forward_ + shift_);
78 }
79 }
80
81 // check we have two strikes at least
82
83 QL_REQUIRE(strikes_.size() >= 2,
84 "CarrMadanMarginalProbability: at least two strikes levels required (after adding -shift)");
85
86 // compute Q
87
88 std::vector<Real> Q(strikes_.size() - 1);
89 for (Size i = 1; i < strikes_.size(); ++i) {
90 Q[i - 1] = (callPrices_[perm[i - 1]] - callPrices_[perm[i]]) / (strikes_[perm[i]] - strikes_[perm[i - 1]]);
91 }
92
93 // compute BS
94
95 std::vector<Real> BS(strikes_.size() - 2);
96 for (Size i = 1; i < strikes_.size() - 1; ++i) {
97 BS[i - 1] = callPrices_[perm[i - 1]] -
98 (strikes_[perm[i + 1]] - strikes_[perm[i - 1]]) / (strikes_[perm[i + 1]] - strikes_[perm[i]]) *
99 callPrices_[perm[i]] +
100 (strikes_[perm[i]] - strikes_[perm[i - 1]]) / (strikes_[perm[i + 1]] - strikes_[perm[i]]) *
101 callPrices_[perm[i + 1]];
102 }
103
104 // perform the checks 1, 2 from the paper, and populate the set of arbitrage
105
107 callSpreadArbitrage_.resize(strikes_.size());
108 butterflyArbitrage_.resize(strikes_.size());
109
110 // check 1: Q(i,j) in [0,1]
111
112 for (Size i = 0; i < Q.size(); ++i) {
113 if (Q[i] < -1.0E-10 || Q[i] > 1.0 + 1.0E-10) {
114 callSpreadArbitrage_[perm[i]] = true;
115 callSpreadArbitrage_[perm[i + 1]] = true;
116 smileIsArbitrageFree_ = false;
117 }
118 }
119
120 // check 2: BS(i,j) >= 0
121
122 for (Size i = 0; i < BS.size(); ++i) {
123 if (BS[i] < -1.0E-10) {
124 butterflyArbitrage_[perm[i]] = true;
125 butterflyArbitrage_[perm[i + 1]] = true;
126 butterflyArbitrage_[perm[i + 2]] = true;
127 smileIsArbitrageFree_ = false;
128 }
129 }
130
131 // compute the density q
132
133 q_.resize(strikes_.size());
134 q_[perm[0]] = 1.0 - Q.front();
135 for (Size i = 0; i < Q.size() - 1; ++i) {
136 q_[perm[i + 1]] = Q[i] - Q[i + 1];
137 }
138 q_[perm.back()] = Q.back();
139
140 // remove zero strike again, if not present from the start
141
142 if (minusShiftStrikeAdded) {
143 strikes_.erase(std::next(strikes_.end(), -1));
144 callPrices_.erase(std::next(callPrices_.end(), -1));
145 callSpreadArbitrage_.erase(std::next(callSpreadArbitrage_.begin(), perm.front()));
146 butterflyArbitrage_.erase(std::next(butterflyArbitrage_.begin(), perm.front()));
147 q_.erase(std::next(q_.begin(), perm.front()));
148 }
149}
150
151const std::vector<Real>& CarrMadanMarginalProbability::strikes() const { return strikes_; }
153const std::vector<Real>& CarrMadanMarginalProbability::callPrices() const { return callPrices_; }
156
158
161const std::vector<Real>& CarrMadanMarginalProbability::density() const { return q_; }
162
163template <class CarrMadanMarginalProbabilityClass>
164std::string arbitrageAsString(const CarrMadanMarginalProbabilityClass& cm) {
165 std::ostringstream out;
166 for (Size i = 0; i < cm.strikes().size(); ++i) {
167 Size code = 0;
168 if (cm.callSpreadArbitrage()[i])
169 code += 1;
170 if (cm.butterflyArbitrage()[i])
171 code += 2;
172 out << (code > 0 ? std::to_string(code) : ".");
173 }
174 return out.str();
175}
176
177// template instantiations
178template std::string arbitrageAsString(const CarrMadanMarginalProbability& cm);
180
181CarrMadanSurface::CarrMadanSurface(const std::vector<Real>& times, const std::vector<Real>& moneyness, const Real spot,
182 const std::vector<Real>& forwards, const std::vector<std::vector<Real>>& callPrices)
183 : times_(times), moneyness_(moneyness), spot_(spot), forwards_(forwards), callPrices_(callPrices) {
184
185 // checks
186
187 QL_REQUIRE(times_.size() == callPrices_.size(),
188 "CarrMadanSurface: times size (" << times_.size() << ") does not match callPrices outer vector size ("
189 << callPrices_.size() << ")");
190 QL_REQUIRE(times.size() == forwards_.size(), "CarrMadanSurface: times size (" << times_.size()
191 << ") does not match forwards size ("
192 << forwards_.size() << ")");
193
194 QL_REQUIRE(!times_.empty(), "CarrMadanSurface: times are empty");
195
196 for (Size i = 1; i < times_.size(); ++i) {
197 QL_REQUIRE(times_[i] > times_[i - 1] && !close_enough(times_[i], times_[i - 1]),
198 "CarrMadanSurface: times not increasing at index " << (i - 1) << ", " << i << ": " << times_[i - 1]
199 << ", " << times_[i]);
200 }
201
202 QL_REQUIRE(times_.front() > 0.0 || close_enough(times_.front(), 0.0),
203 "CarrMadanSurface: all input times must be positive or zero, got " << times_.front());
204
205 for (Size i = 0; i < times_.size(); ++i) {
206 QL_REQUIRE(callPrices_[i].size() == moneyness_.size(), "CarrMadanSurface: callPrices at time "
207 << times_[i] << "(" << callPrices_[i].size()
208 << ") should match moneyness size ("
209 << moneyness_.size() << ")");
210 }
211
212 // construct the time slices
213
215 for (Size i = 0; i < times_.size(); ++i) {
216 std::vector<Real> strikes(moneyness_.size());
217 Real f = forwards_[i];
218 std::transform(moneyness_.begin(), moneyness_.end(), strikes.begin(), [&f](Real m) { return m * f; });
221 callSpreadArbitrage_.push_back(timeSlices_.back().callSpreadArbitrage());
222 butterflyArbitrage_.push_back(timeSlices_.back().butterflyArbitrage());
223 }
224
225 // check for calendar arbitrage
226
227 calendarArbitrage_ = std::vector<std::vector<bool>>(times_.size(), std::vector<bool>(moneyness_.size()));
228 for (Size i = 0; i < moneyness_.size(); ++i) {
229 for (Size j = 0; j < times_.size() - 1; ++j) {
230 Real c2 = callPrices_[j + 1][i] * spot_ / forwards_[j + 1];
231 Real c1 = callPrices_[j][i] * spot_ / forwards_[j];
232 bool af = c2 > c1 || close_enough(c1, c2);
233 if (!af) {
234 calendarArbitrage_[j][i] = true;
235 calendarArbitrage_[j + 1][i] = true;
237 }
238 }
239 }
240}
241
242const std::vector<Real>& CarrMadanSurface::times() const { return times_; }
243const std::vector<Real>& CarrMadanSurface::moneyness() const { return moneyness_; }
244Real CarrMadanSurface::spot() const { return spot_; }
245const std::vector<Real>& CarrMadanSurface::forwards() const { return forwards_; }
246const std::vector<std::vector<Real>>& CarrMadanSurface::callPrices() const { return callPrices_; }
247
249const std::vector<std::vector<bool>>& CarrMadanSurface::calendarArbitrage() const { return calendarArbitrage_; }
250
251const std::vector<std::vector<bool>>& CarrMadanSurface::callSpreadArbitrage() const { return callSpreadArbitrage_; }
252const std::vector<std::vector<bool>>& CarrMadanSurface::butterflyArbitrage() const { return butterflyArbitrage_; }
253
254const std::vector<CarrMadanMarginalProbability>& CarrMadanSurface::timeSlices() const { return timeSlices_; }
255
256std::string arbitrageAsString(const CarrMadanSurface& cm) {
257 std::ostringstream out;
258 for (Size j = 0; j < cm.times().size(); ++j) {
259 for (Size i = 0; i < cm.moneyness().size(); ++i) {
260 Size code = 0;
261 if (cm.timeSlices()[j].callSpreadArbitrage()[i])
262 code += 1;
263 if (cm.timeSlices()[j].butterflyArbitrage()[i])
264 code += 2;
265 if (cm.calendarArbitrage()[j][i])
266 code += 4;
267 out << (code > 0 ? std::to_string(code) : ".");
268 }
269 out << "\n";
270 }
271 return out.str();
272}
273
275 const Real forward,
276 const std::vector<Real>& callPrices,
277 const VolatilityType volType,
278 const Real shift)
279 : strikes_(strikes), forward_(forward), callPrices_(callPrices), volType_(volType), shift_(shift) {
280
281 QL_REQUIRE(strikes_.size() == callPrices_.size(), "CarrMadanMarginalProbabilitySafeStrikes: strike size ("
282 << strikes_.size() << ") must match callPrices size ("
283 << callPrices_.size() << ")");
284
285 // handle edge cases (no strikes given, invalid forward given)
286
287 if (strikes_.empty()) {
289 return;
290 }
291
292 if (volType == ShiftedLognormal && forward < -shift && !close_enough(forward, -shift)) {
294 callSpreadArbitrage_ = std::vector<bool>(strikes_.size(), false);
295 butterflyArbitrage_ = std::vector<bool>(strikes_.size(), false);
296 q_ = std::vector<Real>(strikes_.size(), 0.0);
297 return;
298 }
299
300 // identify the strikes that are not valid (i.e. < -shift)
301
302 validStrike_.resize(strikes_.size(), true);
303 for (Size i = 0; i < strikes_.size(); ++i) {
304 if (volType_ == ShiftedLognormal && (strikes_[i] < -shift && !close_enough(strikes[i], -shift))) {
305 validStrike_[i] = false;
306 }
307 }
308
309 // build input for regular CM class
310
311 std::vector<Real> regStrikes;
312 std::vector<Real> regCallPrices;
313
314 for (Size i = 0; i < validStrike_.size(); ++i) {
315 if (validStrike_[i]) {
316 regStrikes.push_back(strikes_[i]);
317 regCallPrices.push_back(callPrices_[i]);
318 }
319 }
320
321 // check if we have at least one strike > -shift
322
323 bool haveNonBoundaryStrike = false;
324 if (volType == Normal) {
325 haveNonBoundaryStrike = true;
326 } else {
327 for (auto const& k : regStrikes) {
328 if (volType_ == ShiftedLognormal && k > -shift && !close_enough(k, -shift)) {
329 haveNonBoundaryStrike = true;
330 }
331 }
332 }
333
334 // if no such strike exists, the result is trivial
335
336 if (!haveNonBoundaryStrike) {
338 callSpreadArbitrage_ = std::vector<bool>(1, false);
339 butterflyArbitrage_ = std::vector<bool>(1, false);
340 q_ = std::vector<Real>(1, 1.0);
341 }
342
343 // otherwise we call the regular CM class on the regular strikes
344
345 CarrMadanMarginalProbability cm(regStrikes, forward_, regCallPrices, volType_, shift_);
346
347 // the results are set for the regular strikes and filled with reasonable values for invalid strike positions
348
350 callSpreadArbitrage_ = std::vector<bool>(strikes_.size(), false);
351 butterflyArbitrage_ = std::vector<bool>(strikes_.size(), false);
352 q_ = std::vector<Real>(strikes_.size(), 0.0);
353
354 for (Size i = 0; i < validStrike_.size(); ++i) {
355 if (validStrike_[i]) {
358 q_[i] = cm.density()[i];
359 }
360 }
361}
362
363const std::vector<Real>& CarrMadanMarginalProbabilitySafeStrikes::strikes() const { return strikes_; }
365const std::vector<Real>& CarrMadanMarginalProbabilitySafeStrikes::callPrices() const { return callPrices_; }
368
370
373}
375 return butterflyArbitrage_;
376}
377const std::vector<Real>& CarrMadanMarginalProbabilitySafeStrikes::density() const { return q_; }
378
379} // namespace QuantExt
arbitrage checks based on Carr, Madan, A note on sufficient conditions for no arbitrage (2005)
const std::vector< bool > & butterflyArbitrage() const
CarrMadanMarginalProbability(const std::vector< Real > &strikes, const Real forward, const std::vector< Real > &callPrices, const VolatilityType volType=ShiftedLognormal, const Real shift=0.0)
const std::vector< Real > & strikes() const
const std::vector< bool > & callSpreadArbitrage() const
const std::vector< Real > & density() const
const std::vector< Real > & callPrices() const
CarrMadanMarginalProbabilitySafeStrikes(const std::vector< Real > &strikes, const Real forward, const std::vector< Real > &callPrices, const VolatilityType volType=ShiftedLognormal, const Real shift=0.0)
const std::vector< std::vector< bool > > & butterflyArbitrage() const
std::vector< CarrMadanMarginalProbability > timeSlices_
const std::vector< Real > & times() const
std::vector< std::vector< bool > > butterflyArbitrage_
std::vector< std::vector< bool > > calendarArbitrage_
const std::vector< std::vector< bool > > & calendarArbitrage() const
std::vector< std::vector< Real > > callPrices_
const std::vector< std::vector< Real > > & callPrices() const
CarrMadanSurface(const std::vector< Real > &times, const std::vector< Real > &moneyness, const Real spot, const std::vector< Real > &forwards, const std::vector< std::vector< Real > > &callPrices)
std::vector< std::vector< bool > > callSpreadArbitrage_
const std::vector< Real > & forwards() const
const std::vector< std::vector< bool > > & callSpreadArbitrage() const
const std::vector< CarrMadanMarginalProbability > & timeSlices() const
const std::vector< Real > & moneyness() const
Filter close_enough(const RandomVariable &x, const RandomVariable &y)
std::string arbitrageAsString(const CarrMadanMarginalProbabilityClass &cm)
vector< Real > strikes