Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
commodityoption.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 <boost/test/unit_test.hpp>
20#include <oret/toplevelfixture.hpp>
21
22#include <boost/algorithm/string/replace.hpp>
23#include <boost/make_shared.hpp>
24
25#include <ql/currencies/america.hpp>
26#include <ql/instruments/vanillaoption.hpp>
27#include <ql/math/interpolations/linearinterpolation.hpp>
28#include <ql/pricingengines/blackformula.hpp>
29#include <ql/termstructures/volatility/equityfx/blackvariancecurve.hpp>
30#include <ql/termstructures/yield/flatforward.hpp>
31
33
38
39using namespace std;
40using namespace boost::unit_test_framework;
41using namespace boost::algorithm;
42using namespace QuantLib;
43using namespace QuantExt;
44using namespace ore::data;
45
46namespace {
47
48// testTolerance for Real comparison
49Real testTolerance = 1e-10;
50
51// Cached prices of call and put
52// Satisfies put call parity: call - put = (1348 - 1340) * 0.990049833749 * 100
53Real cachedCallPrice = 5711.6321329012244;
54Real cachedPutPrice = 4919.5922659018906;
55
56class TestMarket : public MarketImpl {
57public:
58 TestMarket() : MarketImpl(false) {
59 // Reference date and common day counter
60 asof_ = Date(19, Feb, 2018);
61 Actual365Fixed dayCounter;
62
63 // Add USD discount curve
64 Handle<YieldTermStructure> discount(QuantLib::ext::make_shared<FlatForward>(asof_, 0.01, dayCounter));
65 yieldCurves_[make_tuple(Market::defaultConfiguration, YieldCurveType::Discount, "USD")] = discount;
66
67 // Add GOLD_USD price curve
68 vector<Date> dates = {asof_, Date(19, Feb, 2019)};
69 vector<Real> prices = {1346.0, 1348.0};
70 Handle<PriceTermStructure> priceCurve(
71 QuantLib::ext::make_shared<InterpolatedPriceCurve<Linear>>(asof_, dates, prices, dayCounter, USDCurrency()));
72 Handle<CommodityIndex> commIdx(QuantLib::ext::make_shared<CommoditySpotIndex>("GOLD_USD", NullCalendar(), priceCurve));
73 commodityIndices_[make_pair(Market::defaultConfiguration, "GOLD_USD")] = commIdx;
74
75 // Add GOLD_USD volatilities
76 vector<Date> volatilityDates = {Date(19, Feb, 2019), Date(19, Feb, 2020)};
77 vector<Real> volatilities = {0.10, 0.11};
78 Handle<BlackVolTermStructure> volatility(
79 QuantLib::ext::make_shared<BlackVarianceCurve>(asof_, volatilityDates, volatilities, dayCounter));
80 commodityVols_[make_pair(Market::defaultConfiguration, "GOLD_USD")] = volatility;
81 }
82};
83
84class CommonData {
85public:
86 Envelope envelope;
87 string commodityName;
88 string currency;
89 Real quantity;
90 TradeStrike strike;
91 bool payOffAtExpiry;
92 vector<string> expiry;
93 Date expiryDate;
94
95 QuantLib::ext::shared_ptr<Market> market;
96 QuantLib::ext::shared_ptr<EngineFactory> engineFactory;
97
98 CommonData()
99 : commodityName("GOLD_USD"), currency("USD"), quantity(100), strike(1340.0, "USD"), payOffAtExpiry(false),
100 expiry(1, "2019-02-19"), expiryDate(19, Feb, 2019) {
101
102 // Create engine factory
103 market = QuantLib::ext::make_shared<TestMarket>();
104 QuantLib::ext::shared_ptr<EngineData> engineData = QuantLib::ext::make_shared<EngineData>();
105 engineData->model("CommodityOption") = "BlackScholes";
106 engineData->engine("CommodityOption") = "AnalyticEuropeanEngine";
107 engineFactory = QuantLib::ext::make_shared<EngineFactory>(engineData, market);
108
109 // Set evaluation date
110 Settings::instance().evaluationDate() = market->asofDate();
111 }
112};
113
114} // namespace
115
116BOOST_FIXTURE_TEST_SUITE(OREDataTestSuite, ore::test::TopLevelFixture)
117
118BOOST_AUTO_TEST_SUITE(CommodityOptionTests)
119
120BOOST_AUTO_TEST_CASE(testCommodityOptionTradeBuilding) {
121
122 BOOST_TEST_MESSAGE("Testing commodity option trade building");
123
124 // Common test data and setup
125 CommonData td;
126
127 // Test the building of a commodity option doesn't throw
128 OptionData optionData("Long", "Call", "European", td.payOffAtExpiry, td.expiry);
129 QuantLib::ext::shared_ptr<CommodityOption> option = QuantLib::ext::make_shared<CommodityOption>(
130 td.envelope, optionData, td.commodityName, td.currency, td.quantity, td.strike);
131 BOOST_CHECK_NO_THROW(option->build(td.engineFactory));
132
133 // Check the underlying instrument was built as expected
134 QuantLib::ext::shared_ptr<Instrument> qlInstrument = option->instrument()->qlInstrument();
135
136 QuantLib::ext::shared_ptr<VanillaOption> vanillaOption = QuantLib::ext::dynamic_pointer_cast<VanillaOption>(qlInstrument);
137 BOOST_CHECK(vanillaOption);
138 BOOST_CHECK_EQUAL(vanillaOption->exercise()->type(), Exercise::Type::European);
139 BOOST_CHECK_EQUAL(vanillaOption->exercise()->dates().size(), 1);
140 BOOST_CHECK_EQUAL(vanillaOption->exercise()->dates()[0], td.expiryDate);
141
142 QuantLib::ext::shared_ptr<TypePayoff> payoff = QuantLib::ext::dynamic_pointer_cast<TypePayoff>(vanillaOption->payoff());
143 BOOST_CHECK(payoff);
144 BOOST_CHECK_EQUAL(payoff->optionType(), Option::Type::Call);
145
146 // Calculate the expected price and check against cached price
147 // This is an extra check of the market etc.
148 // Know it is then safe to use the cached price elsewhere in this suite
149 Real forwardPrice = td.market->commodityPriceCurve(td.commodityName)->price(td.expiryDate);
150 DiscountFactor discount = td.market->discountCurve(td.currency)->discount(td.expiryDate);
151 Real variance = td.market->commodityVolatility(td.commodityName)->blackVariance(td.expiryDate, td.strike.value());
152 Real expectedPrice =
153 td.quantity * blackFormula(Option::Type::Call, td.strike.value(), forwardPrice, sqrt(variance), discount);
154 BOOST_CHECK_CLOSE(expectedPrice, cachedCallPrice, testTolerance);
155
156 // Check the price
157 BOOST_CHECK_CLOSE(option->instrument()->NPV(), expectedPrice, testTolerance);
158}
159
160BOOST_AUTO_TEST_CASE(testCommodityOptionFromXml) {
161
162 BOOST_TEST_MESSAGE("Testing parsing of commodity option trade from XML");
163
164 // Common test data and setup
165 CommonData td;
166
167 // Create an XML string representation of the trade
168 string tradeXml;
169 tradeXml.append("<Portfolio>");
170 tradeXml.append(" <Trade id=\"CommodityOption_Gold\">");
171 tradeXml.append(" <TradeType>CommodityOption</TradeType>");
172 tradeXml.append(" <Envelope>");
173 tradeXml.append(" <CounterParty>CPTY_A</CounterParty>");
174 tradeXml.append(" <NettingSetId>CPTY_A</NettingSetId>");
175 tradeXml.append(" <AdditionalFields/>");
176 tradeXml.append(" </Envelope>");
177 tradeXml.append(" <CommodityOptionData>");
178 tradeXml.append(" <OptionData>");
179 tradeXml.append(" <LongShort>Long</LongShort>");
180 tradeXml.append(" <OptionType>Call</OptionType>");
181 tradeXml.append(" <Style>European</Style>");
182 tradeXml.append(" <Settlement>Cash</Settlement>");
183 tradeXml.append(" <PayOffAtExpiry>false</PayOffAtExpiry>");
184 tradeXml.append(" <ExerciseDates>");
185 tradeXml.append(" <ExerciseDate>2019-02-19</ExerciseDate>");
186 tradeXml.append(" </ExerciseDates>");
187 tradeXml.append(" </OptionData>");
188 tradeXml.append(" <Name>GOLD_USD</Name>");
189 tradeXml.append(" <Currency>USD</Currency>");
190 tradeXml.append(" <Strike>1340</Strike>");
191 tradeXml.append(" <Quantity>100</Quantity>");
192 tradeXml.append(" </CommodityOptionData>");
193 tradeXml.append(" </Trade>");
194 tradeXml.append("</Portfolio>");
195
196 // Load portfolio from XML string
197 Portfolio portfolio;
198 portfolio.fromXMLString(tradeXml);
199
200 // Extract CommodityOption trade from portfolio
201 QuantLib::ext::shared_ptr<Trade> trade = portfolio.trades().begin()->second;
202 QuantLib::ext::shared_ptr<CommodityOption> option = QuantLib::ext::dynamic_pointer_cast<ore::data::CommodityOption>(trade);
203
204 // Check fields after checking that the cast was successful
205 BOOST_CHECK(option);
206 BOOST_CHECK_EQUAL(option->tradeType(), "CommodityOption");
207 BOOST_CHECK_EQUAL(option->id(), "CommodityOption_Gold");
208 BOOST_CHECK_EQUAL(option->asset(), "GOLD_USD");
209 BOOST_CHECK_EQUAL(option->currency(), "USD");
210 BOOST_CHECK_CLOSE(option->strike().value(), 1340, testTolerance);
211 BOOST_CHECK_CLOSE(option->quantity(), 100, testTolerance);
212 BOOST_CHECK_EQUAL(option->option().longShort(), "Long");
213 BOOST_CHECK_EQUAL(option->option().callPut(), "Call");
214 BOOST_CHECK_EQUAL(option->option().style(), "European");
215 BOOST_CHECK_EQUAL(option->option().exerciseDates().size(), 1);
216 BOOST_CHECK_EQUAL(option->option().exerciseDates()[0], "2019-02-19");
217
218 // Build the option and check the price
219 BOOST_CHECK_NO_THROW(option->build(td.engineFactory));
220 BOOST_CHECK_CLOSE(option->instrument()->NPV(), cachedCallPrice, testTolerance);
221
222 // Make trade short call and test price is negated
223 replace_all(tradeXml, ">Long<", ">Short<");
224 portfolio.clear();
225 portfolio.fromXMLString(tradeXml);
226 trade = portfolio.trades().begin()->second;
227 BOOST_CHECK_NO_THROW(trade->build(td.engineFactory));
228 BOOST_CHECK_CLOSE(trade->instrument()->NPV(), -cachedCallPrice, testTolerance);
229
230 // Make trade short put and test price
231 replace_all(tradeXml, ">Call<", ">Put<");
232 portfolio.clear();
233 portfolio.fromXMLString(tradeXml);
234 trade = portfolio.trades().begin()->second;
235 BOOST_CHECK_NO_THROW(trade->build(td.engineFactory));
236 BOOST_CHECK_CLOSE(trade->instrument()->NPV(), -cachedPutPrice, testTolerance);
237
238 // Make trade long put and test price
239 replace_all(tradeXml, ">Short<", ">Long<");
240 portfolio.clear();
241 portfolio.fromXMLString(tradeXml);
242 trade = portfolio.trades().begin()->second;
243 BOOST_CHECK_NO_THROW(trade->build(td.engineFactory));
244 BOOST_CHECK_CLOSE(trade->instrument()->NPV(), cachedPutPrice, testTolerance);
245}
246
247BOOST_AUTO_TEST_CASE(testLongShortCallPutPrices) {
248
249 BOOST_TEST_MESSAGE("Testing commodity option prices");
250
251 // Common test data and setup
252 CommonData td;
253
254 // Option
255 QuantLib::ext::shared_ptr<CommodityOption> option;
256
257 // Long call
258 OptionData optionData("Long", "Call", "European", td.payOffAtExpiry, td.expiry);
259 option = QuantLib::ext::make_shared<CommodityOption>(td.envelope, optionData, td.commodityName, td.currency,
260 td.quantity, td.strike);
261 option->build(td.engineFactory);
262 BOOST_CHECK_CLOSE(option->instrument()->NPV(), cachedCallPrice, testTolerance);
263
264 // Short call
265 optionData = OptionData("Short", "Call", "European", td.payOffAtExpiry, td.expiry);
266 option = QuantLib::ext::make_shared<CommodityOption>(td.envelope, optionData, td.commodityName, td.currency,
267 td.quantity, td.strike);
268 option->build(td.engineFactory);
269 BOOST_CHECK_CLOSE(option->instrument()->NPV(), -cachedCallPrice, testTolerance);
270
271 // Long put
272 optionData = OptionData("Long", "Put", "European", td.payOffAtExpiry, td.expiry);
273 option = QuantLib::ext::make_shared<CommodityOption>(td.envelope, optionData, td.commodityName, td.currency,
274 td.quantity, td.strike);
275 option->build(td.engineFactory);
276 BOOST_CHECK_CLOSE(option->instrument()->NPV(), cachedPutPrice, testTolerance);
277
278 // Short put
279 optionData = OptionData("Short", "Put", "European", td.payOffAtExpiry, td.expiry);
280 option = QuantLib::ext::make_shared<CommodityOption>(td.envelope, optionData, td.commodityName, td.currency, td.quantity,
281 td.strike);
282 option->build(td.engineFactory);
283 BOOST_CHECK_CLOSE(option->instrument()->NPV(), -cachedPutPrice, testTolerance);
284}
285
286BOOST_AUTO_TEST_CASE(testCommodityOptionBuildExceptions) {
287
288 BOOST_TEST_MESSAGE("Testing commodity option exceptions during building");
289
290 // Common test data and setup
291 CommonData td;
292
293 // Option
294 QuantLib::ext::shared_ptr<CommodityOption> option;
295
296 // Negative strike throws
297 OptionData optionData("Long", "Call", "European", td.payOffAtExpiry, td.expiry);
298 TradeStrike ts(TradeStrike::Type::Price, -td.strike.value());
299 option = QuantLib::ext::make_shared<CommodityOption>(td.envelope, optionData, td.commodityName, td.currency, td.quantity, ts);
300 BOOST_CHECK_THROW(option->build(td.engineFactory), Error);
301
302
303 // Name of commodity with no market data throws
304 option = QuantLib::ext::make_shared<CommodityOption>(td.envelope, optionData, "GOLD_USD_MISSING", td.currency,
305 td.quantity, td.strike);
306 BOOST_CHECK_THROW(option->build(td.engineFactory), Error);
307
308 // Non-European OptionData style throws
309 optionData = OptionData("Long", "Call", "American", td.payOffAtExpiry, td.expiry);
310 option = QuantLib::ext::make_shared<CommodityOption>(td.envelope, optionData, td.commodityName, td.currency, td.quantity,
311 td.strike);
312 BOOST_CHECK_THROW(option->build(td.engineFactory), Error);
313
314 // More than one expiry date throws
315 vector<string> extraExpiries = td.expiry;
316 extraExpiries.push_back("2019-08-19");
317 optionData = OptionData("Long", "Call", "European", td.payOffAtExpiry, extraExpiries);
318 option = QuantLib::ext::make_shared<CommodityOption>(td.envelope, optionData, td.commodityName, td.currency,
319 td.quantity, td.strike);
320 BOOST_CHECK_THROW(option->build(td.engineFactory), Error);
321}
322
323BOOST_AUTO_TEST_CASE(testCommodityOptionPremium) {
324
325 BOOST_TEST_MESSAGE("Testing commodity option premium works");
326
327 // Common test data and setup
328 CommonData td;
329
330 // Premium
331 Real premium = 5000;
332 Date premiumDate(21, Feb, 2018);
333
334 // Create option
335 OptionData optionData("Long", "Call", "European", td.payOffAtExpiry, td.expiry, "Cash", "",
336 PremiumData{premium, td.currency, Date(21, Feb, 2018)});
337 QuantLib::ext::shared_ptr<CommodityOption> option = QuantLib::ext::make_shared<CommodityOption>(
338 td.envelope, optionData, td.commodityName, td.currency, td.quantity, td.strike);
339
340 // Test building succeeds
341 BOOST_CHECK_NO_THROW(option->build(td.engineFactory));
342
343 // Test that price is correct
344 DiscountFactor premiumDiscount = td.market->discountCurve(td.currency)->discount(premiumDate);
345 BOOST_CHECK_CLOSE(option->instrument()->NPV(), cachedCallPrice - premiumDiscount * premium, testTolerance);
346}
347
348BOOST_AUTO_TEST_SUITE_END()
349
350BOOST_AUTO_TEST_SUITE_END()
Engine builder for commodity options.
Serializable object holding generic trade data, reporting dimensions.
Definition: envelope.hpp:51
static const string defaultConfiguration
Default configuration label.
Definition: market.hpp:296
Market Implementation.
Definition: marketimpl.hpp:53
map< tuple< string, YieldCurveType, string >, Handle< YieldTermStructure > > yieldCurves_
Definition: marketimpl.hpp:208
map< pair< string, string >, QuantLib::Handle< QuantExt::CommodityIndex > > commodityIndices_
Definition: marketimpl.hpp:230
map< pair< string, string >, QuantLib::Handle< QuantLib::BlackVolTermStructure > > commodityVols_
Definition: marketimpl.hpp:231
Serializable object holding option data.
Definition: optiondata.hpp:42
Serializable portfolio.
Definition: portfolio.hpp:43
const std::map< std::string, QuantLib::ext::shared_ptr< Trade > > & trades() const
Return the map tradeId -> trade.
Definition: portfolio.cpp:162
void clear()
Clear the portfolio.
Definition: portfolio.cpp:39
Serializable object holding premium data.
Definition: premiumdata.hpp:37
void fromXMLString(const std::string &xml)
Parse from XML string.
Definition: xmlutils.cpp:162
Commodity option representation.
An implementation of the Market class that stores the required objects in maps.
RandomVariable sqrt(RandomVariable x)
RandomVariable variance(const RandomVariable &r)
Portfolio class.
BOOST_AUTO_TEST_CASE(testCommodityOptionTradeBuilding)