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/changelog.d/2710.added.md b/changelog.d/2710.added.md new file mode 100644 index 0000000000..2e2a395c32 --- /dev/null +++ b/changelog.d/2710.added.md @@ -0,0 +1 @@ +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/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/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..b64209bcd7 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) @@ -167,26 +177,105 @@ 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 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)). +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. +[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 +`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. 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, +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. 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 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. -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 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 600d59943c..57efacef55 100644 --- a/docs/engineering/skills/dataset-sources.md +++ b/docs/engineering/skills/dataset-sources.md @@ -126,6 +126,35 @@ 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, 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, 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 +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. + ## Defaults `policyengine_uk` does not silently choose a default dataset when no situation is diff --git a/policyengine_uk/data/uprating_indices.yaml b/policyengine_uk/data/uprating_indices.yaml index 8c2e4a57ae..ee591c95de 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,13 +142,12 @@ 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 - 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 new file mode 100644 index 0000000000..d23096f9ca --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/isa/annual_subscription_limit.yaml @@ -0,0 +1,23 @@ +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. 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 + 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 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/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..d29db18aa3 --- /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. 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: + label: Cash ISA full limit age + period: year + unit: year + reference: + - 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..ca1de3600f --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/isa/cash/subscription_limit.yaml @@ -0,0 +1,27 @@ +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 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. +# 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: + label: Cash ISA subscription limit + period: year + unit: currency-GBP + 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 + 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/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..a33fa1f8b7 --- /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 (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 3803f6da39..734a227b56 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 @@ -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: 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 name: Income tax full_name: Income Tax 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/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..945a5123d1 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/isa/isa_subscription_limits.yaml @@ -0,0 +1,113 @@ +# 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. + +- 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 + input: + age: 40 + cash_isa_subscriptions: 20_000 + output: + cash_isa_subscription_limit: 20_000 + cash_isa_subscriptions_above_limit: 0 + +- 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: A GBP 12,000 cash limit applies to people under 65 at the end of the year + period: 2027 + input: + gov.hmrc.isa.cash.subscription_limit: 12_000 + age: [40, 40, 40] + cash_isa_subscriptions: [12_000, 12_001, 20_000] + output: + 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: 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, 70, 70] + cash_isa_subscriptions: [20_000, 20_000, 20_000, 20_001] + output: + cash_isa_subscription_limit: [12_000, 20_000, 20_000, 20_000] + cash_isa_subscriptions_above_limit: [8_000, 0, 0, 1] + +- 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] + 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 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: 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: [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/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/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/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..11b7d58a2a --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscription_limit.py @@ -0,0 +1,32 @@ +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. 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 + 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..bb7a8f847f --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/isa/cash_isa_subscriptions.py @@ -0,0 +1,33 @@ +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, summed over all the " + "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 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", + "https://www.legislation.gov.uk/uksi/1998/1870/regulation/5D", + ) 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/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/lifetime_isa_withdrawal_charge.py b/policyengine_uk/variables/gov/hmrc/isa/lifetime_isa_withdrawal_charge.py index f5c6311a4c..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. 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 @@ -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 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..8bbe8067ec --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/isa/stocks_and_shares_isa_subscriptions.py @@ -0,0 +1,31 @@ +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, summed " + "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", + "https://www.legislation.gov.uk/uksi/1998/1870/regulation/5D", + ) 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