Functional Weave
Code in TypeScript

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 accrued
  • bondYield(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
settlementdatethe date the buyer pays and takes the bond; before maturity
maturitydatethe redemption date; coupon dates are stepped back from it
couponBasisPointsintthe annual coupon rate: 575 = 5.75%; 0 to 100000
cleanPricestringper 100 of face value, without accrued interest, at most 6 decimal places: "95.04287"
frequencyintcoupons a year: 1, 2 or 4
basisBondDayCount30-360 (Excel basis 0, bond basis) or act-act (Excel basis 1, as gilts)
returnsBondYield

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";
impl/typescript.ts · 129 lines · open · raw

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
Download for TypeScript invest.bond-yield-1.0.2-typescript.fune · 24,239 bytes sha256 da956b47b47ba7b4813cbb0ff97f576be59af266e6a6e8bfd0043f9b9a7a4beb

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md4,529
impl/python.py5,774
impl/rust.rs8,788
impl/typescript.ts5,973
vectors.json8,396