Add HMRC capital gains targets to constrain the gains distribution#440
Conversation
The Enhanced FRS imputes capital gains but nothing constrains them to administrative totals. On the 2026 stock weights the microdata carries 1.29m CGT taxpayers holding £112bn of gains (£87,158 mean) against HMRC's 378k, £65.9bn and ~£174,000. Adds hmrc/capital_gains_total and hmrc/cgt_taxpayers, both gated on the annual exempt amount read from PolicyEngine's parameter tree for the simulation year, so they match HMRC's CGT-taxpayer concept rather than "anyone with a positive gain". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The existing tests drive the compute functions through a _dummy_ctx stub, which would keep passing if pe_person or household_from_person were renamed on the real _SimContext. Add coverage that binds to the real integration points and pins the properties the module docstring claims: - build a real _SimContext against a Microsimulation and run both compute functions (marked slow, skips when the enhanced FRS is absent), plus a dataset-free introspection guard on _SimContext so a rename fails even when the data test skips - assert both targets survive registry year filtering across the projection range and keep custom_compute, which is Field(exclude=True) - pin the AEA gate boundary: gains exactly at the AEA do not count (strict >), one pound above does, and the boundary moves with the year - pin that the amount gate and the count gate select the same people - pin that losses contribute nothing, and that gated gains can exceed ungated household-mapped gains because losses are dropped - pin the flat taxpayer count as a deliberate, documented choice Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Flagging one methodological choice in this PR that I don't think should be settled without a second opinion, since it's a data-sourcing question rather than an implementation one. The taxpayer count is held flat at 378,000 across 2024-2029, but the gate applied in those years is not the gate that produced 378,000. HMRC's 378,000 is the 2023-24 outturn, realised under a £6,000 annual exempt amount. From 2024-25 the AEA is £3,000, and Halving the allowance pulls materially more people into CGT, so the true count under the £3,000 regime should be well above 378,000. The OBR's own receipts step up sharply over the same window (£13.7bn in 2024-25 to £21.8bn in 2025-26 in the March 2026 EFO, reflecting both the rate rises and the allowance cut). Targeting a flat 378,000 therefore asks the calibration to squeeze the taxpayer count below the level the policy implies, and because the gains total is uprated while the count is not, the implied mean gain drifts upward year on year to absorb the difference (roughly £174k in 2024 to £203k by 2029). The PR documents this as a caveat and I think the reasoning given — that a fixed £6,000 gate "would not describe any year's actual policy" — is right as far as it goes. The issue is that the current combination isn't obviously better: it pairs a correct, year-tracking threshold with a count that belongs to a different threshold. Three options, roughly in order of preference:
I don't have a strong enough view on UK target sourcing to pick unilaterally — deferring to whoever owns this area. Happy to implement whichever way it goes. For context on why the count matters at all: the motivation for these targets is that the stock weights carry 1.29m CGT taxpayers against HMRC's 378k, and the count target is the thing doing most of the work in fixing that. Getting its level wrong in the projection years is a smaller error than having no target, but it is the one number here I'd want a second pair of eyes on. |
CI measured 5.18m weighted taxpayers against the band's 3m ceiling. The count depends on which dataset build the test runs against - a fresh in-CI build and the published projection differ by millions - so the band failed for reasons unrelated to what the test exists to check. The structural and relative assertions around it still cover the integration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Problem
The Enhanced FRS imputes capital gains, but nothing in the calibration constrains how those gains are distributed. Measured on the stock weights for 2026 (reproduced from a real
Microsimulationvia_SimContext):3.4x the taxpayer count with a mean gain about half the administrative figure — gains spread across far too many households in amounts individually too small.
This is distributional error, not a level shift, so a revenue target does not fix it. #439 restores the silently-dropped
obr/capital_gains_taxrevenue target, which constrains what CGT raises; these targets constrain how gains are distributed. The two are complementary, not duplicative.In a CGT reform analysis, adding these two targets moved the share of people affected from 5.0% to 1.65% — a factor of three.
What this adds
policyengine_uk_data/targets/sources/hmrc_cgt.py(auto-discovered bytargets/registry.py, no registration needed):hmrc/capital_gains_total— £65.9bn,variable="capital_gains",Unit.GBPhmrc/cgt_taxpayers— 378,000,is_count=TrueBoth use a
custom_compute. The default count path (build_loss_matrix._compute_simple_count→ctx.pe_count) counts people withcapital_gains > 0, which is not HMRC's concept: HMRC counts CGT taxpayers, i.e. people whose gains exceed the annual exempt amount (AEA).The amount target is gated on the same AEA condition, and the module docstring records why: HMRC's £65.9bn is the gains of CGT-liable taxpayers, not all gains realised. Gating only the count would target a mean gain that is a ratio of two different populations, and would let the optimiser satisfy the amount with sub-threshold gains HMRC never counts.
The AEA is read from PolicyEngine's own parameter tree (
gov.hmrc.cgt.annual_exempt_amount) for the simulation year — verified to return £12,300 (2022), £6,000 (2023), £3,000 (2024 onward) — with an explicit module-level mapping as fallback. A single hard-coded threshold would silently change meaning across years.Also adds the HMRC CGT statistics URL to the
hmrc:section ofsources.yamland achangelog.d/fragment.Year handling
get_all_targets(year=...)filters on exact year membership, andbuild_loss_matrix._resolve_valueonly falls back to a past year within 3 years, with no uprating for non-VOA sources. Supplying the 2023-24 outturn year alone would therefore constrain 2024-2027 with a flat nominal value and nothing thereafter — materially limiting usefulness for the projection years the calibration actually runs.So: base year 2024 (tax year 2023-24 mapped to calendar 2024, following
hmrc_spi._SPI_YEAR), with the gains amount projected to 2029 using the repo's existingutils/uprating.uprate_valueswith PolicyEngine'scapital_gainsfactors: £65.9bn (2024) → £69.7bn (2026) → £76.6bn (2029).The taxpayer count is held flat across 2024-2029. There is no administrative basis in this repo for forecasting CGT taxpayer numbers, and the uprating factors are nominal-income indices that should not be applied to a headcount.
Documented caveat (also in the module docstring): the 2023-24 outturn was realised under a £6,000 AEA, while the gate in later years uses the £3,000 AEA then in force. The targets are approximate for projection years; a fixed £6,000 gate would be wrong in a less transparent way, since it would describe no year's actual policy.
Verification
policyengine_uk_data/tests/test_hmrc_cgt_targets.py— 6 tests, all passing (plustest_target_registry.py, 21 total):get_all_targets(year=2026)Beyond the stubbed-context unit tests, both
custom_computecallables were run against a realMicrosimulationfor 2026 through the actual_SimContext, returning correctly-shaped household arrays (53,508) and reproducing the headline table above exactly (1,285,101 taxpayers, £112.0bn, £87,158 mean).🤖 Generated with Claude Code