Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
analyticcashsettledeuropeanengine.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
19#include "toplevelfixture.hpp"
20// clang-format off
21#include <boost/test/unit_test.hpp>
22#include <boost/test/data/test_case.hpp>
23// clang-format on
24#include <ql/currencies/america.hpp>
25#include <ql/math/interpolations/linearinterpolation.hpp>
26#include <ql/pricingengines/vanilla/analyticeuropeanengine.hpp>
27#include <ql/quotes/simplequote.hpp>
28#include <ql/termstructures/volatility/equityfx/blackconstantvol.hpp>
29#include <ql/termstructures/yield/flatforward.hpp>
30#include <ql/time/calendars/nullcalendar.hpp>
34
35using namespace boost::unit_test_framework;
36using namespace QuantLib;
37using namespace QuantExt;
38using std::fixed;
39using std::map;
40using std::setprecision;
41using std::string;
42using std::vector;
43
45
46namespace {
47
48// Create a flat yield term structure where DF(0, t) = exp (-r * t)
49Handle<YieldTermStructure> flatYts(Rate r) {
50 return Handle<YieldTermStructure>(QuantLib::ext::make_shared<FlatForward>(0, NullCalendar(), r, Actual365Fixed()));
51}
52
53// Create a process for the tests
54QuantLib::ext::shared_ptr<GeneralizedBlackScholesProcess> getProcess(Rate spot, Volatility vol, Rate r, Rate q) {
55
56 // Set up the term structures
57 Handle<Quote> spotQuote(QuantLib::ext::make_shared<SimpleQuote>(spot));
58 Handle<YieldTermStructure> rTs = flatYts(r);
59 Handle<YieldTermStructure> qTs = flatYts(q);
60 Handle<BlackVolTermStructure> volTs(QuantLib::ext::make_shared<BlackConstantVol>(0, NullCalendar(), vol, Actual365Fixed()));
61
62 // Return the process
63 return QuantLib::ext::make_shared<GeneralizedBlackScholesProcess>(spotQuote, qTs, rTs, volTs);
64}
65
66// Create a dummy price term structure
67Handle<PriceTermStructure> priceTs() {
68 vector<Period> tenors{ 0 * Days, 1 * Years };
69 vector<Real> prices{ 60.0, 69.0 };
70 return Handle<PriceTermStructure>(
71 QuantLib::ext::make_shared<InterpolatedPriceCurve<Linear> >(tenors, prices, Actual365Fixed(), USDCurrency()));
72}
73
74// Return a map containing all of the CashSettledEuropeanOption results
75map<string, Real> results(const CashSettledEuropeanOption& option) {
76
77 map<string, Real> mp;
78
79 mp["npv"] = option.NPV();
80 try {
81 // might not be set by engine
82 mp["delta"] = option.delta();
83 mp["deltaForward"] = option.deltaForward();
84 mp["elasticity"] = option.elasticity();
85 mp["gamma"] = option.gamma();
86 mp["theta"] = option.theta();
87 mp["thetaPerDay"] = option.thetaPerDay();
88 mp["vega"] = option.vega();
89 mp["rho"] = option.rho();
90 mp["dividendRho"] = option.dividendRho();
91 } catch (...) {
92 }
93
94 return mp;
95}
96
97// Check option values on or after expiry date
98void checkOptionValues(CashSettledEuropeanOption& option, Rate r, Real exercisePrice, Real tolerance = 1e-12) {
99
100 // Discount factor from payment and time to payment date.
101 auto yts = flatYts(r);
102 DiscountFactor df_tp = yts->discount(option.paymentDate());
103 Time tp = yts->timeFromReference(option.paymentDate());
104 BOOST_TEST_MESSAGE("Discount factor from payment is: " << fixed << setprecision(12) << df_tp);
105
106 // Value at exercise
107 Real valueAtExpiry = (*option.payoff())(exercisePrice);
108
109 // Check the results after manual exercise.
110 map<string, Real> cashSettledResults = results(option);
111 BOOST_REQUIRE(cashSettledResults.count("npv") == 1);
112 for (const auto& kv : cashSettledResults) {
113 BOOST_TEST_CONTEXT("With result " << kv.first) {
114 BOOST_TEST_MESSAGE("Value for " << kv.first << " with cash settlement is: " << fixed << setprecision(12)
115 << kv.second);
116 if (kv.first == "npv") {
117 BOOST_CHECK_SMALL(kv.second - df_tp * valueAtExpiry, tolerance);
118 } else if (kv.first == "rho") {
119 BOOST_CHECK_SMALL(kv.second + tp * cashSettledResults.at("npv"), tolerance);
120 } else if (kv.first == "theta") {
121 if (tp > 0.0 && !close(tp, 0.0)) {
122 BOOST_CHECK_SMALL(kv.second + std::log(df_tp) / tp * cashSettledResults.at("npv"), tolerance);
123 } else {
124 BOOST_CHECK(close(kv.second, 0.0));
125 }
126 } else if (kv.first == "thetaPerDay") {
127 if (cashSettledResults.count("theta") == 0) {
128 BOOST_ERROR("Expected results to contain a value for theta");
129 continue;
130 } else {
131 BOOST_CHECK_SMALL(kv.second - cashSettledResults.at("theta") / 365.0, tolerance);
132 }
133 } else {
134 BOOST_CHECK(close(kv.second, 0.0));
135 }
136 }
137 }
138}
139
140} // namespace
141
142BOOST_FIXTURE_TEST_SUITE(QuantExtTestSuite, qle::test::TopLevelFixture)
143
144BOOST_AUTO_TEST_SUITE(AnalyticCashSettledEuropeanEngineTest)
145
146vector<Real> strikes{ 55, 58, 62, 65 };
147vector<Option::Type> optionTypes{ Option::Type::Call, Option::Type::Put };
148
149BOOST_DATA_TEST_CASE(testOptionBeforeExpiry, bdata::make(strikes) * bdata::make(optionTypes), strike, optionType) {
150
151 BOOST_TEST_MESSAGE("Testing cash settled option pricing before expiry...");
152
153 Settings::instance().evaluationDate() = Date(3, Jun, 2020);
154
155 // Create cash settled option instrument
156 Date expiry(3, Sep, 2020);
157 Date payment(7, Sep, 2020);
158 bool automaticExercise = false;
159 CashSettledEuropeanOption option(optionType, strike, expiry, payment, automaticExercise);
160
161 // Create engine that accounts for cash settlement.
162 Rate spot = 60.00;
163 Volatility vol = 0.30;
164 Rate r = 0.02;
165 Rate q = 0.01;
166 QuantLib::ext::shared_ptr<PricingEngine> engine =
167 QuantLib::ext::make_shared<AnalyticCashSettledEuropeanEngine>(getProcess(spot, vol, r, q));
168
169 // Value the option accounting for cash settlement and store all results
170 option.setPricingEngine(engine);
171 map<string, Real> cashSettledResults = results(option);
172
173 // Value the option ignoring cash settlement
174 engine = QuantLib::ext::make_shared<AnalyticEuropeanEngine>(getProcess(spot, vol, r, q));
175 option.setPricingEngine(engine);
176 map<string, Real> theoreticalResults = results(option);
177
178 // Discount factor from payment to expiry.
179 auto yts = flatYts(r);
180 DiscountFactor df_te_tp = yts->discount(payment) / yts->discount(expiry);
181 BOOST_TEST_MESSAGE("Discount factor from payment to expiry is: " << fixed << setprecision(12) << df_te_tp);
182
183 // Check the results
184 Real tolerance = 1e-12;
185 BOOST_REQUIRE_EQUAL(cashSettledResults.size(), theoreticalResults.size());
186 BOOST_REQUIRE(cashSettledResults.count("npv") == 1);
187 BOOST_REQUIRE(theoreticalResults.count("npv") == 1);
188 for (const auto& kv : cashSettledResults) {
189
190 BOOST_TEST_CONTEXT("With result " << kv.first) {
191
192 auto it = theoreticalResults.find(kv.first);
193 BOOST_CHECK(it != theoreticalResults.end());
194
195 // If there is no matching result in theoreticalResults, skip to next result.
196 if (it == theoreticalResults.end())
197 continue;
198
199 // Check the result is as expected by comparing to the results that do not account for delayed payment.
200 Real theorResult = it->second;
201 BOOST_TEST_MESSAGE("Value for " << kv.first << " with cash settlement is: " << fixed << setprecision(12)
202 << kv.second);
203 BOOST_TEST_MESSAGE("Value for " << kv.first << " ignoring cash settlement is: " << fixed << setprecision(12)
204 << theorResult);
205
206 // Most results should be of the form cashSettledResult = DF(t_e, t_p) * theoreticalResult.
207 // There are some exceptions dealt with below.
208 if (close(theorResult, 0.0)) {
209 BOOST_CHECK(close(kv.second, 0.0));
210 } else if (kv.first == "elasticity" || kv.first == "itmCashProbability") {
211 BOOST_CHECK(close(kv.second, theorResult));
212 } else if (kv.first == "rho") {
213 Time delta_te_tp = yts->timeFromReference(payment) - yts->timeFromReference(expiry);
214 Real expRho = df_te_tp * (theorResult - delta_te_tp * theoreticalResults.at("npv"));
215 BOOST_TEST_MESSAGE("Value for expected rho is: " << fixed << setprecision(12) << expRho);
216 BOOST_CHECK(close(kv.second, expRho));
217 } else {
218 BOOST_CHECK_SMALL(kv.second / theorResult - df_te_tp, tolerance);
219 }
220 }
221 }
222}
223
224BOOST_DATA_TEST_CASE(testOptionManualExerciseAfterExpiry, bdata::make(strikes) * bdata::make(optionTypes), strike,
225 optionType) {
226
227 BOOST_TEST_MESSAGE("Testing cash settled manual exercise option pricing after expiry...");
228
229 Settings::instance().evaluationDate() = Date(4, Sep, 2020);
230
231 // Create cash settled option instrument
232 Date expiry(3, Sep, 2020);
233 Date payment(7, Sep, 2020);
234 bool automaticExercise = false;
235 CashSettledEuropeanOption option(optionType, strike, expiry, payment, automaticExercise);
236
237 // Create engine that accounts for cash settlement.
238 Rate spot = 60.00;
239 Volatility vol = 0.30;
240 Rate r = 0.02;
241 Rate q = 0.01;
242 QuantLib::ext::shared_ptr<PricingEngine> engine =
243 QuantLib::ext::make_shared<AnalyticCashSettledEuropeanEngine>(getProcess(spot, vol, r, q));
244
245 // Value the option accounting for cash settlement and store all results
246 option.setPricingEngine(engine);
247 map<string, Real> cashSettledResults = results(option);
248
249 // Option has not been manually exercised so all results should be zero.
250 for (const auto& kv : cashSettledResults) {
251 BOOST_TEST_CONTEXT("With result " << kv.first) { BOOST_CHECK(close(kv.second, 0.0)); }
252 }
253
254 // Manually exercise the option with an expiry value of 59.
255 Real exercisePrice = 59.00;
256 option.exercise(exercisePrice);
257
258 // Check the updated results
259 checkOptionValues(option, r, exercisePrice);
260}
261
262// Values for includeReferenceDateEvents in tests.
263vector<bool> irdes{ true, false };
264
265BOOST_DATA_TEST_CASE(testOptionManualExerciseOnExpiry,
266 bdata::make(strikes) * bdata::make(optionTypes) * bdata::make(irdes), strike, optionType, irde) {
267
268 BOOST_TEST_MESSAGE("Testing cash settled manual exercise option on expiry date...");
269
270 // Should work for either setting of includeReferenceDateEvents.
271 Settings::instance().includeReferenceDateEvents() = irde;
272
273 // Create cash settled option instrument
274 Date expiry(3, Sep, 2020);
275 Settings::instance().evaluationDate() = expiry;
276 Date payment(7, Sep, 2020);
277 bool automaticExercise = false;
278 CashSettledEuropeanOption option(optionType, strike, expiry, payment, automaticExercise);
279
280 // Create engine that accounts for cash settlement.
281 Rate spot = 60.00;
282 Volatility vol = 0.30;
283 Rate r = 0.02;
284 Rate q = 0.01;
285 QuantLib::ext::shared_ptr<PricingEngine> engine =
286 QuantLib::ext::make_shared<AnalyticCashSettledEuropeanEngine>(getProcess(spot, vol, r, q));
287
288 // Set the pricing engine
289 option.setPricingEngine(engine);
290
291 // We are on the expiry date but have not exercised the option. Expect the valuation to proceed and hence
292 // the value to be based off the market spot price.
293 checkOptionValues(option, r, spot);
294
295 // Manually exercise the option with an expiry value of 59.
296 Real exercisePrice = 59.00;
297 option.exercise(exercisePrice);
298
299 // Check the updated option values.
300 checkOptionValues(option, r, exercisePrice);
301}
302
303BOOST_DATA_TEST_CASE(testOptionManualExerciseOnPayment, bdata::make(strikes) * bdata::make(optionTypes), strike,
304 optionType) {
305
306 BOOST_TEST_MESSAGE("Testing cash settled manual exercise option on payment date...");
307
308 // Create cash settled option instrument
309 Date expiry(3, Sep, 2020);
310 Date payment(7, Sep, 2020);
311 Settings::instance().evaluationDate() = payment;
312 bool automaticExercise = false;
313 CashSettledEuropeanOption option(optionType, strike, expiry, payment, automaticExercise);
314
315 // Create engine that accounts for cash settlement.
316 Rate spot = 60.00;
317 Volatility vol = 0.30;
318 Rate r = 0.02;
319 Rate q = 0.01;
320 QuantLib::ext::shared_ptr<PricingEngine> engine =
321 QuantLib::ext::make_shared<AnalyticCashSettledEuropeanEngine>(getProcess(spot, vol, r, q));
322
323 // Set the pricing engine
324 option.setPricingEngine(engine);
325
326 // Manually exercise the option with an expiry value of 59.
327 Real exercisePrice = 59.00;
328 option.exercise(exercisePrice);
329
330 // Set include reference date events to true so that the option is not considered expired.
331 Settings::instance().includeReferenceDateEvents() = true;
332
333 // Check the option values against expected values.
334 checkOptionValues(option, r, exercisePrice);
335
336 // Set include reference date events to false so that the option is considered expired.
337 // Must recalculate for the setting to take effect.
338 Settings::instance().includeReferenceDateEvents() = false;
339 option.recalculate();
340
341 // Check that all the values are zero, i.e. the expired state.
342 map<string, Real> cashSettledResults = results(option);
343 for (const auto& kv : cashSettledResults) {
344 BOOST_TEST_CONTEXT("With result " << kv.first) { BOOST_CHECK(close(kv.second, 0.0)); }
345 }
346}
347
348BOOST_DATA_TEST_CASE(testOptionAutomaticExerciseAfterExpiry, bdata::make(strikes) * bdata::make(optionTypes), strike,
349 optionType) {
350
351 BOOST_TEST_MESSAGE("Testing cash settled automatic exercise option pricing after expiry...");
352
353 Settings::instance().evaluationDate() = Date(4, Sep, 2020);
354
355 // Create index to be used in option.
356 Date expiry(3, Sep, 2020);
357 NullCalendar fixingCalendar;
358 QuantLib::ext::shared_ptr<Index> index =
359 QuantLib::ext::make_shared<CommodityFuturesIndex>("TEST", expiry, fixingCalendar, priceTs());
360
361 // Add the expiry date fixing for the index.
362 Real exercisePrice = 59.00;
363 index->addFixing(expiry, exercisePrice);
364
365 // Create cash settled option instrument
366 Date payment(7, Sep, 2020);
367 bool automaticExercise = true;
368 CashSettledEuropeanOption option(optionType, strike, expiry, payment, automaticExercise, index);
369
370 // Create engine that accounts for cash settlement.
371 Rate spot = 60.00;
372 Volatility vol = 0.30;
373 Rate r = 0.02;
374 Rate q = 0.01;
375 QuantLib::ext::shared_ptr<PricingEngine> engine =
376 QuantLib::ext::make_shared<AnalyticCashSettledEuropeanEngine>(getProcess(spot, vol, r, q));
377
378 // Set the pricing engine
379 option.setPricingEngine(engine);
380
381 // Check the option values against expected values.
382 checkOptionValues(option, r, exercisePrice);
383}
384
385BOOST_DATA_TEST_CASE(testOptionAutomaticExerciseOnExpiry,
386 bdata::make(strikes) * bdata::make(optionTypes) * bdata::make(irdes), strike, optionType, irde) {
387
388 BOOST_TEST_MESSAGE("Testing cash settled automatic exercise option pricing on expiry...");
389
390 // Should work for either setting of includeReferenceDateEvents.
391 Settings::instance().includeReferenceDateEvents() = irde;
392
393 // Create index to be used in option.
394 Date expiry(3, Sep, 2020);
395 Settings::instance().evaluationDate() = expiry;
396 NullCalendar fixingCalendar;
397 Handle<PriceTermStructure> pts = priceTs();
398 QuantLib::ext::shared_ptr<Index> index = QuantLib::ext::make_shared<CommodityFuturesIndex>("TEST", expiry, fixingCalendar, pts);
399
400 // Create cash settled option instrument
401 Date payment(7, Sep, 2020);
402 bool automaticExercise = true;
403 CashSettledEuropeanOption option(optionType, strike, expiry, payment, automaticExercise, index);
404
405 // Create engine that accounts for cash settlement.
406 Rate spot = 60.00;
407 Volatility vol = 0.30;
408 Rate r = 0.02;
409 Rate q = 0.01;
410 QuantLib::ext::shared_ptr<PricingEngine> engine =
411 QuantLib::ext::make_shared<AnalyticCashSettledEuropeanEngine>(getProcess(spot, vol, r, q));
412
413 // Set the pricing engine
414 option.setPricingEngine(engine);
415
416 // We have not added a fixing for the index so it will be projected of the price term structure above to
417 // give the payoff. So, we use that value here in our check initially.
418 Real ptsPrice = pts->price(0.0);
419 checkOptionValues(option, r, ptsPrice);
420
421 // Add an expiry date fixing for the index.
422 Real exercisePrice = 59.00;
423 index->addFixing(expiry, exercisePrice);
424
425 // Check the updated values
426 checkOptionValues(option, r, exercisePrice);
427}
428
429BOOST_DATA_TEST_CASE(testOptionAutomaticExerciseOnPayment, bdata::make(strikes) * bdata::make(optionTypes), strike,
430 optionType) {
431
432 BOOST_TEST_MESSAGE("Testing cash settled automatic exercise option pricing on payment date...");
433
434 // Create index to be used in option.
435 Date expiry(3, Sep, 2020);
436 NullCalendar fixingCalendar;
437 QuantLib::ext::shared_ptr<Index> index =
438 QuantLib::ext::make_shared<CommodityFuturesIndex>("TEST", expiry, fixingCalendar, priceTs());
439
440 // Add the expiry date fixing for the index.
441 Real exercisePrice = 59.00;
442 index->addFixing(expiry, exercisePrice);
443
444 // Create cash settled option instrument
445 Date payment(7, Sep, 2020);
446 Settings::instance().evaluationDate() = payment;
447 bool automaticExercise = true;
448 CashSettledEuropeanOption option(optionType, strike, expiry, payment, automaticExercise, index);
449
450 // Create engine that accounts for cash settlement.
451 Rate spot = 60.00;
452 Volatility vol = 0.30;
453 Rate r = 0.02;
454 Rate q = 0.01;
455 QuantLib::ext::shared_ptr<PricingEngine> engine =
456 QuantLib::ext::make_shared<AnalyticCashSettledEuropeanEngine>(getProcess(spot, vol, r, q));
457
458 // Set the pricing engine
459 option.setPricingEngine(engine);
460
461 // Set include reference date events to true so that the option is not considered expired.
462 Settings::instance().includeReferenceDateEvents() = true;
463
464 // Check the option values against expected values.
465 checkOptionValues(option, r, exercisePrice);
466
467 // Set include reference date events to false so that the option is considered expired.
468 // Must recalculate for the setting to take effect.
469 Settings::instance().includeReferenceDateEvents() = false;
470 option.recalculate();
471
472 // Check that all the values are zero, i.e. the expired state.
473 map<string, Real> cashSettledResults = results(option);
474 for (const auto& kv : cashSettledResults) {
475 BOOST_TEST_CONTEXT("With result " << kv.first) { BOOST_CHECK(close(kv.second, 0.0)); }
476 }
477}
478
479BOOST_AUTO_TEST_SUITE_END()
480
481BOOST_AUTO_TEST_SUITE_END()
pricing engine for cash settled European vanilla options.
const QuantLib::Date & paymentDate() const
void exercise(QuantLib::Real priceAtExercise)
Mark option as manually exercised at the given priceAtExercise.
Interpolated price curve.
Definition: pricecurve.hpp:50
commodity index class for holding commodity spot and futures price histories and forwarding.
Interpolated price curve.
vector< bool > irdes
BOOST_DATA_TEST_CASE(testOptionBeforeExpiry, bdata::make(strikes) *bdata::make(optionTypes), strike, optionType)
vector< Real > strikes
vector< Option::Type > optionTypes
Fixture that can be used at top level.