Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/cgt-rate-effective-dates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- Corrected the effective date of the capital gains tax rate rise to 30 October 2024, as Finance Act 2025 s.7 provides, from 6 April 2025.
- Added opt-in day-weighted annualisation for parameters that change part-way through a fiscal year, and applied it to the capital gains rates, so 2024-25 reflects both rate schedules rather than only the one in force at the start of the year.
- Annual Scenario parameter changes now cover the UK fiscal year they name, so an override applies at full strength instead of blending with prior law over its final five months.
5 changes: 3 additions & 2 deletions policyengine_uk/parameters/gov/hmrc/cgt/additional_rate.yaml
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
description: Capital gains tax rate on additional rate taxpayers. This parameter is under active development and reforms including it should not be cited.
description: Capital gains tax rate on additional rate taxpayers. The rates changed part-way through 2024-25, so the value for that year is the day-weighted average of the rates in force before and after 30 October 2024.
values:
2015-01-01:
value: 0.2
reference:
- title: Finance Act 2016 s. 83
href: https://www.legislation.gov.uk/ukpga/2016/24/section/83
2025-04-06:
2024-10-30:
value: 0.24
reference:
- title: Finance Act 2025 s. 7
href: https://www.legislation.gov.uk/ukpga/2025/8/section/7
metadata:
fiscal_year_blend: true
unit: /1
label: Capital Gains Tax additional rate
reference:
Expand Down
5 changes: 3 additions & 2 deletions policyengine_uk/parameters/gov/hmrc/cgt/basic_rate.yaml
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
description: Capital gains tax rate on basic rate taxpayers. This parameter is under active development and reforms including it should not be cited.
description: Capital gains tax rate on basic rate taxpayers. The rates changed part-way through 2024-25, so the value for that year is the day-weighted average of the rates in force before and after 30 October 2024.
values:
2015-01-01:
value: 0.1
reference:
- title: Finance Act 2016 s. 83
href: https://www.legislation.gov.uk/ukpga/2016/24/section/83
2025-04-06:
2024-10-30:
value: 0.18
reference:
- title: Finance Act 2025 s. 7
href: https://www.legislation.gov.uk/ukpga/2025/8/section/7
metadata:
fiscal_year_blend: true
unit: /1
label: Capital Gains Tax basic rate
reference:
Expand Down
5 changes: 3 additions & 2 deletions policyengine_uk/parameters/gov/hmrc/cgt/higher_rate.yaml
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
description: Capital gains tax rate on higher rate taxpayers. This parameter is under active development and reforms including it should not be cited.
description: Capital gains tax rate on higher rate taxpayers. The rates changed part-way through 2024-25, so the value for that year is the day-weighted average of the rates in force before and after 30 October 2024.
values:
2015-01-01:
value: 0.2
reference:
- title: Finance Act 2016 s. 83
href: https://www.legislation.gov.uk/ukpga/2016/24/section/83
2025-04-06:
2024-10-30:
value: 0.24
reference:
- title: Finance Act 2025 s. 7
href: https://www.legislation.gov.uk/ukpga/2025/8/section/7
metadata:
fiscal_year_blend: true
unit: /1
label: Capital Gains Tax higher rate
reference:
Expand Down
8 changes: 6 additions & 2 deletions policyengine_uk/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from policyengine_core.tools.hugging_face import download_huggingface_dataset
from policyengine_core.tracers import FullTracer, SimpleTracer

from policyengine_uk.utils.parameters import uk_fiscal_year_period

# PolicyEngine UK imports
from policyengine_uk.data.dataset_schema import (
UKMultiYearDataset,
Expand Down Expand Up @@ -236,10 +238,12 @@ def apply_parameter_changes(self, changes: dict):
canonical_parameter
)
if isinstance(changes[parameter], dict):
# Time-period specific changes
# Time-period specific changes. A bare year names the UK
# fiscal year, so the override covers 6 April to 5 April and
# survives fiscal-year conversion at full strength.
for time_period in changes[parameter]:
p.update(
period=time_period,
period=uk_fiscal_year_period(time_period),
value=changes[parameter][time_period],
)
else:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,77 +1,85 @@
# Rate mechanics are tested in 2025-26, where one set of rates applies to the
# whole year. The rates rose part-way through 2024-25, so that year carries a
# day-weighted average of the rates before and after 30 October 2024, tested
# separately at the end of this file.

