Functional Weave
Code in Rust

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 accrued
  • bond_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.

pub fn bond_yield(settlement: &str, maturity: &str, coupon_basis_points: i64, clean_price: &str, frequency: i64, basis: &str) -> BondYield
settlementdatethe date the buyer pays and takes the bond; before maturity
maturitydatethe redemption date; coupon dates are stepped back from it
coupon_basis_pointsintthe annual coupon rate: 575 = 5.75%; 0 to 100000
clean_pricestringper 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

// BondDayCount is a string in Rust, one of: "30-360", "act-act".
// Parameters take it as &str and results hold it as String.

/// The yields, and the accrual they were worked from.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BondYield {
    /// yield to maturity, half away from zero: 650 = 6.50%
    pub yield_basis_points: i64,
    /// the annual yield to 9 decimal places, half away from zero: "0.065000007"
    pub yield_to_maturity: String,
    /// annual coupon / clean price, half away from zero
    pub current_yield_basis_points: i64,
    /// per 100, 6 decimal places, half-up
    pub accrued_interest: String,
    /// clean price plus accrued interest, per 100, 6 decimal places
    pub dirty_price: String,
    /// the coupon date on or before settlement
    pub previous_coupon: String,
    /// the first coupon date after settlement
    pub next_coupon: String,
    /// coupons still to be paid, the one at maturity included
    pub coupons_remaining: i64,
}

Your code names it in one line, in the file that uses it

fune!(invest.bond-yield@^1);  // then call bond_yield(…)
impl/rust.rs · 217 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::{days_in_month, epoch_day_from_iso, format_iso_date, parse_iso_date, CivilDate};  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_add_months::add_months;  ← from dates.add-months ^1.0.0 · built alongside by fune
use super::dates_day_count_fraction::day_count_fraction;  ← from dates.day-count-fraction ^1.0.0 · built alongside by fune
use super::math_big_integer::{parse_big_integer, BigInt};  ← from math.big-integer ^1.0.0 · built alongside by fune
use super::math_fractional_power::{fixed_scale, mul_fixed, pow_fixed, root_fixed};  ← from math.fractional-power ^1.0.0 · built alongside by fune

/// n / d rounded half away from zero; d > 0.
fn round_half_away(n: &BigInt, d: &BigInt) -> BigInt {
    let two = BigInt::from_i64(2);
    let magnitude = n.abs().mul(&two).add(d).div(&two.mul(d));
    if n.is_negative() {
        magnitude.neg()
    } else {
        magnitude
    }
}

fn big(v: i64) -> BigInt {
    BigInt::from_i64(v)
}

fn decimal(value: &BigInt, places: u32) -> String {
    let unit = big(10i64.pow(places));
    let (whole, fraction) = value.abs().div_rem(&unit);
    format!(
        "{}{}.{:0width$}",
        if value.is_negative() { "-" } else { "" },
        whole,
        fraction.to_i64(),
        width = places as usize
    )
}

/// The coupon date k periods before maturity; month ends stay month ends.
fn coupon_date(maturity: &str, k: i64, months: i64, end_of_month: bool) -> String {
    let date = add_months(maturity, -k * months);
    if !end_of_month {
        return date;
    }
    let civil = parse_iso_date(&date);
    format_iso_date(&CivilDate { year: civil.year, month: civil.month, day: days_in_month(civil.year, civil.month) })
}

fn is_price(text: &str) -> bool {
    let (whole, fraction) = match text.split_once('.') {
        Some((w, f)) => (w, Some(f)),
        None => (text, None),
    };
    !whole.is_empty()
        && whole.bytes().all(|b| b.is_ascii_digit())
        && match fraction {
            None => true,
            Some(f) => !f.is_empty() && f.len() <= 6 && f.bytes().all(|b| b.is_ascii_digit()),
        }
}

