From 5aaf6daca2b2c5fdfb0312f84ecb8e79db2d36d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:56:25 +0000 Subject: [PATCH 01/11] Model the ISA annual and cash subscription limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add gov.hmrc.isa.annual_subscription_limit (£20,000, reg 4ZA(1)), gov.hmrc.isa.cash.subscription_limit (no limit of its own until 6 April 2027, then £12,000 under reg 4ZA(1ZA)) and gov.hmrc.isa.cash.full_limit_age (65 at the end of the tax year, from the date of birth). Add per-person cash_isa_subscriptions and stocks_and_shares_isa_subscriptions inputs, isa_subscriptions (with lifetime_isa_payments), the person's cash_isa_subscription_limit, and cash_isa_subscriptions_above_limit and isa_subscriptions_above_limit. Inputs are not capped, so data collected under a higher limit shows what a lower one leaves out. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2710.added.md | 1 + .../hmrc/isa/annual_subscription_limit.yaml | 15 +++ .../parameters/gov/hmrc/isa/cash/README.md | 1 + .../gov/hmrc/isa/cash/full_limit_age.yaml | 15 +++ .../gov/hmrc/isa/cash/subscription_limit.yaml | 17 +++ policyengine_uk/programs.yaml | 11 ++ .../gov/hmrc/isa/isa_subscription_limits.yaml | 106 ++++++++++++++++++ .../hmrc/isa/cash_isa_subscription_limit.py | 30 +++++ .../gov/hmrc/isa/cash_isa_subscriptions.py | 24 ++++ .../isa/cash_isa_subscriptions_above_limit.py | 24 ++++ .../gov/hmrc/isa/isa_subscriptions.py | 28 +++++ .../hmrc/isa/isa_subscriptions_above_limit.py | 23 ++++ .../stocks_and_shares_isa_subscriptions.py | 20 ++++ 13 files changed, 315 insertions(+) create mode 100644 changelog.d/2710.added.md create mode 100644 policyengine_uk/parameters/gov/hmrc/isa/annual_subscription_limit.yaml create mode 100644 policyengine_uk/parameters/gov/hmrc/isa/cash/README.md create mode 100644 policyengine_uk/parameters/gov/hmrc/isa/cash/full_limit_age.yaml create mode 100644 policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/hmrc/isa/isa_subscription_limits.yaml create mode 100644 policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscription_limit.py create mode 100644 policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py create mode 100644 policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions_above_limit.py create mode 100644 policyengine_uk/variables/gov/hmrc/isa/isa_subscriptions.py create mode 100644 policyengine_uk/variables/gov/hmrc/isa/isa_subscriptions_above_limit.py create mode 100644 policyengine_uk/variables/gov/hmrc/isa/stocks_and_shares_isa_subscriptions.py diff --git a/changelog.d/2710.added.md b/changelog.d/2710.added.md new file mode 100644 index 0000000000..6a001b437a --- /dev/null +++ b/changelog.d/2710.added.md @@ -0,0 +1 @@ +Added the ISA annual subscription limit (£20,000) and the £12,000 cash ISA limit for people under 65 at the end of the tax year from 6 April 2027 (ISA Regulations 1998 reg 4ZA, as amended by SI 2026/1018), per-person cash and stocks-and-shares ISA subscription inputs, and `cash_isa_subscriptions_above_limit` and `isa_subscriptions_above_limit`, the subscriptions a limit leaves out. diff --git a/policyengine_uk/parameters/gov/hmrc/isa/annual_subscription_limit.yaml b/policyengine_uk/parameters/gov/hmrc/isa/annual_subscription_limit.yaml new file mode 100644 index 0000000000..837dca9144 --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/isa/annual_subscription_limit.yaml @@ -0,0 +1,15 @@ +description: > + Limit on a person's total subscriptions in the tax year to ISAs other than + Junior ISAs: cash, stocks-and-shares, innovative finance and new Lifetime ISA + payments together. +values: + 2017-04-06: 20000 +metadata: + label: ISA annual subscription limit + period: year + unit: currency-GBP + reference: + - title: Individual Savings Account Regulations 1998 regulation 4ZA(1) + href: https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA + - title: Individual Savings Account (Amendment) Regulations 2017 regulation 5 + href: https://www.legislation.gov.uk/uksi/2017/186/regulation/5/made diff --git a/policyengine_uk/parameters/gov/hmrc/isa/cash/README.md b/policyengine_uk/parameters/gov/hmrc/isa/cash/README.md new file mode 100644 index 0000000000..c5a3692533 --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/isa/cash/README.md @@ -0,0 +1 @@ +# Cash ISA diff --git a/policyengine_uk/parameters/gov/hmrc/isa/cash/full_limit_age.yaml b/policyengine_uk/parameters/gov/hmrc/isa/cash/full_limit_age.yaml new file mode 100644 index 0000000000..a95714886f --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/isa/cash/full_limit_age.yaml @@ -0,0 +1,15 @@ +description: > + Age a person must have reached by the end of the tax year (5 April) for the + cash ISA subscription limit not to apply. Their cash subscriptions then count + only towards the annual subscription limit. +values: + 2027-04-06: 65 +metadata: + label: Cash ISA full limit age + period: year + unit: year + reference: + - title: Individual Savings Account Regulations 1998 regulation 4ZA(1ZA) + href: https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA + - title: Individual Savings Account (Amendment) (No. 2) Regulations 2026 regulation 6 + href: https://www.legislation.gov.uk/uksi/2026/1018/regulation/6/made diff --git a/policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml b/policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml new file mode 100644 index 0000000000..db52ba77ab --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml @@ -0,0 +1,17 @@ +description: > + Limit on a person's subscriptions in the tax year to cash ISAs while they are + under the cash ISA full limit age at the end of the year. Cash has had no + limit of its own since 1 July 2014, so until 6 April 2027 cash subscriptions + count only towards the annual subscription limit. +values: + 2017-04-06: .inf + 2027-04-06: 12000 +metadata: + label: Cash ISA subscription limit + period: year + unit: currency-GBP + reference: + - title: Individual Savings Account Regulations 1998 regulation 4ZA(1ZA) + href: https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA + - title: Individual Savings Account (Amendment) (No. 2) Regulations 2026 regulations 1 and 6 + href: https://www.legislation.gov.uk/uksi/2026/1018/regulation/6/made diff --git a/policyengine_uk/programs.yaml b/policyengine_uk/programs.yaml index 3803f6da39..d51e08d885 100644 --- a/policyengine_uk/programs.yaml +++ b/policyengine_uk/programs.yaml @@ -29,6 +29,17 @@ programs: verified_start_year: 2027 notes: Separate final manager charge at the savings basic rate from 6 April 2027. Requires the cash-interest component as an input; subscription-limit compliance is not modelled. + - id: isa_subscription_limits + name: ISA subscription limits + full_name: Individual Savings Account annual and cash subscription limits + category: Savings + agency: HMRC + status: partial + coverage: UK + variable: isa_subscriptions_above_limit + verified_start_year: 2017 + notes: The annual limit (gov.hmrc.isa.annual_subscription_limit) and, from 6 April 2027, the cash limit for people under 65 at the end of the tax year (gov.hmrc.isa.cash). Reports subscriptions above each limit from per-person subscription inputs. Innovative finance and Junior ISAs are not modelled. + - id: income_tax name: Income tax full_name: Income Tax diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/isa_subscription_limits.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/isa_subscription_limits.yaml new file mode 100644 index 0000000000..ffc150cf54 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/isa_subscription_limits.yaml @@ -0,0 +1,106 @@ +# ISA Regulations 1998 reg 4ZA(1) (GBP 20,000 annual limit) and reg 4ZA(1ZA), +# inserted by SI 2026/1018 reg 6 from 6 April 2027: GBP 12,000 for cash while +# a person is 64 or under at the end of the year (5 April). + +- name: Cash has no limit of its own in fiscal 2026-27 + period: 2026 + input: + age: 40 + cash_isa_subscriptions: 20_000 + output: + cash_isa_subscription_limit: 20_000 + cash_isa_subscriptions_above_limit: 0 + isa_subscriptions_above_limit: 0 + +- name: The GBP 12,000 cash limit applies from fiscal 2027-28 + period: 2027 + input: + age: 40 + cash_isa_subscriptions: 20_000 + output: + cash_isa_subscription_limit: 12_000 + cash_isa_subscriptions_above_limit: 8_000 + isa_subscriptions_above_limit: 0 + +- name: Cash exactly at the limit has no excess and a pound above it has one + period: 2027 + input: + age: [40, 40] + cash_isa_subscriptions: [12_000, 12_001] + output: + cash_isa_subscriptions_above_limit: [0, 1] + +- name: People aged 65 at the end of the year keep the annual limit for cash + period: 2027 + input: + # Born 6 April 1963 and 1962 (months_since_last_birthday defaults to 6): + # 64 and 65 at the end of 5 April 2028. + age: [64, 65] + cash_isa_subscriptions: [20_000, 20_000] + output: + cash_isa_subscription_limit: [12_000, 20_000] + cash_isa_subscriptions_above_limit: [8_000, 0] + +- name: A 65th birthday on the last day of the year counts and one a day later does not + period: 2027 + input: + # Both are 64 on 6 October 2027. One turns 65 on 5 April 2028, the last + # day of the tax year; the other on 6 April 2028, the first day of the next. + age: [64, 64] + date_of_birth: [19630405, 19630406] + cash_isa_subscriptions: [20_000, 20_000] + output: + cash_isa_subscription_limit: [20_000, 12_000] + cash_isa_subscriptions_above_limit: [0, 8_000] + +- name: Cash for people aged 65 or over is limited only by the annual limit + period: 2027 + input: + age: [70, 70] + cash_isa_subscriptions: [20_000, 20_001] + output: + cash_isa_subscriptions_above_limit: [0, 1] + isa_subscriptions_above_limit: [0, 1] + +- name: Cash, stocks and shares and Lifetime ISA payments share the annual limit + period: 2027 + input: + age: [30, 30] + cash_isa_subscriptions: [12_000, 12_000] + stocks_and_shares_isa_subscriptions: [4_000, 4_001] + lifetime_isa_payments: [4_000, 4_000] + output: + isa_subscriptions: [20_000, 20_001] + isa_subscriptions_above_limit: [0, 1] + cash_isa_subscriptions_above_limit: [0, 0] + +- name: Cash above both limits counts in both excesses + period: 2027 + input: + age: 40 + cash_isa_subscriptions: 25_000 + output: + cash_isa_subscriptions_above_limit: 13_000 + isa_subscriptions_above_limit: 5_000 + +- name: Restoring a GBP 20,000 cash limit removes the cash excess + period: 2027 + input: + gov.hmrc.isa.cash.subscription_limit: 20_000 + age: 40 + cash_isa_subscriptions: 20_000 + output: + cash_isa_subscription_limit: 20_000 + cash_isa_subscriptions_above_limit: 0 + +- name: A GBP 15,000 annual limit caps cash at every age and leaves the rest above it + period: 2027 + input: + gov.hmrc.isa.annual_subscription_limit: 15_000 + age: [40, 70] + cash_isa_subscriptions: [12_000, 20_000] + stocks_and_shares_isa_subscriptions: [8_000, 0] + output: + cash_isa_subscription_limit: [12_000, 15_000] + cash_isa_subscriptions_above_limit: [0, 5_000] + isa_subscriptions_above_limit: [5_000, 5_000] diff --git a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscription_limit.py b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscription_limit.py new file mode 100644 index 0000000000..2e26fba286 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscription_limit.py @@ -0,0 +1,30 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.dates import birth_day + + +class cash_isa_subscription_limit(Variable): + value_type = float + entity = Person + label = "Cash ISA subscription limit for the person" + documentation = ( + "The most the person can subscribe to cash ISAs in the tax year: the " + "cash ISA subscription limit while they are under the full limit age " + "at the end of the year, and otherwise the annual subscription limit, " + "which also caps the cash limit. The age at the end of the year (5 " + "April) comes from the date of birth, so a person aged 64 on 6 October " + "whose 65th birthday falls by 5 April has the annual limit." + ) + definition_period = YEAR + unit = GBP + reference = "https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA" + + def formula(person, period, parameters): + p = parameters(period).gov.hmrc.isa + # Reg 4ZA(1ZA) applies to a person who is 64 or under at the end of + # the year, 5 April. A person attains an age at the start of their + # birthday, so they have reached the full-limit age by then if they + # were born before 6 April that many years before the year ends. + born_before = (period.start.year + 1 - p.cash.full_limit_age) * 10_000 + 406 + reached_full_limit_age = birth_day(person, period) < born_before + cash_limit = where(reached_full_limit_age, np.inf, p.cash.subscription_limit) + return min_(cash_limit, p.annual_subscription_limit) diff --git a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py new file mode 100644 index 0000000000..da0e49dbbd --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py @@ -0,0 +1,24 @@ +from policyengine_uk.model_api import * + + +class cash_isa_subscriptions(Variable): + value_type = float + entity = Person + label = "Cash ISA subscriptions" + documentation = ( + "New money paid into cash ISAs in the tax year, which counts towards " + "the cash and annual subscription limits. Exclude transfers between " + "ISAs, interest and growth, and replacements of cash withdrawn from a " + "flexible ISA earlier in the same tax year, which do not count towards " + "the limits. The model compares this amount with the limits in force " + "and does not cap it: data collected under a higher limit, such as " + "before the 6 April 2027 cash limit, can exceed a later limit, and " + "cash_isa_subscriptions_above_limit reports the part that cannot be " + "paid in. Amounts are not uprated, as the limits are fixed in cash " + "terms." + ) + definition_period = YEAR + unit = GBP + quantity_type = FLOW + default_value = 0 + reference = "https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA" diff --git a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions_above_limit.py b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions_above_limit.py new file mode 100644 index 0000000000..8d9856a0d0 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions_above_limit.py @@ -0,0 +1,24 @@ +from policyengine_uk.model_api import * + + +class cash_isa_subscriptions_above_limit(Variable): + value_type = float + entity = Person + label = "Cash ISA subscriptions above the cash limit" + documentation = ( + "Cash ISA subscriptions above the cash limit that applies to the " + "person. It is zero for data that met the limits in force when it was " + "collected; under a lower limit it is cash that cannot go into a cash " + "ISA, and the model does not say where it goes. Cash above the annual " + "limit also counts in isa_subscriptions_above_limit, so the two " + "overlap and must not be added." + ) + definition_period = YEAR + unit = GBP + quantity_type = FLOW + reference = "https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA" + + def formula(person, period, parameters): + subscriptions = person("cash_isa_subscriptions", period) + limit = person("cash_isa_subscription_limit", period) + return max_(subscriptions - limit, 0) diff --git a/policyengine_uk/variables/gov/hmrc/isa/isa_subscriptions.py b/policyengine_uk/variables/gov/hmrc/isa/isa_subscriptions.py new file mode 100644 index 0000000000..87a112e28a --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/isa/isa_subscriptions.py @@ -0,0 +1,28 @@ +from policyengine_uk.model_api import * + + +class isa_subscriptions(Variable): + value_type = float + entity = Person + label = "ISA subscriptions" + documentation = ( + "Subscriptions in the tax year that count towards the annual ISA " + "subscription limit: cash, stocks-and-shares and Lifetime ISA. " + "lifetime_isa_payments includes transfers into a Lifetime ISA from " + "other ISAs, which count towards the Lifetime ISA limit but are not new " + "subscriptions for the annual limit, so where that input includes them " + "this total overstates subscriptions by the amount transferred. " + "Innovative finance ISAs are not included." + ) + definition_period = YEAR + unit = GBP + quantity_type = FLOW + reference = ( + "https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA", + "https://www.gov.uk/guidance/manage-isa-subscriptions-for-your-investors", + ) + adds = [ + "cash_isa_subscriptions", + "stocks_and_shares_isa_subscriptions", + "lifetime_isa_payments", + ] diff --git a/policyengine_uk/variables/gov/hmrc/isa/isa_subscriptions_above_limit.py b/policyengine_uk/variables/gov/hmrc/isa/isa_subscriptions_above_limit.py new file mode 100644 index 0000000000..c47e0144c6 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/isa/isa_subscriptions_above_limit.py @@ -0,0 +1,23 @@ +from policyengine_uk.model_api import * + + +class isa_subscriptions_above_limit(Variable): + value_type = float + entity = Person + label = "ISA subscriptions above the annual limit" + documentation = ( + "ISA subscriptions above the annual subscription limit: an amount that " + "cannot be held in any ISA in the year. It is zero for data that met " + "the limits in force when it was collected. The model does not say " + "which ISA type the excess comes from or where it goes. Cash above the " + "cash limit counts in cash_isa_subscriptions_above_limit too, so the " + "two overlap and must not be added." + ) + definition_period = YEAR + unit = GBP + quantity_type = FLOW + reference = "https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA" + + def formula(person, period, parameters): + limit = parameters(period).gov.hmrc.isa.annual_subscription_limit + return max_(person("isa_subscriptions", period) - limit, 0) diff --git a/policyengine_uk/variables/gov/hmrc/isa/stocks_and_shares_isa_subscriptions.py b/policyengine_uk/variables/gov/hmrc/isa/stocks_and_shares_isa_subscriptions.py new file mode 100644 index 0000000000..908df95b35 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/isa/stocks_and_shares_isa_subscriptions.py @@ -0,0 +1,20 @@ +from policyengine_uk.model_api import * + + +class stocks_and_shares_isa_subscriptions(Variable): + value_type = float + entity = Person + label = "Stocks and shares ISA subscriptions" + documentation = ( + "New money paid into stocks-and-shares ISAs in the tax year, including " + "eligible shares subscribed by transfer, which counts towards the annual " + "subscription limit. Exclude transfers between ISAs, income and growth, " + "and replacements of cash withdrawn from a flexible ISA earlier in the " + "same tax year. Innovative finance ISAs are not included. Amounts are " + "not uprated, as the limit is fixed in cash terms." + ) + definition_period = YEAR + unit = GBP + quantity_type = FLOW + default_value = 0 + reference = "https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA" From d29cc811645af39eedaa712c1a200c3caef05b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:02:41 +0000 Subject: [PATCH 02/11] Add an opt-in response for cash above the cash ISA limit gov.contrib.behavioral_responses.cash_isa_excess splits cash ISA subscriptions above the person's cash limit between stocks-and-shares ISAs (within the room the annual limit leaves), taxable savings and other uses, with an interest rate for the taxable part (default: Bank of England effective rate on individuals' outstanding time deposits, June 2026). Both shares default to zero, so no result moves until a reform sets them. cash_isa_excess_savings_interest enters taxable_savings_interest_income and not income: the interest is earned either way, so a cash limit changes tax, not gross income. Shares that are negative or sum above one raise. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2710.added.md | 2 +- .../cash_isa_excess/README.md | 1 + .../cash_isa_excess/interest_rate.yaml | 14 ++++ .../stocks_and_shares_isa_share.yaml | 18 +++++ .../taxable_savings_share.yaml | 17 +++++ policyengine_uk/programs.yaml | 2 +- .../hmrc/isa/cash_isa_excess_response.yaml | 76 +++++++++++++++++++ .../tests/test_cash_isa_excess_response.py | 70 +++++++++++++++++ .../taxable_savings_interest_income.py | 5 +- .../isa/cash_isa_excess_savings_interest.py | 29 +++++++ ...ash_isa_excess_to_stocks_and_shares_isa.py | 32 ++++++++ .../isa/cash_isa_excess_to_taxable_savings.py | 24 ++++++ .../isa/cash_isa_subscriptions_above_limit.py | 3 +- 13 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/README.md create mode 100644 policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/interest_rate.yaml create mode 100644 policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/stocks_and_shares_isa_share.yaml create mode 100644 policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/taxable_savings_share.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/hmrc/isa/cash_isa_excess_response.yaml create mode 100644 policyengine_uk/tests/test_cash_isa_excess_response.py create mode 100644 policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_savings_interest.py create mode 100644 policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_to_stocks_and_shares_isa.py create mode 100644 policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_to_taxable_savings.py diff --git a/changelog.d/2710.added.md b/changelog.d/2710.added.md index 6a001b437a..55110e7031 100644 --- a/changelog.d/2710.added.md +++ b/changelog.d/2710.added.md @@ -1 +1 @@ -Added the ISA annual subscription limit (£20,000) and the £12,000 cash ISA limit for people under 65 at the end of the tax year from 6 April 2027 (ISA Regulations 1998 reg 4ZA, as amended by SI 2026/1018), per-person cash and stocks-and-shares ISA subscription inputs, and `cash_isa_subscriptions_above_limit` and `isa_subscriptions_above_limit`, the subscriptions a limit leaves out. +Added the ISA annual subscription limit (£20,000) and the £12,000 cash ISA limit for people under 65 at the end of the tax year from 6 April 2027 (ISA Regulations 1998 reg 4ZA, as amended by SI 2026/1018), per-person cash and stocks-and-shares ISA subscription inputs, and `cash_isa_subscriptions_above_limit` and `isa_subscriptions_above_limit`, the subscriptions a limit leaves out. An opt-in response, `gov.contrib.behavioral_responses.cash_isa_excess`, off by default, sends cash above the cash limit to stocks-and-shares ISAs, taxable savings or other uses, and taxes the interest on the taxable part. diff --git a/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/README.md b/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/README.md new file mode 100644 index 0000000000..1f338c54b6 --- /dev/null +++ b/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/README.md @@ -0,0 +1 @@ +# Response to cash ISA subscriptions above the cash limit diff --git a/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/interest_rate.yaml b/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/interest_rate.yaml new file mode 100644 index 0000000000..f6730b69d8 --- /dev/null +++ b/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/interest_rate.yaml @@ -0,0 +1,14 @@ +description: > + Annual interest rate on cash ISA subscriptions above the cash limit that are + held in taxable savings. The default is the Bank of England's effective + interest rate on individuals' outstanding time deposits in June 2026, held + for every year. +values: + 2000-01-01: 0.0336 +metadata: + label: Interest rate on cash ISA excess in taxable savings + period: year + unit: /1 + reference: + - title: Bank of England, Effective interest rates, June 2026, Table A (series Z6IW) + href: https://www.bankofengland.co.uk/statistics/effective-interest-rates/2026/june-2026 diff --git a/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/stocks_and_shares_isa_share.yaml b/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/stocks_and_shares_isa_share.yaml new file mode 100644 index 0000000000..36ac95ecef --- /dev/null +++ b/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/stocks_and_shares_isa_share.yaml @@ -0,0 +1,18 @@ +description: > + Share of cash ISA subscriptions above the person's cash limit that they pay + into stocks-and-shares ISAs instead, as far as the annual subscription limit + leaves room; any part it leaves no room for is held in taxable savings. The + money is taken to be invested: uninvested cash in a stocks-and-shares ISA + would pay the charge on non-cash ISA cash interest, which this does not add. + It is zero by default, as neither HMRC nor the OBR has published a split. +values: + 2000-01-01: 0 +metadata: + label: Cash ISA excess paid into stocks-and-shares ISAs + period: year + unit: /1 + reference: + - title: HMRC, Cash ISA limit reduction, tax information and impact note (17 September 2026) + href: https://www.gov.uk/government/publications/reduction-in-the-cash-individual-savings-account-isa-limit/cash-individual-savings-account-isa-limit-reduction + - title: HM Treasury, Budget 2025 policy costings, page 79 + href: https://assets.publishing.service.gov.uk/media/692872fd2a37784b16ecf676/Budget_2025-Policy_Costings.pdf#page=79 diff --git a/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/taxable_savings_share.yaml b/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/taxable_savings_share.yaml new file mode 100644 index 0000000000..13bd3b5146 --- /dev/null +++ b/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/taxable_savings_share.yaml @@ -0,0 +1,17 @@ +description: > + Share of cash ISA subscriptions above the person's cash limit that they hold + in taxable savings instead, where the interest is taxed. What neither this + nor the stocks-and-shares ISA share covers goes to other uses, such as + spending, pensions or repaying debt, which the model does not follow. It is + zero by default, as neither HMRC nor the OBR has published a split. +values: + 2000-01-01: 0 +metadata: + label: Cash ISA excess held in taxable savings + period: year + unit: /1 + reference: + - title: HMRC, Cash ISA limit reduction, tax information and impact note (17 September 2026) + href: https://www.gov.uk/government/publications/reduction-in-the-cash-individual-savings-account-isa-limit/cash-individual-savings-account-isa-limit-reduction + - title: HM Treasury, Budget 2025 policy costings, page 79 + href: https://assets.publishing.service.gov.uk/media/692872fd2a37784b16ecf676/Budget_2025-Policy_Costings.pdf#page=79 diff --git a/policyengine_uk/programs.yaml b/policyengine_uk/programs.yaml index d51e08d885..4d0c803d6e 100644 --- a/policyengine_uk/programs.yaml +++ b/policyengine_uk/programs.yaml @@ -38,7 +38,7 @@ programs: coverage: UK variable: isa_subscriptions_above_limit verified_start_year: 2017 - notes: The annual limit (gov.hmrc.isa.annual_subscription_limit) and, from 6 April 2027, the cash limit for people under 65 at the end of the tax year (gov.hmrc.isa.cash). Reports subscriptions above each limit from per-person subscription inputs. Innovative finance and Junior ISAs are not modelled. + notes: The annual limit (gov.hmrc.isa.annual_subscription_limit) and, from 6 April 2027, the cash limit for people under 65 at the end of the tax year (gov.hmrc.isa.cash). Reports subscriptions above each limit from per-person subscription inputs. Where cash above the cash limit goes is an opt-in assumption (gov.contrib.behavioral_responses.cash_isa_excess), off by default. Innovative finance and Junior ISAs are not modelled. - id: income_tax name: Income tax diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/cash_isa_excess_response.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/cash_isa_excess_response.yaml new file mode 100644 index 0000000000..390bb3819c --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/cash_isa_excess_response.yaml @@ -0,0 +1,76 @@ +# The opt-in response to cash above the cash ISA limit +# (gov.contrib.behavioral_responses.cash_isa_excess): GBP 8,000 above the +# GBP 12,000 limit at the default 3.36% rate earns GBP 268.80 a year. + +- name: By default cash above the limit changes no tax + period: 2027 + input: + age: 40 + cash_isa_subscriptions: 20_000 + output: + cash_isa_subscriptions_above_limit: 8_000 + cash_isa_excess_to_stocks_and_shares_isa: 0 + cash_isa_excess_to_taxable_savings: 0 + cash_isa_excess_savings_interest: 0 + taxable_savings_interest_income: 0 + +- name: Excess held in taxable savings earns taxable interest + period: 2027 + absolute_error_margin: 0.01 + input: + gov.contrib.behavioral_responses.cash_isa_excess.taxable_savings_share: 1 + age: 40 + cash_isa_subscriptions: 20_000 + savings_interest_income: 100 + output: + cash_isa_excess_to_taxable_savings: 8_000 + cash_isa_excess_savings_interest: 268.80 + taxable_savings_interest_income: 368.80 + +- name: The excess splits between stocks and shares ISAs, taxable savings and other uses + period: 2027 + absolute_error_margin: 0.01 + input: + gov.contrib.behavioral_responses.cash_isa_excess.stocks_and_shares_isa_share: 0.5 + gov.contrib.behavioral_responses.cash_isa_excess.taxable_savings_share: 0.3 + age: 40 + cash_isa_subscriptions: 20_000 + output: + cash_isa_excess_to_stocks_and_shares_isa: 4_000 + cash_isa_excess_to_taxable_savings: 2_400 + cash_isa_excess_savings_interest: 80.64 + +- name: Cash the annual limit leaves no room for in stocks and shares ISAs goes to taxable savings + period: 2027 + absolute_error_margin: 0.01 + input: + gov.hmrc.isa.annual_subscription_limit: 16_000 + gov.contrib.behavioral_responses.cash_isa_excess.stocks_and_shares_isa_share: 1 + age: 40 + # GBP 3,000 above the cash limit; GBP 12,000 cash and GBP 3,000 stocks + # and shares leave GBP 1,000 of the annual limit. + cash_isa_subscriptions: 15_000 + stocks_and_shares_isa_subscriptions: 3_000 + output: + cash_isa_excess_to_stocks_and_shares_isa: 1_000 + cash_isa_excess_to_taxable_savings: 2_000 + cash_isa_excess_savings_interest: 67.20 + +- name: Nothing moves where no cash limit binds + period: 2027 + input: + gov.contrib.behavioral_responses.cash_isa_excess.taxable_savings_share: 1 + age: [40, 70] + cash_isa_subscriptions: [12_000, 20_000] + output: + cash_isa_excess_to_taxable_savings: [0, 0] + cash_isa_excess_savings_interest: [0, 0] + +- name: Nothing moves in fiscal 2026-27, before the cash limit + period: 2026 + input: + gov.contrib.behavioral_responses.cash_isa_excess.taxable_savings_share: 1 + age: 40 + cash_isa_subscriptions: 20_000 + output: + cash_isa_excess_savings_interest: 0 diff --git a/policyengine_uk/tests/test_cash_isa_excess_response.py b/policyengine_uk/tests/test_cash_isa_excess_response.py new file mode 100644 index 0000000000..b7d48d3142 --- /dev/null +++ b/policyengine_uk/tests/test_cash_isa_excess_response.py @@ -0,0 +1,70 @@ +"""The opt-in response to cash above the cash ISA limit changes tax, not income. + +A basic-rate taxpayer who already uses their personal savings allowance puts +GBP 20,000 into cash ISAs in fiscal 2027-28. With all of the GBP 8,000 above +the GBP 12,000 limit held in taxable savings, its interest at the default +rate is taxed at the savings basic rate. Restoring a GBP 20,000 cash limit +removes that tax, which is how a reversal of the limit scores. +""" + +import pytest + +from policyengine_uk import CountryTaxBenefitSystem, Simulation + +YEAR = 2027 +RESPONSE = "gov.contrib.behavioral_responses.cash_isa_excess" +SPAN = f"{YEAR}-01-01.{YEAR}-12-31" +TAXABLE = {f"{RESPONSE}.taxable_savings_share": {SPAN: 1}} +SITUATION = { + "people": { + "saver": { + "age": {YEAR: 40}, + "employment_income": {YEAR: 30_000}, + "savings_interest_income": {YEAR: 1_000}, + "cash_isa_subscriptions": {YEAR: 20_000}, + } + }, + "benunits": {"benunit": {"members": ["saver"]}}, + "households": {"household": {"members": ["saver"], "region": "LONDON"}}, +} + + +def _calculate(reform, variable): + simulation = Simulation(situation=SITUATION, reform=reform) + return float(simulation.calculate(variable, YEAR)[0]) + + +def test_response_taxes_the_displaced_interest_without_adding_income(): + parameters = CountryTaxBenefitSystem().parameters + rate = parameters.get_child(f"{RESPONSE}.interest_rate")(f"{YEAR}-04-30") + savings_basic_rate = parameters.gov.hmrc.income_tax.rates.savings.basic( + f"{YEAR}-04-30" + ) + expected_tax = 8_000 * rate * savings_basic_rate + + tax_change = _calculate(TAXABLE, "income_tax") - _calculate({}, "income_tax") + assert tax_change == pytest.approx(expected_tax, abs=0.01) + for variable in ("household_market_income", "household_gross_income"): + assert _calculate(TAXABLE, variable) == pytest.approx( + _calculate({}, variable), abs=0.01 + ), variable + + +def test_restoring_the_cash_limit_removes_the_tax_on_displaced_interest(): + restored = { + **TAXABLE, + "gov.hmrc.isa.cash.subscription_limit": {SPAN: 20_000}, + } + assert _calculate(restored, "cash_isa_excess_savings_interest") == 0 + assert _calculate(restored, "income_tax") == pytest.approx( + _calculate({}, "income_tax"), abs=0.01 + ) + + +def test_shares_above_one_are_rejected(): + reform = { + f"{RESPONSE}.stocks_and_shares_isa_share": {SPAN: 0.6}, + f"{RESPONSE}.taxable_savings_share": {SPAN: 0.6}, + } + with pytest.raises(ValueError, match="sum to at most one"): + _calculate(reform, "cash_isa_excess_savings_interest") diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/bases/savings_income/taxable_savings_interest_income.py b/policyengine_uk/variables/gov/hmrc/income_tax/bases/savings_income/taxable_savings_interest_income.py index a70649905c..f39d61797e 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/bases/savings_income/taxable_savings_interest_income.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/bases/savings_income/taxable_savings_interest_income.py @@ -15,7 +15,10 @@ class taxable_savings_interest_income(Variable): def formula(person, period, parameters): standard_interest = person("savings_interest_income", period) other_interest = person("other_investment_income", period) - total_interest = standard_interest + other_interest + # Zero unless a reform sets the cash ISA excess response: interest on + # cash a cash ISA limit keeps out, held in taxable savings instead. + displaced_interest = person("cash_isa_excess_savings_interest", period) + total_interest = standard_interest + other_interest + displaced_interest # Exempt interest is a separate additive input, not an allowance # against interest from unrelated taxable savings. return max_(0, total_interest) diff --git a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_savings_interest.py b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_savings_interest.py new file mode 100644 index 0000000000..3e358a0d56 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_savings_interest.py @@ -0,0 +1,29 @@ +from policyengine_uk.model_api import * + + +class cash_isa_excess_savings_interest(Variable): + value_type = float + entity = Person + label = "Interest on cash ISA excess held in taxable savings" + documentation = ( + "Interest on the cash ISA subscriptions above the cash limit that the " + "person holds in taxable savings, at the rate in " + "gov.contrib.behavioral_responses.cash_isa_excess. It is taxed as " + "savings interest but not added to income: the person earns it either " + "way, tax-free in a cash ISA or taxed outside one, so a cash limit " + "changes the tax and not the gross income. It is a full year's " + "interest on the year's excess and leaves out cash moved out of ISAs " + "in earlier years, which a static annual model does not hold. Zero " + "unless a reform sets the response's shares." + ) + definition_period = YEAR + unit = GBP + quantity_type = FLOW + + def formula(person, period, parameters): + p = parameters(period).gov.contrib.behavioral_responses.cash_isa_excess + responds = p.stocks_and_shares_isa_share > 0 or p.taxable_savings_share > 0 + if not responds or p.interest_rate == 0: + return 0 + held = person("cash_isa_excess_to_taxable_savings", period) + return held * p.interest_rate diff --git a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_to_stocks_and_shares_isa.py b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_to_stocks_and_shares_isa.py new file mode 100644 index 0000000000..2325cf46c9 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_to_stocks_and_shares_isa.py @@ -0,0 +1,32 @@ +from policyengine_uk.model_api import * + + +class cash_isa_excess_to_stocks_and_shares_isa(Variable): + value_type = float + entity = Person + label = "Cash ISA excess paid into stocks-and-shares ISAs" + documentation = ( + "Cash ISA subscriptions above the cash limit that the person pays into " + "stocks-and-shares ISAs instead: the stocks-and-shares ISA share in " + "gov.contrib.behavioral_responses.cash_isa_excess, up to the room the " + "annual subscription limit leaves after the cash kept in cash ISAs and " + "the other ISA subscriptions. Zero unless a reform sets that share. " + "It is not added to stocks_and_shares_isa_subscriptions." + ) + definition_period = YEAR + unit = GBP + quantity_type = FLOW + + def formula(person, period, parameters): + p = parameters(period).gov.contrib.behavioral_responses.cash_isa_excess + shares = [p.stocks_and_shares_isa_share, p.taxable_savings_share] + if min(shares) < 0 or sum(shares) > 1: + raise ValueError( + "The shares in gov.contrib.behavioral_responses.cash_isa_excess " + "must be non-negative and sum to at most one" + ) + excess = person("cash_isa_subscriptions_above_limit", period) + kept = person("isa_subscriptions", period) - excess + limit = parameters(period).gov.hmrc.isa.annual_subscription_limit + room = max_(limit - kept, 0) + return min_(excess * p.stocks_and_shares_isa_share, room) diff --git a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_to_taxable_savings.py b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_to_taxable_savings.py new file mode 100644 index 0000000000..eaa8f7b85e --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_to_taxable_savings.py @@ -0,0 +1,24 @@ +from policyengine_uk.model_api import * + + +class cash_isa_excess_to_taxable_savings(Variable): + value_type = float + entity = Person + label = "Cash ISA excess held in taxable savings" + documentation = ( + "Cash ISA subscriptions above the cash limit that the person holds in " + "taxable savings instead: the taxable savings share in " + "gov.contrib.behavioral_responses.cash_isa_excess, plus any cash meant " + "for a stocks-and-shares ISA that the annual limit leaves no room for. " + "Zero unless a reform sets those shares." + ) + definition_period = YEAR + unit = GBP + quantity_type = FLOW + + def formula(person, period, parameters): + p = parameters(period).gov.contrib.behavioral_responses.cash_isa_excess + excess = person("cash_isa_subscriptions_above_limit", period) + moved = person("cash_isa_excess_to_stocks_and_shares_isa", period) + no_room = excess * p.stocks_and_shares_isa_share - moved + return excess * p.taxable_savings_share + no_room diff --git a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions_above_limit.py b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions_above_limit.py index 8d9856a0d0..4fbeaffc0a 100644 --- a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions_above_limit.py +++ b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions_above_limit.py @@ -9,7 +9,8 @@ class cash_isa_subscriptions_above_limit(Variable): "Cash ISA subscriptions above the cash limit that applies to the " "person. It is zero for data that met the limits in force when it was " "collected; under a lower limit it is cash that cannot go into a cash " - "ISA, and the model does not say where it goes. Cash above the annual " + "ISA. gov.contrib.behavioral_responses.cash_isa_excess sets where it " + "goes, and by default none of it changes tax. Cash above the annual " "limit also counts in isa_subscriptions_above_limit, so the two " "overlap and must not be added." ) From 3d5e7cc80b8a93286bf15b9146b1016ec675df09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:18:06 +0000 Subject: [PATCH 03/11] Hold cash and stocks-and-shares ISA balances per person Add person inputs cash_isa_balance and stocks_and_shares_isa_balance, uprated with savings and corporate_wealth, and make the household cash_isa and stocks_and_shares_isa their sums, as household_lifetime_isa_balance sums lifetime_isa_balance. The household totals stay in uprating_indices.yaml, so datasets that store only the totals keep them, projected as before, until Microcosm exports the person balances. Core does not combine a sum with uprating, so a household total given in a situation for one year no longer carries into later years; the person balances carry the uprating instead. test_wealth_components now checks the class uprating on the summed inputs. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2710.changed.md | 1 + policyengine_uk/data/uprating_indices.yaml | 5 ++ .../baseline/gov/dwp/cash_isa_capital.yaml | 13 +++ .../household/wealth/isa_balances.yaml | 41 +++++++++ policyengine_uk/tests/test_isa_balances.py | 87 +++++++++++++++++++ .../tests/test_wealth_components.py | 22 ++++- .../variables/household/wealth/cash_isa.py | 11 ++- .../household/wealth/stocks_and_shares_isa.py | 10 ++- .../variables/input/cash_isa_balance.py | 19 ++++ .../input/stocks_and_shares_isa_balance.py | 20 +++++ 10 files changed, 221 insertions(+), 8 deletions(-) create mode 100644 changelog.d/2710.changed.md create mode 100644 policyengine_uk/tests/policy/baseline/household/wealth/isa_balances.yaml create mode 100644 policyengine_uk/tests/test_isa_balances.py create mode 100644 policyengine_uk/variables/input/cash_isa_balance.py create mode 100644 policyengine_uk/variables/input/stocks_and_shares_isa_balance.py diff --git a/changelog.d/2710.changed.md b/changelog.d/2710.changed.md new file mode 100644 index 0000000000..51c153571b --- /dev/null +++ b/changelog.d/2710.changed.md @@ -0,0 +1 @@ +The household `cash_isa` and `stocks_and_shares_isa` now sum new per-person `cash_isa_balance` and `stocks_and_shares_isa_balance` inputs. A dataset or situation that stores a household total keeps it, and stored totals still project with savings, but a household total given for one year no longer carries into later years. diff --git a/policyengine_uk/data/uprating_indices.yaml b/policyengine_uk/data/uprating_indices.yaml index 8c2e4a57ae..821af819c8 100644 --- a/policyengine_uk/data/uprating_indices.yaml +++ b/policyengine_uk/data/uprating_indices.yaml @@ -105,7 +105,11 @@ gov.economic_assumptions.yoy_growth.capital_gains: - capital_gains_carried_interest gov.economic_assumptions.yoy_growth.obr.per_capita.gdp: - benunit_reported_capital +# cash_isa and stocks_and_shares_isa sum the person balances listed with +# them; they are listed so a dataset that stores the household totals +# grows them like their parts. - cash_isa +- cash_isa_balance - corporate_wealth - directly_held_shares - dividend_income @@ -138,6 +142,7 @@ gov.economic_assumptions.yoy_growth.obr.per_capita.gdp: - non_cash_isa_cash_interest - spousal_maintenance_income - stocks_and_shares_isa +- stocks_and_shares_isa_balance - sublet_income - cash_isa - consumer_debt diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/cash_isa_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/cash_isa_capital.yaml index 4e73d9a830..3b858806e2 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/cash_isa_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/cash_isa_capital.yaml @@ -99,3 +99,16 @@ pension_credit_reported_capital: [0, 5_000] output: pension_credit_assessable_capital: [0, 5_000] + +- name: A person's cash ISA balance enters capital through the household total + period: 2026 + absolute_error_margin: 0.01 + input: + age: 30 + would_claim_uc: false + savings: 5_000 + cash_isa_balance: 3_000 + output: + cash_isa: 3_000 + uc_assessable_capital: 8_000 + housing_benefit_assessable_capital: 8_000 diff --git a/policyengine_uk/tests/policy/baseline/household/wealth/isa_balances.yaml b/policyengine_uk/tests/policy/baseline/household/wealth/isa_balances.yaml new file mode 100644 index 0000000000..0882f1ceaf --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/household/wealth/isa_balances.yaml @@ -0,0 +1,41 @@ +# Household cash and stocks-and-shares ISA totals sum the members' balances, +# as household_lifetime_isa_balance sums lifetime_isa_balance. + +- name: Household ISA totals sum the members' balances + period: 2026 + input: + people: + first_adult: + age: 40 + cash_isa_balance: 3_000 + stocks_and_shares_isa_balance: 1_000 + second_adult: + age: 38 + cash_isa_balance: 2_000 + stocks_and_shares_isa_balance: 500 + benunits: + benunit: + members: [first_adult, second_adult] + households: + household: + members: [first_adult, second_adult] + output: + cash_isa: 5_000 + stocks_and_shares_isa: 1_500 + +- name: A household total given directly takes precedence over the members' balances + period: 2026 + input: + people: + adult: + age: 40 + cash_isa_balance: 3_000 + benunits: + benunit: + members: [adult] + households: + household: + members: [adult] + cash_isa: 7_000 + output: + cash_isa: 7_000 diff --git a/policyengine_uk/tests/test_isa_balances.py b/policyengine_uk/tests/test_isa_balances.py new file mode 100644 index 0000000000..a38b6cc2f4 --- /dev/null +++ b/policyengine_uk/tests/test_isa_balances.py @@ -0,0 +1,87 @@ +"""Household cash and stocks-and-shares ISA totals sum per-person balances. + +Datasets built before the per-person balances existed store only the +household totals. Those totals must keep their stored values, projected with +savings, rather than fall to the sum of balances the dataset does not have. +""" + +import pandas as pd +import pytest + +from policyengine_uk import Microsimulation +from policyengine_uk.data.dataset_schema import UKSingleYearDataset + + +def _dataset(person_columns, household_columns): + person = pd.DataFrame( + { + "person_id": [1, 2], + "person_benunit_id": [1, 1], + "person_household_id": [1, 1], + "age": [30, 28], + **person_columns, + } + ) + household = pd.DataFrame( + { + "household_id": [1], + "household_weight": [1.0], + "region": ["LONDON"], + "tenure_type": ["RENT_PRIVATELY"], + "council_tax": [1_500.0], + "rent": [12_000.0], + "savings": [1_000.0], + **household_columns, + } + ) + return UKSingleYearDataset( + person=person, + benunit=pd.DataFrame({"benunit_id": [1]}), + household=household, + fiscal_year=2025, + ) + + +def _growth(simulation, year): + growth = simulation.calculate("savings", year).values[0] / 1_000.0 + assert growth != pytest.approx(1.0) + return growth + + +def test_stored_household_totals_without_person_balances_project_with_savings(): + simulation = Microsimulation( + dataset=_dataset( + {}, {"cash_isa": [3_000.0], "stocks_and_shares_isa": [2_000.0]} + ) + ) + growth = _growth(simulation, 2027) + cash = simulation.calculate("cash_isa", 2027).values[0] + shares = simulation.calculate("stocks_and_shares_isa", 2027).values[0] + assert cash == pytest.approx(3_000.0 * growth) + assert shares == pytest.approx(2_000.0 * growth) + # Savings, the cash ISA and the stocks-and-shares ISA (through + # share_like_capital) all reach the capital pool. + capital = simulation.calculate("uc_assessable_capital", 2027).values[0] + assert capital == pytest.approx(6_000.0 * growth, rel=1e-5) + + +def test_household_totals_sum_the_person_balances_in_every_year(): + simulation = Microsimulation( + dataset=_dataset( + { + "cash_isa_balance": [4_000.0, 1_000.0], + "stocks_and_shares_isa_balance": [0.0, 2_500.0], + }, + {}, + ) + ) + growth = _growth(simulation, 2027) + for year, factor in ((2025, 1.0), (2027, growth)): + cash = simulation.calculate("cash_isa_balance", year).values + shares = simulation.calculate("stocks_and_shares_isa_balance", year).values + assert list(cash) == pytest.approx([4_000.0 * factor, 1_000.0 * factor]) + assert list(shares) == pytest.approx([0.0, 2_500.0 * factor]) + household_cash = simulation.calculate("cash_isa", year).values[0] + household_shares = simulation.calculate("stocks_and_shares_isa", year) + assert household_cash == pytest.approx(cash.sum()) + assert household_shares.values[0] == pytest.approx(shares.sum()) diff --git a/policyengine_uk/tests/test_wealth_components.py b/policyengine_uk/tests/test_wealth_components.py index 489d6ac0f8..38c57e5eba 100644 --- a/policyengine_uk/tests/test_wealth_components.py +++ b/policyengine_uk/tests/test_wealth_components.py @@ -1,6 +1,10 @@ """corporate_wealth's components share its entity, quantity type and uprating metadata (class uprating and uprating_indices.yaml index), the condition for a -dataset's identity corporate_wealth == sum of components to survive projection.""" +dataset's identity corporate_wealth == sum of components to survive projection. + +A component summed from per-person inputs, such as stocks_and_shares_isa, keeps +the uprating_indices.yaml index for a stored total; its inputs carry the class +uprating, since Core does not combine a sum with uprating.""" from pathlib import Path @@ -24,20 +28,30 @@ def _uprating_index(variable): return [index for index, names in UPRATING_INDICES.items() if variable in names] +def _inputs(name): + """The input variables a variable is stored as or summed from.""" + return system.variables[name].adds or [name] + + def test_components_match_corporate_wealth(): corporate_wealth = system.variables["corporate_wealth"] for name in COMPONENTS: variable = system.variables[name] - assert variable.is_input_variable() assert variable.entity.key == corporate_wealth.entity.key assert variable.quantity_type == corporate_wealth.quantity_type - assert variable.uprating == corporate_wealth.uprating assert _uprating_index(name) == _uprating_index("corporate_wealth") + for source in _inputs(name): + assert system.variables[source].is_input_variable() + assert system.variables[source].uprating == corporate_wealth.uprating + assert _uprating_index(source) == _uprating_index("corporate_wealth") def test_cash_isa_is_uprated_with_savings(): - assert system.variables["cash_isa"].uprating == system.variables["savings"].uprating assert _uprating_index("cash_isa") == _uprating_index("savings") + for source in _inputs("cash_isa"): + assert system.variables[source].is_input_variable() + assert system.variables[source].uprating == system.variables["savings"].uprating + assert _uprating_index(source) == _uprating_index("savings") def test_components_are_not_part_of_total_wealth(): diff --git a/policyengine_uk/variables/household/wealth/cash_isa.py b/policyengine_uk/variables/household/wealth/cash_isa.py index 69bcfca51f..968a74544e 100644 --- a/policyengine_uk/variables/household/wealth/cash_isa.py +++ b/policyengine_uk/variables/household/wealth/cash_isa.py @@ -3,10 +3,17 @@ class cash_isa(Variable): label = "cash ISA holdings" - documentation = "Amount held in cash Individual Savings Accounts" + documentation = ( + "Amount held in cash Individual Savings Accounts: cash_isa_balance " + "summed over the household's members. A dataset that stores this " + "column keeps the stored value, and projects it with savings: it is " + "recomputed only when not stored, so a reform or situation that edits " + "cash_isa_balance leaves a stored total unchanged. A household input " + "is used for the year it is given for only." + ) entity = Household definition_period = YEAR value_type = float unit = GBP - uprating = "gov.economic_assumptions.indices.obr.per_capita.gdp" quantity_type = STOCK + adds = ["cash_isa_balance"] diff --git a/policyengine_uk/variables/household/wealth/stocks_and_shares_isa.py b/policyengine_uk/variables/household/wealth/stocks_and_shares_isa.py index caa743d61e..d91949f784 100644 --- a/policyengine_uk/variables/household/wealth/stocks_and_shares_isa.py +++ b/policyengine_uk/variables/household/wealth/stocks_and_shares_isa.py @@ -5,11 +5,17 @@ class stocks_and_shares_isa(Variable): label = "stocks and shares ISA holdings" documentation = ( "Amount held in stocks and shares (investment) Individual Savings " - "Accounts. Part of corporate_wealth, so the two must never be summed." + "Accounts: stocks_and_shares_isa_balance summed over the household's " + "members. Part of corporate_wealth, so the two must never be summed. " + "A dataset that stores this column keeps the stored value, and " + "projects it with corporate_wealth: it is recomputed only when not " + "stored, so a reform or situation that edits " + "stocks_and_shares_isa_balance leaves a stored total unchanged. A " + "household input is used for the year it is given for only." ) entity = Household definition_period = YEAR value_type = float unit = GBP - uprating = "gov.economic_assumptions.indices.obr.per_capita.gdp" quantity_type = STOCK + adds = ["stocks_and_shares_isa_balance"] diff --git a/policyengine_uk/variables/input/cash_isa_balance.py b/policyengine_uk/variables/input/cash_isa_balance.py new file mode 100644 index 0000000000..152cbde845 --- /dev/null +++ b/policyengine_uk/variables/input/cash_isa_balance.py @@ -0,0 +1,19 @@ +from policyengine_uk.model_api import * + + +class cash_isa_balance(Variable): + label = "Cash ISA balance" + documentation = ( + "Current value of the person's cash ISAs, Lifetime ISAs excluded " + "(lifetime_isa_balance). The household's cash_isa sums it over its " + "members, so a dataset that stores cash_isa itself keeps that stored " + "total instead. It is separate from savings, the household's ordinary " + "savings." + ) + entity = Person + definition_period = YEAR + value_type = float + unit = GBP + uprating = "gov.economic_assumptions.indices.obr.per_capita.gdp" + quantity_type = STOCK + default_value = 0 diff --git a/policyengine_uk/variables/input/stocks_and_shares_isa_balance.py b/policyengine_uk/variables/input/stocks_and_shares_isa_balance.py new file mode 100644 index 0000000000..23f0d1a8fd --- /dev/null +++ b/policyengine_uk/variables/input/stocks_and_shares_isa_balance.py @@ -0,0 +1,20 @@ +from policyengine_uk.model_api import * + + +class stocks_and_shares_isa_balance(Variable): + label = "Stocks and shares ISA balance" + documentation = ( + "Current value of the person's stocks and shares (investment) ISAs, " + "Lifetime ISAs excluded (lifetime_isa_balance). The household's " + "stocks_and_shares_isa sums it over its members, so a dataset that " + "stores stocks_and_shares_isa itself keeps that stored total instead. " + "Like that total, it is part of corporate_wealth, so the two must " + "never be summed." + ) + entity = Person + definition_period = YEAR + value_type = float + unit = GBP + uprating = "gov.economic_assumptions.indices.obr.per_capita.gdp" + quantity_type = STOCK + default_value = 0 From 020f230086d45fc35eb86b96f60db5929b643212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:18:20 +0000 Subject: [PATCH 04/11] Document the ISA subscription limits, the opt-in response and balances The ISA assumptions page gains a subscription limits section (inputs, the age test, the two excesses and how a reform scores, the opt-in response and its static-model limits) and describes the per-person balances under capital stocks. The dataset-sources guide tells producers how to export the balances and subscriptions; the growth factor list names the new balances. Co-Authored-By: Claude Opus 5.5 --- docs/book/assumptions/growthfactors.md | 2 +- .../isa-inputs-and-capital-flows.md | 106 +++++++++++++++--- docs/engineering/skills/dataset-sources.md | 20 ++++ 3 files changed, 113 insertions(+), 15 deletions(-) diff --git a/docs/book/assumptions/growthfactors.md b/docs/book/assumptions/growthfactors.md index b26c734edc..e87c72ec2c 100644 --- a/docs/book/assumptions/growthfactors.md +++ b/docs/book/assumptions/growthfactors.md @@ -103,7 +103,7 @@ We derive these rates as calendar-year nominal GDP growth from the OBR's March 2 |-------------|------|------|------|------|------|------|------|------| | Growth rate | 10.0% | 5.2% | 3.9% | 4.7% | 2.9% | 3.3% | 3.2% | 3.1% | -We use per capita GDP for these variables: `benunit_reported_capital`, `capital_gains`, `capital_gains_badr`, `capital_gains_before_response`, `capital_gains_carried_interest`, `capital_gains_residential_property`, `cash_isa`, `corporate_wealth`, `directly_held_shares`, `dividend_income`, `gross_financial_wealth`, `household_lifetime_isa_balance`, `lifetime_isa_balance`, `lump_sum_income`, `main_residence_value`, `maintenance_income`, `miscellaneous_income`, `mortgage_capital_repayment`, `net_financial_wealth`, `non_residential_property_value`, `other_investment_income`, `other_residential_property_value`, `owned_land`, `pension_credit_reported_capital`, `pension_income`, `private_pension_wealth`, `private_transfer_income`, `property_income`, `property_rental_income`, `savings`, `savings_interest_income`, `spousal_maintenance_income`, `stocks_and_shares_isa`, `sublet_income`, `uc_reported_capital`, `unit_and_investment_trusts` +We use per capita GDP for these variables: `benunit_reported_capital`, `capital_gains`, `capital_gains_badr`, `capital_gains_before_response`, `capital_gains_carried_interest`, `capital_gains_residential_property`, `cash_isa`, `cash_isa_balance`, `corporate_wealth`, `directly_held_shares`, `dividend_income`, `gross_financial_wealth`, `household_lifetime_isa_balance`, `lifetime_isa_balance`, `lump_sum_income`, `main_residence_value`, `maintenance_income`, `miscellaneous_income`, `mortgage_capital_repayment`, `net_financial_wealth`, `non_residential_property_value`, `other_investment_income`, `other_residential_property_value`, `owned_land`, `pension_credit_reported_capital`, `pension_income`, `private_pension_wealth`, `private_transfer_income`, `property_income`, `property_rental_income`, `savings`, `savings_interest_income`, `spousal_maintenance_income`, `stocks_and_shares_isa`, `stocks_and_shares_isa_balance`, `sublet_income`, `uc_reported_capital`, `unit_and_investment_trusts` ## Council tax diff --git a/docs/book/assumptions/isa-inputs-and-capital-flows.md b/docs/book/assumptions/isa-inputs-and-capital-flows.md index 5d32927bb1..c8f6af4936 100644 --- a/docs/book/assumptions/isa-inputs-and-capital-flows.md +++ b/docs/book/assumptions/isa-inputs-and-capital-flows.md @@ -96,14 +96,24 @@ established by the household tests. ## Capital stocks -`cash_isa` is a household stock separate from `savings`. Ordinary accessible -cash ISA balances enter the capital sources for Universal Credit, Housing -Benefit, Pension Credit, Income Support, income-based JSA and income-related -ESA. There is no general ISA-wrapper disregard and no generic 10% sale-cost -deduction for cash. Existing source-specific disregards, ownership allocation -assumptions and reported-capital overrides still apply. A complete reported -capital amount already includes relevant ISA holdings; it replaces covered -components rather than adding them again. +`cash_isa` is a household stock separate from `savings`. It sums the members' +`cash_isa_balance`, and `stocks_and_shares_isa` sums their +`stocks_and_shares_isa_balance`, as `household_lifetime_isa_balance` sums +`lifetime_isa_balance`. A dataset or situation that stores a household total +keeps it: the total is recomputed only when it is not stored, and a stored +total is projected with `savings` or `corporate_wealth`. A household input is +used only for the year it is given for; the person balances carry the +variable-level uprating to later years. The means tests read the household +totals, so a person's balance is pooled with the household's other financial +capital rather than attributed to their own benefit unit. + +Ordinary accessible cash ISA balances enter the capital sources for Universal +Credit, Housing Benefit, Pension Credit, Income Support, income-based JSA and +income-related ESA. There is no general ISA-wrapper disregard and no generic +10% sale-cost deduction for cash. Existing source-specific disregards, +ownership allocation assumptions and reported-capital overrides still apply. A +complete reported capital amount already includes relevant ISA holdings; it +replaces covered components rather than adding them again. For example, £5,000 savings and a £3,000 ordinary cash ISA give £8,000 UC capital. [UC regulation 72](https://www.legislation.gov.uk/uksi/2013/376/regulation/72) @@ -173,6 +183,72 @@ flows, excluded from household current benefits, taxes and net income. The flows do not automatically update a supplied account balance. This is an accounting convention for the model's current-income measures. +## Subscription limits + +`gov.hmrc.isa.annual_subscription_limit` is the £20,000 limit on a person's +subscriptions in a tax year to all their ISAs other than Junior ISAs +([ISA Regulations regulation 4ZA(1)](https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA)). +From 6 April 2027, `gov.hmrc.isa.cash.subscription_limit` limits cash ISA +subscriptions to £12,000 for a person who is 64 or under at the end of the +tax year (regulation 4ZA(1ZA), inserted by +[SI 2026/1018 regulation 6](https://www.legislation.gov.uk/uksi/2026/1018/regulation/6/made)). +Cash has had no limit of its own since 1 July 2014, so before 2027 the +parameter is infinite and cash counts only towards the annual limit. +`gov.hmrc.isa.cash.full_limit_age` (65) is the age at which the cash limit +stops applying. `cash_isa_subscription_limit` reads the age at the end of the +year (5 April) from the date of birth, so a person aged 64 on 6 October whose +65th birthday falls on or before 5 April has the annual limit for cash. + +The inputs are per-person flows in the year: `cash_isa_subscriptions`, +`stocks_and_shares_isa_subscriptions` and the existing +`lifetime_isa_payments`, which `isa_subscriptions` adds up. They exclude +transfers between ISAs, interest and growth, and replacements of cash +withdrawn from a flexible ISA earlier in the same year +([HMRC guidance](https://www.gov.uk/guidance/manage-isa-subscriptions-for-your-investors)). +`lifetime_isa_payments` includes transfers into a Lifetime ISA from other +ISAs, which count towards the Lifetime ISA limit but are not new +subscriptions for the annual limit, so the total overstates subscriptions +where that input includes them. Innovative finance ISAs are not included. +The inputs are not uprated, as the limits are fixed in cash terms. + +The model compares the inputs with the limits in force and does not cap them. +`cash_isa_subscriptions_above_limit` is cash above the person's cash limit, +and `isa_subscriptions_above_limit` is the total above the annual limit. Cash +above both limits counts in both, so the two must not be added. Both are zero +for data that met the limits in force when it was collected. Data collected +under a higher limit, such as subscriptions from before April 2027, shows +what a lower limit leaves out: scoring the 2027 cash limit, or its reversal, +compares that excess under the two sets of limits. + +By default the excess changes no tax. +`gov.contrib.behavioral_responses.cash_isa_excess` is an opt-in assumption +about where cash above the cash limit goes: + +- a share into stocks-and-shares ISAs, within the room the annual limit + leaves, with any part it leaves no room for held in taxable savings; +- a share into taxable savings; +- the rest to other uses the model does not follow, such as spending, + pensions or repaying debt. + +Both shares are zero by default. Neither +[HMRC's impact note](https://www.gov.uk/government/publications/reduction-in-the-cash-individual-savings-account-isa-limit/cash-individual-savings-account-isa-limit-reduction) +nor the Budget 2025 policy costing (page 79) publishes a split. The taxable +part earns `interest_rate`, by default 3.36%, the Bank of England's effective +rate on individuals' outstanding time deposits in June 2026. +`cash_isa_excess_savings_interest` enters `taxable_savings_interest_income` +but not market income: the person earns the interest either way, tax-free in +a cash ISA or taxed outside one, so the response changes tax and not gross +income. It is a full year's interest on the year's excess. A static annual +model holds no balances, so it leaves out interest on cash moved out of ISAs +in earlier years; later years need that as a supplied input. Money above the +annual limit is reported only. + +For example, a person aged 40 paying £20,000 into cash ISAs in 2027–28 has +£8,000 above the £12,000 limit. With all of it in taxable savings, it earns +£268.80 at the default rate. For a basic-rate taxpayer who has used their +personal savings allowance, that is £59.14 of income tax at the 22% savings +basic rate. + ## Annual resolution and coverage A bare year Y represents the fiscal year from 6 April Y to 5 April Y+1. @@ -184,9 +260,11 @@ conditions similarly represent the relevant transactions at one annual status. Multiple transactions with different conditions require externally prepared qualifying amounts. -Help to Buy ISA bonuses are not modelled here. Subscription-limit compliance, -Junior ISAs, Child Trust Funds and a proposed First Time Buyer ISA are also -outside this implementation. Population data without payment, withdrawal or -non-cash-interest detail inputs retain zero for those new flows. Household -tests establish the rules; population effects require a separate certified -dataset measurement. +Help to Buy ISA bonuses are not modelled here. Junior ISAs, Child Trust +Funds, transfers between ISA types (including the 2027 restrictions on +transfers into cash ISAs) and a proposed First Time Buyer ISA are also +outside this implementation. Population data without payment, withdrawal, +subscription or non-cash-interest detail inputs retain zero for those flows; +data without per-person ISA balances keeps its stored household totals. +Household tests establish the rules; population effects require a separate +certified dataset measurement. diff --git a/docs/engineering/skills/dataset-sources.md b/docs/engineering/skills/dataset-sources.md index 600d59943c..a29a31a681 100644 --- a/docs/engineering/skills/dataset-sources.md +++ b/docs/engineering/skills/dataset-sources.md @@ -126,6 +126,26 @@ advance apprenticeship tenure, so a producer or caller must supply changed classifications for later years rather than rely on a carried input to expire automatically. +## ISA balances and subscriptions + +Producers can export per-person `cash_isa_balance` and +`stocks_and_shares_isa_balance`. The household `cash_isa` and +`stocks_and_shares_isa` sum them, but a stored household column takes +precedence, so existing datasets that store only the household totals keep +them, projected with `savings` and `corporate_wealth` through +`uprating_indices.yaml`. A producer that exports the person balances should +either stop exporting the household totals or keep them equal to the sums; +the model does not reconcile the two. + +`cash_isa_subscriptions` and `stocks_and_shares_isa_subscriptions` are +annual person flows that count towards the ISA subscription limits, with +`lifetime_isa_payments` for Lifetime ISAs. Projection carries them flat, +since the limits are fixed in cash terms. They are not capped at the limits: +a producer should export subscriptions as observed, so that data from before +the 6 April 2027 cash limit shows what that limit leaves out. Absent columns +default to zero, so a change to an ISA limit changes nothing on such data. See +[issue #2710](https://github.com/PolicyEngine/policyengine-uk/issues/2710). + ## Defaults `policyengine_uk` does not silently choose a default dataset when no situation is From 677d76756b4f8bc8b4c4776dd8f8176fd9baa603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:53:29 +0000 Subject: [PATCH 05/11] Leave cash without a limit of its own and report the excess only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Budget 2026 retains the £20,000 cash ISA limit from April 2027 (OOTLAR para 2.5, legislation in November 2026), so gov.hmrc.isa.cash.subscription_limit no longer steps to £12,000. SI 2026/1018 reg 6's limit for people 64 or under becomes a reform setting, kept as a lever with the full-limit age; the limit tests run it as one. Section 4 of #2710 is decided as option (b): the model reports the amounts above each limit and no more. Remove the opt-in gov.contrib.behavioral_responses.cash_isa_excess block, its three variables and tests, and its term in taxable_savings_interest_income. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2710.added.md | 2 +- .../cash_isa_excess/README.md | 1 - .../cash_isa_excess/interest_rate.yaml | 14 --- .../stocks_and_shares_isa_share.yaml | 18 ---- .../taxable_savings_share.yaml | 17 ---- .../gov/hmrc/isa/cash/full_limit_age.yaml | 6 +- .../gov/hmrc/isa/cash/subscription_limit.yaml | 13 ++- policyengine_uk/programs.yaml | 2 +- .../hmrc/isa/cash_isa_excess_response.yaml | 76 --------------- .../gov/hmrc/isa/isa_subscription_limits.yaml | 93 ++++++++----------- .../tests/test_cash_isa_excess_response.py | 70 -------------- .../taxable_savings_interest_income.py | 5 +- .../isa/cash_isa_excess_savings_interest.py | 29 ------ ...ash_isa_excess_to_stocks_and_shares_isa.py | 32 ------- .../isa/cash_isa_excess_to_taxable_savings.py | 24 ----- .../hmrc/isa/cash_isa_subscription_limit.py | 8 +- .../gov/hmrc/isa/cash_isa_subscriptions.py | 9 +- .../isa/cash_isa_subscriptions_above_limit.py | 3 +- 18 files changed, 65 insertions(+), 357 deletions(-) delete mode 100644 policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/README.md delete mode 100644 policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/interest_rate.yaml delete mode 100644 policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/stocks_and_shares_isa_share.yaml delete mode 100644 policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/taxable_savings_share.yaml delete mode 100644 policyengine_uk/tests/policy/baseline/gov/hmrc/isa/cash_isa_excess_response.yaml delete mode 100644 policyengine_uk/tests/test_cash_isa_excess_response.py delete mode 100644 policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_savings_interest.py delete mode 100644 policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_to_stocks_and_shares_isa.py delete mode 100644 policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_to_taxable_savings.py diff --git a/changelog.d/2710.added.md b/changelog.d/2710.added.md index 55110e7031..099d903245 100644 --- a/changelog.d/2710.added.md +++ b/changelog.d/2710.added.md @@ -1 +1 @@ -Added the ISA annual subscription limit (£20,000) and the £12,000 cash ISA limit for people under 65 at the end of the tax year from 6 April 2027 (ISA Regulations 1998 reg 4ZA, as amended by SI 2026/1018), per-person cash and stocks-and-shares ISA subscription inputs, and `cash_isa_subscriptions_above_limit` and `isa_subscriptions_above_limit`, the subscriptions a limit leaves out. An opt-in response, `gov.contrib.behavioral_responses.cash_isa_excess`, off by default, sends cash above the cash limit to stocks-and-shares ISAs, taxable savings or other uses, and taxes the interest on the taxable part. +Added the £20,000 ISA annual subscription limit (ISA Regulations 1998 reg 4ZA), a cash ISA limit for people under an age at the end of the tax year that current law leaves unset (Budget 2026 retains £20,000 for cash, so SI 2026/1018's £12,000 for under-65s is a reform setting), per-person cash and stocks-and-shares ISA subscription inputs, and `cash_isa_subscriptions_above_limit` and `isa_subscriptions_above_limit`, the subscriptions a limit leaves out. diff --git a/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/README.md b/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/README.md deleted file mode 100644 index 1f338c54b6..0000000000 --- a/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/README.md +++ /dev/null @@ -1 +0,0 @@ -# Response to cash ISA subscriptions above the cash limit diff --git a/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/interest_rate.yaml b/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/interest_rate.yaml deleted file mode 100644 index f6730b69d8..0000000000 --- a/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/interest_rate.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: > - Annual interest rate on cash ISA subscriptions above the cash limit that are - held in taxable savings. The default is the Bank of England's effective - interest rate on individuals' outstanding time deposits in June 2026, held - for every year. -values: - 2000-01-01: 0.0336 -metadata: - label: Interest rate on cash ISA excess in taxable savings - period: year - unit: /1 - reference: - - title: Bank of England, Effective interest rates, June 2026, Table A (series Z6IW) - href: https://www.bankofengland.co.uk/statistics/effective-interest-rates/2026/june-2026 diff --git a/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/stocks_and_shares_isa_share.yaml b/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/stocks_and_shares_isa_share.yaml deleted file mode 100644 index 36ac95ecef..0000000000 --- a/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/stocks_and_shares_isa_share.yaml +++ /dev/null @@ -1,18 +0,0 @@ -description: > - Share of cash ISA subscriptions above the person's cash limit that they pay - into stocks-and-shares ISAs instead, as far as the annual subscription limit - leaves room; any part it leaves no room for is held in taxable savings. The - money is taken to be invested: uninvested cash in a stocks-and-shares ISA - would pay the charge on non-cash ISA cash interest, which this does not add. - It is zero by default, as neither HMRC nor the OBR has published a split. -values: - 2000-01-01: 0 -metadata: - label: Cash ISA excess paid into stocks-and-shares ISAs - period: year - unit: /1 - reference: - - title: HMRC, Cash ISA limit reduction, tax information and impact note (17 September 2026) - href: https://www.gov.uk/government/publications/reduction-in-the-cash-individual-savings-account-isa-limit/cash-individual-savings-account-isa-limit-reduction - - title: HM Treasury, Budget 2025 policy costings, page 79 - href: https://assets.publishing.service.gov.uk/media/692872fd2a37784b16ecf676/Budget_2025-Policy_Costings.pdf#page=79 diff --git a/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/taxable_savings_share.yaml b/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/taxable_savings_share.yaml deleted file mode 100644 index 13bd3b5146..0000000000 --- a/policyengine_uk/parameters/gov/contrib/behavioral_responses/cash_isa_excess/taxable_savings_share.yaml +++ /dev/null @@ -1,17 +0,0 @@ -description: > - Share of cash ISA subscriptions above the person's cash limit that they hold - in taxable savings instead, where the interest is taxed. What neither this - nor the stocks-and-shares ISA share covers goes to other uses, such as - spending, pensions or repaying debt, which the model does not follow. It is - zero by default, as neither HMRC nor the OBR has published a split. -values: - 2000-01-01: 0 -metadata: - label: Cash ISA excess held in taxable savings - period: year - unit: /1 - reference: - - title: HMRC, Cash ISA limit reduction, tax information and impact note (17 September 2026) - href: https://www.gov.uk/government/publications/reduction-in-the-cash-individual-savings-account-isa-limit/cash-individual-savings-account-isa-limit-reduction - - title: HM Treasury, Budget 2025 policy costings, page 79 - href: https://assets.publishing.service.gov.uk/media/692872fd2a37784b16ecf676/Budget_2025-Policy_Costings.pdf#page=79 diff --git a/policyengine_uk/parameters/gov/hmrc/isa/cash/full_limit_age.yaml b/policyengine_uk/parameters/gov/hmrc/isa/cash/full_limit_age.yaml index a95714886f..d29db18aa3 100644 --- a/policyengine_uk/parameters/gov/hmrc/isa/cash/full_limit_age.yaml +++ b/policyengine_uk/parameters/gov/hmrc/isa/cash/full_limit_age.yaml @@ -1,7 +1,9 @@ description: > Age a person must have reached by the end of the tax year (5 April) for the cash ISA subscription limit not to apply. Their cash subscriptions then count - only towards the annual subscription limit. + only towards the annual subscription limit. It matters only where a cash + limit is set, as SI 2026/1018 regulation 6 sets one for people aged 64 or + under. values: 2027-04-06: 65 metadata: @@ -9,7 +11,5 @@ metadata: period: year unit: year reference: - - title: Individual Savings Account Regulations 1998 regulation 4ZA(1ZA) - href: https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA - title: Individual Savings Account (Amendment) (No. 2) Regulations 2026 regulation 6 href: https://www.legislation.gov.uk/uksi/2026/1018/regulation/6/made diff --git a/policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml b/policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml index db52ba77ab..f0fa3ecd59 100644 --- a/policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml +++ b/policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml @@ -1,17 +1,22 @@ description: > Limit on a person's subscriptions in the tax year to cash ISAs while they are under the cash ISA full limit age at the end of the year. Cash has had no - limit of its own since 1 July 2014, so until 6 April 2027 cash subscriptions - count only towards the annual subscription limit. + limit of its own since 1 July 2014, so cash subscriptions count only towards + the annual subscription limit. SI 2026/1018 regulation 6 sets £12,000 from 6 + April 2027 for people aged 64 or under, but Budget 2026 retains the £20,000 + limit for all savers, with secondary legislation to be laid in November + 2026, so cash keeps no limit of its own here. Set this to 12,000 to model the + lower limit. values: 2017-04-06: .inf - 2027-04-06: 12000 metadata: label: Cash ISA subscription limit period: year unit: currency-GBP reference: - - title: Individual Savings Account Regulations 1998 regulation 4ZA(1ZA) + - title: Individual Savings Account Regulations 1998 regulation 4ZA href: https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA - title: Individual Savings Account (Amendment) (No. 2) Regulations 2026 regulations 1 and 6 href: https://www.legislation.gov.uk/uksi/2026/1018/regulation/6/made + - title: Budget 2026, overview of tax legislation and rates, paragraph 2.5 + href: https://www.gov.uk/government/publications/budget-2026-overview-of-tax-legislation-and-rates-ootlar/budget-2026-overview-of-tax-legislation-and-rates-ootlar diff --git a/policyengine_uk/programs.yaml b/policyengine_uk/programs.yaml index 4d0c803d6e..653e4c3f39 100644 --- a/policyengine_uk/programs.yaml +++ b/policyengine_uk/programs.yaml @@ -38,7 +38,7 @@ programs: coverage: UK variable: isa_subscriptions_above_limit verified_start_year: 2017 - notes: The annual limit (gov.hmrc.isa.annual_subscription_limit) and, from 6 April 2027, the cash limit for people under 65 at the end of the tax year (gov.hmrc.isa.cash). Reports subscriptions above each limit from per-person subscription inputs. Where cash above the cash limit goes is an opt-in assumption (gov.contrib.behavioral_responses.cash_isa_excess), off by default. Innovative finance and Junior ISAs are not modelled. + notes: The annual limit (gov.hmrc.isa.annual_subscription_limit), and a cash limit for people under an age at the end of the tax year (gov.hmrc.isa.cash) that current law leaves unset, as Budget 2026 retains £20,000 for cash; a reform can restore SI 2026/1018's £12,000 for under-65s. Reports subscriptions above each limit from per-person subscription inputs, not where they go. Innovative finance and Junior ISAs are not modelled. - id: income_tax name: Income tax diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/cash_isa_excess_response.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/cash_isa_excess_response.yaml deleted file mode 100644 index 390bb3819c..0000000000 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/cash_isa_excess_response.yaml +++ /dev/null @@ -1,76 +0,0 @@ -# The opt-in response to cash above the cash ISA limit -# (gov.contrib.behavioral_responses.cash_isa_excess): GBP 8,000 above the -# GBP 12,000 limit at the default 3.36% rate earns GBP 268.80 a year. - -- name: By default cash above the limit changes no tax - period: 2027 - input: - age: 40 - cash_isa_subscriptions: 20_000 - output: - cash_isa_subscriptions_above_limit: 8_000 - cash_isa_excess_to_stocks_and_shares_isa: 0 - cash_isa_excess_to_taxable_savings: 0 - cash_isa_excess_savings_interest: 0 - taxable_savings_interest_income: 0 - -- name: Excess held in taxable savings earns taxable interest - period: 2027 - absolute_error_margin: 0.01 - input: - gov.contrib.behavioral_responses.cash_isa_excess.taxable_savings_share: 1 - age: 40 - cash_isa_subscriptions: 20_000 - savings_interest_income: 100 - output: - cash_isa_excess_to_taxable_savings: 8_000 - cash_isa_excess_savings_interest: 268.80 - taxable_savings_interest_income: 368.80 - -- name: The excess splits between stocks and shares ISAs, taxable savings and other uses - period: 2027 - absolute_error_margin: 0.01 - input: - gov.contrib.behavioral_responses.cash_isa_excess.stocks_and_shares_isa_share: 0.5 - gov.contrib.behavioral_responses.cash_isa_excess.taxable_savings_share: 0.3 - age: 40 - cash_isa_subscriptions: 20_000 - output: - cash_isa_excess_to_stocks_and_shares_isa: 4_000 - cash_isa_excess_to_taxable_savings: 2_400 - cash_isa_excess_savings_interest: 80.64 - -- name: Cash the annual limit leaves no room for in stocks and shares ISAs goes to taxable savings - period: 2027 - absolute_error_margin: 0.01 - input: - gov.hmrc.isa.annual_subscription_limit: 16_000 - gov.contrib.behavioral_responses.cash_isa_excess.stocks_and_shares_isa_share: 1 - age: 40 - # GBP 3,000 above the cash limit; GBP 12,000 cash and GBP 3,000 stocks - # and shares leave GBP 1,000 of the annual limit. - cash_isa_subscriptions: 15_000 - stocks_and_shares_isa_subscriptions: 3_000 - output: - cash_isa_excess_to_stocks_and_shares_isa: 1_000 - cash_isa_excess_to_taxable_savings: 2_000 - cash_isa_excess_savings_interest: 67.20 - -- name: Nothing moves where no cash limit binds - period: 2027 - input: - gov.contrib.behavioral_responses.cash_isa_excess.taxable_savings_share: 1 - age: [40, 70] - cash_isa_subscriptions: [12_000, 20_000] - output: - cash_isa_excess_to_taxable_savings: [0, 0] - cash_isa_excess_savings_interest: [0, 0] - -- name: Nothing moves in fiscal 2026-27, before the cash limit - period: 2026 - input: - gov.contrib.behavioral_responses.cash_isa_excess.taxable_savings_share: 1 - age: 40 - cash_isa_subscriptions: 20_000 - output: - cash_isa_excess_savings_interest: 0 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/isa_subscription_limits.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/isa_subscription_limits.yaml index ffc150cf54..25685ba861 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/isa_subscription_limits.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/isa_subscription_limits.yaml @@ -1,6 +1,17 @@ -# ISA Regulations 1998 reg 4ZA(1) (GBP 20,000 annual limit) and reg 4ZA(1ZA), -# inserted by SI 2026/1018 reg 6 from 6 April 2027: GBP 12,000 for cash while -# a person is 64 or under at the end of the year (5 April). +# ISA Regulations 1998 reg 4ZA(1): a GBP 20,000 annual limit. Cash has had no +# limit of its own since 1 July 2014, and Budget 2026 retains that from April +# 2027 (OOTLAR para 2.5). The GBP 12,000 limit that SI 2026/1018 reg 6 set for +# people 64 or under at the end of the year (5 April) is tested as a reform. + +- name: Budget 2026 leaves cash with no limit of its own in fiscal 2027-28 + period: 2027 + input: + age: [40, 40] + cash_isa_subscriptions: [20_000, 20_001] + output: + cash_isa_subscription_limit: [20_000, 20_000] + cash_isa_subscriptions_above_limit: [0, 1] + isa_subscriptions_above_limit: [0, 1] - name: Cash has no limit of its own in fiscal 2026-27 period: 2026 @@ -10,40 +21,46 @@ output: cash_isa_subscription_limit: 20_000 cash_isa_subscriptions_above_limit: 0 - isa_subscriptions_above_limit: 0 -- name: The GBP 12,000 cash limit applies from fiscal 2027-28 +- name: Cash, stocks and shares and Lifetime ISA payments share the annual limit period: 2027 input: - age: 40 - cash_isa_subscriptions: 20_000 + age: [30, 30] + cash_isa_subscriptions: [12_000, 12_000] + stocks_and_shares_isa_subscriptions: [4_000, 4_001] + lifetime_isa_payments: [4_000, 4_000] output: - cash_isa_subscription_limit: 12_000 - cash_isa_subscriptions_above_limit: 8_000 - isa_subscriptions_above_limit: 0 + isa_subscriptions: [20_000, 20_001] + isa_subscriptions_above_limit: [0, 1] + cash_isa_subscriptions_above_limit: [0, 0] -- name: Cash exactly at the limit has no excess and a pound above it has one +- name: A GBP 12,000 cash limit applies to people under 65 at the end of the year period: 2027 input: - age: [40, 40] - cash_isa_subscriptions: [12_000, 12_001] + gov.hmrc.isa.cash.subscription_limit: 12_000 + age: [40, 40, 40] + cash_isa_subscriptions: [12_000, 12_001, 20_000] output: - cash_isa_subscriptions_above_limit: [0, 1] + cash_isa_subscription_limit: [12_000, 12_000, 12_000] + cash_isa_subscriptions_above_limit: [0, 1, 8_000] + isa_subscriptions_above_limit: [0, 0, 0] -- name: People aged 65 at the end of the year keep the annual limit for cash +- name: Under a GBP 12,000 cash limit people aged 65 at the end of the year keep the annual limit period: 2027 input: + gov.hmrc.isa.cash.subscription_limit: 12_000 # Born 6 April 1963 and 1962 (months_since_last_birthday defaults to 6): # 64 and 65 at the end of 5 April 2028. - age: [64, 65] - cash_isa_subscriptions: [20_000, 20_000] + age: [64, 65, 70, 70] + cash_isa_subscriptions: [20_000, 20_000, 20_000, 20_001] output: - cash_isa_subscription_limit: [12_000, 20_000] - cash_isa_subscriptions_above_limit: [8_000, 0] + cash_isa_subscription_limit: [12_000, 20_000, 20_000, 20_000] + cash_isa_subscriptions_above_limit: [8_000, 0, 0, 1] -- name: A 65th birthday on the last day of the year counts and one a day later does not +- name: Under a GBP 12,000 cash limit a 65th birthday on the last day of the year counts and one a day later does not period: 2027 input: + gov.hmrc.isa.cash.subscription_limit: 12_000 # Both are 64 on 6 October 2027. One turns 65 on 5 April 2028, the last # day of the tax year; the other on 6 April 2028, the first day of the next. age: [64, 64] @@ -53,46 +70,16 @@ cash_isa_subscription_limit: [20_000, 12_000] cash_isa_subscriptions_above_limit: [0, 8_000] -- name: Cash for people aged 65 or over is limited only by the annual limit - period: 2027 - input: - age: [70, 70] - cash_isa_subscriptions: [20_000, 20_001] - output: - cash_isa_subscriptions_above_limit: [0, 1] - isa_subscriptions_above_limit: [0, 1] - -- name: Cash, stocks and shares and Lifetime ISA payments share the annual limit - period: 2027 - input: - age: [30, 30] - cash_isa_subscriptions: [12_000, 12_000] - stocks_and_shares_isa_subscriptions: [4_000, 4_001] - lifetime_isa_payments: [4_000, 4_000] - output: - isa_subscriptions: [20_000, 20_001] - isa_subscriptions_above_limit: [0, 1] - cash_isa_subscriptions_above_limit: [0, 0] - -- name: Cash above both limits counts in both excesses +- name: Cash above a GBP 12,000 cash limit and the annual limit counts in both excesses period: 2027 input: + gov.hmrc.isa.cash.subscription_limit: 12_000 age: 40 cash_isa_subscriptions: 25_000 output: cash_isa_subscriptions_above_limit: 13_000 isa_subscriptions_above_limit: 5_000 -- name: Restoring a GBP 20,000 cash limit removes the cash excess - period: 2027 - input: - gov.hmrc.isa.cash.subscription_limit: 20_000 - age: 40 - cash_isa_subscriptions: 20_000 - output: - cash_isa_subscription_limit: 20_000 - cash_isa_subscriptions_above_limit: 0 - - name: A GBP 15,000 annual limit caps cash at every age and leaves the rest above it period: 2027 input: @@ -101,6 +88,6 @@ cash_isa_subscriptions: [12_000, 20_000] stocks_and_shares_isa_subscriptions: [8_000, 0] output: - cash_isa_subscription_limit: [12_000, 15_000] + cash_isa_subscription_limit: [15_000, 15_000] cash_isa_subscriptions_above_limit: [0, 5_000] isa_subscriptions_above_limit: [5_000, 5_000] diff --git a/policyengine_uk/tests/test_cash_isa_excess_response.py b/policyengine_uk/tests/test_cash_isa_excess_response.py deleted file mode 100644 index b7d48d3142..0000000000 --- a/policyengine_uk/tests/test_cash_isa_excess_response.py +++ /dev/null @@ -1,70 +0,0 @@ -"""The opt-in response to cash above the cash ISA limit changes tax, not income. - -A basic-rate taxpayer who already uses their personal savings allowance puts -GBP 20,000 into cash ISAs in fiscal 2027-28. With all of the GBP 8,000 above -the GBP 12,000 limit held in taxable savings, its interest at the default -rate is taxed at the savings basic rate. Restoring a GBP 20,000 cash limit -removes that tax, which is how a reversal of the limit scores. -""" - -import pytest - -from policyengine_uk import CountryTaxBenefitSystem, Simulation - -YEAR = 2027 -RESPONSE = "gov.contrib.behavioral_responses.cash_isa_excess" -SPAN = f"{YEAR}-01-01.{YEAR}-12-31" -TAXABLE = {f"{RESPONSE}.taxable_savings_share": {SPAN: 1}} -SITUATION = { - "people": { - "saver": { - "age": {YEAR: 40}, - "employment_income": {YEAR: 30_000}, - "savings_interest_income": {YEAR: 1_000}, - "cash_isa_subscriptions": {YEAR: 20_000}, - } - }, - "benunits": {"benunit": {"members": ["saver"]}}, - "households": {"household": {"members": ["saver"], "region": "LONDON"}}, -} - - -def _calculate(reform, variable): - simulation = Simulation(situation=SITUATION, reform=reform) - return float(simulation.calculate(variable, YEAR)[0]) - - -def test_response_taxes_the_displaced_interest_without_adding_income(): - parameters = CountryTaxBenefitSystem().parameters - rate = parameters.get_child(f"{RESPONSE}.interest_rate")(f"{YEAR}-04-30") - savings_basic_rate = parameters.gov.hmrc.income_tax.rates.savings.basic( - f"{YEAR}-04-30" - ) - expected_tax = 8_000 * rate * savings_basic_rate - - tax_change = _calculate(TAXABLE, "income_tax") - _calculate({}, "income_tax") - assert tax_change == pytest.approx(expected_tax, abs=0.01) - for variable in ("household_market_income", "household_gross_income"): - assert _calculate(TAXABLE, variable) == pytest.approx( - _calculate({}, variable), abs=0.01 - ), variable - - -def test_restoring_the_cash_limit_removes_the_tax_on_displaced_interest(): - restored = { - **TAXABLE, - "gov.hmrc.isa.cash.subscription_limit": {SPAN: 20_000}, - } - assert _calculate(restored, "cash_isa_excess_savings_interest") == 0 - assert _calculate(restored, "income_tax") == pytest.approx( - _calculate({}, "income_tax"), abs=0.01 - ) - - -def test_shares_above_one_are_rejected(): - reform = { - f"{RESPONSE}.stocks_and_shares_isa_share": {SPAN: 0.6}, - f"{RESPONSE}.taxable_savings_share": {SPAN: 0.6}, - } - with pytest.raises(ValueError, match="sum to at most one"): - _calculate(reform, "cash_isa_excess_savings_interest") diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/bases/savings_income/taxable_savings_interest_income.py b/policyengine_uk/variables/gov/hmrc/income_tax/bases/savings_income/taxable_savings_interest_income.py index f39d61797e..a70649905c 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/bases/savings_income/taxable_savings_interest_income.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/bases/savings_income/taxable_savings_interest_income.py @@ -15,10 +15,7 @@ class taxable_savings_interest_income(Variable): def formula(person, period, parameters): standard_interest = person("savings_interest_income", period) other_interest = person("other_investment_income", period) - # Zero unless a reform sets the cash ISA excess response: interest on - # cash a cash ISA limit keeps out, held in taxable savings instead. - displaced_interest = person("cash_isa_excess_savings_interest", period) - total_interest = standard_interest + other_interest + displaced_interest + total_interest = standard_interest + other_interest # Exempt interest is a separate additive input, not an allowance # against interest from unrelated taxable savings. return max_(0, total_interest) diff --git a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_savings_interest.py b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_savings_interest.py deleted file mode 100644 index 3e358a0d56..0000000000 --- a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_savings_interest.py +++ /dev/null @@ -1,29 +0,0 @@ -from policyengine_uk.model_api import * - - -class cash_isa_excess_savings_interest(Variable): - value_type = float - entity = Person - label = "Interest on cash ISA excess held in taxable savings" - documentation = ( - "Interest on the cash ISA subscriptions above the cash limit that the " - "person holds in taxable savings, at the rate in " - "gov.contrib.behavioral_responses.cash_isa_excess. It is taxed as " - "savings interest but not added to income: the person earns it either " - "way, tax-free in a cash ISA or taxed outside one, so a cash limit " - "changes the tax and not the gross income. It is a full year's " - "interest on the year's excess and leaves out cash moved out of ISAs " - "in earlier years, which a static annual model does not hold. Zero " - "unless a reform sets the response's shares." - ) - definition_period = YEAR - unit = GBP - quantity_type = FLOW - - def formula(person, period, parameters): - p = parameters(period).gov.contrib.behavioral_responses.cash_isa_excess - responds = p.stocks_and_shares_isa_share > 0 or p.taxable_savings_share > 0 - if not responds or p.interest_rate == 0: - return 0 - held = person("cash_isa_excess_to_taxable_savings", period) - return held * p.interest_rate diff --git a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_to_stocks_and_shares_isa.py b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_to_stocks_and_shares_isa.py deleted file mode 100644 index 2325cf46c9..0000000000 --- a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_to_stocks_and_shares_isa.py +++ /dev/null @@ -1,32 +0,0 @@ -from policyengine_uk.model_api import * - - -class cash_isa_excess_to_stocks_and_shares_isa(Variable): - value_type = float - entity = Person - label = "Cash ISA excess paid into stocks-and-shares ISAs" - documentation = ( - "Cash ISA subscriptions above the cash limit that the person pays into " - "stocks-and-shares ISAs instead: the stocks-and-shares ISA share in " - "gov.contrib.behavioral_responses.cash_isa_excess, up to the room the " - "annual subscription limit leaves after the cash kept in cash ISAs and " - "the other ISA subscriptions. Zero unless a reform sets that share. " - "It is not added to stocks_and_shares_isa_subscriptions." - ) - definition_period = YEAR - unit = GBP - quantity_type = FLOW - - def formula(person, period, parameters): - p = parameters(period).gov.contrib.behavioral_responses.cash_isa_excess - shares = [p.stocks_and_shares_isa_share, p.taxable_savings_share] - if min(shares) < 0 or sum(shares) > 1: - raise ValueError( - "The shares in gov.contrib.behavioral_responses.cash_isa_excess " - "must be non-negative and sum to at most one" - ) - excess = person("cash_isa_subscriptions_above_limit", period) - kept = person("isa_subscriptions", period) - excess - limit = parameters(period).gov.hmrc.isa.annual_subscription_limit - room = max_(limit - kept, 0) - return min_(excess * p.stocks_and_shares_isa_share, room) diff --git a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_to_taxable_savings.py b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_to_taxable_savings.py deleted file mode 100644 index eaa8f7b85e..0000000000 --- a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_excess_to_taxable_savings.py +++ /dev/null @@ -1,24 +0,0 @@ -from policyengine_uk.model_api import * - - -class cash_isa_excess_to_taxable_savings(Variable): - value_type = float - entity = Person - label = "Cash ISA excess held in taxable savings" - documentation = ( - "Cash ISA subscriptions above the cash limit that the person holds in " - "taxable savings instead: the taxable savings share in " - "gov.contrib.behavioral_responses.cash_isa_excess, plus any cash meant " - "for a stocks-and-shares ISA that the annual limit leaves no room for. " - "Zero unless a reform sets those shares." - ) - definition_period = YEAR - unit = GBP - quantity_type = FLOW - - def formula(person, period, parameters): - p = parameters(period).gov.contrib.behavioral_responses.cash_isa_excess - excess = person("cash_isa_subscriptions_above_limit", period) - moved = person("cash_isa_excess_to_stocks_and_shares_isa", period) - no_room = excess * p.stocks_and_shares_isa_share - moved - return excess * p.taxable_savings_share + no_room diff --git a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscription_limit.py b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscription_limit.py index 2e26fba286..11b7d58a2a 100644 --- a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscription_limit.py +++ b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscription_limit.py @@ -10,9 +10,11 @@ class cash_isa_subscription_limit(Variable): "The most the person can subscribe to cash ISAs in the tax year: the " "cash ISA subscription limit while they are under the full limit age " "at the end of the year, and otherwise the annual subscription limit, " - "which also caps the cash limit. The age at the end of the year (5 " - "April) comes from the date of birth, so a person aged 64 on 6 October " - "whose 65th birthday falls by 5 April has the annual limit." + "which also caps the cash limit. Current law sets no cash limit of its " + "own, so this is the annual limit unless a reform sets one. The age at " + "the end of the year (5 April) comes from the date of birth, so a " + "person aged 64 on 6 October whose 65th birthday falls by 5 April has " + "the annual limit." ) definition_period = YEAR unit = GBP diff --git a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py index da0e49dbbd..ca2b568199 100644 --- a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py +++ b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py @@ -11,11 +11,10 @@ class cash_isa_subscriptions(Variable): "ISAs, interest and growth, and replacements of cash withdrawn from a " "flexible ISA earlier in the same tax year, which do not count towards " "the limits. The model compares this amount with the limits in force " - "and does not cap it: data collected under a higher limit, such as " - "before the 6 April 2027 cash limit, can exceed a later limit, and " - "cash_isa_subscriptions_above_limit reports the part that cannot be " - "paid in. Amounts are not uprated, as the limits are fixed in cash " - "terms." + "and does not cap it: under a lower limit, such as a reform that " + "brings in a £12,000 cash limit, cash_isa_subscriptions_above_limit " + "reports the part that cannot be paid in. Amounts are not uprated, as " + "the limits are fixed in cash terms." ) definition_period = YEAR unit = GBP diff --git a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions_above_limit.py b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions_above_limit.py index 4fbeaffc0a..8d9856a0d0 100644 --- a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions_above_limit.py +++ b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions_above_limit.py @@ -9,8 +9,7 @@ class cash_isa_subscriptions_above_limit(Variable): "Cash ISA subscriptions above the cash limit that applies to the " "person. It is zero for data that met the limits in force when it was " "collected; under a lower limit it is cash that cannot go into a cash " - "ISA. gov.contrib.behavioral_responses.cash_isa_excess sets where it " - "goes, and by default none of it changes tax. Cash above the annual " + "ISA, and the model does not say where it goes. Cash above the annual " "limit also counts in isa_subscriptions_above_limit, so the two " "overlap and must not be added." ) From da16986dbff71612db42622f0db4630ce5872a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:53:41 +0000 Subject: [PATCH 06/11] Charge 20% on Lifetime ISA withdrawals for homes above the price cap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SI 2026/1130 substitutes Schedule paragraph 5 and inserts paragraph 6A: from 29 October 2026, a withdrawal that would be a first-time residential purchase but for the £450,000 price cap in paragraph 6(3) pays 20%, and other withdrawals that are not exempt still pay 25%. Add gov.hmrc.isa.lifetime.above_cap_purchase (in_effect from 29 October 2026, withdrawal_charge 0.2). lifetime_isa_withdrawal_charge charges the home payment of such a purchase at that rate and the rest at withdrawal_charge. Parameters are read on 30 April, so the model applies the rate from fiscal 2027-28; the parameter documentation says so. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2710-lisa-above-cap.added.md | 1 + .../isa/lifetime/above_cap_purchase/README.md | 1 + .../above_cap_purchase/in_effect.yaml | 17 ++++++ .../above_cap_purchase/withdrawal_charge.yaml | 16 +++++ policyengine_uk/programs.yaml | 2 +- .../isa/lifetime_isa_withdrawal_charge.yaml | 61 +++++++++++++++++++ .../isa/lifetime_isa_withdrawal_charge.py | 22 +++++-- ...time_isa_withdrawals_towards_first_home.py | 2 +- 8 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 changelog.d/2710-lisa-above-cap.added.md create mode 100644 policyengine_uk/parameters/gov/hmrc/isa/lifetime/above_cap_purchase/README.md create mode 100644 policyengine_uk/parameters/gov/hmrc/isa/lifetime/above_cap_purchase/in_effect.yaml create mode 100644 policyengine_uk/parameters/gov/hmrc/isa/lifetime/above_cap_purchase/withdrawal_charge.yaml diff --git a/changelog.d/2710-lisa-above-cap.added.md b/changelog.d/2710-lisa-above-cap.added.md new file mode 100644 index 0000000000..262b681f53 --- /dev/null +++ b/changelog.d/2710-lisa-above-cap.added.md @@ -0,0 +1 @@ +Added the 20% Lifetime ISA withdrawal charge for a first-home purchase above the £450,000 property price cap that meets every other condition, for withdrawals from 29 October 2026 (ISA Regulations 1998 Schedule paragraphs 5 and 6A, inserted by SI 2026/1130); the annual model applies it from fiscal 2027-28. diff --git a/policyengine_uk/parameters/gov/hmrc/isa/lifetime/above_cap_purchase/README.md b/policyengine_uk/parameters/gov/hmrc/isa/lifetime/above_cap_purchase/README.md new file mode 100644 index 0000000000..e6e2975d7a --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/isa/lifetime/above_cap_purchase/README.md @@ -0,0 +1 @@ +# First-time purchase above the property price cap diff --git a/policyengine_uk/parameters/gov/hmrc/isa/lifetime/above_cap_purchase/in_effect.yaml b/policyengine_uk/parameters/gov/hmrc/isa/lifetime/above_cap_purchase/in_effect.yaml new file mode 100644 index 0000000000..c738b1ddfd --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/isa/lifetime/above_cap_purchase/in_effect.yaml @@ -0,0 +1,17 @@ +description: > + Whether a Lifetime ISA withdrawal towards a first home that meets every + first-time purchase condition except the property price cap pays the reduced + above-cap charge, rather than the withdrawal charge. It has effect for + withdrawals from 29 October 2026. The annual model reads parameters on 30 + April, so it applies from fiscal 2027-28: in the model, a withdrawal from 29 + October 2026 to 5 April 2027 pays the withdrawal charge. +values: + 2000-01-01: false + 2026-10-29: true +metadata: + label: Lifetime ISA reduced charge above the price cap in effect + period: year + unit: bool + reference: + - title: Individual Savings Account (Amendment) (No. 3) Regulations 2026 regulation 1 + href: https://www.legislation.gov.uk/uksi/2026/1130/regulation/1/made diff --git a/policyengine_uk/parameters/gov/hmrc/isa/lifetime/above_cap_purchase/withdrawal_charge.yaml b/policyengine_uk/parameters/gov/hmrc/isa/lifetime/above_cap_purchase/withdrawal_charge.yaml new file mode 100644 index 0000000000..1950e28d1b --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/isa/lifetime/above_cap_purchase/withdrawal_charge.yaml @@ -0,0 +1,16 @@ +description: > + Share of a Lifetime ISA withdrawal used towards a first home above the + property price cap that is charged, where the purchase meets every other + first-time purchase condition. Other withdrawals that are not exempt pay the + withdrawal charge. +values: + 2026-10-29: 0.2 +metadata: + label: Lifetime ISA withdrawal charge above the price cap + period: year + unit: /1 + reference: + - title: Individual Savings Account Regulations 1998, Schedule paragraphs 5 and 6A + href: https://www.legislation.gov.uk/uksi/1998/1870/schedule + - title: Individual Savings Account (Amendment) (No. 3) Regulations 2026 regulation 5 + href: https://www.legislation.gov.uk/uksi/2026/1130/regulation/5/made diff --git a/policyengine_uk/programs.yaml b/policyengine_uk/programs.yaml index 653e4c3f39..65ad4ec7b3 100644 --- a/policyengine_uk/programs.yaml +++ b/policyengine_uk/programs.yaml @@ -15,7 +15,7 @@ programs: variable: lifetime_isa_bonus parameter_prefix: gov.hmrc.isa.lifetime verified_start_year: 2017 - notes: Annual qualifying-payment bonus and gross-withdrawal charge, including individual first-home conditions. Assumes valid account opening and transaction qualification; does not update balances. Late-March 2020 withdrawals are outside the annual timing resolution. + notes: Annual qualifying-payment bonus and gross-withdrawal charge, including individual first-home conditions and the 20% charge for purchases above the price cap from 29 October 2026 (fiscal 2027-28 in the model). Assumes valid account opening and transaction qualification; does not update balances. Late-March 2020 withdrawals are outside the annual timing resolution. - id: isa_cash_interest_charge name: Non-cash ISA cash-interest charge diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/lifetime_isa_withdrawal_charge.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/lifetime_isa_withdrawal_charge.yaml index 284ace1c85..037eb96523 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/lifetime_isa_withdrawal_charge.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/lifetime_isa_withdrawal_charge.yaml @@ -1,4 +1,6 @@ # ISA Regulations1998 Schedule paras4-7 and5A. +# SI2026/1130: from29October2026 a purchase failing only the para6(3) price +# cap pays20 percent (paras5(a) and6A); fiscal2027 in the annual model. # Withdrawals are GROSS, excluding exempt inter-LISA transfers and death. # Annual2020/2021 use the existing30April fiscal-year parameter convention. @@ -92,3 +94,62 @@ main_residential_property_purchased_is_in_uk: [true, true, true, true, true] output: lifetime_isa_withdrawal_charge: [500, 0, 1_000, 0, 500] + +- name: From fiscal2027-28 a first-home purchase above the price cap pays20 percent + period: 2027 + input: + age: [35, 35, 35, 60] + lifetime_isa_withdrawals: [10_000, 10_000, 10_000, 10_000] + lifetime_isa_withdrawals_towards_first_home: [10_000, 10_000, 10_000, 10_000] + lifetime_isa_first_time_buyer: [true, true, true, true] + lifetime_isa_months_since_first_payment: [12, 12, 12, 12] + main_residential_property_purchased: [450_000, 450_001, 600_000, 600_000] + main_residential_property_purchased_with_mortgage: [true, true, true, true] + main_residential_property_purchased_is_in_uk: [true, true, true, true] + output: + # At the cap the purchase is exempt; above it20 percent; age60 is exempt. + lifetime_isa_withdrawal_charge: [0, 2_000, 2_000, 0] + +- name: Before29October2026 a first-home purchase above the price cap pays25 percent + period: 2026 + input: + age: [35, 35] + lifetime_isa_withdrawals: [10_000, 10_000] + lifetime_isa_withdrawals_towards_first_home: [10_000, 10_000] + lifetime_isa_first_time_buyer: [true, true] + lifetime_isa_months_since_first_payment: [12, 12] + main_residential_property_purchased: [450_001, 600_000] + main_residential_property_purchased_with_mortgage: [true, true] + main_residential_property_purchased_is_in_uk: [true, true] + output: + lifetime_isa_withdrawal_charge: [2_500, 2_500] + +- name: Above the price cap only a purchase meeting every other condition pays20 percent + period: 2027 + input: + age: [35, 35, 35, 35] + lifetime_isa_withdrawals: [10_000, 10_000, 10_000, 10_000] + lifetime_isa_withdrawals_towards_first_home: [10_000, 10_000, 10_000, 10_000] + lifetime_isa_first_time_buyer: [false, true, true, true] + lifetime_isa_months_since_first_payment: [12, 11, 12, 12] + main_residential_property_purchased: [600_000, 600_000, 600_000, 600_000] + main_residential_property_purchased_with_mortgage: [true, true, false, true] + main_residential_property_purchased_is_in_uk: [true, true, true, false] + output: + lifetime_isa_withdrawal_charge: [2_500, 2_500, 2_500, 2_500] + +- name: A withdrawal not paid towards the home keeps the25 percent charge above the cap + period: 2027 + input: + age: [35, 35] + lifetime_isa_withdrawals: [12_000, 12_000] + # The first pays10000 of12000 to the conveyancer; the second pays none. + lifetime_isa_withdrawals_towards_first_home: [10_000, 0] + lifetime_isa_first_time_buyer: [true, true] + lifetime_isa_months_since_first_payment: [12, 12] + main_residential_property_purchased: [600_000, 600_000] + main_residential_property_purchased_with_mortgage: [true, true] + main_residential_property_purchased_is_in_uk: [true, true] + output: + # 10000 x20% + 2000 x25%, and 12000 x25%. + lifetime_isa_withdrawal_charge: [2_500, 3_000] diff --git a/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_withdrawal_charge.py b/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_withdrawal_charge.py index f5c6311a4c..4b6f88fcdf 100644 --- a/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_withdrawal_charge.py +++ b/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_withdrawal_charge.py @@ -5,7 +5,7 @@ class lifetime_isa_withdrawal_charge(Variable): value_type = float entity = Person label = "Lifetime ISA withdrawal charge" - documentation = "Charge deducted from non-exempt gross Lifetime ISA withdrawals. The model records this capital payment as government revenue, outside household current-income taxes; a supplied balance is not automatically updated. Annual rates use the model's fiscal-year parameter sampling." + documentation = "Charge deducted from non-exempt gross Lifetime ISA withdrawals. From 29 October 2026, the part paid towards a first home that meets every first-time purchase condition except the property price cap pays the reduced above-cap charge (Schedule paragraph 6A); the annual model applies it from fiscal 2027-28. The model records this capital payment as government revenue, outside household current-income taxes; a supplied balance is not automatically updated. Annual rates use the model's fiscal-year parameter sampling." definition_period = YEAR unit = GBP quantity_type = FLOW @@ -18,7 +18,8 @@ def formula(person, period, parameters): return 0 gross = max_(person("lifetime_isa_withdrawals", period), 0) price = person.household("main_residential_property_purchased", period) - qualifying_home = ( + # Schedule paragraph 6, apart from the price cap in 6(3). + first_home = ( person("lifetime_isa_first_time_buyer", period) & person.household("main_residential_property_purchased_is_in_uk", period) & person.household( @@ -29,8 +30,12 @@ def formula(person, period, parameters): >= p.minimum_holding_period ) & (price > 0) - & (price <= p.property_price_cap) ) + within_cap = price <= p.property_price_cap + qualifying_home = first_home & within_cap + # Paragraph 6A: a purchase that fails only the price cap pays the + # reduced above-cap charge instead of the withdrawal charge. + above_cap_home = first_home & ~within_cap & p.above_cap_purchase.in_effect home_payment = min_( max_(person("lifetime_isa_withdrawals_towards_first_home", period), 0), min_(gross, max_(price, 0)), @@ -38,5 +43,12 @@ def formula(person, period, parameters): exempt = (person("age", period) >= p.charge_free_age) | person( "lifetime_isa_terminally_ill", period ) - chargeable = max_(gross - where(qualifying_home, home_payment, 0), 0) - return where(exempt, 0, chargeable * p.withdrawal_charge) + above_cap_payment = where(above_cap_home, home_payment, 0) + chargeable = max_( + gross - where(qualifying_home, home_payment, 0) - above_cap_payment, 0 + ) + charge = ( + chargeable * p.withdrawal_charge + + above_cap_payment * p.above_cap_purchase.withdrawal_charge + ) + return where(exempt, 0, charge) diff --git a/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_withdrawals_towards_first_home.py b/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_withdrawals_towards_first_home.py index ac483c4eea..c81acb696b 100644 --- a/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_withdrawals_towards_first_home.py +++ b/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_withdrawals_towards_first_home.py @@ -5,7 +5,7 @@ class lifetime_isa_withdrawals_towards_first_home(Variable): value_type = float entity = Person label = "Lifetime ISA withdrawals paid for a first home" - documentation = "Part of gross Lifetime ISA withdrawals paid directly to an eligible conveyancer towards the purchase price of a first home, with the required declarations and completion or return procedures satisfied. Exclude fees and amounts paid through the investor. Ownership, UK location, financing, holding period and price conditions are checked separately." + documentation = "Part of gross Lifetime ISA withdrawals paid directly to an eligible conveyancer towards the purchase price of a first home, with the required declarations and completion or return procedures satisfied. Exclude fees and amounts paid through the investor. Include a purchase above the property price cap, which pays a reduced charge from 29 October 2026. Ownership, UK location, financing, holding period and price conditions are checked separately." definition_period = YEAR unit = GBP quantity_type = FLOW From 00adbd1e74858c41d95c6c1222bb2363dd57575c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:53:41 +0000 Subject: [PATCH 07/11] Update the ISA documentation for Budget 2026 and the above-cap charge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The subscription limits section now says cash has no limit of its own, with £12,000 as a reform setting, and that the model reports the amounts above each limit and no more, without the opt-in response. The Lifetime ISA section describes the 20% charge above the price cap and its fiscal-year timing, the coverage note names the First Time Buyer ISA, and the dataset-sources guide points producers to microcosm#1415. Co-Authored-By: Claude Opus 5.5 --- .../isa-inputs-and-capital-flows.md | 92 +++++++++---------- docs/engineering/skills/dataset-sources.md | 9 +- 2 files changed, 48 insertions(+), 53 deletions(-) diff --git a/docs/book/assumptions/isa-inputs-and-capital-flows.md b/docs/book/assumptions/isa-inputs-and-capital-flows.md index c8f6af4936..152d2ebe20 100644 --- a/docs/book/assumptions/isa-inputs-and-capital-flows.md +++ b/docs/book/assumptions/isa-inputs-and-capital-flows.md @@ -177,6 +177,17 @@ Only the bounded qualifying home-payment portion is exempt. The residual gross withdrawal is charged at 25%, or 20% during the temporary coronavirus period. These rules come from [Schedule paragraphs 4–7 and 5A](https://www.legislation.gov.uk/uksi/1998/1870/schedule). +From 29 October 2026, a purchase that meets every one of these conditions +except the £450,000 price cap pays 20% on the home payment instead of 25% +(Schedule paragraphs 5 and 6A, inserted by +[SI 2026/1130](https://www.legislation.gov.uk/uksi/2026/1130/made)). The rest +of the withdrawal still pays 25%. `gov.hmrc.isa.lifetime.above_cap_purchase` +holds the rate and its start. Parameters are read on 30 April, so the model +applies the 20% rate from fiscal 2027–28; a withdrawal from 29 October 2026 to +5 April 2027 pays 25% in the model. When such a purchase fails and the money +is returned, the charge is restored to the account (paragraph 10A), so a +returned withdrawal should be left out of the inputs. + The model records `lifetime_isa_bonus` in government spending and `lifetime_isa_withdrawal_charge` in government revenue. Both are capital flows, excluded from household current benefits, taxes and net income. The @@ -188,16 +199,20 @@ accounting convention for the model's current-income measures. `gov.hmrc.isa.annual_subscription_limit` is the £20,000 limit on a person's subscriptions in a tax year to all their ISAs other than Junior ISAs ([ISA Regulations regulation 4ZA(1)](https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA)). -From 6 April 2027, `gov.hmrc.isa.cash.subscription_limit` limits cash ISA -subscriptions to £12,000 for a person who is 64 or under at the end of the -tax year (regulation 4ZA(1ZA), inserted by -[SI 2026/1018 regulation 6](https://www.legislation.gov.uk/uksi/2026/1018/regulation/6/made)). -Cash has had no limit of its own since 1 July 2014, so before 2027 the -parameter is infinite and cash counts only towards the annual limit. -`gov.hmrc.isa.cash.full_limit_age` (65) is the age at which the cash limit -stops applying. `cash_isa_subscription_limit` reads the age at the end of the -year (5 April) from the date of birth, so a person aged 64 on 6 October whose -65th birthday falls on or before 5 April has the annual limit for cash. +Cash has had no limit of its own since 1 July 2014, so +`gov.hmrc.isa.cash.subscription_limit` is infinite and cash counts only +towards the annual limit. +[SI 2026/1018 regulation 6](https://www.legislation.gov.uk/uksi/2026/1018/regulation/6/made) +sets a £12,000 cash limit from 6 April 2027 for a person who is 64 or under +at the end of the tax year, but Budget 2026 retains the £20,000 cash limit +for all savers, with secondary legislation to be laid in November 2026 +([OOTLAR paragraph 2.5](https://www.gov.uk/government/publications/budget-2026-overview-of-tax-legislation-and-rates-ootlar/budget-2026-overview-of-tax-legislation-and-rates-ootlar)). +Current law here therefore has no cash limit, and the £12,000 is a reform +setting. `gov.hmrc.isa.cash.full_limit_age` (65) is the age at which a cash +limit stops applying. `cash_isa_subscription_limit` reads the age at the end +of the year (5 April) from the date of birth, so a person aged 64 on +6 October whose 65th birthday falls on or before 5 April has the annual limit +for cash. The inputs are per-person flows in the year: `cash_isa_subscriptions`, `stocks_and_shares_isa_subscriptions` and the existing @@ -215,39 +230,17 @@ The model compares the inputs with the limits in force and does not cap them. `cash_isa_subscriptions_above_limit` is cash above the person's cash limit, and `isa_subscriptions_above_limit` is the total above the annual limit. Cash above both limits counts in both, so the two must not be added. Both are zero -for data that met the limits in force when it was collected. Data collected -under a higher limit, such as subscriptions from before April 2027, shows -what a lower limit leaves out: scoring the 2027 cash limit, or its reversal, -compares that excess under the two sets of limits. - -By default the excess changes no tax. -`gov.contrib.behavioral_responses.cash_isa_excess` is an opt-in assumption -about where cash above the cash limit goes: - -- a share into stocks-and-shares ISAs, within the room the annual limit - leaves, with any part it leaves no room for held in taxable savings; -- a share into taxable savings; -- the rest to other uses the model does not follow, such as spending, - pensions or repaying debt. - -Both shares are zero by default. Neither -[HMRC's impact note](https://www.gov.uk/government/publications/reduction-in-the-cash-individual-savings-account-isa-limit/cash-individual-savings-account-isa-limit-reduction) -nor the Budget 2025 policy costing (page 79) publishes a split. The taxable -part earns `interest_rate`, by default 3.36%, the Bank of England's effective -rate on individuals' outstanding time deposits in June 2026. -`cash_isa_excess_savings_interest` enters `taxable_savings_interest_income` -but not market income: the person earns the interest either way, tax-free in -a cash ISA or taxed outside one, so the response changes tax and not gross -income. It is a full year's interest on the year's excess. A static annual -model holds no balances, so it leaves out interest on cash moved out of ISAs -in earlier years; later years need that as a supplied input. Money above the -annual limit is reported only. - -For example, a person aged 40 paying £20,000 into cash ISAs in 2027–28 has -£8,000 above the £12,000 limit. With all of it in taxable savings, it earns -£268.80 at the default rate. For a basic-rate taxpayer who has used their -personal savings allowance, that is £59.14 of income tax at the 22% savings -basic rate. +for data that met the limits in force when it was collected. A reform that +lowers a limit reports what it leaves out. For example, a person aged 40 +paying £20,000 into cash ISAs in 2027–28 has £8,000 above a £12,000 cash +limit. + +The model reports these amounts and no more. It does not say where money +above a limit goes or change any tax for it: the model is static by year and +the data has no ISA history, so it holds no balances moved out of ISAs in +earlier years. A population run gives the first year's amount above a limit; +the tax on money moved out of ISAs over several years rests on the official +costing. ## Annual resolution and coverage @@ -261,10 +254,11 @@ status. Multiple transactions with different conditions require externally prepared qualifying amounts. Help to Buy ISA bonuses are not modelled here. Junior ISAs, Child Trust -Funds, transfers between ISA types (including the 2027 restrictions on -transfers into cash ISAs) and a proposed First Time Buyer ISA are also -outside this implementation. Population data without payment, withdrawal, -subscription or non-cash-interest detail inputs retain zero for those flows; -data without per-person ISA balances keeps its stored household totals. -Household tests establish the rules; population effects require a separate +Funds, transfers between ISA types (including the SI 2026/1018 restrictions on +transfers into cash ISAs) and the First Time Buyer ISA announced at Budget +2026 for April 2029 are also outside this implementation. Population data +without payment, withdrawal, subscription or non-cash-interest detail inputs +retain zero for those flows; data without per-person ISA balances keeps its +stored household totals. Household tests establish the rules; population +effects require a separate certified dataset measurement. diff --git a/docs/engineering/skills/dataset-sources.md b/docs/engineering/skills/dataset-sources.md index a29a31a681..dbc8df6811 100644 --- a/docs/engineering/skills/dataset-sources.md +++ b/docs/engineering/skills/dataset-sources.md @@ -141,10 +141,11 @@ the model does not reconcile the two. annual person flows that count towards the ISA subscription limits, with `lifetime_isa_payments` for Lifetime ISAs. Projection carries them flat, since the limits are fixed in cash terms. They are not capped at the limits: -a producer should export subscriptions as observed, so that data from before -the 6 April 2027 cash limit shows what that limit leaves out. Absent columns -default to zero, so a change to an ISA limit changes nothing on such data. See -[issue #2710](https://github.com/PolicyEngine/policyengine-uk/issues/2710). +a producer should export subscriptions as observed, so that a reform that +lowers a limit shows what it leaves out. Absent columns default to zero, so a +change to an ISA limit changes nothing on such data. +[Microcosm issue 1415](https://github.com/PolicyEngine/microcosm/issues/1415) +covers imputing the subscriptions and balances. ## Defaults From 18aa2a46c689d483a27ac6ff069efe1a8810c8d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:46:03 +0000 Subject: [PATCH 08/11] Ask producers to draw the three ISA subscription inputs jointly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vahid's review of microcosm#1415 (finding 5): cash, stocks-and-shares and Lifetime ISA subscriptions share the £20,000 annual limit, and HMRC's Table 9.7 leaves Lifetime ISAs out. The dataset-sources guide now asks for a joint draw, so no one is above the limit in the baseline. The subscription inputs say they sum over all of a person's accounts of a type, since a person can pay into more than one in a year (his finding 3). Co-Authored-By: Claude Opus 5.5 --- docs/engineering/skills/dataset-sources.md | 17 +++++++++++------ .../gov/hmrc/isa/cash_isa_subscriptions.py | 11 ++++++----- .../isa/stocks_and_shares_isa_subscriptions.py | 5 +++-- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/docs/engineering/skills/dataset-sources.md b/docs/engineering/skills/dataset-sources.md index dbc8df6811..88e2630d30 100644 --- a/docs/engineering/skills/dataset-sources.md +++ b/docs/engineering/skills/dataset-sources.md @@ -138,12 +138,17 @@ either stop exporting the household totals or keep them equal to the sums; the model does not reconcile the two. `cash_isa_subscriptions` and `stocks_and_shares_isa_subscriptions` are -annual person flows that count towards the ISA subscription limits, with -`lifetime_isa_payments` for Lifetime ISAs. Projection carries them flat, -since the limits are fixed in cash terms. They are not capped at the limits: -a producer should export subscriptions as observed, so that a reform that -lowers a limit shows what it leaves out. Absent columns default to zero, so a -change to an ISA limit changes nothing on such data. +annual person flows, summed over all of a person's accounts of each type, that +count towards the ISA subscription limits, with `lifetime_isa_payments` for +Lifetime ISAs. Projection carries them flat, since the limits are fixed in +cash terms. The model does not cap them at the limits: a producer should +export subscriptions as observed, so a reform that lowers a limit shows what +it leaves out. Observed subscriptions stay within the £20,000 annual +limit, which the three inputs share, so an imputation should draw them +jointly. Drawn separately, for example from tables that leave Lifetime ISAs +out, they can sum above the limit and put people above it in the baseline, +where `isa_subscriptions_above_limit` should be zero. Absent columns default +to zero, so a change to an ISA limit changes nothing on such data. [Microcosm issue 1415](https://github.com/PolicyEngine/microcosm/issues/1415) covers imputing the subscriptions and balances. diff --git a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py index ca2b568199..1d76f1e7d2 100644 --- a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py +++ b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py @@ -6,11 +6,12 @@ class cash_isa_subscriptions(Variable): entity = Person label = "Cash ISA subscriptions" documentation = ( - "New money paid into cash ISAs in the tax year, which counts towards " - "the cash and annual subscription limits. Exclude transfers between " - "ISAs, interest and growth, and replacements of cash withdrawn from a " - "flexible ISA earlier in the same tax year, which do not count towards " - "the limits. The model compares this amount with the limits in force " + "New money paid into cash ISAs in the tax year, summed over all the " + "person's cash ISAs, as a person can pay into more than one in a year. " + "It counts towards the cash and annual subscription limits. Exclude " + "transfers between ISAs, interest and growth, and replacements of cash " + "withdrawn from a flexible ISA earlier in the same tax year, which do " + "not count towards the limits. The model compares this amount with the limits in force " "and does not cap it: under a lower limit, such as a reform that " "brings in a £12,000 cash limit, cash_isa_subscriptions_above_limit " "reports the part that cannot be paid in. Amounts are not uprated, as " diff --git a/policyengine_uk/variables/gov/hmrc/isa/stocks_and_shares_isa_subscriptions.py b/policyengine_uk/variables/gov/hmrc/isa/stocks_and_shares_isa_subscriptions.py index 908df95b35..fd05d5db6e 100644 --- a/policyengine_uk/variables/gov/hmrc/isa/stocks_and_shares_isa_subscriptions.py +++ b/policyengine_uk/variables/gov/hmrc/isa/stocks_and_shares_isa_subscriptions.py @@ -6,8 +6,9 @@ class stocks_and_shares_isa_subscriptions(Variable): entity = Person label = "Stocks and shares ISA subscriptions" documentation = ( - "New money paid into stocks-and-shares ISAs in the tax year, including " - "eligible shares subscribed by transfer, which counts towards the annual " + "New money paid into stocks-and-shares ISAs in the tax year, summed " + "over all the person's stocks-and-shares ISAs and including eligible " + "shares subscribed by transfer. It counts towards the annual " "subscription limit. Exclude transfers between ISAs, income and growth, " "and replacements of cash withdrawn from a flexible ISA earlier in the " "same tax year. Innovative finance ISAs are not included. Amounts are " From 1915a91bc7c8cc7a0383a67d987d2639d67b79a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:20:52 +0000 Subject: [PATCH 09/11] Address Pavel's review of the ISA limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Give the real reason the subscription inputs are held flat in projection (growing them would put people who pay in exactly the limit above it), and say that this understates a lowered limit's excess in later years. - Count subscriptions net of current-year flexible-ISA withdrawals and exclude what regulation 5D disregards; exclude paragraph 10A charge repayments from lifetime_isa_payments (SI 2026/1130 regs 3 and 4). - Date the annual limit's earlier sums: £15,000 from 1 July 2014 (SI 2014/1450 reg 6(a)(i)) and £15,240 from 6 April 2015 (SI 2015/608 reg 3(1)), so 2015-16 and 2016-17 no longer read £20,000. The fiscal-year conversion writes annual values from 2015, so earlier years are not read. - Start the cash limit's infinity on 1 July 2014 and cite SI 2014/1450 reg 6(a)(ii), which omitted the cash sub-limits. - Put SI 2026/1130 reg 5 first for the above-cap charge. - Remove the duplicate cash_isa and stocks_and_shares_isa entries from the uprating map. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2710.added.md | 2 +- .../isa-inputs-and-capital-flows.md | 23 ++++++++++++---- docs/engineering/skills/dataset-sources.md | 7 +++-- policyengine_uk/data/uprating_indices.yaml | 2 -- .../hmrc/isa/annual_subscription_limit.yaml | 10 ++++++- .../gov/hmrc/isa/cash/subscription_limit.yaml | 4 ++- .../above_cap_purchase/withdrawal_charge.yaml | 4 +-- policyengine_uk/programs.yaml | 2 +- .../gov/hmrc/isa/isa_subscription_limits.yaml | 22 ++++++++++++++- .../gov/hmrc/isa/cash_isa_subscriptions.py | 27 ++++++++++++------- .../gov/hmrc/isa/lifetime_isa_payments.py | 2 +- .../stocks_and_shares_isa_subscriptions.py | 24 ++++++++++++----- 12 files changed, 96 insertions(+), 33 deletions(-) diff --git a/changelog.d/2710.added.md b/changelog.d/2710.added.md index 099d903245..2e2a395c32 100644 --- a/changelog.d/2710.added.md +++ b/changelog.d/2710.added.md @@ -1 +1 @@ -Added the £20,000 ISA annual subscription limit (ISA Regulations 1998 reg 4ZA), a cash ISA limit for people under an age at the end of the tax year that current law leaves unset (Budget 2026 retains £20,000 for cash, so SI 2026/1018's £12,000 for under-65s is a reform setting), per-person cash and stocks-and-shares ISA subscription inputs, and `cash_isa_subscriptions_above_limit` and `isa_subscriptions_above_limit`, the subscriptions a limit leaves out. +Added the ISA annual subscription limit (ISA Regulations 1998 reg 4ZA: £15,240 from 2015-16 and £20,000 from 2017-18), a cash ISA limit for people under an age at the end of the tax year that current law leaves unset (Budget 2026 retains £20,000 for cash, so SI 2026/1018's £12,000 for under-65s is a reform setting), per-person cash and stocks-and-shares ISA subscription inputs, and `cash_isa_subscriptions_above_limit` and `isa_subscriptions_above_limit`, the subscriptions a limit leaves out. diff --git a/docs/book/assumptions/isa-inputs-and-capital-flows.md b/docs/book/assumptions/isa-inputs-and-capital-flows.md index 152d2ebe20..4e3e74f5f4 100644 --- a/docs/book/assumptions/isa-inputs-and-capital-flows.md +++ b/docs/book/assumptions/isa-inputs-and-capital-flows.md @@ -199,6 +199,9 @@ accounting convention for the model's current-income measures. `gov.hmrc.isa.annual_subscription_limit` is the £20,000 limit on a person's subscriptions in a tax year to all their ISAs other than Junior ISAs ([ISA Regulations regulation 4ZA(1)](https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA)). +It was £15,000 from 1 July 2014 and £15,240 from 6 April 2015. The model reads +these limits from 2015–16; 2014–15 and earlier years, with their lower limits +and cash sub-limits, are not modelled. Cash has had no limit of its own since 1 July 2014, so `gov.hmrc.isa.cash.subscription_limit` is infinite and cash counts only towards the annual limit. @@ -216,15 +219,25 @@ for cash. The inputs are per-person flows in the year: `cash_isa_subscriptions`, `stocks_and_shares_isa_subscriptions` and the existing -`lifetime_isa_payments`, which `isa_subscriptions` adds up. They exclude -transfers between ISAs, interest and growth, and replacements of cash -withdrawn from a flexible ISA earlier in the same year +`lifetime_isa_payments`, which `isa_subscriptions` adds up. They sum over +all of a person's accounts of each type, net of current-year subscriptions +withdrawn from a flexible ISA. They exclude transfers between ISAs, interest +and growth, and the subscriptions that +[regulation 5D](https://www.legislation.gov.uk/uksi/1998/1870/regulation/5D) +disregards, such as replacement subscriptions to a flexible ISA, additional +permitted subscriptions, defaulted subscriptions and transfers from a matured +Child Trust Fund ([HMRC guidance](https://www.gov.uk/guidance/manage-isa-subscriptions-for-your-investors)). `lifetime_isa_payments` includes transfers into a Lifetime ISA from other ISAs, which count towards the Lifetime ISA limit but are not new subscriptions for the annual limit, so the total overstates subscriptions -where that input includes them. Innovative finance ISAs are not included. -The inputs are not uprated, as the limits are fixed in cash terms. +where that input includes them. A withdrawal charge restored under Schedule +paragraph 10A is neither a subscription nor a Lifetime ISA payment (SI +2026/1130 regulations 3 and 4). Innovative finance ISAs are not included. +The inputs are not uprated: growing them would put people who pay in exactly +the annual limit above it in the baseline. Nominal subscriptions grow, so flat +amounts understate how much a lowered limit leaves out in later years. A +producer can grow them instead and cap each person at the limit in force. The model compares the inputs with the limits in force and does not cap them. `cash_isa_subscriptions_above_limit` is cash above the person's cash limit, diff --git a/docs/engineering/skills/dataset-sources.md b/docs/engineering/skills/dataset-sources.md index 88e2630d30..57efacef55 100644 --- a/docs/engineering/skills/dataset-sources.md +++ b/docs/engineering/skills/dataset-sources.md @@ -140,8 +140,11 @@ the model does not reconcile the two. `cash_isa_subscriptions` and `stocks_and_shares_isa_subscriptions` are annual person flows, summed over all of a person's accounts of each type, that count towards the ISA subscription limits, with `lifetime_isa_payments` for -Lifetime ISAs. Projection carries them flat, since the limits are fixed in -cash terms. The model does not cap them at the limits: a producer should +Lifetime ISAs. Projection carries them flat, because growing them would put +people who pay in exactly the annual limit above it in the baseline. Nominal +subscriptions grow, so flat amounts understate how much a lowered limit leaves +out in later years; a producer can grow them itself and cap each person at the +limit in force. The model does not cap them at the limits: a producer should export subscriptions as observed, so a reform that lowers a limit shows what it leaves out. Observed subscriptions stay within the £20,000 annual limit, which the three inputs share, so an imputation should draw them diff --git a/policyengine_uk/data/uprating_indices.yaml b/policyengine_uk/data/uprating_indices.yaml index 821af819c8..ee591c95de 100644 --- a/policyengine_uk/data/uprating_indices.yaml +++ b/policyengine_uk/data/uprating_indices.yaml @@ -144,12 +144,10 @@ gov.economic_assumptions.yoy_growth.obr.per_capita.gdp: - stocks_and_shares_isa - stocks_and_shares_isa_balance - sublet_income -- cash_isa - consumer_debt - mortgage_debt - rent_paid_as_boarder - rent_paid_as_lodger -- stocks_and_shares_isa - owned_company_capital - owned_company_holding_value - owned_company_trade_assets diff --git a/policyengine_uk/parameters/gov/hmrc/isa/annual_subscription_limit.yaml b/policyengine_uk/parameters/gov/hmrc/isa/annual_subscription_limit.yaml index 837dca9144..d23096f9ca 100644 --- a/policyengine_uk/parameters/gov/hmrc/isa/annual_subscription_limit.yaml +++ b/policyengine_uk/parameters/gov/hmrc/isa/annual_subscription_limit.yaml @@ -1,8 +1,12 @@ description: > Limit on a person's total subscriptions in the tax year to ISAs other than Junior ISAs: cash, stocks-and-shares, innovative finance and new Lifetime ISA - payments together. + payments together. It was £15,000 from 1 July 2014 and £15,240 from 6 April + 2015. The annual model reads it from 2015-16; 2014-15 and earlier years, with + their lower limits and cash sub-limits, are not modelled. values: + 2014-07-01: 15000 + 2015-04-06: 15240 2017-04-06: 20000 metadata: label: ISA annual subscription limit @@ -11,5 +15,9 @@ metadata: reference: - title: Individual Savings Account Regulations 1998 regulation 4ZA(1) href: https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA + - title: Individual Savings Account (Amendment No. 2) Regulations 2014 regulation 6(a)(i) + href: https://www.legislation.gov.uk/uksi/2014/1450/regulation/6/made + - title: Individual Savings Account (Amendment) Regulations 2015 regulation 3(1) + href: https://www.legislation.gov.uk/uksi/2015/608/regulation/3/made - title: Individual Savings Account (Amendment) Regulations 2017 regulation 5 href: https://www.legislation.gov.uk/uksi/2017/186/regulation/5/made diff --git a/policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml b/policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml index f0fa3ecd59..e182639871 100644 --- a/policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml +++ b/policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml @@ -8,7 +8,7 @@ description: > 2026, so cash keeps no limit of its own here. Set this to 12,000 to model the lower limit. values: - 2017-04-06: .inf + 2014-07-01: .inf metadata: label: Cash ISA subscription limit period: year @@ -16,6 +16,8 @@ metadata: reference: - title: Individual Savings Account Regulations 1998 regulation 4ZA href: https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA + - title: Individual Savings Account (Amendment No. 2) Regulations 2014 regulation 6(a)(ii) (omits the cash sub-limits in regulation 4ZA(1)(a) and (b)) + href: https://www.legislation.gov.uk/uksi/2014/1450/regulation/6/made - title: Individual Savings Account (Amendment) (No. 2) Regulations 2026 regulations 1 and 6 href: https://www.legislation.gov.uk/uksi/2026/1018/regulation/6/made - title: Budget 2026, overview of tax legislation and rates, paragraph 2.5 diff --git a/policyengine_uk/parameters/gov/hmrc/isa/lifetime/above_cap_purchase/withdrawal_charge.yaml b/policyengine_uk/parameters/gov/hmrc/isa/lifetime/above_cap_purchase/withdrawal_charge.yaml index 1950e28d1b..a33fa1f8b7 100644 --- a/policyengine_uk/parameters/gov/hmrc/isa/lifetime/above_cap_purchase/withdrawal_charge.yaml +++ b/policyengine_uk/parameters/gov/hmrc/isa/lifetime/above_cap_purchase/withdrawal_charge.yaml @@ -10,7 +10,7 @@ metadata: period: year unit: /1 reference: - - title: Individual Savings Account Regulations 1998, Schedule paragraphs 5 and 6A - href: https://www.legislation.gov.uk/uksi/1998/1870/schedule - title: Individual Savings Account (Amendment) (No. 3) Regulations 2026 regulation 5 href: https://www.legislation.gov.uk/uksi/2026/1130/regulation/5/made + - title: Individual Savings Account Regulations 1998, Schedule paragraphs 5 and 6A, as amended by SI 2026/1130 regulation 5 + href: https://www.legislation.gov.uk/uksi/1998/1870/schedule diff --git a/policyengine_uk/programs.yaml b/policyengine_uk/programs.yaml index 65ad4ec7b3..734a227b56 100644 --- a/policyengine_uk/programs.yaml +++ b/policyengine_uk/programs.yaml @@ -37,7 +37,7 @@ programs: status: partial coverage: UK variable: isa_subscriptions_above_limit - verified_start_year: 2017 + verified_start_year: 2015 notes: The annual limit (gov.hmrc.isa.annual_subscription_limit), and a cash limit for people under an age at the end of the tax year (gov.hmrc.isa.cash) that current law leaves unset, as Budget 2026 retains £20,000 for cash; a reform can restore SI 2026/1018's £12,000 for under-65s. Reports subscriptions above each limit from per-person subscription inputs, not where they go. Innovative finance and Junior ISAs are not modelled. - id: income_tax diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/isa_subscription_limits.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/isa_subscription_limits.yaml index 25685ba861..945a5123d1 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/isa_subscription_limits.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/isa_subscription_limits.yaml @@ -1,4 +1,6 @@ -# ISA Regulations 1998 reg 4ZA(1): a GBP 20,000 annual limit. Cash has had no +# ISA Regulations 1998 reg 4ZA(1): GBP 15,000 from 1 July 2014 (SI 2014/1450), +# GBP 15,240 from 6 April 2015 (SI 2015/608) and a GBP 20,000 annual limit from +# 6 April 2017 (SI 2017/186). Cash has had no # limit of its own since 1 July 2014, and Budget 2026 retains that from April # 2027 (OOTLAR para 2.5). The GBP 12,000 limit that SI 2026/1018 reg 6 set for # people 64 or under at the end of the year (5 April) is tested as a reform. @@ -91,3 +93,21 @@ cash_isa_subscription_limit: [15_000, 15_000] cash_isa_subscriptions_above_limit: [0, 5_000] isa_subscriptions_above_limit: [5_000, 5_000] + +- name: The annual limit was GBP 15,240 in fiscal 2015-16 + period: 2015 + input: + age: [40, 40] + stocks_and_shares_isa_subscriptions: [15_240, 15_241] + output: + isa_subscriptions_above_limit: [0, 1] + +- name: The annual limit was GBP 15,240 in fiscal 2016-17, for cash too + period: 2016 + input: + age: [40, 40] + cash_isa_subscriptions: [15_240, 15_241] + output: + cash_isa_subscription_limit: [15_240, 15_240] + cash_isa_subscriptions_above_limit: [0, 1] + isa_subscriptions_above_limit: [0, 1] diff --git a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py index 1d76f1e7d2..bb7a8f847f 100644 --- a/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py +++ b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py @@ -7,18 +7,27 @@ class cash_isa_subscriptions(Variable): label = "Cash ISA subscriptions" documentation = ( "New money paid into cash ISAs in the tax year, summed over all the " - "person's cash ISAs, as a person can pay into more than one in a year. " + "person's cash ISAs, as a person can pay into more than one in a year, " + "and net of current-year subscriptions withdrawn from a flexible ISA. " "It counts towards the cash and annual subscription limits. Exclude " - "transfers between ISAs, interest and growth, and replacements of cash " - "withdrawn from a flexible ISA earlier in the same tax year, which do " - "not count towards the limits. The model compares this amount with the limits in force " - "and does not cap it: under a lower limit, such as a reform that " - "brings in a £12,000 cash limit, cash_isa_subscriptions_above_limit " - "reports the part that cannot be paid in. Amounts are not uprated, as " - "the limits are fixed in cash terms." + "transfers between ISAs, interest and growth, and the subscriptions " + "that regulation 5D disregards, such as replacement subscriptions to a " + "flexible ISA, additional permitted subscriptions, defaulted " + "subscriptions and transfers from a matured Child Trust Fund. The model " + "compares this amount with the limits in force and does not cap it: " + "under a lower limit, such as a reform that brings in a £12,000 cash " + "limit, cash_isa_subscriptions_above_limit reports the part that cannot " + "be paid in. Amounts are not uprated: growing them would put people who " + "pay in exactly the annual limit above it in the baseline. As nominal " + "subscriptions grow, flat amounts understate how much a lowered limit " + "leaves out in later years; a producer can grow them instead and cap " + "each person at the limit in force." ) definition_period = YEAR unit = GBP quantity_type = FLOW default_value = 0 - reference = "https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA" + reference = ( + "https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA", + "https://www.legislation.gov.uk/uksi/1998/1870/regulation/5D", + ) diff --git a/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_payments.py b/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_payments.py index 24d302ab06..870f0b7c3d 100644 --- a/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_payments.py +++ b/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_payments.py @@ -5,7 +5,7 @@ class lifetime_isa_payments(Variable): value_type = float entity = Person label = "Lifetime ISA qualifying payments" - documentation = "Current-year qualifying additions to a Lifetime ISA, including qualifying transfers from other ISA types. Exclude bonuses, interest and growth, Lifetime-ISA-to-Lifetime-ISA transfers, returned failed-purchase withdrawals, defaulted payments and rebated fees. Contributions at ages 40 to 49 assume an account first funded before age 40. Annual age and payment timing are approximations." + documentation = "Current-year qualifying additions to a Lifetime ISA, including qualifying transfers from other ISA types. Exclude bonuses, interest and growth, Lifetime-ISA-to-Lifetime-ISA transfers, returned failed-purchase withdrawals, repayments of a withdrawal charge under Schedule paragraph 10A (from 29 October 2026), defaulted payments and rebated fees. Contributions at ages 40 to 49 assume an account first funded before age 40. Annual age and payment timing are approximations." definition_period = YEAR unit = GBP quantity_type = FLOW diff --git a/policyengine_uk/variables/gov/hmrc/isa/stocks_and_shares_isa_subscriptions.py b/policyengine_uk/variables/gov/hmrc/isa/stocks_and_shares_isa_subscriptions.py index fd05d5db6e..8bbe8067ec 100644 --- a/policyengine_uk/variables/gov/hmrc/isa/stocks_and_shares_isa_subscriptions.py +++ b/policyengine_uk/variables/gov/hmrc/isa/stocks_and_shares_isa_subscriptions.py @@ -7,15 +7,25 @@ class stocks_and_shares_isa_subscriptions(Variable): label = "Stocks and shares ISA subscriptions" documentation = ( "New money paid into stocks-and-shares ISAs in the tax year, summed " - "over all the person's stocks-and-shares ISAs and including eligible " - "shares subscribed by transfer. It counts towards the annual " - "subscription limit. Exclude transfers between ISAs, income and growth, " - "and replacements of cash withdrawn from a flexible ISA earlier in the " - "same tax year. Innovative finance ISAs are not included. Amounts are " - "not uprated, as the limit is fixed in cash terms." + "over all the person's stocks-and-shares ISAs, including eligible shares " + "subscribed by transfer, and net of current-year subscriptions withdrawn " + "from a flexible ISA. It counts towards the annual subscription limit. " + "Exclude transfers between ISAs, income and growth, and the " + "subscriptions that regulation 5D disregards, such as replacement " + "subscriptions to a flexible ISA, additional permitted subscriptions, " + "defaulted subscriptions and transfers from a matured Child Trust Fund. " + "Innovative finance ISAs are not included. Amounts are not uprated: " + "growing them would put people who pay in exactly the annual limit above " + "it in the baseline. As nominal subscriptions grow, flat amounts " + "understate how much a lowered limit leaves out in later years; a " + "producer can grow them instead and cap each person at the limit in " + "force." ) definition_period = YEAR unit = GBP quantity_type = FLOW default_value = 0 - reference = "https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA" + reference = ( + "https://www.legislation.gov.uk/uksi/1998/1870/regulation/4ZA", + "https://www.legislation.gov.uk/uksi/1998/1870/regulation/5D", + ) From 52b09bc79e4fd4e124fc1af64e67c0d29d620855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:31:07 +0000 Subject: [PATCH 10/11] Link the November instrument tracker from the cash ISA limit Pavel's should fix 1: from 6 April 2027 the cash limit rests on Budget 2026 until the November 2026 instrument is laid, which policyengine-uk#2728 tracks. Co-Authored-By: Claude Opus 5.5 --- .../parameters/gov/hmrc/isa/cash/subscription_limit.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml b/policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml index e182639871..ca1de3600f 100644 --- a/policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml +++ b/policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml @@ -7,6 +7,9 @@ description: > limit for all savers, with secondary legislation to be laid in November 2026, so cash keeps no limit of its own here. Set this to 12,000 to model the lower limit. +# From 6 April 2027 this rests on Budget 2026 (OOTLAR para 2.5) until the +# November 2026 instrument is laid: tracked in +# https://github.com/PolicyEngine/policyengine-uk/issues/2728. values: 2014-07-01: .inf metadata: From 04895b0ace6466141d7c15e49a04d1987a8c83d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:44:27 +0000 Subject: [PATCH 11/11] Record the 2026-27 caveat for the above-cap Lifetime ISA charge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pavel's nice to have 6, decided with María: keep the 30 April convention. SI 2026/1130's 20% has effect for withdrawals from 29 October 2026, but fiscal 2026 samples 30 April 2026, so a 2026-27 run charges 25% on such a withdrawal made from 29 October 2026 to 5 April 2027. The withdrawal charge's docstring and the assumptions page's annual resolution section now say so, alongside the parameter description, the Lifetime ISA section, programs.yaml and the changelog. Co-Authored-By: Claude Opus 5.5 --- docs/book/assumptions/isa-inputs-and-capital-flows.md | 6 +++++- .../gov/hmrc/isa/lifetime_isa_withdrawal_charge.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/book/assumptions/isa-inputs-and-capital-flows.md b/docs/book/assumptions/isa-inputs-and-capital-flows.md index 4e3e74f5f4..b64209bcd7 100644 --- a/docs/book/assumptions/isa-inputs-and-capital-flows.md +++ b/docs/book/assumptions/isa-inputs-and-capital-flows.md @@ -261,7 +261,11 @@ A bare year Y represents the fiscal year from 6 April Y to 5 April Y+1. Parameters retain their legal effective dates; the annual model samples rates at 30 April. Fiscal 2020 therefore uses the temporary 20% withdrawal charge and fiscal 2021 uses 25%. The annual model cannot distinguish a -withdrawal in late March 2020 within fiscal 2019. Annual ages and purchase +withdrawal in late March 2020 within fiscal 2019. Likewise, the 20% charge for +a purchase above the price cap has effect for withdrawals from 29 October +2026, but fiscal 2026 samples 30 April 2026. So a 2026–27 run charges 25% on +such a withdrawal made from 29 October 2026 to 5 April 2027 (159 of the +year's 365 days), and the 20% applies from fiscal 2027. Annual ages and purchase conditions similarly represent the relevant transactions at one annual status. Multiple transactions with different conditions require externally prepared qualifying amounts. diff --git a/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_withdrawal_charge.py b/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_withdrawal_charge.py index 4b6f88fcdf..f588f939c3 100644 --- a/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_withdrawal_charge.py +++ b/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_withdrawal_charge.py @@ -5,7 +5,7 @@ class lifetime_isa_withdrawal_charge(Variable): value_type = float entity = Person label = "Lifetime ISA withdrawal charge" - documentation = "Charge deducted from non-exempt gross Lifetime ISA withdrawals. From 29 October 2026, the part paid towards a first home that meets every first-time purchase condition except the property price cap pays the reduced above-cap charge (Schedule paragraph 6A); the annual model applies it from fiscal 2027-28. The model records this capital payment as government revenue, outside household current-income taxes; a supplied balance is not automatically updated. Annual rates use the model's fiscal-year parameter sampling." + documentation = "Charge deducted from non-exempt gross Lifetime ISA withdrawals. From 29 October 2026, the part paid towards a first home that meets every first-time purchase condition except the property price cap pays the reduced above-cap charge (Schedule paragraph 6A). The annual model reads parameters on 30 April, so it applies the reduced charge from fiscal 2027-28: a 2026-27 run charges such a withdrawal made from 29 October 2026 to 5 April 2027 at the full withdrawal charge, 25% rather than 20%. The model records this capital payment as government revenue, outside household current-income taxes; a supplied balance is not automatically updated. Annual rates use the model's fiscal-year parameter sampling." definition_period = YEAR unit = GBP quantity_type = FLOW