Repository navigation
Stack gains on taxable income when splitting the higher and additional CGT bands - #2433
vahid-ahmadi wants to merge 6 commits into
Conversation
…l CGT bands The split of gains above the basic rate band gave every gainer the whole higher band (higher rate limit less basic rate limit), never subtracting taxable income already above the basic rate limit. Gains are the top slice of income (TCGA 1992 s. 1H), so the band left for them is the higher rate limit less taxable income or the basic rate limit, whichever is higher. Current law charges the higher and additional CGT rates alike, so only reforms that set them apart change; current-law results move by float32 rounding where gains straddle the new split point. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juaristi22
left a comment
There was a problem hiding this comment.
Reviewed 54fa62d64d28a976df2ac6cc3ce5e79a38d53a5d. The new remaining-band expression matches the declared third-band reform convention in isolation, but it exposes one material interaction with the annual exemption.
C1 — Allocate the exemption at the rates actually reached after stacking. At capital_gains_tax.py:75–77 (consumed at line 179), the higher-band capacity becomes zero for a person already above the higher limit. The unchanged exemption ordering at lines 141–151 still compares only each schedule’s higher_rate, so it can exempt the wrong gains.
Reproducer: one adult aged 45 in 2025, employment income £200,000, total gains £50,000, of which residential-property gains are £25,000; reform only gov.hmrc.cgt.additional_rate to 0.45. No intermediate tax, allowance or eligibility inputs are supplied, and behavioural elasticities retain their zero defaults.
Head charges £16,530 CGT. It assigns the £3,000 exemption to residential gains at 24%, while the remaining main gains are now charged at 45%. Assigning the exemption to main gains instead gives £15,900: £25,000 × 24% + £22,000 × 45%. The resulting £630 excess reaches household tax and net income. An independent same-head simulation using only the exact captured-base CGT formula gives £11,280: under the old whole-higher-band allocation all £47,000 taxable gains in this case remained at 24%, so exemption placement was immaterial. This demonstrates why the changed split exposes the interaction.
TCGA 1992 s.1K(5) preserves the most-beneficial exemption allocation, irrespective of the otherwise applicable rate. The third rate remains a hypothetical reform hook; this finding does not claim it is an enacted general CGT rate. Please preserve beneficial exemption allocation under the new stacked bands and add a mixed-schedule regression. The new YAML cases each use only one taxable gain schedule.
Validation: 243 repository checks and 11 independent diagnostics passed; the added mixed-gain exemption regression failed at £16,530 versus £15,900. One private microsimulation test was deselected. Independent boundary, Gift Aid/pension extension, four-schedule conservation and final household-income checks passed. A 384-household synthetic current-law comparison against the captured-base formula found only float32 rounding differences (maximum CGT £0.0078125, household tax £0.015625; final net income unchanged). Nine official source originals checked. The Axiom n/a explanation and required reproducers/changelog are adequate.
At capture and the mid-review check, four hosted Python shards were still running; other reported checks passed. Review status: COMPLETE. Review severity: REQUEST_CHANGES. Confirmed critical count: 1.
… gains reach With gains stacked on taxable income, a schedule's top gains can sit at its additional rate, but the exemption was still ordered by each schedule's higher rate. Under an additional-rate reform that could set the exemption against residential gains at 24% while main gains were charged at 45% (TCGA 1992 s. 1K(5) requires the most beneficial allocation). Order the exemption per person by the rate on each schedule's top gains before the exemption, which leaves current law unchanged because the higher and additional rates are equal. Adds the mixed-schedule regression from review: 200,000 earnings, 50,000 gains of which 25,000 residential, additional rate 45%: 15,900, not 16,530. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, María. On your review of
Nothing here waits on Vahid. 🤖 Generated with Claude Code |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's default realisation response (#2419) moves gains under a rate reform, so switch it off as the equalisation tests do; the test checks where the exemption goes on fixed gains. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Follow-up: the first CI run failed only on the new pytest, because main's default realisation response (#2419, merged 15 October) moved the gains under the reform (13,516.78 rather than 15,900). I merged 🤖 Generated with Claude Code |
juaristi22
left a comment
There was a problem hiding this comment.
Follow-up review of a6a975001f332a0017373b00d5ad416e7d19beea for C1. The original £630 example is fixed: it now gives £15,900. The same exemption-allocation requirement remains open at a rate boundary; see the single inline finding.
Independent validation: 56 passed, one reproducer failed, one private-data case deselected. All 53 selected existing checks passed. The identical-input current-law control passes at £7,520; the defect is under the supported hypothetical 45% additional-rate reform, not a claim that current law charges 45%. Nine cached official originals checked; no new downloads or private population run.
Review status: COMPLETE. Severity: REQUEST_CHANGES. Confirmed critical count: 1 (C1, still open).
| for position in range(len(names)): | ||
| index = order[position : position + 1] | ||
| amount = np.take_along_axis(stacked, index, axis=0)[0] | ||
| used = min_(amount, aea_left) |
There was a problem hiding this comment.
C1 — stop exemption allocation when the marginal rate changes. The fixed ranking exempts a whole schedule even after its top-rate slice is exhausted.
Reproducer: one adult age 45 in 2025, employment income £124,000, total capital_gains £27,000, including capital_gains_carried_interest £25,000; household region LONDON, and all other inputs default. Set gov.hmrc.cgt.additional_rate to 0.45 and gov.simulation.capital_gains_responses.elasticity to 0 for 2025. No tax, allowance or band intermediate is supplied.
Native PA is £570 and taxable income £123,430, leaving £1,710 of higher band. Only £290 of the £2,000 main gains reaches 45%. Head exempts all £2,000 main gains plus £1,000 carried gains and charges £7,680. Exempting £290 main plus £2,710 carried instead gives £7,543.20 (1710×24% + 22290×32%), £136.80 less. The one-time 45% ranking continues exempting main gains after their rate falls to 24%, below carried interest's 32%.
Please preserve the most-beneficial allocation across marginal tranches (the retained TCGA 1992 s.1K(5) principle) and add this boundary regression. The 45% band is a hypothetical supported reform; the original £630 case is resolved, but C1's underlying allocation contract is still open.
The one-off ranking of schedules by the rate their top gains reach kept exempting a schedule after its top-rate slice was used up. The exemption is now allocated in passes: each puts it against the schedule where a pound saves most tax at the margin, including any band that removing it frees for another schedule, and stops where a schedule's gains cross a band boundary (TCGA 1992 s. 1K(5)). Adds the 124,000 earnings / carried interest boundary case from review as a YAML and a pytest regression. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Aggregate impact on certified data: passBase Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 0 (thresholds in Notes
Moves flagged for reviewNone. Receipts by tax head
Spending by benefit and service
Caseloads
Government balance, each component counted once
Model totals
Household income totals
Mean household net income in each side's own deciles
Standard reform costings (change in gov_balance)
Household net income by decileMean change in household net income per household, and as a share of the decile's base income; households stay in the base run's deciles.
No change in any year: components the model counts twice; population; poverty rates; people in poverty; inequality; standard reforms: change in each reform's target. Checks failing on both base and head (1)
Counted-once checkEach of 84 components was raised by £1 per unit in 2026-27, and every head of its totals was read household by household: its own head must rise by exactly £1, no other tax head may move, no other benefit may rise, and the model's own total must move by what its heads moved. A head that breaks a rule is measured again twice: with every other head held at its baseline value, and with every other head held at its value after the first £1 while the component rises again. It fails where either still shows the response. 0 failed; 0 were skipped.
Provenance
Dataset: Receipts by tax head: head levels
Spending by benefit and service: head levels
Caseloads: head levels
Government balance, each component counted once: head levels
Model totals: head levels
Components the model counts twice: head levels
Household income totals: head levels
Population: head levels
Poverty rates: head levels
People in poverty: head levels
Inequality: head levels
Mean household net income in each side's own deciles: head levels
Standard reform costings (change in gov_balance): head levels
Standard reforms: change in each reform's target: head levels
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #2432
What and why
capital_gains_taxsplit gains above the basic rate band between the higher and additional CGT bands using the whole higher band (higher_rate_limit - basic_rate_limit, £87,440 in 2025-26) for every gainer. It never subtracted taxable income already above the basic rate limit, so a reform that sets the CGT higher and additional rates apart charged someone with £200,000 of income at the higher rate on their gains.Gains are the top slice of income (TCGA 1992 ss. 1H(3), 1I). In law CGT has only two rates, 18% and 24%, and the model's
additional_rateparameters exist only so reforms can mirror the income tax additional-rate band. For that split to mirror income tax, the gains above the basic rate band start at taxable income or the basic rate limit, whichever is higher:The main, residential property, carried interest and relief schedules share one higher-band loop, so the fix covers all four. No values are hard-coded.
£200,000 of earnings and £50,000 of gains, 2025-26:
Current law
The higher and additional CGT rates are equal at every date, so current-law CGT is unchanged apart from float32 rounding where gains now straddle the moved split point. Two frozen expectations were updated for this:
test_gift_aid.py::test_zero_gift_preserves_main_outputs_bit_for_bit: one row (£101,000 earnings, £40,000 gains) moves from 8880 to 8879.9990234375.test_capital_gains_tax_components.py::legacy_capital_gains_tax: the reference formula behind the bit-identity tests now uses the stacked higher band, so it still matches the new formula bit for bit.TestEqualisationReformshard-coded the buggy figure (£39,278: 87,440 at 40% and 9,560 at 45% on £87,430 of taxable income). Its expectation is now £41,764.50: 37,710 at 40% and 59,290 at 45%.Interaction with #2419
#2419 (default CGT realisation response) answers the rate on the last pound of gains, so the wrong band also drives the wrong response there. Its docs example (£100,000 earnings, £200,000 gains, higher rate raised to 45%) quotes tax rising from £47,280 to £65,642 with no response. After this fix it rises to about £55,199: 37,710 at 45% and 159,290 at 24%. Its other example, with both rates at 45%, does not change.
Test plan
capital_gains_tax_rate_bands.yaml(10 cases, hand-computed): income below the basic rate limit, between the limits and above the higher rate limit under an additional-rate reform; higher-rate reforms; residential property and carried interest additional-rate reforms; current law; and the earnings-based example above. 7 fail on main and all pass here. The 3 that pass on main are the two current-law cases and the below-basic-limit case, where the whole band is correctly left.policyengine-core test policyengine_uk/tests/policy: 3,444 passed.pytest test_capital_gains_tax_components.py test_capital_gains_responses.py test_capital_gains_projection.py test_gift_aid.pyand the other tests that touch CGT: pass.pytest -n 8 policyengine_uk/tests: 5,966 passed, 102 skipped, 1 xfailed.ruff format --checkandruff check: clean.changelog.d/Reproducer
Reproducer: policyengine_uk/tests/policy/baseline/gov/hmrc/capital_gains_tax/capital_gains_tax_rate_bands.yaml, policyengine_uk/tests/test_capital_gains_tax_components.py::TestEqualisationReforms::test_main_rate_reform_leaves_the_other_schedules_at_current_law
axiom: n/a: the additional CGT band is a model-only reform hook; TCGA 1992 ss. 1H-1I as enacted have two rates (18% and 24%) and the higher and additional parameters are equal at every date, so no encoded rule or current-law result changes
🤖 Generated with Claude Code