Skip to content

Limit investment interest deductions under §163(d) and Form 4952 - #9977

Draft
MaxGhenis wants to merge 12 commits into
mainfrom
investment-interest-163d-limit
Draft

MaxGhenis wants to merge 12 commits into
mainfrom
investment-interest-163d-limit

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

What and why

Apply the §163(d) investment-interest deduction limit through Form 4952. Schedule A replaces current raw investment interest with allowed line 8, preserving the existing aggregate interest and mortgage conventions. Explicit prior-year disallowed interest is available on line 2 and the unused amount is exposed on line 7; carryover is not automatically propagated between simulations.

All Form 4952 income, gains, elections, current debt and expenses belong to the filer and spouse. The deduction and Schedule D/§911 calculations share the effective capped election and investment gains, retaining independent Schedule D gain arithmetic and default gain-first allocation. Montana spouses compute separate limits before 2024. California, New York and Virginia substitutions use allowed federal interest; California's high-AGI exclusion retains the federal-form approach. Federal miscellaneous investment fees remain disallowed after 2017, including 2026.

Sources: 26 USC 163(d), IRS Form 4952, 26 USC 67(h), and 2023 Montana instructions, printed page 31.

Test plan

  • Externally derived YAML cases for the deduction cap, carryover, dependent exclusion and Montana separate limits; independent vectorized restatement, bounds, monotonicity and conservation properties.
  • make format and changelog fragment.
  • 90 targeted passes: federal Form 4952 (25), properties (15), Montana (6), California (2), New York (8), Arkansas (8), Virginia (5), election (13), and §911 (8). Each file ran separately, serially in the foreground, with core 3.32.20.
  • CI suites, including partner contracts: queued at the latest check. Partner expectations are unchanged.

Seven source-overlay regressions fail with prior formulas and pass with final formulas (not a full old-checkout replay). The federal example changes tax from $20,447 to $21,647; Montana separate deductions change from [150,450] to [300,0]. The new AR fixture initially failed entity parsing; correcting its input placement preserved the expected deduction. Local property setup-inclusive time was 158.62s and peak memory ~1.02GiB; Linux CI resource evidence is pending. The federal assertions passed even though its external timing wrapper hit a sandbox restriction afterward.

The saved build's 21 federal YAML cases, six property cases and 60 serial targets passed on an older baseline; these are historical evidence, not validation of this head.

Impact and data limitation

Hub microsimulation (2026-10-07): merge base dd9cb3f683 against this head 0522816764, on populace_us_2024.h5@populace-us-2024-spm-20260909. It ran under the shared heavy lock, one run per side, compared record by record.

2025 2026
Itemized deductions (itemized_taxable_income_deductions) −$29.92bn (6,868 records, 14.8M weighted units) −$29.95bn (6,828 records)
Federal income tax +$8.06bn (2,830 records, 5.66M weighted) +$7.85bn (2,594 records)
State income tax +$0.24bn +$0.24bn
Household net income −$8.30bn (3,232 households) −$8.10bn (3,035 households)
investment_interest_expense (input, unchanged) $52.07bn $53.68bn

Read these totals together with the data limitation below. The law change is the Form 4952 limit: the deduction cannot exceed net investment income, with the excess carried forward. Main deducted the dataset's entire $52bn of investment_interest_expense with no limit. The default dataset's value is an imputed Schedule A residual, not observed interest paid, so most of the ~$30bn now disallowed is imputed amount that has no investment income behind it. The direction is the statute's. The size mostly measures how far that imputed field exceeds modeled net investment income, and the true revenue effect is likely much smaller. A data follow-up should re-derive investment interest from observed sources before anyone cites the aggregate.

The default populace-us-2024-spm-20260909 input is an imputed Schedule A deduction residual, not observed raw investment interest paid. Its parent build assigns deductible points, mortgage-insurance premiums and investment-interest deductions to this residual. The old $53.68B input screen and illustrative +$5.4–9.4B estimate are therefore not a validated §163(d) revenue score. A pinned comparison can show mechanical model changes; economic interpretation needs corrected data concepts or a sensitivity design from the data owners.

Coverage limits: no alternative “Elec.” allocation; no investment/noninvestment capital-asset split; missing distinct annuity/royalty/K-1 income; no Montana-specific income adjustments or per-spouse carryover inputs. Federal AMT's second Form 4952 and NIIT Form 8960 line 9a remain explicit follow-ups. California’s own FTB 3526 income/election/carryover calculation remains assigned to the separate California work. This PR precedes that work because both touch its pre-limitation formula and exclusion parameter.