- name: No capital gains
period: 2024
period: 2025
input:
adjusted_net_income: 0
capital_gains: 0
output:
capital_gains_tax: 0

- name: Capital gains below annual exempt amount
period: 2024
period: 2025
input:
adjusted_net_income: 0
capital_gains: 2_000
output:
capital_gains_tax: 0

- name: Capital gains just above annual exempt amount
period: 2024
period: 2025
input:
adjusted_net_income: 0
capital_gains: 3_500
output:
capital_gains_tax: 50
capital_gains_tax: 90

- name: Basic rate capital gains tax
period: 2024
period: 2025
input:
adjusted_net_income: 20_000
capital_gains: 23_000
output:
capital_gains_tax: 2_000
capital_gains_tax: 3_600

- name: Higher rate capital gains tax
period: 2024
period: 2025
absolute_error_margin: 0.01
input:
adjusted_net_income: 50_000
capital_gains: 50_000
output:
capital_gains_tax: 9_373
capital_gains_tax: 11_263.80

- name: Additional rate capital gains tax
period: 2024
period: 2025
input:
adjusted_net_income: 150_000
capital_gains: 100_000
output:
capital_gains_tax: 19_400
capital_gains_tax: 23_280

- name: Mixed rates capital gains tax
period: 2024
period: 2025
absolute_error_margin: 0.01
input:
adjusted_net_income: 35_000
capital_gains: 40_000
output:
capital_gains_tax: 5_873
capital_gains_tax: 7_963.80

- name: High capital gains, low income
period: 2024
period: 2025
input:
adjusted_net_income: 10_000
capital_gains: 200_000
output:
capital_gains_tax: 35_630
capital_gains_tax: 45_018

- name: Very high capital gains and income
period: 2024
period: 2025
absolute_error_margin: 0.1
input:
adjusted_net_income: 500_000
capital_gains: 1_000_000
output:
capital_gains_tax: 199_400
capital_gains_tax: 239_280

- name: Edge case, capital gains exactly equal to annual exempt amount
period: 2024
period: 2025
input:
adjusted_net_income: 0
capital_gains: 3_000
Expand Down Expand Up @@ -132,3 +140,26 @@
capital_gains: 32_942.86
output:
capital_gains_tax: 5_389.71

# Finance Act 2025 s.7 raised the main rates for disposals from 30 October
# 2024, part-way through 2024-25. An annual model cannot place a disposal
# before or after that date, so the year takes the day-weighted average of the
# two schedules: 207 days at 10% and 20%, then 158 days at 18% and 24%.

- name: Case 16, the split 2024-25 year taxes basic rate gains at the blended rate.
period: 2024
absolute_error_margin: 0.01
input:
adjusted_net_income: 20_000
capital_gains: 23_000
output:
capital_gains_tax: 2_692.60

- name: Case 17, the split 2024-25 year taxes higher rate gains at the blended rate.
period: 2024
absolute_error_margin: 0.01
input:
adjusted_net_income: 150_000
capital_gains: 100_000
output:
capital_gains_tax: 21_079.56
115 changes: 115 additions & 0 deletions policyengine_uk/tests/test_fiscal_year_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,121 @@ def test_december_2025_returns_fiscal_year_2025_value(self, uk_system):
assert child_limit == 2


class TestMidYearRateChanges:
"""Tests for changes taking effect part-way through a fiscal year.

Sampling one date per year drops any change taking effect after it.
Parameters carrying `fiscal_year_blend: true` are day-weighted across
the year instead.
"""

