Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Capital gains realisation response

People choose when to realise capital gains, so a change in the tax on gains changes the gains realised. PolicyEngine UK models this realisation response for every reform: by default, realised gains respond at a retention-rate elasticity of 1.

How the response works

A reform scales each person’s realised gains by

G1=G0×(1−t11−t0)eG_1 = G_0 \times \left(\frac{1 - t_1}{1 - t_0}\right)^{e}

where G0G_0 is their gains before the response (capital_gains_before_response), t0t_0 and t1t_1 their marginal rates on gains under the baseline’s and the reform’s rules, and ee the elasticity. 1−t1 - t is the retention rate, the share of a marginal pound of gains kept after tax.

The default assumption

gov.simulation.capital_gains_responses.elasticity is 1.0, and gains qualifying for the relief take the same 1.0. This is PolicyEngine’s central assumption: the value Advani, Lonsdale and Summers (2024, pp. 36–37) use for the medium-term (five-year) effect of their reform package, with the range they consider, 0.5 to 2.0, as sensitivities.

The assumption has limits:

For a marginal rate on gains rising from 24% to 45%, the default scales realised gains by 0.55/0.76≈0.72370.55 / 0.76 \approx 0.7237. A marginal tax rate elasticity of -0.7, a different convention, gives (0.45/0.24)−0.7≈0.6440(0.45 / 0.24)^{-0.7} \approx 0.6440: the two are not interchangeable.

from policyengine_uk import Simulation

situation = {
    "people": {
        "person": {
            "age": {2026: 45},
            "employment_income": {2026: 100_000},
            "capital_gains": {2026: 200_000},
        }
    },
    "benunits": {"benunit": {"members": ["person"]}},
    "households": {"household": {"members": ["person"]}},
}
rates = {
    "gov.hmrc.cgt.higher_rate": {"2026": 0.45},
    "gov.hmrc.cgt.additional_rate": {"2026": 0.45},
}
reformed = Simulation(situation=situation, reform=rates)
# The marginal rate on the gains rises from 24% to 45%, so realised gains
# fall to 0.55 / 0.76 of £200,000, about £144,737.
print(reformed.calculate("capital_gains", 2026))

Large rate rises

With this response, the revenue from a single rate on gains peaks where the rate is 1/(1+e)1 / (1 + e): 50% at the default, 40% at 1.5, 33% at 2.0 and about 22% at the 3.6 of the OBR comparison below. So for a large rate rise the elasticity can decide whether revenue rises or falls. In the example above, the person’s capital gains tax rises by £16,502 at the default and by £6,777 at 1.5, and falls by £20,536 at 3.6. Official costings can have the other sign. As of 2024, HMRC’s ready reckoner had a 10 percentage point rise in the top rates raising over £5bn before behaviour, and losing £2bn a year by the third year once behaviour is counted (Advani, Lonsdale and Summers 2024, p. 33).

What the response moves

When there is no response

The response answers changes in the rules. A simulation whose inputs change while its rules don’t has no response. Marginal tax rates on earnings hold realised gains at the simulation’s own values: a pay rise is not a reform for the response to answer.

Rules changed after a simulation is built count too. A simulation built without a reform keeps a copy of itself, taken when it was built, as its baseline. If a modifier then edits its parameters, it responds against that copy.

Other assumptions

Every setting is an ordinary parameter, so a reform or scenario can change it for the years it covers. A reform dict’s year key, such as "2026", covers that fiscal year only; "2000-01-01.2100-12-31" covers every year.

RESPONSES = "gov.simulation.capital_gains_responses"
EVERY_YEAR = "2000-01-01.2100-12-31"

# Static: no response.
static = {
    f"{RESPONSES}.elasticity": {EVERY_YEAR: 0},
    f"{RESPONSES}.mtr_elasticity": {EVERY_YEAR: 0},
    f"{RESPONSES}.separate_badr_elasticity": {EVERY_YEAR: False},
}
# CenTax's sensitivities, or any other value.
lower = {f"{RESPONSES}.elasticity": {"2026": 0.5}}
upper = {f"{RESPONSES}.elasticity": {"2026": 2.0}}
# The OBR's elasticities: 3.6 for main-rate gains and badr_elasticity's 1.4
# for qualifying gains. This compares elasticities, not the OBR's full
# costing, which includes further effects.
obr = {
    f"{RESPONSES}.elasticity": {"2026": 3.6},
    f"{RESPONSES}.separate_badr_elasticity": {"2026": True},
}
static_costing = Simulation(situation=situation, reform={**rates, **static})

The response scales the gains already in the data, so it can’t change their level. A comparison in pounds with the OBR’s costing therefore mixes the gap between the elasticities with any gap in the level of gains and capital gains tax.

The marginal tax rate convention

gov.simulation.capital_gains_responses.mtr_elasticity scales gains by (t1/t0)e(t_1 / t_0)^{e} instead, and can’t be combined with a retention-rate elasticity in the same period.

legacy = {f"{RESPONSES}.mtr_elasticity": {"2026": -0.7}}
# Warns: the retention-rate default counts as zero in 2026.
with_warning = Simulation(situation=situation, reform={**rates, **legacy})
# The same results, without the warning.
explicit = {**legacy, f"{RESPONSES}.elasticity": {"2026": 0}}
without_warning = Simulation(situation=situation, reform={**rates, **explicit})

The assumption a simulation applies

capital_gains_response_assumption resolves a simulation’s settings for a year as the response does, after every reform and modifier applied so far. Its to_dict() gives the convention, the elasticities for main-rate and qualifying gains, the MTR elasticity, the separate relief switch, and whether each is the default, an override or set to zero for an MTR elasticity. It is there to record with a run’s results.

from policyengine_uk.utils.capital_gains import capital_gains_response_assumption

print(capital_gains_response_assumption(reformed, 2026).to_dict())

Moving from the static default

The elasticity used to default to 0, so CGT reforms were costed statically unless a caller set an elasticity.

Runtime

A reform that changes the rules measures two marginal rates on gains per simulation and year: one for the reform and one for its baseline. Each recalculates capital gains tax, and what it depends on, in branches of the simulation; on the enhanced FRS that adds about two seconds per reformed simulation and year. Simulations without a reform, baselines, reforms to the behavioural settings alone, and populations without gains skip the measurement.

The marginal rate on gains is the rise in capital gains tax per pound of marginal gains. Realised gains reach household net income only as market income and through that tax, so it is also the share of marginal gains net income does not keep; a test checks that no other variable reads realised gains.