/// 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.
///
/// # Panics
/// Panics on a bad frequency, coupon, price, basis or dates, a maturity more
/// than 100 years away, or a price no yield above -50% a period explains.
pub fn bond_yield(settlement: &str, maturity: &str, coupon_basis_points: i64, clean_price: &str, frequency: i64, basis: &str) -> BondYield {
    if frequency != 1 && frequency != 2 && frequency != 4 {
        panic!("frequency must be 1, 2 or 4, received {}", frequency);
    }
    let c = coupon_basis_points;
    if c < 0 || c > 100000 {
        panic!("couponBasisPoints must be a whole number from 0 to 100000, received {}", c);
    }
    if !is_price(clean_price) {
        panic!("cleanPrice must be a positive decimal with at most 6 places, received \"{}\"", clean_price);
    }
    if basis != "30-360" && basis != "act-act" {
        panic!("basis must be 30-360 or act-act, received \"{}\"", basis);
    }
    let (whole, fraction) = clean_price.split_once('.').unwrap_or((clean_price, ""));
    let mut digits = whole.to_string();
    digits.push_str(&format!("{:0<6}", fraction));
    let digits = digits.trim_start_matches('0');
    let price_micro = if digits.is_empty() { BigInt::zero() } else { parse_big_integer(digits) };
    if price_micro.is_zero() {
        panic!("cleanPrice must be greater than zero");
    }
    let settle_day = epoch_day_from_iso(settlement);
    if settle_day >= epoch_day_from_iso(maturity) {
        panic!("settlement must be before maturity");
    }

    let months = 12 / frequency;
    let civil = parse_iso_date(maturity);
    let end_of_month = civil.day == days_in_month(civil.year, civil.month);
    let mut k: i64 = 1;
    while epoch_day_from_iso(&coupon_date(maturity, k, months, end_of_month)) > settle_day {
        k += 1;
        if k > 100 * frequency {
            panic!("maturity must be within 100 years of settlement");
        }
    }
    let previous = coupon_date(maturity, k, months, end_of_month);
    let next = coupon_date(maturity, k - 1, months, end_of_month);
    let (a, e) = if basis == "30-360" {
        let yf = day_count_fraction(&previous, settlement, "30-360");
        (yf.numerator * 360 / yf.denominator, 360 / frequency)
    } else {
        let p = epoch_day_from_iso(&previous);
        (settle_day - p, epoch_day_from_iso(&next) - p)
    };
    let dsc = e - a;
    if dsc <= 0 {
        panic!("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.
    let micro = big(1_000_000);
    let coupon_term = big(c * e).mul(&micro); // C * M, C = coupon / (100 f) per 100
    let redemption_term = big(10000 * frequency * e).mul(&micro); // 100 * M
    let target = price_micro.mul(&big(100 * frequency * e)).add(&big(c * a).mul(&micro)); // (clean + accrued) * M
    let scale = fixed_scale();
    let e12 = big(1_000_000_000_000);

    let settled = if k == 1 {
        // ((100 + C) - dirty) / dirty * (f * E / DSC)
        let numerator = redemption_term.add(&coupon_term).sub(&target).mul(&big(frequency * e));
        round_half_away(&numerator.mul(&e12), &target.mul(&big(dsc)))
    } else {
        let n = k;
        let value = |z: &BigInt| -> BigInt {
            let step = pow_fixed(z, e as u64);
            let mut p = pow_fixed(z, dsc as u64);
            let mut total = BigInt::zero();
            for i in 1..=n {
                total = total.add(&coupon_term.mul(&p));
                if i < n {
                    p = mul_fixed(&p, &step);
                }
            }
            total.add(&redemption_term.mul(&p)).sub(&target.mul(&scale))
        };
        let one = big(1);
        let two = big(2);
        let mut lo = BigInt::zero();
        let mut hi = root_fixed(&scale.mul(&two), e as u64);
        if value(&hi).is_negative() {
            panic!("the price implies a yield below -50% a coupon period");
        }
        while hi.sub(&lo) > one {
            let mid = lo.add(&hi).div(&two);
            if !value(&mid).is_negative() {
                hi = mid;
            } else {
                lo = mid;
            }
        }
        let growth = pow_fixed(&hi, e as u64);
        if growth.is_zero() {
            panic!("the yield is too large to compute");
        }
        let per_period = scale.mul(&scale).div(&growth).sub(&scale);
        round_half_away(&per_period.mul(&big(frequency)), &micro)
    };
    let accrued_micro = round_half_away(&big(c * a).mul(&micro), &big(100 * frequency * e));
    BondYield {
        yield_basis_points: round_half_away(&settled.mul(&big(10000)), &e12).to_i64(),
        yield_to_maturity: decimal(&round_half_away(&settled, &big(1000)), 9),
        current_yield_basis_points: round_half_away(&big(c * 100).mul(&micro), &price_micro).to_i64(),
        accrued_interest: decimal(&accrued_micro, 6),
        dirty_price: decimal(&price_micro.add(&accrued_micro), 6),
        previous_coupon: previous,
        next_coupon: next,
        coupons_remaining: k,
    }
}

pub fn bond_yield_to_value(result: &BondYield) -> Value {
    Value::obj(vec![
        ("yieldBasisPoints", Value::Int(result.yield_basis_points)),
        ("yieldToMaturity", Value::str(&result.yield_to_maturity)),
        ("currentYieldBasisPoints", Value::Int(result.current_yield_basis_points)),
        ("accruedInterest", Value::str(&result.accrued_interest)),
        ("dirtyPrice", Value::str(&result.dirty_price)),
        ("previousCoupon", Value::str(&result.previous_coupon)),
        ("nextCoupon", Value::str(&result.next_coupon)),
        ("couponsRemaining", Value::Int(result.coupons_remaining)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    // Refuse what the typed signature cannot hold, with the wording TypeScript
    // and Python use.
    if let Value::Float(f) = args[2] {
        if f.fract() != 0.0 {
            panic!("couponBasisPoints must be a whole number from 0 to 100000, received {}", f);
        }
    }
    if let Value::Float(f) = args[4] {
        if f.fract() != 0.0 {
            panic!("frequency must be 1, 2 or 4, received {}", f);
        }
    }
    if !matches!(args[3], Value::Str(_)) {
        panic!("cleanPrice must be a positive decimal with at most 6 places, received \"{:?}\"", args[3]);
    }
    bond_yield_to_value(&bond_yield(
        args[0].as_str(),
        args[1].as_str(),
        args[2].as_i64(),
        args[3].as_str(),
        args[4].as_i64(),
        args[5].as_str(),
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 5 dependencies, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add invest.bond-yield
Download for Rust invest.bond-yield-1.0.2-rust.fune · 27,153 bytes sha256 3c6e863a36ae5457d79566ea5ae946130168013cfdecb88473de5a7f77138f78

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./invest.bond-yield-1.0.2-rust.fune, or fetch it from a terminal with fune pull invest.bond-yield@1.0.2:rust.

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