8
$\begingroup$

I am trying to price a simple U.S. treasury in QuantLib, using two methods. The first method calls FixedRatebond.dirtyPrice(...), passing in a YTM and other parameters.

The second method involves building a FlatForward YieldTermStructure with the same YTM and parameters as method #1.

The two methods give me slightly different dirty prices, and I expect them to be the same. Can anyone explain what I have done wrong, or, explain why the dirty price should be different using the two methods?

Here is some sample code (full disclosure: this code was plagiarized from here):

try {
    // date set up
    Calendar calendar = TARGET();

    Date settlementDate(28, January, 2011);
    // the settlement date must be a business day
    settlementDate = calendar.adjust(settlementDate);

    // Evaluation date
    Integer fixingDays = 1;
    Natural settlementDays = 1;
    Date todaysDate = calendar.advance(settlementDate, -fixingDays, Days);
    Settings::instance().evaluationDate() = todaysDate;

    // bond set up  
    Real faceAmount = 100.0;
    Real redemption = 100.0;
    Date issueDate(27, January, 2011);
    Date maturity(31, August, 2020);
    Real couponRate = 0.03625;
    Real yield = 0.034921;

    RelinkableHandle<YieldTermStructure> discountingTermStructure;
    boost::shared_ptr<YieldTermStructure> flatTermStructure(
    new FlatForward(
        settlementDate,
        yield,
        ActualActual(ActualActual::Bond),
        Compounding::Compounded,
        Semiannual));
    discountingTermStructure.linkTo(flatTermStructure);

    // Pricing engine
    boost::shared_ptr<PricingEngine> bondEngine(
        new DiscountingBondEngine(discountingTermStructure));

     // Rate
    Schedule fixedBondSchedule(
        issueDate,
        maturity,
        Period(Semiannual),
        UnitedStates(UnitedStates::GovernmentBond),
        BusinessDayConvention::Unadjusted,
        BusinessDayConvention::Unadjusted,
        DateGeneration::Rule::Backward,
        false);

    FixedRateBond fixedRateBond(
        settlementDays,
        faceAmount,
        fixedBondSchedule,
        std::vector<Rate>(1, couponRate),
        ActualActual(ActualActual::Bond),
        BusinessDayConvention::Unadjusted,
        redemption,
        issueDate);

    //Calculate pricing without term structure
    Real cp = fixedRateBond.cleanPrice(yield, fixedRateBond.dayCounter(), Compounding::Compounded, Semiannual);
    Real dp = fixedRateBond.dirtyPrice(yield, fixedRateBond.dayCounter(), Compounding::Compounded, Semiannual);
    Rate ytm = fixedRateBond.yield(cp, fixedRateBond.dayCounter(), Compounding::Compounded, Semiannual);
    Real accrued = fixedRateBond.accruedAmount();

    fixedRateBond.setPricingEngine(bondEngine);


    // write column headings
    Size widths[] = { 18, 15, 15};

    std::cout << std::setw(widths[0]) <<  "                 "
        << std::setw(widths[1]) << "Without TS"
        << std::setw(widths[2]) << "With TS"
        << std::endl;

    Size width = widths[0]
                    + widths[1]
                            + widths[2];
    std::string rule(width, '-'), dblrule(width, '=');

    std::cout << rule << std::endl;

    std::cout << std::setw(widths[0]) << "Clean Price"
        << std::setw(widths[1]) << std::setprecision (8) << cp
        << std::setw(widths[2]) << std::setprecision (8) << fixedRateBond.cleanPrice()
        << std::endl;

    std::cout << std::setw(widths[0]) << "Dirty Price"
        << std::setw(widths[1]) << std::setprecision (8) << dp
        << std::setw(widths[2]) << std::setprecision (8) << fixedRateBond.dirtyPrice()
        << std::endl;

    std::cout << std::setw(widths[0]) << "Accrued"
        << std::setw(widths[1]) << std::setprecision (8) << accrued
        << std::setw(widths[2]) << std::setprecision (8) << fixedRateBond.accruedAmount()
        << 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;
}

}

$\endgroup$

