Functional Weave
Code in TypeScript

invest.section-104-pool

UK share matching for CGT: same-day, 30-day bed-and-breakfast, then the Section 104 pool; allowable cost per disposal.

1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra

Pinned by 23 tests, run in TypeScript, Python and Rust.

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 (CTA) review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

**Status: needs review by a tax professional before it is published.**

Works out the allowable cost of every disposal of one holding of shares under the UK share identification rules for individuals, for disposals on or after 6 April 2008. Give it every purchase and sale of one class of shares in one company, in date order and in sterling; it returns each day's disposal with the shares it was matched against, the proceeds and cost of each match, the gain or loss, and the Section 104 holding left at the end.

For example

  • section104Pool(transactions ×6) → disposals ×2, pool quantity 300, pool cost £444.00 HMRC CG51590 example: 30-day match gains 60, then the pool costs 2,200 of 2,500 shares at 3,256
  • section104Pool(transactions ×3) → disposals ×1, pool quantity 6,000, pool cost £6,000.00 HMRC HS284 example 2: 500 matched with shares bought 12 days later at a 100 loss, 3,500 from the pool
  • section104Pool(transactions ×4) → disposals ×1, pool quantity 700, pool cost £700.00 same-day purchase is matched before the pool, and the day's sales are one disposal

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 section104Pool(transactions: readonly ShareTransaction[]): Section104Result
transactionsShareTransaction[]every purchase and sale of one class of shares in one company, in date order, in GBP
returnsSection104Resulteach disposal matched and costed, and the pool left at the end

The types it declares, generated into your project

/** One purchase or sale of shares of the same class in the same company. */
export interface ShareTransaction {
  readonly date: string;
  readonly kind: ShareTransactionKind;
  /** shares, greater than zero */
  readonly quantity: number;
  /** a buy: the allowable cost, dealing costs included; a sale: the proceeds after dealing costs */
  readonly amount: Money;
}

export type ShareTransactionKind = "buy" | "sell";

export type MatchRule = "same-day" | "bed-and-breakfast" | "section-104";

/** Part of a disposal identified with one acquisition, or with the pool. */
export interface ShareMatch {
  readonly rule: MatchRule;
  /** the matched purchase date; null for the Section 104 pool */
  readonly acquisitionDate: string | null;
  readonly quantity: number;
  /** the disposal proceeds apportioned to these shares */
  readonly proceeds: Money;
  /** the allowable cost of these shares */
  readonly cost: Money;
  /** proceeds less cost; negative for a loss */
  readonly gain: Money;
}

/** All sales on one day, treated as one disposal. */
export interface ShareDisposal {
  readonly date: string;
  readonly quantity: number;
  readonly proceeds: Money;
  readonly allowableCost: Money;
  /** negative for a loss */
  readonly gain: Money;
  /** same-day first, then bed-and-breakfast, then the pool */
  readonly matches: readonly ShareMatch[];
}

/** Every disposal, and the Section 104 holding after the last transaction. */
export interface Section104Result {
  readonly disposals: readonly ShareDisposal[];
  readonly poolQuantity: number;
  readonly poolCost: Money;
}

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

import { section104Pool } from "#fune/invest.section-104-pool@^1";
impl/typescript.ts · 193 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 { epochDayFromIso } from "./dates_add_days.ts";  ← from dates.add-days ^1.0.0 · built alongside by fune
import { type Money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type MatchRule, type Section104Result, type ShareDisposal, type ShareMatch, type ShareTransaction } from "./invest_section_104_pool_types.ts";

/** The identification rules below apply to disposals on or after this date (FA 2008). */
const RULES_START = "2008-04-06";

/** One day's purchases or sales, added together as the same-day rule requires. */
interface DayTotal {
  date: string;
  day: number;
  quantity: number;
  amount: bigint;
  /** Shares and cost still unmatched (purchases) or unidentified (sales). */
  remaining: number;
  remainingAmount: bigint;
}

interface Pending {
  rule: MatchRule;
  acquisitionDate: string | null;
  quantity: number;
  cost: bigint;
}

function gbp(minor: bigint): Money {
  return { minor: Number(minor), currency: "GBP" };
}

/** a * b / c rounded half-up, all non-negative. */
function share(a: bigint, b: bigint, c: bigint): bigint {
  return (2n * a * b + c) / (2n * c);
}

/**
 * Take `quantity` shares from a purchase (or the pool) and the cost that goes
 * with them. Cost is apportioned by number of shares, rounded half-up to the
 * penny, and taken from what is left, so the pieces always add back to the
 * whole: the last shares out take the last pennies.
 */
