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
bondYield(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%bondYield(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 accruedbondYield(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.
export function bondYield(settlement: string, maturity: string, couponBasisPoints: number, cleanPrice: string, frequency: number, 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 |
| couponBasisPoints | int | the annual coupon rate: 575 = 5.75%; 0 to 100000 |
| cleanPrice | 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
export type BondDayCount = "30-360" | "act-act";
/** The yields, and the accrual they were worked from. */
export interface BondYield {
/** yield to maturity, half away from zero: 650 = 6.50% */
readonly yieldBasisPoints: number;
/** the annual yield to 9 decimal places, half away from zero: "0.065000007" */
readonly yieldToMaturity: string;
/** annual coupon / clean price, half away from zero */
readonly currentYieldBasisPoints: number;
/** per 100, 6 decimal places, half-up */
readonly accruedInterest: string;
/** clean price plus accrued interest, per 100, 6 decimal places */
readonly dirtyPrice: string;
/** the coupon date on or before settlement */
readonly previousCoupon: string;
/** the first coupon date after settlement */
readonly nextCoupon: string;
/** coupons still to be paid, the one at maturity included */
readonly couponsRemaining: number;
}
Your code names it in one line, in the file that uses it
import { bondYield } from "#fune/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 { daysInMonth, epochDayFromIso, formatIsoDate, parseIsoDate } from "./dates_add_days.ts"; ← from dates.add-days ^1.0.0 · built alongside by fune
import { addMonths } from "./dates_add_months.ts"; ← from dates.add-months ^1.0.0 · built alongside by fune
import { dayCountFraction } from "./dates_day_count_fraction.ts"; ← from dates.day-count-fraction ^1.0.0 · built alongside by fune
import { FIXED_SCALE, mulFixed, powFixed, rootFixed } from "./math_fractional_power.ts"; ← from math.fractional-power ^1.0.0 · built alongside by fune
import { type BondDayCount, type BondYield } from "./invest_bond_yield_types.ts";
const PRICE = /^[0-9]+(\.[0-9]{1,6})?$/;
const MICRO = 1000000n;
/** n / d rounded half away from zero; d > 0. */
function roundHalfAway(n: bigint, d: bigint): bigint {
const magnitude = ((n < 0n ? -n : n) * 2n + d) / (2n * d);
return n < 0n ? -magnitude : magnitude;
}
function decimal(value: bigint, places: number): string {
const unit = 10n ** BigInt(places);
const magnitude = value < 0n ? -value : value;
return `${value < 0n ? "-" : ""}${magnitude / unit}.${(magnitude % unit).toString().padStart(places, "0")}`;
}
/** The coupon date k periods before maturity; month ends stay month ends. */
function couponDate(maturity: string, k: number, months: number, endOfMonth: boolean): string {
const date = addMonths(maturity, -k * months);
if (!endOfMonth) return date;
const civil = parseIsoDate(date);
return formatIsoDate({ ...civil, day: daysInMonth(civil.year, civil.month) });
}
/**
* 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.
*/
export function bondYield(settlement: string, maturity: string, couponBasisPoints: number, cleanPrice: string, frequency: number, basis: BondDayCount): BondYield {
if (frequency !== 1 && frequency !== 2 && frequency !== 4) {
throw new RangeError(`frequency must be 1, 2 or 4, received ${frequency}`);
}
if (!Number.isInteger(couponBasisPoints) || couponBasisPoints < 0 || couponBasisPoints > 100000) {
throw new RangeError(`couponBasisPoints must be a whole number from 0 to 100000, received ${couponBasisPoints}`);
}
if (typeof cleanPrice !== "string" || !PRICE.test(cleanPrice)) {
throw new RangeError(`cleanPrice must be a positive decimal with at most 6 places, received "${cleanPrice}"`);
}
if (basis !== "30-360" && basis !== "act-act") {
throw new RangeError(`basis must be 30-360 or act-act, received "${basis}"`);
}
const [whole, fraction = ""] = cleanPrice.split(".");
const priceMicro = BigInt(whole) * MICRO + BigInt(fraction.padEnd(6, "0"));
if (priceMicro === 0n) throw new RangeError("cleanPrice must be greater than zero");
const settleDay = epochDayFromIso(settlement);
if (settleDay >= epochDayFromIso(maturity)) throw new RangeError("settlement must be before maturity");
const months = 12 / frequency;
const civil = parseIsoDate(maturity);
const endOfMonth = civil.day === daysInMonth(civil.year, civil.month);
let k = 1;
while (epochDayFromIso(couponDate(maturity, k, months, endOfMonth)) > settleDay) {
k += 1;
if (k > 100 * frequency) throw new RangeError("maturity must be within 100 years of settlement");
}
const previous = couponDate(maturity, k, months, endOfMonth);
const next = couponDate(maturity, k - 1, months, endOfMonth);
let a: number;
let e: number;
if (basis === "30-360") {
const f = dayCountFraction(previous, settlement, "30-360");
a = (f.numerator * 360) / f.denominator;
e = 360 / frequency;
} else {
a = settleDay - epochDayFromIso(previous);
e = epochDayFromIso(next) - epochDayFromIso(previous);
}
const dsc = e - a;
if (dsc <= 0) throw new RangeError("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.
const coupon = BigInt(couponBasisPoints);
const bigE = BigInt(e);
const f = BigInt(frequency);
const couponTerm = coupon * bigE * MICRO; // C * M, C = coupon / (100 f) per 100
const redemptionTerm = 100n * 100n * f * bigE * MICRO; // 100 * M
const target = priceMicro * 100n * f * bigE + coupon * BigInt(a) * MICRO; // (clean + accrued) * M
let settled: bigint;
if (k === 1) {
// ((100 + C) - dirty) / dirty * (f * E / DSC)
const numerator = (redemptionTerm + couponTerm - target) * f * bigE;
settled = roundHalfAway(numerator * 10n ** 12n, target * BigInt(dsc));
} else {
const n = k;
const value = (z: bigint): bigint => {
const step = powFixed(z, e);
let p = powFixed(z, dsc);
let sum = 0n;
for (let i = 1; i <= n; i++) {
sum += couponTerm * p;
if (i < n) p = mulFixed(p, step);
}
return sum + redemptionTerm * p - target * FIXED_SCALE;
};
let lo = 0n;
let hi = rootFixed(2n * FIXED_SCALE, e);
if (value(hi) < 0n) throw new RangeError("the price implies a yield below -50% a coupon period");
while (hi - lo > 1n) {
const mid = (lo + hi) / 2n;
if (value(mid) >= 0n) hi = mid;
else lo = mid;
}
const growth = powFixed(hi, e);
if (growth === 0n) throw new RangeError("the yield is too large to compute");
const perPeriod = (FIXED_SCALE * FIXED_SCALE) / growth - FIXED_SCALE;
settled = roundHalfAway(perPeriod * f, 1000000n);
}
const accruedMicro = roundHalfAway(coupon * BigInt(a) * MICRO, 100n * f * bigE);
return {
yieldBasisPoints: Number(roundHalfAway(settled * 10000n, 10n ** 12n)),
yieldToMaturity: decimal(roundHalfAway(settled, 1000n), 9),
currentYieldBasisPoints: Number(roundHalfAway(coupon * 100n * MICRO, priceMicro)),
accruedInterest: decimal(accruedMicro, 6),
dirtyPrice: decimal(priceMicro + accruedMicro, 6),
previousCoupon: previous,
nextCoupon: next,
couponsRemaining: k,
};
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 5 dependencies, pins them in fune.lock, downloads only the TypeScript 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 TypeScript implementation. Install it without the registry with fune add ./invest.bond-yield-1.0.2-typescript.fune, or fetch it from a terminal with fune pull invest.bond-yield@1.0.2:typescript.
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 |