Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
commodityvolcurve.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2018 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// clang-format off
20#include <boost/test/unit_test.hpp>
21#include <boost/test/data/test_case.hpp>
22// clang-format on
23#include <oret/datapaths.hpp>
24#include <oret/toplevelfixture.hpp>
25
26#include <boost/make_shared.hpp>
27
28#include <cmath>
38#include <ql/termstructures/volatility/equityfx/blackvariancesurface.hpp>
44
45using namespace std;
46using namespace boost::unit_test_framework;
47using namespace QuantLib;
48using namespace QuantExt;
49using namespace ore::data;
50
52
53namespace {
54
55// testTolerance for Real comparison
56Real testTolerance = 1e-10;
57
58class MockLoader : public Loader {
59public:
60 MockLoader();
61 vector<QuantLib::ext::shared_ptr<MarketDatum>> loadQuotes(const Date&) const override { return data_; }
62 set<Fixing> loadFixings() const override { return dummyFixings_; }
63 set<QuantExt::Dividend> loadDividends() const override { return dummyDividends_; }
64 void add(QuantLib::Date date, const string& name, QuantLib::Real value) {}
65 void addFixing(QuantLib::Date date, const string& name, QuantLib::Real value) {}
66 void addDividend(const QuantExt::Dividend& dividend) {}
67
68private:
69 vector<QuantLib::ext::shared_ptr<MarketDatum>> data_;
70 QuantLib::ext::shared_ptr<MarketDatum> dummyDatum_;
71 set<Fixing> dummyFixings_;
72 set<QuantExt::Dividend> dummyDividends_;
73};
74
75MockLoader::MockLoader() {
76 Date asof(5, Feb, 2016);
77 data_ = {
78 QuantLib::ext::make_shared<CommodityOptionQuote>(0.11, asof, "COMMODITY_OPTION/RATE_LNVOL/GOLD/USD/1Y/ATM/AtmFwd",
79 MarketDatum::QuoteType::RATE_LNVOL, "GOLD", "USD",
80 QuantLib::ext::make_shared<ExpiryPeriod>(1 * Years),
81 QuantLib::ext::make_shared<AtmStrike>(DeltaVolQuote::AtmFwd)),
82 QuantLib::ext::make_shared<CommodityOptionQuote>(0.10, asof, "COMMODITY_OPTION/RATE_LNVOL/GOLD/USD/2Y/ATM/AtmFwd",
83 MarketDatum::QuoteType::RATE_LNVOL, "GOLD", "USD",
84 QuantLib::ext::make_shared<ExpiryPeriod>(2 * Years),
85 QuantLib::ext::make_shared<AtmStrike>(DeltaVolQuote::AtmFwd)),
86 QuantLib::ext::make_shared<CommodityOptionQuote>(0.09, asof, "COMMODITY_OPTION/RATE_LNVOL/GOLD/USD/5Y/ATM/AtmFwd",
87 MarketDatum::QuoteType::RATE_LNVOL, "GOLD", "USD",
88 QuantLib::ext::make_shared<ExpiryPeriod>(5 * Years),
89 QuantLib::ext::make_shared<AtmStrike>(DeltaVolQuote::AtmFwd)),
90 QuantLib::ext::make_shared<CommodityOptionQuote>(
91 0.105, asof, "COMMODITY_OPTION/RATE_LNVOL/GOLD_USD_VOLS/USD/1Y/1150", MarketDatum::QuoteType::RATE_LNVOL,
92 "GOLD", "USD", QuantLib::ext::make_shared<ExpiryPeriod>(1 * Years), QuantLib::ext::make_shared<AbsoluteStrike>(1150)),
93 QuantLib::ext::make_shared<CommodityOptionQuote>(
94 0.115, asof, "COMMODITY_OPTION/RATE_LNVOL/GOLD_USD_VOLS/USD/1Y/1190", MarketDatum::QuoteType::RATE_LNVOL,
95 "GOLD", "USD", QuantLib::ext::make_shared<ExpiryPeriod>(1 * Years), QuantLib::ext::make_shared<AbsoluteStrike>(1190)),
96 QuantLib::ext::make_shared<CommodityOptionQuote>(
97 0.095, asof, "COMMODITY_OPTION/RATE_LNVOL/GOLD_USD_VOLS/USD/2Y/1150", MarketDatum::QuoteType::RATE_LNVOL,
98 "GOLD", "USD", QuantLib::ext::make_shared<ExpiryPeriod>(2 * Years), QuantLib::ext::make_shared<AbsoluteStrike>(1150)),
99 QuantLib::ext::make_shared<CommodityOptionQuote>(
100 0.105, asof, "COMMODITY_OPTION/RATE_LNVOL/GOLD_USD_VOLS/USD/2Y/1190", MarketDatum::QuoteType::RATE_LNVOL,
101 "GOLD", "USD", QuantLib::ext::make_shared<ExpiryPeriod>(2 * Years), QuantLib::ext::make_shared<AbsoluteStrike>(1190)),
102 QuantLib::ext::make_shared<CommodityOptionQuote>(
103 0.085, asof, "COMMODITY_OPTION/RATE_LNVOL/GOLD_USD_VOLS/USD/5Y/1150", MarketDatum::QuoteType::RATE_LNVOL,
104 "GOLD", "USD", QuantLib::ext::make_shared<ExpiryPeriod>(5 * Years), QuantLib::ext::make_shared<AbsoluteStrike>(1150)),
105 QuantLib::ext::make_shared<CommodityOptionQuote>(
106 0.095, asof, "COMMODITY_OPTION/RATE_LNVOL/GOLD_USD_VOLS/USD/5Y/1190", MarketDatum::QuoteType::RATE_LNVOL,
107 "GOLD", "USD", QuantLib::ext::make_shared<ExpiryPeriod>(5 * Years), QuantLib::ext::make_shared<AbsoluteStrike>(1190))};
108}
109
110QuantLib::ext::shared_ptr<TodaysMarket> createTodaysMarket(const Date& asof, const string& inputDir,
111 const string& curveConfigFile,
112 const string& marketFile = "market.txt",
113 const string& fixingsFile = "fixings.txt") {
114
115 auto conventions = QuantLib::ext::make_shared<Conventions>();
116 conventions->fromFile(TEST_INPUT_FILE(string(inputDir + "/conventions.xml")));
117 InstrumentConventions::instance().setConventions(conventions);
118
119 auto curveConfigs = QuantLib::ext::make_shared<CurveConfigurations>();
120 curveConfigs->fromFile(TEST_INPUT_FILE(string(inputDir + "/" + curveConfigFile)));
121
122 auto todaysMarketParameters = QuantLib::ext::make_shared<TodaysMarketParameters>();
123 todaysMarketParameters->fromFile(TEST_INPUT_FILE(string(inputDir + "/todaysmarket.xml")));
124
125 auto loader = QuantLib::ext::make_shared<CSVLoader>(TEST_INPUT_FILE(string(inputDir + "/" + marketFile)),
126 TEST_INPUT_FILE(string(inputDir + "/" + fixingsFile)), false);
127
128 return QuantLib::ext::make_shared<TodaysMarket>(asof, todaysMarketParameters, loader, curveConfigs);
129}
130
131// clang-format off
132// NYMEX input volatility data that is provided in the input market data file
133struct NymexVolatilityData {
134
135 vector<Date> expiries;
136 map<Date, vector<Real>> strikes;
137 map<Date, vector<Real>> volatilities;
138 map<Date, Real> atmVolatilities;
139
140 NymexVolatilityData() {
141 expiries = { Date(17, Oct, 2019), Date(16, Dec, 2019), Date(17, Mar, 2020) };
142 strikes = {
143 { Date(17, Oct, 2019), { 60, 61, 62 } },
144 { Date(16, Dec, 2019), { 59, 60, 61 } },
145 { Date(17, Mar, 2020), { 57, 58, 59 } }
146 };
147 volatilities = {
148 { Date(17, Oct, 2019), { 0.4516, 0.4558, 0.4598 } },
149 { Date(16, Dec, 2019), { 0.4050, 0.4043, 0.4041 } },
150 { Date(17, Mar, 2020), { 0.3599, 0.3573, 0.3545 } }
151 };
152 atmVolatilities = {
153 { Date(17, Oct, 2019), 0.4678 },
154 { Date(16, Dec, 2019), 0.4353 },
155 { Date(17, Mar, 2020), 0.3293 }
156 };
157 }
158};
159
160// clang-format on
161
162} // namespace
163
164BOOST_FIXTURE_TEST_SUITE(OREDataTestSuite, ore::test::TopLevelFixture)
165
166BOOST_AUTO_TEST_SUITE(CommodityVolCurveTests)
167
168BOOST_AUTO_TEST_CASE(testCommodityVolCurveTypeConstant) {
169
170 BOOST_TEST_MESSAGE("Testing commodity vol curve building with a single configured volatility");
171
172 // As of date
173 Date asof(5, Feb, 2016);
174
175 // Constant volatility config
176 vector<QuantLib::ext::shared_ptr<VolatilityConfig>> cvc;
177 cvc.push_back(QuantLib::ext::make_shared<ConstantVolatilityConfig>("COMMODITY_OPTION/RATE_LNVOL/GOLD/USD/2Y/ATM/AtmFwd"));
178
179 // Volatility configuration with a single quote
180 QuantLib::ext::shared_ptr<CommodityVolatilityConfig> curveConfig =
181 QuantLib::ext::make_shared<CommodityVolatilityConfig>("GOLD_USD_VOLS", "", "USD", cvc, "A365", "NullCalendar");
182
183 // Curve configurations
185 curveConfigs.add(CurveSpec::CurveType::CommodityVolatility, "GOLD_USD_VOLS", curveConfig);
186
187 // Commodity curve spec
188 CommodityVolatilityCurveSpec curveSpec("USD", "GOLD_USD_VOLS");
189
190 // Market data loader
191 MockLoader loader;
192
193 // Empty Conventions
194 Conventions conventions;
195
196 // Check commodity volatility construction works
197 QuantLib::ext::shared_ptr<CommodityVolCurve> curve;
198 BOOST_CHECK_NO_THROW(curve = QuantLib::ext::make_shared<CommodityVolCurve>(asof, curveSpec, loader, curveConfigs));
199
200 // Check volatilities are all equal to the configured volatility regardless of strike and expiry
201 Real configuredVolatility = 0.10;
202 QuantLib::ext::shared_ptr<BlackVolTermStructure> volatility = curve->volatility();
203 BOOST_CHECK_CLOSE(volatility->blackVol(0.25, 1000.0), configuredVolatility, testTolerance);
204 BOOST_CHECK_CLOSE(volatility->blackVol(0.25, 1200.0), configuredVolatility, testTolerance);
205 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 3 * Months, 1000.0), configuredVolatility, testTolerance);
206 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 3 * Months, 1200.0), configuredVolatility, testTolerance);
207 BOOST_CHECK_CLOSE(volatility->blackVol(50.0, 1000.0), configuredVolatility, testTolerance);
208 BOOST_CHECK_CLOSE(volatility->blackVol(50.0, 1200.0), configuredVolatility, testTolerance);
209 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 50 * Years, 1000.0), configuredVolatility, testTolerance);
210 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 50 * Years, 1200.0), configuredVolatility, testTolerance);
211}
212
213BOOST_AUTO_TEST_CASE(testCommodityVolCurveTypeCurve) {
214
215 BOOST_TEST_MESSAGE("Testing commodity vol curve building with time dependent volatilities");
216
217 // As of date
218 Date asof(5, Feb, 2016);
219
220 // Quotes for the volatility curve
221 vector<string> quotes{"COMMODITY_OPTION/RATE_LNVOL/GOLD/USD/1Y/ATM/AtmFwd",
222 "COMMODITY_OPTION/RATE_LNVOL/GOLD/USD/2Y/ATM/AtmFwd",
223 "COMMODITY_OPTION/RATE_LNVOL/GOLD/USD/5Y/ATM/AtmFwd"};
224
225 // Volatility curve config with linear interpolation and flat extrapolation.
226 vector<QuantLib::ext::shared_ptr<VolatilityConfig>> vcc;
227 vcc.push_back(QuantLib::ext::make_shared<VolatilityCurveConfig>(quotes, "Linear", "Flat"));
228
229 // Commodity volatility configuration with time dependent volatilities
230 QuantLib::ext::shared_ptr<CommodityVolatilityConfig> curveConfig =
231 QuantLib::ext::make_shared<CommodityVolatilityConfig>("GOLD_USD_VOLS", "", "USD", vcc, "A365", "NullCalendar");
232
233 // Curve configurations
235 curveConfigs.add(CurveSpec::CurveType::CommodityVolatility,"GOLD_USD_VOLS", curveConfig);
236
237 // Commodity curve spec
238 CommodityVolatilityCurveSpec curveSpec("USD", "GOLD_USD_VOLS");
239
240 // Market data loader
241 MockLoader loader;
242
243 // Empty Conventions
244 Conventions conventions;
245
246 // Check commodity volatility construction works
247 QuantLib::ext::shared_ptr<CommodityVolCurve> curve;
248 BOOST_CHECK_NO_THROW(curve = QuantLib::ext::make_shared<CommodityVolCurve>(asof, curveSpec, loader, curveConfigs));
249
250 // Check time depending volatilities are as expected
251 QuantLib::ext::shared_ptr<BlackVolTermStructure> volatility = curve->volatility();
252 Real configuredVolatility;
253
254 // Check configured pillar points: { (1Y, 0.11), (2Y, 0.10), (5Y, 0.09) }
255 // Check also strike independence
256 configuredVolatility = 0.11;
257 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 1 * Years, 1000.0), configuredVolatility, testTolerance);
258 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 1 * Years, 1200.0), configuredVolatility, testTolerance);
259
260 configuredVolatility = 0.10;
261 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 2 * Years, 1000.0), configuredVolatility, testTolerance);
262 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 2 * Years, 1200.0), configuredVolatility, testTolerance);
263
264 configuredVolatility = 0.09;
265 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 5 * Years, 1000.0), configuredVolatility, testTolerance);
266 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 5 * Years, 1200.0), configuredVolatility, testTolerance);
267
268 // Check briefly the default linear interpolation and extrapolation
269 Time t_s = volatility->dayCounter().yearFraction(asof, asof + 2 * Years);
270 Real v_s = 0.10;
271 Time t_e = volatility->dayCounter().yearFraction(asof, asof + 5 * Years);
272 Real v_e = 0.09;
273 // at 3 years
274 Time t = volatility->dayCounter().yearFraction(asof, asof + 3 * Years);
275 Real v = sqrt((v_s * v_s * t_s + (v_e * v_e * t_e - v_s * v_s * t_s) * (t - t_s) / (t_e - t_s)) / t);
276 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 3 * Years, 1000.0), v, testTolerance);
277 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 3 * Years, 1200.0), v, testTolerance);
278 // at 6 years, extrapolation is with a flat vol
279 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 6 * Years, 1000.0), v_e, testTolerance);
280 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 6 * Years, 1200.0), v_e, testTolerance);
281}
282
283BOOST_AUTO_TEST_CASE(testCommodityVolCurveTypeSurface) {
284
285 BOOST_TEST_MESSAGE("Testing commodity vol curve building with time and strike dependent volatilities");
286
287 // As of date
288 Date asof(5, Feb, 2016);
289
290 // Volatility configuration with expiry period vs. absolute strike matrix. Bilinear interpolation and flat
291 // extrapolation.
292 vector<string> strikes{"1150", "1190"};
293 vector<string> expiries{"1Y", "2Y", "5Y"};
294
295 vector<QuantLib::ext::shared_ptr<VolatilityConfig>> vssc;
296 vssc.push_back(
297 QuantLib::ext::make_shared<VolatilityStrikeSurfaceConfig>(strikes, expiries, "Linear", "Linear", true, "Flat", "Flat"));
298
299 // Commodity volatility configuration
300 QuantLib::ext::shared_ptr<CommodityVolatilityConfig> curveConfig =
301 QuantLib::ext::make_shared<CommodityVolatilityConfig>("GOLD_USD_VOLS", "", "USD", vssc, "A365", "NullCalendar");
302
303 // Curve configurations
305 curveConfigs.add(CurveSpec::CurveType::CommodityVolatility, "GOLD_USD_VOLS", curveConfig);
306
307 // Commodity curve spec
308 CommodityVolatilityCurveSpec curveSpec("USD", "GOLD_USD_VOLS");
309
310 // Market data loader
311 MockLoader loader;
312
313 // Empty Conventions
314 Conventions conventions;
315
316 // Check commodity volatility construction works
317 QuantLib::ext::shared_ptr<CommodityVolCurve> curve;
318 BOOST_CHECK_NO_THROW(curve = QuantLib::ext::make_shared<CommodityVolCurve>(asof, curveSpec, loader, curveConfigs));
319
320 // Check time and strike depending volatilities are as expected
321 QuantLib::ext::shared_ptr<BlackVolTermStructure> volatility = curve->volatility();
322
323 // Check configured pillar points
324 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 1 * Years, 1150.0), 0.105, testTolerance);
325 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 1 * Years, 1190.0), 0.115, testTolerance);
326 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 2 * Years, 1150.0), 0.095, testTolerance);
327 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 2 * Years, 1190.0), 0.105, testTolerance);
328 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 5 * Years, 1150.0), 0.085, testTolerance);
329 BOOST_CHECK_CLOSE(volatility->blackVol(asof + 5 * Years, 1190.0), 0.095, testTolerance);
330}
331
332BOOST_AUTO_TEST_CASE(testCommodityVolSurfaceWildcardExpiriesWildcardStrikes) {
333
334 // Testing commodity volatility curve building wildcard expiries and strikes in configuration and more than one
335 // set of commodity volatility quotes in the market data. In particular, the market data in the wildcard_data
336 // folder has commodity volatility data for two surfaces NYMEX:CL and ICE:B. Check here that the commodity
337 // volatility curve building for NYMEX:CL uses only the 9 NYMEX:CL quotes - 3 tenors, each with 3 strikes.
338 BOOST_TEST_MESSAGE("Testing commodity volatility curve building wildcard expiries and strikes in configuration");
339
340 auto todaysMarket =
341 createTodaysMarket(Date(16, Sep, 2019), "wildcard_data", "curveconfig_surface_wc_expiries_wc_strikes.xml");
342
343 auto vts = todaysMarket->commodityVolatility("NYMEX:CL");
344
345 // Wildcards in configuration so we know that a BlackVarianceSurfaceSparse has been created and fed to a
346 // BlackVolatilityWithATM surface in TodaysMarket
347 auto tmSurface = QuantLib::ext::dynamic_pointer_cast<BlackVolatilityWithATM>(*vts);
348 BOOST_REQUIRE_MESSAGE(tmSurface, "Expected the commodity vol structure in TodaysMarket"
349 << " to be of type BlackVolatilityWithATM");
350 auto surface = QuantLib::ext::dynamic_pointer_cast<BlackVarianceSurfaceSparse>(tmSurface->surface());
351 BOOST_REQUIRE_MESSAGE(tmSurface, "Expected the commodity vol structure in TodaysMarket to contain"
352 << " a surface of type BlackVarianceSurfaceSparse");
353
354 // The expected NYMEX CL volatility data
355 NymexVolatilityData expData;
356
357 // Check what is loaded against expected data as provided in market data file for NYMEX:CL.
358 // Note: the BlackVarianceSurfaceSparse adds a dummy expiry slice at time zero
359 BOOST_REQUIRE_EQUAL(surface->expiries().size() - 1, expData.expiries.size());
360 for (Size i = 0; i < expData.strikes.size(); i++) {
361 BOOST_CHECK_EQUAL(surface->expiries()[i + 1], expData.expiries[i]);
362 }
363
364 BOOST_REQUIRE_EQUAL(surface->strikes().size() - 1, expData.strikes.size());
365 for (Size i = 0; i < expData.strikes.size(); i++) {
366
367 // Check the strikes against the input
368 Date e = expData.expiries[i];
369 BOOST_REQUIRE_EQUAL(surface->strikes()[i + 1].size(), expData.strikes[e].size());
370 for (Size j = 0; j < surface->strikes()[i + 1].size(); j++) {
371 BOOST_CHECK_CLOSE(expData.strikes[e][j], surface->strikes()[i + 1][j], 1e-12);
372 }
373
374 // Check the volatilities against the input
375 BOOST_REQUIRE_EQUAL(surface->values()[i + 1].size(), expData.volatilities[e].size());
376 for (Size j = 0; j < surface->values()[i + 1].size(); j++) {
377 BOOST_CHECK_CLOSE(expData.volatilities[e][j], surface->blackVol(e, surface->strikes()[i + 1][j]), 1e-12);
378 }
379 }
380}
381
382BOOST_AUTO_TEST_CASE(testCommodityVolSurfaceWildcardExpiriesExplicitStrikes) {
383
384 BOOST_TEST_MESSAGE(
385 "Testing commodity volatility curve building wildcard expiries and explicit strikes in configuration");
386
387 auto todaysMarket = createTodaysMarket(Date(16, Sep, 2019), "wildcard_data",
388 "curveconfig_surface_wc_expiries_explicit_strikes.xml");
389
390 auto vts = todaysMarket->commodityVolatility("NYMEX:CL");
391
392 // Wildcards in configuration so we know that a BlackVarianceSurfaceSparse has been created and fed to a
393 // BlackVolatilityWithATM surface in TodaysMarket
394 auto tmSurface = QuantLib::ext::dynamic_pointer_cast<BlackVolatilityWithATM>(*vts);
395 BOOST_REQUIRE_MESSAGE(tmSurface, "Expected the commodity vol structure in TodaysMarket"
396 << " to be of type BlackVolatilityWithATM");
397 auto surface = QuantLib::ext::dynamic_pointer_cast<BlackVarianceSurfaceSparse>(tmSurface->surface());
398 BOOST_REQUIRE_MESSAGE(tmSurface, "Expected the commodity vol structure in TodaysMarket to contain"
399 << " a surface of type BlackVarianceSurfaceSparse");
400
401 // The expected NYMEX CL volatility data
402 NymexVolatilityData expData;
403
404 // Check what is loaded against expected data as provided in market data file for NYMEX:CL. The explicit strikes
405 // that we have chosen lead have only two corresponding expiries i.e. 2019-10-17 and 2019-12-16
406 // Note: the BlackVarianceSurfaceSparse adds a dummy expiry slice at time zero
407 vector<Date> expExpiries{Date(17, Oct, 2019), Date(16, Dec, 2019)};
408 BOOST_REQUIRE_EQUAL(surface->expiries().size() - 1, expExpiries.size());
409 for (Size i = 0; i < expExpiries.size(); i++) {
410 BOOST_CHECK_EQUAL(surface->expiries()[i + 1], expExpiries[i]);
411 }
412
413 // The explicit strikes in the config are 60 and 61
414 vector<Real> expStrikes{60, 61};
415 BOOST_REQUIRE_EQUAL(surface->strikes().size() - 1, expExpiries.size());
416 for (Size i = 0; i < expExpiries.size(); i++) {
417
418 // Check the strikes against the expected explicit strikes
419 BOOST_REQUIRE_EQUAL(surface->strikes()[i + 1].size(), expStrikes.size());
420 for (Size j = 0; j < surface->strikes()[i + 1].size(); j++) {
421 BOOST_CHECK_CLOSE(expStrikes[j], surface->strikes()[i + 1][j], 1e-12);
422 }
423
424 // Check the volatilities against the input
425 Date e = expExpiries[i];
426 BOOST_REQUIRE_EQUAL(surface->values()[i + 1].size(), expStrikes.size());
427 for (Size j = 0; j < surface->values()[i + 1].size(); j++) {
428 // Find the index of the explicit strike in the input data
429 Real expStrike = expStrikes[j];
430 auto it = find_if(expData.strikes[e].begin(), expData.strikes[e].end(),
431 [expStrike](Real s) { return close(expStrike, s); });
432 BOOST_REQUIRE_MESSAGE(it != expData.strikes[e].end(), "Strike not found in input strikes");
433 auto idx = distance(expData.strikes[e].begin(), it);
434
435 BOOST_CHECK_CLOSE(expData.volatilities[e][idx], surface->blackVol(e, surface->strikes()[i + 1][j]), 1e-12);
436 }
437 }
438}
439
440BOOST_AUTO_TEST_CASE(testCommodityVolSurfaceExplicitExpiriesWildcardStrikes) {
441
442 BOOST_TEST_MESSAGE(
443 "Testing commodity volatility curve building explicit expiries and wildcard strikes in configuration");
444
445 auto todaysMarket = createTodaysMarket(Date(16, Sep, 2019), "wildcard_data",
446 "curveconfig_surface_explicit_expiries_wc_strikes.xml");
447
448 auto vts = todaysMarket->commodityVolatility("NYMEX:CL");
449
450 // Wildcards in configuration so we know that a BlackVarianceSurfaceSparse has been created and fed to a
451 // BlackVolatilityWithATM surface in TodaysMarket
452 auto tmSurface = QuantLib::ext::dynamic_pointer_cast<BlackVolatilityWithATM>(*vts);
453 BOOST_REQUIRE_MESSAGE(tmSurface, "Expected the commodity vol structure in TodaysMarket"
454 << " to be of type BlackVolatilityWithATM");
455 auto surface = QuantLib::ext::dynamic_pointer_cast<BlackVarianceSurfaceSparse>(tmSurface->surface());
456 BOOST_REQUIRE_MESSAGE(tmSurface, "Expected the commodity vol structure in TodaysMarket to contain"
457 << " a surface of type BlackVarianceSurfaceSparse");
458
459 // The expected NYMEX CL volatility data
460 NymexVolatilityData expData;
461
462 // Check what is loaded against expected data as provided in market data file for NYMEX:CL.
463 // We have chosen the explicit expiries 2019-10-17 and 2019-12-16
464 // Note: the BlackVarianceSurfaceSparse adds a dummy expiry slice at time zero
465 vector<Date> expExpiries{Date(17, Oct, 2019), Date(16, Dec, 2019)};
466 BOOST_REQUIRE_EQUAL(surface->expiries().size() - 1, expExpiries.size());
467 for (Size i = 0; i < expExpiries.size(); i++) {
468 BOOST_CHECK_EQUAL(surface->expiries()[i + 1], expExpiries[i]);
469 }
470
471 BOOST_REQUIRE_EQUAL(surface->strikes().size() - 1, expExpiries.size());
472 for (Size i = 0; i < expExpiries.size(); i++) {
473
474 // Check the strikes against the input
475 Date e = expExpiries[i];
476 BOOST_REQUIRE_EQUAL(surface->strikes()[i + 1].size(), expData.strikes[e].size());
477 for (Size j = 0; j < surface->strikes()[i + 1].size(); j++) {
478 BOOST_CHECK_CLOSE(expData.strikes[e][j], surface->strikes()[i + 1][j], 1e-12);
479 }
480
481 // Check the volatilities against the input
482 BOOST_REQUIRE_EQUAL(surface->values()[i + 1].size(), expData.volatilities[e].size());
483 for (Size j = 0; j < surface->values()[i + 1].size(); j++) {
484 BOOST_CHECK_CLOSE(expData.volatilities[e][j], surface->blackVol(e, surface->strikes()[i + 1][j]), 1e-12);
485 }
486 }
487}
488
489BOOST_AUTO_TEST_CASE(testCommodityVolSurfaceExplicitExpiriesExplicitStrikes) {
490
491 BOOST_TEST_MESSAGE(
492 "Testing commodity volatility curve building explicit expiries and explicit strikes in configuration");
493
494 auto todaysMarket = createTodaysMarket(Date(16, Sep, 2019), "wildcard_data",
495 "curveconfig_surface_explicit_expiries_explicit_strikes.xml");
496
497 auto vts = todaysMarket->commodityVolatility("NYMEX:CL");
498
499 // The expected NYMEX CL volatility data
500 NymexVolatilityData expData;
501
502 // We have provided two explicit expiries, 2019-10-17 and 2019-12-16, and two explicit strikes, 60 and 61.
503 // We check the volatility term structure at these 4 points against the input data
504 vector<Date> expExpiries{Date(17, Oct, 2019), Date(16, Dec, 2019)};
505 vector<Real> expStrikes{60, 61};
506 for (const Date& e : expExpiries) {
507 for (Real s : expStrikes) {
508 // Find the index of the explicit strike in the input data
509 auto it = find_if(expData.strikes[e].begin(), expData.strikes[e].end(),
510 [s](Real strike) { return close(strike, s); });
511 BOOST_REQUIRE_MESSAGE(it != expData.strikes[e].end(), "Strike not found in input strikes");
512 auto idx = distance(expData.strikes[e].begin(), it);
513
514 Real inputVol = expData.volatilities[e][idx];
515 BOOST_CHECK_CLOSE(inputVol, vts->blackVol(e, s), 1e-12);
516 }
517 }
518}
519
520// As of dates for delta surface building below.
521// 15 Jan is when c1 contract rolls from NYMEX WTI Feb option contract to NYMEX WTI Mar option contract
522vector<Date> asofDates{Date(13, Jan, 2020), Date(15, Jan, 2020)};
523
524// Different curve configurations for the surface building below.
525vector<string> curveConfigs{"curveconfig_explicit_expiries.xml", "curveconfig_wildcard_expiries.xml"};
526
527BOOST_DATA_TEST_CASE(testCommodityVolDeltaSurface, bdata::make(asofDates) * bdata::make(curveConfigs), asof,
528 curveConfig) {
529
530 BOOST_TEST_MESSAGE("Testing commodity volatility delta surface building");
531
532 auto todaysMarket = createTodaysMarket(asof, "delta_surface", curveConfig, "market.txt");
533
534 // Get the built commodity volatility surface
535 auto vts = todaysMarket->commodityVolatility("NYMEX:CL");
536
537 // Cast to expected type and check that it succeeds
538 // For some reason, todaysmarket wraps the surface built in CommodityVolCurve in a BlackVolatilityWithATM.
539 auto bvwa = dynamic_pointer_cast<BlackVolatilityWithATM>(*vts);
540 BOOST_REQUIRE(bvwa);
541 auto bvsd = QuantLib::ext::dynamic_pointer_cast<BlackVolatilitySurfaceDelta>(bvwa->surface());
542 BOOST_REQUIRE(bvsd);
543
544 // Tolerance for float comparison
545 Real tol = 1e-12;
546
547 // Read in the expected on-grid results for the given date.
548 string filename = "delta_surface/expected_grid_" + to_string(io::iso_date(asof)) + ".csv";
549 CSVFileReader reader(TEST_INPUT_FILE(filename), true, ",");
550 BOOST_REQUIRE_EQUAL(reader.numberOfColumns(), 3);
551
552 while (reader.next()) {
553
554 // Get the expected expiry date, strike and volatility grid point
555 Date expiryDate = parseDate(reader.get(0));
556 Real strike = parseReal(reader.get(1));
557 Real volatility = parseReal(reader.get(2));
558
559 // Check that the expected grid expiry date is one of the surface dates
560 auto itExpiry = find(bvsd->dates().begin(), bvsd->dates().end(), expiryDate);
561 BOOST_REQUIRE(itExpiry != bvsd->dates().end());
562
563 // Get the smile section, cast to expected type and check cast succeeds.
564 auto fxss = bvsd->blackVolSmile(expiryDate);
565 auto iss = QuantLib::ext::dynamic_pointer_cast<InterpolatedSmileSection>(fxss);
566 BOOST_REQUIRE(iss);
567
568 // Check that the expected grid strike is one of the smile section strikes.
569 auto itStrike =
570 find_if(iss->strikes().begin(), iss->strikes().end(), [&](Real s) { return std::abs(s - strike) < tol; });
571 BOOST_REQUIRE(itStrike != iss->strikes().end());
572
573 // Check that the expected volatility is equal to that at the grid strike
574 auto pos = distance(iss->strikes().begin(), itStrike);
575 BOOST_CHECK_SMALL(volatility - iss->volatilities()[pos], tol);
576 }
577
578 // Check flat time extrapolation
579 auto fxss = bvsd->blackVolSmile(bvsd->dates().back());
580 auto iss = QuantLib::ext::dynamic_pointer_cast<InterpolatedSmileSection>(fxss);
581 BOOST_REQUIRE(iss);
582 vector<Real> lastVolatilities = iss->volatilities();
583
584 fxss = bvsd->blackVolSmile(bvsd->dates().back() + 1 * Years);
585 iss = QuantLib::ext::dynamic_pointer_cast<InterpolatedSmileSection>(fxss);
586 BOOST_REQUIRE(iss);
587 vector<Real> extrapVolatilities = iss->volatilities();
588
589 BOOST_REQUIRE_EQUAL(lastVolatilities.size(), extrapVolatilities.size());
590 for (Size i = 0; i < lastVolatilities.size(); i++) {
591 BOOST_CHECK_SMALL(lastVolatilities[i] - extrapVolatilities[i], tol);
592 }
593
594 // Check flat strike extrapolation.
595 Date testDate = asof + 1 * Years;
596
597 fxss = bvsd->blackVolSmile(testDate);
598 iss = QuantLib::ext::dynamic_pointer_cast<InterpolatedSmileSection>(fxss);
599 BOOST_REQUIRE(iss);
600
601 Volatility volAtMinStrike = iss->volatilities().front();
602 Real minStrike = iss->strikes().front();
603 Real extrapStrike = minStrike / 2.0;
604 BOOST_CHECK_SMALL(volAtMinStrike - bvsd->blackVol(testDate, extrapStrike), tol);
605
606 Volatility volAtMaxStrike = iss->volatilities().back();
607 Real maxStrike = iss->strikes().back();
608 extrapStrike = maxStrike * 2.0;
609 BOOST_CHECK_SMALL(volAtMaxStrike - bvsd->blackVol(testDate, extrapStrike), tol);
610}
611
612BOOST_DATA_TEST_CASE(testCommodityVolMoneynessSurface, bdata::make(asofDates) * bdata::make(curveConfigs), asof,
613 curveConfig) {
614
615 BOOST_TEST_MESSAGE("Testing commodity volatility forward moneyness surface building");
616
617 Settings::instance().evaluationDate() = asof;
618 auto todaysMarket = createTodaysMarket(asof, "moneyness_surface", curveConfig, "market.txt");
619
620 // Get the built commodity volatility surface
621 auto vts = todaysMarket->commodityVolatility("NYMEX:CL");
622
623 // Tolerance for float comparison
624 Real tol = 1e-12;
625
626 // Read in the expected on-grid results for the given date.
627 string filename = "moneyness_surface/expected_grid_" + to_string(io::iso_date(asof)) + ".csv";
628 CSVFileReader reader(TEST_INPUT_FILE(filename), true, ",");
629 BOOST_REQUIRE_EQUAL(reader.numberOfColumns(), 3);
630
631 while (reader.next()) {
632
633 // Get the expected expiry date, strike and volatility grid point
634 Date expiryDate = parseDate(reader.get(0));
635 Real strike = parseReal(reader.get(1));
636 Real volatility = parseReal(reader.get(2));
637
638 // Check the surface on the grid point.
639 BOOST_CHECK_SMALL(volatility - vts->blackVol(expiryDate, strike), tol);
640 }
641
642 // Price term structure
643 auto pts = todaysMarket->commodityPriceCurve("NYMEX:CL");
644
645 // Pick two future expiries beyond max vol surface time and get their prices
646 // This should correspond to moneyness 1.0 and we should see flat vol extrapolation.
647 // This only passes because we have set sticky strike to false in CommodityVolCurve.
648 Date extrapDate_1(20, Mar, 2024);
649 Date extrapDate_2(22, Apr, 2024);
650 Real strike_1 = pts->price(extrapDate_1);
651 Real strike_2 = pts->price(extrapDate_2);
652 Volatility vol_1 = vts->blackVol(extrapDate_1, strike_1);
653 Volatility vol_2 = vts->blackVol(extrapDate_2, strike_2);
654 BOOST_TEST_MESSAGE("The two time extrapolated volatilities are: " << fixed << setprecision(12) << vol_1 << ","
655 << vol_2 << ".");
656 BOOST_CHECK_SMALL(vol_1 - vol_2, tol);
657
658 // Test flat strike extrapolation at lower and upper strikes i.e. at 50% and 150% forward moneyness.
659 Date optionExpiry(14, Jan, 2021);
660 Date futureExpiry(20, Jan, 2021);
661 Real futurePrice = pts->price(futureExpiry);
662
663 Real lowerStrike = 0.5 * futurePrice;
664 Volatility volLowerStrike = vts->blackVol(optionExpiry, lowerStrike);
665 Volatility volLowerExtrapStrike = vts->blackVol(optionExpiry, lowerStrike / 2.0);
666 BOOST_TEST_MESSAGE("The two lower strike extrapolated volatilities are: "
667 << fixed << setprecision(12) << volLowerStrike << "," << volLowerExtrapStrike << ".");
668 BOOST_CHECK_SMALL(volLowerStrike - volLowerExtrapStrike, tol);
669
670 Real upperStrike = 1.5 * futurePrice;
671 Volatility volUpperStrike = vts->blackVol(optionExpiry, upperStrike);
672 Volatility volUpperExtrapStrike = vts->blackVol(optionExpiry, upperStrike * 2.0);
673 BOOST_TEST_MESSAGE("The two upper strike extrapolated volatilities are: "
674 << fixed << setprecision(12) << volUpperStrike << "," << volUpperExtrapStrike << ".");
675 BOOST_CHECK_SMALL(volUpperStrike - volUpperExtrapStrike, tol);
676}
677
678BOOST_DATA_TEST_CASE(testCommodityApoSurface, bdata::make(asofDates), asof) {
679
680 BOOST_TEST_MESSAGE("Testing commodity volatility forward moneyness surface building");
681
682 Settings::instance().evaluationDate() = asof;
683
684 string fixingsFile = "fixings_" + to_string(io::iso_date(asof)) + ".txt";
685 auto todaysMarket = createTodaysMarket(asof, "apo_surface", "curveconfig.xml", "market.txt", fixingsFile);
686
687 // Get the built commodity volatility surface
688 auto vts = todaysMarket->commodityVolatility("NYMEX:FF");
689
690 // Tolerance for float comparison
691 Real tol = 1e-12;
692
693 // Read in the expected on-grid results for the given date.
694 string filename = "apo_surface/expected_grid_" + to_string(io::iso_date(asof)) + ".csv";
695 CSVFileReader reader(TEST_INPUT_FILE(filename), true, ",");
696 BOOST_REQUIRE_EQUAL(reader.numberOfColumns(), 3);
697
698 BOOST_TEST_MESSAGE("exp_vol,calc_vol,difference");
699 while (reader.next()) {
700
701 // Get the expected expiry date, strike and volatility grid point
702 Date expiryDate = parseDate(reader.get(0));
703 Real strike = parseReal(reader.get(1));
704 Real volatility = parseReal(reader.get(2));
705
706 // Check the surface on the grid point.
707 auto calcVolatility = vts->blackVol(expiryDate, strike);
708 auto difference = volatility - calcVolatility;
709 BOOST_TEST_MESSAGE(std::fixed << std::setprecision(12) << strike << "," << volatility << "," << calcVolatility
710 << "," << difference);
711 BOOST_CHECK_SMALL(difference, tol);
712 }
713}
714
715// Main point of this test is to test that the option expiries are interpreted correctly.
716BOOST_AUTO_TEST_CASE(testCommodityVolSurfaceMyrCrudePalmOil) {
717
718 BOOST_TEST_MESSAGE("Testing commodity volatility delta surface building for MYR Crude Palm Oil");
719
720 Date asof(14, Oct, 2020);
721 auto todaysMarket = createTodaysMarket(asof, "myr_crude_palm_oil", "curveconfig.xml", "market.txt");
722
723 // Get the built commodity volatility surface
724 auto vts = todaysMarket->commodityVolatility("XKLS:FCPO");
725
726 // Check that it built ok i.e. can query a volatility.
727 BOOST_CHECK_NO_THROW(vts->blackVol(1.0, 2800));
728
729 // Cast to expected type and check that it succeeds
730 // For some reason, todaysmarket wraps the surface built in CommodityVolCurve in a BlackVolatilityWithATM.
731 auto bvwa = dynamic_pointer_cast<BlackVolatilityWithATM>(*vts);
732 BOOST_REQUIRE(bvwa);
733 auto bvsd = QuantLib::ext::dynamic_pointer_cast<BlackVolatilitySurfaceDelta>(bvwa->surface());
734 BOOST_REQUIRE(bvsd);
735
736 // Now check that the surface dates are as expected.
737
738 // Calculated surface dates.
739 const vector<Date>& surfaceDates = bvsd->dates();
740
741 // Read in the expected dates.
742 vector<Date> expectedDates;
743 string filename = "myr_crude_palm_oil/expected_expiries.csv";
744 CSVFileReader reader(TEST_INPUT_FILE(filename), true, ",");
745 BOOST_REQUIRE_EQUAL(reader.numberOfColumns(), 1);
746 while (reader.next()) {
747 expectedDates.push_back(parseDate(reader.get(0)));
748 }
749
750 // Check equal.
751 BOOST_CHECK_EQUAL_COLLECTIONS(surfaceDates.begin(), surfaceDates.end(), expectedDates.begin(), expectedDates.end());
752}
753
754BOOST_AUTO_TEST_SUITE_END()
755
756BOOST_AUTO_TEST_SUITE_END()
Size numberOfColumns() const
std::string get(const std::string &field) const
Commodity volatility description.
Definition: curvespec.hpp:440
Repository for currency dependent market conventions.
Container class for all Curve Configurations.
Market data loader base class.
Definition: loader.hpp:47
Wrapper class for building commodity volatility structures.
SafeStack< ValueType > value
utility class to access CSV files
Market Datum Loader Implementation.
Curve configuration repository.
Curve requirements specification.
Date parseDate(const string &s)
Convert std::string to QuantLib::Date.
Definition: parsers.cpp:51
Real parseReal(const string &s)
Convert text to Real.
Definition: parsers.cpp:112
Market Datum Loader Interface.
RandomVariable sqrt(RandomVariable x)
Size size(const ValueType &v)
Definition: value.cpp:145
std::string to_string(const LocationInfo &l)
Definition: ast.cpp:28
Map text representations to QuantLib/QuantExt types.
vector< Real > strikes
vector< Date > asofDates
BOOST_AUTO_TEST_CASE(testCommodityVolCurveTypeConstant)
BOOST_DATA_TEST_CASE(testCommodityVolDeltaSurface, bdata::make(asofDates) *bdata::make(curveConfigs), asof, curveConfig)
vector< string > curveConfigs
string conversion utilities
An concrete implementation of the Market class that loads todays market and builds the required curve...
string name