function take(total: DayTotal, quantity: number): bigint {
  const cost =
    quantity === total.remaining
      ? total.remainingAmount
      : share(total.remainingAmount, BigInt(quantity), BigInt(total.remaining));
  total.remaining -= quantity;
  total.remainingAmount -= cost;
  return cost;
}

function check(tx: ShareTransaction, previous: string | null): void {
  epochDayFromIso(tx.date);
  if (previous !== null && tx.date < previous) {
    throw new RangeError(`transactions must be in date order: ${tx.date} comes after ${previous}`);
  }
  if (tx.kind !== "buy" && tx.kind !== "sell") {
    throw new RangeError(`kind must be buy or sell, received "${tx.kind}"`);
  }
  if (!Number.isInteger(tx.quantity) || tx.quantity <= 0) {
    throw new RangeError(`quantity must be a whole number greater than zero, received ${tx.quantity} on ${tx.date}`);
  }
  if (tx.amount.currency !== "GBP") {
    throw new RangeError(`amounts must be in GBP, received ${tx.amount.currency} on ${tx.date}`);
  }
  if (!Number.isInteger(tx.amount.minor) || tx.amount.minor < 0) {
    throw new RangeError(`amount must be a whole number of pence, not negative, received ${tx.amount.minor} on ${tx.date}`);
  }
  if (tx.kind === "sell" && tx.date < RULES_START) {
    throw new RangeError(`disposals before ${RULES_START} follow earlier identification rules, received ${tx.date}`);
  }
}

/**
 * Identify each disposal of shares with acquisitions the way TCGA 1992
 * s105-106A require for individuals from 6 April 2008: first with shares
 * bought the same day, then with shares bought in the 30 days after the
 * disposal (earliest first), then with the Section 104 pool at average cost.
 *
 * Same-day matching is resolved for every day before any 30-day matching,
 * and 30-day matching for every disposal (earliest disposal first) before
 * the pool is walked, because a purchase claimed by an earlier rule never
 * enters the pool.
 */
export function section104Pool(transactions: readonly ShareTransaction[]): Section104Result {
  const buys: DayTotal[] = [];
  const sells: DayTotal[] = [];
  let previous: string | null = null;
  let held = 0;
  for (const tx of transactions) {
    check(tx, previous);
    previous = tx.date;
    const list = tx.kind === "buy" ? buys : sells;
    let last = list.length > 0 ? list[list.length - 1] : null;
    if (last === null || last.date !== tx.date) {
      last = { date: tx.date, day: epochDayFromIso(tx.date), quantity: 0, amount: 0n, remaining: 0, remainingAmount: 0n };
      list.push(last);
    }
    last.quantity += tx.quantity;
    last.amount += BigInt(tx.amount.minor);
    last.remaining = last.quantity;
    last.remainingAmount = last.amount;
  }
  // Nobody can sell shares they do not hold at the end of that day.
  let b = 0;
  for (const sell of sells) {
    while (b < buys.length && buys[b].date <= sell.date) held += buys[b++].quantity;
    held -= sell.quantity;
    if (held < 0) {
      throw new RangeError(`cannot sell more shares than are held: ${sell.quantity} sold on ${sell.date}`);
    }
  }

  const matches = new Map<string, Pending[]>();
  for (const sell of sells) matches.set(sell.date, []);

  // 1. Same day: TCGA 1992 s105(1)(b).
  for (const sell of sells) {
    const buy = buys.find((candidate) => candidate.date === sell.date);
    if (buy === undefined) continue;
    const quantity = Math.min(sell.remaining, buy.remaining);
    if (quantity === 0) continue;
    matches.get(sell.date)!.push({ rule: "same-day", acquisitionDate: buy.date, quantity, cost: take(buy, quantity) });
    sell.remaining -= quantity;
  }

  // 2. The next 30 days, earliest acquisition first: TCGA 1992 s106A(5).
  for (const sell of sells) {
    for (const buy of buys) {
      if (sell.remaining === 0) break;
      const after = buy.day - sell.day;
      if (after < 1 || after > 30 || buy.remaining === 0) continue;
      const quantity = Math.min(sell.remaining, buy.remaining);
      matches.get(sell.date)!.push({ rule: "bed-and-breakfast", acquisitionDate: buy.date, quantity, cost: take(buy, quantity) });
      sell.remaining -= quantity;
    }
  }

  // 3. Everything else through the Section 104 pool, in date order.
  const pool: DayTotal = { date: "", day: 0, quantity: 0, amount: 0n, remaining: 0, remainingAmount: 0n };
  const disposals: ShareDisposal[] = [];
  let next = 0;
  for (const sell of sells) {
    while (next < buys.length && buys[next].date <= sell.date) {
      pool.remaining += buys[next].remaining;
      pool.remainingAmount += buys[next].remainingAmount;
      next++;
    }
    const pending = matches.get(sell.date)!;
    if (sell.remaining > 0) {
      if (sell.remaining > pool.remaining) {
        throw new RangeError(`cannot sell more shares than are held: ${sell.quantity} sold on ${sell.date}`);
      }
      pending.push({ rule: "section-104", acquisitionDate: null, quantity: sell.remaining, cost: take(pool, sell.remaining) });
    }
    disposals.push(dispose(sell, pending));
  }
  while (next < buys.length) {
    pool.remaining += buys[next].remaining;
    pool.remainingAmount += buys[next].remainingAmount;
    next++;
  }
  return { disposals, poolQuantity: pool.remaining, poolCost: gbp(pool.remainingAmount) };
}

