1/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2/*
3 Copyright (C) 2023 Marcin Rybacki
4
5 This file is part of QuantLib, a free-software/open-source library
6 for financial quantitative analysts and developers - http://quantlib.org/
7
8 QuantLib is free software: you can redistribute it and/or modify it
9 under the terms of the QuantLib license. You should have received a
10 copy of the license along with this program; if not, please email
11 <quantlib-dev@lists.sf.net>. The license is also available online at
12 <http://quantlib.org/license.shtml>.
13
14 This program is distributed in the hope that it will be useful, but WITHOUT
15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 FOR A PARTICULAR PURPOSE. See the license for more details.
17*/
18
19#include "equitytotalreturnswap.hpp"
20#include "utilities.hpp"
21#include <ql/instruments/equitytotalreturnswap.hpp>
22#include <ql/indexes/equityindex.hpp>
23#include <ql/indexes/ibor/sofr.hpp>
24#include <ql/indexes/ibor/usdlibor.hpp>
25#include <ql/time/calendars/target.hpp>
26#include <ql/quotes/simplequote.hpp>
27#include <ql/pricingengines/swap/discountingswapengine.hpp>
28
29#include <string>
30
31using namespace QuantLib;
32using namespace boost::unit_test_framework;
33
34namespace equitytotalreturnswap_test {
35
36 struct CommonVars {
37
38 Date today;
39 Calendar calendar;
40 DayCounter dayCount;
41
42 ext::shared_ptr<EquityIndex> equityIndex;
43 ext::shared_ptr<IborIndex> usdLibor;
44 ext::shared_ptr<OvernightIndex> sofr;
45 RelinkableHandle<YieldTermStructure> interestHandle;
46 RelinkableHandle<YieldTermStructure> dividendHandle;
47 ext::shared_ptr<Quote> spot;
48 RelinkableHandle<Quote> spotHandle;
49 ext::shared_ptr<PricingEngine> discountEngine;
50
51 // utilities
52
53 CommonVars() {
54 calendar = TARGET();
55 dayCount = Actual365Fixed();
56
57 today = calendar.adjust(Date(27, January, 2023));
58 Settings::instance().evaluationDate() = today;
59
60 equityIndex = ext::make_shared<EquityIndex>(args: "eqIndex", args&: calendar, args&: interestHandle,
61 args&: dividendHandle, args&: spotHandle);
62 equityIndex->addFixing(fixingDate: Date(5, January, 2023), fixing: 9010.0);
63 equityIndex->addFixing(fixingDate: today, fixing: 8690.0);
64
65 sofr = ext::make_shared<Sofr>(args&: interestHandle);
66 sofr->addFixing(fixingDate: Date(3, January, 2023), fixing: 0.03);
67 sofr->addFixing(fixingDate: Date(4, January, 2023), fixing: 0.031);
68 sofr->addFixing(fixingDate: Date(5, January, 2023), fixing: 0.031);
69 sofr->addFixing(fixingDate: Date(6, January, 2023), fixing: 0.031);
70 sofr->addFixing(fixingDate: Date(9, January, 2023), fixing: 0.032);
71 sofr->addFixing(fixingDate: Date(10, January, 2023), fixing: 0.033);
72 sofr->addFixing(fixingDate: Date(11, January, 2023), fixing: 0.033);
73 sofr->addFixing(fixingDate: Date(12, January, 2023), fixing: 0.033);
74 sofr->addFixing(fixingDate: Date(13, January, 2023), fixing: 0.033);
75 sofr->addFixing(fixingDate: Date(17, January, 2023), fixing: 0.033);
76 sofr->addFixing(fixingDate: Date(18, January, 2023), fixing: 0.034);
77 sofr->addFixing(fixingDate: Date(19, January, 2023), fixing: 0.034);
78 sofr->addFixing(fixingDate: Date(20, January, 2023), fixing: 0.034);
79 sofr->addFixing(fixingDate: Date(23, January, 2023), fixing: 0.034);
80 sofr->addFixing(fixingDate: Date(24, January, 2023), fixing: 0.034);
81 sofr->addFixing(fixingDate: Date(25, January, 2023), fixing: 0.034);
82 sofr->addFixing(fixingDate: Date(26, January, 2023), fixing: 0.034);
83
84 usdLibor = ext::make_shared<USDLibor>(args: 3 * Months, args&: interestHandle);
85 usdLibor->addFixing(fixingDate: Date(3, January, 2023), fixing: 0.035);
86
87 interestHandle.linkTo(h: flatRate(forward: 0.0375, dc: dayCount));
88 dividendHandle.linkTo(h: flatRate(forward: 0.005, dc: dayCount));
89
90 discountEngine =
91 ext::shared_ptr<PricingEngine>(new DiscountingSwapEngine(interestHandle));
92
93 spot = ext::make_shared<SimpleQuote>(args: 8700.0);
94 spotHandle.linkTo(h: spot);
95 }
96
97 ext::shared_ptr<EquityTotalReturnSwap> createTRS(Swap::Type type,
98 const Schedule& schedule,
99 bool useOvernightIndex,
100 Rate margin = 0.0,
101 Real nominal = 1.0e7,
102 Real gearing = 1.0,
103 Natural paymentDelay = 0) {
104 ext::shared_ptr<EquityTotalReturnSwap> swap;
105 if (useOvernightIndex) {
106 swap = ext::make_shared<EquityTotalReturnSwap>(
107 args&: type, args&: nominal, args: schedule, args&: equityIndex, args&: sofr, args&: dayCount, args&: margin, args&: gearing,
108 args: schedule.calendar(), args: Following, args&: paymentDelay);
109 } else {
110 swap = ext::make_shared<EquityTotalReturnSwap>(
111 args&: type, args&: nominal, args: schedule, args&: equityIndex, args&: usdLibor, args&: dayCount, args&: margin, args&: gearing,
112 args: schedule.calendar(), args: Following, args&: paymentDelay);
113 }
114 swap->setPricingEngine(discountEngine);
115 return swap;
116 }
117
118 ext::shared_ptr<EquityTotalReturnSwap> createTRS(Swap::Type type,
119 const Date& start,
120 const Date& end,
121 bool useOvernightIndex,
122 Rate margin = 0.0,
123 Real nominal = 1.0e7,
124 Real gearing = 1.0,
125 Natural paymentDelay = 0) {
126 Schedule schedule = MakeSchedule()
127 .from(effectiveDate: start)
128 .to(terminationDate: end)
129 .withTenor(3 * Months)
130 .withCalendar(calendar)
131 .withConvention(Following)
132 .backwards();
133 return createTRS(type, schedule, useOvernightIndex, margin, nominal, gearing,
134 paymentDelay);
135 }
136 };
137
138 void checkFairMarginCalculation(Swap::Type type,
139 const Date& start,
140 const Date& end,
141 bool useOvernightIndex,
142 Rate margin = 0.0,
143 Real gearing = 1.0,
144 Natural paymentDelay = 0) {
145 CommonVars vars;
146
147 const Real tolerance = 1.0e-8;
148 const Real nominal = 1.0e7;
149
150 auto trs = vars.createTRS(type, start, end, useOvernightIndex, margin, nominal,
151 gearing, paymentDelay);
152 auto fairMargin = trs->fairMargin();
153 auto parTrs = vars.createTRS(type, start, end, useOvernightIndex, margin: fairMargin,
154 nominal, gearing, paymentDelay);
155
156 if ((std::fabs(x: parTrs->NPV()) > tolerance))
157 BOOST_ERROR("unable to imply a fair margin\n"
158 << " actual NPV: " << parTrs->NPV() << "\n"
159 << " expected NPV: 0.0 \n"
160 << " fair margin: " << fairMargin << "\n"
161 << " IR index name: " << trs->interestRateIndex()->name() << "\n");
162 }
163
164 Real legNPV(const Leg& leg, const Handle<YieldTermStructure>& ts) {
165 Real npv = 0.0;
166 std::for_each(first: leg.begin(), last: leg.end(), f: [&](const ext::shared_ptr<CashFlow>& cf) {
167 npv += cf->amount() * ts->discount(d: cf->date());
168 });
169 return npv;
170 }
171
172 void checkNPVCalculation(Swap::Type type,
173 const Date& start,
174 const Date& end,
175 bool useOvernightIndex,
176 Rate margin = 0.0,
177 Real gearing = 1.0,
178 Natural paymentDelay = 0) {
179 CommonVars vars;
180
181 const Real tolerance = 1.0e-2;
182 const Real nominal = 1.0e7;
183
184 auto trs = vars.createTRS(type, start, end, useOvernightIndex, margin, nominal,
185 gearing, paymentDelay);
186
187 auto npv = trs->NPV();
188
189 Real scaling = type == Swap::Type::Receiver ? 1.0 : -1.0;
190 auto equityLegNPV = trs->equityLegNPV();
191 auto replicatedEquityLegNPV = scaling * legNPV(leg: trs->equityLeg(), ts: vars.interestHandle);
192
193 if ((std::fabs(x: equityLegNPV - replicatedEquityLegNPV) > tolerance))
194 BOOST_ERROR("incorrect NPV of the equity leg\n"
195 << " actual NPV: " << equityLegNPV << "\n"
196 << " expected NPV: " << replicatedEquityLegNPV << "\n");
197
198 auto interestLegNPV = trs->interestRateLegNPV();
199 auto replicatedInterestLegNPV = -scaling * legNPV(leg: trs->interestRateLeg(), ts: vars.interestHandle);
200
201 if ((std::fabs(x: interestLegNPV - replicatedInterestLegNPV) > tolerance))
202 BOOST_ERROR("incorrect NPV of the interest leg\n"
203 << " actual NPV: " << interestLegNPV << "\n"
204 << " expected NPV: " << replicatedInterestLegNPV << "\n");
205
206 if ((std::fabs(x: npv - (equityLegNPV + interestLegNPV)) > tolerance))
207 BOOST_ERROR("summing legs NPV does not replicate the instrument NPV\n"
208 << " actual NPV: " << npv << "\n"
209 << " NPV from summing legs: " << equityLegNPV + interestLegNPV << "\n");
210 }
211}
212
213void EquityTotalReturnSwapTest::testFairMargin() {
214 BOOST_TEST_MESSAGE("Testing fair margin...");
215
216 using namespace equitytotalreturnswap_test;
217
218 // Check TRS vs Libor-type index
219 checkFairMarginCalculation(type: Swap::Receiver, start: Date(5, January, 2023), end: Date(5, April, 2023), useOvernightIndex: false);
220 checkFairMarginCalculation(type: Swap::Payer, start: Date(5, January, 2023), end: Date(5, April, 2023), useOvernightIndex: false,
221 margin: 0.01);
222 checkFairMarginCalculation(type: Swap::Payer, start: Date(5, January, 2023), end: Date(5, April, 2023), useOvernightIndex: false,
223 margin: 0.0, gearing: 0.0);
224 checkFairMarginCalculation(type: Swap::Receiver, start: Date(31, January, 2023), end: Date(30, April, 2023),
225 useOvernightIndex: false, margin: -0.005, gearing: 1.0, paymentDelay: 2);
226
227 // Check TRS vs overnight index
228 checkFairMarginCalculation(type: Swap::Receiver, start: Date(5, January, 2023), end: Date(5, April, 2023), useOvernightIndex: true);
229 checkFairMarginCalculation(type: Swap::Payer, start: Date(5, January, 2023), end: Date(5, April, 2023), useOvernightIndex: true,
230 margin: 0.01);
231 checkFairMarginCalculation(type: Swap::Receiver, start: Date(31, January, 2023), end: Date(30, April, 2023), useOvernightIndex: true,
232 margin: -0.005, gearing: 1.0, paymentDelay: 2);
233}
234
235void EquityTotalReturnSwapTest::testErrorWhenNegativeNominal() {
236 BOOST_TEST_MESSAGE("Testing error when negative nominal...");
237
238 using namespace equitytotalreturnswap_test;
239
240 CommonVars vars;
241
242 BOOST_CHECK_EXCEPTION(
243 vars.createTRS(Swap::Receiver, Date(5, January, 2023), Date(5, April, 2023), false, 0.0,
244 -1.e7),
245 Error,
246 ExpectedErrorMessage("Nominal cannot be negative"));
247}
248
249void EquityTotalReturnSwapTest::testErrorWhenNoPaymentCalendar() {
250 BOOST_TEST_MESSAGE("Testing error when payment calendar is missing...");
251
252 using namespace equitytotalreturnswap_test;
253
254 CommonVars vars;
255
256 auto sch = Schedule(Date(5, January, 2023), Date(5, April, 2023), 3 * Months, Calendar(),
257 Unadjusted, Unadjusted, DateGeneration::Rule::Backward, false);
258
259 BOOST_CHECK_EXCEPTION(
260 vars.createTRS(Swap::Receiver, sch, false), Error,
261 ExpectedErrorMessage("Calendar in schedule cannot be empty"));
262}
263
264void EquityTotalReturnSwapTest::testEquityLegNPV() {
265 BOOST_TEST_MESSAGE("Testing equity leg NPV replication...");
266
267 using namespace equitytotalreturnswap_test;
268
269 CommonVars vars;
270
271 const Real tolerance = 1.0e-8;
272
273 Date start(5, January, 2023);
274 Date end(5, April, 2023);
275
276 auto trs = vars.createTRS(type: Swap::Receiver, start, end, useOvernightIndex: false);
277 auto actualEquityLegNPV = trs->equityLegNPV();
278
279 auto eqIdx = trs->equityIndex();
280 auto discount = vars.interestHandle->discount(d: end);
281 auto expectedEquityLegNPV =
282 (eqIdx->fixing(fixingDate: end) / eqIdx->fixing(fixingDate: start) - 1.0) * trs->nominal() * discount;
283
284 if ((std::fabs(x: actualEquityLegNPV - expectedEquityLegNPV) > tolerance))
285 BOOST_ERROR("unable to replicate equity leg NPV\n"
286 << " actual NPV: " << actualEquityLegNPV << "\n"
287 << " expected NPV: " << expectedEquityLegNPV << "\n");
288}
289
290void EquityTotalReturnSwapTest::testTRSNPV() {
291 BOOST_TEST_MESSAGE("Testing TRS NPV...");
292
293 using namespace equitytotalreturnswap_test;
294
295 CommonVars vars;
296
297 // Check TRS vs Libor-type index
298 checkNPVCalculation(type: Swap::Receiver, start: Date(5, January, 2023), end: Date(5, April, 2023), useOvernightIndex: false);
299 checkNPVCalculation(type: Swap::Payer, start: Date(5, January, 2023), end: Date(5, April, 2023), useOvernightIndex: false, margin: 0.01);
300 checkNPVCalculation(type: Swap::Payer, start: Date(5, January, 2023), end: Date(5, April, 2023), useOvernightIndex: false, margin: 0.0, gearing: 0.0);
301 checkNPVCalculation(type: Swap::Receiver, start: Date(31, January, 2023), end: Date(30, April, 2023), useOvernightIndex: false,
302 margin: -0.005, gearing: 1.0, paymentDelay: 2);
303
304 //// Check TRS vs overnight index
305 checkNPVCalculation(type: Swap::Receiver, start: Date(5, January, 2023), end: Date(5, April, 2023), useOvernightIndex: true);
306 checkNPVCalculation(type: Swap::Payer, start: Date(5, January, 2023), end: Date(5, April, 2023), useOvernightIndex: true, margin: 0.01);
307 checkNPVCalculation(type: Swap::Receiver, start: Date(31, January, 2023), end: Date(30, April, 2023), useOvernightIndex: true,
308 margin: -0.005, gearing: 1.0, paymentDelay: 2);
309}
310
311test_suite* EquityTotalReturnSwapTest::suite() {
312 auto* suite = BOOST_TEST_SUITE("Equity total return swap tests");
313
314 suite->add(QUANTLIB_TEST_CASE(&EquityTotalReturnSwapTest::testFairMargin));
315 suite->add(QUANTLIB_TEST_CASE(&EquityTotalReturnSwapTest::testErrorWhenNegativeNominal));
316 suite->add(QUANTLIB_TEST_CASE(&EquityTotalReturnSwapTest::testErrorWhenNoPaymentCalendar));
317 suite->add(QUANTLIB_TEST_CASE(&EquityTotalReturnSwapTest::testEquityLegNPV));
318 suite->add(QUANTLIB_TEST_CASE(&EquityTotalReturnSwapTest::testTRSNPV));
319
320 return suite;
321}
322

source code of quantlib/test-suite/equitytotalreturnswap.cpp