Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
commodityvolcurve.cpp
Go to the documentation of this file.
1/*
2Copyright (C) 2018 Quaternion Risk Management Ltd
3Copyright (C) 2022 Skandinaviska Enskilda Banken AB (publ)
4All rights reserved.
5
6This file is part of ORE, a free-software/open-source library
7for transparent pricing and risk analysis - http://opensourcerisk.org
8
9ORE is free software: you can redistribute it and/or modify it
10under the terms of the Modified BSD License. You should have received a
11copy of the license along with this program.
12The license is also available online at <http://opensourcerisk.org>
13
14This program is distributed on the basis that it will form a useful
15contribution to risk analytics and model standardisation, but WITHOUT
16ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
17FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
18*/
19
20#include <algorithm>
21#include <boost/algorithm/string/join.hpp>
22#include <boost/algorithm/string/replace.hpp>
23#include <boost/range/adaptor/indexed.hpp>
24#include <boost/range/adaptor/transformed.hpp>
32#include <ql/math/interpolations/bicubicsplineinterpolation.hpp>
33#include <ql/math/interpolations/loginterpolation.hpp>
34#include <ql/math/matrix.hpp>
35#include <ql/termstructures/volatility/equityfx/blackconstantvol.hpp>
36#include <ql/termstructures/volatility/equityfx/blackvariancecurve.hpp>
37#include <ql/termstructures/volatility/equityfx/blackvariancesurface.hpp>
38#include <ql/time/calendars/weekendsonly.hpp>
50#include <ql/pricingengines/blackformula.hpp>
52
53using namespace std;
54using namespace QuantLib;
55using namespace QuantExt;
56
57namespace {
58
59// Utility struct to represent (expiry, strike) pair when storing volatilities below in map.
60struct ExpiryStrike {
61 ExpiryStrike(const Date& exp = Date(), Real stk = Null<Real>()) : expiry(exp), strike(stk) {}
62 Date expiry;
63 Real strike;
64};
65
66ostream& operator<<(ostream& os, const ExpiryStrike& es) {
67 return os << "(" << io::iso_date(es.expiry) << "," << es.strike << ")";
68}
69
70// Comparator so that ExpiryStrike may be used as a map key.
71struct ExpiryStrikeComp {
72 bool operator() (const ExpiryStrike& lhs, const ExpiryStrike& rhs) const {
73 return (lhs.expiry < rhs.expiry) || (!(rhs.expiry < lhs.expiry) &&
74 !close(lhs.strike, rhs.strike) && (lhs.strike < rhs.strike));
75 }
76};
77
78// Struct that stores a call data point (price or vol) and a put data point (price or vol).
79// Null<Real>() indicates the absence of the data point.
80struct CallPutDatum {
81 CallPutDatum() : call(Null<Real>()), put(Null<Real>()) {}
82
83 CallPutDatum(Option::Type optionType, Real value)
84 : call(Null<Real>()), put(Null<Real>()) {
85 if (optionType == Option::Call) {
86 call = value;
87 } else {
88 put = value;
89 }
90 }
91
92 Real call;
93 Real put;
94};
95
96// Container that stores all the relevant points.
97struct CallPutData {
98
99 void addDatum(ExpiryStrike node, Option::Type optionType, Real value) {
100
101 auto it = data.find(node);
102 if (it != data.end()) {
103 // Already have call or put data for the (expiry, strike)
104 CallPutDatum& cpd = it->second;
105 Real& toUpdate = optionType == Option::Call ? cpd.call : cpd.put;
106 if (toUpdate == Null<Real>()) {
107 toUpdate = value;
108 TLOG("Updated " << optionType << " option data point with value " << fixed <<
109 setprecision(9) << value << " for expiry strike pair " << node << ".");
110 } else {
111 TLOG("Expiry strike pair " << node << " already has " << optionType << " option data " <<
112 "point with value " << fixed << setprecision(9) << toUpdate <<
113 " so did not update with value " << value << ".");
114 }
115 } else {
116 // Have no call or put data for the (expiry, strike)
117 data[node] = CallPutDatum(optionType, value);
118 TLOG("Added " << optionType << " option data point with value " << fixed << setprecision(9) <<
119 value << " for expiry strike pair " << node << ".");
120 }
121 }
122
123 map<ExpiryStrike, CallPutDatum, ExpiryStrikeComp> data;
124};
125
126// Return the relevant surface using the data
127QuantLib::ext::shared_ptr<OptionPriceSurface> optPriceSurface(const CallPutData& cpData,
128 const Date& asof, const DayCounter& dc, bool forCall) {
129
130 DLOG("Creating " << (forCall ? "Call" : "Put") << " option price surface.");
131
132 const auto& priceData = cpData.data;
133 auto n = priceData.size();
134
135 vector<Date> expiries; expiries.reserve(n);
136 vector<Real> strikes; strikes.reserve(n);
137 vector<Real> prices; prices.reserve(n);
138
139 for (const auto& kv : cpData.data) {
140
141 if ((forCall && kv.second.call != Null<Real>()) || (!forCall && kv.second.put != Null<Real>())) {
142
143 expiries.push_back(kv.first.expiry);
144 strikes.push_back(kv.first.strike);
145 prices.push_back(forCall ? kv.second.call : kv.second.put);
146
147 TLOG("Using option datum (" << (forCall ? "Call" : "Put") << "," << io::iso_date(expiries.back()) <<
148 "," << fixed << setprecision(9) << strikes.back() << "," << prices.back() << ")");
149 }
150 }
151
152 // This is not enough but OptionPriceSurface should throw if data is not sufficient.
153 QL_REQUIRE(!prices.empty(), "Need at least one point for " << (forCall ? "Call" : "Put") <<
154 " commodity option price surface.");
155
156 return QuantLib::ext::make_shared<OptionPriceSurface>(asof, expiries, strikes, prices, dc);
157}
158
159}
160
161namespace ore {
162namespace data {
163
164CommodityVolCurve::CommodityVolCurve(const Date& asof, const CommodityVolatilityCurveSpec& spec, const Loader& loader,
165 const CurveConfigurations& curveConfigs,
166 const map<string, QuantLib::ext::shared_ptr<YieldCurve>>& yieldCurves,
167 const map<string, QuantLib::ext::shared_ptr<CommodityCurve>>& commodityCurves,
168 const map<string, QuantLib::ext::shared_ptr<CommodityVolCurve>>& commodityVolCurves,
169 const map<string, QuantLib::ext::shared_ptr<FXVolCurve>>& fxVolCurves,
170 const map<string, QuantLib::ext::shared_ptr<CorrelationCurve>>& correlationCurves,
171 const Market* fxIndices, const bool buildCalibrationInfo) {
172
173 try {
174 LOG("CommodityVolCurve: start building commodity volatility structure with ID " << spec.curveConfigID());
175
176 auto config = *curveConfigs.commodityVolatilityConfig(spec.curveConfigID());
177
178 QuantLib::ext::shared_ptr<Conventions> conventions = InstrumentConventions::instance().conventions();
179
180 if (!config.futureConventionsId().empty()) {
181 const auto& cId = config.futureConventionsId();
182 QL_REQUIRE(conventions->has(cId),
183 "Conventions, " << cId << " for config " << config.curveID() << " not found.");
184 convention_ = QuantLib::ext::dynamic_pointer_cast<CommodityFutureConvention>(conventions->get(cId));
185 QL_REQUIRE(convention_, "Convention with ID '" << cId << "' should be of type CommodityFutureConvention");
186 expCalc_ = QuantLib::ext::make_shared<ConventionsBasedFutureExpiry>(*convention_);
187 }
188
189 calendar_ = parseCalendar(config.calendar());
190 dayCounter_ = parseDayCounter(config.dayCounter());
191
192 // loop over the volatility configs attempting to build in the order provided
193 DLOG("CommodityVolCurve: Attempting to build commodity vol curve from volatilityConfig, "
194 << config.volatilityConfig().size() << " volatility configs provided.");
195 for (auto vc : config.volatilityConfig()) {
196 try {
197 // if the volatility config has it's own calendar, we use that.
198 if (!vc->calendar().empty())
199 calendar_ = vc->calendar();
200 // Do different things depending on the type of volatility configured
201 if (auto eqvc = QuantLib::ext::dynamic_pointer_cast<ProxyVolatilityConfig>(vc)) {
202 buildVolatility(asof, spec, curveConfigs, *eqvc, commodityCurves, commodityVolCurves, fxVolCurves,
203 correlationCurves, fxIndices);
204 } else if (auto qvc = QuantLib::ext::dynamic_pointer_cast<QuoteBasedVolatilityConfig>(vc)) {
205
206 if (auto cvc = QuantLib::ext::dynamic_pointer_cast<ConstantVolatilityConfig>(vc)) {
207 buildVolatility(asof, config, *cvc, loader);
208 } else if (auto vcc = QuantLib::ext::dynamic_pointer_cast<VolatilityCurveConfig>(vc)) {
209 buildVolatility(asof, config, *vcc, loader);
210 } else if (auto vssc = QuantLib::ext::dynamic_pointer_cast<VolatilityStrikeSurfaceConfig>(vc)) {
211 // Try to populate the price and yield term structure. Need them in some cases here.
212 populateCurves(config, yieldCurves, commodityCurves, true, true);
213 buildVolatility(asof, config, *vssc, loader);
214 } else if (auto vdsc = QuantLib::ext::dynamic_pointer_cast<VolatilityDeltaSurfaceConfig>(vc)) {
215 // Need a yield curve and price curve to create a delta surface.
216 populateCurves(config, yieldCurves, commodityCurves, true);
217 buildVolatility(asof, config, *vdsc, loader);
218 } else if (auto vmsc = QuantLib::ext::dynamic_pointer_cast<VolatilityMoneynessSurfaceConfig>(vc)) {
219 // Need a yield curve (if forward moneyness) and price curve to create a moneyness surface.
220 MoneynessStrike::Type moneynessType = parseMoneynessType(vmsc->moneynessType());
221 bool fwdMoneyness = moneynessType == MoneynessStrike::Type::Forward;
222 populateCurves(config, yieldCurves, commodityCurves, fwdMoneyness);
223 buildVolatility(asof, config, *vmsc, loader);
224 } else if (auto vapo = QuantLib::ext::dynamic_pointer_cast<VolatilityApoFutureSurfaceConfig>(vc)) {
225
226 // Get the base conventions and create the associated expiry calculator.
227 QL_REQUIRE(!vapo->baseConventionsId().empty(),
228 "The APO FutureConventions must be populated to build a future APO surface");
229 QL_REQUIRE(conventions->has(vapo->baseConventionsId()),
230 "Conventions, " << vapo->baseConventionsId() << " for config " << config.curveID()
231 << " not found.");
232 auto convention = QuantLib::ext::dynamic_pointer_cast<CommodityFutureConvention>(
233 conventions->get(vapo->baseConventionsId()));
234 QL_REQUIRE(convention, "Convention with ID '"
235 << config.futureConventionsId()
236 << "' should be of type CommodityFutureConvention");
237 auto baseExpCalc = QuantLib::ext::make_shared<ConventionsBasedFutureExpiry>(*convention);
238
239 // Need to get the base commodity volatility structure.
240 QL_REQUIRE(!vapo->baseVolatilityId().empty(),
241 "The APO VolatilityId must be populated to build a future APO surface.");
242 auto itVs = commodityVolCurves.find(vapo->baseVolatilityId());
243 QL_REQUIRE(itVs != commodityVolCurves.end(),
244 "Can't find commodity volatility with id " << vapo->baseVolatilityId());
245 auto baseVs = Handle<BlackVolTermStructure>(itVs->second->volatility());
246
247 // Need to get the base price curve
248 QL_REQUIRE(!vapo->basePriceCurveId().empty(),
249 "The APO PriceCurveId must be populated to build a future APO surface.");
250 auto itPts = commodityCurves.find(vapo->basePriceCurveId());
251 QL_REQUIRE(itPts != commodityCurves.end(),
252 "Can't find price curve with id " << vapo->basePriceCurveId());
253 auto basePts = Handle<PriceTermStructure>(itPts->second->commodityPriceCurve());
254
255 // Need a yield curve and price curve to create an APO surface.
256 populateCurves(config, yieldCurves, commodityCurves, true);
257
258 buildVolatility(asof, config, *vapo, baseVs, basePts);
259
260 } else {
261 QL_FAIL("Unexpected VolatilityConfig in CommodityVolatilityConfig");
262 }
263 } else {
264 QL_FAIL("CommodityVolCurve: VolatilityConfig must be QuoteBased or a Proxy");
265 }
266 if (buildCalibrationInfo)
267 this->buildVolCalibrationInfo(asof, vc, curveConfigs, config);
268 } catch (std::exception& e) {
269 DLOG("CommodityVolCurve: commodity vol curve building failed :" << e.what());
270 } catch (...) {
271 DLOG("CommodityVolCurve: commodity vol curve building failed: unknown error");
272 }
273 if (volatility_)
274 break;
275 }
276 QL_REQUIRE(volatility_ , "CommodityVolCurve: Failed to build commodity volatility structure from "
277 << config.volatilityConfig().size() << " volatility configs provided.");
278
279 LOG("CommodityVolCurve: finished building commodity volatility structure with ID " << spec.curveConfigID());
280
281 } catch (exception& e) {
282 QL_FAIL("Commodity volatility curve building failed : " << e.what());
283 } catch (...) {
284 QL_FAIL("Commodity volatility curve building failed: unknown error");
285 }
286}
287
288void CommodityVolCurve::buildVolatility(const Date& asof, const CommodityVolatilityConfig& vc,
289 const ConstantVolatilityConfig& cvc, const Loader& loader) {
290
291 LOG("CommodityVolCurve: start building constant volatility structure");
292
293 const QuantLib::ext::shared_ptr<MarketDatum>& md = loader.get(cvc.quote(), asof);
294 QL_REQUIRE(md->asofDate() == asof, "MarketDatum asofDate '" << md->asofDate() << "' <> asof '" << asof << "'");
295 QL_REQUIRE(md->instrumentType() == MarketDatum::InstrumentType::COMMODITY_OPTION,
296 "MarketDatum instrument type '" << md->instrumentType() << "' <> 'MarketDatum::InstrumentType::COMMODITY_OPTION'");
297 QuantLib::ext::shared_ptr<CommodityOptionQuote> q = QuantLib::ext::dynamic_pointer_cast<CommodityOptionQuote>(md);
298 QL_REQUIRE(q, "Internal error: could not downcast MarketDatum '" << md->name() << "' to CommodityOptionQuote");
299 QL_REQUIRE(q->name() == cvc.quote(),
300 "CommodityOptionQuote name '" << q->name() << "' <> ConstantVolatilityConfig quote '" << cvc.quote() << "'");
301 TLOG("Found the constant volatility quote " << q->name());
302 Real quoteValue = q->quote()->value();
303
304 DLOG("Creating BlackConstantVol structure");
305 volatility_ = QuantLib::ext::make_shared<BlackConstantVol>(asof, calendar_, quoteValue, dayCounter_);
306
307 LOG("CommodityVolCurve: finished building constant volatility structure");
308}
309
310void CommodityVolCurve::buildVolatility(const QuantLib::Date& asof, const CommodityVolatilityConfig& vc,
311 const VolatilityCurveConfig& vcc, const Loader& loader) {
312
313 LOG("CommodityVolCurve: start building 1-D volatility curve");
314
315 // Must have at least one quote
316 QL_REQUIRE(vcc.quotes().size() > 0, "No quotes specified in config " << vc.curveID());
317
318 QL_REQUIRE(vcc.quoteType() == MarketDatum::QuoteType::RATE_LNVOL, "CommodityVolCurve: only quote type" <<
319 " RATE_LNVOL is currently supported for 1-D commodity volatility curves.");
320
321 // Check if we are using a regular expression to select the quotes for the curve. If we are, the quotes should
322 // contain exactly one element.
323 auto wildcard = getUniqueWildcard(vcc.quotes());
324
325 // curveData will be populated with the expiry dates and volatility values.
326 map<Date, Real> curveData;
327
328 // Different approaches depending on whether we are using a regex or searching for a list of explicit quotes.
329 if (wildcard) {
330
331 DLOG("Have single quote with pattern " << wildcard->pattern());
332
333 // Loop over quotes and process commodity option quotes matching pattern on asof
334 for (const auto& md : loader.get(*wildcard, asof)) {
335
336 // Go to next quote if the market data point's date does not equal our asof
337 if (md->asofDate() != asof)
338 continue;
339
340 auto q = QuantLib::ext::dynamic_pointer_cast<CommodityOptionQuote>(md);
341 if (!q)
342 continue;
343 QL_REQUIRE(q->quoteType() == vcc.quoteType(),
344 "CommodityOptionQuote type '" << q->quoteType() << "' <> VolatilityCurveConfig quote type '" << vcc.quoteType() << "'");
345
346 TLOG("The quote " << q->name() << " matched the pattern");
347
348 Date expiryDate = getExpiry(asof, q->expiry(), vc.futureConventionsId(), vc.optionExpiryRollDays());
349 if (expiryDate > asof) {
350 // Add the quote to the curve data
351 auto it = curveData.find(expiryDate);
352 if (it != curveData.end()) {
353 LOG("Already added quote " << fixed << setprecision(9) << it->second << " for the expiry" <<
354 " date " << io::iso_date(expiryDate) << " for commodity curve " << vc.curveID() <<
355 " so not adding " << q->name());
356 } else {
357 curveData[expiryDate] = q->quote()->value();
358 TLOG("Added quote " << q->name() << ": (" << io::iso_date(expiryDate) << "," <<
359 fixed << setprecision(9) << q->quote()->value() << ")");
360 }
361 }
362 }
363
364 // Check that we have quotes in the end
365 QL_REQUIRE(curveData.size() > 0, "No quotes found matching regular expression " << vcc.quotes()[0]);
366
367 } else {
368
369 DLOG("Have " << vcc.quotes().size() << " explicit quotes");
370
371 // Loop over quotes and process commodity option quotes that are explicitly specified in the config
372 Size quotesAdded = 0;
373 Size skippedExpiredQuotes = 0;
374 for (const auto& quoteName : vcc.quotes()) {
375 auto md = loader.get(quoteName, asof);
376 QL_REQUIRE(md, "Unable to load quote '" << quoteName << "'");
377
378 QL_REQUIRE(md->asofDate() == asof, "MarketDatum asofDate '" << md->asofDate() << "' <> asof '" << asof << "'");
379
380 auto q = QuantLib::ext::dynamic_pointer_cast<CommodityOptionQuote>(md);
381 QL_REQUIRE(q, "Internal error: could not downcast MarketDatum '" << md->name() << "' to CommodityOptionQuote");
382
383 TLOG("Found the configured quote " << q->name());
384
385 Date expiryDate = getExpiry(asof, q->expiry(), vc.futureConventionsId(), vc.optionExpiryRollDays());
386 if (expiryDate > asof) {
387 QL_REQUIRE(expiryDate > asof, "Commodity volatility quote '"
388 << q->name() << "' has expiry in the past ("
389 << io::iso_date(expiryDate) << ")");
390 QL_REQUIRE(curveData.count(expiryDate) == 0, "Duplicate quote for the date "
391 << io::iso_date(expiryDate)
392 << " provided by commodity volatility config "
393 << vc.curveID());
394 curveData[expiryDate] = q->quote()->value();
395 quotesAdded++;
396 TLOG("Added quote " << q->name() << ": (" << io::iso_date(expiryDate) << "," << fixed
397 << setprecision(9) << q->quote()->value() << ")");
398 } else {
399 skippedExpiredQuotes++;
400 WLOG("Skipped quote " << q->name() << ": (" << io::iso_date(expiryDate) << "," << fixed
401 << setprecision(9) << q->quote()->value() << ")");
402 }
403 }
404
405 // Check that we have found all of the explicitly configured quotes
406 QL_REQUIRE((quotesAdded + skippedExpiredQuotes) == vcc.quotes().size(),
407 "Found " << quotesAdded << " live quotes and " << skippedExpiredQuotes << " expired quotes"
408 << " but " << vcc.quotes().size() << " quotes were given in config.");
409 }
410
411 // Create the dates and volatility vector
412 vector<Date> dates;
413 vector<Volatility> volatilities;
414 for (const auto& datum : curveData) {
415 dates.push_back(datum.first);
416 volatilities.push_back(datum.second);
417 TLOG("Added data point (" << io::iso_date(dates.back()) << "," << fixed << setprecision(9)
418 << volatilities.back() << ")");
419 }
420 // set max expiry date (used in buildCalibrationInfo())
421 if (!dates.empty())
422 maxExpiry_ = dates.back();
423 DLOG("Creating BlackVarianceCurve object.");
424 auto tmp = QuantLib::ext::make_shared<BlackVarianceCurve>(asof, dates, volatilities, dayCounter_);
425
426 // Set the interpolation.
427 if (vcc.interpolation() == "Linear") {
428 DLOG("Interpolation set to Linear.");
429 } else if (vcc.interpolation() == "Cubic") {
430 DLOG("Setting interpolation to Cubic.");
431 tmp->setInterpolation<Cubic>();
432 } else if (vcc.interpolation() == "LogLinear") {
433 DLOG("Setting interpolation to LogLinear.");
434 tmp->setInterpolation<LogLinear>();
435 } else {
436 DLOG("Interpolation " << vcc.interpolation() << " not recognised so leaving it Linear.");
437 }
438
439 // Set the volatility_ member after we have possibly updated the interpolation.
440 volatility_ = tmp;
441
442 // Set the extrapolation
444 DLOG("Enabling BlackVarianceCurve flat volatility extrapolation.");
445 volatility_->enableExtrapolation();
447 DLOG("Disabling BlackVarianceCurve extrapolation.");
448 volatility_->disableExtrapolation();
450 DLOG("BlackVarianceCurve does not support using interpolator for extrapolation "
451 << "so default to flat volatility extrapolation.");
452 volatility_->enableExtrapolation();
453 } else {
454 DLOG("Unexpected extrapolation so default to flat volatility extrapolation.");
455 volatility_->enableExtrapolation();
456 }
457
458 LOG("CommodityVolCurve: finished building 1-D volatility curve");
459}
460
462 const VolatilityStrikeSurfaceConfig& vssc, const Loader& loader) {
463
464 LOG("CommodityVolCurve: start building 2-D volatility absolute strike surface");
465
466 // We are building a commodity volatility surface here of the form expiry vs strike where the strikes are absolute
467 // numbers. The list of expiries may be explicit or contain a single wildcard character '*'. Similarly, the list
468 // of strikes may be explicit or contain a single wildcard character '*'. So, we have four options here which
469 // ultimately lead to two different types of surface being built:
470 // 1. explicit strikes and explicit expiries => BlackVarianceSurface
471 // 2. wildcard strikes and/or wildcard expiries (3 combinations) => BlackVarianceSurfaceSparse
472
473 bool expWc = false;
474 if (find(vssc.expiries().begin(), vssc.expiries().end(), "*") != vssc.expiries().end()) {
475 expWc = true;
476 QL_REQUIRE(vssc.expiries().size() == 1, "Wild card expiry specified but more expiries also specified.");
477 DLOG("Have expiry wildcard pattern " << vssc.expiries()[0]);
478 }
479
480 bool strkWc = false;
481 if (find(vssc.strikes().begin(), vssc.strikes().end(), "*") != vssc.strikes().end()) {
482 strkWc = true;
483 QL_REQUIRE(vssc.strikes().size() == 1, "Wild card strike specified but more strikes also specified.");
484 DLOG("Have strike wildcard pattern " << vssc.strikes()[0]);
485 }
486
487 // If we do not have a strike wild card, we expect a list of absolute strike values
488 vector<Real> configuredStrikes;
489 if (!strkWc) {
490 // Parse the list of absolute strikes
491 configuredStrikes = parseVectorOfValues<Real>(vssc.strikes(), &parseReal);
492 sort(configuredStrikes.begin(), configuredStrikes.end(), [](Real x, Real y) { return !close(x, y) && x < y; });
493 QL_REQUIRE(adjacent_find(configuredStrikes.begin(), configuredStrikes.end(),
494 [](Real x, Real y) { return close(x, y); }) == configuredStrikes.end(),
495 "The configured strikes contain duplicates");
496 DLOG("Parsed " << configuredStrikes.size() << " unique configured absolute strikes");
497 }
498
499 // If we do not have an expiry wild card, parse the configured expiries.
500 vector<QuantLib::ext::shared_ptr<Expiry>> configuredExpiries;
501 if (!expWc) {
502 // Parse the list of expiry strings.
503 for (const string& strExpiry : vssc.expiries()) {
504 configuredExpiries.push_back(parseExpiry(strExpiry));
505 }
506 DLOG("Parsed " << configuredExpiries.size() << " unique configured expiries");
507 }
508
509 // If there are no wildcard strikes or wildcard expiries, delegate to buildVolatilityExplicit.
510 if (!expWc && !strkWc) {
511 buildVolatilityExplicit(asof, vc, vssc, loader, configuredStrikes);
512 return;
513 }
514
515 DLOG("Expiries and or strikes have been configured via wildcards so building a "
516 << "wildcard based absolute strike surface");
517
518 // Store grid points along with call and/or put data point
519 CallPutData cpData;
520
521 // If the price term structure is not empty, populate the map with (expiry, forward) pairs
522 // that will be used below when choosing between a call and a put if have volatilities.
523 map<Date, Real> fwdCurve;
524
525 // Loop over quotes and process any commodity option quote that matches a wildcard
526 std::ostringstream ss;
527 ss << MarketDatum::InstrumentType::COMMODITY_OPTION << "/" << vssc.quoteType() << "/" << vc.curveID() << "/"
528 << vc.currency() << "/*";
529 Wildcard w(ss.str());
530 for (const auto& md : loader.get(w, asof)) {
531
532 QL_REQUIRE(md->asofDate() == asof, "MarketDatum asofDate '" << md->asofDate() << "' <> asof '" << asof << "'");
533
534 auto q = QuantLib::ext::dynamic_pointer_cast<CommodityOptionQuote>(md);
535 QL_REQUIRE(q, "Internal error: could not downcast MarketDatum '" << md->name() << "' to CommodityOptionQuote");
536
537 QL_REQUIRE(vc.curveID() == q->commodityName(),
538 "CommodityVolatilityConfig curve ID '" << vc.curveID() <<
539 "' <> CommodityOptionQuote commodity name '" << q->commodityName() << "'");
540 QL_REQUIRE(vc.currency() == q->quoteCurrency(),
541 "CommodityVolatilityConfig currency '" << vc.currency() <<
542 "' <> CommodityOptionQuote currency '" << q->quoteCurrency() << "'");
543 QL_REQUIRE(vssc.quoteType() == q->quoteType(),
544 "VolatilityStrikeSurfaceConfig quote type '" << vssc.quoteType() <<
545 "' <> CommodityOptionQuote quote type '" << q->quoteType() << "'");
546
547 // This surface is for absolute strikes only.
548 auto strike = QuantLib::ext::dynamic_pointer_cast<AbsoluteStrike>(q->strike());
549 if (!strike)
550 continue;
551
552 // If we have been given a list of explicit expiries, check that the quote matches one of them.
553 // Move to the next quote if it does not.
554 if (!expWc) {
555 auto expiryIt = find_if(configuredExpiries.begin(), configuredExpiries.end(),
556 [&q](QuantLib::ext::shared_ptr<Expiry> e) { return *e == *q->expiry(); });
557 if (expiryIt == configuredExpiries.end())
558 continue;
559 }
560
561 // If we have been given a list of explicit strikes, check that the quote matches one of them.
562 // Move to the next quote if it does not.
563 if (!strkWc) {
564 auto strikeIt = find_if(configuredStrikes.begin(), configuredStrikes.end(),
565 [&strike](Real s) { return close(s, strike->strike()); });
566 if (strikeIt == configuredStrikes.end())
567 continue;
568 }
569
570 // Store in the data container.
571 Date expiry = getExpiry(asof, q->expiry(), vc.futureConventionsId(), vc.optionExpiryRollDays());
572 if (expiry > asof) {
573 ExpiryStrike node(expiry, strike->strike());
574 cpData.addDatum(ExpiryStrike(expiry, strike->strike()), q->optionType(), q->quote()->value());
575
576 // If we have a price term structure, add forward for expiry if not there already.
577 if (vssc.quoteType() == MarketDatum::QuoteType::RATE_LNVOL && !pts_.empty() &&
578 fwdCurve.count(expiry) == 0) {
579 fwdCurve[expiry] = pts_->price(expiry);
580 }
581
582 TLOG("Added quote " << q->name() << " to intermediate data.");
583 } else {
584 TLOG("Skipped quote " << q->name() << " to intermediate data, already expired at " << QuantLib::io::iso_date(expiry));
585 }
586 }
587
588 // Set the strike extrapolation which only matters if extrapolation is turned on for the whole surface.
589 bool flatStrikeExtrap = true;
590 bool flatTimeExtrap = true;
591 if (vssc.extrapolation()) {
592
593 auto strikeExtrapType = parseExtrapolation(vssc.strikeExtrapolation());
594 if (strikeExtrapType == Extrapolation::UseInterpolator) {
595 DLOG("Strike extrapolation switched to using interpolator.");
596 flatStrikeExtrap = false;
597 } else if (strikeExtrapType == Extrapolation::None) {
598 DLOG("Strike extrapolation cannot be turned off on its own so defaulting to flat.");
599 } else if (strikeExtrapType == Extrapolation::Flat) {
600 DLOG("Strike extrapolation has been set to flat.");
601 } else {
602 DLOG("Strike extrapolation " << strikeExtrapType << " not expected so default to flat.");
603 }
604
605 auto timeExtrapType = parseExtrapolation(vssc.timeExtrapolation());
606 if (timeExtrapType == Extrapolation::UseInterpolator) {
607 DLOG("Time extrapolation switched to using interpolator.");
608 flatTimeExtrap = false;
609 } else if (timeExtrapType == Extrapolation::None) {
610 DLOG("Time extrapolation cannot be turned off on its own so defaulting to flat.");
611 } else if (timeExtrapType == Extrapolation::Flat) {
612 DLOG("Time extrapolation has been set to flat.");
613 } else {
614 DLOG("Time extrapolation " << timeExtrapType << " not expected so default to flat.");
615 }
616
617 } else {
618 DLOG("Extrapolation is turned off for the whole surface so the time and"
619 << " strike extrapolation settings are ignored");
620 }
621
622 // Determine which option type to pick at each expiry and strike.
623 bool preferOutOfTheMoney = !vc.preferOutOfTheMoney() ? true : *vc.preferOutOfTheMoney();
624 TLOG("Prefer out of the money set to: " << ore::data::to_string(preferOutOfTheMoney) << ".");
625
626 // Build the surface depending on the quote type.
628
629 DLOG("Creating the BlackVarianceSurfaceSparse object");
630
631 // Now pick the quotes that we want from the data container. If the fwdCurve was populated above, we use
632 // it in conjunction with preferOutOfTheMoney, to pick the volatilities. If fwdCurve is empty, we just pick
633 // a call volatility if there is one else a put volatility (should have at least one).
634 vector<Real> strikes;
635 vector<Date> expiries;
636 vector<Volatility> vols;
637 Size quotesAdded = 0;
638
639 for (const auto& kv : cpData.data) {
640
641 const Date& expiry = kv.first.expiry;
642 const Real& stk = kv.first.strike;
643 const CallPutDatum& cpd = kv.second;
644 QL_REQUIRE(cpd.call != Null<Real>() || cpd.put != Null<Real>(),
645 "CommodityVolCurve: expected the call or put value to be populated.");
646
647 expiries.push_back(expiry);
648 strikes.push_back(stk);
649 bool useCall = cpd.call != Null<Real>();
650
651 if ((cpd.call != Null<Real>() && cpd.put != Null<Real>()) && !fwdCurve.empty()) {
652 // We have a call and a put and a forward curve so use it to decide.
653 auto it = fwdCurve.find(expiry);
654 QL_REQUIRE(it != fwdCurve.end(), "CommodityVolCurve: expected forwards for all expiries.");
655 const Real& fwd = it->second;
656 useCall = (preferOutOfTheMoney && stk >= fwd) || (!preferOutOfTheMoney && stk <= fwd);
657 TLOG("(Forward,Strike,UseCall) = (" << fixed << setprecision(6) <<
658 fwd << "," << stk << "," << to_string(useCall) << ").");
659 }
660
661 vols.push_back(useCall ? cpd.call : cpd.put);
662 quotesAdded++;
663
664 TLOG("Using option datum (" << (useCall ? "Call" : "Put") << "," << io::iso_date(expiries.back()) <<
665 "," << fixed << setprecision(9) << strikes.back() << "," << vols.back() << ")");
666 }
667
668 LOG("CommodityVolCurve: added " << quotesAdded << " quotes building wildcard based absolute strike surface.");
669 QL_REQUIRE(quotesAdded > 0, "No quotes loaded for " << vc.curveID());
670
671 volatility_ = QuantLib::ext::make_shared<BlackVarianceSurfaceSparse>(asof, calendar_, expiries, strikes,
672 vols, dayCounter_, flatStrikeExtrap, flatStrikeExtrap, flatTimeExtrap);
673
674 } else if (vssc.quoteType() == MarketDatum::QuoteType::PRICE) {
675
676 QL_REQUIRE(!pts_.empty(), "CommodityVolCurve: require non-empty price term structure" <<
677 " to strip volatilities from prices.");
678 QL_REQUIRE(!yts_.empty(), "CommodityVolCurve: require non-empty yield term structure" <<
679 " to strip volatilities from prices.");
680
681 // Create the 1D solver options used in the price stripping.
682 Solver1DOptions solverOptions = vc.solverConfig();
683
684 // Need to create the call and put option price surfaces.
685 auto cSurface = optPriceSurface(cpData, asof, dayCounter_, true);
686 auto pSurface = optPriceSurface(cpData, asof, dayCounter_, false);
687
688 DLOG("CommodityVolCurve: stripping volatility surface from the option premium surfaces.");
689 auto coss = QuantLib::ext::make_shared<CommodityOptionSurfaceStripper>(pts_, yts_, cSurface, pSurface, calendar_,
690 dayCounter_, vssc.exerciseType(), flatStrikeExtrap, flatStrikeExtrap, flatTimeExtrap,
691 preferOutOfTheMoney, solverOptions);
692 volatility_ = coss->volSurface();
693
694 // If data level logging, output the stripped volatilities.
695 if (Log::instance().filter(ORE_DATA)) {
696 volatility_->enableExtrapolation(vssc.extrapolation());
697 TLOG("CommodityVolCurve: stripped volatilities:");
698 TLOG("expiry,strike,forward_price,call_price,put_price,discount,volatility");
699 for (const auto& kv : cpData.data) {
700 const Date& expiry = kv.first.expiry;
701 const Real& strike = kv.first.strike;
702 Real fwd = pts_->price(expiry);
703 string cPrice;
704 if (kv.second.call != Null<Real>()) {
705 stringstream ss;
706 ss << fixed << setprecision(6) << kv.second.call;
707 cPrice = ss.str();
708 }
709 string pPrice;
710 if (kv.second.put != Null<Real>()) {
711 stringstream ss;
712 ss << fixed << setprecision(6) << kv.second.put;
713 pPrice = ss.str();
714 }
715 Real discount = yts_->discount(expiry);
716 Real vol = volatility_->blackVol(expiry, strike);
717 TLOG(io::iso_date(expiry) << "," << fixed << setprecision(6) << strike << "," <<
718 fwd << "," << cPrice << "," << pPrice << "," << discount << "," << vol);
719 }
720 }
721
722 } else {
723 QL_FAIL("CommodityVolCurve: invalid quote type " << vssc.quoteType() << " provided.");
724 }
725
726 DLOG("Setting BlackVarianceSurfaceSparse extrapolation to " << to_string(vssc.extrapolation()));
727 volatility_->enableExtrapolation(vssc.extrapolation());
728
729 LOG("CommodityVolCurve: finished building 2-D volatility absolute strike surface");
730}
731
733 const VolatilityStrikeSurfaceConfig& vssc, const Loader& loader,
734 const vector<Real>& configuredStrikes) {
735
736 LOG("CommodityVolCurve: start building 2-D volatility absolute strike surface with explicit strikes and expiries");
737
738 QL_REQUIRE(vssc.quoteType() == MarketDatum::QuoteType::RATE_LNVOL, "CommodityVolCurve: only quote type" <<
739 " RATE_LNVOL is currently supported for 2-D volatility strike surface with explicit strikes and expiries.");
740
741 // Map to hold the rows of the commodity volatility matrix. The keys are the expiry dates and the values are the
742 // vectors of volatilities, one for each configured strike.
743 map<Date, vector<Real>> surfaceData;
744
745 // Count the number of quotes added. We check at the end that we have added all configured quotes.
746 Size quotesAdded = 0;
747 Size skippedExpiredQuotes = 0;
748 // Loop over quotes and process commodity option quotes that have been requested
749 std::ostringstream ss;
750 ss << MarketDatum::InstrumentType::COMMODITY_OPTION << "/" << vssc.quoteType() << "/" << vc.curveID() << "/"
751 << vc.currency() << "/*";
752 Wildcard w(ss.str());
753 for (const auto& md : loader.get(w, asof)) {
754
755 QL_REQUIRE(md->asofDate() == asof, "MarketDatum asofDate '" << md->asofDate() << "' <> asof '" << asof << "'");
756
757 auto q = QuantLib::ext::dynamic_pointer_cast<CommodityOptionQuote>(md);
758 QL_REQUIRE(q, "Internal error: could not downcast MarketDatum '" << md->name() << "' to CommodityOptionQuote");
759
760 // This surface is for absolute strikes only.
761 auto strike = QuantLib::ext::dynamic_pointer_cast<AbsoluteStrike>(q->strike());
762 if (!strike)
763 continue;
764
765 // If the quote is not in the configured quotes continue
766 auto it = find(vc.quotes().begin(), vc.quotes().end(), q->name());
767 if (it == vc.quotes().end())
768 continue;
769
770 // Process the quote
771 Date eDate = getExpiry(asof, q->expiry(), vc.futureConventionsId(), vc.optionExpiryRollDays());
772
773 if (eDate > asof) {
774
775 // Position of quote in vector of strikes
776 auto strikeIt = find_if(configuredStrikes.begin(), configuredStrikes.end(),
777 [&strike](Real s) { return close(s, strike->strike()); });
778 Size pos = distance(configuredStrikes.begin(), strikeIt);
779 QL_REQUIRE(pos < configuredStrikes.size(),
780 "The quote '"
781 << q->name()
782 << "' is in the list of configured quotes but does not match any of the configured strikes");
783
784 // Add quote to surface
785 if (surfaceData.count(eDate) == 0)
786 surfaceData[eDate] = vector<Real>(configuredStrikes.size(), Null<Real>());
787
788 QL_REQUIRE(surfaceData[eDate][pos] == Null<Real>(),
789 "Quote " << q->name() << " provides a duplicate quote for the date " << io::iso_date(eDate)
790 << " and the strike " << configuredStrikes[pos]);
791 surfaceData[eDate][pos] = q->quote()->value();
792 quotesAdded++;
793
794 TLOG("Added quote " << q->name() << ": (" << io::iso_date(eDate) << "," << fixed << setprecision(9)
795 << configuredStrikes[pos] << "," << q->quote()->value() << ")");
796 } else {
797 skippedExpiredQuotes++;
798 TLOG("Skipped quote " << q->name() << ": (" << io::iso_date(eDate) << "," << fixed << setprecision(9)
799 << q->quote()->value() << ")");
800 }
801 }
802
803 LOG("CommodityVolCurve: added " << quotesAdded << " quotes in building explicit absolute strike surface.");
804
805 QL_REQUIRE(vc.quotes().size() == quotesAdded + skippedExpiredQuotes,
806 "Found " << quotesAdded << " live quotes and " << skippedExpiredQuotes << "expired quotes, but " << vc.quotes().size()
807 << " quotes required by config.");
808
809 // Set up the BlackVarianceSurface. Note that the rows correspond to strikes and that the columns correspond to
810 // the expiry dates in the matrix that is fed to the BlackVarianceSurface ctor.
811 vector<Date> expiryDates;
812 Matrix volatilities = Matrix(configuredStrikes.size(), surfaceData.size());
813 Size expiryIdx = 0;
814 for (const auto& expiryRow : surfaceData) {
815 expiryDates.push_back(expiryRow.first);
816 for (Size i = 0; i < configuredStrikes.size(); i++) {
817 volatilities[i][expiryIdx] = expiryRow.second[i];
818 }
819 expiryIdx++;
820 }
821
822 // set max expiry date (used in buildCalibrationInfo())
823 if (!expiryDates.empty())
824 maxExpiry_ = *max_element(expiryDates.begin(), expiryDates.end());
825
826 // Trace log the surface
827 TLOG("Explicit strike surface grid points:");
828 TLOG("expiry,strike,volatility");
829 for (Size i = 0; i < volatilities.rows(); i++) {
830 for (Size j = 0; j < volatilities.columns(); j++) {
831 TLOG(io::iso_date(expiryDates[j])
832 << "," << fixed << setprecision(9) << configuredStrikes[i] << "," << volatilities[i][j]);
833 }
834 }
835
836 // Set the strike extrapolation which only matters if extrapolation is turned on for the whole surface.
837 // BlackVarianceSurface time extrapolation is hard-coded to constant in volatility.
838 BlackVarianceSurface::Extrapolation strikeExtrap = BlackVarianceSurface::ConstantExtrapolation;
839 if (vssc.extrapolation()) {
840
841 auto strikeExtrapType = parseExtrapolation(vssc.strikeExtrapolation());
842 if (strikeExtrapType == Extrapolation::UseInterpolator) {
843 DLOG("Strike extrapolation switched to using interpolator.");
844 strikeExtrap = BlackVarianceSurface::InterpolatorDefaultExtrapolation;
845 } else if (strikeExtrapType == Extrapolation::None) {
846 DLOG("Strike extrapolation cannot be turned off on its own so defaulting to flat.");
847 } else if (strikeExtrapType == Extrapolation::Flat) {
848 DLOG("Strike extrapolation has been set to flat.");
849 } else {
850 DLOG("Strike extrapolation " << strikeExtrapType << " not expected so default to flat.");
851 }
852
853 auto timeExtrapType = parseExtrapolation(vssc.timeExtrapolation());
854 if (timeExtrapType != Extrapolation::Flat) {
855 DLOG("BlackVarianceSurface only supports flat volatility extrapolation in the time direction");
856 }
857 } else {
858 DLOG("Extrapolation is turned off for the whole surface so the time and"
859 << " strike extrapolation settings are ignored");
860 }
861
862 DLOG("Creating BlackVarianceSurface object");
863 auto tmp = QuantLib::ext::make_shared<BlackVarianceSurface>(asof, calendar_, expiryDates, configuredStrikes, volatilities,
864 dayCounter_, strikeExtrap, strikeExtrap);
865
866 // Set the interpolation if configured properly. The default is Bilinear.
867 if (!(vssc.timeInterpolation() == "Linear" && vssc.strikeInterpolation() == "Linear")) {
868 if (vssc.timeInterpolation() != vssc.strikeInterpolation()) {
869 DLOG("Time and strike interpolation must be the same for BlackVarianceSurface but we got strike "
870 << "interpolation " << vssc.strikeInterpolation() << " and time interpolation "
871 << vssc.timeInterpolation());
872 } else if (vssc.timeInterpolation() == "Cubic") {
873 DLOG("Setting interpolation to BiCubic");
874 tmp->setInterpolation<Bicubic>();
875 } else {
876 DLOG("Interpolation type " << vssc.timeInterpolation() << " not supported in 2 dimensions");
877 }
878 }
879
880 // Set the volatility_ member after we have possibly updated the interpolation.
881 volatility_ = tmp;
882
883 DLOG("Setting BlackVarianceSurface extrapolation to " << to_string(vssc.extrapolation()));
884 volatility_->enableExtrapolation(vssc.extrapolation());
885
886 LOG("CommodityVolCurve: finished building 2-D volatility absolute strike "
887 << "surface with explicit strikes and expiries");
888}
889
891 const VolatilityDeltaSurfaceConfig& vdsc, const Loader& loader) {
892
893 using boost::adaptors::transformed;
894 using boost::algorithm::join;
895
896 LOG("CommodityVolCurve: start building 2-D volatility delta strike surface");
897
898 QL_REQUIRE(vdsc.quoteType() == MarketDatum::QuoteType::RATE_LNVOL, "CommodityVolCurve: only quote type" <<
899 " RATE_LNVOL is currently supported for a 2-D volatility delta strike surface.");
900
901 // Parse, sort and check the vector of configured put deltas
902 vector<Real> putDeltas = parseVectorOfValues<Real>(vdsc.putDeltas(), &parseReal);
903 sort(putDeltas.begin(), putDeltas.end(), [](Real x, Real y) { return !close(x, y) && x < y; });
904 QL_REQUIRE(adjacent_find(putDeltas.begin(), putDeltas.end(), [](Real x, Real y) { return close(x, y); }) ==
905 putDeltas.end(),
906 "The configured put deltas contain duplicates");
907 DLOG("Parsed " << putDeltas.size() << " unique configured put deltas");
908 DLOG("Put deltas are: " << join(putDeltas | transformed([](Real d) { return ore::data::to_string(d); }), ","));
909
910 // Parse, sort descending and check the vector of configured call deltas
911 vector<Real> callDeltas = parseVectorOfValues<Real>(vdsc.callDeltas(), &parseReal);
912 sort(callDeltas.begin(), callDeltas.end(), [](Real x, Real y) { return !close(x, y) && x > y; });
913 QL_REQUIRE(adjacent_find(callDeltas.begin(), callDeltas.end(), [](Real x, Real y) { return close(x, y); }) ==
914 callDeltas.end(),
915 "The configured call deltas contain duplicates");
916 DLOG("Parsed " << callDeltas.size() << " unique configured call deltas");
917 DLOG("Call deltas are: " << join(callDeltas | transformed([](Real d) { return ore::data::to_string(d); }), ","));
918
919 // Expiries may be configured with a wildcard or given explicitly
920 bool expWc = false;
921 if (find(vdsc.expiries().begin(), vdsc.expiries().end(), "*") != vdsc.expiries().end()) {
922 expWc = true;
923 QL_REQUIRE(vdsc.expiries().size() == 1, "Wild card expiry specified but more expiries also specified.");
924 DLOG("Have expiry wildcard pattern " << vdsc.expiries()[0]);
925 }
926
927 // Map to hold the rows of the commodity volatility matrix. The keys are the expiry dates and the values are the
928 // vectors of volatilities, one for each configured delta.
929 map<Date, vector<Real>> surfaceData;
930
931 // Number of strikes = number of put deltas + ATM + number of call deltas
932 Size numStrikes = putDeltas.size() + 1 + callDeltas.size();
933
934 // Count the number of quotes added. We check at the end that we have added all configured quotes.
935 Size quotesAdded = 0;
936 Size skippedExpiredQuotes = 0;
937 // Configured delta and Atm types.
938 DeltaVolQuote::DeltaType deltaType = parseDeltaType(vdsc.deltaType());
939 DeltaVolQuote::AtmType atmType = parseAtmType(vdsc.atmType());
940 boost::optional<DeltaVolQuote::DeltaType> atmDeltaType;
941 if (!vdsc.atmDeltaType().empty()) {
942 atmDeltaType = parseDeltaType(vdsc.atmDeltaType());
943 }
944
945 // Populate the configured strikes.
946 vector<QuantLib::ext::shared_ptr<BaseStrike>> strikes;
947 for (const auto& pd : putDeltas) {
948 strikes.push_back(QuantLib::ext::make_shared<DeltaStrike>(deltaType, Option::Put, pd));
949 }
950 strikes.push_back(QuantLib::ext::make_shared<AtmStrike>(atmType, atmDeltaType));
951 for (const auto& cd : callDeltas) {
952 strikes.push_back(QuantLib::ext::make_shared<DeltaStrike>(deltaType, Option::Call, cd));
953 }
954
955 // Read the quotes to fill the expiry dates and vols matrix.
956 std::ostringstream ss;
957 ss << MarketDatum::InstrumentType::COMMODITY_OPTION << "/" << vdsc.quoteType() << "/" << vc.curveID() << "/"
958 << vc.currency() << "/*";
959 Wildcard w(ss.str());
960 for (const auto& md : loader.get(w, asof)) {
961
962 QL_REQUIRE(md->asofDate() == asof, "MarketDatum asofDate '" << md->asofDate() << "' <> asof '" << asof << "'");
963
964 auto q = QuantLib::ext::dynamic_pointer_cast<CommodityOptionQuote>(md);
965 QL_REQUIRE(q, "Internal error: could not downcast MarketDatum '" << md->name() << "' to CommodityOptionQuote");
966
967 QL_REQUIRE(vc.curveID() == q->commodityName(),
968 "CommodityVolatilityConfig curve ID '" << vc.curveID() <<
969 "' <> CommodityOptionQuote commodity name '" << q->commodityName() << "'");
970 QL_REQUIRE(vc.currency() == q->quoteCurrency(),
971 "CommodityVolatilityConfig currency '" << vc.currency() <<
972 "' <> CommodityOptionQuote currency '" << q->quoteCurrency() << "'");
973
974 // Iterator to one of the configured strikes.
975 vector<QuantLib::ext::shared_ptr<BaseStrike>>::iterator strikeIt;
976
977 if (expWc) {
978
979 // Check if quote's strike is in the configured strikes and continue if it is not.
980 strikeIt = find_if(strikes.begin(), strikes.end(),
981 [&q](QuantLib::ext::shared_ptr<BaseStrike> s) { return *s == *q->strike(); });
982 if (strikeIt == strikes.end())
983 continue;
984 } else {
985
986 // If we have explicitly configured expiries and the quote is not in the configured quotes continue.
987 auto it = find(vc.quotes().begin(), vc.quotes().end(), q->name());
988 if (it == vc.quotes().end())
989 continue;
990
991 // Check if quote's strike is in the configured strikes.
992 // It should be as we have selected from the explicitly configured quotes in the last step.
993 strikeIt = find_if(strikes.begin(), strikes.end(),
994 [&q](QuantLib::ext::shared_ptr<BaseStrike> s) { return *s == *q->strike(); });
995 QL_REQUIRE(strikeIt != strikes.end(),
996 "The quote '"
997 << q->name()
998 << "' is in the list of configured quotes but does not match any of the configured strikes");
999 }
1000
1001 // Position of quote in vector of strikes
1002 Size pos = std::distance(strikes.begin(), strikeIt);
1003
1004 // Process the quote
1005 Date eDate = getExpiry(asof, q->expiry(), vc.futureConventionsId(), vc.optionExpiryRollDays());
1006 if (eDate > asof) {
1007
1008 // Add quote to surface
1009 if (surfaceData.count(eDate) == 0)
1010 surfaceData[eDate] = vector<Real>(numStrikes, Null<Real>());
1011
1012 QL_REQUIRE(surfaceData[eDate][pos] == Null<Real>(),
1013 "Quote " << q->name() << " provides a duplicate quote for the date " << io::iso_date(eDate)
1014 << " and strike " << *q->strike());
1015 surfaceData[eDate][pos] = q->quote()->value();
1016 quotesAdded++;
1017
1018 TLOG("Added quote " << q->name() << ": (" << io::iso_date(eDate) << "," << *q->strike() << "," << fixed
1019 << setprecision(9) << "," << q->quote()->value() << ")");
1020 } else {
1021 skippedExpiredQuotes++;
1022 TLOG("Skiped quote, already expired: " << q->name() << ": (" << io::iso_date(eDate) << "," << *q->strike() << "," << fixed
1023 << setprecision(9) << "," << q->quote()->value() << ")");
1024 }
1025 }
1026
1027 LOG("CommodityVolCurve: added " << quotesAdded << " quotes in building delta strike surface.");
1028
1029 // Check the data gathered.
1030 if (expWc) {
1031 // If the expiries were configured via a wildcard, check that no surfaceData element has a Null<Real>().
1032 for (const auto& kv : surfaceData) {
1033 for (Size j = 0; j < numStrikes; j++) {
1034 QL_REQUIRE(kv.second[j] != Null<Real>(), "Volatility for expiry date "
1035 << io::iso_date(kv.first) << " and strike " << *strikes[j]
1036 << " not found. Cannot proceed with a sparse matrix.");
1037 }
1038 }
1039 } else {
1040 // If expiries were configured explicitly, the number of configured quotes should equal the
1041 // number of quotes added.
1042 QL_REQUIRE(vc.quotes().size() == quotesAdded + skippedExpiredQuotes,
1043 "Found " << quotesAdded << " quotes and "<< skippedExpiredQuotes <<" expired quotes , but " << vc.quotes().size() << " quotes required by config.");
1044 }
1045
1046 // Populate the matrix of volatilities and the expiry dates.
1047 vector<Date> expiryDates;
1048 Matrix vols(surfaceData.size(), numStrikes);
1049 for (const auto row : surfaceData | boost::adaptors::indexed(0)) {
1050 expiryDates.push_back(row.value().first);
1051 copy(row.value().second.begin(), row.value().second.end(), vols.row_begin(row.index()));
1052 }
1053
1054 if (!expiryDates.empty())
1055 maxExpiry_ = *std::max_element(expiryDates.begin(), expiryDates.end());
1056
1057 // Need to multiply each put delta value by -1 before passing it to the BlackVolatilitySurfaceDelta ctor
1058 // i.e. a put delta of 0.25 that is passed in to the config must be -0.25 when passed to the ctor.
1059 transform(putDeltas.begin(), putDeltas.end(), putDeltas.begin(), [](Real pd) { return -1.0 * pd; });
1060 DLOG("Multiply put deltas by -1.0 before creating BlackVolatilitySurfaceDelta object.");
1061 DLOG("Put deltas are: " << join(putDeltas | transformed([](Real d) { return ore::data::to_string(d); }), ","));
1062
1063 // Set the strike extrapolation which only matters if extrapolation is turned on for the whole surface.
1064 // BlackVolatilitySurfaceDelta time extrapolation is hard-coded to constant in volatility.
1065 bool flatExtrapolation = true;
1066 if (vdsc.extrapolation()) {
1067
1068 auto strikeExtrapType = parseExtrapolation(vdsc.strikeExtrapolation());
1069 if (strikeExtrapType == Extrapolation::UseInterpolator) {
1070 DLOG("Strike extrapolation switched to using interpolator.");
1071 flatExtrapolation = false;
1072 } else if (strikeExtrapType == Extrapolation::None) {
1073 DLOG("Strike extrapolation cannot be turned off on its own so defaulting to flat.");
1074 } else if (strikeExtrapType == Extrapolation::Flat) {
1075 DLOG("Strike extrapolation has been set to flat.");
1076 } else {
1077 DLOG("Strike extrapolation " << strikeExtrapType << " not expected so default to flat.");
1078 }
1079
1080 auto timeExtrapType = parseExtrapolation(vdsc.timeExtrapolation());
1081 if (timeExtrapType != Extrapolation::Flat) {
1082 DLOG("BlackVolatilitySurfaceDelta only supports flat volatility extrapolation in the time direction");
1083 }
1084 } else {
1085 DLOG("Extrapolation is turned off for the whole surface so the time and"
1086 << " strike extrapolation settings are ignored");
1087 }
1088
1089 // Time interpolation
1090 if (vdsc.timeInterpolation() != "Linear") {
1091 DLOG("BlackVolatilitySurfaceDelta only supports linear time interpolation.");
1092 }
1093
1094 // Strike interpolation
1096 if (vdsc.strikeInterpolation() == "Linear") {
1097 im = InterpolatedSmileSection::InterpolationMethod::Linear;
1098 } else if (vdsc.strikeInterpolation() == "NaturalCubic") {
1099 im = InterpolatedSmileSection::InterpolationMethod::NaturalCubic;
1100 } else if (vdsc.strikeInterpolation() == "FinancialCubic") {
1101 im = InterpolatedSmileSection::InterpolationMethod::FinancialCubic;
1102 } else if (vdsc.strikeInterpolation() == "CubicSpline") {
1103 im = InterpolatedSmileSection::InterpolationMethod::CubicSpline;
1104 } else {
1105 im = InterpolatedSmileSection::InterpolationMethod::Linear;
1106 DLOG("BlackVolatilitySurfaceDelta does not support strike interpolation '" << vdsc.strikeInterpolation()
1107 << "' so setting it to linear.");
1108 }
1109
1110 // Apply correction to future price term structure if requested and we have a valid expiry calculator
1111 QL_REQUIRE(!pts_.empty(), "Expected the price term structure to be populated for a delta surface.");
1112 Handle<PriceTermStructure> cpts = pts_;
1113 if (vdsc.futurePriceCorrection() && expCalc_)
1114 cpts = correctFuturePriceCurve(asof, vc.futureConventionsId(), *pts_, expiryDates);
1115
1116 // Need to construct a dummy spot and foreign yts such that fwd = spot * DF_for / DF
1117 QL_REQUIRE(!yts_.empty(), "Expected the yield term structure to be populated for a delta surface.");
1118 Handle<Quote> spot(QuantLib::ext::make_shared<DerivedPriceQuote>(cpts));
1119 Handle<YieldTermStructure> pyts =
1120 Handle<YieldTermStructure>(QuantLib::ext::make_shared<PriceTermStructureAdapter>(*cpts, *yts_));
1121 pyts->enableExtrapolation();
1122
1123 DLOG("Creating BlackVolatilitySurfaceDelta object");
1124 bool hasAtm = true;
1125 volatility_ = QuantLib::ext::make_shared<BlackVolatilitySurfaceDelta>(
1126 asof, expiryDates, putDeltas, callDeltas, hasAtm, vols, dayCounter_, calendar_, spot, yts_, pyts, deltaType,
1127 atmType, atmDeltaType, 0 * Days, deltaType, atmType, atmDeltaType, im, flatExtrapolation);
1128
1129 DLOG("Setting BlackVolatilitySurfaceDelta extrapolation to " << to_string(vdsc.extrapolation()));
1130 volatility_->enableExtrapolation(vdsc.extrapolation());
1131
1132 LOG("CommodityVolCurve: finished building 2-D volatility delta strike surface");
1133}
1134
1135void CommodityVolCurve::buildVolatility(const Date& asof, CommodityVolatilityConfig& vc,
1136 const VolatilityMoneynessSurfaceConfig& vmsc, const Loader& loader) {
1137
1138 using boost::adaptors::transformed;
1139 using boost::algorithm::join;
1140
1141 LOG("CommodityVolCurve: start building 2-D volatility moneyness strike surface");
1142
1143 QL_REQUIRE(vmsc.quoteType() == MarketDatum::QuoteType::RATE_LNVOL, "CommodityVolCurve: only quote type" <<
1144 " RATE_LNVOL is currently supported for a 2-D volatility moneyness strike surface.");
1145
1146 // Parse, sort and check the vector of configured moneyness levels
1147 vector<Real> moneynessLevels = checkMoneyness(vmsc.moneynessLevels());
1148
1149 // Expiries may be configured with a wildcard or given explicitly
1150 bool expWc = false;
1151 if (find(vmsc.expiries().begin(), vmsc.expiries().end(), "*") != vmsc.expiries().end()) {
1152 expWc = true;
1153 QL_REQUIRE(vmsc.expiries().size() == 1, "Wild card expiry specified but more expiries also specified.");
1154 DLOG("Have expiry wildcard pattern " << vmsc.expiries()[0]);
1155 }
1156
1157 // Map to hold the rows of the commodity volatility matrix. The keys are the expiry dates and the values are the
1158 // vectors of volatilities, one for each configured moneyness.
1159 map<Date, vector<Real>> surfaceData;
1160
1161 // Count the number of quotes added. We check at the end that we have added all configured quotes.
1162 Size quotesAdded = 0;
1163 Size skippedExpiredQuotes = 0;
1164
1165 // Configured moneyness type.
1166 MoneynessStrike::Type moneynessType = parseMoneynessType(vmsc.moneynessType());
1167
1168 // Populate the configured strikes.
1169 vector<QuantLib::ext::shared_ptr<BaseStrike>> strikes;
1170 for (const auto& moneynessLevel : moneynessLevels) {
1171 strikes.push_back(QuantLib::ext::make_shared<MoneynessStrike>(moneynessType, moneynessLevel));
1172 }
1173
1174 // Read the quotes to fill the expiry dates and vols matrix.
1175 std::ostringstream ss;
1176 ss << MarketDatum::InstrumentType::COMMODITY_OPTION << "/" << vmsc.quoteType() << "/" << vc.curveID() << "/"
1177 << vc.currency() << "/*";
1178 Wildcard w(ss.str());
1179 for (const auto& md : loader.get(w, asof)) {
1180
1181 QL_REQUIRE(md->asofDate() == asof, "MarketDatum asofDate '" << md->asofDate() << "' <> asof '" << asof << "'");
1182
1183 auto q = QuantLib::ext::dynamic_pointer_cast<CommodityOptionQuote>(md);
1184 QL_REQUIRE(q, "Internal error: could not downcast MarketDatum '" << md->name() << "' to CommodityOptionQuote");
1185
1186 QL_REQUIRE(vc.curveID() == q->commodityName(),
1187 "CommodityVolatilityConfig curve ID '" << vc.curveID() <<
1188 "' <> CommodityOptionQuote commodity name '" << q->commodityName() << "'");
1189 QL_REQUIRE(vc.currency() == q->quoteCurrency(),
1190 "CommodityVolatilityConfig currency '" << vc.currency() <<
1191 "' <> CommodityOptionQuote currency '" << q->quoteCurrency() << "'");
1192
1193 // Iterator to one of the configured strikes.
1194 vector<QuantLib::ext::shared_ptr<BaseStrike>>::iterator strikeIt;
1195
1196 if (expWc) {
1197
1198 // Check if quote's strike is in the configured strikes and continue if it is not.
1199 strikeIt = find_if(strikes.begin(), strikes.end(),
1200 [&q](QuantLib::ext::shared_ptr<BaseStrike> s) { return *s == *q->strike(); });
1201 if (strikeIt == strikes.end())
1202 continue;
1203 } else {
1204
1205 // If we have explicitly configured expiries and the quote is not in the configured quotes continue.
1206 auto it = find(vc.quotes().begin(), vc.quotes().end(), q->name());
1207 if (it == vc.quotes().end())
1208 continue;
1209
1210 // Check if quote's strike is in the configured strikes.
1211 // It should be as we have selected from the explicitly configured quotes in the last step.
1212 strikeIt = find_if(strikes.begin(), strikes.end(),
1213 [&q](QuantLib::ext::shared_ptr<BaseStrike> s) { return *s == *q->strike(); });
1214 QL_REQUIRE(strikeIt != strikes.end(),
1215 "The quote '"
1216 << q->name()
1217 << "' is in the list of configured quotes but does not match any of the configured strikes");
1218 }
1219
1220 // Position of quote in vector of strikes
1221 Size pos = std::distance(strikes.begin(), strikeIt);
1222
1223 // Process the quote
1224 Date eDate = getExpiry(asof, q->expiry(), vc.futureConventionsId(), vc.optionExpiryRollDays());
1225 if (eDate > asof) {
1226 // Add quote to surface
1227 if (surfaceData.count(eDate) == 0)
1228 surfaceData[eDate] = vector<Real>(moneynessLevels.size(), Null<Real>());
1229
1230 QL_REQUIRE(surfaceData[eDate][pos] == Null<Real>(),
1231 "Quote " << q->name() << " provides a duplicate quote for the date " << io::iso_date(eDate)
1232 << " and strike " << *q->strike());
1233 surfaceData[eDate][pos] = q->quote()->value();
1234 quotesAdded++;
1235
1236 TLOG("Added quote " << q->name() << ": (" << io::iso_date(eDate) << "," << *q->strike() << "," << fixed
1237 << setprecision(9) << "," << q->quote()->value() << ")");
1238 }
1239 else {
1240 skippedExpiredQuotes++;
1241 TLOG("Skip quote, already expired: " << q->name() << ": (" << io::iso_date(eDate) << "," << *q->strike() << "," << fixed
1242 << setprecision(9) << "," << q->quote()->value() << ")");
1243 }
1244 }
1245
1246 LOG("CommodityVolCurve: added " << quotesAdded << " quotes in building moneyness strike surface.");
1247
1248 // Check the data gathered.
1249 if (expWc) {
1250 // If the expiries were configured via a wildcard, check that no surfaceData element has a Null<Real>().
1251 for (const auto& kv : surfaceData) {
1252 for (Size j = 0; j < moneynessLevels.size(); j++) {
1253 QL_REQUIRE(kv.second[j] != Null<Real>(), "Volatility for expiry date "
1254 << io::iso_date(kv.first) << " and strike " << *strikes[j]
1255 << " not found. Cannot proceed with a sparse matrix.");
1256 }
1257 }
1258 } else {
1259 // If expiries were configured explicitly, the number of configured quotes should equal the
1260 // number of quotes added.
1261 QL_REQUIRE(vc.quotes().size() == quotesAdded + skippedExpiredQuotes,
1262 "Found " << quotesAdded << " quotes and " << skippedExpiredQuotes << " expired quotes, but " << vc.quotes().size()
1263 << " quotes required by config.");
1264 }
1265
1266 // Populate the volatility quotes and the expiry times.
1267 // Rows are moneyness levels and columns are expiry times - this is what the ctor needs below.
1268 vector<Date> expiryDates(surfaceData.size());
1269 vector<Time> expiryTimes(surfaceData.size());
1270 vector<vector<Handle<Quote>>> vols(moneynessLevels.size());
1271 for (const auto row : surfaceData | boost::adaptors::indexed(0)) {
1272 expiryDates[row.index()] = row.value().first;
1273 expiryTimes[row.index()] = dayCounter_.yearFraction(asof, row.value().first);
1274 for (Size i = 0; i < row.value().second.size(); i++) {
1275 vols[i].push_back(Handle<Quote>(QuantLib::ext::make_shared<SimpleQuote>(row.value().second[i])));
1276 }
1277 }
1278
1279 if (!expiryDates.empty())
1280 maxExpiry_ = *std::max_element(expiryDates.begin(), expiryDates.end());
1281
1282 // Set the strike extrapolation which only matters if extrapolation is turned on for the whole surface.
1283 // BlackVarianceSurfaceMoneyness time extrapolation is hard-coded to constant in volatility.
1284 bool flatExtrapolation = true;
1285 if (vmsc.extrapolation()) {
1286
1287 auto strikeExtrapType = parseExtrapolation(vmsc.strikeExtrapolation());
1288 if (strikeExtrapType == Extrapolation::UseInterpolator) {
1289 DLOG("Strike extrapolation switched to using interpolator.");
1290 flatExtrapolation = false;
1291 } else if (strikeExtrapType == Extrapolation::None) {
1292 DLOG("Strike extrapolation cannot be turned off on its own so defaulting to flat.");
1293 } else if (strikeExtrapType == Extrapolation::Flat) {
1294 DLOG("Strike extrapolation has been set to flat.");
1295 } else {
1296 DLOG("Strike extrapolation " << strikeExtrapType << " not expected so default to flat.");
1297 }
1298
1299 auto timeExtrapType = parseExtrapolation(vmsc.timeExtrapolation());
1300 if (timeExtrapType != Extrapolation::Flat) {
1301 DLOG("BlackVarianceSurfaceMoneyness only supports flat volatility extrapolation in the time direction");
1302 }
1303 } else {
1304 DLOG("Extrapolation is turned off for the whole surface so the time and"
1305 << " strike extrapolation settings are ignored");
1306 }
1307
1308 // Time interpolation
1309 if (vmsc.timeInterpolation() != "Linear") {
1310 DLOG("BlackVarianceSurfaceMoneyness only supports linear time interpolation in variance.");
1311 }
1312
1313 // Strike interpolation
1314 if (vmsc.strikeInterpolation() != "Linear") {
1315 DLOG("BlackVarianceSurfaceMoneyness only supports linear strike interpolation in variance.");
1316 }
1317
1318 // Apply correction to future price term structure if requested and we have a valid expiry calculator
1319 QL_REQUIRE(!pts_.empty(), "Expected the price term structure to be populated for a moneyness surface.");
1320 Handle<PriceTermStructure> cpts = pts_;
1321 if (vmsc.futurePriceCorrection() && expCalc_)
1322 cpts = correctFuturePriceCurve(asof, vc.futureConventionsId(), *pts_, expiryDates);
1323
1324 // Both moneyness surfaces need a spot quote.
1325 Handle<Quote> spot(QuantLib::ext::make_shared<DerivedPriceQuote>(cpts));
1326
1327 // The choice of false here is important for forward moneyness. It means that we use the cpts and yts in the
1328 // BlackVarianceSurfaceMoneynessForward to get the forward value at all times and in particular at times that
1329 // are after the last expiry time. If we set it to true, BlackVarianceSurfaceMoneynessForward uses a linear
1330 // interpolated forward curve on the expiry times internally which is poor.
1331 bool stickyStrike = false;
1332
1333 if (moneynessType == MoneynessStrike::Type::Forward) {
1334
1335 QL_REQUIRE(!yts_.empty(), "Expected yield term structure to be populated for a forward moneyness surface.");
1336 Handle<YieldTermStructure> pyts =
1337 Handle<YieldTermStructure>(QuantLib::ext::make_shared<PriceTermStructureAdapter>(*cpts, *yts_));
1338 pyts->enableExtrapolation();
1339
1340 DLOG("Creating BlackVarianceSurfaceMoneynessForward object");
1341 volatility_ = QuantLib::ext::make_shared<BlackVarianceSurfaceMoneynessForward>(calendar_, spot, expiryTimes,
1342 moneynessLevels, vols, dayCounter_, pyts,
1343 yts_, stickyStrike, flatExtrapolation);
1344
1345 } else {
1346
1347 DLOG("Creating BlackVarianceSurfaceMoneynessSpot object");
1348 volatility_ = QuantLib::ext::make_shared<BlackVarianceSurfaceMoneynessSpot>(
1349 calendar_, spot, expiryTimes, moneynessLevels, vols, dayCounter_, stickyStrike, flatExtrapolation);
1350 }
1351
1352 DLOG("Setting BlackVarianceSurfaceMoneyness extrapolation to " << to_string(vmsc.extrapolation()));
1353 volatility_->enableExtrapolation(vmsc.extrapolation());
1354
1355 LOG("CommodityVolCurve: finished building 2-D volatility moneyness strike surface");
1356}
1357
1358void CommodityVolCurve::buildVolatility(const Date& asof, CommodityVolatilityConfig& vc,
1359 const VolatilityApoFutureSurfaceConfig& vapo,
1360 const Handle<BlackVolTermStructure>& baseVts,
1361 const Handle<PriceTermStructure>& basePts) {
1362
1363 LOG("CommodityVolCurve: start building the APO surface");
1364
1365 QL_REQUIRE(vapo.quoteType() == MarketDatum::QuoteType::RATE_LNVOL, "CommodityVolCurve: only quote type" <<
1366 " RATE_LNVOL is currently supported for an APO surface.");
1367
1368 // Get the base conventions and create the associated expiry calculator.
1369 QuantLib::ext::shared_ptr<Conventions> conventions = InstrumentConventions::instance().conventions();
1370 QL_REQUIRE(!vapo.baseConventionsId().empty(),
1371 "The APO FutureConventions must be populated to build a future APO surface");
1372 QL_REQUIRE(conventions->has(vapo.baseConventionsId()),
1373 "Conventions, " << vapo.baseConventionsId() << " for config " << vc.curveID() << " not found.");
1374 auto baseConvention =
1375 QuantLib::ext::dynamic_pointer_cast<CommodityFutureConvention>(conventions->get(vapo.baseConventionsId()));
1376 QL_REQUIRE(baseConvention,
1377 "Convention with ID '" << vapo.baseConventionsId() << "' should be of type CommodityFutureConvention");
1378
1379 auto baseExpCalc = QuantLib::ext::make_shared<ConventionsBasedFutureExpiry>(*baseConvention);
1380
1381 // Get the max tenor from the config if provided.
1382 boost::optional<QuantLib::Period> maxTenor;
1383 if (!vapo.maxTenor().empty())
1384 maxTenor = parsePeriod(vapo.maxTenor());
1385
1386 // Get the moneyness levels
1387 vector<Real> moneynessLevels = checkMoneyness(vapo.moneynessLevels());
1388
1389 // Get the beta parameter to use for valuing the APOs in the surface
1390 Real beta = vapo.beta();
1391
1392 // Construct the commodity index.
1393 auto index = parseCommodityIndex(baseConvention->id(), false, basePts);
1394
1395 // Set the strike extrapolation which only matters if extrapolation is turned on for the whole surface.
1396 // BlackVarianceSurfaceMoneyness, which underlies the ApoFutureSurface, has time extrapolation hard-coded to
1397 // constant in volatility.
1398 bool flatExtrapolation = true;
1399 if (vapo.extrapolation()) {
1400
1401 auto strikeExtrapType = parseExtrapolation(vapo.strikeExtrapolation());
1402 if (strikeExtrapType == Extrapolation::UseInterpolator) {
1403 DLOG("Strike extrapolation switched to using interpolator.");
1404 flatExtrapolation = false;
1405 } else if (strikeExtrapType == Extrapolation::None) {
1406 DLOG("Strike extrapolation cannot be turned off on its own so defaulting to flat.");
1407 } else if (strikeExtrapType == Extrapolation::Flat) {
1408 DLOG("Strike extrapolation has been set to flat.");
1409 } else {
1410 DLOG("Strike extrapolation " << strikeExtrapType << " not expected so default to flat.");
1411 }
1412
1413 auto timeExtrapType = parseExtrapolation(vapo.timeExtrapolation());
1414 if (timeExtrapType != Extrapolation::Flat) {
1415 DLOG("ApoFutureSurface only supports flat volatility extrapolation in the time direction");
1416 }
1417 } else {
1418 DLOG("Extrapolation is turned off for the whole surface so the time and"
1419 << " strike extrapolation settings are ignored");
1420 }
1421
1422 // Time interpolation
1423 if (vapo.timeInterpolation() != "Linear") {
1424 DLOG("ApoFutureSurface only supports linear time interpolation in variance.");
1425 }
1426
1427 // Strike interpolation
1428 if (vapo.strikeInterpolation() != "Linear") {
1429 DLOG("ApoFutureSurface only supports linear strike interpolation in variance.");
1430 }
1431
1432 DLOG("Creating ApoFutureSurface object");
1433 volatility_ = QuantLib::ext::make_shared<ApoFutureSurface>(asof, moneynessLevels, index, pts_, yts_, expCalc_, baseVts,
1434 baseExpCalc, beta, flatExtrapolation, maxTenor);
1435
1436 DLOG("Setting ApoFutureSurface extrapolation to " << to_string(vapo.extrapolation()));
1437 volatility_->enableExtrapolation(vapo.extrapolation());
1438
1439 LOG("CommodityVolCurve: finished building the APO surface");
1440}
1441
1443 const QuantLib::Date& asof, const CommodityVolatilityCurveSpec& spec, const CurveConfigurations& curveConfigs,
1444 const ProxyVolatilityConfig& pvc, const map<string, QuantLib::ext::shared_ptr<CommodityCurve>>& comCurves,
1445 const map<string, QuantLib::ext::shared_ptr<CommodityVolCurve>>& volCurves,
1446 const map<string, QuantLib::ext::shared_ptr<FXVolCurve>>& fxVolCurves,
1447 const map<string, QuantLib::ext::shared_ptr<CorrelationCurve>>& requiredCorrelationCurves, const Market* fxIndices) {
1448
1449 DLOG("Build Proxy Vol surface");
1450 // get all the configurations and the curve needed for proxying
1451 auto config = *curveConfigs.commodityVolatilityConfig(spec.curveConfigID());
1452
1453 auto proxy = pvc.proxyVolatilityCurve();
1454 auto comConfig = *curveConfigs.commodityCurveConfig(spec.curveConfigID());
1455 auto proxyConfig = *curveConfigs.commodityCurveConfig(proxy);
1456 auto proxyVolConfig = *curveConfigs.commodityVolatilityConfig(proxy);
1457
1458 // create dummy specs to look up the required curves
1459 CommodityCurveSpec comSpec(comConfig.currency(), spec.curveConfigID());
1460 CommodityCurveSpec proxySpec(proxyConfig.currency(), proxy);
1461 CommodityVolatilityCurveSpec proxyVolSpec(proxyVolConfig.currency(), proxy);
1462
1463 // Get all necessary curves
1464 auto curve = comCurves.find(comSpec.name());
1465 QL_REQUIRE(curve != comCurves.end(),
1466 "CommodityVolCurve: Failed to find commodity curve, when building commodity vol curve " << spec.name());
1467 auto proxyCurve = comCurves.find(proxySpec.name());
1468 QL_REQUIRE(proxyCurve != comCurves.end(), "currency: Failed to find commodity curve for proxy "
1469 << proxySpec.name() << ", when building commodity vol curve "
1470 << spec.name());
1471 auto proxyVolCurve = volCurves.find(proxyVolSpec.name());
1472 QL_REQUIRE(proxyVolCurve != volCurves.end(), "CommodityVolCurve: Failed to find commodity vol curve for proxy "
1473 << proxyVolSpec.name() << ", when building currency vol curve "
1474 << spec.name());
1475
1476 // check the currency against the proxy surface currrency
1477
1478 QuantLib::ext::shared_ptr<BlackVolTermStructure> fxSurface;
1479 QuantLib::ext::shared_ptr<FxIndex> fxIndex;
1480 QuantLib::ext::shared_ptr<QuantExt::CorrelationTermStructure> correlation;
1481 if (config.currency() != proxyVolConfig.currency() && fxIndices != nullptr) {
1482 QL_REQUIRE(!pvc.fxVolatilityCurve().empty(),
1483 "CommodityVolCurve: FXVolatilityCurve must be provided for commodity vol config "
1484 << spec.curveConfigID() << " as proxy currencies if different from commodity currency.");
1485 QL_REQUIRE(!pvc.correlationCurve().empty(),
1486 "CommodityVolCurve: CorrelationCurve must be provided for commodity vol config "
1487 << spec.curveConfigID() << " as proxy currencies if different from commodity currency.");
1488
1489 // get the fx vol surface
1490 QL_REQUIRE(pvc.fxVolatilityCurve().size() == 6, "CommodityVolCurve: FXVolatilityCurve provided "
1491 << pvc.fxVolatilityCurve() << " for commodity vol config "
1492 << spec.curveConfigID()
1493 << " must be of length 6, and of form CC1CCY2 e.g EURUSD");
1494 string proxyVolForCcy = pvc.fxVolatilityCurve().substr(0, 3);
1495 string proxyVolDomCcy = pvc.fxVolatilityCurve().substr(3, 3);
1496 FXVolatilityCurveSpec fxSpec(proxyVolForCcy, proxyVolDomCcy, pvc.fxVolatilityCurve());
1497 auto volIt = fxVolCurves.find(fxSpec.name());
1498 if (volIt == fxVolCurves.end())
1499 QL_FAIL("CommodityVolCurve: cannot find required Fx volatility surface "
1500 << fxSpec.name() << " to build proxy vol surface for " << comSpec.name());
1501 fxSurface = volIt->second->volTermStructure();
1502
1503 // check if the fx vol surface needs to be inverted
1504 if (proxyVolForCcy != proxyVolConfig.currency()) {
1505 Handle<BlackVolTermStructure> hFx(fxSurface);
1506 fxSurface = QuantLib::ext::make_shared<QuantExt::BlackInvertedVolTermStructure>(hFx);
1507 fxSurface->enableExtrapolation();
1508 }
1509
1510 fxIndex = fxIndices->fxIndex(proxyVolConfig.currency() + config.currency()).currentLink();
1511 FXSpotSpec spotSpec(proxyVolConfig.currency(), config.currency());
1512
1513 CorrelationCurveSpec corrSpec(pvc.correlationCurve());
1514 auto corrIt = requiredCorrelationCurves.find(corrSpec.name());
1515 if (corrIt == requiredCorrelationCurves.end())
1516 QL_FAIL("CommodityVolCurve: cannot find required correlation curve "
1517 << pvc.correlationCurve() << " to build proxy vol surface for " << comSpec.name());
1518 correlation = corrIt->second->corrTermStructure();
1519 }
1520
1521 volatility_ = QuantLib::ext::make_shared<BlackVolatilitySurfaceProxy>(
1522 proxyVolCurve->second->volatility(), curve->second->commodityIndex(), proxyCurve->second->commodityIndex(),
1523 fxSurface, fxIndex, correlation);
1524}
1525
1526Handle<PriceTermStructure> CommodityVolCurve::correctFuturePriceCurve(const Date& asof, const string& contractName,
1527 const QuantLib::ext::shared_ptr<PriceTermStructure>& pts,
1528 const vector<Date>& optionExpiries) const {
1529
1530 LOG("CommodityVolCurve: start adding future price correction at option expiry.");
1531
1532 // Gather curve dates and prices
1533 map<Date, Real> curveData;
1534
1535 // Get existing curve dates and prices. Messy but can't think of another way.
1536 vector<Date> ptsDates;
1537 if (auto ipc = QuantLib::ext::dynamic_pointer_cast<InterpolatedPriceCurve<Linear>>(pts)) {
1538 ptsDates = ipc->pillarDates();
1539 } else if (auto ipc = QuantLib::ext::dynamic_pointer_cast<InterpolatedPriceCurve<LogLinear>>(pts)) {
1540 ptsDates = ipc->pillarDates();
1541 } else if (auto ipc = QuantLib::ext::dynamic_pointer_cast<InterpolatedPriceCurve<Cubic>>(pts)) {
1542 ptsDates = ipc->pillarDates();
1543 } else if (auto ipc = QuantLib::ext::dynamic_pointer_cast<InterpolatedPriceCurve<LinearFlat>>(pts)) {
1544 ptsDates = ipc->pillarDates();
1545 } else if (auto ipc = QuantLib::ext::dynamic_pointer_cast<InterpolatedPriceCurve<LogLinearFlat>>(pts)) {
1546 ptsDates = ipc->pillarDates();
1547 } else if (auto ipc = QuantLib::ext::dynamic_pointer_cast<InterpolatedPriceCurve<CubicFlat>>(pts)) {
1548 ptsDates = ipc->pillarDates();
1549 } else {
1550 DLOG("Could not cast the price term structure so do not have its pillar dates.");
1551 }
1552
1553 // Add existing dates and prices to data.
1554 DLOG("Got " << ptsDates.size() << " pillar dates from the price curve.");
1555 for (const Date& d : ptsDates) {
1556 curveData[d] = pts->price(d);
1557 TLOG("Added (" << io::iso_date(d) << "," << fixed << setprecision(9) << curveData.at(d) << ").");
1558 }
1559
1560 // Add future price at option expiry to the curve i.e. override any interpolation between future option
1561 // expiry (oed) and future expiry (fed).
1562 auto futureExpiryCalculator = expCalc_;
1563 if (convention_ && !convention_->optionUnderlyingFutureConvention().empty()) {
1564 QuantLib::ext::shared_ptr<Conventions> conventions = InstrumentConventions::instance().conventions();
1565 auto [found, conv] =
1566 conventions->get(convention_->optionUnderlyingFutureConvention(), Convention::Type::CommodityFuture);
1567 if (found) {
1568 futureExpiryCalculator = QuantLib::ext::make_shared<ConventionsBasedFutureExpiry>(
1569 *QuantLib::ext::dynamic_pointer_cast<CommodityFutureConvention>(conv));
1570 }
1571 }
1572 for (const Date& oed : optionExpiries) {
1573 Date fed = futureExpiryCalculator->nextExpiry(true, oed, 0, false);
1574 // In general, expect that the future expiry date will already be in curveData but maybe not.
1575 auto it = curveData.find(fed);
1576 if (it != curveData.end()) {
1577 curveData[oed] = it->second;
1578 TLOG("Found future expiry in existing data. (Option Expiry,Future Expiry,Future Price) is ("
1579 << io::iso_date(oed) << "," << io::iso_date(fed) << "," << fixed << setprecision(9)
1580 << curveData.at(oed) << ").");
1581 } else {
1582 curveData[oed] = pts->price(fed);
1583 curveData[fed] = pts->price(fed);
1584 TLOG("Future expiry not found in existing data. (Option Expiry,Future Expiry,Future Price) is ("
1585 << io::iso_date(oed) << "," << io::iso_date(fed) << "," << fixed << setprecision(9)
1586 << curveData.at(oed) << ").");
1587 }
1588 }
1589
1590 // Gather data for building the "corrected" curve.
1591 vector<Date> curveDates;
1592 vector<Real> curvePrices;
1593 for (const auto& kv : curveData) {
1594 curveDates.push_back(kv.first);
1595 curvePrices.push_back(kv.second);
1596 }
1597 DayCounter dc = pts->dayCounter();
1598
1599 // Create the "corrected" curve. Again messy but can't think of another way.
1600 QuantLib::ext::shared_ptr<PriceTermStructure> cpts;
1601 if (auto ipc = QuantLib::ext::dynamic_pointer_cast<InterpolatedPriceCurve<Linear>>(pts)) {
1602 cpts = QuantLib::ext::make_shared<InterpolatedPriceCurve<Linear>>(asof, curveDates, curvePrices, dc, pts->currency());
1603 } else if (auto ipc = QuantLib::ext::dynamic_pointer_cast<InterpolatedPriceCurve<LogLinear>>(pts)) {
1604 cpts =
1605 QuantLib::ext::make_shared<InterpolatedPriceCurve<LogLinear>>(asof, curveDates, curvePrices, dc, pts->currency());
1606 } else if (auto ipc = QuantLib::ext::dynamic_pointer_cast<InterpolatedPriceCurve<Cubic>>(pts)) {
1607 cpts = QuantLib::ext::make_shared<InterpolatedPriceCurve<Cubic>>(asof, curveDates, curvePrices, dc, pts->currency());
1608 } else if (auto ipc = QuantLib::ext::dynamic_pointer_cast<InterpolatedPriceCurve<LinearFlat>>(pts)) {
1609 cpts =
1610 QuantLib::ext::make_shared<InterpolatedPriceCurve<LinearFlat>>(asof, curveDates, curvePrices, dc, pts->currency());
1611 } else if (auto ipc = QuantLib::ext::dynamic_pointer_cast<InterpolatedPriceCurve<LogLinearFlat>>(pts)) {
1612 cpts = QuantLib::ext::make_shared<InterpolatedPriceCurve<LogLinearFlat>>(asof, curveDates, curvePrices, dc,
1613 pts->currency());
1614 } else if (auto ipc = QuantLib::ext::dynamic_pointer_cast<InterpolatedPriceCurve<CubicFlat>>(pts)) {
1615 cpts =
1616 QuantLib::ext::make_shared<InterpolatedPriceCurve<CubicFlat>>(asof, curveDates, curvePrices, dc, pts->currency());
1617 } else {
1618 DLOG("Could not cast the price term structure so corrected curve is a linear InterpolatedPriceCurve.");
1619 cpts = QuantLib::ext::make_shared<InterpolatedPriceCurve<Linear>>(asof, curveDates, curvePrices, dc, pts->currency());
1620 }
1621 cpts->enableExtrapolation(pts->allowsExtrapolation());
1622
1623 LOG("CommodityVolCurve: finished adding future price correction at option expiry.");
1624
1625 return Handle<PriceTermStructure>(cpts);
1626}
1627
1628Date CommodityVolCurve::getExpiry(const Date& asof, const QuantLib::ext::shared_ptr<Expiry>& expiry, const string& name,
1629 Natural rollDays) const {
1630
1631 Date result;
1632
1633 if (auto expiryDate = QuantLib::ext::dynamic_pointer_cast<ExpiryDate>(expiry)) {
1634 result = expiryDate->expiryDate();
1635 } else if (auto expiryPeriod = QuantLib::ext::dynamic_pointer_cast<ExpiryPeriod>(expiry)) {
1636 // We may need more conventions here eventually.
1637 result = calendar_.adjust(asof + expiryPeriod->expiryPeriod());
1638 } else if (auto fcExpiry = QuantLib::ext::dynamic_pointer_cast<FutureContinuationExpiry>(expiry)) {
1639
1640 QL_REQUIRE(expCalc_, "CommodityVolCurve::getExpiry: need a future expiry calculator for continuation quotes.");
1641 QL_REQUIRE(convention_, "CommodityVolCurve::getExpiry: need a future convention for continuation quotes.");
1642 DLOG("Future option continuation expiry is " << *fcExpiry);
1643
1644 // Firstly, get the next option expiry on or after the asof date
1645 result = expCalc_->nextExpiry(true, asof, 0, true);
1646 TLOG("CommodityVolCurve::getExpiry: next option expiry relative to " << io::iso_date(asof) << " is "
1647 << io::iso_date(result) << ".");
1648
1649 // Market quotes may be delivered with a given number of roll days.
1650 if (rollDays > 0) {
1651 Date roll;
1652 roll = calendar_.advance(result, -static_cast<Integer>(rollDays), Days);
1653 TLOG("CommodityVolCurve::getExpiry: roll days is " << rollDays << " giving a roll date "
1654 << io::iso_date(roll) << ".");
1655 // Take the next option expiry if the roll days means the roll date is before asof.
1656 if (roll < asof) {
1657 result = expCalc_->nextExpiry(true, asof, 1, true);
1658 roll = calendar_.advance(result, -static_cast<Integer>(rollDays), Days);
1659 QL_REQUIRE(roll > asof, "CommodityVolCurve::getExpiry: expected roll to be greater than asof.");
1660 TLOG("CommodityVolCurve::getExpiry: roll date " << io::iso_date(roll) << " is less than asof "
1661 << io::iso_date(asof) << " so take next option expiry "
1662 << io::iso_date(result));
1663 }
1664 }
1665
1666 // At this stage, 'result' should hold the next option expiry on or after the asof date accounting for roll
1667 // days.
1668 TLOG("CommodityVolCurve::getExpiry: first option expiry is " << io::iso_date(result) << ".");
1669
1670 // If the continuation index is greater than 1 get the corresponding expiry.
1671 Natural fcIndex = fcExpiry->expiryIndex();
1672
1673 // The option continuation expiry may be mapped to another one.
1674 const auto& ocm = convention_->optionContinuationMappings();
1675 auto it = ocm.find(fcIndex);
1676 if (it != ocm.end())
1677 fcIndex = it->second;
1678
1679 if (fcIndex > 1) {
1680 result += 1 * Days;
1681 result = expCalc_->nextExpiry(true, result, fcIndex - 2, true);
1682 }
1683
1684 DLOG("Expiry date corresponding to continuation expiry, " << *fcExpiry <<
1685 ", is " << io::iso_date(result) << ".");
1686
1687 } else {
1688 QL_FAIL("CommodityVolCurve::getExpiry: cannot determine expiry type.");
1689 }
1690
1691 return result;
1692}
1693
1695 const map<string, QuantLib::ext::shared_ptr<YieldCurve>>& yieldCurves,
1696 const map<string, QuantLib::ext::shared_ptr<CommodityCurve>>& commodityCurves,
1697 bool searchYield, bool dontThrow) {
1698
1699 if (searchYield) {
1700 const string& ytsId = config.yieldCurveId();
1701 if (!ytsId.empty()) {
1702 auto itYts = yieldCurves.find(ytsId);
1703 if (itYts != yieldCurves.end()) {
1704 yts_ = itYts->second->handle();
1705 } else if (!dontThrow) {
1706 QL_FAIL("CommodityVolCurve: can't find yield curve with id " << ytsId);
1707 }
1708 } else if (!dontThrow) {
1709 QL_FAIL("CommodityVolCurve: YieldCurveId was not populated for " << config.curveID());
1710 }
1711 }
1712
1713 const string& ptsId = config.priceCurveId();
1714 if (!ptsId.empty()) {
1715 auto itPts = commodityCurves.find(ptsId);
1716 if (itPts != commodityCurves.end()) {
1717 pts_ = Handle<PriceTermStructure>(itPts->second->commodityPriceCurve());
1718 } else if (!dontThrow) {
1719 QL_FAIL("CommodityVolCurve: can't find price curve with id " << ptsId);
1720 }
1721 } else if (!dontThrow) {
1722 QL_FAIL("CommodityVolCurve: PriceCurveId was not populated for " << config.curveID());
1723 }
1724}
1725
1726vector<Real> CommodityVolCurve::checkMoneyness(const vector<string>& strMoneynessLevels) const {
1727
1728 using boost::adaptors::transformed;
1729 using boost::algorithm::join;
1730
1731 vector<Real> moneynessLevels = parseVectorOfValues<Real>(strMoneynessLevels, &parseReal);
1732 sort(moneynessLevels.begin(), moneynessLevels.end(), [](Real x, Real y) { return !close(x, y) && x < y; });
1733 QL_REQUIRE(adjacent_find(moneynessLevels.begin(), moneynessLevels.end(),
1734 [](Real x, Real y) { return close(x, y); }) == moneynessLevels.end(),
1735 "The configured moneyness levels contain duplicates");
1736 DLOG("Parsed " << moneynessLevels.size() << " unique configured moneyness levels.");
1737 DLOG("The moneyness levels are: " << join(
1738 moneynessLevels | transformed([](Real d) { return ore::data::to_string(d); }), ","));
1739
1740 return moneynessLevels;
1741}
1742void CommodityVolCurve::buildVolCalibrationInfo(const Date& asof, QuantLib::ext::shared_ptr<VolatilityConfig>& vc,
1744 const CommodityVolatilityConfig& config) {
1745 DLOG("CommodityVolCurve: building volatility calibration info");
1746 try{
1747
1748 ReportConfig rc = effectiveReportConfig(curveConfigs.reportConfigCommVols(), config.reportConfig());
1749 bool reportOnDeltaGrid = *rc.reportOnDeltaGrid();
1750 bool reportOnMoneynessGrid = *rc.reportOnMoneynessGrid();
1751 std::vector<Real> moneyness = *rc.moneyness();
1752 std::vector<std::string> deltas = *rc.deltas();
1753 std::vector<Period> expiries = *rc.expiries();
1754
1755
1756 auto info = QuantLib::ext::make_shared<FxEqCommVolCalibrationInfo>();
1757
1758 DeltaVolQuote::AtmType atmType = DeltaVolQuote::AtmType::AtmDeltaNeutral;
1759 DeltaVolQuote::DeltaType deltaType = DeltaVolQuote::DeltaType::Fwd;
1760
1761 if (auto vdsc = QuantLib::ext::dynamic_pointer_cast<VolatilityDeltaSurfaceConfig>(vc)) {
1762 atmType = parseAtmType(vdsc->atmType());
1763 deltaType = parseDeltaType(vdsc->deltaType());
1764 }
1765
1766 info->dayCounter = to_string(dayCounter_);
1767 info->calendar = to_string(calendar_).empty() ? "na" : calendar_.name();
1768 info->atmType = ore::data::to_string(atmType);
1769 info->deltaType = ore::data::to_string(deltaType);
1770 info->longTermAtmType = ore::data::to_string(atmType);
1771 info->longTermDeltaType = ore::data::to_string(deltaType);
1772 info->switchTenor = "na";
1773 info->riskReversalInFavorOf = "na";
1774 info->butterflyStyle = "na";
1775
1776 std::vector<Real> times, forwards;
1777 for (auto const& p : expiries) {
1778 auto d = volatility_->optionDateFromTenor(p);
1779 info->expiryDates.push_back(d);
1780 times.push_back(volatility_->dayCounter().empty() ? Actual365Fixed().yearFraction(asof, d)
1781 : volatility_->timeFromReference(d));
1782 forwards.push_back(pts_->price(d));
1783 }
1784
1785 info->times = times;
1786 info->forwards = forwards;
1787 std::vector<std::vector<Real>> callPricesDelta(times.size(), std::vector<Real>(deltas.size(), 0.0));
1788 if (reportOnDeltaGrid) {
1789 info->deltas = deltas;
1790 info->deltaCallPrices =
1791 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(deltas.size(), 0.0));
1792 info->deltaPutPrices =
1793 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(deltas.size(), 0.0));
1794 info->deltaGridStrikes =
1795 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(deltas.size(), 0.0));
1796 info->deltaGridProb = std::vector<std::vector<Real>>(times.size(), std::vector<Real>(deltas.size(), 0.0));
1797 info->deltaGridImpliedVolatility =
1798 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(deltas.size(), 0.0));
1799 info->deltaGridCallSpreadArbitrage =
1800 std::vector<std::vector<bool>>(times.size(), std::vector<bool>(deltas.size(), true));
1801 info->deltaGridButterflyArbitrage =
1802 std::vector<std::vector<bool>>(times.size(), std::vector<bool>(deltas.size(), true));
1803
1804 Real maxTime = QL_MAX_REAL;
1805 if (maxExpiry_ != Date()) {
1806 if (volatility_->dayCounter().empty())
1807 maxTime = Actual365Fixed().yearFraction(asof, maxExpiry_);
1808 else
1809 maxTime = volatility_->timeFromReference(maxExpiry_);
1810 }
1811
1812 DeltaVolQuote::AtmType at;
1813 DeltaVolQuote::DeltaType dt;
1814 for (Size i = 0; i < times.size(); ++i) {
1815 Real t = times[i];
1816 at = atmType;
1817 dt = deltaType;
1818 // for times after the last quoted expiry we use artificial conventions to avoid problems with strike (as in equities)
1819 // from delta conversions: we use fwd delta always and ATM DNS
1820 if (t > maxTime) {
1821 at = DeltaVolQuote::AtmDeltaNeutral;
1822 dt = DeltaVolQuote::Fwd;
1823 }
1824 bool validSlice = true;
1825 for (Size j = 0; j < deltas.size(); ++j) {
1826 DeltaString d(deltas[j]);
1827 try {
1828 Real strike;
1829 if (d.isAtm()) {
1830 strike =
1831 QuantExt::getAtmStrike(dt, at, pts_->price(asof,true), yts_->discount(t), 1., volatility_, t);
1832 } else if (d.isCall()) {
1833 strike = QuantExt::getStrikeFromDelta(Option::Call, d.delta(), dt, pts_->price(asof,true),
1834 yts_->discount(t), 1., volatility_, t);
1835 } else {
1836 strike = QuantExt::getStrikeFromDelta(Option::Put, d.delta(), dt, pts_->price(asof,true),
1837 yts_->discount(t), 1., volatility_, t);
1838 }
1839 Real stddev = std::sqrt(volatility_->blackVariance(t, strike));
1840 callPricesDelta[i][j] = QuantExt::blackFormula(Option::Call, strike, forwards[i], stddev);
1841
1842 if (d.isPut()) {
1843 info->deltaPutPrices[i][j] = blackFormula(Option::Put, strike, forwards[i], stddev, yts_->discount(t));
1844 } else {
1845 info->deltaCallPrices[i][j] = blackFormula(Option::Call, strike, forwards[i], stddev, yts_->discount(t));
1846 }
1847
1848 info->deltaGridStrikes[i][j] = strike;
1849 info->deltaGridImpliedVolatility[i][j] = stddev / std::sqrt(t);
1850 } catch (const std::exception& e) {
1851 validSlice = false;
1852 TLOG("CommodityVolCurve: error for time " << t << " delta " << deltas[j] << ": " << e.what());
1853 }
1854 }
1855 if (validSlice) {
1856 try {
1857 QuantExt::CarrMadanMarginalProbability cm(info->deltaGridStrikes[i], forwards[i],
1858 callPricesDelta[i]);
1859 info->deltaGridCallSpreadArbitrage[i] = cm.callSpreadArbitrage();
1860 info->deltaGridButterflyArbitrage[i] = cm.butterflyArbitrage();
1861 if (!cm.arbitrageFree())
1862 info->isArbitrageFree = false;
1863 info->deltaGridProb[i] = cm.density();
1865 } catch (const std::exception& e) {
1866 TLOG("error for time " << t << ": " << e.what());
1867 info->isArbitrageFree = false;
1868 TLOGGERSTREAM("..(invalid slice)..");
1869 }
1870 } else {
1871 info->isArbitrageFree = false;
1872 TLOGGERSTREAM("..(invalid slice)..");
1873 }
1874 }
1875 TLOG("CommodityVolCurve: Delta surface arbitrage analysis completed.");
1876 }
1877 std::vector<std::vector<Real>> callPricesMoneyness(times.size(), std::vector<Real>(moneyness.size(), 0.0));
1878 if (reportOnMoneynessGrid) {
1879 info->moneyness = moneyness;
1880 info->moneynessCallPrices =
1881 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(moneyness.size(), 0.0));
1882 info->moneynessPutPrices =
1883 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(moneyness.size(), 0.0));
1884 info->moneynessGridStrikes =
1885 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(moneyness.size(), 0.0));
1886 info->moneynessGridProb =
1887 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(moneyness.size(), 0.0));
1888 info->moneynessGridImpliedVolatility =
1889 std::vector<std::vector<Real>>(times.size(), std::vector<Real>(moneyness.size(), 0.0));
1890 info->moneynessGridCallSpreadArbitrage =
1891 std::vector<std::vector<bool>>(times.size(), std::vector<bool>(moneyness.size(), true));
1892 info->moneynessGridButterflyArbitrage =
1893 std::vector<std::vector<bool>>(times.size(), std::vector<bool>(moneyness.size(), true));
1894 info->moneynessGridCalendarArbitrage =
1895 std::vector<std::vector<bool>>(times.size(), std::vector<bool>(moneyness.size(), true));
1896 for (Size i = 0; i < times.size(); ++i) {
1897 Real t = times[i];
1898 for (Size j = 0; j < moneyness.size(); ++j) {
1899 try {
1900 Real strike = moneyness[j] * forwards[i];
1901 info->moneynessGridStrikes[i][j] = strike;
1902 Real stddev = std::sqrt(volatility_->blackVariance(t, strike));
1903 callPricesMoneyness[i][j] = blackFormula(Option::Call, strike, forwards[i], stddev);
1904 info->moneynessGridImpliedVolatility[i][j] = stddev / std::sqrt(t);
1905 if (moneyness[j] >= 1) {
1906 calibrationInfo_->moneynessCallPrices[i][j] = blackFormula(Option::Call, strike, forwards[i], stddev, yts_->discount(t));
1907 } else {
1908 calibrationInfo_->moneynessPutPrices[i][j] = blackFormula(Option::Put, strike, forwards[i], stddev, yts_->discount(t));
1909 };
1910 } catch (const std::exception& e) {
1911 TLOG("CommodityVolCurve: error for time " << t << " moneyness " << moneyness[j] << ": " << e.what());
1912 }
1913 }
1914 }
1915 if (!times.empty() && !moneyness.empty()) {
1916 try {
1917 QuantExt::CarrMadanSurface cm(times, moneyness, pts_->price(asof,true), forwards,
1918 callPricesMoneyness);
1919 for (Size i = 0; i < times.size(); ++i) {
1920 info->moneynessGridProb[i] = cm.timeSlices()[i].density();
1921 }
1922 info->moneynessGridCallSpreadArbitrage = cm.callSpreadArbitrage();
1923 info->moneynessGridButterflyArbitrage = cm.butterflyArbitrage();
1924 info->moneynessGridCalendarArbitrage = cm.calendarArbitrage();
1925 if (!cm.arbitrageFree())
1926 info->isArbitrageFree = false;
1927 TLOG("CommodityVolCurve: Moneyness surface Arbitrage analysis result:");
1929 } catch (const std::exception& e) {
1930 TLOG("CommodityVolCurve: error: " << e.what());
1931 info->isArbitrageFree = false;
1932 }
1933 TLOG("CommodityVolCurve: Moneyness surface Arbitrage analysis completed:");
1934 }
1935 }
1936 calibrationInfo_ = info;
1937 }
1938 catch (std::exception& e){
1939 QL_FAIL("CommodityVolCurve: calibration info building failed: " << e.what());
1940 } catch (...) {
1941 QL_FAIL("CommodityVolCurve: calibration info building failed: unknown error");
1942 }
1943 }
1944
1945
1946} // namespace data
1947} // namespace ore
const std::vector< bool > & butterflyArbitrage() const
const std::vector< bool > & callSpreadArbitrage() const
const std::vector< Real > & density() const
const std::vector< std::vector< bool > > & butterflyArbitrage() const
const std::vector< std::vector< bool > > & calendarArbitrage() const
const std::vector< std::vector< bool > > & callSpreadArbitrage() const
const std::vector< CarrMadanMarginalProbability > & timeSlices() const
Commodity curve description.
Definition: curvespec.hpp:413
void buildVolatility(const QuantLib::Date &asof, const CommodityVolatilityConfig &vc, const ConstantVolatilityConfig &cvc, const Loader &loader)
Build a volatility structure from a single constant volatility quote.
QuantLib::Date getExpiry(const QuantLib::Date &asof, const QuantLib::ext::shared_ptr< Expiry > &expiry, const std::string &name, QuantLib::Natural rollDays) const
Get an explicit expiry date from a commodity option quote's Expiry.
QuantLib::ext::shared_ptr< QuantExt::FutureExpiryCalculator > expCalc_
QuantLib::Handle< QuantExt::PriceTermStructure > pts_
QuantLib::ext::shared_ptr< CommodityFutureConvention > convention_
QuantLib::ext::shared_ptr< QuantLib::BlackVolTermStructure > volatility_
QuantLib::ext::shared_ptr< FxEqCommVolCalibrationInfo > calibrationInfo_
void buildVolCalibrationInfo(const Date &asof, QuantLib::ext::shared_ptr< VolatilityConfig > &volatilityConfig, const CurveConfigurations &curveConfigs, const CommodityVolatilityConfig &config)
Build the calibration info.
QuantLib::Handle< QuantExt::PriceTermStructure > correctFuturePriceCurve(const QuantLib::Date &asof, const std::string &contractName, const QuantLib::ext::shared_ptr< QuantExt::PriceTermStructure > &pts, const std::vector< QuantLib::Date > &optionExpiries) const
void populateCurves(const CommodityVolatilityConfig &config, const std::map< std::string, QuantLib::ext::shared_ptr< YieldCurve > > &yieldCurves, const std::map< std::string, QuantLib::ext::shared_ptr< CommodityCurve > > &commodityCurves, bool searchYield, bool dontThrow=false)
Populate price curve, pts_, and yield curve, yts_.
const CommodityVolatilityCurveSpec & spec() const
CommodityVolCurve()
Default constructor.
QuantLib::Handle< QuantLib::YieldTermStructure > yts_
std::vector< QuantLib::Real > checkMoneyness(const std::vector< std::string > &moneynessLevels) const
Check and return moneyness levels.
void buildVolatilityExplicit(const QuantLib::Date &asof, CommodityVolatilityConfig &vc, const VolatilityStrikeSurfaceConfig &vssc, const Loader &loader, const std::vector< QuantLib::Real > &configuredStrikes)
QuantLib::DayCounter dayCounter_
Commodity volatility configuration.
const std::string & futureConventionsId() const
const boost::optional< bool > & preferOutOfTheMoney() const
Commodity volatility description.
Definition: curvespec.hpp:440
Correlation curve description.
Definition: curvespec.hpp:467
const string & curveID() const
Definition: curveconfig.hpp:54
virtual const vector< string > & quotes()
Return all the market quotes required for this config.
Definition: curveconfig.hpp:69
Container class for all Curve Configurations.
const std::string & curveConfigID() const
Definition: curvespec.hpp:83
string name() const
returns the unique curve name
Definition: curvespec.hpp:78
Utility class for handling delta strings ATM, 10P, 25C, ... used e.g. for FX Surfaces.
Definition: strike.hpp:81
bool isCall() const
Definition: strike.hpp:86
bool isAtm() const
Definition: strike.hpp:84
Real delta() const
Definition: strike.hpp:87
bool isPut() const
Definition: strike.hpp:85
FX Spot description.
Definition: curvespec.hpp:263
FX Volatility curve description.
Definition: curvespec.hpp:288
Market data loader base class.
Definition: loader.hpp:47
virtual QuantLib::ext::shared_ptr< MarketDatum > get(const std::string &name, const QuantLib::Date &d) const
get quote by its unique name, throws if not existent, override in derived classes for performance
Definition: loader.cpp:24
QuantLib::Handle< QuantExt::FxIndex > fxIndex(const string &fxIndex, const string &configuration=Market::defaultConfiguration) const
Definition: market.cpp:151
const std::string & correlationCurve() const
const std::string & fxVolatilityCurve() const
const std::string & proxyVolatilityCurve() const
const MarketDatum::QuoteType & quoteType() const
const QuantLib::Exercise::Type & exerciseType() const
const boost::optional< bool > reportOnDeltaGrid() const
const boost::optional< std::vector< std::string > > & deltas() const
const boost::optional< bool > reportOnMoneynessGrid() const
const boost::optional< std::vector< Period > > & expiries() const
const boost::optional< std::vector< Real > > & moneyness() const
const std::string & interpolation() const
const std::string & extrapolation() const
const std::vector< std::string > & quotes() const
const std::vector< std::string > & putDeltas() const
const std::vector< std::string > & callDeltas() const
const std::vector< std::string > & expiries() const
const std::string & atmDeltaType() const
const std::vector< std::string > & expiries() const
const std::vector< std::string > & strikes() const
const std::string & timeInterpolation() const
const std::string & strikeInterpolation() const
const std::string & strikeExtrapolation() const
const std::string & timeExtrapolation() const
Wrapper class for building commodity volatility structures.
SafeStack< ValueType > value
SafeStack< Filter > filter
Base class for classes that perform date calculations for future contracts.
Calendar parseCalendar(const string &s)
Convert text to QuantLib::Calendar.
Definition: parsers.cpp:157
Period parsePeriod(const string &s)
Convert text to QuantLib::Period.
Definition: parsers.cpp:171
DeltaVolQuote::AtmType parseAtmType(const std::string &s)
Convert text to QuantLib::DeltaVolQuote::AtmType.
Definition: parsers.cpp:746
Real parseReal(const string &s)
Convert text to Real.
Definition: parsers.cpp:112
DayCounter parseDayCounter(const string &s)
Convert text to QuantLib::DayCounter.
Definition: parsers.cpp:209
DeltaVolQuote::DeltaType parseDeltaType(const std::string &s)
Convert text to QuantLib::DeltaVolQuote::DeltaType.
Definition: parsers.cpp:763
Map text representations to QuantLib/QuantExt types.
Classes and functions for log message handling.
@ data
Definition: log.hpp:77
#define LOG(text)
Logging Macro (Level = Notice)
Definition: log.hpp:552
#define DLOG(text)
Logging Macro (Level = Debug)
Definition: log.hpp:554
#define TLOGGERSTREAM(text)
Definition: log.hpp:633
#define ORE_DATA
Definition: log.hpp:33
#define WLOG(text)
Logging Macro (Level = Warning)
Definition: log.hpp:550
#define TLOG(text)
Logging Macro (Level = Data)
Definition: log.hpp:556
Real getAtmStrike(DeltaVolQuote::DeltaType dt, DeltaVolQuote::AtmType at, Real spot, Real domDiscount, Real forDiscount, boost::shared_ptr< BlackVolTermStructure > vol, Real t, Real accuracy, Size maxIterations)
RandomVariable exp(RandomVariable x)
std::ostream & operator<<(std::ostream &out, EquityReturnType t)
std::string arbitrageAsString(const CarrMadanMarginalProbabilityClass &cm)
Real getStrikeFromDelta(Option::Type optionType, Real delta, DeltaVolQuote::DeltaType dt, Real spot, Real domDiscount, Real forDiscount, boost::shared_ptr< BlackVolTermStructure > vol, Real t, Real accuracy, Size maxIterations)
MoneynessStrike::Type parseMoneynessType(const string &type)
Parse MoneynessStrike::Type from type.
Definition: strike.cpp:252
ReportConfig effectiveReportConfig(const ReportConfig &globalConfig, const ReportConfig &localConfig)
std::string to_string(const LocationInfo &l)
Definition: ast.cpp:28
QuantLib::ext::shared_ptr< QuantExt::CommodityIndex > parseCommodityIndex(const string &name, bool hasPrefix, const Handle< PriceTermStructure > &ts, const Calendar &cal, const bool enforceFutureIndex)
Extrapolation parseExtrapolation(const string &s)
Parse Extrapolation from string.
Definition: parsers.cpp:778
QuantLib::ext::shared_ptr< Expiry > parseExpiry(const string &strExpiry)
Parse an Expiry from its string representation, strExpiry.
Definition: expiry.cpp:110
boost::optional< Wildcard > getUniqueWildcard(const C &c)
checks if at most one element in C has a wild card and returns it in this case
Definition: wildcard.hpp:65
Serializable Credit Default Swap.
Definition: namespaces.docs:23
Map text representations to QuantLib/QuantExt types.
vector< Real > strikes
vector< string > curveConfigs
string conversion utilities
string name
utilities for wildcard handling