/** Apportion the day's proceeds across its matches by shares, the last match taking what is left. */
function dispose(sell: DayTotal, pending: readonly Pending[]): ShareDisposal {
  let proceedsLeft = sell.amount;
  let sharesLeft = sell.quantity;
  let allowable = 0n;
  const out: ShareMatch[] = pending.map((match) => {
    const proceeds =
      match.quantity === sharesLeft ? proceedsLeft : share(proceedsLeft, BigInt(match.quantity), BigInt(sharesLeft));
    proceedsLeft -= proceeds;
    sharesLeft -= match.quantity;
    allowable += match.cost;
    return {
      rule: match.rule,
      acquisitionDate: match.acquisitionDate,
      quantity: match.quantity,
      proceeds: gbp(proceeds),
      cost: gbp(match.cost),
      gain: gbp(proceeds - match.cost),
    };
  });
  return {
    date: sell.date,
    quantity: sell.quantity,
    proceeds: gbp(sell.amount),
    allowableCost: gbp(allowable),
    gain: gbp(sell.amount - allowable),
    matches: out,
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 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.section-104-pool
Download for TypeScript invest.section-104-pool-1.0.0-typescript.fune · 46,792 bytes sha256 2569cef8d73ec2c97df7c26f80acc49ec6549f63cd1281bf052d529f13db3919

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./invest.section-104-pool-1.0.0-typescript.fune, or fetch it from a terminal with fune pull invest.section-104-pool@1.0.0:typescript.

The whole function, every language, is one file too: invest.section-104-pool-1.0.0.fune, 66,041 bytes, sha256 86f7a6b5c95e6efd15123a84948372931748ee9e17d854dc99fa63d4246fd72c. 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.section-104-pool

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

// fune: after invest.section-104-pool

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.section-104-pool
// fune: replace money.amount in invest.section-104-pool

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.section-104-pool --steps.

// fune: step invest.section-104-pool 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
HMRC CG51590 example: 30-day match gains 60, then the pool costs 2,200 of 2,500 shares at 3,256 transactions ×6 → disposals ×2, pool quantity 300, pool cost £444.00
HMRC HS284 example 2: 500 matched with shares bought 12 days later at a 100 loss, 3,500 from the pool transactions ×3 → disposals ×1, pool quantity 6,000, pool cost £6,000.00
same-day purchase is matched before the pool, and the day's sales are one disposal transactions ×4 → disposals ×1, pool quantity 700, pool cost £700.00
a purchase on day 30 after the sale is matched transactions ×3 → disposals ×1, pool quantity 100, pool cost £100.00
a purchase on day 31 after the sale goes to the pool instead transactions ×3 → disposals ×1, pool quantity 100, pool cost £150.00
a purchase before the sale is never bed-and-breakfast: it joins the pool at average cost transactions ×3 → disposals ×1, pool quantity 100, pool cost £200.00
the earlier disposal takes the later purchase first (s106A(5)); a pool-first reading gets both wrong transactions ×4 → disposals ×2, pool quantity 800, pool cost £800.00
a same-day match outranks a 30-day match for an earlier sale transactions ×4 → disposals ×2, pool quantity 400, pool cost £400.00
pool cost is apportioned to the penny, half-up, and the pool keeps the remainder transactions ×3 → disposals ×2, pool quantity 1, pool cost £3.33
proceeds are apportioned across matches by shares, the last match taking the remainder transactions ×3 → disposals ×1, pool quantity 0, pool cost £0.00
Show the other 13 tests
CaseArgumentsExpected
selling the whole holding at a loss empties the pool transactions ×3 → disposals ×1, pool quantity 0, pool cost £0.00
no transactions: no disposals and an empty pool → disposals , pool quantity 0, pool cost £0.00
purchases only build the pool, whenever they were made transactions ×2 → disposals , pool quantity 15, pool cost £62.34
a purchase partly matched by bed-and-breakfast puts only its remainder into the pool transactions ×4 → disposals ×2, pool quantity 60, pool cost £105.00
selling more than is held is refused transactions ×2 → error: cannot sell more shares than are held
a sale covered only by a later purchase (a short sale) is refused transactions ×2 → error: cannot sell more shares than are held
disposals before 6 April 2008 are refused transactions ×2 → error: disposals before 2008-04-06 follow earlier identification rules
transactions out of date order are refused transactions ×2 → error: transactions must be in date order
amounts must be in sterling transactions ×1 → error: amounts must be in GBP
a fractional quantity is refused transactions ×1 → error: quantity must be a whole number greater than zero
a zero quantity is refused transactions ×1 → error: quantity must be a whole number greater than zero
a negative amount is refused transactions ×1 → error: amount must be a whole number of pence, not negative
an unknown kind is refused transactions ×1 → error: kind must be buy or sell

More from the author

## The rules

Each disposal is identified with acquisitions in this order:

1. **Same day.** Shares bought on the same day as the disposal (TCGA 1992 s105(1)(b)). All purchases on one day are one acquisition and all sales on one day are one disposal (s105(1)(a)), so the result has one disposal per day however many trades there were. 2. **The next 30 days** ("bed and breakfasting"). Shares bought in the 30 days after the disposal, earliest purchase first (s106A(5)). Day 30 counts; day 31 does not. Where two disposals could both claim one later purchase, the earlier disposal is matched first. 3. **The Section 104 holding.** Everything else comes out of the pool at its average cost (s104). Shares matched under 1 or 2 never enter the pool (CG51550).

Same-day matching is settled for every day before any 30-day matching, so a purchase is taken by a sale on its own day before it can be claimed by an earlier sale's 30-day window.

## Rounding

HMRC's own guidance apportions pool cost "by reference to the number of shares sold" (CG51575). Here the cost of a part of a purchase or of the pool is `cost left × shares taken / shares left`, rounded half-up to the penny, and taken from what is left, so the pieces always add back to the whole: selling the last share takes the last pennies. The day's proceeds are split across its matches the same way, so each match has its own gain or loss, as in the HS284 example. HMRC's worked examples use whole pounds; a reviewer should confirm the penny rounding.

## Edge cases and limits

- A sale of more shares than are held at the end of that day is refused, even when a later purchase would be matched with it (a short sale). - Disposals before 6 April 2008 are refused: they followed different identification rules (and indexation). Purchases may be any date; for shares held on 31 March 1982, pass the purchase at its 31 March 1982 market value (CG51550). - Amounts are in GBP pence. Convert foreign-currency costs and proceeds at the rate on each transaction date before calling. - Out of scope: bonus and rights issues, reorganisations, takeovers, employee share schemes, the non-resident and trading-company variants of the 30-day rule (s106A(5A)), and the older "kink test". A buy's `amount` is its full allowable cost including dealing costs and stamp duty; a sell's is the proceeds after dealing costs.

## Sources

Read on 2026-09-23:

- HMRC Capital Gains Manual CG51550, "Shares and securities: identification rules: shares pooling from 6 April 2008": https://www.gov.uk/hmrc-internal-manuals/capital-gains-manual/cg51550 - CG51560, "Identification rules for individuals from 6 April 2008" (same day, then 30 days, then Section 104; TCGA92/S105(1), S106A(5) and (5A)): https://www.gov.uk/hmrc-internal-manuals/capital-gains-manual/cg51560 - CG51575, "Section 104 holding: part disposal" (apportion by number of shares): https://www.gov.uk/hmrc-internal-manuals/capital-gains-manual/cg51575 - CG51590, Example 1 (Ms Davy), used as a vector: https://www.gov.uk/hmrc-internal-manuals/capital-gains-manual/cg51590 - HS284 "Shares and Capital Gains Tax (2025)", Example 2 (Mr Schneider), used as a vector with a pool cost of our own (the helpsheet gives none): https://www.gov.uk/government/publications/shares-and-capital-gains-tax-hs284-self-assessment-helpsheet/hs284-shares-and-capital-gains-tax-2025

## What a reviewer should check

- Penny rounding of apportioned costs and proceeds. - That refusing short sales, rather than matching them with later purchases, is the right default. - The order of 30-day matching when several disposals and purchases overlap.

Files

PathBytes
README.md4,227
impl/python.py7,545
impl/rust.rs11,016
impl/typescript.ts7,542
vectors.json25,915