Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
commodityspreadoption.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2022 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*/
26
27using namespace QuantExt;
28using namespace QuantLib;
29using std::max;
30using std::string;
31using std::vector;
32
33namespace ore::data {
34
35class OptionPaymentDateAdjuster {
36public:
37 virtual ~OptionPaymentDateAdjuster() = default;
38 virtual void updatePaymentDate(const QuantLib::Date& exiryDate, Date& paymentDate) const {
39 // unadjusted
40 }
41};
42
43
44class OptionPaymentDataAdjuster : public OptionPaymentDateAdjuster {
45public:
46 OptionPaymentDataAdjuster(const OptionPaymentData& opd) : opd_(opd) {}
47
48 void updatePaymentDate(const QuantLib::Date& expiryDate, Date& paymentDate) const override {
49 if (opd_.rulesBased()) {
50 const Calendar& cal = opd_.calendar();
51 QL_REQUIRE(cal != Calendar(), "Need a non-empty calendar for rules based payment date.");
52 paymentDate = cal.advance(expiryDate, opd_.lag(), Days, opd_.convention());
53 } else {
54 const vector<Date>& dates = opd_.dates();
55 QL_REQUIRE(dates.size() == 1, "Need exactly one payment date for cash settled European option.");
56 paymentDate = dates[0];
57 }
58 }
59
60private:
61 OptionPaymentData opd_;
62};
63
64class OptionStripPaymentDateAdjuster : public OptionPaymentDateAdjuster {
65public:
66 OptionStripPaymentDateAdjuster(const std::vector<Date>& expiryDates,
67 const CommoditySpreadOptionData::OptionStripData& stripData)
68 : calendar_(stripData.calendar()), bdc_(stripData.bdc()), lag_(stripData.lag()) {
69 optionStripSchedule_ = makeSchedule(stripData.schedule());
70 latestExpiryDateInStrip_.resize(optionStripSchedule_.size(), Date());
71 QL_REQUIRE(optionStripSchedule_.size() >= 2,
72 "Need at least a start and end date in the optionstripschedule. Please check the trade xml");
73 // Check if the optionstrip definition include all expiries
74 Date minExpiryDate = *std::min_element(expiryDates.begin(), expiryDates.end());
75 Date maxExpiryDate = *std::max_element(expiryDates.begin(), expiryDates.end());
76 Date minOptionStripDate =
77 *std::min_element(optionStripSchedule_.dates().begin(), optionStripSchedule_.dates().end());
78 Date maxOptionStripDate =
79 *std::max_element(optionStripSchedule_.dates().begin(), optionStripSchedule_.dates().end());
80 QL_REQUIRE(
81 minOptionStripDate <= minExpiryDate && maxOptionStripDate > maxExpiryDate,
82 "optionStrips ending before latest expiry date, please check the optionstrip definition in the trade xml");
83 for (const auto& e : expiryDates) {
84 auto it = std::upper_bound(optionStripSchedule_.begin(), optionStripSchedule_.end(), e);
85 if (it != optionStripSchedule_.end()) {
86 auto idx = std::distance(optionStripSchedule_.begin(), it);
87 if (e > latestExpiryDateInStrip_[idx])
88 latestExpiryDateInStrip_[idx] = e;
89 }
90 }
91 }
92
93 void updatePaymentDate(const QuantLib::Date& expiryDate, Date& paymentDate) const override {
94 auto upperBound =
95 std::upper_bound(optionStripSchedule_.dates().begin(), optionStripSchedule_.dates().end(), expiryDate);
96 if (upperBound != optionStripSchedule_.dates().end()) {
97 size_t idx = std::distance(optionStripSchedule_.dates().begin(), upperBound);
98 paymentDate = calendar_.advance(latestExpiryDateInStrip_[idx], lag_ * Days, bdc_);
99 } else {
100 // Skip adjustment
101 }
102 }
103
104private:
105 vector<Date> latestExpiryDateInStrip_;
106 Schedule optionStripSchedule_;
107 Calendar calendar_;
108 BusinessDayConvention bdc_;
109 int lag_;
110};
111
112QuantLib::ext::shared_ptr<OptionPaymentDateAdjuster> makeOptionPaymentDateAdjuster(CommoditySpreadOptionData& optionData,
113 const std::vector<Date>& expiryDates) {
114
115 if (optionData.optionStrip().has_value()) {
116 return QuantLib::ext::make_shared<OptionStripPaymentDateAdjuster>(expiryDates, optionData.optionStrip().get());
117 } else if (optionData.optionData().paymentData().has_value()) {
118 return QuantLib::ext::make_shared<OptionPaymentDataAdjuster>(optionData.optionData().paymentData().get());
119 } else {
120 return QuantLib::ext::make_shared<OptionPaymentDateAdjuster>();
121 }
122}
123
124void CommoditySpreadOption::build(const QuantLib::ext::shared_ptr<ore::data::EngineFactory>& engineFactory) {
125
126 DLOG("CommoditySpreadOption::build() called for trade " << id());
127
128 // ISDA taxonomy
129 additionalData_["isdaAssetClass"] = std::string("Commodity");
130 additionalData_["isdaBaseProduct"] = std::string("Other");
131 additionalData_["isdaSubProduct"] = std::string("");
132 // skip the transaction level mapping for now
133 additionalData_["isdaTransaction"] = std::string("");
134
135 reset();
136 auto legData_ = csoData_.legData();
137 auto optionData_ = csoData_.optionData();
138 auto strike_ = csoData_.strike();
139
140 QL_REQUIRE(legData_.size() == 2, "Only two legs supported");
141 QL_REQUIRE(legData_[0].currency() == legData_[1].currency(), "Both legs must have same currency");
142 QL_REQUIRE(legData_[0].isPayer() != legData_[1].isPayer(), "Need one payer and one receiver leg");
143
144 if (!optionData_.style().empty()) {
145 QuantLib::Exercise::Type exerciseType = parseExerciseType(optionData_.style());
146 QL_REQUIRE(exerciseType == QuantLib::Exercise::Type::European, "Only European spread option supported");
147 }
148
149 maturity_ = Date();
150 npvCurrency_ = legData_[0].currency();
151 Size payerLegId = legData_[0].isPayer() ? 0 : 1;
152
153 // build the relevant fxIndexes;
154 std::vector<QuantLib::ext::shared_ptr<QuantExt::FxIndex>> fxIndexes(2, nullptr);
155 Currency ccy = parseCurrency(npvCurrency_);
156 Option::Type optionType = parseOptionType(optionData_.callPut());
157
158 // init engine factory builder
159 QuantLib::ext::shared_ptr<EngineBuilder> builder = engineFactory->builder(tradeType_);
160 auto engineBuilder = QuantLib::ext::dynamic_pointer_cast<CommoditySpreadOptionEngineBuilder>(builder);
161 // get config
162 auto config = builder->configuration(MarketContext::pricing);
163
164 // set exercise date to the pricing date of the coupon
165 QL_REQUIRE(optionData_.exerciseDates().size() == 0,
166 "Only European spread option supported, expiry date is end_date of the period");
167
168 // Build the commodity legs
169
170 for (Size i = 0; i < legData_.size();
171 ++i) { // The order is important, the first leg is always the long position, the second is the short;
172 legPayers_.push_back(legData_[i].isPayer());
173
174 // build legs
175
176 auto commLegData = (QuantLib::ext::dynamic_pointer_cast<CommodityFloatingLegData>(legData_[i].concreteLegData()));
177 QL_REQUIRE(commLegData, "CommoditySpreadOption leg data should be of type CommodityFloating");
178
179 auto legBuilder = engineFactory->legBuilder(legData_[i].legType());
180 auto cflb = QuantLib::ext::dynamic_pointer_cast<CommodityFloatingLegBuilder>(legBuilder);
181
182 QL_REQUIRE(cflb, "CommoditySpreadOption: Expected a CommodityFloatingLegBuilder for leg "
183 << i << " but got " << legData_[i].legType());
184 Leg leg = cflb->buildLeg(legData_[i], engineFactory, requiredFixings_, config);
185
186 // setup the cf indexes
187 QL_REQUIRE(!leg.empty(), "CommoditySpreadOption: Leg " << i << " has no coupons");
188 auto index = QuantLib::ext::dynamic_pointer_cast<QuantExt::CommodityCashFlow>(leg.front())->index();
189
190 // check ccy consistency
191 auto underlyingCcy = index->priceCurve()->currency();
192 auto tmpFx = commLegData->fxIndex();
193 if (tmpFx.empty()) {
194 QL_REQUIRE(underlyingCcy.code() == npvCurrency_,
195 "CommoditySpreadOption, inconsistent currencies: Settlement currency is "
196 << npvCurrency_ << ", leg " << i + 1 << " currency " << legData_[i].currency()
197 << ", underlying currency " << underlyingCcy << ", no FxIndex provided");
198 } else {
199 QL_REQUIRE(underlyingCcy.code() != npvCurrency_,
200 "CommoditySpreadOption, inconsistent currencies: Settlement currency is "
201 << npvCurrency_ << ", leg " << i + 1 << " currency " << legData_[i].currency()
202 << ", underlying currency " << underlyingCcy << ", FxIndex " << tmpFx << "provided");
203 auto domestic = npvCurrency_;
204 auto foreign = underlyingCcy.code();
205 fxIndexes[i] = buildFxIndex(tmpFx, domestic, foreign, engineFactory->market(),
206 engineFactory->configuration(MarketContext::pricing));
207 // update required fixings. This is handled automatically in the leg builder in the averaging case
208 if (commLegData->isAveraged()) {
209 for (auto cf : leg) {
210 auto fixingDate = cf->date();
211 if (!fxIndexes[i]->fixingCalendar().isBusinessDay(
212 fixingDate)) { // If fx index is not available for the cashflow pricing day,
213 // this ensures to require the previous valid one which will be used in pricing
214 // from fxIndex()->fixing(...)
215 Date adjustedFixingDate = fxIndexes[i]->fixingCalendar().adjust(fixingDate, Preceding);
216 requiredFixings_.addFixingDate(adjustedFixingDate, tmpFx);
217 } else {
218 requiredFixings_.addFixingDate(fixingDate, tmpFx);
219 }
220 }
221 }
222 }
223 legs_.push_back(leg);
224 legCurrencies_.push_back(legData_[i].currency()); // all legs and cf are priced with the same ccy
225 }
226
227 QL_REQUIRE(legs_[0].size() == legs_[1].size(),
228 "CommoditySpreadOption: the two legs must contain the same number of options.");
229
230 QL_REQUIRE(legs_[0].size() > 0, "CommoditySpreadOption: need at least one option, please check the trade xml");
231
232 Position::Type positionType = parsePositionType(optionData_.longShort());
233 Real bsInd = (positionType == QuantLib::Position::Long ? 1.0 : -1.0);
234
235 QuantLib::ext::shared_ptr<QuantLib::Instrument> firstInstrument;
236 double firstMultiplier = 0.0;
237 vector<QuantLib::ext::shared_ptr<Instrument>> additionalInstruments;
238 vector<Real> additionalMultipliers;
239
240 vector<Date> expiryDates;
241 for (size_t i = 0; i < legs_[0].size(); ++i) {
242 auto longFlow = QuantLib::ext::dynamic_pointer_cast<QuantExt::CommodityCashFlow>(legs_[1 - payerLegId][i]);
243 auto shortFlow = QuantLib::ext::dynamic_pointer_cast<QuantExt::CommodityCashFlow>(legs_[payerLegId][i]);
244 expiryDates.push_back(std::max(longFlow->lastPricingDate(), shortFlow->lastPricingDate()));
245 }
246
247 auto paymentDateAdjuster = makeOptionPaymentDateAdjuster(csoData_, expiryDates);
248
249 for (size_t i = 0; i < legs_[0].size(); ++i) {
250 auto longFlow = QuantLib::ext::dynamic_pointer_cast<QuantExt::CommodityCashFlow>(legs_[1 - payerLegId][i]);
251 auto shortFlow = QuantLib::ext::dynamic_pointer_cast<QuantExt::CommodityCashFlow>(legs_[payerLegId][i]);
252
253 QuantLib::Real quantity = longFlow->periodQuantity();
254
255 QL_REQUIRE(quantity == shortFlow->periodQuantity(), "all cashflows must refer to the same quantity");
256
257 QuantLib::Date expiryDate = expiryDates[i];
258
259 QuantLib::Date paymentDate = longFlow->date();
260
261 QL_REQUIRE(paymentDate == shortFlow->date(),
262 "all cashflows must refer to the same paymentDate, its used as the settlementDate of the option");
263
264 paymentDateAdjuster->updatePaymentDate(expiryDate, paymentDate);
265
266 QL_REQUIRE(paymentDate >= expiryDate, "Payment date must be greater than or equal to expiry date.");
267
268 QuantLib::ext::shared_ptr<Exercise> exercise = QuantLib::ext::make_shared<EuropeanExercise>(expiryDate);
269
270 // maturity gets overwritten every time, and it is ok. If the last option is settled with delay, maturity is set
271 // to the settlement date.
272 maturity_ = maturity_ == Date() ? paymentDate : std::max(maturity_, paymentDate);
273
274 // build the instrument for the i-th cfs
275 QuantLib::ext::shared_ptr<QuantExt::CommoditySpreadOption> spreadOption =
276 QuantLib::ext::make_shared<QuantExt::CommoditySpreadOption>(longFlow, shortFlow, exercise, quantity, strike_,
277 optionType, paymentDate, fxIndexes[1 - payerLegId],
278 fxIndexes[1 - payerLegId]);
279
280 // build and assign the engine
281 QuantLib::ext::shared_ptr<PricingEngine> commoditySpreadOptionEngine =
282 engineBuilder->engine(ccy, longFlow->index(), shortFlow->index(), id());
283 spreadOption->setPricingEngine(commoditySpreadOptionEngine);
284 setSensitivityTemplate(*engineBuilder);
285 if (i > 0) {
286 additionalInstruments.push_back(spreadOption);
287 additionalMultipliers.push_back(bsInd);
288 } else {
289 firstInstrument = spreadOption;
290 firstMultiplier = bsInd;
291 }
292 }
293
294 // Add premium
295 auto configuration = engineBuilder->configuration(MarketContext::pricing);
296 maturity_ = std::max(maturity_, addPremiums(additionalInstruments, additionalMultipliers, firstMultiplier,
297 optionData_.premiumData(), -bsInd, ccy, engineFactory, configuration));
298
299 instrument_ = QuantLib::ext::make_shared<VanillaInstrument>(firstInstrument, firstMultiplier, additionalInstruments,
300 additionalMultipliers);
301 if (!optionData_.premiumData().premiumData().empty()) {
302 auto premium = optionData_.premiumData().premiumData().front();
303 additionalData_["premiumAmount"] = -bsInd * premium.amount;
304 additionalData_["premiumPaymentDate"] = premium.payDate;
305 additionalData_["premiumCurrency"] = premium.ccy;
306 }
307}
308
309std::map<ore::data::AssetClass, std::set<std::string>>
310CommoditySpreadOption::underlyingIndices(const QuantLib::ext::shared_ptr<ReferenceDataManager>& referenceDataManager) const {
311 std::map<ore::data::AssetClass, std::set<std::string>> result;
312 auto legData = csoData_.legData();
313 for (const auto& leg : legData) {
314 set<string> indices = leg.indices();
315 for (auto ind : indices) {
316 QuantLib::ext::shared_ptr<Index> index = parseIndex(ind);
317 // only handle commodity
318 if (auto ci = QuantLib::ext::dynamic_pointer_cast<QuantExt::CommodityIndex>(index)) {
319 result[ore::data::AssetClass::COM].insert(ci->name());
320 }
321 }
322 }
323 return result;
324}
325
326
327void CommoditySpreadOption::fromXML(XMLNode* node) {
328 Trade::fromXML(node);
329 XMLNode* csoNode = XMLUtils::getChildNode(node, "CommoditySpreadOptionData");
330 csoData_.fromXML(csoNode);
331}
332XMLNode* CommoditySpreadOption::toXML(XMLDocument& doc) const {
333 XMLNode* node = Trade::toXML(doc);
334 auto csoNode = csoData_.toXML(doc);
335 XMLUtils::appendNode(node, csoNode);
336 return node;
337}
338
339void CommoditySpreadOptionData::fromXML(XMLNode* csoNode) {
340 XMLUtils::checkNode(csoNode, "CommoditySpreadOptionData");
341
342 XMLNode* optionDataNode = XMLUtils::getChildNode(csoNode, "OptionData");
343 QL_REQUIRE(optionDataNode, "Invalid CommmoditySpreadOption trade xml: found no OptionData Node");
344
345 optionData_.fromXML(optionDataNode);
346 strike_ = XMLUtils::getChildValueAsDouble(csoNode, "SpreadStrike", true);
347
348 vector<XMLNode*> nodes = XMLUtils::getChildrenNodes(csoNode, "LegData");
349 QL_REQUIRE(nodes.size() == 2, "CommoditySpreadOption: Exactly two LegData nodes expected");
350 for (auto& node : nodes) {
351 auto ld = createLegData();
352 ld->fromXML(node);
353 legData_.push_back(*ld);
354 }
355
356 XMLNode* optionStripNode = XMLUtils::getChildNode(csoNode, "OptionStripPaymentDates");
357 if (optionStripNode) {
358 optionStrip_ = OptionStripData();
359 optionStrip_->fromXML(optionStripNode);
360 }
361
362 QL_REQUIRE(legData_[0].isPayer() != legData_[1].isPayer(),
363 "CommoditySpreadOption: both a long and a short Assets are required.");
364}
365
366XMLNode* CommoditySpreadOptionData::toXML(XMLDocument& doc) const {
367 XMLNode* csoNode = doc.allocNode("CommoditySpreadOptionData");
368 for (size_t i = 0; i < legData_.size(); ++i) {
369 XMLUtils::appendNode(csoNode, legData_[i].toXML(doc));
370 }
371 XMLUtils::appendNode(csoNode, optionData_.toXML(doc));
372 XMLUtils::addChild(doc, csoNode, "SpreadStrike", strike_);
373 if (optionStrip_.has_value()) {
374 XMLUtils::appendNode(csoNode, optionStrip_->toXML(doc));
375 }
376 return csoNode;
377}
378
379
380void CommoditySpreadOptionData::OptionStripData::fromXML(XMLNode* node) {
381 XMLUtils::checkNode(node, "OptionStripPaymentDates");
382 XMLNode* optionStripScheduleNode = XMLUtils::getChildNode(node, "OptionStripDefinition");
383 QL_REQUIRE(optionStripScheduleNode, "Schedule required to define the option strips");
384 schedule_.fromXML(optionStripScheduleNode);
385 calendar_ = parseCalendar(XMLUtils::getChildValue(node, "PaymentCalendar", false, "NullCalendar"));
386 lag_ = parseInteger(XMLUtils::getChildValue(node, "PaymentLag", false, "0"));
387 bdc_ = parseBusinessDayConvention(XMLUtils::getChildValue(node, "PaymentConvention", false, "MF"));
388}
389
390XMLNode* CommoditySpreadOptionData::OptionStripData::toXML(XMLDocument& doc) const {
391 XMLNode* node = doc.allocNode("OptionStripPaymentDates");
392 auto tmp = schedule_.toXML(doc);
393 XMLUtils::setNodeName(doc, tmp, "OptionStripDefinition");
394 XMLUtils::appendNode(node, tmp);
395 XMLUtils::addChild(doc, node, "PaymentCalendar", to_string(calendar_));
396 XMLUtils::addChild(doc, node, "PaymentLag", to_string(lag_));
397 XMLUtils::addChild(doc, node, "PaymentConvention", to_string(bdc_));
398 return node;
399}
400
401} // namespace ore::data
const ore::data::OptionData & optionData() const
boost::optional< OptionStripData > optionStrip()
void build(const QuantLib::ext::shared_ptr< ore::data::EngineFactory > &engineFactory) override
Implement the build method.
const boost::optional< OptionPaymentData > & paymentData() const
Definition: optiondata.hpp:93
Small XML Document wrapper class.
Definition: xmlutils.hpp:65
XMLNode * allocNode(const string &nodeName)
util functions that wrap rapidxml
Definition: xmlutils.cpp:132
Commodity fixed and floating leg builders.
QuantLib::ext::shared_ptr< LegAdditionalData > createLegData()
Calendar parseCalendar(const string &s)
Convert text to QuantLib::Calendar.
Definition: parsers.cpp:157
Exercise::Type parseExerciseType(const std::string &s)
Convert text to QuantLib::Exercise::Type.
Definition: parsers.cpp:466
Currency parseCurrency(const string &s)
Convert text to QuantLib::Currency.
Definition: parsers.cpp:290
Position::Type parsePositionType(const std::string &s)
Convert text to QuantLib::Position::Type.
Definition: parsers.cpp:404
BusinessDayConvention parseBusinessDayConvention(const string &s)
Convert text to QuantLib::BusinessDayConvention.
Definition: parsers.cpp:173
QuantLib::ext::shared_ptr< Index > parseIndex(const string &s)
Convert std::string to QuantLib::Index.
Integer parseInteger(const string &s)
Convert text to QuantLib::Integer.
Definition: parsers.cpp:136
Option::Type parseOptionType(const std::string &s)
Convert text to QuantLib::Option::Type.
Definition: parsers.cpp:481
#define DLOG(text)
Logging Macro (Level = Debug)
Definition: log.hpp:554
market data related utilties
Calendar calendar
Definition: utilities.cpp:441
QuantLib::Date fixingDate(const QuantLib::Date &d, const QuantLib::Period obsLag, const QuantLib::Frequency freq, bool interpolated)
Size size(const ValueType &v)
Definition: value.cpp:145
void reset(const ASTNodePtr root)
Definition: astresetter.cpp:44
std::string to_string(const LocationInfo &l)
Definition: ast.cpp:28
QuantLib::ext::shared_ptr< OptionPaymentDateAdjuster > makeOptionPaymentDateAdjuster(CommoditySpreadOptionData &optionData, const std::vector< Date > &expiryDates)
QuantLib::ext::shared_ptr< QuantExt::FxIndex > buildFxIndex(const string &fxIndex, const string &domestic, const string &foreign, const QuantLib::ext::shared_ptr< Market > &market, const string &configuration, bool useXbsCurves)
Definition: marketdata.cpp:137
Schedule makeSchedule(const ScheduleDates &data)
Definition: schedule.cpp:263
string conversion utilities