axiom: 26 USC 163(d) TheAxiomFoundation/rulespec-us#1558 queued | 26 USC 67(a)-(b); 2017 Form4952 line5 TheAxiomFoundation/rulespec-us#1559 queued | MT Form2 line10 (TY2023), WorksheetA (TY2024) TheAxiomFoundation/rulespec-us#1560 queued | CA ScheduleCA line9 TheAxiomFoundation/rulespec-us#1561 queued | CA ScheduleCA itemized worksheet line2 TheAxiomFoundation/rulespec-us#1562 queued | NY IT196 lines14,24–28,40 TheAxiomFoundation/rulespec-us#1563 queued | VA2021 ScheduleA PartA TheAxiomFoundation/rulespec-us#1564 queued | 26 USC 1(h)(2),(11)(D)(i) TheAxiomFoundation/rulespec-us#1488 queued | 26 USC 1(h)(4),(6) TheAxiomFoundation/rulespec-us#1499 queued | 26 USC 911(f) TheAxiomFoundation/rulespec-us#1478 queued | 26 USC 68(c)(2) historical TheAxiomFoundation/rulespec-us#1449 queued | Ark.Code 26-51-437 TheAxiomFoundation/rulespec-us#1411 queued | 26 USC 67(h) encoded-correct (us/statutes/26/67/h.yaml#individual_miscellaneous_itemized_deduction_disallowed_in_2026)

Recovery and detailed evidence: fix report, dataset lineage and exact hub impact request. Code head: 05228167642374813d0afbdf4c0aa2cf559de7aa; main merged at dd9cb3f6839.

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Hold (Max, d1054, 2026-10-07). This PR merges only after the dataset stops routing non-investment interest into investment_interest_expense.

The §163(d) / Form 4952 logic is correct, and review confirmed the arithmetic. The problem is the default-dataset input:

  • Microcosm's split_us_puf_e19200_by_agi_band (packages/microcosm-build/src/microcosm/build/us_runtime/puf_interest_components.py) sends PUF E19200 minus home mortgage interest to investment_interest_expense.
  • That residual also carries deductible points and qualified mortgage-insurance premiums (SOI Table 2.1 columns CN and CP), alongside investment interest (CR).
  • This PR's cap therefore disallows interest that §163(d) doesn't limit. The measured +$8.06bn in 2025 federal income tax mostly reflects that mapping.

The fix, following Max's principle that inputs should be as close to leaf facts as possible:

  1. PolicyEngine-US adds separate inputs for deductible points and qualified mortgage-insurance premiums, each deducted under its own law.
  2. Microcosm splits the residual into the three leaves, using the SOI columns it already carries.
  3. A data release follows, on Max's go.
  4. This PR's impact is re-run against that release, and the PR lands then.

