[go: up one dir, main page]

1/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3/*
4 Copyright (C) 2006, 2008, 2009 StatPro Italia srl
5 Copyright (C) 2007 Ferdinando Ametrano
6
7 This file is part of QuantLib, a free-software/open-source library
8 for financial quantitative analysts and developers - http://quantlib.org/
9
10 QuantLib is free software: you can redistribute it and/or modify it
11 under the terms of the QuantLib license. You should have received a
12 copy of the license along with this program; if not, please email
13 <quantlib-dev@lists.sf.net>. The license is also available online at
14 <http://quantlib.org/license.shtml>.
15
16 This program is distributed in the hope that it will be useful, but WITHOUT
17 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18 FOR A PARTICULAR PURPOSE. See the license for more details.
19*/
20
21#include "convertiblebonds.hpp"
22#include "utilities.hpp"
23#include <ql/instruments/bonds/convertiblebonds.hpp>
24#include <ql/instruments/bonds/zerocouponbond.hpp>
25#include <ql/instruments/bonds/fixedratebond.hpp>
26#include <ql/instruments/bonds/floatingratebond.hpp>
27#include <ql/instruments/vanillaoption.hpp>
28#include <ql/pricingengines/bond/binomialconvertibleengine.hpp>
29#include <ql/pricingengines/vanilla/binomialengine.hpp>
30#include <ql/time/calendars/target.hpp>
31#include <ql/time/calendars/unitedstates.hpp>
32#include <ql/time/daycounters/actual360.hpp>
33#include <ql/time/daycounters/thirty360.hpp>
34#include <ql/indexes/ibor/euribor.hpp>
35#include <ql/termstructures/yield/flatforward.hpp>
36#include <ql/termstructures/yield/forwardcurve.hpp>
37#include <ql/termstructures/yield/forwardspreadedtermstructure.hpp>
38#include <ql/termstructures/volatility/equityfx/blackconstantvol.hpp>
39#include <ql/math/interpolations/backwardflatinterpolation.hpp>
40#include <ql/utilities/dataformatters.hpp>
41#include <ql/cashflows/couponpricer.hpp>
42#include <ql/cashflows/cashflows.hpp>
43#include <ql/pricingengines/bond/discountingbondengine.hpp>
44
45using namespace QuantLib;
46using namespace boost::unit_test_framework;
47
48namespace convertible_bonds_test {
49
50 struct CommonVars {
51 // global data
52 Date today, issueDate, maturityDate;
53 Calendar calendar;
54 DayCounter dayCounter;
55 Frequency frequency;
56 Natural settlementDays;
57
58 RelinkableHandle<Quote> underlying;
59 RelinkableHandle<YieldTermStructure> dividendYield, riskFreeRate;
60 RelinkableHandle<BlackVolTermStructure> volatility;
61 ext::shared_ptr<BlackScholesMertonProcess> process;
62
63 RelinkableHandle<Quote> creditSpread;
64
65 CallabilitySchedule no_callability;
66
67 Real faceAmount, redemption, conversionRatio;
68
69 // setup
70 CommonVars() {
71 calendar = TARGET();
72
73 today = calendar.adjust(Date::todaysDate());
74 Settings::instance().evaluationDate() = today;
75
76 dayCounter = Actual360();
77 frequency = Annual;
78 settlementDays = 3;
79
80 issueDate = calendar.advance(today,n: 2,unit: Days);
81 maturityDate = calendar.advance(issueDate, n: 10, unit: Years);
82 // reset to avoid inconsistencies as the schedule is backwards
83 issueDate = calendar.advance(maturityDate, n: -10, unit: Years);
84
85 underlying.linkTo(h: ext::make_shared<SimpleQuote>(args: 50.0));
86 dividendYield.linkTo(h: flatRate(today, forward: 0.02, dc: dayCounter));
87 riskFreeRate.linkTo(h: flatRate(today, forward: 0.05, dc: dayCounter));
88 volatility.linkTo(h: flatVol(today, volatility: 0.15, dc: dayCounter));
89
90 process = ext::make_shared<BlackScholesMertonProcess>(
91 args&: underlying, args&: dividendYield, args&: riskFreeRate, args&: volatility);
92
93 creditSpread.linkTo(h: ext::make_shared<SimpleQuote>(args: 0.005));
94
95 // it fails with 1000000
96 // faceAmount = 1000000.0;
97 faceAmount = 100.0;
98 redemption = 100.0;
99 conversionRatio = redemption/underlying->value();
100 }
101 };
102
103}
104
105
106void ConvertibleBondTest::testBond() {
107
108 /* when deeply out-of-the-money, the value of the convertible bond
109 should equal that of the underlying plain-vanilla bond. */
110
111 BOOST_TEST_MESSAGE(
112 "Testing out-of-the-money convertible bonds against vanilla bonds...");
113
114 using namespace convertible_bonds_test;
115
116 CommonVars vars;
117
118 vars.conversionRatio = 1.0e-16;
119
120 ext::shared_ptr<Exercise> euExercise =
121 ext::make_shared<EuropeanExercise>(args&: vars.maturityDate);
122 ext::shared_ptr<Exercise> amExercise =
123 ext::make_shared<AmericanExercise>(args&: vars.issueDate,
124 args&: vars.maturityDate);
125
126 Size timeSteps = 1001;
127 ext::shared_ptr<PricingEngine> engine =
128 ext::make_shared<BinomialConvertibleEngine<CoxRossRubinstein> >(args&: vars.process,
129 args&: timeSteps,
130 args&: vars.creditSpread);
131
132 Handle<YieldTermStructure> discountCurve(
133 ext::make_shared<ForwardSpreadedTermStructure>(args&: vars.riskFreeRate,
134 args&: vars.creditSpread));
135
136 // zero-coupon
137
138 Schedule schedule =
139 MakeSchedule().from(effectiveDate: vars.issueDate)
140 .to(terminationDate: vars.maturityDate)
141 .withFrequency(Once)
142 .withCalendar(vars.calendar)
143 .backwards();
144
145 ConvertibleZeroCouponBond euZero(euExercise, vars.conversionRatio,
146 vars.no_callability,
147 vars.issueDate, vars.settlementDays,
148 vars.dayCounter, schedule,
149 vars.redemption);
150 euZero.setPricingEngine(engine);
151
152 ConvertibleZeroCouponBond amZero(amExercise, vars.conversionRatio,
153 vars.no_callability,
154 vars.issueDate, vars.settlementDays,
155 vars.dayCounter, schedule,
156 vars.redemption);
157 amZero.setPricingEngine(engine);
158
159 ZeroCouponBond zero(vars.settlementDays, vars.calendar,
160 100.0, vars.maturityDate,
161 Following, vars.redemption, vars.issueDate);
162
163 ext::shared_ptr<PricingEngine> bondEngine =
164 ext::make_shared<DiscountingBondEngine>(args&: discountCurve);
165 zero.setPricingEngine(bondEngine);
166
167 Real tolerance = 1.0e-2 * (vars.faceAmount/100.0);
168
169 Real error = std::fabs(x: euZero.NPV()-zero.settlementValue());
170 if (error > tolerance) {
171 BOOST_ERROR("failed to reproduce zero-coupon bond price:"
172 << "\n calculated: " << euZero.NPV()
173 << "\n expected: " << zero.settlementValue()
174 << "\n error: " << error);
175 }
176
177 error = std::fabs(x: amZero.NPV()-zero.settlementValue());
178 if (error > tolerance) {
179 BOOST_ERROR("failed to reproduce zero-coupon bond price:"
180 << "\n calculated: " << amZero.NPV()
181 << "\n expected: " << zero.settlementValue()
182 << "\n error: " << error);
183 }
184
185 // coupon
186
187 std::vector<Rate> coupons(1, 0.05);
188
189 schedule = MakeSchedule().from(effectiveDate: vars.issueDate)
190 .to(terminationDate: vars.maturityDate)
191 .withFrequency(vars.frequency)
192 .withCalendar(vars.calendar)
193 .backwards();
194
195 ConvertibleFixedCouponBond euFixed(euExercise, vars.conversionRatio,
196 vars.no_callability,
197 vars.issueDate, vars.settlementDays,
198 coupons, vars.dayCounter,
199 schedule, vars.redemption);
200 euFixed.setPricingEngine(engine);
201
202 ConvertibleFixedCouponBond amFixed(amExercise, vars.conversionRatio,
203 vars.no_callability,
204 vars.issueDate, vars.settlementDays,
205 coupons, vars.dayCounter,
206 schedule, vars.redemption);
207 amFixed.setPricingEngine(engine);
208
209 FixedRateBond fixed(vars.settlementDays, vars.faceAmount, schedule,
210 coupons, vars.dayCounter, Following,
211 vars.redemption, vars.issueDate);
212
213 fixed.setPricingEngine(bondEngine);
214
215 tolerance = 2.0e-2 * (vars.faceAmount/100.0);
216
217 error = std::fabs(x: euFixed.NPV()-fixed.settlementValue());
218 if (error > tolerance) {
219 BOOST_ERROR("failed to reproduce fixed-coupon bond price:"
220 << "\n calculated: " << euFixed.NPV()
221 << "\n expected: " << fixed.settlementValue()
222 << "\n error: " << error);
223 }
224
225 error = std::fabs(x: amFixed.NPV()-fixed.settlementValue());
226 if (error > tolerance) {
227 BOOST_ERROR("failed to reproduce fixed-coupon bond price:"
228 << "\n calculated: " << amFixed.NPV()
229 << "\n expected: " << fixed.settlementValue()
230 << "\n error: " << error);
231 }
232
233 // floating-rate
234
235 ext::shared_ptr<IborIndex> index =
236 ext::make_shared<Euribor1Y>(args&: discountCurve);
237 Natural fixingDays = 2;
238 std::vector<Real> gearings(1, 1.0);
239 std::vector<Rate> spreads;
240
241 ConvertibleFloatingRateBond euFloating(euExercise, vars.conversionRatio,
242 vars.no_callability,
243 vars.issueDate, vars.settlementDays,
244 index, fixingDays, spreads,
245 vars.dayCounter, schedule,
246 vars.redemption);
247 euFloating.setPricingEngine(engine);
248
249 ConvertibleFloatingRateBond amFloating(amExercise, vars.conversionRatio,
250 vars.no_callability,
251 vars.issueDate, vars.settlementDays,
252 index, fixingDays, spreads,
253 vars.dayCounter, schedule,
254 vars.redemption);
255 amFloating.setPricingEngine(engine);
256
257 ext::shared_ptr<IborCouponPricer> pricer =
258 ext::make_shared<BlackIborCouponPricer>(
259 args: Handle<OptionletVolatilityStructure>());
260
261 Schedule floatSchedule(vars.issueDate, vars.maturityDate,
262 Period(vars.frequency),
263 vars.calendar, Following, Following,
264 DateGeneration::Backward, false);
265
266 FloatingRateBond floating(vars.settlementDays, vars.faceAmount, floatSchedule,
267 index, vars.dayCounter, Following, fixingDays,
268 gearings, spreads,
269 std::vector<Rate>(), std::vector<Rate>(),
270 false,
271 vars.redemption, vars.issueDate);
272
273 floating.setPricingEngine(bondEngine);
274 setCouponPricer(leg: floating.cashflows(),pricer);
275
276 tolerance = 2.0e-2 * (vars.faceAmount/100.0);
277
278 error = std::fabs(x: euFloating.NPV()-floating.settlementValue());
279 if (error > tolerance) {
280 BOOST_ERROR("failed to reproduce floating-rate bond price:"
281 << "\n calculated: " << euFloating.NPV()
282 << "\n expected: " << floating.settlementValue()
283 << "\n error: " << error);
284 }
285
286 error = std::fabs(x: amFloating.NPV()-floating.settlementValue());
287 if (error > tolerance) {
288 BOOST_ERROR("failed to reproduce floating-rate bond price:"
289 << "\n calculated: " << amFloating.NPV()
290 << "\n expected: " << floating.settlementValue()
291 << "\n error: " << error);
292 }
293}
294
295void ConvertibleBondTest::testOption() {
296
297 /* a zero-coupon convertible bond with no credit spread is
298 equivalent to a call option. */
299
300 BOOST_TEST_MESSAGE(
301 "Testing zero-coupon convertible bonds against vanilla option...");
302
303 using namespace convertible_bonds_test;
304
305 CommonVars vars;
306
307 ext::shared_ptr<Exercise> euExercise =
308 ext::make_shared<EuropeanExercise>(args&: vars.maturityDate);
309
310 vars.settlementDays = 0;
311
312 Size timeSteps = 2001;
313 ext::shared_ptr<PricingEngine> engine =
314 ext::make_shared<BinomialConvertibleEngine<CoxRossRubinstein> >(
315 args&: vars.process, args&: timeSteps, args&: vars.creditSpread);
316 ext::shared_ptr<PricingEngine> vanillaEngine =
317 ext::make_shared<BinomialVanillaEngine<CoxRossRubinstein> >(
318 args&: vars.process, args&: timeSteps);
319
320 vars.creditSpread.linkTo(h: ext::make_shared<SimpleQuote>(args: 0.0));
321
322 Real conversionStrike = vars.redemption/vars.conversionRatio;
323 ext::shared_ptr<StrikedTypePayoff> payoff =
324 ext::make_shared<PlainVanillaPayoff>(args: Option::Call, args&: conversionStrike);
325
326 Schedule schedule = MakeSchedule().from(effectiveDate: vars.issueDate)
327 .to(terminationDate: vars.maturityDate)
328 .withFrequency(Once)
329 .withCalendar(vars.calendar)
330 .backwards();
331
332 ConvertibleZeroCouponBond euZero(euExercise, vars.conversionRatio,
333 vars.no_callability,
334 vars.issueDate, vars.settlementDays,
335 vars.dayCounter, schedule,
336 vars.redemption);
337 euZero.setPricingEngine(engine);
338
339 VanillaOption euOption(payoff, euExercise);
340 euOption.setPricingEngine(vanillaEngine);
341
342 Real tolerance = 5.0e-2 * (vars.faceAmount/100.0);
343
344 Real expected = vars.faceAmount/100.0 *
345 (vars.redemption * vars.riskFreeRate->discount(d: vars.maturityDate)
346 + vars.conversionRatio* euOption.NPV());
347 Real error = std::fabs(x: euZero.NPV()-expected);
348 if (error > tolerance) {
349 BOOST_ERROR("failed to reproduce plain-option price:"
350 << "\n calculated: " << euZero.NPV()
351 << "\n expected: " << expected
352 << "\n error: " << error
353 << "\n tolerance: " << tolerance);
354 }
355}
356
357void ConvertibleBondTest::testRegression() {
358
359 BOOST_TEST_MESSAGE(
360 "Testing fixed-coupon convertible bond in known regression case...");
361
362 Date today = Date(23, December, 2008);
363 Date tomorrow = today + 1;
364
365 Settings::instance().evaluationDate() = tomorrow;
366
367 Handle<Quote> u(ext::make_shared<SimpleQuote>(args: 2.9084382818797443));
368
369 std::vector<Date> dates(25);
370 std::vector<Rate> forwards(25);
371 dates[0] = Date(29,December,2008); forwards[0] = 0.0025999342800;
372 dates[1] = Date(5,January,2009); forwards[1] = 0.0025999342800;
373 dates[2] = Date(29,January,2009); forwards[2] = 0.0053123275500;
374 dates[3] = Date(27,February,2009); forwards[3] = 0.0197049598721;
375 dates[4] = Date(30,March,2009); forwards[4] = 0.0220524845296;
376 dates[5] = Date(29,June,2009); forwards[5] = 0.0217076395643;
377 dates[6] = Date(29,December,2009); forwards[6] = 0.0230349627478;
378 dates[7] = Date(29,December,2010); forwards[7] = 0.0087631647476;
379 dates[8] = Date(29,December,2011); forwards[8] = 0.0219084299499;
380 dates[9] = Date(31,December,2012); forwards[9] = 0.0244798766219;
381 dates[10] = Date(30,December,2013); forwards[10] = 0.0267885498456;
382 dates[11] = Date(29,December,2014); forwards[11] = 0.0266922867562;
383 dates[12] = Date(29,December,2015); forwards[12] = 0.0271052126386;
384 dates[13] = Date(29,December,2016); forwards[13] = 0.0268829891648;
385 dates[14] = Date(29,December,2017); forwards[14] = 0.0264594744498;
386 dates[15] = Date(31,December,2018); forwards[15] = 0.0273450367424;
387 dates[16] = Date(30,December,2019); forwards[16] = 0.0294852614749;
388 dates[17] = Date(29,December,2020); forwards[17] = 0.0285556119719;
389 dates[18] = Date(29,December,2021); forwards[18] = 0.0305557764659;
390 dates[19] = Date(29,December,2022); forwards[19] = 0.0292244738422;
391 dates[20] = Date(29,December,2023); forwards[20] = 0.0263917004194;
392 dates[21] = Date(29,December,2028); forwards[21] = 0.0239626970243;
393 dates[22] = Date(29,December,2033); forwards[22] = 0.0216417108090;
394 dates[23] = Date(29,December,2038); forwards[23] = 0.0228343838422;
395 dates[24] = Date(31,December,2199); forwards[24] = 0.0228343838422;
396
397 Handle<YieldTermStructure> r(
398 ext::make_shared<ForwardCurve>(args&: dates, args&: forwards, args: Actual360()));
399
400 Handle<BlackVolTermStructure> sigma(ext::make_shared<BlackConstantVol>(
401 args&: tomorrow, args: NullCalendar(), args: 21.685235548092248,
402 args: Thirty360(Thirty360::BondBasis)));
403
404 ext::shared_ptr<BlackProcess> process =
405 ext::make_shared<BlackProcess>(args&: u,args&: r,args&: sigma);
406
407 Handle<Quote> spread(ext::make_shared<SimpleQuote>(args: 0.11498700678012874));
408
409 Date issueDate(23, July, 2008);
410 Date maturityDate(1, August, 2013);
411 Calendar calendar = UnitedStates(UnitedStates::GovernmentBond);
412 Schedule schedule = MakeSchedule().from(effectiveDate: issueDate)
413 .to(terminationDate: maturityDate)
414 .withTenor(6*Months)
415 .withCalendar(calendar)
416 .withConvention(Unadjusted);
417 Natural settlementDays = 3;
418 ext::shared_ptr<Exercise> exercise =
419 ext::make_shared<EuropeanExercise>(args&: maturityDate);
420 Real conversionRatio = 100.0/20.3175;
421 std::vector<Rate> coupons(schedule.size()-1, 0.05);
422 DayCounter dayCounter = Thirty360(Thirty360::BondBasis);
423 CallabilitySchedule no_callability;
424 DividendSchedule no_dividends;
425 Real redemption = 100.0;
426
427 ConvertibleFixedCouponBond bond(exercise, conversionRatio,
428 no_callability,
429 issueDate, settlementDays,
430 coupons, dayCounter,
431 schedule, redemption);
432 bond.setPricingEngine(ext::make_shared<BinomialConvertibleEngine<CoxRossRubinstein> >(
433 args&: process, args: 600, args&: spread, args&: no_dividends));
434
435 try {
436 Real x = bond.NPV(); // should throw; if not, an INF was not detected.
437 BOOST_FAIL("INF result was not detected: " << x << " returned");
438 } catch (Error&) {
439 // as expected. Do nothing.
440
441 // Note: we're expecting an Error we threw, not just any
442 // exception. If something else is thrown, then there's
443 // another problem and the test must fail.
444 }
445}
446
447
448test_suite* ConvertibleBondTest::suite() {
449 auto* suite = BOOST_TEST_SUITE("Convertible bond tests");
450
451 suite->add(QUANTLIB_TEST_CASE(&ConvertibleBondTest::testBond));
452 suite->add(QUANTLIB_TEST_CASE(&ConvertibleBondTest::testOption));
453 suite->add(QUANTLIB_TEST_CASE(&ConvertibleBondTest::testRegression));
454
455 return suite;
456}
457

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