Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BondYield: Difference from expected yield for bonds with clean_price equal to face_value near maturity #1958

Closed
alonwengierko opened this issue Apr 23, 2024 · 6 comments

Comments

@alonwengierko
Copy link

alonwengierko commented Apr 23, 2024

I am testing bondYield method for a FixedRateBond passing clean price. I am using clean price equal to face value (100), so I expect that yield to maturity would be equal to the coupon rate independently of the valuation_date. However, I see that as the valuation date approach maturity. I see that yield to maturity is smaller than expected.

For example, the following code, computes the yield to maturity for a bond with semiannual coupons. Coupon rate is 9.25%. So if clean price is equal to face value. I would expect that bondYield method, returns ytm equal to 9.25% independent of the valudation_date. But I see that values differ from expected values, and they differ more as valuation date approach maturity.

valuation_date = ql.Date(5, 1, 2015) -> ytm = 9.24%
valuation_date = ql.Date(5, 1, 2018) -> ytm = 9.16%

import QuantLib as ql

def compute_bond_schedule(bond_characteristics):
    # Unpack bond characteristics
    issueDate = bond_characteristics['issueDate']
    maturityDate = bond_characteristics['maturityDate']
    tenor = bond_characteristics.get('tenor', ql.Period(ql.Semiannual))
    calendar = bond_characteristics.get('calendar', ql.UnitedStates(ql.UnitedStates.GovernmentBond))
    business_convention = bond_characteristics.get('business_convention', ql.Following)
    date_generation = bond_characteristics.get('date_generation', ql.DateGeneration.Backward)
    month_end = bond_characteristics.get('month_end', False)
    
    # Compute Bond Schedule
    schedule = ql.Schedule (issueDate, maturityDate, tenor, calendar, business_convention, business_convention, date_generation, month_end)
    
    return schedule

def create_fixed_rate_bond(settlementDays, faceValue, schedule, couponRate, day_count):
    # Create Fixed Rate Bond
    fixedRateBond = ql.FixedRateBond(settlementDays, faceValue, schedule, [couponRate] * len(schedule), day_count)

    for c in fixedRateBond.cashflows():
        print(c.date(), c.amount())
    return fixedRateBond

def compute_ytm(fixedRateBond, clean_price, valuation_date):    
    # Compute YTM 
    yield_rate = fixedRateBond.bondYield(clean_price, fixedRateBond.dayCounter(), ql.Compounded, fixedRateBond.frequency(), valuation_date)
    
    return yield_rate

# Example usage
bond_characteristics = {
    'issueDate': ql.Date(20, 4, 2009),
    'maturityDate': ql.Date(20, 4, 2018), 
    'couponRate': 0.0925,
    'faceValue': 100,
    'settlementDays': 2,
    'day_count': ql.ActualActual(ql.ActualActual.Bond),
    'compounding': ql.Compounded,
    'freq': ql.Semiannual,
    'tenor': ql.Period(ql.Semiannual),
    'calendar': ql.UnitedStates(ql.UnitedStates.GovernmentBond),
    'business_convention': ql.Following,
    'date_generation': ql.DateGeneration.Backward,
    'month_end': False
}

clean_price = 100
valuation_date = ql.Date(5, 1, 2018)

schedule = compute_bond_schedule(bond_characteristics)
fixedRateBond = create_fixed_rate_bond(bond_characteristics['settlementDays'], bond_characteristics['faceValue'], schedule, bond_characteristics['couponRate'], bond_characteristics['day_count'])
ytm = compute_ytm(fixedRateBond, clean_price, valuation_date)
print("Yield to maturity:", round(ytm * 100,2), "%")
Copy link

boring-cyborg bot commented Apr 23, 2024

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.

