Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
commoditycurve.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2018 Quaternion Risk Management Ltd
3 All rights reserved.
4
5 This file is part of ORE, a free-software/open-source library
6 for transparent pricing and risk analysis - http://opensourcerisk.org
7
8 ORE is free software: you can redistribute it and/or modify it
9 under the terms of the Modified BSD License. You should have received a
10 copy of the license along with this program.
11 The license is also available online at <http://opensourcerisk.org>
12
13 This program is distributed on the basis that it will form a useful
14 contribution to risk analytics and model standardisation, but WITHOUT
15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
17*/
18
19#include <algorithm>
20#include <boost/algorithm/string.hpp>
35#include <ql/time/calendars/weekendsonly.hpp>
36#include <ql/time/date.hpp>
37
53using QuantLib::BootstrapHelper;
54using std::make_pair;
55using std::map;
56using std::string;
57
58// Explicit template instantiation to avoid "error C2079: ... uses undefined class ..."
59// Explained in the answer to the SO question here:
60// https://stackoverflow.com/a/57666066/1771882
61// Needs to be in global namespace also i.e. not under ore::data
62// https://stackoverflow.com/a/25594741/1771882
70
71namespace {
72
75using QuantLib::Date;
76using QuantLib::Error;
77using QuantLib::io::iso_date;
78using QuantLib::Real;
79
80void addMarketFixing(const string& idxConvId, const Date& expiry, Real value) {
81 QuantLib::ext::shared_ptr<Conventions> conventions = ore::data::InstrumentConventions::instance().conventions();
82 auto p = conventions->get(idxConvId, Convention::Type::CommodityFuture);
83 if (p.first) {
84 auto idx = ore::data::parseCommodityIndex(idxConvId, false);
85 idx = idx->clone(expiry);
86 if (idx->isValidFixingDate(expiry)) {
87 try {
88 idx->addFixing(expiry, value);
89 TLOG("Added fixing (" << iso_date(expiry) << "," << idx->name() << "," << value << ").");
90 } catch (const Error& e) {
91 TLOG("Failed to add fixing (" << iso_date(expiry) << "," << idx->name() << "," <<
92 value << "): " << e.what());
93 }
94 } else {
95 TLOG("Failed to add fixing (" << iso_date(expiry) << "," << idx->name() << "," <<
96 value << ") because " << iso_date(expiry) << " is not a valid fixing date.");
97 }
98 } else {
99 TLOG("Failed to add fixing because no commodity future convention for " << idxConvId << ".");
100 }
101}
102
103}
104
105namespace ore {
106namespace data {
107
108CommodityCurve::CommodityCurve()
109 : commoditySpot_(Null<Real>()), onValue_(Null<Real>()), tnValue_(Null<Real>()), regexQuotes_(false) {}
110
111CommodityCurve::CommodityCurve(const Date& asof, const CommodityCurveSpec& spec, const Loader& loader,
113 const FXTriangulation& fxSpots,
114 const map<string, QuantLib::ext::shared_ptr<YieldCurve>>& yieldCurves,
115 const map<string, QuantLib::ext::shared_ptr<CommodityCurve>>& commodityCurves,
116 bool const buildCalibrationInfo)
117 : spec_(spec), commoditySpot_(Null<Real>()), onValue_(Null<Real>()), tnValue_(Null<Real>()), regexQuotes_(false) {
118
119 try {
120
121 QuantLib::ext::shared_ptr<CommodityCurveConfig> config = curveConfigs.commodityCurveConfig(spec_.curveConfigID());
122
123 dayCounter_ = config->dayCountId().empty() ? Actual365Fixed() : parseDayCounter(config->dayCountId());
124 interpolationMethod_ = config->interpolationMethod().empty() ? "Linear" : config->interpolationMethod();
125
126 if (config->type() == CommodityCurveConfig::Type::Direct) {
127
128 // Populate the raw price curve data
129 map<Date, Handle<Quote>> data;
130 populateData(data, asof, config, loader);
131
132 // Create the commodity price curve
133 buildCurve(asof, data, config);
134
135 } else if (config->type() == CommodityCurveConfig::Type::Basis) {
136
137 // We have a commodity basis configuration
138
139 // Look up the required base price curve in the commodityCurves map
140 CommodityCurveSpec ccSpec(config->currency(), config->basePriceCurveId());
141 DLOG("Looking for base price curve with id, " << config->basePriceCurveId() << ", and spec, " << ccSpec
142 << ".");
143 auto itCc = commodityCurves.find(ccSpec.name());
144 QL_REQUIRE(itCc != commodityCurves.end(), "Can't find price curve with id " << config->basePriceCurveId());
145 auto pts = Handle<PriceTermStructure>(itCc->second->commodityPriceCurve());
146
147 buildBasisPriceCurve(asof, *config, pts, loader);
148
149 } else if (config->type() == CommodityCurveConfig::Type::Piecewise) {
150
151 // We have a piecewise commodity configuration
152 buildPiecewiseCurve(asof, *config, loader, commodityCurves);
153
154 } else {
155
156 // We have a cross currency type commodity curve configuration
157 QuantLib::ext::shared_ptr<CommodityCurveConfig> baseConfig =
158 curveConfigs.commodityCurveConfig(config->basePriceCurveId());
159
160 buildCrossCurrencyPriceCurve(asof, config, baseConfig, fxSpots, yieldCurves, commodityCurves);
161 }
162
163 // Apply extrapolation from the curve configuration
164 commodityPriceCurve_->enableExtrapolation(config->extrapolation());
165
166 // Ask for price now so that errors are thrown during the build, not later.
167 commodityPriceCurve_->price(asof + 1 * Days);
168
169
170 Handle<PriceTermStructure> pts(commodityPriceCurve_);
172 commodityPriceCurve_->pillarDates();
173
174 if (buildCalibrationInfo) { // the curve is built, save info for later usage
175 auto calInfo = QuantLib::ext::make_shared<CommodityCurveCalibrationInfo>();
176 calInfo->dayCounter = dayCounter_.name();
177 calInfo->interpolationMethod = interpolationMethod_;
178 calInfo->calendar = commodityPriceCurve_->calendar().name();
179 calInfo->currency = commodityPriceCurve_->currency().code();
180 for (auto d : commodityPriceCurve_->pillarDates()){
181 calInfo->times.emplace_back(commodityPriceCurve_->timeFromReference(d));
182 calInfo->pillarDates.emplace_back(d);
183 calInfo->futurePrices.emplace_back(commodityPriceCurve_->price(d, true));
184 }
185 calibrationInfo_ = calInfo;
186 }
187 } catch (std::exception& e) {
188 QL_FAIL("commodity curve building failed: " << e.what());
189 } catch (...) {
190 QL_FAIL("commodity curve building failed: unknown error");
191 }
192}
193
194void CommodityCurve::populateData(map<Date, Handle<Quote>>& data, const Date& asof,
195 const QuantLib::ext::shared_ptr<CommodityCurveConfig>& config, const Loader& loader) {
196
197 // Some default conventions for building the commodity curve
198 Period spotTenor = 2 * Days;
199 Real pointsFactor = 1.0;
200
201 Calendar cal = parseCalendar(config->currency());
202 bool spotRelative = true;
203 BusinessDayConvention bdc = Following;
204 bool outright = true;
205
206 QuantLib::ext::shared_ptr<Conventions> conventions = InstrumentConventions::instance().conventions();
207
208 // Overwrite the default conventions if the commodity curve config provides explicit conventions
209 if (!config->conventionsId().empty()) {
210 QL_REQUIRE(conventions->has(config->conventionsId()),
211 "Commodity conventions " << config->conventionsId() << " requested by commodity config "
212 << config->curveID() << " not found");
213 auto convention =
214 QuantLib::ext::dynamic_pointer_cast<CommodityForwardConvention>(conventions->get(config->conventionsId()));
215 QL_REQUIRE(convention, "Convention " << config->conventionsId() << " not of expected type CommodityConvention");
216
217 spotTenor = convention->spotDays() * Days;
218 pointsFactor = convention->pointsFactor();
219 if (!convention->strAdvanceCalendar().empty())
220 cal = convention->advanceCalendar();
221 spotRelative = convention->spotRelative();
222 bdc = convention->bdc();
223 outright = convention->outright();
224 }
225
226 // Commodity spot quote if provided by the configuration
227 Date spotDate = cal.advance(asof, spotTenor);
228 if (config->commoditySpotQuoteId().empty()) {
229 QL_REQUIRE(outright, "If the commodity forward quotes are not outright,"
230 << " a commodity spot quote needs to be configured");
231 } else {
232 auto spot = loader.get(config->commoditySpotQuoteId(), asof)->quote();
233 commoditySpot_ = spot->value();
234 data[spotDate] = spot;
235 }
236
237 // Add the forward quotes to the curve data
238 for (auto& q : getQuotes(asof, config->curveID(), config->fwdQuotes(), loader)) {
239
240 // We add ON and TN quotes after this loop if they are given and not outright quotes
241 TLOG("Commodity Forward Price found for quote: " << q->name());
242 Date expiry;
243 Real value = q->quote()->value();
244 if (!q->tenorBased()) {
245 expiry = q->expiryDate();
246 add(asof, expiry, value, data, outright, pointsFactor);
247 } else {
248 if (q->startTenor() == boost::none) {
249 expiry = cal.advance(spotRelative ? spotDate : asof, q->tenor(), bdc);
250 add(asof, expiry, value, data, outright, pointsFactor);
251 } else {
252 if (*q->startTenor() == 0 * Days && q->tenor() == 1 * Days) {
253 onValue_ = q->quote()->value();
254 if (outright)
255 add(asof, asof, value, data, outright);
256 } else if (*q->startTenor() == 1 * Days && q->tenor() == 1 * Days) {
257 tnValue_ = q->quote()->value();
258 if (outright) {
259 expiry = cal.advance(asof, 1 * Days, bdc);
260 add(asof, expiry, value, data, outright);
261 }
262 } else {
263 expiry = cal.advance(cal.advance(asof, *q->startTenor(), bdc), q->tenor(), bdc);
264 add(asof, expiry, value, data, outright, pointsFactor);
265 }
266 }
267 }
268 }
269
270 // Deal with ON and TN if quotes are not outright quotes
271 if (spotTenor == 2 * Days && tnValue_ != Null<Real>() && !outright) {
272 add(asof, cal.advance(asof, 1 * Days, bdc), -tnValue_, data, outright, pointsFactor);
273 if (onValue_ != Null<Real>()) {
274 add(asof, asof, -onValue_ - tnValue_, data, outright, pointsFactor);
275 }
276 }
277
278 // Some logging and checks
279 LOG("Read " << data.size() << " quotes for commodity curve " << config->curveID());
280 if (!regexQuotes_) {
281 QL_REQUIRE(data.size() == config->quotes().size(), "Found " << data.size() << " quotes, but "
282 << config->quotes().size()
283 << " quotes given in config " << config->curveID());
284 } else {
285 QL_REQUIRE(data.size() > 0,
286 "Regular expression specified in commodity config " << config->curveID() << " but no quotes read");
287 }
288}
289
290void CommodityCurve::add(const Date& asof, const Date& expiry, Real value, map<Date, Handle<Quote>>& data, bool outright,
291 Real pointsFactor) {
292
293 if (expiry < asof)
294 return;
295
296 if (data.find(expiry) != data.end()) {
297 WLOG("building " << spec_.name() << ": skipping duplicate expiry " << io::iso_date(expiry));
298 return;
299 }
300
301 if (!outright) {
302 QL_REQUIRE(commoditySpot_ != Null<Real>(), "Can't use forward points without a commodity spot value");
303 value = commoditySpot_ + value / pointsFactor;
304 }
305
306 data[expiry] = Handle<Quote>(QuantLib::ext::make_shared<SimpleQuote>(value));
307}
308
309void CommodityCurve::buildCurve(const Date& asof, const map<Date, Handle<Quote>>& data,
310 const QuantLib::ext::shared_ptr<CommodityCurveConfig>& config) {
311
312 vector<Date> curveDates;
313 curveDates.reserve(data.size());
314 vector<Handle<Quote>> curvePrices;
315 curvePrices.reserve(data.size());
316 for (auto const& datum : data) {
317 curveDates.push_back(datum.first);
318 curvePrices.push_back(datum.second);
319 }
320
321 // Build the curve using the data
322 populateCurve<InterpolatedPriceCurve>(asof, curveDates, curvePrices, dayCounter_,
323 parseCurrency(config->currency()));
324}
325
327 const Date& asof, const QuantLib::ext::shared_ptr<CommodityCurveConfig>& config,
328 const QuantLib::ext::shared_ptr<CommodityCurveConfig>& baseConfig, const FXTriangulation& fxSpots,
329 const map<string, QuantLib::ext::shared_ptr<YieldCurve>>& yieldCurves,
330 const map<string, QuantLib::ext::shared_ptr<CommodityCurve>>& commodityCurves) {
331
332 // Look up the required base price curve in the commodityCurves map
333 // We pass in the commodity curve ID only in the member basePriceCurveId of config e.g. PM:XAUUSD.
334 // But, the map commodityCurves is keyed on the spec name e.g. Commodity/USD/PM:XAUUSD
335 auto commIt = commodityCurves.find(CommodityCurveSpec(baseConfig->currency(), baseConfig->curveID()).name());
336 QL_REQUIRE(commIt != commodityCurves.end(), "Could not find base commodity curve with id "
337 << baseConfig->curveID()
338 << " required in the building of commodity curve with id "
339 << config->curveID());
340
341 // Look up the two yield curves in the yieldCurves map
342 auto baseYtsIt = yieldCurves.find(YieldCurveSpec(baseConfig->currency(), config->baseYieldCurveId()).name());
343 QL_REQUIRE(baseYtsIt != yieldCurves.end(),
344 "Could not find base yield curve with id "
345 << config->baseYieldCurveId() << " and currency " << baseConfig->currency()
346 << " required in the building of commodity curve with id " << config->curveID());
347
348 auto ytsIt = yieldCurves.find(YieldCurveSpec(config->currency(), config->yieldCurveId()).name());
349 QL_REQUIRE(ytsIt != yieldCurves.end(), "Could not find yield curve with id "
350 << config->yieldCurveId() << " and currency " << config->currency()
351 << " required in the building of commodity curve with id "
352 << config->curveID());
353
354 // Get the FX spot rate, number of units of this currency per unit of base currency
355 Handle<Quote> fxSpot = fxSpots.getQuote(baseConfig->currency() + config->currency());
356
357 // Populate the commodityPriceCurve_ member
358 commodityPriceCurve_ = QuantLib::ext::make_shared<CrossCurrencyPriceTermStructure>(
359 asof, QuantLib::Handle<PriceTermStructure>(commIt->second->commodityPriceCurve()), fxSpot,
360 baseYtsIt->second->handle(), ytsIt->second->handle(), parseCurrency(config->currency()));
361}
362
364 const Handle<PriceTermStructure>& basePts, const Loader& loader) {
365
366 LOG("CommodityCurve: start building commodity basis curve.");
367
368 QL_REQUIRE(!basePts.empty() && basePts.currentLink() != nullptr,
369 "Internal error: Can not build commodityBasisCurve '" << config.curveID() << "'without empty baseCurve");
370
371 QuantLib::ext::shared_ptr<Conventions> conventions = InstrumentConventions::instance().conventions();
372
373 // We need to have commodity future conventions for both the base curve and the basis curve
374 QL_REQUIRE(conventions->has(config.conventionsId()), "Commodity conventions " << config.conventionsId()
375 << " requested by commodity config "
376 << config.curveID() << " not found");
377 auto basisConvention =
378 QuantLib::ext::dynamic_pointer_cast<CommodityFutureConvention>(conventions->get(config.conventionsId()));
379 QL_REQUIRE(basisConvention,
380 "Convention " << config.conventionsId() << " not of expected type CommodityFutureConvention");
381 auto basisFec = QuantLib::ext::make_shared<ConventionsBasedFutureExpiry>(*basisConvention);
382
383 QL_REQUIRE(conventions->has(config.baseConventionsId()),
384 "Commodity conventions " << config.baseConventionsId() << " requested by commodity config "
385 << config.curveID() << " not found");
386 auto baseConvention =
387 QuantLib::ext::dynamic_pointer_cast<CommodityFutureConvention>(conventions->get(config.baseConventionsId()));
388 QL_REQUIRE(baseConvention,
389 "Convention " << config.baseConventionsId() << " not of expected type CommodityFutureConvention");
390 auto baseFec = QuantLib::ext::make_shared<ConventionsBasedFutureExpiry>(*baseConvention);
391
392 // Construct the commodity index.
393 auto baseIndex = parseCommodityIndex(baseConvention->id(), false, basePts);
394
395
396 // Sort the configured quotes on expiry dates
397 // Ignore tenor based quotes i.e. we expect an explicit expiry date and log a warning if the expiry date does not
398 // match our own calculated expiry date based on the basis conventions.
399 map<Date, Handle<Quote>> basisData;
400 for (auto& q : getQuotes(asof, config.curveID(), config.fwdQuotes(), loader, true)) {
401
402 QL_REQUIRE(basisData.find(q->expiryDate()) == basisData.end(), "Found duplicate quote, "
403 << q->name() << ", for expiry date "
404 << io::iso_date(q->expiryDate()) << ".");
405
406 basisData[q->expiryDate()] = q->quote();
407 TLOG("Using quote " << q->name() << " in commodity basis curve.");
408
409 // We expect the expiry date in the quotes to match our calculated expiry date. The code will work if it does
410 // not but we log a warning in this case.
411 Date calcExpiry = basisFec->nextExpiry(true, q->expiryDate());
412 if (calcExpiry != q->expiryDate()) {
413 WLOG("Calculated expiry date, " << io::iso_date(calcExpiry) << ", does not equal quote's expiry date "
414 << io::iso_date(q->expiryDate()) << ".");
415 }
416 }
417
418 if (basisConvention->isAveraging()) {
419 // We are building a curve that will be used to return an average price.
420 if (!baseConvention->isAveraging() && config.averageBase()) {
421 DLOG("Creating a CommodityAverageBasisPriceCurve.");
422 populateCurve<CommodityAverageBasisPriceCurve>(asof, basisData, basisFec, baseIndex, baseFec,
423 config.addBasis(), config.priceAsHistFixing());
424 } else {
425 // Either 1) base convention is not averaging and config.averageBase() is false or 2) the base convention
426 // is averaging. Either way, we build a CommodityBasisPriceCurve.
427 DLOG("Creating a CommodityBasisPriceCurve for an average price curve.");
428 populateCurve<CommodityBasisPriceCurve>(asof, basisData, basisFec, baseIndex, baseFec,
429 config.addBasis(), config.monthOffset(), config.priceAsHistFixing());
430 }
431 } else {
432 // We are building a curve that will be used to return a price on a single date.
433 QL_REQUIRE(!baseConvention->isAveraging(), "A commodity basis curve with non-averaging"
434 << " basis and averaging base is not valid.");
435
436 populateCurve<CommodityBasisPriceCurve>(asof, basisData, basisFec, baseIndex, baseFec, config.addBasis(),
437 config.monthOffset(), config.priceAsHistFixing());
438 }
439
440 LOG("CommodityCurve: finished building commodity basis curve.");
441}
442
443// Allow for more readable code in method below.
445template<class C> using BS = QuantExt::IterativeBootstrap<C>;
446
448 const Loader& loader, const map<string, QuantLib::ext::shared_ptr<CommodityCurve>>& commodityCurves) {
449
450 LOG("CommodityCurve: start building commodity piecewise curve.");
451
452 // We store the instruments in a map. The key is the instrument's pillar date. The segments are ordered in
453 // priority so if we encounter the same pillar date later, we ignore it with a debug log.
454 map<Date, QuantLib::ext::shared_ptr<Helper>> mpInstruments;
455 const auto& priceSegments = config.priceSegments();
456 QL_REQUIRE(!priceSegments.empty(), "CommodityCurve: need at least one price segment to build piecewise curve.");
457 for (const auto& kv : priceSegments) {
458 if (kv.second.type() != PriceSegment::Type::OffPeakPowerDaily) {
459 addInstruments(asof, loader, config.curveID(), config.currency(), kv.second,
460 commodityCurves, mpInstruments);
461 } else {
462 addOffPeakPowerInstruments(asof, loader, config.curveID(), kv.second, mpInstruments);
463 }
464 }
465
466 // Populate the vector of helpers.
467 vector<QuantLib::ext::shared_ptr<Helper>> instruments;
468 instruments.reserve(mpInstruments.size());
469 for (const auto& kv : mpInstruments) {
470 instruments.push_back(kv.second);
471 }
472
473 // Use bootstrap configuration if provided.
475 if (config.bootstrapConfig()) {
476 bc = *config.bootstrapConfig();
477 }
478 Real acc = bc.accuracy();
479 Real globalAcc = bc.globalAccuracy();
480 bool noThrow = bc.dontThrow();
481 Size maxAttempts = bc.maxAttempts();
482 Real maxF = bc.maxFactor();
483 Real minF = bc.minFactor();
484 Size noThrowSteps = bc.dontThrowSteps();
485
486 // Create curve based on interpolation method provided.
487 Currency ccy = parseCurrency(config.currency());
488 if (interpolationMethod_ == "Linear") {
489 BS<Crv<Linear>> bs(acc, globalAcc, noThrow, maxAttempts, maxF, minF, noThrowSteps);
490 commodityPriceCurve_ = QuantLib::ext::make_shared<Crv<Linear>>(asof, instruments, dayCounter_, ccy, Linear(), bs);
491 } else if (interpolationMethod_ == "LogLinear") {
492 BS<Crv<QuantLib::LogLinear>> bs(acc, globalAcc, noThrow, maxAttempts, maxF, minF, noThrowSteps);
493 commodityPriceCurve_ = QuantLib::ext::make_shared<Crv<QuantLib::LogLinear>>(asof, instruments,
494 dayCounter_, ccy, QuantLib::LogLinear(), bs);
495 } else if (interpolationMethod_ == "Cubic") {
496 BS<Crv<QuantLib::Cubic>> bs(acc, globalAcc, noThrow, maxAttempts, maxF, minF, noThrowSteps);
497 commodityPriceCurve_ = QuantLib::ext::make_shared<Crv<QuantLib::Cubic>>(asof, instruments,
498 dayCounter_, ccy, QuantLib::Cubic(), bs);
499 } else if (interpolationMethod_ == "LinearFlat") {
500 BS<Crv<QuantExt::LinearFlat>> bs(acc, globalAcc, noThrow, maxAttempts, maxF, minF, noThrowSteps);
501 commodityPriceCurve_ = QuantLib::ext::make_shared<Crv<QuantExt::LinearFlat>>(asof, instruments,
503 } else if (interpolationMethod_ == "LogLinearFlat") {
504 BS<Crv<QuantExt::LogLinearFlat>> bs(acc, globalAcc, noThrow, maxAttempts, maxF, minF, noThrowSteps);
505 commodityPriceCurve_ = QuantLib::ext::make_shared<Crv<QuantExt::LogLinearFlat>>(asof, instruments,
507 } else if (interpolationMethod_ == "CubicFlat") {
508 BS<Crv<QuantExt::CubicFlat>> bs(acc, globalAcc, noThrow, maxAttempts, maxF, minF, noThrowSteps);
509 commodityPriceCurve_ = QuantLib::ext::make_shared<Crv<QuantExt::CubicFlat>>(asof, instruments,
510 dayCounter_, ccy, QuantExt::CubicFlat(), bs);
511 } else if (interpolationMethod_ == "BackwardFlat") {
512 BS<Crv<BackwardFlat>> bs(acc, globalAcc, noThrow, maxAttempts, maxF, minF, noThrowSteps);
513 commodityPriceCurve_ = QuantLib::ext::make_shared<Crv<BackwardFlat>>(asof, instruments,
514 dayCounter_, ccy, BackwardFlat(), bs);
515 } else {
516 QL_FAIL("The interpolation method, " << interpolationMethod_ << ", is not supported.");
517 }
518
519 LOG("CommodityCurve: finished building commodity piecewise curve.");
520}
521
522vector<QuantLib::ext::shared_ptr<CommodityForwardQuote>>
523CommodityCurve::getQuotes(const Date& asof, const string& /*configId*/, const vector<string>& quotes,
524 const Loader& loader, bool filter) {
525
526 LOG("CommodityCurve: start getting configured commodity quotes.");
527
528 // Check if we are using a regular expression to select the quotes for the curve. If we are, the quotes should
529 // contain exactly one element.
530 auto wildcard = getUniqueWildcard(quotes);
531 regexQuotes_ = wildcard != boost::none;
532
533 std::set<QuantLib::ext::shared_ptr<MarketDatum>> data;
534 if (wildcard) {
535 data = loader.get(*wildcard, asof);
536 } else {
537 std::ostringstream ss;
539 Wildcard w(ss.str());
540 data = loader.get(w, asof);
541 }
542
543 // Add the relevant forward quotes to the result vector
544 vector<QuantLib::ext::shared_ptr<CommodityForwardQuote>> result;
545 for (const auto& md : data) {
546 QL_REQUIRE(md->asofDate() == asof, "MarketDatum asofDate '" << md->asofDate() << "' <> asof '" << asof << "'");
547
548 // Only looking for quotes on asof date, with quote type PRICE and instrument type commodity forward
549
550 QuantLib::ext::shared_ptr<CommodityForwardQuote> q = QuantLib::ext::dynamic_pointer_cast<CommodityForwardQuote>(md);
551 QL_REQUIRE(q, "Internal error: could not downcast MarketDatum '" << md->name() << "' to CommodityForwardQuote");
552
553 if (!wildcard) {
554 vector<string>::const_iterator it =
555 find(quotes.begin(), quotes.end(), q->name());
556 if (it == quotes.end())
557 continue;
558 }
559
560 // If filter is true, remove tenor based quotes and quotes with expiry before asof.
561 if (filter) {
562 if (q->tenorBased()) {
563 TLOG("Skipping tenor based quote, " << q->name() << ".");
564 continue;
565 }
566 if (q->expiryDate() < asof) {
567 TLOG("Skipping quote because its expiry date, " << io::iso_date(q->expiryDate())
568 << ", is before the market date " << io::iso_date(asof));
569 continue;
570 }
571 }
572
573 // If we make it here, the quote is relevant.
574 result.push_back(q);
575 TLOG("Added quote " << q->name() << ".");
576 }
577
578 LOG("CommodityCurve: finished getting configured commodity quotes.");
579
580 return result;
581}
582
583void CommodityCurve::addInstruments(const Date& asof, const Loader& loader, const string& configId,
584 const string& currency, const PriceSegment& priceSegment,
585 const map<string, QuantLib::ext::shared_ptr<CommodityCurve>>& commodityCurves,
586 map<Date, QuantLib::ext::shared_ptr<Helper>>& instruments) {
587
588 using PST = PriceSegment::Type;
590 PST type = priceSegment.type();
591
592 // Pre-populate some variables if averaging segment.
593 QuantLib::ext::shared_ptr<Conventions> conventions = InstrumentConventions::instance().conventions();
594 QuantLib::ext::shared_ptr<CommodityFutureConvention> convention;
595 AD ad;
596 QuantLib::ext::shared_ptr<CommodityIndex> index;
597 QuantLib::ext::shared_ptr<FutureExpiryCalculator> uFec;
598 if (type == PST::AveragingFuture || type == PST::AveragingSpot || type == PST::AveragingOffPeakPower) {
599
600 // Get the associated averaging commodity future convention.
601 convention = QuantLib::ext::dynamic_pointer_cast<CommodityFutureConvention>(
602 conventions->get(priceSegment.conventionsId()));
603 QL_REQUIRE(convention, "Convention " << priceSegment.conventionsId() <<
604 " not of expected type CommodityFutureConvention.");
605
606 ad = convention->averagingData();
607 QL_REQUIRE(!ad.empty(), "CommodityCurve: convention " << convention->id() <<
608 " should have non-empty averaging data for piecewise price curve construction.");
609
610 // The commodity index for which we are building a price curve.
611 index = parseCommodityIndex(ad.commodityName(), false);
612
613 // If referencing a future, we need conventions for the underlying future that is being averaged.
614 if (type == PST::AveragingFuture || type == PST::AveragingOffPeakPower) {
615
616 auto uConvention = QuantLib::ext::dynamic_pointer_cast<CommodityFutureConvention>(
617 conventions->get(ad.conventionsId()));
618 QL_REQUIRE(uConvention, "Convention " << priceSegment.conventionsId() <<
619 " not of expected type CommodityFutureConvention.");
620 uFec = QuantLib::ext::make_shared<ConventionsBasedFutureExpiry>(*uConvention);
621
622 if (ad.dailyExpiryOffset() != Null<Natural>() && ad.dailyExpiryOffset() > 0) {
623 QL_REQUIRE(uConvention->contractFrequency() == Daily, "CommodityCurve: the averaging data has" <<
624 " a positive DailyExpiryOffset (" << ad.dailyExpiryOffset() << ") but the underlying future" <<
625 " contract frequency is not daily (" << uConvention->contractFrequency() << ").");
626 }
627 }
628
629 }
630
631 // Pre-populate some variables if the price segment is AveragingOffPeakPower.
632 QuantLib::ext::shared_ptr<CommodityIndex> peakIndex;
633 Natural peakHoursPerDay = 16;
634 Calendar peakCalendar;
635 if (type == PST::AveragingOffPeakPower) {
636
637 // Look up the peak price curve in the commodityCurves map
638 const string& ppId = priceSegment.peakPriceCurveId();
639 QL_REQUIRE(!ppId.empty(), "CommodityCurve: AveragingOffPeakPower segment in " <<
640 " curve configuration " << configId << " does not provide a peak price curve ID.");
641 CommodityCurveSpec ccSpec(currency, ppId);
642 DLOG("Looking for peak price curve with id, " << ppId << ", and spec, " << ccSpec << ".");
643 auto itCc = commodityCurves.find(ccSpec.name());
644 QL_REQUIRE(itCc != commodityCurves.end(), "Can't find peak price curve with id " << ppId);
645 auto peakPts = Handle<PriceTermStructure>(itCc->second->commodityPriceCurve());
646
647 // Create the daily peak price index linked to the peak price term structure.
648 peakIndex = parseCommodityIndex(ppId, false, peakPts);
649
650 // Calendar defining the peak business days.
651 peakCalendar = parseCalendar(priceSegment.peakPriceCalendar());
652
653 // Look up the conventions for the peak price commodity to determine peak hours per day.
654 if (conventions->has(ppId)) {
655 auto peakConvention = QuantLib::ext::dynamic_pointer_cast<CommodityFutureConvention>(conventions->get(ppId));
656 if (peakConvention && peakConvention->hoursPerDay() != Null<Natural>()) {
657 peakHoursPerDay = peakConvention->hoursPerDay();
658 }
659 }
660 }
661
662 // Get the relevant quotes
663 auto quotes = getQuotes(asof, configId, priceSegment.quotes(), loader, true);
664
665 // Add an instrument for each relevant quote.
666 for (const auto& quote : quotes) {
667
668 const Date& expiry = quote->expiryDate();
669 switch (type) {
670 case PST::Future:
671 if (expiry == asof) {
672 TLOG("Quote " << quote->name() << " has expiry date " << io::iso_date(expiry) << " equal to asof" <<
673 " so not adding to instruments. Attempt to add as fixing instead.");
674 addMarketFixing(priceSegment.conventionsId(), expiry, quote->quote()->value());
675 } else if (instruments.count(expiry) == 0) {
676 instruments[expiry] = QuantLib::ext::make_shared<FuturePriceHelper>(quote->quote(), expiry);
677 } else {
678 TLOG("Skipping quote, " << quote->name() << ", because its expiry date, " <<
679 io::iso_date(expiry) << ", is already in the instrument set.");
680 }
681 break;
682
683 // An averaging future referencing an underlying future or spot. Setup is similar.
684 case PST::AveragingFuture:
685 case PST::AveragingSpot:
686 case PST::AveragingOffPeakPower: {
687
688 // Determine the calculation period.
689 using ADCP = AD::CalculationPeriod;
690 Date start;
691 Date end;
692 if (ad.period() == ADCP::ExpiryToExpiry) {
693
694 auto fec = QuantLib::ext::make_shared<ConventionsBasedFutureExpiry>(*convention);
695 end = fec->nextExpiry(true, expiry);
696 if (end != expiry) {
697 WLOG("Calculated expiry date, " << io::iso_date(end) << ", does not equal quote's expiry date "
698 << io::iso_date(expiry) << ". Proceed with quote's expiry.");
699 }
700 start = fec->priorExpiry(false, end) + 1;
701
702 } else if (ad.period() == ADCP::PreviousMonth) {
703
704 end = Date::endOfMonth(expiry - 1 * Months);
705 start = Date(1, end.month(), end.year());
706 }
707
708 QuantLib::ext::shared_ptr<Helper> helper;
709 if (type == PST::AveragingOffPeakPower) {
710 TLOG("Building average off-peak power helper from quote, " << quote->name() << ".");
711 helper = QuantLib::ext::make_shared<AverageOffPeakPowerHelper>(quote->quote(), index, start,
712 end, uFec, peakIndex, peakCalendar, peakHoursPerDay);
713 } else {
714 TLOG("Building average future price helper from quote, " << quote->name() << ".");
715 helper = QuantLib::ext::make_shared<AverageFuturePriceHelper>(quote->quote(), index, start, end, uFec,
716 ad.pricingCalendar(), ad.deliveryRollDays(), ad.futureMonthOffset(), ad.useBusinessDays(),
717 ad.dailyExpiryOffset());
718 }
719
720 // Only add to instruments if an instrument with the same pillar date is not there already.
721 Date pillar = helper->pillarDate();
722 if (instruments.count(pillar) == 0) {
723 instruments[pillar] = helper;
724 } else {
725 TLOG("Skipping quote, " << quote->name() << ", because an instrument with its pillar date, " <<
726 io::iso_date(pillar) << ", is already in the instrument set.");
727 }
728 break;
729 }
730
731 default:
732 QL_FAIL("CommodityCurve: unrecognised price segment type.");
733 break;
734 }
735 }
736}
737
738void CommodityCurve::addOffPeakPowerInstruments(const Date& asof, const Loader& loader, const string& configId,
739 const PriceSegment& priceSegment, map<Date, QuantLib::ext::shared_ptr<Helper>>& instruments) {
740
741 // Check that we have been called with the expected segment type.
742 using PST = PriceSegment::Type;
743 QL_REQUIRE(priceSegment.type() == PST::OffPeakPowerDaily, "Expecting a price segment type of OffPeakPowerDaily.");
744
745 QuantLib::ext::shared_ptr<Conventions> conventions = InstrumentConventions::instance().conventions();
746
747 // Check we have a commodity future convention for the price segment.
748 const string& convId = priceSegment.conventionsId();
749 auto p = conventions->get(convId, Convention::Type::CommodityFuture);
750 QL_REQUIRE(p.first, "Could not get conventions with id " << convId << " for OffPeakPowerDaily price segment" <<
751 " in curve configuration " << configId << ".");
752 auto convention = QuantLib::ext::dynamic_pointer_cast<CommodityFutureConvention>(p.second);
753
754 // Check that the commodity future convention has off-peak information for the name.
755 const auto& oppIdxData = convention->offPeakPowerIndexData();
756 QL_REQUIRE(oppIdxData, "Conventions with id " << convId << " for OffPeakPowerDaily price segment" <<
757 " should have an OffPeakPowerIndexData section.");
758 Real offPeakHours = oppIdxData->offPeakHours();
759 TLOG("Off-peak hours is " << offPeakHours);
760 const Calendar& peakCalendar = oppIdxData->peakCalendar();
761
762 // Check that the price segment has off-peak daily section.
763 const auto& opd = priceSegment.offPeakDaily();
764 QL_REQUIRE(opd, "The OffPeakPowerDaily price segment for curve configuration " << configId <<
765 " should have an OffPeakDaily section.");
766
767 // Get all the peak and off-peak quotes that we have and store them in a map. The map key is the expiry date and
768 // the map value is a pair of values the first being the off-peak value for that expiry and the second being the
769 // peak value for that expiry. We only need the peak portion to form the quote on peakCalendar holidays. We need
770 // the off-peak portion always.
771 map<Date, pair<Real, Real>> quotes;
772
773 auto opqs = getQuotes(asof, configId, opd->offPeakQuotes(), loader, true);
774 for (const auto& q : opqs) {
775 Real value = q->quote()->value();
776 Date expiry = q->expiryDate();
777 if (quotes.count(expiry) != 0) {
778 TLOG("Already have off-peak quote with expiry " << io::iso_date(expiry) << " so skipping " << q->name());
779 } else {
780 TLOG("Adding off-peak quote " << q->name() << ": " << io::iso_date(expiry) << "," << value);
781 quotes[expiry] = make_pair(value, Null<Real>());
782 }
783 }
784
785 auto pqs = getQuotes(asof, configId, opd->peakQuotes(), loader, true);
786 for (const auto& q : pqs) {
787 Real value = q->quote()->value();
788 Date expiry = q->expiryDate();
789 auto it = quotes.find(expiry);
790 if (it == quotes.end()) {
791 TLOG("Have no off-peak quote with expiry " << io::iso_date(expiry) << " so skipping " << q->name());
792 } else if (it->second.second != Null<Real>()) {
793 TLOG("Already have a peak quote with expiry " << io::iso_date(expiry) << " so skipping " << q->name());
794 } else {
795 TLOG("Adding peak quote " << q->name() << ": " << io::iso_date(expiry) << "," << value);
796 it->second.second = value;
797 }
798 }
799
800 // Now, use the quotes to create the future instruments in the curve.
801 for (const auto& kv : quotes) {
802
803 // If the expiry is already in the instrument set, we skip it.
804 const Date& expiry = kv.first;
805 if (instruments.count(expiry) != 0) {
806 TLOG("Skipping expiry " << io::iso_date(expiry) << " because it is already in the instrument set.");
807 continue;
808 }
809
810 // If the expiry is equal to the asof, we add fixings.
811 if (expiry == asof) {
812 TLOG("The off-peak power expiry date " << io::iso_date(expiry) << " is equal to asof" <<
813 " so not adding to instruments. Attempt to add fixing(s) instead.");
814 if (peakCalendar.isHoliday(expiry) && kv.second.second == Null<Real>()) {
815 DLOG("The peak portion of the quote on holiday " << io::iso_date(expiry) <<
816 " is missing so can't add fixings.");
817 } else {
818 // Add the off-peak and if necessary peak fixing
819 addMarketFixing(oppIdxData->offPeakIndex(), expiry, kv.second.first);
820 if (peakCalendar.isHoliday(expiry))
821 addMarketFixing(oppIdxData->peakIndex(), expiry, kv.second.second);
822 }
823 continue;
824 }
825
826 // Determine the quote that we will use in the future instrument for this expiry.
827 Real quote = 0.0;
828 if (peakCalendar.isHoliday(expiry)) {
829 Real peakValue = kv.second.second;
830 if (peakValue == Null<Real>()) {
831 DLOG("The peak portion of the quote on holiday " << io::iso_date(expiry) << " is missing so skip.");
832 continue;
833 } else {
834 Real offPeakValue = kv.second.first;
835 quote = (offPeakHours * offPeakValue + (24.0 - offPeakHours) * peakValue) / 24.0;
836 TLOG("The quote on holiday " << io::iso_date(expiry) << " is " << quote << ". (off-peak,peak) is" <<
837 " (" << offPeakValue << "," << peakValue << ").");
838 }
839 } else {
840 quote = kv.second.first;
841 TLOG("The quote on business day " << io::iso_date(expiry) << " is the off-peak value " << quote << ".");
842 }
843
844 // Add the future helper for this expiry.
845 instruments[expiry] = QuantLib::ext::make_shared<FuturePriceHelper>(quote, expiry);
846
847 }
848}
849
850} // namespace data
851} // namespace ore
QuantLib::Real maxFactor() const
QuantLib::Size dontThrowSteps() const
QuantLib::Real globalAccuracy() const
QuantLib::Real accuracy() const
QuantLib::Real minFactor() const
QuantLib::Size maxAttempts() const
Commodity curve configuration.
const std::string & conventionsId() const
const std::string & currency() const
const std::map< unsigned short, PriceSegment > & priceSegments() const
const boost::optional< BootstrapConfig > & bootstrapConfig() const
const vector< string > & fwdQuotes() const
QuantLib::Natural monthOffset() const
const std::string & baseConventionsId() const
std::string interpolationMethod_
Store the interpolation method.
QuantLib::ext::shared_ptr< QuantExt::CommodityIndex > commodityIndex_
QuantLib::ext::shared_ptr< QuantExt::PriceTermStructure > commodityPriceCurve_
QuantLib::ext::shared_ptr< CommodityCurveCalibrationInfo > calibrationInfo_
QuantLib::Real onValue_
Store the overnight value if any.
void buildCurve(const QuantLib::Date &asof, const std::map< QuantLib::Date, QuantLib::Handle< QuantLib::Quote > > &data, const QuantLib::ext::shared_ptr< CommodityCurveConfig > &config)
Build price curve using the curve data.
CommodityCurve()
Default constructor.
void buildPiecewiseCurve(const QuantLib::Date &asof, const CommodityCurveConfig &config, const Loader &loader, const std::map< std::string, QuantLib::ext::shared_ptr< CommodityCurve > > &commodityCurves)
Build commodity piecewise price curve.
void buildCrossCurrencyPriceCurve(const QuantLib::Date &asof, const QuantLib::ext::shared_ptr< CommodityCurveConfig > &config, const QuantLib::ext::shared_ptr< CommodityCurveConfig > &baseConfig, const FXTriangulation &fxSpots, const std::map< std::string, QuantLib::ext::shared_ptr< YieldCurve > > &yieldCurves, const std::map< std::string, QuantLib::ext::shared_ptr< CommodityCurve > > &commodityCurves)
Build cross currency commodity price curve.
std::vector< QuantLib::ext::shared_ptr< CommodityForwardQuote > > getQuotes(const QuantLib::Date &asof, const std::string &, const std::vector< std::string > &quotes, const Loader &loader, bool filter=false)
Get the configured quotes. If filter is true, remove tenor based quotes and quotes with expiry before...
void add(const QuantLib::Date &asof, const QuantLib::Date &expiry, QuantLib::Real value, std::map< QuantLib::Date, QuantLib::Handle< QuantLib::Quote > > &data, bool outright, QuantLib::Real pointsFactor=1.0)
Add node to price curve data with check for duplicate expiry dates.
QuantLib::Real tnValue_
Store the tomorrow next value if any.
bool regexQuotes_
Populated with true if the quotes are configured via a wildcard.
CommodityCurveSpec spec_
QuantLib::DayCounter dayCounter_
Store the curve's day counter.
void addOffPeakPowerInstruments(const QuantLib::Date &asof, const Loader &loader, const std::string &configId, const PriceSegment &priceSegment, std::map< QuantLib::Date, QuantLib::ext::shared_ptr< Helper > > &instruments)
Special method to add instruments when the priceSegment is OffPeakPowerDaily.
void populateData(std::map< QuantLib::Date, QuantLib::Handle< QuantLib::Quote > > &data, const QuantLib::Date &asof, const QuantLib::ext::shared_ptr< CommodityCurveConfig > &config, const Loader &loader)
Populate data with dates and prices from the loader.
QuantLib::Real commoditySpot_
Store the commodity spot value with Null<Real>() indicating that none has been provided.
void buildBasisPriceCurve(const QuantLib::Date &asof, const CommodityCurveConfig &config, const QuantLib::Handle< QuantExt::PriceTermStructure > &basePts, const Loader &loader)
Build commodity basis price curve.
void addInstruments(const QuantLib::Date &asof, const Loader &loader, const std::string &configId, const std::string &currency, const PriceSegment &priceSegment, const std::map< std::string, QuantLib::ext::shared_ptr< CommodityCurve > > &commodityCurves, std::map< QuantLib::Date, QuantLib::ext::shared_ptr< Helper > > &instruments)
Commodity curve description.
Definition: curvespec.hpp:413
CalculationPeriod
Indicate location of calculation period relative to the future expiry date.
Abstract base class for convention objects.
Definition: conventions.hpp:55
Repository for currency dependent market conventions.
QuantLib::ext::shared_ptr< Convention > get(const string &id) const
const string & curveID() const
Definition: curveconfig.hpp:54
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
QuantLib::Handle< QuantLib::Quote > getQuote(const std::string &pair) const
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
Type
Type of price segment being represented, i.e. type of instrument in the price segment.
const std::string & peakPriceCalendar() const
const boost::optional< OffPeakDaily > & offPeakDaily() const
const std::string & peakPriceCurveId() const
const std::string & conventionsId() const
const std::vector< std::string > & quotes() const
Yield curve description.
Definition: curvespec.hpp:108
Class for building a commodity price curve.
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
Currency parseCurrency(const string &s)
Convert text to QuantLib::Currency.
Definition: parsers.cpp:290
DayCounter parseDayCounter(const string &s)
Convert text to QuantLib::DayCounter.
Definition: parsers.cpp:209
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 WLOG(text)
Logging Macro (Level = Warning)
Definition: log.hpp:550
#define TLOG(text)
Logging Macro (Level = Data)
Definition: log.hpp:556
QuantLib::ext::shared_ptr< QuantExt::CommodityIndex > parseCommodityIndex(const string &name, bool hasPrefix, const Handle< PriceTermStructure > &ts, const Calendar &cal, const bool enforceFutureIndex)
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
QuantLib::BootstrapHelper< QuantLib::OptionletVolatilityStructure > helper
vector< string > curveConfigs
utilities for wildcard handling