diff --git a/changelog.d/cgt-additional-band-stacking.fixed.md b/changelog.d/cgt-additional-band-stacking.fixed.md new file mode 100644 index 000000000..c14c40830 --- /dev/null +++ b/changelog.d/cgt-additional-band-stacking.fixed.md @@ -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. diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/capital_gains_tax/capital_gains_tax_rate_bands.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/capital_gains_tax/capital_gains_tax_rate_bands.yaml new file mode 100644 index 000000000..9bc38acd2 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/capital_gains_tax/capital_gains_tax_rate_bands.yaml @@ -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 diff --git a/policyengine_uk/tests/test_capital_gains_tax_components.py b/policyengine_uk/tests/test_capital_gains_tax_components.py index d79fc42b0..a74387ba6 100644 --- a/policyengine_uk/tests/test_capital_gains_tax_components.py +++ b/policyengine_uk/tests/test_capital_gains_tax_components.py @@ -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 @@ -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 @@ -327,9 +331,11 @@ 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 @@ -337,7 +343,86 @@ 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: diff --git a/policyengine_uk/tests/test_gift_aid.py b/policyengine_uk/tests/test_gift_aid.py index 2ee5dfa4b..34d7bb182 100644 --- a/policyengine_uk/tests/test_gift_aid.py +++ b/policyengine_uk/tests/test_gift_aid.py @@ -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(): diff --git a/policyengine_uk/variables/gov/hmrc/capital_gains_tax/capital_gains_tax.py b/policyengine_uk/variables/gov/hmrc/capital_gains_tax/capital_gains_tax.py index 31a414c0f..3a6d27647 100644 --- a/policyengine_uk/variables/gov/hmrc/capital_gains_tax/capital_gains_tax.py +++ b/policyengine_uk/variables/gov/hmrc/capital_gains_tax/capital_gains_tax.py @@ -1,3 +1,5 @@ +import numpy as np + from policyengine_uk.model_api import * from policyengine_uk.utils.capital_gains import badr_gains_before_response @@ -15,6 +17,12 @@ def _take(amount, band_left): return inside, above, band_left - inside +# Each pass of the exemption allocation ends where a schedule's gains cross a +# band boundary, so a handful is enough; four schedules in three bands give at +# most twelve boundaries. +_MAX_EXEMPTION_PASSES = 16 + + class capital_gains_tax(Variable): label = "capital gains tax" documentation = ( @@ -29,8 +37,8 @@ class capital_gains_tax(Variable): "other two in proportion with the rest of the gains. All three are " "clipped so that together they never exceed capital_gains. The annual " "exempt amount is set " - "against the schedule with the highest top rate first (TCGA 1992 " - "s. 1K(5)); the unused basic rate band goes to relief gains first, as " + "against the gains where it saves most tax, pound by pound " + "(TCGA 1992 s. 1K(5)); the unused basic rate band goes to relief gains first, as " "s. 1I(4)-(6) requires, and then to the schedule with the widest gap " "between its higher and basic rates (s. 1I(7)-(9)). Gains above the " "basic rate band are split at the income tax higher rate limit only so " @@ -68,7 +76,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 @@ -132,19 +146,6 @@ def component(amount): "main": main, "badr": badr, } - top_rate = {name: rates[name].higher_rate for name in rates} - top_rate["badr"] = cgt.badr.rate - - # Annual exempt amount, TCGA 1992 s. 1K(5): deducted in the way most - # beneficial to the taxpayer, which is against the highest top rate - # first. Relief gains carry the lowest rate and come last. - after_aea = {} - aea_left = aea - for name in sorted(amounts, key=lambda name: -top_rate[name]): - used = min_(amounts[name], aea_left) - after_aea[name] = amounts[name] - used - aea_left = aea_left - used - # Basic rate band. Relief gains take it first, TCGA 1992 s. 1I(4)-(6). # The taxpayer then allocates the rest, s. 1I(7)-(9), which saves most # on the schedule with the widest higher-minus-basic gap. Carried @@ -154,26 +155,118 @@ def component(amount): rates, key=lambda name: -(rates[name].higher_rate - rates[name].basic_rate), ) - inside_basic, above_basic = {}, {} - band_left = remaining_basic_rate_band - for name in basic_order: - inside_basic[name], above_basic[name], band_left = _take( - 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 - for name in higher_order: - inside_higher[name], above_higher[name], band_left = _take( - above_basic[name], band_left - ) + + def allocate_bands(schedule_amounts): + inside_basic, above_basic = {}, {} + band_left = remaining_basic_rate_band + for name in basic_order: + inside_basic[name], above_basic[name], band_left = _take( + schedule_amounts[name], band_left + ) + inside_higher, above_higher = {}, {} + 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 + ) + return inside_basic, inside_higher, above_higher + + # Annual exempt amount, TCGA 1992 s. 1K(5): deducted in the way most + # beneficial to the taxpayer. Each pound saves the tax it would have + # borne at the margin, which depends on the schedule it comes off and, + # where removing it frees band for another schedule, on that schedule + # too. So the exemption is set greedily: in each pass, against the + # schedule where a pound saves most, up to the point where any + # schedule's gains cross a band boundary and the saving can change. + # Ties keep declaration order. Under current law, where the higher and + # additional rates are equal, this gives the same allocation as + # ranking the schedules by rate, apart from float rounding where the + # exemption is split across schedules. + names = list(amounts) + flat_rate = {"badr": cgt.badr.rate} + + def slices(schedule_amounts): + inside_basic, inside_higher, above_higher = allocate_bands(schedule_amounts) + result = [] + for name in names: + if name in flat_rate: + band_rates = [flat_rate[name]] * 3 + else: + band_rates = [ + rates[name].basic_rate, + rates[name].higher_rate, + rates[name].additional_rate, + ] + parts = [inside_basic[name], inside_higher[name], above_higher[name]] + result += list(zip(parts, band_rates)) + return result + + left = {name: amounts[name].astype(float) for name in names} + used = {name: np.zeros_like(left[name]) for name in names} + aea_left = np.full_like(left["main"], aea) + step = 1e-6 + for _ in range(_MAX_EXEMPTION_PASSES): + if not np.any(aea_left > 0): + break + base = slices(left) + savings, room = [], [] + for name in names: + nudged = dict(left) + nudged[name] = left[name] - step + saving = np.zeros_like(aea_left) + distance = np.full_like(aea_left, np.inf) + for (part, rate), (moved, _) in zip(base, slices(nudged)): + slope = np.round((moved - part) / step) + saving = saving - slope * rate + shrinking = (slope < 0) & (part > 0) + distance = where( + shrinking, + min_(distance, part / where(shrinking, -slope, 1)), + distance, + ) + active = (left[name] > 0) & (aea_left > 0) + savings.append(where(active, np.round(saving, 9), -np.inf)) + room.append(min_(left[name], distance)) + savings = np.stack(savings) + best = np.argmax(savings, axis=0) + can_take = np.max(savings, axis=0) > -np.inf + for i, name in enumerate(names): + taken = where(can_take & (best == i), min_(aea_left, room[i]), 0) + left[name] = left[name] - taken + used[name] = used[name] + taken + aea_left = aea_left - taken + # Any exemption left after the passes, which only happens when gains + # cross more band boundaries than there are passes, goes in + # declaration order. + for name in names: + taken = min_(aea_left, left[name]) + used[name] = used[name] + taken + aea_left = aea_left - taken + # Where the whole exemption went against one schedule, deduct it in a + # single step so that the passes add no rounding of their own. + schedules_used = sum((used[name] > 0).astype(int) for name in names) + after_aea = { + name: where( + schedules_used <= 1, + where( + used[name] > 0, + amounts[name] - min_(amounts[name], aea), + amounts[name], + ), + amounts[name] - used[name], + ).astype(amounts[name].dtype) + for name in names + } + + inside_basic, inside_higher, above_higher = allocate_bands(after_aea) def schedule_tax(name): schedule = rates[name]