@alonwengierko alonwengierko changed the title Error computing yield to maturity using clean_price near to maturity FixedRateBond bondYield result differ from expected value as valuation_date approach maturity Apr 24, 2024
@alonwengierko alonwengierko changed the title FixedRateBond bondYield result differ from expected value as valuation_date approach maturity FixedRateBond bondYield result differ from expected value clean_price = face_value Apr 24, 2024
@alonwengierko alonwengierko changed the title FixedRateBond bondYield result differ from expected value clean_price = face_value FixedRateBond bondYield result differ from expected value with clean_price equal to face_value Apr 24, 2024
@alonwengierko alonwengierko changed the title FixedRateBond bondYield result differ from expected value with clean_price equal to face_value BondYield: Difference from expected yield for bonds with clean_price equal to face_value near maturity Apr 24, 2024
@Anzhi1
Copy link

Anzhi1 commented May 21, 2024

Hi, I implemented your code in C++ and performed some debugging. I found that by default, QuantLib uses the bondYield() method to calculate YTM with the dirty price (full price). Therefore, the final calculation result is affected by the settlement date. When you use a date that does not include the accrued amount for the calculation, you can get a YTM equal to the coupon rate. Perhaps this design is to reflect the actual market value of the bond? I'm not sure why QuantLib is designed this way.

1716271413217

Copy link
Contributor

This issue was automatically marked as stale because it has been open 60 days with no activity. Remove stale label or comment, or this will be closed in two weeks.

@github-actions github-actions bot added the stale label Jul 21, 2024
@lballabio lballabio removed the stale label Jul 21, 2024
Copy link
Contributor

This issue was automatically marked as stale because it has been open 60 days with no activity. Remove stale label or comment, or this will be closed in two weeks.

@github-actions github-actions bot added the stale label Sep 20, 2024
@lballabio lballabio removed the stale label Sep 20, 2024
@lballabio
Copy link
Owner

Matching the dirty price to the NPV of the coupons is the same as matching the clean price to the NPV minus the accrual; the accrual is constant, doesn't depend on the yield.

It seems to be the same problem as #1168. I'm closing this one and adding a reference on that ticket.

@lballabio
Copy link
Owner

On second thought, it's a different issue. I think your expectation is off.

In your example, we're during the last semiannual coupon of the bond; let's say we're at time $t$ from the start of the last coupon, with the length of the full coupon being $T=0.5$. The remaining time to maturity is $T-t$. The coupon rate $C$ is 0.0925.

At maturity, we'll receive $100 \cdot C \cdot T$ (the coupon) plus $100$ (the redemption). The present value of the payments is $(100 + 100 \cdot C \cdot T) \cdot B(T-t)$ where $B$ is the discount factor, which depends on the yield. For a yield $y$ semiannually compounded, $B(T-t) = 1 / \left( 1 + y/2 \right)^{2(T-t)}$. The present value gives you the dirty price; to get the clean price, you need to subtract the accrued amount $100 \cdot C \cdot t$. You expect this to be 100; putting everything together,

$$ (100 + 100 \cdot C \cdot T) \cdot \frac{1}{\left( 1 + y/2 \right)^{2(T-t)}} - 100 \cdot C \cdot t = 100 $$

Reordering and solving for the yield $y$,

$$ \frac{1}{\left( 1 + y/2 \right)^{2(T-t)}} = \frac{100 + 100 \cdot C \cdot t}{100 + 100 \cdot C \cdot T} $$

$$ \left( 1 + y/2 \right)^{2(T-t)} = \frac{100 + 100 \cdot C \cdot T}{100 + 100 \cdot C \cdot t} $$

$$ 1 + y/2 = \left[ \frac{100 + 100 \cdot C \cdot T}{100 + 100 \cdot C \cdot t} \right]^{\frac{1}{2(T-t)}} $$

and finally

$$ y = 2 \cdot \left( \left[ \frac{100 + 100 \cdot C \cdot T}{100 + 100 \cdot C \cdot t} \right]^{\frac{1}{2(T-t)}} - 1 \right) $$

If you plot the function above, here is what you get:

plot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants