Functional Weave
Code in Rust

invest.drawdown-sustainability Unreviewed

How many years a pension or investment pot lasts under a yearly withdrawal and a constant growth rate, year by year.

1.0.1 · published 2026-10-03 by charlie · Anterra

Pinned by 19 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

How long a pot lasts when a fixed (or inflation-rising) income is drawn from it each year and the rest grows at a constant rate: the arithmetic behind "will my pension last?" and the 4% rule.

## The convention

For example

  • drawdown_sustainability(£1,000.00, £300.00, 0%, 0%, 10) → full years 3, exhausted true, total withdrawn £1,000.00, closing balance £0.00, schedule ×4 £1,000 drawn at £300 a year with no growth: three full years and a £100 fourth
  • drawdown_sustainability(£1,000.00, £100.00, 10%, 0%, 3) → full years 3, exhausted false, total withdrawn £300.00, closing balance £966.90, schedule ×3 withdrawal first, then 10% growth on what is left (not growth first)
  • drawdown_sustainability(£1,100.00, £100.00, 10%, 0%, 5) → full years 5, exhausted false, total withdrawn £500.00, closing balance £1,100.00, schedule ×5 a withdrawal equal to the growth on the remainder lasts for ever

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 drawdown_sustainability(pot: &Money, annual_withdrawal: &Money, annual_growth_basis_points: i64, withdrawal_increase_basis_points: i64, max_years: i64) -> Drawdown
potMoneythe pot at the start, 0 or more
annual_withdrawalMoneythe first year's withdrawal, taken at the start of the year; greater than zero
annual_growth_basis_pointsintgrowth on what is left each year, 400 = 4%; not below -10000
withdrawal_increase_basis_pointsintyearly rise in the withdrawal, 250 = 2.5% for inflation; 0 for a level income
max_yearsinthow far to model, 1 to 100
returnsDrawdown

The types it declares, generated into your project

/// One year of the drawdown.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DrawdownYear {
    /// 1 for the first year
    pub year: i64,
    pub opening: Money,
    /// the planned withdrawal, or what was left if less
    pub withdrawal: Money,
    /// on the balance after the withdrawal; negative in a falling year
    pub growth: Money,
    pub closing: Money,
}

/// How long the pot lasted and how it got there.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Drawdown {
    /// years in which the whole planned withdrawal was paid
    pub full_years: i64,
    /// the pot reached zero within maxYears
    pub exhausted: bool,
    pub total_withdrawn: Money,
    /// at the end of the last year modelled
    pub closing_balance: Money,
    /// one row per year, ending in the year the pot ran out or at maxYears
    pub schedule: Vec<DrawdownYear>,
}

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

fune!(invest.drawdown-sustainability@^1);  // then call drawdown_sustainability(…)
impl/rust.rs · 141 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::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::money_apply_rate::apply_rate;  ← from money.apply-rate ^1.0.0 · built alongside by fune

/// Draw an income from a pot year by year, withdrawal first and growth on
/// what is left, until the pot runs out or max_years is reached.
///
/// # Panics
/// Panics on a negative pot, a withdrawal of 0 or less, growth or an increase
/// below -10000, max_years outside 1 to 100, or mixed currencies.
pub fn drawdown_sustainability(
    pot: &Money,
    annual_withdrawal: &Money,
    annual_growth_basis_points: i64,
    withdrawal_increase_basis_points: i64,
    max_years: i64,
) -> Drawdown {
    let currency = pot.currency.as_str();
    if annual_withdrawal.currency != currency {
        panic!("currency mismatch: {} and {}", currency, annual_withdrawal.currency);
    }
    if pot.minor < 0 {
        panic!("pot must be whole minor units, 0 or more; received {}", pot.minor);
    }
    if annual_withdrawal.minor <= 0 {
        panic!(
            "annualWithdrawal must be whole minor units greater than zero; received {}",
            annual_withdrawal.minor
        );
    }
    if annual_growth_basis_points < -10000 {
        panic!(
            "annualGrowthBasisPoints must be a whole number, -10000 or more; received {}",
            annual_growth_basis_points
        );
    }
    if withdrawal_increase_basis_points < -10000 {
        panic!(
            "withdrawalIncreaseBasisPoints must be a whole number, -10000 or more; received {}",
            withdrawal_increase_basis_points
        );
    }
    if !(1..=100).contains(&max_years) {
        panic!("maxYears must be a whole number from 1 to 100; received {}", max_years);
    }
    let mut balance = pot.minor;
    let mut planned = annual_withdrawal.minor;
    let mut full_years: i64 = 0;
    let mut total: i64 = 0;
    let mut exhausted = false;
    let mut schedule: Vec<DrawdownYear> = Vec::new();
    for year in 1..=max_years {
        let opening = balance;
        let withdrawal = planned.min(opening);
        if withdrawal == planned {
            full_years += 1;
        }
        total += withdrawal;
        let remaining = opening - withdrawal;
        let growth = apply_rate(&money(remaining, currency), annual_growth_basis_points, "half-up").minor;
        balance = remaining + growth;
        schedule.push(DrawdownYear {
            year,
            opening: money(opening, currency),
            withdrawal: money(withdrawal, currency),
            growth: money(growth, currency),
            closing: money(balance, currency),
        });
        if balance == 0 {
            exhausted = true;
            break;
        }
        planned += apply_rate(&money(planned, currency), withdrawal_increase_basis_points, "half-up").minor;
    }
    Drawdown {
        full_years,
        exhausted,
        total_withdrawn: money(total, currency),
        closing_balance: money(balance, currency),
        schedule,
    }
}

