QuantLib icon indicating copy to clipboard operation
QuantLib copied to clipboard

Bloomberg bond clean price and accrued amount differs from Quantlib

Open tsutsl opened this issue 3 years ago • 7 comments

I'm getting the different bond clean price from Bloomberg and from QL but surprisingly Bloomberg price matches with excel price() function

I have the following bond : GETC21117030. The parameters are given below:

Parameter Name Value
Settlment Date 30-12-20
Bond Issue Date 17-Jan-19
Interest Acrual date 17-Jan-19
Maturity Date 17-Jan-21
Last Coupon Date 17-Jul-20
Coupon Rate 7.25%
Coupon Frequency 2
Day Count ACT/ACT
Redemption 100
Yield 7.95%
Calendar NullCalendar
Convention Unadjusted
TermPayConv Unadjusted
GenRule Backward

if you look at the parameters you'll notice that there is only one payment left on maturity date. If I calculate price and accrued interest in QLXL I'm getting different results from Bloomberg. But surprisingly Bloomberg numbers match if I use excel native price calculation function.

I looked into the excel formula and it looks that the price calculate formula is different when there is only one payment left (see below)

When N > 1 (N is the number of coupons payable between the settlement date and redemption date), PRICE is calculated as follows:

enter image description here

When N = 1 (N is the number of coupons payable between the settlement date and redemption date), PRICE is calculated as follows:

enter image description here

DSR = number of days from settlement to next coupon date.

E = number of days in coupon period in which the settlement date falls.

A = number of days from beginning of coupon period to settlement date.

It seems that for the last payment calculation excel is moving from compounded yield to simple yield.

I calculated the clean price both with compounded yield and with simple yield. While the clean price with simple yield is close to the Bloomberg/Excel price it still does not match.

With 7.95% yield and settlement date 30 Dec 2020:

Bloomberg/Excel clean price is 99.953226, Accrued Amount 3.270380

QLXL(semiannually compounded yield) clean price is 99.95983172, Accrued Amount 3.288251366

QLXL(simple yield) clean price is 99.95278714, Accrued Amount 3.288251366

I'm not C++ specialist but the QL code I've checked does not change the price calculation algorithm when only one payment is left.

The question is: Is it possible with current implementation of QuantLib to match the price and accrued amount for the above mentioned bond (and in general with all coupon bonds when there is only one cashflow is left)?

tsutsl avatar Dec 30 '20 13:12 tsutsl

Thanks for posting! It might take a while before we look at your issue, so don't worry if there seems to be no feedback. We'll get to it.

boring-cyborg[bot] avatar Dec 30 '20 13:12 boring-cyborg[bot]

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Apr 18 '21 23:04 stale[bot]

Never used QLXL but I encountered similar issue recently (on python). Seems related to a difference between the calculated coupon amount compared to the actual bond. There is related discussion in this quant stackexchange question but I think none of the suggestions solves the issue completely.

For most bonds, the coupon amounts are just a fraction (1/half/quarter) of the coupon rate but there is difficulty setting that up on QuantLib. Currently for FixedRateBond looks like the day count and business day convention for calculating the coupon amounts are tied to same arguments for generating the coupon payment dates. Perhaps we can have another constructor for separately specifying those.

Using your situation as example (expect 3.625 coupon payments but not the case):

bond = ql.FixedRateBond(3, ql.NullCalendar(), 100.0, ql.Date(17,1,2019), ql.Date(17,1,2021), ql.Period('6M'), [0.0725], ql.ActualActual())

print('Cashflows:')
for c in bond.cashflows():
    print('%20s %12f' % (c.date(), c.amount()))

print('Accrual: %.9f' % bond.accruedAmount(ql.Date(30,12,2020)))

Result:

Cashflows:
     July 17th, 2019     3.595205
  January 17th, 2020     3.653926
     July 17th, 2020     3.605191
  January 17th, 2021     3.645677
  January 17th, 2021   100.000000
Accrual: 3.288251366

bensonluk avatar Jun 03 '21 08:06 bensonluk

There are a bunch of different act/act conventions, and for historical reasons, unfortunately, the default you get when you write ql.ActualActual() is not the one you expect. This gives you exact half-coupons:

bond = ql.FixedRateBond(3, ql.NullCalendar(), 100.0, ql.Date(17,1,2019), ql.Date(17,1,2021),
                        ql.Period('6M'), [0.0725], ql.ActualActual(ql.ActualActual.Bond))

print('Cashflows:')
for c in bond.cashflows():
    print('%20s %12f' % (c.date(), c.amount()))

print('Accrual: %.9f' % bond.accruedAmount(ql.Date(30,12,2020)))

Results:

Cashflows:
     July 17th, 2019     3.625000
  January 17th, 2020     3.625000
     July 17th, 2020     3.625000
  January 17th, 2021     3.625000
  January 17th, 2021   100.000000
Accrual: 3.270380435

lballabio avatar Jun 03 '21 09:06 lballabio

Thanks lballabio, solves the problem for me though in that case I am not sure about OP's.

bensonluk avatar Jun 03 '21 10:06 bensonluk

Hopefully — I'm getting the expected numbers quoted by the OP with:

import QuantLib as ql

bond = ql.FixedRateBond(3, ql.NullCalendar(), 100.0, ql.Date(17,1,2019), ql.Date(17,1,2021),
                        ql.Period('6M'), [0.0725], ql.ActualActual(ql.ActualActual.Bond))

ql.Settings.instance().evaluationDate = ql.Date(27,12,2020)  # trade date

print(bond.settlementDate())  # check that the settlement is on the 30th
print(bond.accruedAmount())
print(bond.cleanPrice(0.0795, ql.ActualActual(ql.ActualActual.Bond), ql.Simple, ql.Semiannual))

Outputs:

December 30th, 2020
3.270380434782605
99.9532255971963

lballabio avatar Jun 03 '21 11:06 lballabio

for bonds where the compounding changes from Compounded to Simple if the settlement date falls within the last coupon period, is it up to the QuantLib user to manually handle this change in compounding?

A different issue #256 gave the suggestion of checking if ql.BondFunctions.nextCashFlowDate(fixedRateBond, QL_settle_date) == ql.BondFunctions.maturityDate(fixedRateBond):. This seems quite cumbersome. It's also rather inefficient because nextCashFlowDate loops over most if not all the cashflows.

this compounding method is used for all AUD bonds.

klin333 avatar Feb 09 '22 08:02 klin333