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
1 change: 1 addition & 0 deletions changelog.d/cgt-additional-band-stacking.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Stack capital gains on top of taxable income when splitting them between the higher and additional CGT bands, so reforms to either rate no longer give every gainer the whole higher band, and set the annual exempt amount against the gains where each pound saves most tax.
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Gains are the top slice of income (TCGA 1992 s. 1H). The model splits gains
# above the basic rate band at the income tax higher rate limit so reforms can
# set an additional CGT rate; the law has none, and the two rates are equal.
# The split stacks gains on top of taxable income, as income tax does: the
# higher band left for gains is the higher rate limit less taxable income or
# the basic rate limit, whichever is higher.
#
# 2025-26: basic rate limit 37,700, higher rate limit 125,140, annual exempt
# amount 3,000, CGT rates 18% and 24%. allowances is set to 0 so taxable income
# equals net_income.

- name: Additional rate reform, income below the basic rate limit.
# Taxable income 20,000 leaves 17,700 of basic rate band. 150,000 - 3,000 =
# 147,000: 17,700 x 0.18 = 3,186; the higher band left is 125,140 - 37,700
# = 87,440, x 0.24 = 20,985.60; the rest, 41,860 x 0.45 = 18,837.
period: 2025
absolute_error_margin: 0.01
input:
gov.hmrc.cgt.additional_rate: 0.45
net_income: 20_000
allowances: 0
capital_gains: 150_000
output:
capital_gains_tax: 43_008.60

- name: Additional rate reform, income between the basic and higher rate limits.
# Taxable income 100,000 uses the basic rate band. The higher band left is
# 125,140 - 100,000 = 25,140, x 0.24 = 6,033.60; the rest of the 97,000,
# 71,860 x 0.45 = 32,337.
period: 2025
absolute_error_margin: 0.01
input:
gov.hmrc.cgt.additional_rate: 0.45
net_income: 100_000
allowances: 0
capital_gains: 100_000
output:
capital_gains_tax: 38_370.60

- name: Additional rate reform, income above the higher rate limit.
# Taxable income 200,000 is above 125,140, so all 47,000 of gains after the
# annual exempt amount are in the additional band: 47,000 x 0.45.
period: 2025
absolute_error_margin: 0.01
input:
gov.hmrc.cgt.additional_rate: 0.45
net_income: 200_000
allowances: 0
capital_gains: 50_000
output:
capital_gains_tax: 21_150

- name: Higher rate reform, income above the higher rate limit.
# No higher band is left, so a higher CGT rate of 45% does not apply:
# 47,000 x 0.24 at the unchanged additional rate.
period: 2025
absolute_error_margin: 0.01
input:
gov.hmrc.cgt.higher_rate: 0.45
net_income: 200_000
allowances: 0
capital_gains: 50_000
output:
capital_gains_tax: 11_280

- name: Higher rate reform, income between the basic and higher rate limits.
# 25,140 x 0.45 = 11,313 in the higher band left; 71,860 x 0.24 = 17,246.40.
period: 2025
absolute_error_margin: 0.01
input:
gov.hmrc.cgt.higher_rate: 0.45
net_income: 100_000
allowances: 0
capital_gains: 100_000
output:
capital_gains_tax: 28_559.40

- name: Residential property additional rate reform, income between the limits.
# The same split applies to each schedule: 25,140 x 0.24 = 6,033.60 and
# 71,860 x 0.45 = 32,337.
period: 2025
absolute_error_margin: 0.01
input:
gov.hmrc.cgt.residential_property.additional_rate: 0.45
net_income: 100_000
allowances: 0
capital_gains: 100_000
capital_gains_residential_property: 100_000
output:
capital_gains_tax: 38_370.60

- name: Carried interest additional rate reform, income between the limits.
# Carried interest is 32% in both bands from 2025-26: 25,140 x 0.32 =
# 8,044.80 in the higher band left and 71,860 x 0.45 = 32,337 above it.
period: 2025
absolute_error_margin: 0.01
input:
gov.hmrc.cgt.carried_interest.additional_rate: 0.45
net_income: 100_000
allowances: 0
capital_gains: 100_000
capital_gains_carried_interest: 100_000
output:
capital_gains_tax: 40_381.80

- name: Current law, income above the higher rate limit.
# The higher and additional rates are both 24%: 47,000 x 0.24.
period: 2025
absolute_error_margin: 0.01
input:
net_income: 200_000
allowances: 0
capital_gains: 50_000
output:
capital_gains_tax: 11_280

