How Convertible Bond Delta Scales with the Conversion Ratio
Summary
This example investigates why a convertible bond priced with QuantLib produces an equity delta greater than one. The bond is near the money, with a conversion ratio of roughly eight shares per bond. The question compares delta from the binomial tree with a finite-difference estimate obtained by repricing after a small increase in the underlying share price; both methods give values near two before adjustment.
The accepted explanation is that the bond’s conversion option represents exposure to multiple shares. Its delta is therefore expressed per bond and can exceed one share-equivalent. Dividing by the conversion ratio converts that exposure to a per-share scale, yielding values below one in the reported example. Those figures illustrate the scale change for this particular setup; the document does not establish a general pricing error or validate the numerical accuracy of the tree methods. The relevant lesson is to check the unit of the instrument and the delta convention before judging whether a sensitivity is unexpectedly large.
Key ideas
- A convertible bond can have delta above one when delta is quoted per bond.
- The conversion ratio determines how many shares the conversion option represents.
- Dividing bond-level delta by the conversion ratio expresses sensitivity on a per-share scale.
- Tree-derived and finite-difference deltas can be compared, but their units must match.
Tags
Full text
# QuantLib convertible bond pricing generates strange delta
# QuantLib convertible bond pricing generates strange delta
I am trying to generate equity delta for convertible bond using QuantLib(version 1.14) functions, but the deltas generated either using a repricing approach or by directly obtaining from the tree(code as below) all generates deltas bigger than 1.
```
#include <ql/qldefines.hpp>
#ifdef BOOST_MSVC
# include <ql/auto_link.hpp>
#endif
#include <ql/experimental/convertiblebonds/convertiblebond.hpp>
#include <ql/experimental/convertiblebonds/binomialconvertibleengine.hpp>
#include <ql/time/calendars/target.hpp>
#include <ql/time/daycounters/thirty360.hpp>
#include <ql/utilities/dataformatters.hpp>
#include <boost/timer.hpp>
#include <iostream>
#include <iomanip>
#define LENGTH(a) (sizeof(a)/sizeof(a[0]))
using namespace QuantLib;
#if defined(QL_ENABLE_SESSIONS)
namespace QuantLib {
Integer sessionId() { return 0; }
}
#endif
template<typename Method>
auto calculate(double underlying)
{
auto analysis_date = Date(8, May, 2020);
Real spreadRate = 0.0125;
Spread dividendYield = 0.0;
Rate riskFreeRate = 0.03;
Volatility volatility = 0.3436553822850044;
Integer settlementDays = 0;
Integer length = 3;
Real redemption = 100.0;
Real conversionRatio = 100 / 12.1; // at the money
// set up dates/schedules
Calendar calendar = TARGET();
Date today = calendar.adjust(analysis_date);
Settings::instance().evaluationDate() = today;
Date settlementDate = calendar.advance(today, settlementDays, Days);
Date exerciseDate = calendar.advance(settlementDate, length, Years);
Date issueDate = calendar.advance(exerciseDate, -length, Years);
BusinessDayConvention convention = ModifiedFollowing;
Frequency frequency = Annual;
Schedule schedule(issueDate, exerciseDate, Period(frequency), calendar, convention, convention, DateGeneration::Backward, false);
DividendSchedule dividends;
CallabilitySchedule callability;
std::vector<Real> coupons(1, 0.05);
DayCounter bondDayCount = Thirty360();
for (Date d = today + 6 * Months; d < exerciseDate; d += 6 * Months)
{
dividends.push_back(boost::shared_ptr<Dividend>(new FixedDividend(1, d)));
}
DayCounter dayCounter = Actual365Fixed();
boost::shared_ptr<Exercise> exercise(new EuropeanExercise(exerciseDate));
boost::shared_ptr<Exercise> amExercise(new AmericanExercise(settlementDate, exerciseDate));
Handle<Quote> underlyingH(boost::shared_ptr<Quote>(new SimpleQuote(underlying)));
Handle<YieldTermStructure> flatTermStructure(boost::shared_ptr<YieldTermStructure>(new FlatForward(settlementDate, riskFreeRate, dayCounter)));
Handle<YieldTermStructure> flatDividendTS(boost::shared_ptr<YieldTermStructure>(new FlatForward(settlementDate, dividendYield, dayCounter)));
Handle<BlackVolTermStructure> flatVolTS(boost::shared_ptr<BlackVolTermStructure>(new BlackConstantVol(settlementDate, calendar, volatility, dayCounter)));
boost::shared_ptr<BlackScholesMertonProcess> stochasticProcess(new BlackScholesMertonProcess(underlyingH, flatDividendTS, flatTermStructure, flatVolTS));
Size timeSteps = 801;
Handle<Quote> creditSpread(boost::shared_ptr<Quote>(new SimpleQuote(spreadRate)));
boost::shared_ptr<Quote> rate(new SimpleQuote(riskFreeRate));
Handle<YieldTermStructure> discountCurve(boost::shared_ptr<YieldTermStructure>(new FlatForward(today, Handle<Quote>(rate), dayCounter)));
ConvertibleFixedCouponBond americanBond(amExercise, conversionRatio, dividends, callability, creditSpread, issueDate, settlementDays, coupons, bondDayCount,
schedule, redemption);
americanBond.setPricingEngine(boost::shared_ptr<PricingEngine>(new BinomialConvertibleEngine<Method>(stochasticProcess, timeSteps)));
Real npv = americanBond.NPV();
Real delta = americanBond.delta();
return std::make_pair(npv, delta);
}
template<typename Method> void calc_sensitivity()
{
auto spot=10.34;
auto [npv, delta]= calculate<Method>(spot);
auto [npv2, delta2]=calculate<Method>(spot*1.01);
delta2 = (npv2 - npv) / (spot*0.01);
// write column headings
Size widths[] =
{ 14, 14, 14, 14 };
Size totalWidth = widths[0] + widths[1] + widths[2] + widths[3];
std::string rule(totalWidth, '-'), dblrule(totalWidth, '=');
std::cout << typeid(Method).name() << std::endl;
std::cout << dblrule << std::endl;
std::cout << std::setw(widths[0]) << std::left << "PV0" << std::setw(widths[1]) << std::left << "PV1" << std::setw(widths[2]) << std::left
<< "Tree Delta" << std::setw(widths[3]) << std::left << "Iterative Delta" << std::endl;
std::cout << rule << std::endl;
std::cout << std::setw(widths[0]) << std::left << npv << std::fixed << std::setw(widths[1]) << std::left << npv2 << std::setw(widths[2]) << std::left
<< delta << std::setw(widths[2]) << std::left << delta2 << std::endl;
std::cout << dblrule << std::endl;
}
int main(int, char*[])
{
try
{
boost::timer timer;
std::cout << std::endl;
calc_sensitivity<JarrowRudd>();
calc_sensitivity<CoxRossRubinstein>();
calc_sensitivity<AdditiveEQPBinomialTree>();
double seconds = timer.elapsed();
Integer hours = int(seconds / 3600);
seconds -= hours * 3600;
Integer minutes = int(seconds / 60);
seconds -= minutes * 60;
std::cout << " \nRun completed in ";
if (hours > 0)
std::cout << hours << " h ";
if (hours > 0 || minutes > 0)
std::cout << minutes << " m ";
std::cout << std::fixed << std::setprecision(0) << seconds << " s\n" << std::endl;
return 0;
} catch (std::exception &e)
{
std::cerr << e.what() << std::endl;
return 1;
} catch (...)
{
std::cerr << "unknown error" << std::endl;
return 1;
}
}
```
below code for generating the delta in binomialconvertibleengine.hpp:
```
convertible.initialize(lattice, maturity);
convertible.rollback(time_grid[1]);
auto value_up = convertible.values()[1];
auto value_down = convertible.values()[0];
auto s_up = tree->underlying(1, 1);
auto s_down = tree->underlying(1, 0);
auto delta = (value_up - value_down) / (s_up - s_down);
```
below is the result:
```
N8QuantLib10JarrowRuddE
========================================================
PV0 PV1 Tree Delta Iterative Delta
--------------------------------------------------------
104.455 104.677186 1.985473 2.148005
========================================================
N8QuantLib17CoxRossRubinsteinE
========================================================
PV0 PV1 Tree Delta Iterative Delta
--------------------------------------------------------
104.454988 104.673906 1.984574 2.117198
========================================================
N8QuantLib23AdditiveEQPBinomialTreeE
========================================================
PV0 PV1 Tree Delta Iterative Delta
--------------------------------------------------------
104.480229 104.704433 1.996642 2.168310
========================================================
Remark: Should have divided by the conversion ratio, the updated result is as below:
========================================================
PV0 PV1 Tree Delta Iterative Delta
--------------------------------------------------------
110.041 110.544284 0.530003 0.588715
========================================================
now the deltas looks nice.
```
## Answer by Luigi Ballabio (score 3, accepted)
https://quant.stackexchange.com/a/54736
You have a conversion ratio of $100/12.1 \approx 8.26$, so the convertibility is an option on about 8 underlying stocks and the delta scales accordingly. I'm not familiar about the way it's quoted, though. Did you expect it to be the delta for one unit of stock?Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.