Matching QuantLib Bond Analytics to Bloomberg Conventions
Summary
The post investigates differences between QuantLib and Bloomberg YAS bond analytics for a fixed-rate bond. Although the author’s cash flows and clean price align, the calculated yield, duration, convexity, and Z-spread differ. The accepted answer identifies date handling as the key issue: Bloomberg does not adjust payment dates when calculating net present value, so the QuantLib bond must use unadjusted dates to reproduce that convention. Matching the clean price also requires using dirty price and subtracting accrued interest rounded to Bloomberg’s convention.
After applying the unadjusted-date setting, the reported yield, clean price, accrued amount, and duration measures align closely with Bloomberg. Convexity and Z-spread still show small differences in the reported output, so the match is not exact across every metric. Another answer highlights the settlement date and gives an independent calculation using a different library, which supports the settlement convention values. The example is specific to the bond setup and vendor conventions shown; it does not establish that one set of conventions applies to every bond or Bloomberg calculation.
Key ideas
- Bond analytics can differ across libraries because date and settlement conventions affect cash-flow valuation.
- The accepted answer says Bloomberg leaves payment dates unadjusted when calculating present value.
- Using unadjusted dates in QuantLib brings the reported yield and duration measures close to Bloomberg’s values.
- Accrued interest rounding can affect the clean price comparison.
- Small differences in convexity and Z-spread remain in the reported results.
Tags
Full text
# Difference in YTM and calculated risk metrics between QuantLib and Bloomberg for Fixed Coupon Bonds
# Difference in YTM and calculated risk metrics between QuantLib and Bloomberg for Fixed Coupon Bonds
I'm trying to get the yield for a given bond as well as other metrics to align with the YAS page on Bloomberg, but they are all sufficiently off to be slightly concerning, i.e. somewhere in the 4th decimal for the yields. Could anyone let me know what I'm doing wrong? I've been trying to change a couple of variables, but to no avail.
```
import QuantLib as ql
import math
# ISIN: USQ6535DBH63
# Set analysis date
ql.Settings.instance().evaluationDate = ql.Date(12, 5, 2025)
# Curve construction
start = ql.Date(12, 5, 2025) #DMY convention
maturity = ql.Date(12, 1, 2033)
ql.Settings.instance().evaluationDate = start
calendar = ql.UnitedStates(ql.UnitedStates.Settlement)
adjustment = ql.Unadjusted
spotTenors = ['1M', '2M', '3M', '4M', '5M', '6M', '7M', '8M', '9M', '10M', '11M', '12M', '18M', '2Y', '3Y', '4Y', '5Y', '6Y', '7Y', '8Y', '9Y', '10Y', '12Y', '15Y', '20Y', '25Y', '30Y', '40Y', '50Y']
spotDates = [ calendar.advance(start, ql.Period(t), adjustment, False) for t in spotTenors ]
spotRates = [x/100 for x in [4.32725, 4.33165, 4.32528, 4.31555, 4.28903, 4.2589, 4.22775, 4.19155, 4.15291, 4.1262, 4.09522, 4.06595, 3.87905, 3.796, 3.7357, 3.73275, 3.7562, 3.7965, 3.83885, 3.87945, 3.9181, 3.95625, 4.02735, 4.10522, 4.1564, 4.1271, 4.0664, 3.9158, 3.7661]]
yts = ql.RelinkableYieldTermStructureHandle()
index = ql.OvernightIndex("USD Overnight Index", 0, ql.USDCurrency(), ql.UnitedStates(ql.UnitedStates.Settlement), ql.Actual360(), yts)
rateHelpers = []
for tenor, rate in zip(spotTenors, spotRates):
helper = ql.OISRateHelper(2, ql.Period(tenor), ql.QuoteHandle(ql.SimpleQuote(rate)), index)
rateHelpers.append(helper)
swapCurve = ql.PiecewiseFlatForward(start, rateHelpers, ql.Actual360())
## Bond construction
issueDate = ql.Date(12, 1, 2023)
maturityDate = ql.Date(12, 1, 2033)
cpnFreq = ql.Period(ql.Semiannual)
calendar = ql.UnitedStates(ql.UnitedStates.Settlement)
bdConvention = ql.Unadjusted # Testing
dateGeneration = ql.DateGeneration.Backward
monthEnd = False
# Coupon details
schedule = ql.Schedule(issueDate, maturityDate, cpnFreq, calendar, bdConvention, bdConvention, dateGeneration, monthEnd)
dayCount = ql.Thirty360(ql.Thirty360.ISMA)
couponRate = 0.06429
settlementDays = 1
faceValue = 100
fixedRateBond = ql.FixedRateBond(settlementDays, faceValue, schedule, [couponRate], dayCount)
# Set Pricing Engine
spotCurveHandle = ql.YieldTermStructureHandle(swapCurve)
bondEngine = ql.DiscountingBondEngine(spotCurveHandle)
fixedRateBond.setPricingEngine(bondEngine)
# Print cashflows
print("\n=== Bond Cashflows ===")
print(f"{'Start Date': <12} {'End Date': <12} {'Pay Date': <12} {'Accural Days': >10} {'Amount': >10}")
for cf in fixedRateBond.cashflows():
coupon = ql.as_coupon(cf)
if hasattr(coupon, 'accrualStartDate'):
print(f"{coupon.accrualStartDate().ISO():12} {coupon.accrualEndDate().ISO():12} {coupon.date().ISO():12} {coupon.accrualDays():10} {coupon.amount():10.4f}")
else:
print(f"{'':12} {'':12} {cf.date().ISO():12} {'':10} {cf.amount():10.2f}")
settlementDate = start
cpnCompound = ql.CompoundedThenSimple
cpnCompoundFreq = ql.Semiannual
inputPx = 104.1735
yld = fixedRateBond.bondYield(inputPx, dayCount, cpnCompound, cpnCompoundFreq)
accruedAmt = ql.BondFunctions.accruedAmount(fixedRateBond)
cleanPx = ql.BondFunctions.cleanPrice(fixedRateBond, yld, dayCount, cpnCompound, cpnCompoundFreq)
macDur = ql.BondFunctions.duration(fixedRateBond, yld, dayCount, cpnCompound, cpnCompoundFreq, ql.Duration.Simple)
modDur = ql.BondFunctions.duration(fixedRateBond, yld, dayCount, cpnCompound, cpnCompoundFreq, ql.Duration.Modified)
convexity = ql.BondFunctions.convexity(fixedRateBond, yld, dayCount, cpnCompound, cpnCompoundFreq)
zSpread = ql.BondFunctions.zSpread(fixedRateBond, cleanPx, swapCurve, dayCount, cpnCompound, cpnCompoundFreq)
# Alt Calculations
metricsCompounding = ql.Compounded
metricsCompoundingFreq = ql.Semiannual
metricsDayCount = ql.Thirty360(ql.Thirty360.ISMA)
altYld = ql.BondFunctions.bondYield(fixedRateBond, inputPx, metricsDayCount, metricsCompounding, metricsCompoundingFreq)
rate = ql.InterestRate(altYld, metricsDayCount, metricsCompounding, metricsCompoundingFreq)
altCleanPx = ql.BondFunctions.cleanPrice(fixedRateBond, rate)
altDirtyPx = altCleanPx + accruedAmt
altMacDur = ql.BondFunctions.duration(fixedRateBond, rate, ql.Duration.Simple)
altModDur = ql.BondFunctions.duration(fixedRateBond, rate, ql.Duration.Modified)
altConvexity = ql.BondFunctions.convexity(fixedRateBond, rate)
altZSpread = ql.BondFunctions.zSpread(fixedRateBond, altCleanPx, swapCurve, metricsDayCount, metricsCompounding, metricsCompoundingFreq)
print(f"""With clean price of {inputPx:.4f} and settlement date of {fixedRateBond.settlementDate()} as input:
Clean Price: {cleanPx:,.4f}
Accrued Amount: {accruedAmt:,.4f}
Yield: {yld:.6%}
Macaulay Duration: {macDur:,.2f}
Modified Duration: {modDur:,.2f}
Convexity: {convexity:,.2f}
Z Spread: {zSpread*10000:,.0f} bps
===== Alternative Calculations =====
Yield: {altYld:.6%}
Clean Price: {altCleanPx:,.4f}
Dirty Price: {altDirtyPx:,.4f}
Mac Duration: {altMacDur:,.3f}
Mod Duration: {altModDur:,.3f}
Convexity: {altConvexity:,.3f}
Z Spread: {altZSpread*10000:,.0f}
""")
```
Here is the output:
```
=== Bond Cashflows ===
Start Date End Date Pay Date Accural Days Amount
2023-01-12 2023-07-12 2023-07-12 180 3.2145
2023-07-12 2024-01-12 2024-01-12 180 3.2145
2024-01-12 2024-07-12 2024-07-12 180 3.2145
2024-07-12 2025-01-12 2025-01-13 180 3.2145
2025-01-12 2025-07-12 2025-07-14 180 3.2145
2025-07-12 2026-01-12 2026-01-12 180 3.2145
2026-01-12 2026-07-12 2026-07-13 180 3.2145
2026-07-12 2027-01-12 2027-01-12 180 3.2145
2027-01-12 2027-07-12 2027-07-12 180 3.2145
2027-07-12 2028-01-12 2028-01-12 180 3.2145
2028-01-12 2028-07-12 2028-07-12 180 3.2145
2028-07-12 2029-01-12 2029-01-12 180 3.2145
2029-01-12 2029-07-12 2029-07-12 180 3.2145
2029-07-12 2030-01-12 2030-01-14 180 3.2145
2030-01-12 2030-07-12 2030-07-12 180 3.2145
2030-07-12 2031-01-12 2031-01-13 180 3.2145
2031-01-12 2031-07-12 2031-07-14 180 3.2145
2031-07-12 2032-01-12 2032-01-12 180 3.2145
2032-01-12 2032-07-12 2032-07-12 180 3.2145
2032-07-12 2033-01-12 2033-01-12 180 3.2145
2033-01-12 100.00
With clean price of 104.1735 and settlement date of May 13th, 2025 as input:
Clean Price: 104.1735
Accrued Amount: 2.1609
Yield: 5.746059%
Macaulay Duration: 6.15
Modified Duration: 4.38
Convexity: 43.23
Z Spread: 250 bps
===== Alternative Calculations =====
Yield: 5.745913%
Clean Price: 104.1735
Dirty Price: 106.3344
Mac Duration: 6.082
Mod Duration: 5.912
Convexity: 43.453
Z Spread: 183
```
The cashflows are correct. For the given input price, the yields, duration, convexity and Z-spreads are sufficiently off that it makes me think it is not just an issue with floating-point arithmetic.
I am unable to access image hosting websites, so here are the values (all in BBG convention):
```
YAS
Trade: 05/12/25
Settle: 05/13/25 (T+1)
Price: 104.1735
Yield: 5.746443 (Maturity)
Z-Spread: 186.4
Duration: 6.081
Mod Dur: 5.911
Convexity: 0.434
Invoice
Face: 1,000 M
Principal: 1,041,735.00
Accrued (121 Days): 21,608.58
Total (USD): 1,063,343.58
```
## Answer by user83507 (score 2, accepted)
https://quant.stackexchange.com/a/82524
EDIT: I think I found the answer from here: Quantlib match clean price with bbg clean price
> BBG does not adjust dates when calculating NPV. To get the same result in QuantLib we need to add ql.Unadjusted to FixedRateBond parameter. Also to get exactly the same clean price we need to calculate dirty price and subtract rounded accrued amount.
With the addition of ql.Unadjusted to the FixedRateBond() method, I get:
```
Yield: 5.746443%
Clean Price: 104.1735
Dirty Price: 106.3344
Mac Duration: 6.081
Mod Duration: 5.911
Convexity: 43.448
Z Spread: 184
```
Which is pretty much exactly what I need!
Original answer: Original author here responding to Attack68 (SE isn't linking my new account to the question for some reason, and I'm unable to comment directly.)
The QuantLib library seems to be able to interpret that the trade date (which is when the curve is priced) is 12th May, and the calculations assumes that the bond settle T+1, which is 13th May. Bloomberg is using the same settlement cycle, so I'm not sure how to fix that. (I've since realized that there was a line that said settlementDate = start, but it looks like it's not referenced further down in the code at all.)
FWIW I've changed it to 13th May (start) for completeness and settlement days to 0, but the yield calculated is the same as above (5.746059%) which makes sense to me, i.e. the curve is no longer shifted for settlement.
## Answer by Attack68 (score 1)
https://quant.stackexchange.com/a/82520
Are you sure its not your `settlement`? Which you set as `start` as 12th May not 13th May?
FWIW I ran these in my library and got the following:
```
from rateslib import * # rateslib 2.0, python 3.12
bond = FixedRateBond(
effective=dt(2023, 1, 12),
termination=dt(2033, 1, 12),
fixed_rate=6.429,
spec="us_corp"
)
bond.ytm(price=104.1735, settlement=dt(2025, 5, 13))
# 5.74644302
bond.accrued(settlement=dt(2025, 5, 13))
# 2.160858
bond.duration(ytm=5.746443021698663, settlement=dt(2025, 5, 13), metric="duration")
# 6.0811637
bond.duration(ytm=5.746443021698663, settlement=dt(2025, 5, 13), metric="modified")
# 5.91131843
bond.convexity(ytm=5.746443021698663, settlement=dt(2025, 5, 13)) * 100.0 / (104.1735 + 2.1608)
# 0.434483
```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.