- name: Current law, income between the basic and higher rate limits.
# 97,000 x 0.24 whatever the split.
period: 2025
absolute_error_margin: 0.01
input:
net_income: 100_000
allowances: 0
capital_gains: 100_000
output:
capital_gains_tax: 23_280

- name: Additional rate reform, 200,000 of earnings and 50,000 of gains.
# The personal allowance is fully withdrawn at this income, so taxable
# income is 200,000, above the higher rate limit: 47,000 x 0.45.
period: 2025
absolute_error_margin: 0.01
input:
gov.hmrc.cgt.additional_rate: 0.45
employment_income: 200_000
capital_gains: 50_000
output:
capital_gains_tax: 21_150

- name: Additional rate reform, main and residential gains, exemption on main gains.
# Taxable income 200,000 is above the higher rate limit, so all gains are
# above the higher rate band. Main gains are at the reformed 45% additional
# rate and residential property gains at their unchanged 24%, so the 3,000
# annual exempt amount is most beneficial against main gains (TCGA 1992
# s. 1K(5)): 25,000 x 0.24 = 6,000 and (25,000 - 3,000) x 0.45 = 9,900.
period: 2025
absolute_error_margin: 0.01
input:
gov.hmrc.cgt.additional_rate: 0.45
age: 45
employment_income: 200_000
capital_gains: 50_000
capital_gains_residential_property: 25_000
output:
capital_gains_tax: 15_900

- name: Additional rate reform, exemption follows the rate as main gains cross the higher rate limit.
# The personal allowance is 12,570 - (124,000 - 100,000) / 2 = 570, so
# taxable income is 123,430 and 125,140 - 123,430 = 1,710 of higher band is
# left. Main gains fill it first: 1,710 of the 2,000 main gains at 24% and
# 290 at the reformed 45%. Carried interest is at 32% in either band. The
# exemption saves 45% on the top 290 of main gains, then 32% on carried
# interest rather than 24% on the rest of the main gains (TCGA 1992
# s. 1K(5)): 1,710 x 0.24 + (25,000 - 2,710) x 0.32 = 7,543.20, not 7,680
# from exempting all the main gains first.
period: 2025
absolute_error_margin: 0.01
input:
gov.hmrc.cgt.additional_rate: 0.45
gov.simulation.capital_gains_responses.elasticity: 0
age: 45
employment_income: 124_000
capital_gains: 27_000
capital_gains_carried_interest: 25_000
output:
capital_gains_tax: 7_543.20
97 changes: 91 additions & 6 deletions policyengine_uk/tests/test_capital_gains_tax_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ def legacy_capital_gains_tax(sim, year):
there is a difference in the new formula. Its income and allowance inputs
follow the current model (#2253 moved Gift Aid out of the allowances), so
it checks how gains are allocated to the schedules, not equivalence with
any earlier treatment of Gift Aid.
any earlier treatment of Gift Aid. Its higher band likewise follows the
current model, which stacks gains on top of taxable income, rather than
giving every gainer the whole band as the formula before #1858 did.
"""
hmrc = sim.tax_benefit_system.parameters(str(year)).gov.hmrc
cgt = hmrc.cgt
Expand Down Expand Up @@ -61,9 +63,11 @@ def get(name):
gains_less_aea - remaining_basic_rate_band, 0
)
higher_rate_limit = it.rates.uk.thresholds[2] + basic_rate_band_extension
# Gains stack on top of taxable income (TCGA 1992 s. 1H), so the higher
# band left for them starts at taxable income or the basic rate limit.
higher_rate_applicable_cg = min_(
higher_and_add_rate_applicable_cg,
higher_rate_limit - basic_rate_limit,
max_(higher_rate_limit - max_(taxable_income, basic_rate_limit), 0),
)
add_rate_applicable_cg = max_(
higher_and_add_rate_applicable_cg - higher_rate_applicable_cg, 0
Expand Down Expand Up @@ -327,17 +331,98 @@ def test_main_rate_reform_leaves_the_other_schedules_at_current_law(self):
baseline = {s: self.tax(s) for s in self.SCHEDULES}
reformed = {s: self.tax(s, self.MAIN_RATES_ONLY) for s in self.SCHEDULES}

# 97,000 above the AEA, all above the basic rate band: 87,440 at 40%
# and 9,560 at 45%.
assert reformed[None] == pytest.approx(39_278)
# Taxable income is 100,000 - 12,570 = 87,430, so the 97,000 above the
# AEA is all above the basic rate band. The higher band left on top of
# that income is 125,140 - 87,430 = 37,710: 37,710 at 40% and 59,290
# at 45%.
assert reformed[None] == pytest.approx(41_764.5)
for schedule in SCHEDULE_INPUTS:
assert reformed[schedule] == baseline[schedule], schedule

def test_setting_every_schedule_taxes_every_gain_alike(self):
reformed = {s: self.tax(s, self.EVERY_SCHEDULE) for s in self.SCHEDULES}

for schedule in self.SCHEDULES:
assert reformed[schedule] == pytest.approx(39_278), schedule
assert reformed[schedule] == pytest.approx(41_764.5), schedule


class TestExemptionAllocation:
"""The annual exempt amount goes where it saves most (TCGA 1992 s. 1K(5)).

Gains stack on taxable income, so a schedule's top gains can be at its
additional rate. The exemption is set for each person, pound by pound,
against the gains where it saves most, so it moves to another schedule
once a schedule's gains fall to a lower rate.
"""

YEAR = 2025
# The tax on gains held fixed shows where the exemption goes, so the
# realisation response is switched off.
ADDITIONAL_RATE_ONLY = {
"gov.hmrc.cgt.additional_rate": {"2025": 0.45},
"gov.simulation.capital_gains_responses.elasticity": {"2025": 0},
}

def test_exemption_follows_each_persons_top_rate(self):
people, benunits, households = {}, {}, {}
for i, income in enumerate((200_000, 100_000)):
people[f"person_{i}"] = {
"age": {self.YEAR: 45},
"employment_income": {self.YEAR: income},
"capital_gains": {self.YEAR: 50_000},
"capital_gains_residential_property": {self.YEAR: 25_000},
}
benunits[f"benunit_{i}"] = {"members": [f"person_{i}"]}
households[f"household_{i}"] = {"members": [f"person_{i}"]}
sim = Microsimulation(
situation={
"people": people,
"benunits": benunits,
"households": households,
},
scenario=Scenario(parameter_changes=self.ADDITIONAL_RATE_ONLY),
)
tax = sim.calculate("capital_gains_tax", self.YEAR).values

# Taxable income 200,000 is above the higher rate limit, so main
# gains are at the reformed 45% and residential gains at 24%. The
# 3,000 exemption goes against main gains: 25,000 x 0.24 + 22,000 x
# 0.45 = 15,900, not 22,000 x 0.24 + 25,000 x 0.45 = 16,530.
assert tax[0] == pytest.approx(15_900)
# Taxable income 87,430 leaves 37,710 of higher band. Main gains fill
# it first, so none reach the 45% rate and every gain is at 24%:
# 47,000 x 0.24, wherever the exemption goes.
assert tax[1] == pytest.approx(11_280)

def test_exemption_moves_on_when_the_marginal_rate_falls(self):
sim = Microsimulation(
situation={
"people": {
"person": {
"age": {self.YEAR: 45},
"employment_income": {self.YEAR: 124_000},
"capital_gains": {self.YEAR: 27_000},
"capital_gains_carried_interest": {self.YEAR: 25_000},
}
},
"benunits": {"benunit": {"members": ["person"]}},
"households": {
"household": {
"members": ["person"],
"region": {self.YEAR: "LONDON"},
}
},
},
scenario=Scenario(parameter_changes=self.ADDITIONAL_RATE_ONLY),
)
tax = sim.calculate("capital_gains_tax", self.YEAR).values

# Taxable income 123,430 leaves 1,710 of higher band, so 290 of the
# 2,000 main gains are at 45% and the rest at 24%; carried interest
# is at 32%. The exemption takes the 290 at 45%, then 2,710 of
# carried interest: 1,710 x 0.24 + 22,290 x 0.32 = 7,543.20, not
# 7,680 from exempting all the main gains first.
assert tax[0] == pytest.approx(7_543.20)


class TestScheduleParameters:
Expand Down
12 changes: 11 additions & 1 deletion policyengine_uk/tests/test_gift_aid.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,17 @@ def test_zero_gift_preserves_baseline_outputs_bit_for_bit():
3307.25,
31407.05078125,
],
"capital_gains_tax": [7663.7998046875, 8880, 8880, 8880, 8263.7998046875, 8880]
# The last row's 37,000 of gains is split between the higher band
# left on top of its 88,930 taxable income and the band above it, at
# equal rates; float32 rounds the two parts' sum a thousandth low.
"capital_gains_tax": [
7663.7998046875,
8880,
8880,
8880,
8263.7998046875,
8879.9990234375,
]
* 2,
}
for variable, values in expected.items():
Expand Down
Loading
Loading