Skip to content

CGT alignment: three clean parameters, and an elasticity that has to be cited (#97) - #102

Open
vahid-ahmadi wants to merge 1 commit into
mainfrom
uk/cgt-reform-lane
Open

CGT alignment: three clean parameters, and an elasticity that has to be cited (#97)#102
vahid-ahmadi wants to merge 1 commit into
mainfrom
uk/cgt-reform-lane

Conversation

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Closes #97. Branches from main.

Aligning capital gains tax rates with income tax rates is the Budget's flagship broad-based option with the "big three" ruled out by the manifesto pledge. PolicyEngine-UK expresses it cleanly — three parameters, all resolved against a real 2.89.2, with the baselines recorded from the engine rather than from reporting:

parameter engine, 2026 aligned
gov.hmrc.cgt.basic_rate 0.18 0.20
gov.hmrc.cgt.higher_rate 0.24 0.40
gov.hmrc.cgt.additional_rate 0.24 0.45

The finding that reshaped the lane

I opened #97 expecting to say "PE-UK has a CGT behavioural response, so this lane can score both legs". Checking the engine says something more specific:

gov.simulation.capital_gains_responses.elasticity is zero from 2000-01-01, and capital_gains_behavioural_response short-circuits to zero when it is.

So a default PolicyEngine-UK score of this measure is static. The engine does not hand you a behavioural number — it hands you the machinery to compute one once you supply an elasticity. (It also requires simulation.baseline to be attached, so it exists only in a reform run.)

That makes the elasticity an assumption this repo makes, and the single parameter the whole comparison turns on — published estimates of this measure disagree mostly about the realisations response, not the arithmetic.

So the lane sweeps, and citations are enforced

It does not pick a value and publish a number. The validator refuses to mark a sweep point publishable until it carries a named citation, because an elasticity without one is an opinion with a decimal point.

publishable legs: ['static']
held pending citation: ['central', 'high_response']

The static leg is publishable because it asserts no behaviour. The other two are held with a note saying what they need — including which elasticity definition the citation must state: semi-elasticity vs elasticity, and with respect to which rate. Those are different numbers wearing the same word, and picking the wrong one silently rescales the whole response.

Comparability, enforced not narrated

An OBR costing of this measure will be post-behavioural. The static leg must never be presented against it — that is #67's finding one level up, and the spec says so in a field the validator checks. The static-vs-behavioural difference is published as its own quantity on the behavioural_elasticity axis rather than folded into a residual.

The validator also refuses a reform that restates the baseline (it would score zero and read as agreement), and warns loudly if the engine ever switches the response on by default — that would invalidate the static/behavioural framing this lane rests on.

Unexecuted, and says so

The compute needs the managed environment and the certified populace-uk bundle, exactly like #51 and #66; a local Microsimulation() refuses without an explicit dataset. Everything below the engine boundary — reform values, paths, baselines, the sweep contract — is verified and tested here.

Verification

Suite 273 passed, ruff format --check clean, validator green with and without the engine.

Reviewers

@MaxGhenis @DTrim99 — the call worth testing: I've made the citation requirement a hard gate rather than a convention, which means this lane currently cannot publish a behavioural number at all. That is deliberate, but it does mean the lane ships less than it could. If you'd rather it carried a documented default elasticity with the caveat attached, that is a reasonable alternative and I'd take the argument.

…asticity (#97)

Aligning capital gains tax rates with income tax rates is the Budget's
flagship broad-based option with the "big three" ruled out, and
PolicyEngine-UK expresses it cleanly: 18/24/24 today to 20/40/45, three
parameters, all resolved against a real 2.89.2 with the baselines
recorded from the engine rather than from reporting.

The finding that reshaped the lane, verified rather than assumed:
gov.simulation.capital_gains_responses.elasticity is ZERO from
2000-01-01, and capital_gains_behavioural_response short-circuits to
zero when it is. So a default PolicyEngine-UK score of this measure is
STATIC. The engine does not hand you a behavioural number — it hands you
the machinery to compute one once you supply an elasticity.

That makes the elasticity an assumption THIS REPO makes, and the single
parameter the whole comparison turns on: published estimates of this
measure disagree mostly about the realisations response, not the
arithmetic. So the lane does not pick a value and publish a number. It
sweeps, and the validator refuses to mark a sweep point publishable
until it carries a named citation — an elasticity without one is an
opinion with a decimal point. Today exactly one leg is publishable, the
static one, because it asserts no behaviour; the central and
high-response legs are held with a note saying what they need, including
which elasticity DEFINITION the citation must state (semi-elasticity vs
elasticity, and with respect to which rate — different numbers wearing
the same word).

Comparability is stated and enforced in the spec: an OBR costing of this
measure will be post-behavioural, and the static leg must never be
presented against it. That is #67's finding one level up, and the
static-vs-behavioural difference is published as its own quantity on the
behavioural_elasticity axis rather than folded into a residual.

The validator also refuses a reform that restates the baseline (it would
score zero and read as agreement), and warns loudly if the engine ever
switches the response on by default, because that would invalidate the
static/behavioural framing this lane rests on.

Unexecuted, and says so: the compute needs the managed environment and
the certified populace-uk bundle, exactly like #51 and #66 — a local
Microsimulation() refuses without an explicit dataset. Everything below
the engine boundary is verified and tested here.

Suite 284 passed, ruff format clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
@vahid-ahmadi

Copy link
Copy Markdown
Contributor Author

Review request — @MaxGhenis @DTrim99.

This one is part of a batch; the whole queue, with a suggested merge order and what is blocked on whom, is in #104 so you can triage in one place rather than PR by PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Capital gains tax reform scoring (mode 2): the Budget's flagship measure, and the one where the behavioural response IS the disagreement

1 participant