← Back

UK CGT Calculator

Capital Gains Tax for share disposals · 2025 · solo build during Blick Rothenberg

Built while doing personal-tax computations at Blick Rothenberg. The brief was self-imposed: clients arriving with multi-broker share trading histories (Interactive Brokers, Hargreaves Lansdown, Trading 212, Freetrade, eToro), each with its own CSV format, and the matching rules being unforgiving enough that spreadsheet errors cost real money in HMRC penalties.

Existing calculators either oversimplified (pretending matching rules don't exist) or required hours of manual data prep. So I wrote one that doesn't.

The hard parts

UK CGT for shares isn't a single computation. It's a sequence of disposal-matching rules applied in a strict order, with a holding pool that mutates as you go. The rules look simple in the manual; they break in interesting ways the moment real trading data hits them.

Section 104 holding pool

All shares of the same class in the same company that don't fall under one of the special matching rules go into a single pool. The pool has a cumulative cost and a cumulative quantity. When you sell, you take a proportional cost out:

cost_of_disposal = (units_sold / pool_units) * pool_cost
pool_units      -= units_sold
pool_cost       -= cost_of_disposal

Easy in the abstract. Painful when a client has 14 years of trading and the pool has been mutated 800 times. One arithmetic error in the middle propagates to every subsequent disposal.

Same-day matching

A disposal is matched first against acquisitions on the same day, before the pool is touched. This is the rule most spreadsheet-based calculations get wrong, because intraday trading produces same-day buys and sells that need to be netted in isolation, not absorbed into the pool.

30-day matching (the "bed and breakfast" rule)

Then a disposal is matched against acquisitions in the 30 days following, in chronological order. This is anti-avoidance: it prevents you from selling at a loss and buying back the next day to crystallise the loss while keeping the position.

The compound case — same-day match for part, 30-day match for part, pool for the remainder — is where most homemade spreadsheets quietly produce wrong numbers. The matching has to be sequenced correctly, and each match has to allocate cost in the right proportions.

Rate banding

The chargeable gain (after the annual exempt amount) is then stacked on top of the taxpayer's other taxable income to determine the rate. Basic-rate band slack is taxed at 10% (18% for residential property, but this calc is shares only), everything above at 20% (24% for residential property as of April 2024).

So you can't compute CGT in isolation. You need the taxpayer's non-CGT income position to know how much basic-rate band remains, which is why the calculator takes that as an input rather than guessing.

Architecture

Multi-broker import

Each broker has a different CSV / Excel export format. Different column names, different date formats, different ways of representing fractional shares, different conventions for buy/sell sign, different handling of corporate actions (splits, mergers, spin-offs, dividends in specie).

The import layer normalises all of these to a single internal transaction shape:

{
  date: ISO8601,
  ticker: string,        // canonical (resolves Trading 212 / IBKR identifier mismatches)
  side: 'buy' | 'sell',
  quantity: Decimal,     // not number — float arithmetic on cost is malpractice
  unit_price: Decimal,
  fees: Decimal,
  currency: string,
  fx_rate_to_gbp: Decimal | null  // null forces user to supply or look up
}

Decimal arithmetic throughout — JavaScript's number type produces wrong CGT answers in ways HMRC will not accept. This is the kind of thing that would feel like over-engineering until the first time a binary-floating-point rounding error puts a gain £0.03 above an annual exempt amount threshold.

Matching engine

Pure deterministic logic: given a sorted transaction list, produces a disposal-by-disposal computation with each gain attributed to the rules that matched it. Output isn't just "total gain £X" — it's a full audit trail showing same-day allocations, 30-day allocations, and pool allocations separately.

This audit trail matters. If HMRC opens an enquiry, the question isn't "what's the total gain"; it's "show your working". The calculator produces the working.

Rate banding

Takes the chargeable gain plus the taxpayer's other taxable income, applies AEA, then bands the remainder. Output shows basic-rate-banded slice and higher-rate-banded slice separately so the advisor can sanity-check.

Testing — HMRC examples as fixtures

HMRC publishes worked examples in the CG (Capital Gains) manual, particularly around CG51500 (share matching rules). Each example has a specified input and a specified output. They become the test fixtures.

Every architectural change runs against the bank. If a refactor changes the output for any HMRC example, that's a regression — even if the code is "cleaner". The HMRC examples are not optional; they are the ground truth.

Beyond HMRC examples, I added private fixtures derived from real client cases (anonymised, identifying data stripped) that exposed edge cases the public examples don't cover — particularly compound same-day-plus-30-day-plus-pool cases on heavily-traded portfolios.

What's not in scope

The calculator does the share-disposal piece well, and stops there. A tool that tries to do everything ends up doing nothing well — particularly in tax, where each sub-area has its own evolved corpus of edge cases.

What it taught me

Two things, both of which carried into how I now think about every domain-heavy AI project:

One. When the rules are this hard and the consequences of being wrong are this real, deterministic logic beats LLM-generated computation, every time. The LLM goes on top of the calculator, not inside it. The model gathers data, asks clarifying questions, presents results — but the matching engine is plain typed code, unit-tested against HMRC fixtures.

Two. Domain knowledge does the load-bearing work. I knew which edge cases mattered because I had spent years cleaning up after spreadsheets that didn't. The architecture is good because the domain is understood, not because the engineering is clever.

This is the same instinct behind TaxPilot — procedure as deterministic decision tree, rates as separately versioned values, citations as non-optional. The CGT calculator was where I first learned to structure tax software that way.