Skip to content

Add Perpetual Bond product type and associated yield and risk measures (#7) - #427

Merged
domokane merged 1 commit into
domokane:masterfrom
huuan26:feat/perpetual-bond-7
Oct 30, 2026
Merged

domokane merged 1 commit into
domokane:masterfrom
huuan26:feat/perpetual-bond-7

Conversation

@huuan26

@huuan26 huuan26 commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #7.

Implements the BondPerpetual product type (financepy/products/bonds/bond_perpetual.py) for undated debt securities / Consols with regular coupon payments and no maturity date ($T = \infty$), along with associated analytical pricing, yield solving, and risk measures as requested in Issue #7.


Mathematical Formulation & Dynamics

1. Dirty and Clean Pricing from Yield to Maturity

For a perpetual bond paying an annualized coupon $c$ at compounding frequency $f$ (with periodic coupon $c/f$ per 100 par) and annualized yield to maturity $y > 0$:

At settlement date $t_s$, let $\alpha \in [0, 1]$ be the remaining fraction of the period until the next coupon date (NCD), where $\alpha = 1 - \text{elapsed fraction}$. The remaining cash flows constitute an infinite coupon annuity discounted from NCD:

$$\sum_{k=1}^\infty \frac{c/f}{(1 + y/f)^k} = \frac{c/f}{y/f} = \frac{c}{y}$$

Including the coupon payable at NCD (conditioned on indicator $I_{\text{pay}} \in {0, 1}$, where $I_{\text{pay}} = 1$ if $t_s \le \text{ex-dividend date}$ and $0$ otherwise):

$$\text{Cash}_{\text{NCD}} = \frac{c}{f} I_{\text{pay}} + \frac{c}{y}$$

Discounting back to settlement date:

  • UK DMO & US Street Conventions (compound fractional discounting):
    $$P_{\text{dirty}} = \left(\frac{1}{1 + y/f}\right)^\alpha \left[\frac{c}{f} I_{\text{pay}} + \frac{c}{y}\right] \times \text{Par}$$
  • US Treasury Convention (simple interest fractional discounting):
    $$P_{\text{dirty}} = \frac{1}{1 + \alpha (y/f)} \left[\frac{c}{f} I_{\text{pay}} + \frac{c}{y}\right] \times \text{Par}$$

On any coupon payment date ($\alpha = 1$, accrued interest $= 0$):
$$P_{\text{dirty}} = P_{\text{clean}} = \frac{c}{y} \times \text{Par}$$
which recovers the classic Consol par formula $P = 100$ when $y = c$.

2. Yield to Maturity Inversion

Given clean price $P_{\text{clean}}$, dirty price is $P_{\text{dirty}} = P_{\text{clean}} + \text{AccruedInterest}$. The yield to perpetuity is solved via Brent's method:
$$f(y) = P_{\text{dirty}}(y) - P_{\text{target}} = 0$$
over $y \in (0, 2.0]$.

3. Duration & Convexity Risk Measures

  • DV01 & Dollar Duration:
    $$\text{DV01} = P_{\text{dirty}}(y) - P_{\text{dirty}}(y + 1\text{ bp})$$
    $$\text{DollarDuration} = 10{,}000 \times \text{DV01}$$
  • Modified Duration:
    $$D_{\text{mod}} = \frac{\text{DollarDuration}}{P_{\text{dirty}}} \approx \frac{1}{y}$$
  • Macaulay Duration:
    $$D_{\text{mac}} = D_{\text{mod}} \left(1 + \frac{y}{f}\right) \approx \frac{1 + y/f}{y}$$
  • Convexity:
    $$C = \frac{1}{P_{\text{dirty}}} \frac{d^2 P_{\text{dirty}}}{dy^2} \approx \frac{P(y - \Delta y) + P(y + \Delta y) - 2P(y)}{(\Delta y)^2 P(y)}$$

4. Valuation from Arbitrary Discount Curves

For any DiscountCurve, future coupons are discounted period-by-period up to horizon $N = 200$ (100 years for semi-annual), combined with an analytical terminal perpetuity tail evaluated at the curve's terminal forward/zero rate $r_\infty$:
$$\text{TailPV} = \text{DF}(t_N) \times \frac{c}{y_\infty}$$


Changes

  1. financepy/products/bonds/bond_perpetual.py:
    • Implements BondPerpetual(Product) class.
    • Methods: dirty_price_from_ytm, clean_price_from_ytm, yield_to_maturity, current_yield, accrued_interest, dv01, dollar_duration, modified_duration, macaulay_duration, convexity_from_ytm, dirty_price_from_discount_curve, clean_price_from_discount_curve, value, __repr__, _print.
    • Vectorization support for arrays of yields and clean prices.
  2. financepy/products/bonds/__init__.py:
    • Added from .bond_perpetual import *.
  3. tests/unit/test_FinBondPerpetual.py:
    • Added unit test suite covering par pricing, premium/discount pricing, between-coupon settlement, YTM inversion, analytical duration and convexity verification, current yield, discount curve valuation, vectorization, and input validation.

Verification

  • pytest tests/unit/test_FinBondPerpetual.py: 8 passed in 1.98s.
  • pytest tests/unit/test_FinBond.py tests/unit/test_FinBondPerpetual.py: 20 passed in 2.45s.
  • ruff check financepy/products/bonds/bond_perpetual.py: All checks passed.
  • git diff --check: Zero whitespace or formatting warnings.

domokane#7)

- Implement BondPerpetual class in financepy/products/bonds/bond_perpetual.py:
  - Clean and dirty pricing from yield to maturity (UK_DMO, US_STREET, US_TREASURY conventions) using infinite geometric annuity valuation.
  - Robust yield to maturity inversion via Brent's method root-finding.
  - Current yield calculation.
  - Accrued interest with ex-dividend and day-count convention support.
  - Analytical and finite-difference risk measures: DV01, Dollar Duration, Modified Duration, Macaulay Duration, and Convexity.
  - Discount curve pricing (clean_price_from_discount_curve, dirty_price_from_discount_curve, value) with terminal perpetuity tail adjustment.
- Export BondPerpetual in financepy/products/bonds/__init__.py.
- Add comprehensive test suite in tests/unit/test_FinBondPerpetual.py covering par/premium/discount pricing, duration and convexity, current yield, discount curve valuation, vectorization, and input validation.

Signed-off-by: huuan26 <phanbuihuuan261103@gmail.com>
@domokane
domokane merged commit 8cce719 into domokane:master Oct 30, 2026
2 checks passed
@domokane

Copy link
Copy Markdown
Owner

Thanks.

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

Successfully merging this pull request may close these issues.

Perpetual Bond

2 participants