1 Answer 1

13
$\begingroup$

Day-count conventions. You can't live with them, you can't live without them.

The reason the prices differ is that the pricing engine can't calculate correctly the time over which the first coupon is discounted, and thus it gets slightly different discount factors to apply to the coupon amounts. Please sit down, it'll take some explaining.

Ultimately, both methods calculate the dirty price by adding the coupon amounts, each discounted according to its payment date. Where they differ is in the calculation of the discount factors.

The method that takes a yield and the corresponding conventions calculates discounts, as can be expected, by compounding the yield. The discount $D_1$ for the first coupon is obtained by accruing the yield over the remaining life of the coupon; the discount $D_2$ for the second coupon, by accruing it over its life and compounding it with the previous result; and so on until the last coupon. The problem is that, in order to calculate the accrual time correctly according to the passed act/act(b) day count convention, you also need a reference period which is given by the frequency of the coupons and in this case is 6 months. So, for instance, the accrual time for the first coupon (from Jan 28, 2011 to Feb 28, 2011) must be calculated as:

dayCounter.yearFraction(Date(28,January,2011), Date(28,February,2011),
                        Date(28,August,2010),  Date(28,February,2011));

which returns 0.08423913043478261 (the third and fourth dates are the start and end date of the 6-months reference period). If the coupons were annual, the result would be slightly different: the reference start date above would be Feb.28 2010, returning 0.08493150684931507. The overload of the dirtyPrice method that takes a yield uses coupon information to select the correct reference period and calculates the first discount factor according to the first value above. The result is 0.997087920498809.

Instead, the method that relies on the pricing engine uses the yield term structure t and just asks for t.discount(coupon.date()) for each of the coupons: the interface of the discount method doesn't allow to pass any extra info about the reference period. Internally, the best that the term structure can do is to calculate

dayCounter.yearFraction(Date(28,January,2011), Date(28,February,2011));

which, by default, takes as start and end of the reference period the same two dates that are passed, making the above equivalent to

dayCounter.yearFraction(Date(28,January,2011), Date(28,February,2011),
                        Date(28,January,2011), Date(28,February,2011));

this returns 0.08333333333333333, and the corresponding discount factor for the first coupon is 0.9971191880350325.

This difference accounts for the change in prices (the other coupons have no effect: they're all regular 6-months coupon, and for them the reference period equals the life of the coupon, making T equal to 0.5 in both cases). The price returned by dirtyPrice() is 101.08980828425344; the one returned by dirtyPrice(yield, ...) is 101.08663832294855; and if we correct for the different discount factors above, we get:

101.08980828425344 * 0.997087920498809 / 0.9971191880350325 = 101.08663832294862

that reconciles the prices.

As a final note: this only happens with those day-count conventions that require a reference period. If you try your code with one that doesn't (say, act/360) the two methods will give you the same price.

Update (September 2019):

It is now possible to create an instance of the ActualActual day counter that uses the correct reference periods for the calculation. Instantiate it as:

DayCounter dayCounter = ActualActual(ActualActual::Bond, fixedBondSchedule);

and use it for both the discount curve and the bond. When asked for year fractions, it will retrieve the correct reference period(s) from the schedule and use them. This will give the correct results.

$\endgroup$
5
  • 2
    $\begingroup$ Luigi, you are a gentleman and a scholar! Thankyou for this clear explanation. $\endgroup$
    – nickos556
    Jun 19, 2014 at 0:05
  • 2
    $\begingroup$ Thanks. I'm considering add "a gentleman and a scholar" on my profile :) $\endgroup$ Jun 19, 2014 at 7:03
  • $\begingroup$ You should! A big thanks for all your contributions to the open source community. $\endgroup$
    – nickos556
    Jun 19, 2014 at 23:19
  • $\begingroup$ Hi @LuigiBallabio, picking up where we left off 5+yrs ago! It seems that this is still an issue, can you suggest any workarounds? $\endgroup$
    – nickos556
    Sep 9, 2019 at 4:48
  • $\begingroup$ I updated the answer. $\endgroup$ Sep 19, 2019 at 8:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.