Bond duration incorrect ?

Posted by Dirk Eddelbuettel on
URL: http://quantlib.414.s1.nabble.com/Bond-duration-incorrect-tp4434.html

Rainy day here so I got to play a little, at long last, with the fixed income
functions. I can't make sense of duration() in CashFlows/analysis.{cpp,hpp}
In particular this line seems wrong:

        totalNPV -= marketPrice;

before the sum of present value times time (here variable totalDuration) gets
divided by totalNPV as in

        return totalDuration/totalNPV;

which is supported via quick googling [ http://www.finpipe.com/duration.htm ]
and one of the investments texts here.

Likewise, modified duration is normaly given as Macaulay Duration divided by
(1 + y / nbcoupons) -- so that a second fix would be

  case Duration::Modified:
    //return totalDuration / totalNPV / y;
    return totalDuration / totalNPV / (1 + y/rate.frequency());

A simplistic program, taken from one of the regression test cases, is below
and I added a local copy of the duration function from analysis.cpp.

I may have overlooked something, but I guess we should at least add some
validated regression tests for some duration, convexity, ... cases.
Any volunteers?

Oh, and one last question: why isn't the redemption at par payment already
included when I call cashflow() on a bond?

Dirk


#include <iostream>
#include <ql/quantlib.hpp>

using namespace std;
using namespace QuantLib;

Time myDuration(const std::vector<boost::shared_ptr<CashFlow> >& cashflows,
                Real marketPrice,
                const InterestRate& rate,
                Duration::Type type,
                Date settlementDate) {

  if (settlementDate == Date())
    settlementDate = Settings::instance().evaluationDate();

  Real totalDuration = 0.0;
  Real totalNPV = 0.0;

  Rate y = 0.0;
  if (type == Duration::Macaulay || type == Duration::Modified) {
    y = Cashflows::irr(cashflows, marketPrice, rate.dayCounter(),
                       rate.compounding(), rate.frequency(), settlementDate);
    cout << "Y is " << y << " " << rate.frequency() << endl;
  }

  for (Size i = 0; i < cashflows.size(); i++) {
    Time t = rate.dayCounter().yearFraction(settlementDate,
                                            cashflows[i]->date());
    Real c = cashflows[i]->amount();
    DiscountFactor discount;
    if (type == Duration::Macaulay)
      discount = std::exp(-y*t);
    else
      discount = rate.discountFactor(t);

    totalNPV += c * discount;
    totalDuration += t * c * discount;
  }
  //totalNPV -= marketPrice;      // edd 28-Jan-2006: this is probably in error

  if (totalNPV == 0.0)
    // what to do?
    return 0.0;

  switch (type){
  case Duration::Modified:
    //return totalDuration / totalNPV / y;  // edd 28-Jan-2006: error?
    return totalDuration / totalNPV / (1 + y/rate.frequency());
  case Duration::Simple:
  case Duration::Macaulay:
    return totalDuration/totalNPV;
  default:
    QL_FAIL("unknown duration type");
  }
}


int main(void) {

  Calendar calendar = TARGET();
  Date today = calendar.adjust(Date::todaysDate());
  Settings::instance().evaluationDate() = today;

  Real redemption = 100.0; // bond pay $100 face at term
  Integer issueMonth = -12; // bond issued 12 months ago
  Integer bondTerm =  5; // bond has a 5yr term-to-maturity
  Real bondCoupon = 0.05;        // bond has 5% coupon
  Frequency bondCoupFreq = Semiannual; // bond pays coupon annually
  DayCounter bondDayCount = Thirty360();
  Compounding compounding = Compounded;
  BusinessDayConvention convention = ModifiedFollowing;
  Integer settlementDays = 3;

  Rate currentYield = 0.0475;

  Date dated = calendar.advance(today, issueMonth, Months);
  Date issue = dated;
  Date maturity = calendar.advance(issue, bondTerm, Years);

  boost::shared_ptr<YieldTermStructure>
    flatRate(new FlatForward(dated, currentYield, bondDayCount,
                             compounding, bondCoupFreq));

  FixedCouponBond bond(issue, dated, maturity, settlementDays,
                       std::vector<Rate>(1, bondCoupon),
                       bondCoupFreq, bondDayCount,
                       calendar, convention, redemption,
                       Handle<YieldTermStructure>(flatRate));

  Real cleanPrice = bond.cleanPrice(currentYield, compounding);
  Real dirtyPrice = bond.dirtyPrice(currentYield, compounding);
  Rate calculated = bond.yield(cleanPrice, compounding);
  Real accrued = bond.accruedAmount();

  cout << "Dirty Price: " << dirtyPrice << endl;
  cout << "Clean Price: " << cleanPrice << endl;
  cout << "Yield      : " << calculated << endl;
  cout << "Accrued    : " << accrued << endl;

  // need to explicitly push redemption cash flow into cf vector -- why ?
  std::vector<boost::shared_ptr<CashFlow> > cfVec(bond.cashflows());
  cfVec.push_back(bond.redemption());

  Real irr = Cashflows::irr(cfVec, cleanPrice,
                            bondDayCount, compounding, bondCoupFreq, issue);
  cout << "Irr        : " << irr << endl;

  InterestRate rate(calculated, bondDayCount, compounding, bondCoupFreq);
  cout << "Rate       : " << rate << endl;
  //  Time duration = Cashflows::duration(cfVec, cleanPrice, rate,
  // Duration::Modified, dated);
  Time macDuration = myDuration(cfVec, cleanPrice, rate,
                                Duration::Macaulay, dated);
  Time modDuration = myDuration(cfVec, cleanPrice, rate,
                                Duration::Modified, dated);
  Time simDuration = myDuration(cfVec, cleanPrice, rate,
                                Duration::Simple, dated);
  cout << "MacDuration: " << macDuration << endl;
  cout << "ModDuration: " << modDuration << endl;
  cout << "SimDuration: " << simDuration << endl;

  for (unsigned int i=0; i<cfVec.size(); i++) {
    cout << "Date " << i << " :\t" << cfVec[i]->date() << "\t"
         << "Amount: " << cfVec[i]->amount() << endl;
  }

}



--
Hell, there are no rules here - we're trying to accomplish something.
                                                  -- Thomas A. Edison