pub fn drawdown_to_value(result: &Drawdown) -> Value {
    Value::obj(vec![
        ("fullYears", Value::Int(result.full_years)),
        ("exhausted", Value::Bool(result.exhausted)),
        ("totalWithdrawn", money_to_value(&result.total_withdrawn)),
        ("closingBalance", money_to_value(&result.closing_balance)),
        (
            "schedule",
            Value::Arr(
                result
                    .schedule
                    .iter()
                    .map(|y| {
                        Value::obj(vec![
                            ("year", Value::Int(y.year)),
                            ("opening", money_to_value(&y.opening)),
                            ("withdrawal", money_to_value(&y.withdrawal)),
                            ("growth", money_to_value(&y.growth)),
                            ("closing", money_to_value(&y.closing)),
                        ])
                    })
                    .collect(),
            ),
        ),
    ])
}

fn fraction(v: &Value) -> Option<f64> {
    match v {
        Value::Float(f) if f.fract() != 0.0 => Some(*f),
        _ => None,
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    if let Some(f) = fraction(args[0].get("minor")) {
        panic!("pot must be whole minor units, 0 or more; received {}", f);
    }
    if let Some(f) = fraction(args[1].get("minor")) {
        panic!("annualWithdrawal must be whole minor units greater than zero; received {}", f);
    }
    if let Some(f) = fraction(&args[2]) {
        panic!("annualGrowthBasisPoints must be a whole number, -10000 or more; received {}", f);
    }
    if let Some(f) = fraction(&args[3]) {
        panic!("withdrawalIncreaseBasisPoints must be a whole number, -10000 or more; received {}", f);
    }
    if let Some(f) = fraction(&args[4]) {
        panic!("maxYears must be a whole number from 1 to 100; received {}", f);
    }
    drawdown_to_value(&drawdown_sustainability(
        &money_from_value(&args[0]),
        &money_from_value(&args[1]),
        args[2].as_i64(),
        args[3].as_i64(),
        args[4].as_i64(),
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 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.drawdown-sustainability
Download for Rust invest.drawdown-sustainability-1.0.1-rust.fune · 31,918 bytes sha256 0760cad68756a4f2b5fd7df731f9fef058e22ab97ab63a3d647d7ec538bc8ac8

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

The whole function, every language, is one file too: invest.drawdown-sustainability-1.0.1.fune, 37,800 bytes, sha256 782e11f00033a2b53709ddb4707847fdaada9402f7b4f482d513e77e0aa1896a. 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.drawdown-sustainability

after — your function gets the result and the arguments, and returns the final result.

// fune: after invest.drawdown-sustainability

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 money.amount in invest.drawdown-sustainability
// fune: replace money.apply-rate in invest.drawdown-sustainability

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.drawdown-sustainability --steps.

// fune: step invest.drawdown-sustainability 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
£1,000 drawn at £300 a year with no growth: three full years and a £100 fourth £1,000.00, £300.00, 0%, 0%, 10 → full years 3, exhausted true, total withdrawn £1,000.00, closing balance £0.00, schedule ×4
withdrawal first, then 10% growth on what is left (not growth first) £1,000.00, £100.00, 10%, 0%, 3 → full years 3, exhausted false, total withdrawn £300.00, closing balance £966.90, schedule ×3
a withdrawal equal to the growth on the remainder lasts for ever £1,100.00, £100.00, 10%, 0%, 5 → full years 5, exhausted false, total withdrawn £500.00, closing balance £1,100.00, schedule ×5
an income rising 10% a year runs out in the fifth year £1,000.00, £200.00, 0%, 10%, 10 → full years 4, exhausted true, total withdrawn £1,000.00, closing balance £0.00, schedule ×5
growth is rounded to the penny every year £100.01, £33.33, 3.33%, 0%, 10 → full years 3, exhausted true, total withdrawn £103.52, closing balance £0.00, schedule ×4
a pot that reaches exactly zero on a full withdrawal counts that year £200.00, £100.00, 0%, 0%, 5 → full years 2, exhausted true, total withdrawn £200.00, closing balance £0.00, schedule ×2
a market halving every year £1,000.00, £100.00, -50%, 0%, 10 → full years 3, exhausted true, total withdrawn £337.50, closing balance £0.00, schedule ×4
one year modelled £1,000.00, £10.00, 5%, 0%, 1 → full years 1, exhausted false, total withdrawn £10.00, closing balance £1,039.50, schedule ×1
an empty pot pays nothing and is exhausted at once £0.00, £10.00, 5%, 0%, 5 → full years 0, exhausted true, total withdrawn £0.00, closing balance £0.00, schedule ×1
a falling withdrawal: 50% less each year £100.00, £40.00, 0%, -50%, 5 → full years 5, exhausted false, total withdrawn £77.50, closing balance £22.50, schedule ×5
Show the other 9 tests
CaseArgumentsExpected
a negative pot is an error -£0.01, £10.00, 0%, 0%, 5 → error: pot must be whole minor units, 0 or more
a zero withdrawal is an error £10.00, £0.00, 0%, 0%, 5 → error: annualWithdrawal must be whole minor units greater than zero
a fractional withdrawal is an error £10.00, £10..5, 0%, 0%, 5 → error: annualWithdrawal must be whole minor units greater than zero
growth below -100% is an error £10.00, £1.00, -100.01%, 0%, 5 → error: annualGrowthBasisPoints must be a whole number, -10000 or more
an increase below -100% is an error £10.00, £1.00, 0%, -100.01%, 5 → error: withdrawalIncreaseBasisPoints must be a whole number, -10000 or more
zero years is an error £10.00, £1.00, 0%, 0%, 0 → error: maxYears must be a whole number from 1 to 100
101 years is an error £10.00, £1.00, 0%, 0%, 101 → error: maxYears must be a whole number from 1 to 100
fractional years are an error £10.00, £1.00, 0%, 0%, 2.5 → error: maxYears must be a whole number from 1 to 100
a withdrawal in another currency is an error £10.00, €1.00, 0%, 0%, 5 → error: currency mismatch: GBP and EUR

More from the author

Each year, in whole minor units:

1. The withdrawal is taken at the **start** of the year: the planned amount, or everything left if that is less. 2. What remains grows by `annualGrowthBasisPoints`, rounded half away from zero (`money.apply-rate`, half-up). 3. The next year's planned withdrawal rises by `withdrawalIncreaseBasisPoints`, rounded the same way.

The run stops in the year the pot reaches zero, or after `maxYears`. `fullYears` counts the years in which the whole planned income was paid; a last, partial year appears in the schedule but not in `fullYears`. A pot that reaches exactly zero on a full withdrawal counts that year and is `exhausted`.

Taking the withdrawal first and growing the remainder is the cautious order, and the one an income drawn in advance follows. Growing first and then withdrawing gives a longer life for the same inputs; the vectors pin the order used here.

## What it does not do

It uses a single constant growth rate, so it says nothing about sequence of returns risk; run it for several rates, or use a stochastic model, for that. It ignores charges (see `invest.fee-drag`), tax on withdrawals and the tax-free lump sum; pass net figures if you need them.

## Errors

A negative pot, a withdrawal of 0 or less, growth or an increase below -10000, `maxYears` outside 1-100, fractional amounts and mixed currencies.

## 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.1 marks it unreviewed. The code and the tests are unchanged.

Files

PathBytes
README.md2,698
impl/python.py2,901
impl/rust.rs5,078
impl/typescript.ts2,778
vectors.json17,360