# Fiscal year 2024-25 runs 6 April 2024 to 5 April 2025: 207 days at the
# rates in force before 30 October 2024, then 158 days at the rates after.
DAYS_BEFORE = 207
DAYS_AFTER = 158
TOTAL_DAYS = DAYS_BEFORE + DAYS_AFTER

def blended(self, before, after):
return (before * self.DAYS_BEFORE + after * self.DAYS_AFTER) / self.TOTAL_DAYS

@pytest.mark.parametrize(
"rate_name, before, after",
[("basic_rate", 0.10, 0.18), ("higher_rate", 0.20, 0.24)],
)
def test_cgt_rates_blend_across_the_split_year(self, rate_name, before, after):
"""2024-25 carries both statutory rates, so the year takes their average."""
system = CountryTaxBenefitSystem()
rates = {
year: getattr(
system.get_parameters_at_instant(str(year)).gov.hmrc.cgt, rate_name
)
for year in (2023, 2024, 2025)
}

assert rates[2023] == pytest.approx(before)
assert rates[2024] == pytest.approx(self.blended(before, after))
assert rates[2025] == pytest.approx(after)

def test_cgt_rate_rise_takes_effect_from_the_statutory_date(self):
"""Finance Act 2025 s.7 applies to disposals from 30 October 2024.

Read before fiscal year conversion, which is what the statutory dates
feed. Regression test for dating the rise to 6 April 2025, which moved
a change the legislation makes in 2024-25 into the following year.
"""
import policyengine_uk
from pathlib import Path
from policyengine_core.parameters import ParameterNode

parameters = ParameterNode(
directory_path=str(Path(policyengine_uk.__file__).parent / "parameters")
)
higher_rate = parameters.gov.hmrc.cgt.higher_rate

assert higher_rate("2024-10-29") == pytest.approx(0.20)
assert higher_rate("2024-10-30") == pytest.approx(0.24)

def test_blending_is_opt_in(self, uk_system):
"""Parameters without the flag keep one sampled value for the year.

National Insurance rates fell part-way through 2022-23, and the year
still takes the rate in force at its start rather than an average.
Whether to blend is a decision per parameter: a transaction is taxed
at the rate in force on its date, not at a yearly average.
"""
parameters = uk_system.get_parameters_at_instant("2022")
employee_rates = parameters.gov.hmrc.national_insurance.class_1.rates.employee

assert employee_rates.main == pytest.approx(0.1325)


class TestScenarioOverrides:
"""Annual Scenario overrides name UK fiscal years.

Written as calendar years, an override's final five months fell back to
prior law and fiscal-year conversion blended the two: an override of 0.5
on a blended CGT rate resolved to 0.42. The override now covers 6 April
to 5 April.
"""

def _reformed(self):
from policyengine_uk import Microsimulation
from policyengine_uk.model_api import Scenario

return Microsimulation(
situation={
"people": {
"person": {
"age": {2025: 45},
"adjusted_net_income": {2025: 20_000},
"capital_gains": {2025: 23_000},
}
},
"benunits": {"benunit": {"members": ["person"]}},
"households": {"household": {"members": ["person"]}},
},
scenario=Scenario(
parameter_changes={"gov.hmrc.cgt.basic_rate": {"2025": 0.5}}
),
)

def test_override_applies_at_full_strength(self):
sim = self._reformed()
rate = sim.tax_benefit_system.parameters.gov.hmrc.cgt.basic_rate

assert rate("2025") == pytest.approx(0.5)
cgt = sim.calculate("capital_gains_tax", 2025).values[0]
assert cgt == pytest.approx(10_000.0)

def test_override_leaves_other_years_on_law(self):
"""The split 2024-25 blend and the 2026 reversion both survive."""
sim = self._reformed()
rate = sim.tax_benefit_system.parameters.gov.hmrc.cgt.basic_rate

assert rate("2024") == pytest.approx((0.10 * 207 + 0.18 * 158) / 365)
assert rate("2026") == pytest.approx(0.18)


class TestFiscalYearCoverage:
"""Tests to verify fiscal year conversion covers all needed years."""

Expand Down
Loading