| 1 | /* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
| 2 | |
| 3 | /* |
| 4 | Copyright (C) 2010 Adrian O' Neill |
| 5 | Copyright (C) 2018 Roy Zywina |
| 6 | |
| 7 | This file is part of QuantLib, a free-software/open-source library |
| 8 | for financial quantitative analysts and developers - http://quantlib.org/ |
| 9 | |
| 10 | QuantLib is free software: you can redistribute it and/or modify it |
| 11 | under the terms of the QuantLib license. You should have received a |
| 12 | copy of the license along with this program; if not, please email |
| 13 | <quantlib-dev@lists.sf.net>. The license is also available online at |
| 14 | <http://quantlib.org/license.shtml>. |
| 15 | |
| 16 | This program is distributed in the hope that it will be useful, but WITHOUT |
| 17 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| 18 | FOR A PARTICULAR PURPOSE. See the license for more details. |
| 19 | */ |
| 20 | |
| 21 | #include "variancegamma.hpp" |
| 22 | #include "utilities.hpp" |
| 23 | #include <ql/time/daycounters/actual360.hpp> |
| 24 | #include <ql/time/daycounters/thirty360.hpp> |
| 25 | #include <ql/instruments/europeanoption.hpp> |
| 26 | #include <ql/experimental/variancegamma/analyticvariancegammaengine.hpp> |
| 27 | #include <ql/experimental/variancegamma/fftvariancegammaengine.hpp> |
| 28 | #include <ql/termstructures/yield/flatforward.hpp> |
| 29 | #include <ql/termstructures/volatility/equityfx/blackconstantvol.hpp> |
| 30 | #include <ql/utilities/dataformatters.hpp> |
| 31 | #include <map> |
| 32 | |
| 33 | using namespace QuantLib; |
| 34 | using namespace boost::unit_test_framework; |
| 35 | |
| 36 | #undef REPORT_FAILURE |
| 37 | #define REPORT_FAILURE(greekName, payoff, exercise, s, q, r, today, sigma, \ |
| 38 | nu, theta, expected, calculated, \ |
| 39 | error, tolerance) \ |
| 40 | BOOST_FAIL(exerciseTypeToString(exercise) << " " \ |
| 41 | << payoff->optionType() << " option with " \ |
| 42 | << payoffTypeToString(payoff) << " payoff:\n" \ |
| 43 | << " underlying value: " << s << "\n" \ |
| 44 | << " strike: " << payoff->strike() <<"\n" \ |
| 45 | << " dividend yield: " << io::rate(q) << "\n" \ |
| 46 | << " risk-free rate: " << io::rate(r) << "\n" \ |
| 47 | << " reference date: " << today << "\n" \ |
| 48 | << " maturity: " << exercise->lastDate() << "\n" \ |
| 49 | << " sigma: " << sigma << "\n" \ |
| 50 | << " nu: " << nu << "\n" \ |
| 51 | << " theta: " << theta << "\n\n" \ |
| 52 | << " expected " << greekName << ": " << expected << "\n" \ |
| 53 | << " calculated " << greekName << ": " << calculated << "\n"\ |
| 54 | << " error: " << error << "\n" \ |
| 55 | << " tolerance: " << tolerance); |
| 56 | |
| 57 | namespace { |
| 58 | |
| 59 | struct VarianceGammaProcessData { |
| 60 | Real s; // spot |
| 61 | Rate q; // dividend |
| 62 | Rate r; // risk-free rate |
| 63 | Real sigma; |
| 64 | Real nu; |
| 65 | Real theta; |
| 66 | |
| 67 | }; |
| 68 | |
| 69 | struct VarianceGammaOptionData { |
| 70 | Option::Type type; |
| 71 | Real strike; |
| 72 | Time t; // time to maturity |
| 73 | }; |
| 74 | } |
| 75 | |
| 76 | |
| 77 | void VarianceGammaTest::testVarianceGamma() { |
| 78 | |
| 79 | BOOST_TEST_MESSAGE("Testing variance-gamma model for European options..." ); |
| 80 | |
| 81 | VarianceGammaProcessData processes[] = { |
| 82 | // spot, q, r,sigma, nu, theta |
| 83 | { .s: 6000, .q: 0.00, .r: 0.05, .sigma: 0.20, .nu: 0.05, .theta: -0.50 }, |
| 84 | { .s: 6000, .q: 0.02, .r: 0.05, .sigma: 0.15, .nu: 0.01, .theta: -0.50 } |
| 85 | }; |
| 86 | |
| 87 | VarianceGammaOptionData options[] = { |
| 88 | // type,strike, t |
| 89 | { .type: Option::Call, .strike: 5550, .t: 1.0}, |
| 90 | { .type: Option::Call, .strike: 5600, .t: 1.0}, |
| 91 | { .type: Option::Call, .strike: 5650, .t: 1.0}, |
| 92 | { .type: Option::Call, .strike: 5700, .t: 1.0}, |
| 93 | { .type: Option::Call, .strike: 5750, .t: 1.0}, |
| 94 | { .type: Option::Call, .strike: 5800, .t: 1.0}, |
| 95 | { .type: Option::Call, .strike: 5850, .t: 1.0}, |
| 96 | { .type: Option::Call, .strike: 5900, .t: 1.0}, |
| 97 | { .type: Option::Call, .strike: 5950, .t: 1.0}, |
| 98 | { .type: Option::Call, .strike: 6000, .t: 1.0}, |
| 99 | { .type: Option::Call, .strike: 6050, .t: 1.0}, |
| 100 | { .type: Option::Call, .strike: 6100, .t: 1.0}, |
| 101 | { .type: Option::Call, .strike: 6150, .t: 1.0}, |
| 102 | { .type: Option::Call, .strike: 6200, .t: 1.0}, |
| 103 | { .type: Option::Call, .strike: 6250, .t: 1.0}, |
| 104 | { .type: Option::Call, .strike: 6300, .t: 1.0}, |
| 105 | { .type: Option::Call, .strike: 6350, .t: 1.0}, |
| 106 | { .type: Option::Call, .strike: 6400, .t: 1.0}, |
| 107 | { .type: Option::Call, .strike: 6450, .t: 1.0}, |
| 108 | { .type: Option::Call, .strike: 6500, .t: 1.0}, |
| 109 | { .type: Option::Call, .strike: 6550, .t: 1.0}, |
| 110 | { .type: Option::Put, .strike: 5550, .t: 1.0} |
| 111 | }; |
| 112 | |
| 113 | Real results[LENGTH(processes)][LENGTH(options)] = { |
| 114 | { |
| 115 | 955.1637, 922.7529, 890.9872, 859.8739, 829.4197, 799.6303, 770.5104, 742.0640, |
| 116 | 714.2943, 687.2032, 660.7921, 635.0613, 610.0103, 585.6379, 561.9416, 538.9186, |
| 117 | 516.5649, 494.8760, 473.8464, 453.4700, 433.7400, 234.4870 |
| 118 | }, |
| 119 | { |
| 120 | 732.8705, 698.5542, 665.1404, 632.6498, 601.1002, 570.5068, 540.8824, 512.2367, |
| 121 | 484.5766, 457.9064, 432.2273, 407.5381, 383.8346, 361.1102, 339.3559, 318.5599, |
| 122 | 298.7087, 279.7864, 261.7751, 244.6552, 228.4057, 130.9974 |
| 123 | } |
| 124 | }; |
| 125 | |
| 126 | Real tol = 0.01; |
| 127 | |
| 128 | DayCounter dc = Actual360(); |
| 129 | Date today = Date::todaysDate(); |
| 130 | |
| 131 | for (Size i=0; i<LENGTH(processes); i++) { |
| 132 | ext::shared_ptr<SimpleQuote> spot(new SimpleQuote(processes[i].s)); |
| 133 | ext::shared_ptr<SimpleQuote> qRate(new SimpleQuote(processes[i].q)); |
| 134 | ext::shared_ptr<YieldTermStructure> qTS = flatRate(today, forward: qRate, dc); |
| 135 | ext::shared_ptr<SimpleQuote> rRate(new SimpleQuote(processes[i].r)); |
| 136 | ext::shared_ptr<YieldTermStructure> rTS = flatRate(today, forward: rRate, dc); |
| 137 | |
| 138 | ext::shared_ptr<VarianceGammaProcess> stochProcess( |
| 139 | new VarianceGammaProcess(Handle<Quote>(spot), |
| 140 | Handle<YieldTermStructure>(qTS), |
| 141 | Handle<YieldTermStructure>(rTS), |
| 142 | processes[i].sigma, |
| 143 | processes[i].nu, |
| 144 | processes[i].theta)); |
| 145 | |
| 146 | // Analytic engine |
| 147 | ext::shared_ptr<PricingEngine> analyticEngine( |
| 148 | new VarianceGammaEngine(stochProcess)); |
| 149 | |
| 150 | // FFT engine |
| 151 | ext::shared_ptr<FFTVarianceGammaEngine> fftEngine( |
| 152 | new FFTVarianceGammaEngine(stochProcess)); |
| 153 | |
| 154 | // which requires a list of options |
| 155 | std::vector<ext::shared_ptr<Instrument> > optionList; |
| 156 | |
| 157 | std::vector<ext::shared_ptr<StrikedTypePayoff> > payoffs; |
| 158 | for (Size j=0; j<LENGTH(options); j++) |
| 159 | { |
| 160 | Date exDate = today + timeToDays(t: options[j].t); |
| 161 | ext::shared_ptr<Exercise> exercise(new EuropeanExercise(exDate)); |
| 162 | |
| 163 | ext::shared_ptr<StrikedTypePayoff> payoff(new |
| 164 | PlainVanillaPayoff(options[j].type, options[j].strike)); |
| 165 | payoffs.push_back(x: payoff); |
| 166 | |
| 167 | // Test analytic engine |
| 168 | ext::shared_ptr<EuropeanOption> option(new EuropeanOption(payoff, exercise)); |
| 169 | option->setPricingEngine(analyticEngine); |
| 170 | |
| 171 | Real calculated = option->NPV(); |
| 172 | Real expected = results[i][j]; |
| 173 | Real error = std::fabs(x: calculated-expected); |
| 174 | |
| 175 | if (error>tol) { |
| 176 | REPORT_FAILURE("analytic value" , payoff, exercise, |
| 177 | processes[i].s, processes[i].q, processes[i].r, |
| 178 | today, processes[i].sigma, processes[i].nu, |
| 179 | processes[i].theta, expected, calculated, |
| 180 | error, tol); |
| 181 | } |
| 182 | optionList.push_back(x: option); |
| 183 | } |
| 184 | |
| 185 | // Test FFT engine |
| 186 | // FFT engine is extremely efficient when sent a list of options to calculate first |
| 187 | fftEngine->precalculate(optionList); |
| 188 | for (Size j=0; j<LENGTH(options); j++) |
| 189 | { |
| 190 | ext::shared_ptr<VanillaOption> option = ext::static_pointer_cast<VanillaOption>(r: optionList[j]); |
| 191 | option->setPricingEngine(fftEngine); |
| 192 | |
| 193 | Real calculated = option->NPV(); |
| 194 | Real expected = results[i][j]; |
| 195 | Real error = std::fabs(x: calculated-expected); |
| 196 | if (error>tol) { |
| 197 | ext::shared_ptr<StrikedTypePayoff> payoff = |
| 198 | ext::dynamic_pointer_cast<StrikedTypePayoff>(r: option->payoff()); |
| 199 | REPORT_FAILURE("fft value" , payoff, option->exercise(), |
| 200 | processes[i].s, processes[i].q, processes[i].r, |
| 201 | today, processes[i].sigma, processes[i].nu, |
| 202 | processes[i].theta, expected, calculated, |
| 203 | error, tol); |
| 204 | } |
| 205 | } |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | void VarianceGammaTest::testSingularityAtZero() { |
| 210 | |
| 211 | BOOST_TEST_MESSAGE( |
| 212 | "Testing variance-gamma model integration around zero..." ); |
| 213 | |
| 214 | Real stock = 100; |
| 215 | Real strike = 98; |
| 216 | Volatility sigma = 0.12; |
| 217 | Real mu = -0.14; |
| 218 | Real kappa = 0.2; |
| 219 | |
| 220 | Date valuation(1,Jan,2017); |
| 221 | Date maturity(10,Jan,2017); |
| 222 | DayCounter discountCounter = Thirty360(Thirty360::BondBasis); |
| 223 | |
| 224 | Settings::instance().evaluationDate() = valuation; |
| 225 | |
| 226 | ext::shared_ptr<Exercise> exercise = |
| 227 | ext::make_shared<EuropeanExercise>(args&: maturity); |
| 228 | ext::shared_ptr<StrikedTypePayoff> payoff = |
| 229 | ext::make_shared<PlainVanillaPayoff>(args: Option::Call, args&: strike); |
| 230 | VanillaOption option(payoff, exercise); |
| 231 | |
| 232 | Handle<YieldTermStructure> dividend( |
| 233 | ext::make_shared<FlatForward>(args&: valuation,args: 0.0,args&: discountCounter)); |
| 234 | Handle<YieldTermStructure> disc( |
| 235 | ext::make_shared<FlatForward>(args&: valuation,args: 0.05,args&: discountCounter)); |
| 236 | Handle<Quote> S0(ext::make_shared<SimpleQuote>(args&: stock)); |
| 237 | ext::shared_ptr<QuantLib::VarianceGammaProcess> process = |
| 238 | ext::make_shared<VarianceGammaProcess>(args&: S0, args&: dividend, args&: disc, |
| 239 | args&: sigma, args&: kappa, args&: mu); |
| 240 | |
| 241 | option.setPricingEngine(ext::make_shared<VarianceGammaEngine>(args&: process)); |
| 242 | // without the fix, the call below goes into an infinite loop, |
| 243 | // which is hard to test for. We're just happy to see the test |
| 244 | // case finish, hence the lack of an assertion. |
| 245 | option.NPV(); |
| 246 | } |
| 247 | |
| 248 | |
| 249 | test_suite* VarianceGammaTest::suite() { |
| 250 | auto* suite = BOOST_TEST_SUITE("Variance Gamma tests" ); |
| 251 | |
| 252 | suite->add(QUANTLIB_TEST_CASE(&VarianceGammaTest::testVarianceGamma)); |
| 253 | suite->add(QUANTLIB_TEST_CASE(&VarianceGammaTest::testSingularityAtZero)); |
| 254 | return suite; |
| 255 | } |
| 256 | |