[go: up one dir, main page]

1/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3/*
4 Copyright (C) 2004, 2007 StatPro Italia srl
5
6 This file is part of QuantLib, a free-software/open-source library
7 for financial quantitative analysts and developers - http://quantlib.org/
8
9 QuantLib is free software: you can redistribute it and/or modify it
10 under the terms of the QuantLib license. You should have received a
11 copy of the license along with this program; if not, please email
12 <quantlib-dev@lists.sf.net>. The license is also available online at
13 <http://quantlib.org/license.shtml>.
14
15 This program is distributed in the hope that it will be useful, but WITHOUT
16 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17 FOR A PARTICULAR PURPOSE. See the license for more details.
18*/
19
20#include "forwardoption.hpp"
21#include "utilities.hpp"
22#include <ql/time/daycounters/actual360.hpp>
23#include <ql/instruments/forwardvanillaoption.hpp>
24#include <ql/models/equity/hestonmodel.hpp>
25#include <ql/pricingengines/vanilla/analyticeuropeanengine.hpp>
26#include <ql/pricingengines/vanilla/analytichestonengine.hpp>
27#include <ql/pricingengines/vanilla/binomialengine.hpp>
28#include <ql/pricingengines/forward/forwardengine.hpp>
29#include <ql/pricingengines/forward/forwardperformanceengine.hpp>
30#include <ql/pricingengines/forward/mcforwardeuropeanbsengine.hpp>
31#include <ql/pricingengines/forward/mcforwardeuropeanhestonengine.hpp>
32#include <ql/experimental/forward/analytichestonforwardeuropeanengine.hpp>
33#include <ql/termstructures/yield/flatforward.hpp>
34#include <ql/termstructures/volatility/equityfx/blackconstantvol.hpp>
35#include <ql/processes/hestonprocess.hpp>
36#include <ql/utilities/dataformatters.hpp>
37#include <map>
38
39using namespace QuantLib;
40using namespace boost::unit_test_framework;
41
42#undef REPORT_FAILURE
43#define REPORT_FAILURE(greekName, payoff, exercise, s, q, r, today, \
44 v, moneyness, reset, expected, calculated, \
45 error, tolerance) \
46 BOOST_ERROR("Forward " << exerciseTypeToString(exercise) << " " \
47 << payoff->optionType() << " option with " \
48 << payoffTypeToString(payoff) << " payoff:\n" \
49 << " spot value: " << s << "\n" \
50 << " strike: " << payoff->strike() <<"\n" \
51 << " moneyness: " << moneyness << "\n" \
52 << " dividend yield: " << io::rate(q) << "\n" \
53 << " risk-free rate: " << io::rate(r) << "\n" \
54 << " reference date: " << today << "\n" \
55 << " reset date: " << reset << "\n" \
56 << " maturity: " << exercise->lastDate() << "\n" \
57 << " volatility: " << io::volatility(v) << "\n\n" \
58 << " expected " << greekName << ": " << expected << "\n" \
59 << " calculated " << greekName << ": " << calculated << "\n"\
60 << " error: " << error << "\n" \
61 << " tolerance: " << tolerance);
62
63namespace {
64
65 struct ForwardOptionData {
66 Option::Type type;
67 Real moneyness;
68 Real s; // spot
69 Rate q; // dividend
70 Rate r; // risk-free rate
71 Time start; // time to reset
72 Time t; // time to maturity
73 Volatility v; // volatility
74 Real result; // expected result
75 Real tol; // tolerance
76 };
77
78}
79
80
81void ForwardOptionTest::testValues() {
82
83 BOOST_TEST_MESSAGE("Testing forward option values...");
84
85 /* The data below are from
86 "Option pricing formulas", E.G. Haug, McGraw-Hill 1998
87 */
88 ForwardOptionData values[] = {
89 // type, moneyness, spot, div, rate,start, t, vol, result, tol
90 // "Option pricing formulas", pag. 37
91 { .type: Option::Call, .moneyness: 1.1, .s: 60.0, .q: 0.04, .r: 0.08, .start: 0.25, .t: 1.0, .v: 0.30, .result: 4.4064, .tol: 1.0e-4 },
92 // "Option pricing formulas", VBA code
93 { .type: Option::Put, .moneyness: 1.1, .s: 60.0, .q: 0.04, .r: 0.08, .start: 0.25, .t: 1.0, .v: 0.30, .result: 8.2971, .tol: 1.0e-4 }
94 };
95
96 DayCounter dc = Actual360();
97 Date today = Settings::instance().evaluationDate();
98
99 ext::shared_ptr<SimpleQuote> spot(new SimpleQuote(0.0));
100 ext::shared_ptr<SimpleQuote> qRate(new SimpleQuote(0.0));
101 Handle<YieldTermStructure> qTS(flatRate(today, forward: qRate, dc));
102 ext::shared_ptr<SimpleQuote> rRate(new SimpleQuote(0.0));
103 Handle<YieldTermStructure> rTS(flatRate(today, forward: rRate, dc));
104 ext::shared_ptr<SimpleQuote> vol(new SimpleQuote(0.0));
105 Handle<BlackVolTermStructure> volTS(flatVol(today, volatility: vol, dc));
106
107 ext::shared_ptr<BlackScholesMertonProcess> stochProcess(
108 new BlackScholesMertonProcess(Handle<Quote>(spot),
109 Handle<YieldTermStructure>(qTS),
110 Handle<YieldTermStructure>(rTS),
111 Handle<BlackVolTermStructure>(volTS)));
112
113 ext::shared_ptr<PricingEngine> engine(
114 new ForwardVanillaEngine<AnalyticEuropeanEngine>(stochProcess));
115
116 for (auto& value : values) {
117
118 ext::shared_ptr<StrikedTypePayoff> payoff(new PlainVanillaPayoff(value.type, 0.0));
119 Date exDate = today + timeToDays(t: value.t);
120 ext::shared_ptr<Exercise> exercise(new EuropeanExercise(exDate));
121 Date reset = today + timeToDays(t: value.start);
122
123 spot->setValue(value.s);
124 qRate->setValue(value.q);
125 rRate->setValue(value.r);
126 vol->setValue(value.v);
127
128 ForwardVanillaOption option(value.moneyness, reset, payoff, exercise);
129 option.setPricingEngine(engine);
130
131 Real calculated = option.NPV();
132 Real error = std::fabs(x: calculated - value.result);
133 Real tolerance = 1e-4;
134 if (error>tolerance) {
135 REPORT_FAILURE("value", payoff, exercise, value.s, value.q, value.r, today, value.v,
136 value.moneyness, reset, value.result, calculated, error, tolerance);
137 }
138 }
139}
140
141
142void ForwardOptionTest::testPerformanceValues() {
143
144 BOOST_TEST_MESSAGE("Testing forward performance option values...");
145
146 /* The data below are the performance equivalent of the
147 forward options tested above and taken from
148 "Option pricing formulas", E.G. Haug, McGraw-Hill 1998
149 */
150 ForwardOptionData values[] = {
151 // type, moneyness, spot, div, rate,start, maturity, vol, result, tol
152 { .type: Option::Call, .moneyness: 1.1, .s: 60.0, .q: 0.04, .r: 0.08, .start: 0.25, .t: 1.0, .v: 0.30, .result: 4.4064/60*std::exp(x: -0.04*0.25), .tol: 1.0e-4 },
153 { .type: Option::Put, .moneyness: 1.1, .s: 60.0, .q: 0.04, .r: 0.08, .start: 0.25, .t: 1.0, .v: 0.30, .result: 8.2971/60*std::exp(x: -0.04*0.25), .tol: 1.0e-4 }
154 };
155
156 DayCounter dc = Actual360();
157 Date today = Settings::instance().evaluationDate();
158
159 ext::shared_ptr<SimpleQuote> spot(new SimpleQuote(0.0));
160 ext::shared_ptr<SimpleQuote> qRate(new SimpleQuote(0.0));
161 Handle<YieldTermStructure> qTS(flatRate(today, forward: qRate, dc));
162 ext::shared_ptr<SimpleQuote> rRate(new SimpleQuote(0.0));
163 Handle<YieldTermStructure> rTS(flatRate(today, forward: rRate, dc));
164 ext::shared_ptr<SimpleQuote> vol(new SimpleQuote(0.0));
165 Handle<BlackVolTermStructure> volTS(flatVol(today, volatility: vol, dc));
166
167 ext::shared_ptr<BlackScholesMertonProcess> stochProcess(
168 new BlackScholesMertonProcess(Handle<Quote>(spot),
169 Handle<YieldTermStructure>(qTS),
170 Handle<YieldTermStructure>(rTS),
171 Handle<BlackVolTermStructure>(volTS)));
172
173 ext::shared_ptr<PricingEngine> engine(
174 new ForwardPerformanceVanillaEngine<AnalyticEuropeanEngine>(
175 stochProcess));
176
177 for (auto& value : values) {
178
179 ext::shared_ptr<StrikedTypePayoff> payoff(new PlainVanillaPayoff(value.type, 0.0));
180 Date exDate = today + timeToDays(t: value.t);
181 ext::shared_ptr<Exercise> exercise(new EuropeanExercise(exDate));
182 Date reset = today + timeToDays(t: value.start);
183
184 spot->setValue(value.s);
185 qRate->setValue(value.q);
186 rRate->setValue(value.r);
187 vol->setValue(value.v);
188
189 ForwardVanillaOption option(value.moneyness, reset, payoff, exercise);
190 option.setPricingEngine(engine);
191
192 Real calculated = option.NPV();
193 Real error = std::fabs(x: calculated - value.result);
194 Real tolerance = 1e-4;
195 if (error>tolerance) {
196 REPORT_FAILURE("value", payoff, exercise, value.s, value.q, value.r, today, value.v,
197 value.moneyness, reset, value.result, calculated, error, tolerance);
198 }
199 }
200}
201
202
203namespace {
204
205 template <template <class> class Engine>
206 void testForwardGreeks() {
207
208 std::map<std::string,Real> calculated, expected, tolerance;
209 tolerance["delta"] = 1.0e-5;
210 tolerance["gamma"] = 1.0e-5;
211 tolerance["theta"] = 1.0e-5;
212 tolerance["rho"] = 1.0e-5;
213 tolerance["divRho"] = 1.0e-5;
214 tolerance["vega"] = 1.0e-5;
215
216 Option::Type types[] = { Option::Call, Option::Put };
217 Real moneyness[] = { 0.9, 1.0, 1.1 };
218 Real underlyings[] = { 100.0 };
219 Rate qRates[] = { 0.04, 0.05, 0.06 };
220 Rate rRates[] = { 0.01, 0.05, 0.15 };
221 Integer lengths[] = { 1, 2 };
222 Integer startMonths[] = { 6, 9 };
223 Volatility vols[] = { 0.11, 0.50, 1.20 };
224
225 DayCounter dc = Actual360();
226 Date today = Settings::instance().evaluationDate();
227
228 ext::shared_ptr<SimpleQuote> spot(new SimpleQuote(0.0));
229 ext::shared_ptr<SimpleQuote> qRate(new SimpleQuote(0.0));
230 Handle<YieldTermStructure> qTS(flatRate(forward: qRate, dc));
231 ext::shared_ptr<SimpleQuote> rRate(new SimpleQuote(0.0));
232 Handle<YieldTermStructure> rTS(flatRate(forward: rRate, dc));
233 ext::shared_ptr<SimpleQuote> vol(new SimpleQuote(0.0));
234 Handle<BlackVolTermStructure> volTS(flatVol(volatility: vol, dc));
235
236 ext::shared_ptr<BlackScholesMertonProcess> stochProcess(
237 new BlackScholesMertonProcess(Handle<Quote>(spot), qTS, rTS, volTS));
238
239 ext::shared_ptr<PricingEngine> engine(
240 new Engine<AnalyticEuropeanEngine>(stochProcess));
241
242 for (auto& type : types) {
243 for (Real& moneynes : moneyness) {
244 for (int length : lengths) {
245 for (int startMonth : startMonths) {
246
247 Date exDate = today + length * Years;
248 ext::shared_ptr<Exercise> exercise(new EuropeanExercise(exDate));
249
250 Date reset = today + startMonth * Months;
251
252 ext::shared_ptr<StrikedTypePayoff> payoff(
253 new PlainVanillaPayoff(type, 0.0));
254
255 ForwardVanillaOption option(moneynes, reset, payoff, exercise);
256 option.setPricingEngine(engine);
257
258 for (Real u : underlyings) {
259 for (Real m : qRates) {
260 for (Real n : rRates) {
261 for (Real v : vols) {
262
263 Rate q = m, r = n;
264 spot->setValue(u);
265 qRate->setValue(q);
266 rRate->setValue(r);
267 vol->setValue(v);
268
269 Real value = option.NPV();
270 calculated["delta"] = option.delta();
271 calculated["gamma"] = option.gamma();
272 calculated["theta"] = option.theta();
273 calculated["rho"] = option.rho();
274 calculated["divRho"] = option.dividendRho();
275 calculated["vega"] = option.vega();
276
277 if (value > spot->value() * 1.0e-5) {
278 // perturb spot and get delta and gamma
279 Real du = u * 1.0e-4;
280 spot->setValue(u + du);
281 Real value_p = option.NPV(), delta_p = option.delta();
282 spot->setValue(u - du);
283 Real value_m = option.NPV(), delta_m = option.delta();
284 spot->setValue(u);
285 expected["delta"] = (value_p - value_m) / (2 * du);
286 expected["gamma"] = (delta_p - delta_m) / (2 * du);
287
288 // perturb rates and get rho and dividend rho
289 Spread dr = r * 1.0e-4;
290 rRate->setValue(r + dr);
291 value_p = option.NPV();
292 rRate->setValue(r - dr);
293 value_m = option.NPV();
294 rRate->setValue(r);
295 expected["rho"] = (value_p - value_m) / (2 * dr);
296
297 Spread dq = q * 1.0e-4;
298 qRate->setValue(q + dq);
299 value_p = option.NPV();
300 qRate->setValue(q - dq);
301 value_m = option.NPV();
302 qRate->setValue(q);
303 expected["divRho"] = (value_p - value_m) / (2 * dq);
304
305 // perturb volatility and get vega
306 Volatility dv = v * 1.0e-4;
307 vol->setValue(v + dv);
308 value_p = option.NPV();
309 vol->setValue(v - dv);
310 value_m = option.NPV();
311 vol->setValue(v);
312 expected["vega"] = (value_p - value_m) / (2 * dv);
313
314 // perturb date and get theta
315 Time dT = dc.yearFraction(d1: today - 1, d2: today + 1);
316 Settings::instance().evaluationDate() = today - 1;
317 value_m = option.NPV();
318 Settings::instance().evaluationDate() = today + 1;
319 value_p = option.NPV();
320 Settings::instance().evaluationDate() = today;
321 expected["theta"] = (value_p - value_m) / dT;
322
323 // compare
324 std::map<std::string, Real>::iterator it;
325 for (it = calculated.begin(); it != calculated.end();
326 ++it) {
327 std::string greek = it->first;
328 Real expct = expected[greek],
329 calcl = calculated[greek],
330 tol = tolerance[greek];
331 Real error = relativeError(x1: expct, x2: calcl, reference: u);
332 if (error > tol) {
333 REPORT_FAILURE(greek, payoff, exercise, u, q, r,
334 today, v, moneynes, reset, expct,
335 calcl, error, tol);
336 }
337 }
338 }
339 }
340 }
341 }
342 }
343 }
344 }
345 }
346 }
347 }
348
349}
350
351
352void ForwardOptionTest::testGreeks() {
353
354 BOOST_TEST_MESSAGE("Testing forward option greeks...");
355
356 testForwardGreeks<ForwardVanillaEngine>();
357}
358
359
360void ForwardOptionTest::testPerformanceGreeks() {
361
362 BOOST_TEST_MESSAGE("Testing forward performance option greeks...");
363
364 testForwardGreeks<ForwardPerformanceVanillaEngine>();
365}
366
367
368class TestBinomialEngine : public BinomialVanillaEngine<CoxRossRubinstein>
369{
370private:
371public:
372 explicit TestBinomialEngine(
373 const ext::shared_ptr<GeneralizedBlackScholesProcess > &process)
374 : BinomialVanillaEngine<CoxRossRubinstein>(process, 300) // fixed steps
375 {}
376};
377
378
379void ForwardOptionTest::testGreeksInitialization() {
380 BOOST_TEST_MESSAGE("Testing forward option greeks initialization...");
381
382 DayCounter dc = Actual360();
383 Date today = Settings::instance().evaluationDate();
384
385 ext::shared_ptr<SimpleQuote> spot(new SimpleQuote(100.0));
386 ext::shared_ptr<SimpleQuote> qRate(new SimpleQuote(0.04));
387 Handle<YieldTermStructure> qTS(flatRate(forward: qRate, dc));
388 ext::shared_ptr<SimpleQuote> rRate(new SimpleQuote(0.01));
389 Handle<YieldTermStructure> rTS(flatRate(forward: rRate, dc));
390 ext::shared_ptr<SimpleQuote> vol(new SimpleQuote(0.11));
391 Handle<BlackVolTermStructure> volTS(flatVol(volatility: vol, dc));
392
393 ext::shared_ptr<BlackScholesMertonProcess> stochProcess(
394 new BlackScholesMertonProcess(Handle<Quote>(spot), qTS, rTS, volTS));
395
396 ext::shared_ptr<PricingEngine> engine(
397 new ForwardVanillaEngine<TestBinomialEngine>(stochProcess));
398 Date exDate = today + 1*Years;
399 ext::shared_ptr<Exercise> exercise(
400 new EuropeanExercise(exDate));
401 Date reset = today + 6*Months;
402 ext::shared_ptr<StrikedTypePayoff> payoff(
403 new PlainVanillaPayoff(Option::Call, 0.0));
404
405 ForwardVanillaOption option(0.9, reset, payoff, exercise);
406 option.setPricingEngine(engine);
407
408 ext::shared_ptr<PricingEngine> ctrlengine(
409 new TestBinomialEngine(stochProcess));
410 VanillaOption ctrloption(payoff, exercise);
411 ctrloption.setPricingEngine(ctrlengine);
412
413 Real delta = 0;
414 try
415 {
416 delta = ctrloption.delta();
417 }
418 catch (const QuantLib::Error &) {
419 // if normal option can't calculate delta,
420 // nor should forward
421 try
422 {
423 delta = option.delta();
424 }
425 catch (const QuantLib::Error &) {
426 delta = Null<Real>();
427 }
428 QL_REQUIRE(delta == Null<Real>(), "Forward delta invalid");
429 }
430
431 Real rho = 0;
432 try
433 {
434 rho = ctrloption.rho();
435 }
436 catch (const QuantLib::Error &) {
437 // if normal option can't calculate rho,
438 // nor should forward
439 try
440 {
441 rho = option.rho();
442 }
443 catch (const QuantLib::Error &) {
444 rho = Null<Real>();
445 }
446 QL_REQUIRE(rho == Null<Real>(), "Forward rho invalid");
447 }
448
449 Real divRho = 0;
450 try
451 {
452 divRho = ctrloption.dividendRho();
453 }
454 catch (const QuantLib::Error &) {
455 // if normal option can't calculate divRho,
456 // nor should forward
457 try
458 {
459 divRho = option.dividendRho();
460 }
461 catch (const QuantLib::Error &) {
462 divRho = Null<Real>();
463 }
464 QL_REQUIRE(divRho == Null<Real>(), "Forward dividendRho invalid");
465 }
466
467 Real vega = 0;
468 try
469 {
470 vega = ctrloption.vega();
471 }
472 catch (const QuantLib::Error &) {
473 // if normal option can't calculate vega,
474 // nor should forward
475 try
476 {
477 vega = option.vega();
478 }
479 catch (const QuantLib::Error &) {
480 vega = Null<Real>();
481 }
482 QL_REQUIRE(vega == Null<Real>(), "Forward vega invalid");
483 }
484}
485
486
487void ForwardOptionTest::testMCPrices() {
488 BOOST_TEST_MESSAGE("Testing forward option MC prices...");
489
490 Real tol[] = {0.002, 0.001, 0.0006, 5e-4, 5e-4};
491
492 Size timeSteps = 100;
493 Size numberOfSamples = 5000;
494 Size mcSeed = 42;
495
496 Real q = 0.04;
497 Real r = 0.01;
498 Real sigma = 0.11;
499 Real s = 100;
500
501 DayCounter dc = Actual360();
502 Date today = Settings::instance().evaluationDate();
503
504 ext::shared_ptr<SimpleQuote> spot(new SimpleQuote(s));
505 ext::shared_ptr<SimpleQuote> qRate(new SimpleQuote(q));
506 Handle<YieldTermStructure> qTS(flatRate(forward: qRate, dc));
507 ext::shared_ptr<SimpleQuote> rRate(new SimpleQuote(r));
508 Handle<YieldTermStructure> rTS(flatRate(forward: rRate, dc));
509 ext::shared_ptr<SimpleQuote> vol(new SimpleQuote(sigma));
510 Handle<BlackVolTermStructure> volTS(flatVol(volatility: vol, dc));
511
512 ext::shared_ptr<BlackScholesMertonProcess> stochProcess(
513 new BlackScholesMertonProcess(Handle<Quote>(spot), qTS, rTS, volTS));
514
515 ext::shared_ptr<PricingEngine> analyticEngine(
516 new ForwardVanillaEngine<AnalyticEuropeanEngine>(stochProcess));
517
518 ext::shared_ptr<PricingEngine> mcEngine
519 = MakeMCForwardEuropeanBSEngine<PseudoRandom>(stochProcess)
520 .withSteps(steps: timeSteps)
521 .withSamples(samples: numberOfSamples)
522 .withSeed(seed: mcSeed);
523
524 Date exDate = today + 1*Years;
525 ext::shared_ptr<Exercise> exercise(
526 new EuropeanExercise(exDate));
527 Date reset = today + 6*Months;
528 ext::shared_ptr<StrikedTypePayoff> payoff(
529 new PlainVanillaPayoff(Option::Call, 0.0));
530
531 Real moneyness[] = { 0.8, 0.9, 1.0, 1.1, 1.2 };
532
533 for (Size moneyness_index = 0; moneyness_index < LENGTH(moneyness); ++moneyness_index) {
534
535 ForwardVanillaOption option(moneyness[moneyness_index], reset, payoff, exercise);
536
537 option.setPricingEngine(analyticEngine);
538 Real analyticPrice = option.NPV();
539
540 option.setPricingEngine(mcEngine);
541 Real mcPrice = option.NPV();
542
543 Real error = relativeError(x1: analyticPrice, x2: mcPrice, reference: s);
544 if (error > tol[moneyness_index]) {
545 REPORT_FAILURE("testMCPrices", payoff, exercise, s, q, r, today, sigma, moneyness[moneyness_index], reset,
546 analyticPrice, mcPrice, error, tol[moneyness_index]);
547 }
548 }
549}
550
551
552void ForwardOptionTest::testHestonMCPrices() {
553 BOOST_TEST_MESSAGE("Testing forward option Heston MC prices...");
554
555 Option::Type optionTypes[] = { Option::Call, Option::Put };
556 Real mcForwardStartTolerance[][6] = {{7e-4, // Call, moneyness=0.8
557 8e-4, // Call, moneyness=0.9
558 6e-4, // Call, moneyness=1.0
559 5e-4, // Call, moneyness=1.1
560 5e-4}, // Call, moneyness=1.2
561 {6e-4, // Put, moneyness=0.8
562 5e-4, // Put, moneyness=0.9
563 6e-4, // Put, moneyness=1.0
564 0.001, // Put, moneyness=1.1
565 0.001}}; // Put, moneyness=1.2
566
567 Real tol[][6] = {{9e-4, // Call, moneyness=0.8
568 9e-4, // Call, moneyness=0.9
569 6e-4, // Call, moneyness=1.0
570 5e-4, // Call, moneyness=1.1
571 5e-4}, // Call, moneyness=1.2
572 {6e-4, // Put, moneyness=0.8
573 5e-4, // Put, moneyness=0.9
574 8e-4, // Put, moneyness=1.0
575 0.002, // Put, moneyness=1.1
576 0.002}}; // Put, moneyness=1.2
577
578 for (Size type_index = 0; type_index < LENGTH(optionTypes); ++type_index) {
579
580 Real analyticTolerance = 5e-4;
581
582 Size timeSteps = 50;
583 Size numberOfSamples = 4095;
584 Size mcSeed = 42;
585
586 Real q = 0.04;
587 Real r = 0.01;
588 Real sigma_bs = 0.245;
589 Real s = 100;
590
591 // Test 1: Set up an equivalent flat Heston and compare to analytical BS pricing
592 Real v0 = sigma_bs * sigma_bs;
593 Real kappa = 1e-8;
594 Real theta = sigma_bs * sigma_bs;
595 Real sigma = 1e-8;
596 Real rho = -0.93;
597
598 DayCounter dc = Actual360();
599 Date today = Settings::instance().evaluationDate();
600
601 Date exDate = today + 1 * Years;
602 ext::shared_ptr<Exercise> exercise(new EuropeanExercise(exDate));
603 Date reset = today + 6 * Months;
604 ext::shared_ptr<StrikedTypePayoff> payoff(new PlainVanillaPayoff(optionTypes[type_index], 0.0));
605
606 ext::shared_ptr<SimpleQuote> spot(new SimpleQuote(s));
607 ext::shared_ptr<SimpleQuote> qRate(new SimpleQuote(q));
608 Handle<YieldTermStructure> qTS(flatRate(forward: qRate, dc));
609 ext::shared_ptr<SimpleQuote> rRate(new SimpleQuote(r));
610 Handle<YieldTermStructure> rTS(flatRate(forward: rRate, dc));
611 ext::shared_ptr<SimpleQuote> vol(new SimpleQuote(sigma_bs));
612 Handle<BlackVolTermStructure> volTS(flatVol(volatility: vol, dc));
613
614 ext::shared_ptr<BlackScholesMertonProcess> bsProcess(
615 new BlackScholesMertonProcess(Handle<Quote>(spot), qTS, rTS, volTS));
616
617 ext::shared_ptr<PricingEngine> analyticEngine(
618 new ForwardVanillaEngine<AnalyticEuropeanEngine>(bsProcess));
619
620 ext::shared_ptr<HestonProcess> hestonProcess(
621 new HestonProcess(rTS, qTS, Handle<Quote>(spot), v0, kappa, theta, sigma, rho));
622
623 ext::shared_ptr<PricingEngine> mcEngine =
624 MakeMCForwardEuropeanHestonEngine<LowDiscrepancy>(hestonProcess)
625 .withSteps(steps: timeSteps)
626 .withSamples(samples: numberOfSamples)
627 .withSeed(seed: mcSeed);
628
629 Real moneyness[] = {0.8, 0.9, 1.0, 1.1, 1.2};
630
631 for (Size moneyness_index = 0; moneyness_index < LENGTH(moneyness); ++moneyness_index) {
632
633 ForwardVanillaOption option(moneyness[moneyness_index], reset, payoff, exercise);
634
635 option.setPricingEngine(analyticEngine);
636 Real analyticPrice = option.NPV();
637
638 option.setPricingEngine(mcEngine);
639 Real mcPrice = option.NPV();
640
641 Real mcError = relativeError(x1: analyticPrice, x2: mcPrice, reference: s);
642
643 if (mcError > mcForwardStartTolerance[type_index][moneyness_index]) {
644 REPORT_FAILURE("testHestonMCForwardStartPrices", payoff, exercise, s, q, r, today,
645 sigma_bs, moneyness[moneyness_index], reset, analyticPrice, mcPrice, mcError, mcForwardStartTolerance[type_index][moneyness_index]);
646 }
647 }
648
649 // Test 2: Using an arbitrary Heston model, check that prices match semi-analytical
650 // Heston prices when reset date is t=0
651 v0 = sigma_bs * sigma_bs;
652 kappa = 1.0;
653 theta = 0.08;
654 sigma = 0.39;
655 rho = -0.93;
656
657 reset = today;
658
659 ext::shared_ptr<HestonProcess> hestonProcessSmile(
660 new HestonProcess(rTS, qTS, Handle<Quote>(spot), v0, kappa, theta, sigma, rho));
661
662 ext::shared_ptr<HestonModel> hestonModel(ext::make_shared<HestonModel>(args&: hestonProcessSmile));
663
664 ext::shared_ptr<PricingEngine> analyticHestonEngine(
665 ext::make_shared<AnalyticHestonEngine>(args&: hestonModel, args: 96));
666
667 ext::shared_ptr<PricingEngine> mcEngineSmile =
668 MakeMCForwardEuropeanHestonEngine<LowDiscrepancy>(hestonProcessSmile)
669 .withSteps(steps: timeSteps)
670 .withSamples(samples: numberOfSamples)
671 .withSeed(seed: mcSeed);
672
673 ext::shared_ptr<AnalyticHestonForwardEuropeanEngine> analyticForwardHestonEngine(
674 new AnalyticHestonForwardEuropeanEngine(hestonProcessSmile));
675
676 for (Size moneyness_index = 0; moneyness_index < LENGTH(moneyness); ++moneyness_index) {
677
678 Real strike = s * moneyness[moneyness_index];
679 ext::shared_ptr<StrikedTypePayoff> vanillaPayoff(new PlainVanillaPayoff(optionTypes[type_index], strike));
680
681 VanillaOption vanillaOption(vanillaPayoff, exercise);
682 ForwardVanillaOption forwardOption(moneyness[moneyness_index], reset, payoff, exercise);
683
684 vanillaOption.setPricingEngine(analyticHestonEngine);
685 Real analyticPrice = vanillaOption.NPV();
686
687 forwardOption.setPricingEngine(mcEngineSmile);
688 Real mcPrice = forwardOption.NPV();
689
690 Real mcError = relativeError(x1: analyticPrice, x2: mcPrice, reference: s);
691 auto tolerance = tol[type_index][moneyness_index];
692
693 if (mcError > tolerance) {
694 REPORT_FAILURE("testHestonMCPrices", vanillaPayoff, exercise, s, q, r, today,
695 sigma_bs, moneyness[moneyness_index], reset, analyticPrice, mcPrice, mcError,
696 tolerance);
697 }
698
699 // T=0, testing the Analytic Pricer's T=0 analytical solution
700 forwardOption.setPricingEngine(analyticForwardHestonEngine);
701 Real hestonAnalyticPrice = forwardOption.NPV();
702
703 Real analyticError = relativeError(x1: analyticPrice, x2: hestonAnalyticPrice, reference: s);
704 if (analyticError > analyticTolerance) {
705 REPORT_FAILURE("testHestonAnalyticForwardStartPrices", vanillaPayoff, exercise, s, q,
706 r, today, sigma_bs, moneyness[moneyness_index], reset, analyticPrice,
707 hestonAnalyticPrice, analyticError, analyticTolerance);
708 }
709 }
710 }
711}
712
713
714void ForwardOptionTest::testHestonAnalyticalVsMCPrices() {
715 BOOST_TEST_MESSAGE("Testing Heston analytic vs MC prices...");
716
717 Option::Type optionTypes[] = { Option::Call, Option::Put };
718 Real tol[][6] = {{0.002, // Call, moneyness=0.8, CV:false
719 0.002, // Call, moneyness=0.8, CV:true
720 0.001, // Call, moneyness=1.0, CV:false
721 0.001, // Call, moneyness=1.8, CV:true
722 0.001, // Call, moneyness=1.2, CV:false
723 0.001}, // Call, moneyness=1.2, CV:true
724 {0.001, // Put, moneyness=0.8, CV:false
725 0.001, // Put, moneyness=0.8, CV:true
726 0.003, // Put, moneyness=1.0, CV:false
727 0.003, // Put, moneyness=1.0, CV:true
728 0.003, // Put, moneyness=1.2, CV:false
729 0.003}}; // Put, moneyness=1.2, CV:true
730
731 for (Size option_type_index = 0; option_type_index < LENGTH(optionTypes); ++option_type_index) {
732
733 Size timeSteps = 50;
734 Size numberOfSamples = 5000;
735 Size mcSeed = 42;
736
737 Real q = 0.03;
738 Real r = 0.005;
739 Real s = 100;
740
741 Real vol = 0.3;
742 Real v0 = vol * vol;
743 Real kappa = 11.35;
744 Real theta = 0.022;
745 Real sigma = 0.618;
746 Real rho = -0.5;
747
748 DayCounter dc = Actual360();
749 Date today = Settings::instance().evaluationDate();
750
751 Date exDate = today + 1 * Years;
752 ext::shared_ptr<Exercise> exercise(new EuropeanExercise(exDate));
753 Date reset = today + 6 * Months;
754 ext::shared_ptr<StrikedTypePayoff> payoff(new PlainVanillaPayoff(optionTypes[option_type_index], 0.0));
755
756 ext::shared_ptr<SimpleQuote> spot(new SimpleQuote(s));
757 ext::shared_ptr<SimpleQuote> qRate(new SimpleQuote(q));
758 Handle<YieldTermStructure> qTS(flatRate(forward: qRate, dc));
759 ext::shared_ptr<SimpleQuote> rRate(new SimpleQuote(r));
760 Handle<YieldTermStructure> rTS(flatRate(forward: rRate, dc));
761
762 ext::shared_ptr<HestonProcess> hestonProcess(
763 new HestonProcess(rTS, qTS, Handle<Quote>(spot), v0, kappa, theta, sigma, rho));
764
765 ext::shared_ptr<PricingEngine> mcEngine =
766 MakeMCForwardEuropeanHestonEngine<PseudoRandom>(hestonProcess)
767 .withSteps(steps: timeSteps)
768 .withSamples(samples: numberOfSamples)
769 .withSeed(seed: mcSeed);
770
771 ext::shared_ptr<PricingEngine> mcEngineCv =
772 MakeMCForwardEuropeanHestonEngine<PseudoRandom>(hestonProcess)
773 .withSteps(steps: timeSteps)
774 .withSamples(samples: numberOfSamples)
775 .withSeed(seed: mcSeed)
776 .withControlVariate(b: true);
777
778 ext::shared_ptr<AnalyticHestonForwardEuropeanEngine> analyticEngine(
779 new AnalyticHestonForwardEuropeanEngine(hestonProcess));
780
781 Real moneyness[] = { 0.8, 1.0, 1.2 };
782
783 for (Size tol_2nd_index = 0; tol_2nd_index < LENGTH(moneyness); ++tol_2nd_index) {
784
785 auto m = moneyness[tol_2nd_index];
786 ForwardVanillaOption option(m, reset, payoff, exercise);
787
788 option.setPricingEngine(analyticEngine);
789 Real analyticPrice = option.NPV();
790
791 option.setPricingEngine(mcEngine);
792 Real mcPrice = option.NPV();
793 Real error = relativeError(x1: analyticPrice, x2: mcPrice, reference: s);
794
795 auto tolerance = tol[option_type_index][tol_2nd_index];
796 if (error > tolerance) {
797 REPORT_FAILURE("testHestonMCVsAnalyticPrices", payoff, exercise, s, q, r, today, vol,
798 m, reset, analyticPrice, mcPrice, error, tolerance);
799 }
800
801 option.setPricingEngine(mcEngineCv);
802 Real mcPriceCv = option.NPV();
803
804 Real errorCv = relativeError(x1: analyticPrice, x2: mcPriceCv, reference: s);
805 tolerance = tol[option_type_index][++tol_2nd_index];
806 if (errorCv > tolerance) {
807 REPORT_FAILURE("testHestonMCControlVariateVsAnalyticPrices", payoff, exercise, s, q,
808 r, today, vol, m, reset, analyticPrice, mcPrice, errorCv,
809 tolerance);
810 }
811 }
812 }
813}
814
815
816
817test_suite* ForwardOptionTest::suite(SpeedLevel speed) {
818 auto* suite = BOOST_TEST_SUITE("Forward option tests");
819
820 suite->add(QUANTLIB_TEST_CASE(&ForwardOptionTest::testValues));
821 suite->add(QUANTLIB_TEST_CASE(&ForwardOptionTest::testGreeks));
822 suite->add(QUANTLIB_TEST_CASE(&ForwardOptionTest::testPerformanceValues));
823 suite->add(QUANTLIB_TEST_CASE(&ForwardOptionTest::testPerformanceGreeks));
824 suite->add(QUANTLIB_TEST_CASE(&ForwardOptionTest::testGreeksInitialization));
825 suite->add(QUANTLIB_TEST_CASE(&ForwardOptionTest::testMCPrices));
826 suite->add(QUANTLIB_TEST_CASE(&ForwardOptionTest::testHestonMCPrices));
827
828 if (speed <= Fast) {
829 suite->add(QUANTLIB_TEST_CASE(&ForwardOptionTest::testHestonAnalyticalVsMCPrices));
830 }
831
832 return suite;
833}
834

source code of quantlib/test-suite/forwardoption.cpp