invest.bond-yield Unreviewed
Current yield and yield to maturity of a fixed-coupon bond from its clean price, as Excel's YIELD defines it.
1.0.2 · published 2026-10-03 by charlie · Anterra
Pinned by 27 tests, run in TypeScript, Python and Rust.
Unreviewed. This capability’s implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified tax adviser has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
Not professional advice. This capability calculates investment figures from published rules. It is a software component for developers, not financial advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have a tax adviser review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
What it does
The current yield and the yield to maturity of a fixed-coupon bond that redeems at 100, from its clean price, with the accrued interest and dirty price they are worked from. It follows Excel's `YIELD(settlement, maturity, rate, pr, 100, frequency, basis)`: Microsoft, *YIELD function*, https://support.microsoft.com/en-us/office/yield-function-f5f5ca43-c4bd-434f-8bd2-ed3c9727a4fe (read 2026-09-23). Its example (settlement 15 Feb 2008, maturity 15 Nov 2016, 5.75%, price 95.04287, semi-annual, basis 0) is 6.5%, and is a vector here; the exact root is 0.0650000069.
## The calculation
For example
bond_yield(2008-02-15, 2016-11-15, 5.75%, 95.04287, 2, 30-360)→ yield basis points 6.5%, yield to maturity 0.065000007, current yield basis points 6.05%, accrued interest 1.437500, dirty price 96.480370, previous coupon 2007-11-15, next coupon… Microsoft's YIELD example: 5.75% to 2016 at 95.04287 on 30/360 is 6.5%bond_yield(2008-02-15, 2016-11-15, 5.75%, 95.04287, 2, act-act)→ yield basis points 6.5%, yield to maturity 0.065001821, current yield basis points 6.05%, accrued interest 1.453297, dirty price 96.496167, previous coupon 2007-11-15, next coupon… the same bond on actual/actual: 92 of 182 days accruedbond_yield(2025-09-23, 2027-12-07, 4.25%, 101.5, 2, act-act)→ yield basis points 3.53%, yield to maturity 0.035347152, current yield basis points 4.19%, accrued interest 1.254098, dirty price 102.754098, previous coupon 2025-06-07, next coup… a gilt-style 4.25% semi-annual on actual/actual
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
def bond_yield(settlement: str, maturity: str, coupon_basis_points: int, clean_price: str, frequency: int, basis: BondDayCount) -> BondYield
| settlement | date | the date the buyer pays and takes the bond; before maturity |
| maturity | date | the redemption date; coupon dates are stepped back from it |
| coupon_basis_points | int | the annual coupon rate: 575 = 5.75%; 0 to 100000 |
| clean_price | string | per 100 of face value, without accrued interest, at most 6 decimal places: "95.04287" |
| frequency | int | coupons a year: 1, 2 or 4 |
| basis | BondDayCount | 30-360 (Excel basis 0, bond basis) or act-act (Excel basis 1, as gilts) |
| returns | BondYield |
The types it declares, generated into your project
BondDayCount = Literal["30-360", "act-act"]
@dataclass(frozen=True)
class BondYield:
"""The yields, and the accrual they were worked from."""
#: yield to maturity, half away from zero: 650 = 6.50%
yield_basis_points: int
#: the annual yield to 9 decimal places, half away from zero: "0.065000007"
yield_to_maturity: str
#: annual coupon / clean price, half away from zero
current_yield_basis_points: int
#: per 100, 6 decimal places, half-up
accrued_interest: str
#: clean price plus accrued interest, per 100, 6 decimal places
dirty_price: str
#: the coupon date on or before settlement
previous_coupon: str
#: the first coupon date after settlement
next_coupon: str
#: coupons still to be paid, the one at maturity included
coupons_remaining: int
Your code names it in one line, in the file that uses it
from fune.invest.bond_yield import bond_yield # invest.bond-yield@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import re
from .dates_add_days import CivilDate, days_in_month, epoch_day_from_iso, format_iso_date, parse_iso_date ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_add_months import add_months ← from dates.add-months ^1.0.0 · built alongside by fune
from .dates_day_count_fraction import day_count_fraction ← from dates.day-count-fraction ^1.0.0 · built alongside by fune
from .invest_bond_yield_types import BondDayCount, BondYield
from .math_fractional_power import FIXED_SCALE, mul_fixed, pow_fixed, root_fixed ← from math.fractional-power ^1.0.0 · built alongside by fune
_PRICE = re.compile(r"[0-9]+(\.[0-9]{1,6})?")
_MICRO = 10 ** 6
def _round_half_away(n: int, d: int) -> int:
"""n / d rounded half away from zero; d > 0."""
magnitude = (abs(n) * 2 + d) // (2 * d)
return -magnitude if n < 0 else magnitude
def _decimal(value: int, places: int) -> str:
unit = 10 ** places
magnitude = abs(value)
return "%s%d.%0*d" % ("-" if value < 0 else "", magnitude // unit, places, magnitude % unit)
def _coupon_date(maturity: str, k: int, months: int, end_of_month: bool) -> str:
"""The coupon date k periods before maturity; month ends stay month ends."""
date = add_months(maturity, -k * months)
if not end_of_month:
return date
civil = parse_iso_date(date)
return format_iso_date(CivilDate(year=civil.year, month=civil.month, day=days_in_month(civil.year, civil.month)))
def bond_yield(settlement: str, maturity: str, coupon_basis_points: int, clean_price: str, frequency: int, basis: BondDayCount) -> BondYield:
"""Yield to maturity by Excel's YIELD: the annual yield y, compounded at
the coupon frequency, at which the discounted coupons and redemption at
100 equal the clean price plus accrued interest, with the first period's
exponent DSC/E. Solved by bisection on the per-day factor
z = (1 + y/f)^(-1/E) in 18-place fixed point, so every power is whole.
With one coupon period or less left, Excel's simple-yield formula, exactly."""
if isinstance(frequency, bool) or frequency not in (1, 2, 4):
raise ValueError("frequency must be 1, 2 or 4, received %r" % (frequency,))
c = coupon_basis_points
if isinstance(c, bool) or not isinstance(c, int) or c < 0 or c > 100000:
raise ValueError("couponBasisPoints must be a whole number from 0 to 100000, received %r" % (c,))
if not isinstance(clean_price, str) or not _PRICE.fullmatch(clean_price):
raise ValueError('cleanPrice must be a positive decimal with at most 6 places, received "%s"' % (clean_price,))
if basis not in ("30-360", "act-act"):
raise ValueError('basis must be 30-360 or act-act, received "%s"' % (basis,))
whole, _, fraction = clean_price.partition(".")
price_micro = int(whole) * _MICRO + int(fraction.ljust(6, "0"))
if price_micro == 0:
raise ValueError("cleanPrice must be greater than zero")
settle_day = epoch_day_from_iso(settlement)
if settle_day >= epoch_day_from_iso(maturity):
raise ValueError("settlement must be before maturity")
months = 12 // frequency
civil = parse_iso_date(maturity)
end_of_month = civil.day == days_in_month(civil.year, civil.month)
k = 1
while epoch_day_from_iso(_coupon_date(maturity, k, months, end_of_month)) > settle_day:
k += 1
if k > 100 * frequency:
raise ValueError("maturity must be within 100 years of settlement")
previous = _coupon_date(maturity, k, months, end_of_month)
nxt = _coupon_date(maturity, k - 1, months, end_of_month)
if basis == "30-360":
yf = day_count_fraction(previous, settlement, "30-360")
a = yf.numerator * 360 // yf.denominator
e = 360 // frequency
else:
a = settle_day - epoch_day_from_iso(previous)
e = epoch_day_from_iso(nxt) - epoch_day_from_iso(previous)
dsc = e - a
if dsc <= 0:
raise ValueError("by the 30/360 count settlement is not before the next coupon date")
# Everything is scaled by M = 100 * f * E * 10^6 so it is a whole number.
f = frequency
coupon_term = c * e * _MICRO # C * M, C = coupon / (100 f) per 100
redemption_term = 100 * 100 * f * e * _MICRO # 100 * M
target = price_micro * 100 * f * e + c * a * _MICRO # (clean + accrued) * M
if k == 1:
# ((100 + C) - dirty) / dirty * (f * E / DSC)
numerator = (redemption_term + coupon_term - target) * f * e
settled = _round_half_away(numerator * 10 ** 12, target * dsc)
else:
n = k
def value(z: int) -> int:
step = pow_fixed(z, e)
p = pow_fixed(z, dsc)
total = 0
for i in range(1, n + 1):
total += coupon_term * p
if i < n:
p = mul_fixed(p, step)
return total + redemption_term * p - target * FIXED_SCALE
lo, hi = 0, root_fixed(2 * FIXED_SCALE, e)
if value(hi) < 0:
raise ValueError("the price implies a yield below -50% a coupon period")
while hi - lo > 1:
mid = (lo + hi) // 2
if value(mid) >= 0:
hi = mid
else:
lo = mid
growth = pow_fixed(hi, e)
if growth == 0:
raise ValueError("the yield is too large to compute")
per_period = FIXED_SCALE * FIXED_SCALE // growth - FIXED_SCALE
settled = _round_half_away(per_period * f, 10 ** 6)
accrued_micro = _round_half_away(c * a * _MICRO, 100 * f * e)
return BondYield(
yield_basis_points=_round_half_away(settled * 10000, 10 ** 12),
yield_to_maturity=_decimal(_round_half_away(settled, 1000), 9),
current_yield_basis_points=_round_half_away(c * 100 * _MICRO, price_micro),
accrued_interest=_decimal(accrued_micro, 6),
dirty_price=_decimal(price_micro + accrued_micro, 6),
previous_coupon=previous,
next_coupon=nxt,
coupons_remaining=k,
)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 5 dependencies, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add invest.bond-yield
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./invest.bond-yield-1.0.2-python.fune, or fetch it from a terminal with fune pull invest.bond-yield@1.0.2:python.
The whole function, every language, is one file too: invest.bond-yield-1.0.2.fune, 39,297 bytes, sha256 ad1fe02add0c15d25868fa0f169af80e02fb2ccc08aa8b76f120da93c3bf7a43. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
# fune: before invest.bond-yield
after — your function gets the result and the arguments, and returns the final result.
# fune: after invest.bond-yield
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
# fune: replace dates.add-days in invest.bond-yield
# fune: replace dates.add-months in invest.bond-yield
# fune: replace dates.day-count-fraction in invest.bond-yield
# fune: replace math.big-integer in invest.bond-yield
# fune: replace math.fractional-power in invest.bond-yield
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show invest.bond-yield --steps.
# fune: step invest.bond-yield after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| Microsoft's YIELD example: 5.75% to 2016 at 95.04287 on 30/360 is 6.5% | 2008-02-15, 2016-11-15, 5.75%, 95.04287, 2, 30-360 | → | yield basis points 6.5%, yield to maturity 0.065000007, current yield basis points 6.05%, accrued interest 1.437500, dirty price 96.480370, previous coupon 2007-11-15, next coupon… |
| the same bond on actual/actual: 92 of 182 days accrued | 2008-02-15, 2016-11-15, 5.75%, 95.04287, 2, act-act | → | yield basis points 6.5%, yield to maturity 0.065001821, current yield basis points 6.05%, accrued interest 1.453297, dirty price 96.496167, previous coupon 2007-11-15, next coupon… |
| a gilt-style 4.25% semi-annual on actual/actual | 2025-09-23, 2027-12-07, 4.25%, 101.5, 2, act-act | → | yield basis points 3.53%, yield to maturity 0.035347152, current yield basis points 4.19%, accrued interest 1.254098, dirty price 102.754098, previous coupon 2025-06-07, next coup… |
| settling on a coupon date accrues nothing | 2025-12-07, 2027-12-07, 4.25%, 101.5, 2, act-act | → | yield basis points 3.47%, yield to maturity 0.034672155, current yield basis points 4.19%, accrued interest 0.000000, dirty price 101.500000, previous coupon 2025-12-07, next coup… |
| at par on a coupon date the yield is the coupon, exactly | 2026-06-15, 2036-06-15, 4.5%, 100, 2, 30-360 | → | yield basis points 4.5%, yield to maturity 0.045000000, current yield basis points 4.5%, accrued interest 0.000000, dirty price 100.000000, previous coupon 2026-06-15, next coupon… |
| inside the last period: Excel's simple-yield formula | 2027-09-01, 2027-12-07, 4.25%, 99.8, 2, act-act | → | yield basis points 4.96%, yield to maturity 0.049649871, current yield basis points 4.26%, accrued interest 0.998634, dirty price 100.798634, previous coupon 2027-06-07, next coup… |
| inside the last period on 30/360 | 2027-09-01, 2027-12-07, 4.25%, 99.8, 2, 30-360 | → | yield basis points 4.96%, yield to maturity 0.049607276, current yield basis points 4.26%, accrued interest 0.991667, dirty price 100.791667, previous coupon 2027-06-07, next coup… |
| annual coupons | 2026-03-10, 2031-06-15, 3%, 97.25, 1, 30-360 | → | yield basis points 3.58%, yield to maturity 0.035802953, current yield basis points 3.08%, accrued interest 2.208333, dirty price 99.458333, previous coupon 2025-06-15, next coupo… |
| quarterly coupons | 2026-01-20, 2029-04-15, 5%, 102.125, 4, act-act | → | yield basis points 4.29%, yield to maturity 0.042930500, current yield basis points 4.9%, accrued interest 0.069444, dirty price 102.194444, previous coupon 2026-01-15, next coupo… |
| a month-end maturity keeps coupons on month ends: 31 December, not 30th | 2026-01-15, 2030-06-30, 4%, 98, 2, act-act | → | yield basis points 4.5%, yield to maturity 0.044996644, current yield basis points 4.08%, accrued interest 0.165746, dirty price 98.165746, previous coupon 2025-12-31, next coupon… |
Show the other 17 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a zero-coupon bond: (100/90)^(1/5) - 1 | 2026-01-01, 2031-01-01, 0%, 90, 1, act-act | → | yield basis points 2.13%, yield to maturity 0.021295688, current yield basis points 0%, accrued interest 0.000000, dirty price 90.000000, previous coupon 2026-01-01, next coupon 2… |
| a price above every future payment is a negative yield | 2026-01-01, 2028-01-01, 0%, 105, 1, act-act | → | yield basis points -2.41%, yield to maturity -0.024099927, current yield basis points 0%, accrued interest 0.000000, dirty price 105.000000, previous coupon 2026-01-01, next coupo… |
| a distressed price | 2026-02-01, 2036-02-01, 5%, 50, 2, 30-360 | → | yield basis points 14.7%, yield to maturity 0.146961743, current yield basis points 10%, accrued interest 0.000000, dirty price 50.000000, previous coupon 2026-02-01, next coupon … |
| settlement on maturity | 2026-01-01, 2026-01-01, 5%, 100, 2, 30-360 | → | error: settlement must be before maturity |
| a frequency Excel does not accept | 2026-01-01, 2030-01-01, 5%, 100, 3, 30-360 | → | error: frequency must be 1, 2 or 4 |
| a negative coupon | 2026-01-01, 2030-01-01, -0.01%, 100, 2, 30-360 | → | error: couponBasisPoints must be a whole number from 0 to 100000 |
| a fractional coupon in basis points | 2026-01-01, 2030-01-01, 4.125%, 100, 2, 30-360 | → | error: couponBasisPoints must be a whole number from 0 to 100000 |
| a price that is not a decimal | 2026-01-01, 2030-01-01, 5%, par, 2, 30-360 | → | error: cleanPrice must be a positive decimal with at most 6 places |
| a price with seven decimal places | 2026-01-01, 2030-01-01, 5%, 99.1234567, 2, 30-360 | → | error: cleanPrice must be a positive decimal with at most 6 places |
| a price of zero | 2026-01-01, 2030-01-01, 5%, 0.000, 2, 30-360 | → | error: cleanPrice must be greater than zero |
| an unsupported day count | 2026-01-01, 2030-01-01, 5%, 100, 2, act-360 | → | error: basis must be 30-360 or act-act |
| an impossible date | 2026-02-30, 2030-01-01, 5%, 100, 2, 30-360 | → | error: is not a real calendar date |
| a price no yield above -50% a period explains | 2026-01-01, 2028-01-01, 0%, 1000, 1, act-act | → | error: the price implies a yield below -50% a coupon period |
| more than 100 years to maturity | 2000-01-01, 2101-01-01, 5%, 100, 1, act-act | → | error: maturity must be within 100 years of settlement |
| 30/360 bond basis can count past a month-end coupon | 2025-08-30, 2030-08-31, 5%, 100, 2, 30-360 | → | error: by the 30/360 count settlement is not before the next coupon date |
| a price with a trailing newline | 2026-01-01, 2030-01-01, 5%, 100 , 2, 30-360 | → | error: cleanPrice must be a positive decimal with at most 6 places |
| a decimal price with a trailing newline | 2026-01-01, 2030-01-01, 5%, 99.5 , 2, 30-360 | → | error: cleanPrice must be a positive decimal with at most 6 places |
More from the author
Coupon dates are stepped back from maturity in whole periods of 12 / frequency months (each one `k` periods from maturity, never one from the last, so the 31st does not decay to the 28th). When maturity is the last day of its month, every coupon date is the last day of its month, as Excel's coupon functions treat it: a bond maturing 30 June pays on 31 December.
- A: days from the previous coupon to settlement; E: days in the coupon period; DSC = E − A, days from settlement to the next coupon. - `30-360` (Excel basis 0): A by 30/360 bond basis (`dates.day-count-fraction`, ISDA 4.16(f)), E = 360 / frequency. - `act-act` (Excel basis 1, and the ICMA actual/actual of gilts): actual days, E the actual length of the period containing settlement. - Accrued interest = coupon / frequency × A / E per 100; dirty = clean + accrued.
**More than one coupon left**: the yield y solves
clean + accrued = Σ_(k=1..N) (c/f) / (1 + y/f)^(k−1+DSC/E) + 100 / (1 + y/f)^(N−1+DSC/E)
Excel uses Newton's method; here, with z = (1 + y/f)^(−1/E), a per-day factor, every power is whole (DSC + E(k−1) days), and z is found by bisection in `math.fractional-power`'s 18-place fixed point, the same floors in the same order in every language. Then y = f × (z^−E − 1). Yields down to −50% a coupon period are covered; a price above that is refused.
**One coupon period or less left** (N = 1), Excel's formula, computed exactly:
y = ((100 + c/f) − (clean + accrued)) / (clean + accrued) × (f × E / DSR)
with DSR, days from settlement to redemption, equal to DSC.
**Current yield** = annual coupon / clean price.
Rounding: the yield is settled to 12 decimal places, then rounded half away from zero to 9 places (`yieldToMaturity`) and to a basis point; current yield to a basis point half away from zero; accrued interest to 6 places, half up.
## Limits and what it does not do
- Redemption is 100. Price is a decimal string per 100, at most 6 places. - Only `30-360` and `act-act`. Excel's basis 0 is the US (NASD) 30/360, which treats the last day of February as the 30th; bond basis here does not, so the two differ when a coupon date or settlement is at the end of February. Where that makes settlement count as on or after the next coupon (e.g. a 28 February coupon and settlement on 30 August), it is refused: use `act-act`. - No ex-dividend periods (gilts trade ex-dividend 7 business days before a coupon, with negative accrued interest), no odd first or last coupons, no business-day adjustment, no call dates. Coupons, not settlement, are what the caller must line up with the real bond. - Settlement must be before maturity and within 100 years of it; frequency 1, 2 or 4; coupon 0 to 100000 basis points.
1.0.1 fixes Python accepting a trailing newline in cleanPrice; adds tests.
## Before you rely on this
**Not professional advice.** This capability calculates investment figures from published rules. It is a software component for developers, not financial advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed above, and have a tax adviser review how you use it, before anyone relies on the output. Provided "as is" under its licence, without warranty.
**Unreviewed.** This capability's implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified tax adviser has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
1.0.2 marks it unreviewed. The code and the tests are unchanged.
Files
| Path | Bytes |
|---|---|
| README.md | 4,529 |
| impl/python.py | 5,774 |
| impl/rust.rs | 8,788 |
| impl/typescript.ts | 5,973 |
| vectors.json | 8,396 |