The open review item (dependents' amounts reaching the 28%-rate and §1250 helpers) is being fixed in the meantime.

This was referenced Oct 7, 2026
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

The three failing checks on this head (a0d4dce2e4) are the section 68 tests from #9695, and they point at a real interaction rather than stale expectations.

What breaks. This PR makes interest_deduction carry investment_interest_expense_deduction (Form 4952 line 8) instead of the investment interest paid. The federal list gov.irs.deductions.itemized.limitation.excluded_deductions (added by #9695 for 26 USC 68(c)(2), pre-2026 and the non-OBBB counterfactual) still names investment_interest_expense, the amount paid. So the 80% ceiling's base now subtracts interest that is no longer in the total. Whenever the paid amount exceeds the deduction, the base is too small and the reduction too low. The California list in this PR already makes the matching switch from non_mortgage_interest.

Evidence from this PR's CI (run 37627884656):

  • Full Suite - Baseline (irs): itemized_taxable_income_deductions_reduction.yaml, case "2017 joint filers with investment interest and a casualty loss": interest_deduction@2017: [0.] differs from 30000.0. The case supplies $30,000 of investment interest paid and no investment income, so Form 4952 allows 0.
  • Full Suite - Rest (Python + variables): all 8 tests in tests/core/test_itemized_deduction_limitation_properties.py fail.

Tested patch. Applied to a detached checkout of a0d4dce2e4; nothing was pushed to this branch:

  1. Federal limitation/excluded_deductions.yaml: investment_interest_expense → investment_interest_expense_deduction.
  2. The 2017 YAML case supplies investment_interest_expense_deduction: 30_000 (Schedule A line 14 is the Form 4952 deduction), keeping the worksheet arithmetic: reduction 8,000.
  3. The property test draws the deduction as the excluded component and lets the paid amount exceed it for half the units, so it now covers the §163(d) cap inside the §68 base.

Results on a0d4dce2e4 with the patch: the property file has 8 passed; the reduction and limitation YAML files have 15 passed; ruff format --check is clean. With items 2 and 3 applied but the list left unchanged, the property tests still fail on real values, for example worksheet line 9 of 31,238.01 against a computed 0.00. So item 1 is a model fix, not just a test update.

Patch (git diff against a0d4dce)
diff --git a/policyengine_us/parameters/gov/irs/deductions/itemized/limitation/excluded_deductions.yaml b/policyengine_us/parameters/gov/irs/deductions/itemized/limitation/excluded_deductions.yaml
index 96e642fd5d..21198c4543 100644
--- a/policyengine_us/parameters/gov/irs/deductions/itemized/limitation/excluded_deductions.yaml
+++ b/policyengine_us/parameters/gov/irs/deductions/itemized/limitation/excluded_deductions.yaml
@@ -3,8 +3,8 @@ values:
   # Section 68(c) excludes the medical expense deduction (section 213), any
   # deduction for investment interest (section 163(d)), and casualty, theft
   # and wagering losses (section 165(c)(2), (c)(3) and (d)).
-  # investment_interest_expense is the investment interest that
-  # interest_deduction includes through non_mortgage_interest.
+  # investment_interest_expense_deduction is the Form 4952 line 8 amount
+  # (limited under section 163(d)) that interest_deduction includes.
   # wagering_losses_deduction is deducted outside
   # total_itemized_taxable_income_deductions, so it never enters the base.
   # The 2017 worksheet also removes qualified disaster-relief contributions
@@ -12,7 +12,7 @@ values:
   # not modeled.
   2009-01-01:
     - medical_expense_deduction
-    - investment_interest_expense
+    - investment_interest_expense_deduction
     - casualty_loss_deduction
 metadata:
   unit: list
diff --git a/policyengine_us/tests/core/test_itemized_deduction_limitation_properties.py b/policyengine_us/tests/core/test_itemized_deduction_limitation_properties.py
index 36c428c709..4bcf5dbe83 100644
--- a/policyengine_us/tests/core/test_itemized_deduction_limitation_properties.py
+++ b/policyengine_us/tests/core/test_itemized_deduction_limitation_properties.py
@@ -57,15 +57,20 @@ APPLICABLE_AMOUNTS = {
 SEPARATE_AMOUNTS = {2015: 154_950, 2016: 155_650, 2017: 156_900}
 
 # Components of total_itemized_taxable_income_deductions, split by whether
-# section 68(c) excludes them. Mortgage and investment interest are person
-# inputs that interest_deduction adds up.
-EXCLUDED = ("medical_expense_deduction", "casualty_loss_deduction")
-EXCLUDED_PERSON = ("investment_interest_expense",)
+# section 68(c) excludes them. interest_deduction adds mortgage interest (a
+# person input) and the Form 4952 investment interest deduction.
+EXCLUDED = (
+    "medical_expense_deduction",
+    "casualty_loss_deduction",
+    "investment_interest_expense_deduction",
+)
+EXCLUDED_PERSON = ()
 OTHER = ("charitable_deduction", "salt_deduction", "misc_deduction")
 OTHER_PERSON = ("deductible_mortgage_interest",)
 COMPONENTS = EXCLUDED + OTHER
 PERSON_COMPONENTS = EXCLUDED_PERSON + OTHER_PERSON
 ALL_COMPONENTS = COMPONENTS + PERSON_COMPONENTS
+PAID = "investment_interest_expense"
 # PolicyEngine computes in float32, whose spacing near $1 million is 6 cents,
 # so tolerances scale with the size of the operands (not of the result: the
 # 80 percent base is a difference of two such sums).
@@ -102,6 +107,12 @@ def _draw(seed):
     for name in ALL_COMPONENTS:
         amount = np.round(rng.uniform(0, 150_000, N), 2)
         units[name] = np.where(rng.random(N) < 0.5, 0, amount)
+    # Investment interest paid can exceed the Form 4952 deduction; only the
+    # deduction is in the total and in the section 68(c) exclusion.
+    disallowed = np.round(rng.uniform(0, 50_000, N), 2)
+    units[PAID] = units["investment_interest_expense_deduction"] + np.where(
+        rng.random(N) < 0.5, 0, disallowed
+    )
     # Every fifth unit has only excluded deductions (worksheet line 3 "No").
     only_excluded = np.arange(N) % 5 == 0
     for name in OTHER + OTHER_PERSON:
@@ -115,7 +126,7 @@ def _simulate(year, units, reform=None):
     for i in range(N):
         person = f"p{i}"
         people[person] = {"age": {period: 50}}
-        for name in PERSON_COMPONENTS:
+        for name in PERSON_COMPONENTS + (PAID,):
             people[person][name] = {period: float(units[name][i])}
         tax_units[f"t{i}"] = {
             "members": [person],
diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/itemized_taxable_income_deductions_reduction.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/itemized_taxable_income_deductions_reduction.yaml
index 965f6ed655..33c73c8c16 100644
--- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/itemized_taxable_income_deductions_reduction.yaml
+++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/itemized_taxable_income_deductions_reduction.yaml
@@ -73,6 +73,8 @@
         members: [head, spouse]
         adjusted_gross_income: 600_000
         filing_status: JOINT
+        # Schedule A line 14 is the Form 4952 deduction, supplied directly.
+        investment_interest_expense_deduction: 30_000
         casualty_loss_deduction: 20_000
         charitable_deduction: 10_000
         medical_expense_deduction: 0

The OBBB branch is unaffected: it uses the total with no exclusion list.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant