Skip to content
Open
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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# 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
18 changes: 12 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 @@ -317,17 +321,19 @@ 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 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 @@ -341,7 +341,17 @@ def test_zero_gift_preserves_main_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
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ def formula(person, period, parameters):
basic_rate_limit = it.rates.uk.thresholds[1] + basic_rate_band_extension
remaining_basic_rate_band = max_(basic_rate_limit - taxable_income, 0)
higher_rate_limit = it.rates.uk.thresholds[2] + basic_rate_band_extension
higher_rate_band = higher_rate_limit - basic_rate_limit
# Gains are the top slice of income (TCGA 1992 s. 1H), so the gains
# above the basic rate band start at the basic rate limit or at
# taxable income, whichever is higher. Only the rest of the band up to
# the higher rate limit is left for them.
remaining_higher_rate_band = max_(
higher_rate_limit - max_(taxable_income, basic_rate_limit), 0
)

# The schedule inputs describe gains before any realisation response.
# Relief gains move by their own response, which has its own
Expand Down Expand Up @@ -161,15 +167,16 @@ def component(amount):
after_aea[name], band_left
)

# Gains above the basic rate band are split at the higher rate limit
# so reforms can charge an additional rate; in law the two rates are
# equal. Relief gains are the lowest part of the gains here too.
# Gains above the basic rate band are split at the higher rate limit,
# stacked on top of taxable income, so reforms can charge an
# additional rate; in law the two rates are equal. Relief gains are
# the lowest part of the gains here too.
higher_order = ["badr"] + sorted(
rates,
key=lambda name: -(rates[name].additional_rate - rates[name].higher_rate),
)
inside_higher, above_higher = {}, {}
band_left = higher_rate_band
band_left = remaining_higher_rate_band
for name in higher_order:
inside_higher[name], above_higher[name], band_left = _take(
above_basic[name], band_left
Expand Down
Loading