Repository navigation
Charge BADR, residential property and carried interest gains at their own CGT schedules - #1861
Conversation
…t schedules gov.hmrc.cgt.badr.rate (10%, 14% from 2025-04-06, 18% from 2026-04-06) and .lifetime_limit (£10m, £1m from 2020-03-11); residential_property and carried_interest basic, higher and additional rates (18/28, residential higher 24% from 2024-04-06; carried interest a flat 32% from 2025-04-06). None of these changed on 30 October 2024, so none is blended across the fiscal year. The additional rates equal the higher rates in law and exist so reforms can mirror the income tax additional-rate band, as the main additional_rate does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… own CGT schedules Three person-level inputs, capital_gains_badr, capital_gains_residential_property and capital_gains_carried_interest, are components of capital_gains measured before any realisation response. capital_gains_tax scales them by realised over pre-response gains, clips them against the total, and charges the remainder at the main rates. The annual exempt amount goes to the schedule with the highest top rate first (TCGA 1992 s. 1K(5)); relief gains take the unused basic rate band before other gains (s. 1I(4)-(6)) and the rest goes to the schedule with the widest higher-minus-basic gap (s. 1I(7)-(9)). With the inputs at zero the result is bit for bit what the single-schedule formula produced. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
YAML cases for each schedule and their interaction, a grid test that proves capital_gains_tax is bit-identical to the legacy formula when the schedule inputs are zero, response and marginal-rate plumbing tests, and parameter date tests. Baseline-policy and growth-factor docs and the changelog fragment. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
huggingface_hub 1.32.0 (17 September 2026) stores cached downloads as read-only blobs in a shared blob store. UKSingleYearDataset and UKMultiYearDataset opened the file with pd.HDFStore's default append mode, which PyTables checks for write access, so every dataset-backed simulation on a fresh CI runner failed with "exists but it can not be written". Reading a dataset needs no write access; open it with mode="r". Regression tests save a small dataset, make the file read-only and load it through both dataset classes and the Simulation constructor. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Review at The formula is right, and I checked it against the law and by hand rather than against its own tests. Every new parameter matches what I read on legislation.gov.uk: BADR 10% → 14% from 6 April 2025 → 18% from 6 April 2026 under FA 2025 s.8, the £1m lifetime limit from 11 March 2020 under FA 2020 Sch 3, residential 18/28 → 18/24 from 6 April 2024 under F(No.2)A 2024 s.6, carried interest 18/28 → flat 32% from 6 April 2025 under FA 2025 s.12 which also inserts s.1I(A1) to take carry out of the band rules, and the 2024-25 main-rate blend at 207 of 365 days. Three hand cases reproduce the engine to the penny: 2025 with £30,000 income and £100,000 all residential gives £22,063.80; 2025 with £60,000 income and £2m of which £1.5m BADR gives £379,280 with the BADR clipped to £1m and the excess on the main schedule; and the 2024 blend-year mixed case with £20,000 carried, £20,000 residential and £10,000 main gives £9,010.10 with the AEA taken off carry and the basic-rate band going carry → main → residential by gap, exactly as item 5 says. The clipping order and the "component without a total is ignored" rule are what the code does, since every 1. Should-fix — the body does not say what this does to the standard equalisation reform, and it matters for a live consumer. With three schedules carrying their own rates, a reform that sets only 2. Question — item 6's higher-band quirk has a reform cost worth a number. Relief gains consume the notional higher band first, so under a 0.40/0.45 reform a BADR-plus-main taxpayer can have up to the full £74,870 band of main gains pushed into the additional band, about £3,700 per affected person at 2025 thresholds. The body lists the quirk under follow-ups; a sentence with the magnitude would let a reader decide whether the follow-up is urgent. Nits. The body says the bit-identity test runs 2018–2030; it runs eight specific years. The lifetime limit dated 11 March 2020 is the correct commencement, but the 30 April sample gives £10m for all of 2019-20, with no engine consequence. The residential and carried-interest files cite FA 2016 s.83 for the 2015 value, which is right by construction as the body says. Could not verify. The offline microsimulation totals and the "+£0.3bn from residential" figure, which need a spine that carries the input, and optimality under arbitrary reforms beyond current-law rates. Approve once 1 is written down. Nothing else needs a code change. On the split-out question: the loader fix is one line and stands on its own; I would split it so #1860 and the rest go green today. |
…ests
A reform that changes only gov.hmrc.cgt.{basic,higher,additional}_rate no
longer reaches residential property, carried interest or relief gains. The
changelog fragment says so and what a full equalisation must set (the other
two schedules' rates, and badr.lifetime_limit to zero, since the relief rate
is band-independent); two tests pin both behaviours at 2025 values. Also
corrects the baseline-policy note that dated the main-rate rise to 2025-26:
Finance Act 2025 s. 7 applies from 30 October 2024 and the model day-weights
2024-25.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Thanks for checking it against the law and by hand. Addressed at 896ea98, on top of b1e5305; the body is updated to match. 1. Should-fix — written down. The changelog fragment now says that a reform changing only 2. Higher-band ordering — the number. At most the notional band width times the rate gap. The band the formula uses is Nits. Item 11 names the eight years. Item 2 notes that the 11 March 2020 commencement is sampled at 30 April, so 2019-20 reads £10m all year, with no engine consequence. Split-out. The standalone fix already exists: #1847 (Max, 3 September, CI green, no review yet) makes the same two-line change with its own tests and fragment. Merging that first is what gets #1860 and the other open PRs green; once it is on main I rebase this branch and drop b1e5305 with its tests and fragment. Until then the commit stays here so this PR's CI runs the dataset-backed tests, including the token-gated identity test, which passed on b1e5305. |
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Second pass at 896ea98e (Claude Code, high effort; worktree checkout, the response commit read against the dispositions — the fragment, the baseline doc and 53 lines of tests; from the worktree against the engine venv: the components suite 40 passed with the token-gated identity test skipped, the whole tests/policy suite 1,192 passed; CI seven of seven green).
Everything closed as described.
1. Verified. The changelog fragment now says a reform changing only the three main rates no longer reaches residential property, carried interest or relief gains, and states what a full equalisation must set: both other schedules' three rates and badr.lifetime_limit to zero because the relief rate is flat. Body item 7 carries the numbers and the uk-equalising-cgt consequence. TestEqualisationReforms pins both behaviours — the main-rate reform leaving the other schedules at current law, and the full dict taxing every gain alike — at 2025 values, alongside the fiscal-year values and the no-blend check.
2. Verified, and the number is better than mine. The band the formula uses is the taxable-income width, £87,440, not the £74,870 gross figure I quoted, so the ceiling is £4,372 per affected person at 2025 thresholds, and your measured case (£1m relief plus £100,000 main, no income, £183,650 against £179,278) shows it. Now in item 6 beside the follow-up.
Nits verified. Item 11 names the eight years; item 2 notes the 30 April sample of the 11 March 2020 commencement; model-baseline.md dates the main-rate rise to disposals from 30 October 2024 with the 2024-25 day-weighting.
On the split-out. Agreed on the sequence: #1847 carries the same two-line loader fix with its own tests and fragment, so merging that first unblocks #1860 and the rest, and this branch drops b1e5305 on rebase. Keeping the commit here until then so the dataset-backed tests run is the right call.
Approving. The consequence for rate-only reforms is now where a consumer will read it, and pinned so it cannot drift.
Describe the BADR, residential property and carried interest components (#1861) and the retention-rate elasticity, replace the per-decile description with the Microcosm stages that fit gains to HMRC Table 3 by size of gain and taxable income, and list what is left for #817 and #818. Type the changelog fragment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe the BADR, residential property and carried interest components (#1861) and the retention-rate elasticity, replace the per-decile description with the Microcosm stages that fit gains to HMRC Table 3 by size of gain and taxable income, and list what is left for #817 and #818. Type the changelog fragment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #1858
capital_gains_taxnow charges four schedules instead of one: gains qualifying for Business Asset Disposal Relief or Investors' Relief (capital_gains_badr), residential property gains (capital_gains_residential_property), carried interest (capital_gains_carried_interest) and the rest ofcapital_gainsat the main rates. The three inputs default to zero, and at zero the liability is bit for bit what it was.Modelling choices and assumptions (please check each)
Each item gives the choice, its basis, the alternative rejected, and where to check it.
capital_gains, not additions to it. Each is a GBP component of the person's pre-response total; the remainder is charged at the main rates. A component entered without a matching total is ignored (tax 0). Inputs that together exceed the total are clipped in the fixed order BADR, residential, carried interest, and the main remainder is then 0. This matches the microcosm export, where the residential column is the whole gain of a flagged person (UK CGT distribution: age/region conditioning, banded and residential targets, asset types (#725, #467) microcosm#939). Check: the "exceed capital_gains", "without capital_gains" and "net loss" cases incapital_gains_tax_components.yaml; the clipping block incapital_gains_tax.py.badr/lifetime_limit.yamldescription.marginal_tax_rate_on_capital_gains, which adds £1,000 tocapital_gains, becomes a share-weighted marginal rate, so a residential-only taxpayer now responds to a residential-rate reform. Rejected alternative: fixed components with the response absorbed by the main remainder, which discards the response for anyone whose gains are all on one of the new schedules and blinds the elasticity to reforms on those schedules. Check:TestResponsePlumbingintest_capital_gains_tax_components.py; YAML tests setcapital_gainsdirectly, so their ratio is exactly 1 (the two residential 2024 cases agree).higher_rate_limit − basic_rate_limitas the old formula did, including the pre-existing quirk that gains receive the whole higher band whatever the taxpayer's income. Relief gains consume that band first ("the lowest part of the gains"), then the others by additional-minus-higher gap, zero in law. Kept for bit-identity and becauseadditional_rateis a live reform lever; the quirk is listed under follow-ups. Cost of the relief-first ordering under a reform withadditional_rateabovehigher_rate: at most the notional band width times the rate gap, £87,440 × 5pp = £4,372 per affected person at 2025 thresholds (the band runs from £37,700 to £125,140 of taxable income). Measured on this head under 0.20/0.40/0.45 with £1m of relief gains and £100,000 of main gains and no income: £183,650, against £179,278 if the main gains took the higher band. In law the two rates are equal and the ordering costs nothing. Check: the identity test;higher_rate_bandin the formula.additional_rateequal to their higher rate, and a rate-only reform no longer reaches them. A reform that sets onlygov.hmrc.cgt.{basic,higher,additional}_rateleaves residential property, carried interest and relief gains at current law. Measured on this head for 2025, income £100,000, gains £100,000 on one schedule, under 0.20/0.40/0.45 on the main rates only: main-rate gains go from £23,280 to £39,278; residential (£23,280), relief (£13,580) and carried-interest (£31,040) gains do not move. Under the old single schedule all four went to £39,278. Today no published dataset carries the inputs, so nothing changes for a consumer such as PolicyEngine/uk-equalising-cgt; once microcosm Replaceif hasattr(person.simulation, "dataset"):withif person.simulation.dataset is not None:#939 puts residential gains on the spine (about £12bn of the base) a rate-only reform understates the yield of equalisation. To tax every gain at income tax rates a reform must also setresidential_property.{basic,higher,additional}_rateandcarried_interest.{basic,higher,additional}_rate, and setbadr.lifetime_limitto zero, becausebadr.rateis flat and cannot express three bands; with that dict all four schedules pay £39,278. The changelog fragment says the same. Check:TestEqualisationReforms(both behaviours pinned); theadditional_rateparameter descriptions.TestScheduleParametersasserts exactly 0.24 for residential in 2024 and nofiscal_year_blendflag.TestZeroInputsAreBitIdenticalruns the old formula, copied operation for operation, against the new variable on 150 single-person households per year for eight years (2018, 2020, 2022, 2023, 2024, 2025, 2026, 2030) with gift aid and pension variants, and under an equalised-rates reform and a fractional AEA, comparing the float32 bit patterns. The only theoretical divergence is the sign of a zero under a negative rate parameter.capital_gains_residential_propertyexists on the microcosm spine (UK CGT distribution: age/region conditioning, banded and residential targets, asset types (#725, #467) microcosm#939); BADR and carried interest read as zero until HMRC Tables 4 and 9 are imputed (Add HMRC CGT Tables 7–9 source packages chronicle#273 has Table 9). This PR does not close the +11.4% liability gap on its own; the residential input alone moves 2024 liability up by roughly £0.3bn.Also in this PR: dataset files are opened read-only
The first CI run failed on 29 dataset-backed tests, none of them CGT-specific, with
PermissionError: file ... exists but it can not be writtenfrom PyTables. huggingface_hub 1.32.0 (released 17 September 2026, the only cache-relevant version change between the green main run on 16 September and the red runs on 18 September; #1860's run failed the same way) stores cached downloads as read-only blobs in its new cross-repo blob store.UKSingleYearDatasetandUKMultiYearDatasetopened the file withpd.HDFStore's default append mode, which PyTables checks for write access. Reading a dataset needs no write access, so both loaders now open withmode="r"; three tests intest_simulation_dataset_sources.pysave a small dataset, make the file read-only and load it through both classes and theSimulationconstructor (all three fail without the fix). Reproduced locally by loading a read-only copy ofenhanced_frs_2024_25.h5. The same two-line change already exists as #1847 (opened 3 September, CI green, no review yet), with its own tests and fragment; that is the standalone fix to merge first so #1860 and the other open PRs go green. Once #1847 is on main this branch rebases and drops its loader commit, tests and fragment.Found in passing, not fixed here:
UKMultiYearDataset.validate_file_pathnever recognises a multi-year file (it looks forperson/2025-style keys in h5py's top-level listing), so a multi-year file given by path falls through to the single-year loader. Separate issue to follow.Verification
uv run policyengine-core test policyengine_uk/tests/policy/baseline/gov/hmrc/capital_gains_tax -c policyengine_uk: 17 existing cases and 16 new cases pass.uv run pytest policyengine_uk/tests/test_capital_gains_tax_components.py policyengine_uk/tests/test_capital_gains_responses.py policyengine_uk/tests/test_fiscal_year_parameters.py policyengine_uk/tests/code_health policyengine_uk/tests/test_parameter_descriptions.py: pass (the dataset identity test skips locally without a token; CI runs it).policyengine-core test policyengine_uk/tests/policy): 1,192 passed. Full pytest suite (pytest policyengine_uk/tests/): 257 passed, 45 skipped (the microsimulation-marked tests skip without a dataset token; CI runs them).enhanced_frs_2024_25.h5(113,617 persons; the dataset carries none of the three inputs):capital_gains_taxis bit-identical to the legacy formula for 2024, 2025, 2026 and 2027, with weighted totals of £10.86bn, £12.77bn, £13.21bn and £13.70bn on both sides.uv run pytest policyengine_uk/tests/test_capital_gains_tax_components.py: 40 passed, 1 skipped locally (the dataset identity test needs the token; CI runs it), including the twoTestEqualisationReformscases.uv run pytest policyengine_uk/tests/test_simulation_dataset_sources.py: 10 passed (3 new read-only cases; 3 failed with the loader fix reverted).ruff format --check .clean;ruff checkclean on the touched files.docs/book/policy/model-baseline.mdnow dates the main-rate rise to disposals from 30 October 2024 with the 2024-25 day-weighting (it said 2025-26), and gains the BADR, residential and carried-interest notes.make documentation(jupyter-book is not installed in this environment; the docs changes are prose edits todocs/book/policy/model-baseline.mdanddocs/book/assumptions/growthfactors.md). No populace-725 staging artifact was available locally, so the residential-only effect on the microcosm dataset is not measured here.Follow-ups
utils/parameters.py::fiscal_year_average); the higher-band quirk in item 6; per-schedule marginal rates; a "lifetime limit already used" input; carried interest into income tax from 2026-27.capital_gains_asset_type, and carried interest from Table 9 (Add HMRC CGT Tables 7–9 source packages chronicle#273). Model income shifting between capital gains and income #1805 (income shifting) reuses the same inputs.🤖 Generated with Claude Code