Skip to content
All library documents

Why Bond NPV and Dirty Price Differ in QuantLib

Article Quant Q&A · Author: ql.user2511

Summary

This discussion explains why a fixed rate bond’s net present value can differ from its dirty price in QuantLib. The key distinction is the discounting date: NPV uses the discount curve’s reference date, while dirty price is expressed at the bond’s settlement date. When those dates differ, the values can differ; a coupon paid between them can also contribute to NPV without being part of the settlement price.

The answer notes that the bond’s settlement date is calculated from the valuation date and settlement convention, rather than from the issue date as in the question’s setup. It also cautions that Actual/Actual ISMA needs coupon reference-period information, which can be supplied by constructing a schedule. The practical checks are to align the settlement and curve dates; when both values discount to the same date, they should agree. The accrued-interest figures in the example match within floating-point precision.

Key ideas

  • NPV and dirty price can differ because they discount cash flows to different dates.
  • A coupon paid before settlement may be included in NPV but excluded from the bond price.
  • Bond settlement should be determined from the valuation date and settlement convention.
  • Actual/Actual ISMA calculations require coupon reference-period information, which a schedule can provide.
  • Aligning the curve reference date and settlement date makes the two values comparable.

Tags

Full text
# Difference arising between Dirty Price and NPV using QuantLib Python


# Difference arising between Dirty Price and NPV using QuantLib Python












I have used QuantLib Python to price a fixed rate bond.

My codes are as follows:

```
import QuantLib as ql

valuationDate = ql.Date(30, 6, 2020)
ql.Settings.instance().evaluationDate = valuationDate
compounding = ql.Continuous
calendar = ql.UnitedStates()
coupon = 0.05
couponFrequency = ql.Annual
issueDate = ql.Date(7, 5, 2016)
maturityDate = ql.Date(7, 5, 2024)
settlementDays = 2
settlementDate = calendar.advance(issueDate, ql.Period(settlementDays, ql.Days))
dayCount = ql.ActualActual(ql.ActualActual.ISMA)
fixedRateBond = ql.FixedRateBond(settlementDays, calendar, 100.0, issueDate, maturityDate, ql.Period(couponFrequency), [coupon], dayCount, ql.Unadjusted, ql.Unadjusted)
curve = ql.FlatForward(valuationDate, ql.QuoteHandle(ql.SimpleQuote(0.02)), dayCount, compounding)
handle = ql.YieldTermStructureHandle(curve)
bondEngine = ql.DiscountingBondEngine(handle)
fixedRateBond.setPricingEngine(bondEngine)
bondYield = fixedRateBond.bondYield(dayCount, compounding, couponFrequency)
cleanPrice = fixedRateBond.cleanPrice()
dirtyPrice = fixedRateBond.dirtyPrice()
print('NPV:', fixedRateBond.NPV())
print('Bond Yield:', bondYield)
print('Clean Price:', cleanPrice)
print('Dirty Price:', dirtyPrice)
print('Accrued Interest:', fixedRateBond.accruedAmount())
print('Actual Accrued Amount', dirtyPrice - cleanPrice)
```

The results I get are as follows:

> NPV: 111.7127354483437

> Bond Yield: 0.01989558171322524

> Clean Price: 110.9578553230744

> Dirty Price: 111.72497861074564

> Accrued Interest: 0.767123287671234

> Actual Accrued Amount 0.7671232876712395

However, there are some discrepancies that I have noted:

- As far as I know, the NPV should be equal to the dirty price, however, this is not the case here.

- QuantLib's `accruedAmount` function is not equal to the `dirtyPrice` minus the `cleanPrice`.

Can someone explain where I went wrong please?

Thanks.

## Answer by Luigi Ballabio (score 3, accepted)

https://quant.stackexchange.com/a/66426

As @AKdemy said, the two accrued amounts are the same within the precision of floating-point math.

The `NPV` and `dirtyPrice` methods return two slightly different quantities: `NPV` discounts the cashflows to the reference date of the discount curve (in your case, the valuation date) while `dirtyPrice` discounts to the settlement date of the bond. This can also give rise to larger differences if a coupon is paid between the valuation date and the settlement date: in that case, the coupon amount will be included in the NPV but not in the price.

A note: the settlement date of the bond is not the one you defined as `settlementDate`. Instead, it's two days after the valuation date, not the issue date, so it should be

```
settlementDate = calendar.advance(valuationDate, ql.Period(settlementDays, ql.Days))
```

or, once you build the bond,

```
settlementDate = bond.settlementDate()
```

Another thing you might want to change: using act/act(ISMA) in a term structure is risky (see https://www.youtube.com/watch?v=dQjd3hAshj4). If you need to use it, what you should do is create first the schedule for the bond, so you can pass it to the day counter:

```
schedule = ql.Schedule(issueDate, maturityDate, ql.Period(couponFrequency), calendar, ql.Unadjusted, ql.Unadjusted, ql.DateGeneration.Forward, True)
dayCount = ql.ActualActual(ql.ActualActual.ISMA, schedule)
fixedRateBond = ql.FixedRateBond(settlementDays, 100.0, schedule, [coupon], dayCount)
```

This way, the day counter has the information about the reference periods of the coupons, which is required by the act/act calculation rules.

Once you do the two changes above, you can check the difference between `NPV` and `dirtyPrice`. If you set `settlementDays` to 0, the dirty price should become equal to the NPV, because in both cases the cashflows will be discounted to the valuation date; if, instead, you pass the `settlementDate` above to the curve, the NPV should become equal to the dirty price, because both will be discounted to the settlement date.

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.