Why Yield and Flat-Curve Bond Prices Can Differ in QuantLib
Summary
This explanation addresses why a fixed-rate bond’s dirty price from a yield-based calculation can differ slightly from the price produced by a flat yield term structure in QuantLib. Both methods discount coupon cash flows, but they can calculate the time to the first coupon differently. Under Actual/Actual Bond, the yield-based bond calculation uses the coupon schedule’s reference period; a generic term structure discount call may lack that schedule context and use the payment interval itself as the reference period.
The document demonstrates that this changes the first coupon’s discount factor and reconciles the resulting price difference. Regular later coupon periods match, so they do not cause the discrepancy in the example. The issue applies to day-count conventions that require reference periods; a convention such as Actual/360 does not have the same mismatch. It also describes using a schedule-aware Actual/Actual day counter for both the bond and curve so that they use consistent reference periods.
Key ideas
- Yield-based bond pricing can use coupon schedule information that a term structure discount call does not receive.
- Under Actual/Actual Bond, a different reference period for the first coupon changes its discount factor.
- Regular coupon periods in the example use matching accrual times, isolating the discrepancy to the first coupon.
- Day-count conventions without reference-period requirements can avoid this particular mismatch.
- A schedule-aware day counter used consistently for the bond and curve can align their calculations.
Tags
Full text
# Pricing a FixedRateBond in Quantlib: yield vs TermStructure
# Pricing a FixedRateBond in Quantlib: yield vs TermStructure
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;
}
```
}
## Answer by Luigi Ballabio (score 14, accepted)
https://quant.stackexchange.com/a/12715
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.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.