From 1d7af3926082cd3e97d3fda6c24adff80a89e36f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 4 Oct 2026 06:53:48 -0400 Subject: [PATCH 01/11] Limit investment interest deductions using Form 4952 --- .../investment-interest-163d-limit.fixed.md | 1 + .../irs/deductions/itemized/misc/applies.yaml | 6 +- .../irs/deductions/itemized/misc/sources.yaml | 9 + ...st_investment_interest_limit_properties.py | 293 +++++++++++++++++ .../investment_interest/form_4952.yaml | 311 ++++++++++++++++++ .../itemizing/interest_deduction.py | 25 +- .../form_4952_elected_investment_income.py | 32 ++ .../form_4952_gross_investment_income.py | 44 +++ .../form_4952_investment_expenses.py | 38 +++ .../form_4952_investment_income.py | 30 ++ ...m_4952_investment_interest_carryforward.py | 27 ++ .../form_4952_net_capital_gain.py | 35 ++ .../form_4952_net_investment_gain.py | 32 ++ .../form_4952_net_investment_income.py | 24 ++ .../form_4952_qualified_dividends.py | 27 ++ ..._4952_total_investment_interest_expense.py | 24 ++ .../investment_interest_expense_deduction.py | 26 ++ .../person/deductible_interest_expense.py | 7 +- .../expense/person/investment_expense.py | 16 +- .../expense/person/non_mortgage_interest.py | 7 +- 20 files changed, 1006 insertions(+), 8 deletions(-) create mode 100644 changelog.d/investment-interest-163d-limit.fixed.md create mode 100644 policyengine_us/tests/core/test_investment_interest_limit_properties.py create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_elected_investment_income.py create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_gross_investment_income.py create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_expenses.py create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_income.py create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_interest_carryforward.py create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_capital_gain.py create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_gain.py create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_income.py create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_qualified_dividends.py create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_total_investment_interest_expense.py create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/investment_interest_expense_deduction.py diff --git a/changelog.d/investment-interest-163d-limit.fixed.md b/changelog.d/investment-interest-163d-limit.fixed.md new file mode 100644 index 00000000000..ee7167ffb8e --- /dev/null +++ b/changelog.d/investment-interest-163d-limit.fixed.md @@ -0,0 +1 @@ +Limit the itemized investment interest deduction to net investment income under 26 U.S.C. 163(d)(1) via Form 4952 and update California, New York, Virginia, and Montana to use the limited federal deduction. diff --git a/policyengine_us/parameters/gov/irs/deductions/itemized/misc/applies.yaml b/policyengine_us/parameters/gov/irs/deductions/itemized/misc/applies.yaml index cbd0245f845..51c81974a08 100644 --- a/policyengine_us/parameters/gov/irs/deductions/itemized/misc/applies.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/itemized/misc/applies.yaml @@ -4,8 +4,10 @@ metadata: period: year label: Miscellaneous expense deduction applies reference: - - title: 26 U.S. Code § 67 - 2-percent floor on miscellaneous itemized deductions (g) - href: https://www.law.cornell.edu/uscode/text/26/67#g + # P.L. 119-21 section 70110 removed the 2025 sunset and redesignated + # former subsection (g) as (h), effective for tax years after 2025. + - title: 26 U.S. Code § 67(h) - Disallowance of miscellaneous itemized deductions (P.L. 119-21 section 70110) + href: https://www.law.cornell.edu/uscode/text/26/67#h values: 2013-01-01: true 2018-01-01: false diff --git a/policyengine_us/parameters/gov/irs/deductions/itemized/misc/sources.yaml b/policyengine_us/parameters/gov/irs/deductions/itemized/misc/sources.yaml index 1a04b0c1206..6f1d8fe6848 100644 --- a/policyengine_us/parameters/gov/irs/deductions/itemized/misc/sources.yaml +++ b/policyengine_us/parameters/gov/irs/deductions/itemized/misc/sources.yaml @@ -3,6 +3,7 @@ values: 2013-01-01: - unreimbursed_business_employee_expenses - tax_preparation_fees + - investment_expenses metadata: period: year unit: list @@ -12,3 +13,11 @@ metadata: href: https://www.law.cornell.edu/uscode/text/26/67 - title: 2017 Schedule A Instructions href: https://www.irs.gov/pub/irs-prior/i1040sca--2017.pdf#page=12 + # Schedule A line 23 includes investment expenses; these states continue + # to use the shared total in years when the federal deduction is disallowed. + - title: 2025 Arkansas AR3 instructions, line 21 (investment fees and expenses) + href: https://www.dfa.arkansas.gov/wp-content/uploads/2025_AR1000F_and_AR1000NR_Instructions.pdf#page=21 + - title: 2025 Arkansas AR3, lines 21-25 (other expenses and 2-percent floor) + href: https://www.dfa.arkansas.gov/wp-content/uploads/2025_AR3_ItemizedDeduction.pdf + - title: 2025 New York IT-196, line 24 (investment expenses) + href: https://www.tax.ny.gov/pdf/2025/inc/it196_2025_fill_in.pdf#page=2 diff --git a/policyengine_us/tests/core/test_investment_interest_limit_properties.py b/policyengine_us/tests/core/test_investment_interest_limit_properties.py new file mode 100644 index 00000000000..17b054bc140 --- /dev/null +++ b/policyengine_us/tests/core/test_investment_interest_limit_properties.py @@ -0,0 +1,293 @@ +"""Vectorized properties of the investment interest limit, 26 U.S.C. 163(d). + +Section 163(d)(1): the deduction "shall not exceed the net investment income +of the taxpayer for the taxable year." Form 4952 (2025), line 8: "Enter the +smaller of line 3 or line 6." Line 7 carries forward the excess of line 3 over +line 6. Its line 4g instructions say "don't enter more than the sum of lines +4b and 4e." Sources: https://www.law.cornell.edu/uscode/text/26/163#d and +https://www.irs.gov/pub/irs-pdf/f4952.pdf. + +For 2017, line 5 includes the smaller of investment expenses on Schedule A +line 23 and Schedule A line 27 (Form 4952 (2017), line 5 instructions, +https://www.irs.gov/pub/irs-prior/f4952--2017.pdf). Section 67(a)'s 2% floor +applies to the generated miscellaneous expenses; section 67(h) disallows +miscellaneous itemized deductions after 2017. The independent calculation +uses raw inputs, without reading model outputs or parameters. + +Each case builds one Simulation containing seeded random single and joint +returns, including dependents. Interest and dividends exclude dependents, +matching irs_gross_income; expenses, gains and elections retain the existing +all-member aggregation. Prior-year disallowed investment interest has no +input, so line 3 contains current-year interest only. Comparison cohorts +share one Simulation, avoiding dependence on computed-value invalidation. +""" + +import numpy as np +import pytest + +from policyengine_us import Simulation + +N = 400 +YEARS = (2017, 2025, 2026) +ROLES = ("head", "spouse", "dependent") +PERSON_INPUTS = ( + "taxable_interest_income", + "qualified_dividend_income", + "non_qualified_dividend_income", + "long_term_capital_gains", + "short_term_capital_gains", + "non_sch_d_capital_gains", + "investment_income_elected_form_4952", + "investment_interest_expense", + "investment_expenses", + "unreimbursed_business_employee_expenses", + "tax_preparation_fees", + "deductible_mortgage_interest", +) +LINES = { + "3": "form_4952_total_investment_interest_expense", + "4a": "form_4952_gross_investment_income", + "4b": "form_4952_qualified_dividends", + "4d": "form_4952_net_investment_gain", + "4e": "form_4952_net_capital_gain", + "4g": "form_4952_elected_investment_income", + "4h": "form_4952_investment_income", + "5": "form_4952_investment_expenses", + "6": "form_4952_net_investment_income", + "7": "form_4952_investment_interest_carryforward", + "8": "investment_interest_expense_deduction", +} +# Like the section 68 property tests, comparisons allow float32 rounding +# against the operands' scale, including cancellation before a zero floor. +RTOL = 1e-6 +ATOL = 0.01 + + +def _draw(seed): + rng = np.random.default_rng(seed) + joint = rng.random(N) < 0.5 + present = np.column_stack((np.ones(N, dtype=bool), joint, np.ones(N, dtype=bool))) + filers = present.copy() + filers[:, 2] = False + raw = { + "present": present, + "filers": filers, + "adjusted_gross_income": np.round(rng.uniform(0, 400_000, N), 2), + } + for name in PERSON_INPUTS: + if name in ("long_term_capital_gains", "short_term_capital_gains"): + amount = rng.uniform(-60_000, 80_000, (N, len(ROLES))) + elif name == "investment_income_elected_form_4952": + # Include negative inputs to exercise the lower election bound, + # and large elections to exercise the line 4b + line 4e cap. + amount = rng.uniform(-20_000, 200_000, (N, len(ROLES))) + else: + amount = rng.uniform(0, 40_000, (N, len(ROLES))) + zero = rng.random(amount.shape) < 0.35 + raw[name] = np.where(present & ~zero, np.round(amount, 2), 0) + + # Repeated boundary conditions complement the continuous random draws. + no_income = np.arange(N) % 8 == 0 + for name in PERSON_INPUTS[:7]: + raw[name][no_income] = 0 + no_interest = np.arange(N) % 8 == 1 + raw["investment_interest_expense"][no_interest] = 0 + no_election = np.arange(N) % 8 == 2 + raw["investment_income_elected_form_4952"][no_election] = 0 + return raw + + +def _simulate(year, raw): + period = str(year) + people, tax_units, households, marital_units = {}, {}, {}, {} + for i in range(len(raw["adjusted_gross_income"])): + members, spouses = [], [] + for j, role in enumerate(ROLES): + if not raw["present"][i, j]: + continue + person = f"u{i}_{role}" + members.append(person) + people[person] = { + "age": {period: 12 if role == "dependent" else 45}, + "is_tax_unit_head": {period: role == "head"}, + "is_tax_unit_spouse": {period: role == "spouse"}, + "is_tax_unit_dependent": {period: role == "dependent"}, + **{name: {period: float(raw[name][i, j])} for name in PERSON_INPUTS}, + } + if role == "dependent": + marital_units[f"m{i}_dependent"] = {"members": [person]} + else: + spouses.append(person) + marital_units[f"m{i}"] = {"members": spouses} + tax_units[f"t{i}"] = { + "members": members, + "filing_status": {period: "JOINT" if raw["present"][i, 1] else "SINGLE"}, + "adjusted_gross_income": {period: float(raw["adjusted_gross_income"][i])}, + } + households[f"h{i}"] = {"members": members} + simulation = Simulation( + situation={ + "people": people, + "tax_units": tax_units, + "households": households, + "marital_units": marital_units, + } + ) + outputs = (*LINES.values(), "interest_deduction", "misc_deduction") + return {name: simulation.calculate(name, period) for name in outputs} + + +def _restatement(year, raw): + """Form 4952 lines 3-8, independently restated from person inputs.""" + filer = raw["filers"] + line_3 = raw["investment_interest_expense"].sum(axis=1) + line_4a = ( + ( + raw["taxable_interest_income"] + + raw["qualified_dividend_income"] + + raw["non_qualified_dividend_income"] + ) + * filer + ).sum(axis=1) + line_4b = (raw["qualified_dividend_income"] * filer).sum(axis=1) + long_term = raw["long_term_capital_gains"].sum(axis=1) + short_term = raw["short_term_capital_gains"].sum(axis=1) + distributions = raw["non_sch_d_capital_gains"].sum(axis=1) + line_4d = np.maximum(0, long_term + short_term + distributions) + line_4e = np.minimum( + line_4d, + np.maximum(0, long_term + distributions - np.maximum(0, -short_term)), + ) + line_4g = np.minimum( + np.maximum(0, raw["investment_income_elected_form_4952"].sum(axis=1)), + line_4b + line_4e, + ) + line_4h = line_4a - line_4b + line_4d - line_4e + line_4g + expenses = raw["investment_expenses"].sum(axis=1) + if year == 2017: + schedule_a_line_24 = ( + expenses + + raw["unreimbursed_business_employee_expenses"].sum(axis=1) + + raw["tax_preparation_fees"].sum(axis=1) + ) + schedule_a_line_27 = np.maximum( + 0, + schedule_a_line_24 - 0.02 * np.maximum(0, raw["adjusted_gross_income"]), + ) + line_5 = np.minimum(expenses, schedule_a_line_27) + else: + schedule_a_line_27 = np.zeros_like(expenses) + line_5 = np.zeros_like(expenses) + line_6 = np.maximum(0, line_4h - line_5) + line_7 = np.maximum(0, line_3 - line_6) + line_8 = np.minimum(line_3, line_6) + line_values = ( + line_3, + line_4a, + line_4b, + line_4d, + line_4e, + line_4g, + line_4h, + line_5, + line_6, + line_7, + line_8, + ) + expected = dict(zip(LINES.values(), line_values)) + expected["misc_deduction"] = schedule_a_line_27 + expected["interest_deduction"] = ( + raw["deductible_mortgage_interest"].sum(axis=1) + line_8 + ) + return expected + + +def _scale(raw): + return raw["adjusted_gross_income"] + sum( + np.abs(raw[name]).sum(axis=1) for name in PERSON_INPUTS + ) + + +def _slack(scale): + return ATOL + RTOL * np.abs(scale) + + +def _close(actual, expected, scale, name): + gap = np.abs(np.asarray(actual, dtype=float) - expected) + worst = int(np.argmax(gap - _slack(scale))) + assert np.all(gap <= _slack(scale)), ( + f"{name}, unit {worst}: {actual[worst]} vs {expected[worst]}" + ) + + +@pytest.mark.parametrize("year", YEARS) +def test_form_4952_matches_independent_restatement_and_bounds(year): + raw = _draw(seed=year) + actual = _simulate(year, raw) + expected = _restatement(year, raw) + scale = _scale(raw) + slack = _slack(scale) + for name, value in expected.items(): + _close(actual[name], value, scale, name) + + paid = actual[LINES["3"]] + net_income = actual[LINES["6"]] + carried = actual[LINES["7"]] + allowed = actual[LINES["8"]] + assert np.all(allowed >= 0) + assert np.all(allowed <= net_income + slack) + assert np.all(allowed <= paid + slack) + assert np.all(carried >= 0) + _close(carried + allowed, paid, scale, "line 7 + line 8 = line 3") + + assert np.all(actual[LINES["4e"]] >= 0) + assert np.all(actual[LINES["4e"]] <= actual[LINES["4d"]] + slack) + assert np.all(actual[LINES["4g"]] >= 0) + assert np.all( + actual[LINES["4g"]] <= actual[LINES["4b"]] + actual[LINES["4e"]] + slack + ) + mortgage = raw["deductible_mortgage_interest"].sum(axis=1) + _close( + actual["interest_deduction"], + mortgage + allowed, + scale, + "mortgage interest + line 8", + ) + + +@pytest.mark.parametrize("year", YEARS) +def test_investment_interest_limit_is_monotone(year): + base = _draw(seed=year + 10) + rng = np.random.default_rng(year + 20) + changed_inputs = ( + "investment_income_elected_form_4952", + "taxable_interest_income", + "investment_interest_expense", + ) + cohorts = [base] + for name in changed_inputs: + raised = {key: value.copy() for key, value in base.items()} + # Raise the head's input; zero increments exercise equality, while + # positive increments are independent of all original amounts. + increment = np.round(rng.uniform(0, 60_000, N), 2) + increment[np.arange(N) % 5 == 0] = 0 + raised[name][:, 0] += increment + cohorts.append(raised) + combined = { + key: np.concatenate([cohort[key] for cohort in cohorts], axis=0) for key in base + } + actual = _simulate(year, combined) + expected = _restatement(year, combined) + scale = _scale(combined) + # Verify the altered cohorts too, independently of the inequality checks. + for name, value in expected.items(): + _close(actual[name], value, scale, name) + + base_allowed = actual[LINES["8"]][:N] + base_carried = actual[LINES["7"]][:N] + for i, name in enumerate(changed_inputs, start=1): + cohort = slice(i * N, (i + 1) * N) + slack = _slack(scale[cohort] + scale[:N]) + assert np.all(actual[LINES["8"]][cohort] >= base_allowed - slack), name + if name == "investment_interest_expense": + assert np.all(actual[LINES["7"]][cohort] >= base_carried - slack) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml new file mode 100644 index 00000000000..0ffff5a18e2 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml @@ -0,0 +1,311 @@ +# Sources: 26 U.S.C. 163(d)(1)-(4), https://www.law.cornell.edu/uscode/text/26/163#d +# 2025 Form 4952: "Enter the smaller of line 3 or line 6" (line 8). +# https://www.irs.gov/pub/irs-pdf/f4952.pdf +# 2017 Form 4952 line 5: "Include on line 5 the smaller of" investment +# expenses on Schedule A line 23 and the total on Schedule A line 27. +# https://www.irs.gov/pub/irs-prior/f4952--2017.pdf + +- name: Interest income above interest paid allows the full deduction + period: 2025 + input: + taxable_interest_income: 10_000 + investment_interest_expense: 6_000 + output: + form_4952_total_investment_interest_expense: 6_000 # line 3 = line 1 6,000 + unmodeled line 2 0 + form_4952_gross_investment_income: 10_000 # line 4a = taxable interest 10,000 + dividends 0 + form_4952_qualified_dividends: 0 # line 4b = qualified dividends 0 + form_4952_net_investment_gain: 0 # line 4d = max(0, gains 0) + form_4952_net_capital_gain: 0 # line 4e = min(4d 0, net long-term gain 0) + form_4952_elected_investment_income: 0 # line 4g = min(election 0, 4b 0 + 4e 0) + form_4952_investment_income: 10_000 # line 4h = (10,000 - 0) + (0 - 0) + 0 + form_4952_investment_expenses: 0 # line 5 = 0 because miscellaneous deductions do not apply + form_4952_net_investment_income: 10_000 # line 6 = max(0, 4h 10,000 - line 5 0) + form_4952_investment_interest_carryforward: 0 # line 7 = max(0, 3 6,000 - 6 10,000) + investment_interest_expense_deduction: 6_000 # line 8 = min(3 6,000, 6 10,000) + +- name: No investment income disallows all investment interest + period: 2025 + input: + investment_interest_expense: 6_000 + taxable_interest_income: 0 + output: + form_4952_net_investment_income: 0 # line 6 = max(0, 4h 0 - 5 0) + form_4952_investment_interest_carryforward: 6_000 # line 7 = max(0, 3 6,000 - 6 0) + investment_interest_expense_deduction: 0 # line 8 = min(3 6,000, 6 0) + +- name: Qualified dividends are excluded without an election + period: 2025 + input: + qualified_dividend_income: 5_000 + investment_interest_expense: 4_000 + output: + form_4952_gross_investment_income: 5_000 # line 4a = ordinary dividends 5,000 + form_4952_qualified_dividends: 5_000 # line 4b = qualified dividends 5,000 + form_4952_investment_income: 0 # line 4c = 4a 5,000 - 4b 5,000; 4f = 4g = 0 + investment_interest_expense_deduction: 0 # line 8 = min(3 4,000, 6 0) + form_4952_investment_interest_carryforward: 4_000 # line 7 = max(0, 3 4,000 - 6 0) + +- name: Qualified dividend election raises the limit one for one + period: 2025 + input: + qualified_dividend_income: 5_000 + investment_income_elected_form_4952: 2_000 + investment_interest_expense: 4_000 + output: + form_4952_elected_investment_income: 2_000 # line 4g = min(2,000, 4b 5,000 + 4e 0) + form_4952_investment_income: 2_000 # line 4h = 4c 0 + 4f 0 + 4g 2,000 + investment_interest_expense_deduction: 2_000 # line 8 = min(3 4,000, 6 2,000) + form_4952_investment_interest_carryforward: 2_000 # line 7 = max(0, 3 4,000 - 6 2,000) + +- name: Election is capped at qualified dividends plus net capital gain + period: 2025 + input: + qualified_dividend_income: 5_000 + long_term_capital_gains: 3_000 + investment_income_elected_form_4952: 20_000 + investment_interest_expense: 15_000 + output: + form_4952_net_investment_gain: 3_000 # line 4d = max(0, long-term 3,000 + short-term 0) + form_4952_net_capital_gain: 3_000 # line 4e = min(4d 3,000, long-term 3,000) + form_4952_elected_investment_income: 8_000 # line 4g = min(20,000, 4b 5,000 + 4e 3,000) + form_4952_net_investment_income: 8_000 # line 6 = 4c 0 + 4f 0 + 4g 8,000 - 5 0 + investment_interest_expense_deduction: 8_000 # line 8 = min(3 15,000, 6 8,000) + form_4952_investment_interest_carryforward: 7_000 # line 7 = 3 15,000 - 6 8,000 + +- name: Negative election is floored at zero + period: 2025 + input: + qualified_dividend_income: 5_000 + investment_income_elected_form_4952: -1_000 + investment_interest_expense: 4_000 + output: + form_4952_elected_investment_income: 0 # line 4g = min(max(0, -1,000), 4b 5,000 + 4e 0) + investment_interest_expense_deduction: 0 # line 8 = min(3 4,000, 6 0) + +- name: Short-term gain counts automatically and long-term gain requires election + period: 2025 + input: + short_term_capital_gains: 3_000 + long_term_capital_gains: 7_000 + investment_interest_expense: 12_000 + output: + form_4952_net_investment_gain: 10_000 # line 4d = max(0, 3,000 + 7,000) + form_4952_net_capital_gain: 7_000 # line 4e = min(10,000, 7,000 - 0) + form_4952_investment_income: 3_000 # line 4h = 4c 0 + (4d 10,000 - 4e 7,000) + 4g 0 + investment_interest_expense_deduction: 3_000 # line 8 = min(3 12,000, 6 3,000) + form_4952_investment_interest_carryforward: 9_000 # line 7 = 3 12,000 - 6 3,000 + +- name: Electing long-term gain adds it to automatic short-term gain + period: 2025 + input: + short_term_capital_gains: 3_000 + long_term_capital_gains: 7_000 + investment_income_elected_form_4952: 5_000 + investment_interest_expense: 12_000 + output: + form_4952_investment_income: 8_000 # line 4h = 4c 0 + (10,000 - 7,000) + 4g 5,000 + investment_interest_expense_deduction: 8_000 # line 8 = min(3 12,000, 6 8,000) + form_4952_investment_interest_carryforward: 4_000 # line 7 = 3 12,000 - 6 8,000 + +- name: Short-term loss reduces the long-term gain available for election + period: 2025 + input: + short_term_capital_gains: -3_000 + long_term_capital_gains: 8_000 + investment_income_elected_form_4952: 4_000 + investment_interest_expense: 6_000 + output: + form_4952_net_investment_gain: 5_000 # line 4d = max(0, 8,000 - 3,000) + form_4952_net_capital_gain: 5_000 # line 4e = min(5,000, max(0, 8,000 - 3,000)) + form_4952_investment_income: 4_000 # line 4h = 4c 0 + (5,000 - 5,000) + 4g 4,000 + investment_interest_expense_deduction: 4_000 # line 8 = min(3 6,000, 6 4,000) + form_4952_investment_interest_carryforward: 2_000 # line 7 = 3 6,000 - 6 4,000 + +- name: Long-term loss offsets short-term gain + period: 2025 + input: + short_term_capital_gains: 10_000 + long_term_capital_gains: -8_000 + investment_interest_expense: 6_000 + output: + form_4952_net_investment_gain: 2_000 # line 4d = max(0, 10,000 - 8,000) + form_4952_net_capital_gain: 0 # line 4e = min(2,000, max(0, -8,000)) + investment_interest_expense_deduction: 2_000 # line 8 = min(3 6,000, 6 2,000) + form_4952_investment_interest_carryforward: 4_000 # line 7 = 3 6,000 - 6 2,000 + +- name: Distributions without Schedule D are long-term gain requiring election + period: 2025 + input: + non_sch_d_capital_gains: 3_000 + investment_interest_expense: 4_000 + output: + form_4952_net_investment_gain: 3_000 # line 4d = max(0, net capital gains 0 + distributions 3,000) + form_4952_net_capital_gain: 3_000 # line 4e = min(3,000, long-term 0 + distributions 3,000) + investment_interest_expense_deduction: 0 # line 8 = min(3 4,000, 6 (4d 3,000 - 4e 3,000)) + +- name: Schedule D distributions are already included in long-term gains + period: 2025 + input: + long_term_capital_gains: 5_000 + schedule_d_capital_gain_distributions: 2_000 # memo component of the 5,000 above + non_sch_d_capital_gains: 3_000 + investment_income_elected_form_4952: 10_000 + investment_interest_expense: 12_000 + output: + form_4952_net_investment_gain: 8_000 # line 4d = max(0, gains 5,000 + non-Schedule-D distributions 3,000) + form_4952_net_capital_gain: 8_000 # line 4e = min(8,000, long-term 5,000 + distributions 3,000) + form_4952_elected_investment_income: 8_000 # line 4g = min(10,000, 4b 0 + 4e 8,000) + investment_interest_expense_deduction: 8_000 # line 8 = min(3 12,000, 6 8,000) + +- name: Joint income excludes dependent interest and dividends while expenses preserve existing membership + period: 2025 + input: + people: + head: + is_tax_unit_head: true + taxable_interest_income: 4_000 + qualified_dividend_income: 1_000 + investment_interest_expense: 6_000 + spouse: + is_tax_unit_spouse: true + taxable_interest_income: 3_000 + non_qualified_dividend_income: 2_000 + qualified_dividend_income: 2_000 + investment_interest_expense: 4_000 + child: + is_tax_unit_dependent: true + taxable_interest_income: 50_000 + qualified_dividend_income: 10_000 + non_qualified_dividend_income: 10_000 + investment_interest_expense: 500 + tax_units: + unit: + members: [head, spouse, child] + output: + form_4952_total_investment_interest_expense: 10_500 # line 3 = 6,000 + 4,000 + 500 (all members) + form_4952_gross_investment_income: 12_000 # line 4a = filers' interest 7,000 + ordinary dividends 5,000 + form_4952_qualified_dividends: 3_000 # line 4b = filers' qualified dividends 1,000 + 2,000 + form_4952_investment_income: 9_000 # line 4c = 4a 12,000 - 4b 3,000; 4f = 4g = 0 + investment_interest_expense_deduction: 9_000 # line 8 = min(3 10,500, 6 9,000) + form_4952_investment_interest_carryforward: 1_500 # line 7 = 3 10,500 - 6 9,000 + +- name: Alaska Permanent Fund dividends and business property gains stay out of line 4a and 4d + period: 2025 + input: + ak_permanent_fund_dividend: 2_000 + other_net_gain: 5_000 # Form 4797 business property gain + tax_exempt_interest_income: 3_000 + investment_interest_expense: 4_000 + output: + form_4952_gross_investment_income: 0 # line 4a = taxable interest 0 + ordinary dividends 0 + form_4952_net_investment_gain: 0 # line 4d = net capital gains 0 + non-Schedule-D distributions 0 + investment_interest_expense_deduction: 0 # line 8 = min(3 4,000, 6 0) + +- name: 2017 line 5 uses miscellaneous deduction after the 2 percent floor + period: 2017 + input: + adjusted_gross_income: 100_000 + taxable_interest_income: 12_000 + investment_interest_expense: 20_000 + investment_expenses: 3_000 + tax_preparation_fees: 500 + unreimbursed_business_employee_expenses: 500 + output: + total_misc_deductions: 4_000 # Schedule A line 24 = investment expenses 3,000 + other expenses 1,000 + misc_deduction: 2_000 # Schedule A line 27 = max(0, 4,000 - 2% * 100,000) + form_4952_investment_expenses: 2_000 # line 5 = min(Schedule A line 23 3,000, line 27 2,000) + form_4952_net_investment_income: 10_000 # line 6 = max(0, 4h 12,000 - 5 2,000) + investment_interest_expense_deduction: 10_000 # line 8 = min(3 20,000, 6 10,000) + form_4952_investment_interest_carryforward: 10_000 # line 7 = 3 20,000 - 6 10,000 + +- name: 2017 line 5 cannot exceed investment expenses even with other miscellaneous deductions + period: 2017 + input: + adjusted_gross_income: 100_000 + taxable_interest_income: 12_000 + investment_interest_expense: 20_000 + investment_expenses: 3_000 + tax_preparation_fees: 3_000 + output: + misc_deduction: 4_000 # Schedule A line 27 = 3,000 + 3,000 - 2% * 100,000 + form_4952_investment_expenses: 3_000 # line 5 = min(investment expenses 3,000, misc deduction 4,000) + investment_interest_expense_deduction: 9_000 # line 8 = min(3 20,000, 6 (12,000 - 3,000)) + +- name: 2017 investment expenses below the floor do not reduce investment income + period: 2017 + input: + adjusted_gross_income: 100_000 + taxable_interest_income: 12_000 + investment_interest_expense: 20_000 + investment_expenses: 500 + output: + form_4952_investment_expenses: 0 # line 5 = min(500, max(0, 500 - 2% * 100,000)) + investment_interest_expense_deduction: 12_000 # line 8 = min(3 20,000, 6 (12,000 - 0)) + +- name: 2026 line 5 remains zero under permanent section 67 h disallowance + period: 2026 + input: + adjusted_gross_income: 100_000 + taxable_interest_income: 12_000 + investment_interest_expense: 20_000 + investment_expenses: 3_000 + tax_preparation_fees: 3_000 + output: + total_misc_deductions: 6_000 # shared Schedule A expense total = investment 3,000 + tax preparation 3,000 + misc_deduction: 0 # section 67(h) disallows miscellaneous itemized deductions after 2017 + form_4952_investment_expenses: 0 # line 5 = 0 because misc.applies is false + investment_interest_expense_deduction: 12_000 # line 8 = min(3 20,000, 6 (12,000 - 0)) + form_4952_investment_interest_carryforward: 8_000 # line 7 = 3 20,000 - 6 12,000 + +- name: Schedule A line 10 adds mortgage interest and Form 4952 line 8 + period: 2025 + input: + deductible_mortgage_interest: 10_000 + investment_interest_expense: 8_000 + taxable_interest_income: 3_000 + output: + deductible_interest_expense: 18_000 # amount paid = mortgage 10,000 + investment 8,000 before line 8 limit + investment_interest_expense_deduction: 3_000 # line 8 = min(3 8,000, 6 3,000) + interest_deduction: 13_000 # Schedule A line 10 = mortgage line 8e 10,000 + investment line 9 3,000 + +- name: A direct deductible interest expense input retains its full deduction + period: 2025 + input: + deductible_interest_expense: 25_000 + investment_interest_expense: 0 + taxable_interest_income: 0 + output: + interest_deduction: 25_000 # Schedule A line 10 = paid override 25,000 - line 3 0 + line 8 0 + +- name: 2025 investment interest limit flows through itemized deductions to federal income tax + absolute_error_margin: 0.02 + period: 2025 + input: + people: + head: + is_tax_unit_head: true + age: 40 + employment_income: 140_000 + taxable_interest_income: 10_000 + deductible_mortgage_interest: 20_000 + investment_interest_expense: 15_000 + tax_units: + unit: + members: [head] + state_income_tax: 0 + state_sales_tax: 0 + households: + household: + members: [head] + state_code: TX + output: + investment_interest_expense_deduction: 10_000 # line 8 = min(3 15,000, 6 10,000) + interest_deduction: 30_000 # Schedule A line 10 = line 8e 20,000 + line 9 10,000 + itemized_taxable_income_deductions: 30_000 # Schedule A line 17 = line 10 30,000 + other itemized deductions 0 + adjusted_gross_income: 150_000 # Form 1040 AGI = wages 140,000 + taxable interest 10,000 + taxable_income: 120_000 # Form 1040 taxable income = AGI 150,000 - itemized deductions 30,000 + # 2025 single rate schedule (taxable income >= 100,000; Tax Table not applicable): + # 11,925 * 10% + (48,475 - 11,925) * 12% + (103,350 - 48,475) * 22% + # + (120,000 - 103,350) * 24% = 1,192.50 + 4,386 + 12,072.50 + 3,996 = 21,647. + # Source: https://www.irs.gov/pub/irs-drop/rp-24-40.pdf#page=6 + income_tax: 21_647 # federal tax after credits = regular tax 21,647 + AMT/NIIT 0 - credits 0 diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/interest_deduction.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/interest_deduction.py index effde0252ef..9b680c33f25 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/interest_deduction.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/interest_deduction.py @@ -6,8 +6,27 @@ class interest_deduction(Variable): entity = TaxUnit label = "Interest deduction" unit = USD - documentation = "Interest expenses deducted from taxable income." + documentation = """ + Schedule A line 10: "Add lines 8e and 9." Line 8e is mortgage interest + and line 9 is "Investment interest. Attach Form 4952 if required." + Replace investment interest paid in deductible_interest_expense with + the Form 4952 line 8 deduction limited under 163(d). Retain the existing + all-member aggregation and honor direct deductible_interest_expense + inputs by adjusting only the separately reported investment interest. + """ definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/163" + reference = [ + "https://www.law.cornell.edu/uscode/text/26/163#d_1", + "https://www.irs.gov/pub/irs-prior/f1040sa--2025.pdf", + "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + ] - adds = ["deductible_interest_expense"] + def formula(tax_unit, period, parameters): + interest_paid = add(tax_unit, period, ["deductible_interest_expense"]) + investment_interest_paid = tax_unit( + "form_4952_total_investment_interest_expense", period + ) + investment_interest_allowed = tax_unit( + "investment_interest_expense_deduction", period + ) + return interest_paid - investment_interest_paid + investment_interest_allowed diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_elected_investment_income.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_elected_investment_income.py new file mode 100644 index 00000000000..874cc1b2791 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_elected_investment_income.py @@ -0,0 +1,32 @@ +from policyengine_us.model_api import * + + +class form_4952_elected_investment_income(Variable): + value_type = float + entity = TaxUnit + label = "Form 4952 elected investment income" + unit = USD + definition_period = YEAR + documentation = """ + Form 4952 line 4g: "Enter the amount from lines 4b and 4e that you elect + to include in investment income." The instructions say "don't enter + more than the sum of lines 4b and 4e". Sum the existing Person election + inputs across the tax unit, floor at zero, and cap at lines 4b plus 4e. + The line 4g note says the election is "attributable first to net capital + gain" and "then to qualified dividends". No input exists for the + alternative "Elec." allocation next to line 4e, so the default gain-first + allocation applies. This deduction calculation does not change the + separate capital gains tax worksheets. + """ + reference = [ + "https://www.law.cornell.edu/uscode/text/26/163#d_4_B_iii", + "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + ] + + def formula(tax_unit, period, parameters): + election = max_( + 0, add(tax_unit, period, ["investment_income_elected_form_4952"]) + ) + qualified_dividends = tax_unit("form_4952_qualified_dividends", period) + net_capital_gain = tax_unit("form_4952_net_capital_gain", period) + return min_(election, qualified_dividends + net_capital_gain) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_gross_investment_income.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_gross_investment_income.py new file mode 100644 index 00000000000..53d2fb66f1d --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_gross_investment_income.py @@ -0,0 +1,44 @@ +from policyengine_us.model_api import * + + +class form_4952_gross_investment_income(Variable): + value_type = float + entity = TaxUnit + label = "Form 4952 gross investment income" + unit = USD + definition_period = YEAR + documentation = """ + Form 4952 line 4a: "Gross income from property held for investment + (excluding any net gain from the disposition of property held for + investment)". Sum taxable interest and ordinary dividends (including + qualified dividends) of nondependent tax-unit members, matching the + is_tax_unit_dependent exclusion in irs_gross_income and AGI. Dependents' + own interest and dividends are not attributed to the filer's return. + Ordinary dividends contain only qualified and non-qualified dividends; + the separately modeled Alaska Permanent Fund dividend is excluded, as + the line 4a instructions require. + + Line 4a omits non-employer annuities because the pension inputs do not + separate them from employer pensions, and royalties because they are + not separated from rental_income. Schedule K-1 portfolio items, net + investment income from estates and trusts, Form 8814 child income, + and publicly traded partnership passive income have no separate inputs + identifying the amounts that belong on this line. Employer pensions + are compensation under 26 CFR 1.469-2T(c)(4)(i)(C)-(D), including + "deferred compensation for services". On that basis, the model treats + them as compensation rather than income from property held for investment. + """ + reference = [ + "https://www.law.cornell.edu/uscode/text/26/163#d_4_B_i", + "https://www.law.cornell.edu/uscode/text/26/163#d_5", + "https://www.law.cornell.edu/cfr/text/26/1.469-2T", + "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + ] + + def formula(tax_unit, period, parameters): + person = tax_unit.members + not_dependent = ~person("is_tax_unit_dependent", period) + income = add( + person, period, ["taxable_interest_income", "ordinary_dividend_income"] + ) + return tax_unit.sum(income * not_dependent) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_expenses.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_expenses.py new file mode 100644 index 00000000000..dcb65c0f62e --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_expenses.py @@ -0,0 +1,38 @@ +from policyengine_us.model_api import * + + +class form_4952_investment_expenses(Variable): + value_type = float + entity = TaxUnit + label = "Form 4952 investment expenses" + unit = USD + definition_period = YEAR + documentation = """ + Form 4952 line 5: "Investment expenses (see instructions)". Section + 163(d)(4)(C) includes "deductions allowed" other than interest that are + "directly connected with the production of investment income". + The 2017 instructions require "the smaller of: (a) the investment + expenses included on Schedule A (Form 1040), line 23; or (b) the total + on Schedule A (Form 1040), line 27." The model's misc_deduction applies + the 2% AGI floor to the miscellaneous expense pool, which includes + investment_expenses from Schedule A line 23. Aggregate these expenses + with the same all-member membership as that pool. + When misc.applies is false, these expenses are not allowed deductions + and line 5 is zero. Section 67(h), as amended by P.L. 119-21 section + 70110, permanently disallows miscellaneous itemized deductions for + taxable years beginning after December 31, 2017. No separate input + identifies other allowed non-interest investment expenses. + """ + reference = [ + "https://www.law.cornell.edu/uscode/text/26/163#d_4_C", + "https://www.law.cornell.edu/uscode/text/26/67#h", + "https://www.irs.gov/pub/irs-prior/f4952--2017.pdf", + "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + ] + + def formula(tax_unit, period, parameters): + if not parameters(period).gov.irs.deductions.itemized.misc.applies: + return 0 + expenses = add(tax_unit, period, ["investment_expenses"]) + allowed_misc_deduction = tax_unit("misc_deduction", period) + return min_(expenses, allowed_misc_deduction) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_income.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_income.py new file mode 100644 index 00000000000..9edbd14b258 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_income.py @@ -0,0 +1,30 @@ +from policyengine_us.model_api import * + + +class form_4952_investment_income(Variable): + value_type = float + entity = TaxUnit + label = "Form 4952 investment income" + unit = USD + definition_period = YEAR + documentation = """ + Form 4952 line 4h: "Investment income. Add lines 4c, 4f, and 4g." + Line 4c is "Subtract line 4b from line 4a" and line 4f is "Subtract + line 4e from line 4d". Thus ordinary investment income and net gains + other than net capital gain count automatically; qualified dividends + and net capital gain count only to the extent elected on line 4g. + """ + reference = [ + "https://www.law.cornell.edu/uscode/text/26/163#d_4_B", + "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + ] + + def formula(tax_unit, period, parameters): + gross_income = tax_unit("form_4952_gross_investment_income", period) + qualified_dividends = tax_unit("form_4952_qualified_dividends", period) + net_gain = tax_unit("form_4952_net_investment_gain", period) + net_capital_gain = tax_unit("form_4952_net_capital_gain", period) + election = tax_unit("form_4952_elected_investment_income", period) + return ( + gross_income - qualified_dividends + net_gain - net_capital_gain + election + ) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_interest_carryforward.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_interest_carryforward.py new file mode 100644 index 00000000000..d25de4c7883 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_interest_carryforward.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class form_4952_investment_interest_carryforward(Variable): + value_type = float + entity = TaxUnit + label = "Form 4952 disallowed investment interest carryforward" + unit = USD + definition_period = YEAR + documentation = """ + Form 4952 line 7: "Disallowed investment interest expense to be carried + forward to 2026. Subtract line 6 from line 3. If zero or less, enter -0-." + Section 163(d)(2) treats disallowed interest as "investment interest + paid or accrued by the taxpayer in the succeeding taxable year". + This variable reports the current-year amount available to carry + forward; prior-year carryforwards (line 2) have no input and do not + automatically enter a later year's calculation. + """ + reference = [ + "https://www.law.cornell.edu/uscode/text/26/163#d_2", + "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + ] + + def formula(tax_unit, period, parameters): + interest = tax_unit("form_4952_total_investment_interest_expense", period) + net_income = tax_unit("form_4952_net_investment_income", period) + return max_(0, interest - net_income) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_capital_gain.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_capital_gain.py new file mode 100644 index 00000000000..a70b47026fa --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_capital_gain.py @@ -0,0 +1,35 @@ +from policyengine_us.model_api import * + + +class form_4952_net_capital_gain(Variable): + value_type = float + entity = TaxUnit + label = "Form 4952 net capital gain" + unit = USD + definition_period = YEAR + documentation = """ + Form 4952 line 4e: "Enter the smaller of line 4d or your net capital gain + from the disposition of property held for investment." The instructions + define net capital gain as the "excess, if any, of your net long-term + capital gain over your net short-term capital loss" and state "Capital + gain distributions from mutual funds are treated as long-term capital + gains." As on line 4d, every capital asset is treated as property held + for investment and all tax-unit members' gains are summed, matching the + Schedule D Tax Worksheet Form 4952 helper. Schedule D distributions + already belong to long_term_capital_gains; add non_sch_d_capital_gains + once. Capital loss carryovers have no model input and are not modeled. + """ + reference = [ + "https://www.law.cornell.edu/uscode/text/26/163#d_4_B_ii_II", + "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + ] + + def formula(tax_unit, period, parameters): + net_gain = tax_unit("form_4952_net_investment_gain", period) + long_term_gains = add(tax_unit, period, ["long_term_capital_gains"]) + short_term_gains = add(tax_unit, period, ["short_term_capital_gains"]) + distributions = add(tax_unit, period, ["non_sch_d_capital_gains"]) + net_capital_gain = max_( + 0, long_term_gains + distributions - max_(0, -short_term_gains) + ) + return min_(net_gain, net_capital_gain) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_gain.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_gain.py new file mode 100644 index 00000000000..ab69ecdd92d --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_gain.py @@ -0,0 +1,32 @@ +from policyengine_us.model_api import * + + +class form_4952_net_investment_gain(Variable): + value_type = float + entity = TaxUnit + label = "Form 4952 net investment gain" + unit = USD + definition_period = YEAR + documentation = """ + Form 4952 line 4d: "Net gain from the disposition of property held for + investment". Its instructions define the "excess, if any, of your total + gains over your total losses" and include capital gain distributions + and capital loss carryovers. The model has no split between investment + and other capital assets, so it treats every capital asset as property + held for investment, matching the Schedule D Tax Worksheet Form 4952 + helper's arithmetic and aggregation of all tax-unit members. + long_term_capital_gains already includes Schedule D distributions; + non_sch_d_capital_gains adds distributions reported without Schedule D. + There is no capital loss carryover input. Form 4797 business-property + gains (other_net_gain) are excluded. + """ + reference = [ + "https://www.law.cornell.edu/uscode/text/26/163#d_4_B_ii", + "https://www.law.cornell.edu/uscode/text/26/163#d_5", + "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + ] + + def formula(tax_unit, period, parameters): + net_gains = tax_unit("net_capital_gains", period) + distributions = add(tax_unit, period, ["non_sch_d_capital_gains"]) + return max_(0, net_gains + distributions) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_income.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_income.py new file mode 100644 index 00000000000..5af45596507 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_income.py @@ -0,0 +1,24 @@ +from policyengine_us.model_api import * + + +class form_4952_net_investment_income(Variable): + value_type = float + entity = TaxUnit + label = "Form 4952 net investment income" + unit = USD + definition_period = YEAR + documentation = """ + Form 4952 line 6: "Net investment income. Subtract line 5 from line 4h. + If zero or less, enter -0-." This implements 163(d)(4)(A)'s excess of + investment income over investment expenses for the interest limit, + separately from net_investment_income used for the NIIT. + """ + reference = [ + "https://www.law.cornell.edu/uscode/text/26/163#d_4_A", + "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + ] + + def formula(tax_unit, period, parameters): + income = tax_unit("form_4952_investment_income", period) + expenses = tax_unit("form_4952_investment_expenses", period) + return max_(0, income - expenses) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_qualified_dividends.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_qualified_dividends.py new file mode 100644 index 00000000000..2bfcae32932 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_qualified_dividends.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class form_4952_qualified_dividends(Variable): + value_type = float + entity = TaxUnit + label = "Form 4952 qualified dividends" + unit = USD + definition_period = YEAR + documentation = """ + Form 4952 line 4b: "Qualified dividends included on line 4a". Sum only + nondependent tax-unit members' qualified dividends, matching line 4a + and the is_tax_unit_dependent exclusion in irs_gross_income and AGI. + Under 163(d)(4)(B), qualified dividend income is investment income + "only to the extent the taxpayer elects to treat such income as + investment income"; line 4b removes it before line 4g's election. + """ + reference = [ + "https://www.law.cornell.edu/uscode/text/26/163#d_4_B", + "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + ] + + def formula(tax_unit, period, parameters): + person = tax_unit.members + not_dependent = ~person("is_tax_unit_dependent", period) + dividends = person("qualified_dividend_income", period) + return tax_unit.sum(dividends * not_dependent) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_total_investment_interest_expense.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_total_investment_interest_expense.py new file mode 100644 index 00000000000..dcee0fb0052 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_total_investment_interest_expense.py @@ -0,0 +1,24 @@ +from policyengine_us.model_api import * + + +class form_4952_total_investment_interest_expense(Variable): + value_type = float + entity = TaxUnit + label = "Form 4952 total investment interest expense" + unit = USD + definition_period = YEAR + documentation = """ + Form 4952 line 3: "Total investment interest expense. Add lines 1 and 2." + Line 1 is "Investment interest expense paid or accrued in 2025". Sum all + tax-unit members' investment_interest_expense, matching the existing + deductible_interest_expense aggregation, which includes dependents. + Prior-year disallowed investment interest (line 2) has no model input + and is not modeled, so line 3 equals current-year line 1. + """ + reference = [ + "https://www.law.cornell.edu/uscode/text/26/163#d_3", + "https://www.law.cornell.edu/uscode/text/26/163#d_2", + "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + ] + + adds = ["investment_interest_expense"] diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/investment_interest_expense_deduction.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/investment_interest_expense_deduction.py new file mode 100644 index 00000000000..c3526ee1261 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/investment_interest_expense_deduction.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * + + +class investment_interest_expense_deduction(Variable): + value_type = float + entity = TaxUnit + label = "Investment interest expense deduction" + unit = USD + definition_period = YEAR + documentation = """ + Form 4952 line 8: "Investment interest expense deduction. Enter the + smaller of line 3 or line 6." Section 163(d)(1) says the investment + interest deduction "shall not exceed the net investment income of + the taxpayer for the taxable year". The Form 4952 line 8 instructions + direct individuals to Schedule A line 9. Prior-year disallowed + investment interest (line 2) is not modeled. + """ + reference = [ + "https://www.law.cornell.edu/uscode/text/26/163#d_1", + "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + ] + + def formula(tax_unit, period, parameters): + interest = tax_unit("form_4952_total_investment_interest_expense", period) + net_income = tax_unit("form_4952_net_investment_income", period) + return min_(interest, net_income) diff --git a/policyengine_us/variables/household/expense/person/deductible_interest_expense.py b/policyengine_us/variables/household/expense/person/deductible_interest_expense.py index 1e42eaeb323..f2645a6de75 100644 --- a/policyengine_us/variables/household/expense/person/deductible_interest_expense.py +++ b/policyengine_us/variables/household/expense/person/deductible_interest_expense.py @@ -4,7 +4,12 @@ class deductible_interest_expense(Variable): value_type = float entity = Person - label = "Interest paid on all loans" + label = "Interest expenses paid before the investment interest limit" + documentation = """ + Mortgage and non-mortgage interest expenses before the 26 U.S.C. 163(d) + investment interest limit. interest_deduction replaces investment + interest paid with the amount allowed on Form 4952 line 8. + """ unit = USD definition_period = YEAR diff --git a/policyengine_us/variables/household/expense/person/investment_expense.py b/policyengine_us/variables/household/expense/person/investment_expense.py index 86d0ee64123..d25c6b92b9d 100644 --- a/policyengine_us/variables/household/expense/person/investment_expense.py +++ b/policyengine_us/variables/household/expense/person/investment_expense.py @@ -4,6 +4,20 @@ class investment_expenses(Variable): value_type = float entity = Person - label = "Investment expenses" + label = "Non-interest expenses directly connected with investment income" + documentation = """ + Expenses other than interest directly connected with producing + investment income. Section 163(d)(4)(C) defines investment expenses + as "deductions allowed under this chapter (other than for interest) + which are directly connected with the production of investment income". + Form 4952 line 5 includes only the allowed deduction for these expenses. + Expenses taken into account in computing passive-activity income or + loss under section 469 are excluded by section 163(d)(4)(D). + """ unit = USD definition_period = YEAR + reference = [ + "https://www.law.cornell.edu/uscode/text/26/163#d_4_C", + "https://www.law.cornell.edu/uscode/text/26/163#d_4_D", + "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + ] diff --git a/policyengine_us/variables/household/expense/person/non_mortgage_interest.py b/policyengine_us/variables/household/expense/person/non_mortgage_interest.py index 38375889451..dcecf3760e8 100644 --- a/policyengine_us/variables/household/expense/person/non_mortgage_interest.py +++ b/policyengine_us/variables/household/expense/person/non_mortgage_interest.py @@ -4,7 +4,12 @@ class non_mortgage_interest(Variable): value_type = float entity = Person - label = "Non-mortgage interest" + label = "Non-mortgage interest paid before the investment interest limit" + documentation = """ + Non-mortgage interest expenses paid before the 26 U.S.C. 163(d) limit. + The investment interest component is limited to net investment income + through Form 4952 when calculating interest_deduction. + """ unit = USD definition_period = YEAR From 7b8e1e13e8b3f29a9cc2dd00f20b7c83d3e895df Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 4 Oct 2026 07:00:38 -0400 Subject: [PATCH 02/11] Use limited federal investment interest in state deductions --- .../itemized/limit/excluded_deductions.yaml | 3 +- .../itemized/reduction/reducible.yaml | 3 +- ...uctions_for_federal_itemization_indiv.yaml | 12 +- ...uctions_for_federal_itemization_joint.yaml | 13 +- .../mt_investment_interest_allocation.yaml | 164 ++++++++++++++++++ .../mt_itemized_deductions_indiv.yaml | 12 +- .../mt_itemized_deductions_joint.yaml | 13 +- .../ny_itemized_deductions_phase_out.yaml | 3 +- .../ca_itemized_deductions_pre_limitation.py | 3 +- ...eductions_for_federal_itemization_indiv.py | 29 +++- ...eductions_for_federal_itemization_joint.py | 18 +- .../general/mt_itemized_deductions_indiv.py | 29 +++- .../general/mt_itemized_deductions_joint.py | 18 +- .../ny_itemized_deductions_phase_out.py | 11 +- 14 files changed, 308 insertions(+), 23 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_allocation.yaml diff --git a/policyengine_us/parameters/gov/states/ca/tax/income/deductions/itemized/limit/excluded_deductions.yaml b/policyengine_us/parameters/gov/states/ca/tax/income/deductions/itemized/limit/excluded_deductions.yaml index d3d5fd934b8..a249824edb5 100644 --- a/policyengine_us/parameters/gov/states/ca/tax/income/deductions/itemized/limit/excluded_deductions.yaml +++ b/policyengine_us/parameters/gov/states/ca/tax/income/deductions/itemized/limit/excluded_deductions.yaml @@ -3,7 +3,8 @@ values: 2021-01-01: - medical_expense_deduction - casualty_loss_deduction - - non_mortgage_interest + # Schedule CA limitation worksheet line 2 uses federal Schedule A line 9. + - investment_interest_expense_deduction metadata: unit: list diff --git a/policyengine_us/parameters/gov/states/va/tax/income/deductions/itemized/reduction/reducible.yaml b/policyengine_us/parameters/gov/states/va/tax/income/deductions/itemized/reduction/reducible.yaml index 8518c31559b..242e0fbb49d 100644 --- a/policyengine_us/parameters/gov/states/va/tax/income/deductions/itemized/reduction/reducible.yaml +++ b/policyengine_us/parameters/gov/states/va/tax/income/deductions/itemized/reduction/reducible.yaml @@ -2,7 +2,8 @@ description: Virginia reduced the applicable itemized deductions by the followin values: 2013-01-01: - medical_expense_deduction - - investment_interest_expense + # Virginia Schedule A line 9 is the investment interest deduction. + - investment_interest_expense_deduction - gambling_losses - casualty_loss_deduction metadata: diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml index 4435e64a801..4f418742bf3 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml @@ -1,3 +1,6 @@ +# 26 U.S.C. 163(d)(1): investment interest "shall not exceed the net investment +# income"; Form 4952 line 8 is the smaller of line 3 or line 6. +# The 2023 MT Form 2 instructions, line 10, use the federal Form 4952 computation. - name: Test case 1 # filing separate period: 2021 input: @@ -10,7 +13,10 @@ mt_federal_income_tax_deduction_for_federal_itemization_indiv: 2_000 state_code: MT output: - mt_itemized_deductions_for_federal_itemization_indiv: 9_188 + # Form 4952 lines 3/6/8 = 265/0/0, so investment interest contributes zero. + # 800 / 2 charity + (400 + 123) misc + 1,000 medical + 5,000 SALT + # + 2,000 federal tax = 8,923; the former 265 interest deduction is disallowed. + mt_itemized_deductions_for_federal_itemization_indiv: 8_923 - name: Test case 2 #salt has a cap of 10000 for single, head of household and joint period: 2022 @@ -24,7 +30,9 @@ mt_federal_income_tax_deduction_for_federal_itemization_indiv: 2_000 state_code: MT output: - mt_itemized_deductions_for_federal_itemization_indiv: 16_234 + # Form 4952 lines 3/6/8 = 1,234/0/0. + # 0 charity + 0 misc + 3,000 medical + 10,000 SALT + 2,000 federal tax = 15,000. + mt_itemized_deductions_for_federal_itemization_indiv: 15_000 - name: Test case 3 #sum all terms period: 2021 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.yaml index 8a1114f4ef5..d17c0823cd6 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.yaml @@ -1,3 +1,6 @@ +# 26 U.S.C. 163(d)(1): investment interest "shall not exceed the net investment +# income"; Form 4952 line 8 is the smaller of line 3 or line 6. +# The 2023 MT Form 2 instructions, line 10, use the federal Form 4952 computation. - name: Test case 1 # filing separate period: 2021 input: @@ -11,7 +14,10 @@ mt_federal_income_tax_deduction_for_federal_itemization: 2_000 state_code: MT output: - mt_itemized_deductions_for_federal_itemization_joint: 9_588 + # Form 4952 lines 3/6/8 = 265/0/0, so investment interest contributes zero. + # 800 charity + (400 + 123) misc + 1,000 medical + 5,000 SALT + # + 2,000 federal tax = 9,323; the former 265 interest deduction is disallowed. + mt_itemized_deductions_for_federal_itemization_joint: 9_323 - name: Test case 2 #salt has a cap of 10000 for single, head of household and joint period: 2022 @@ -73,4 +79,7 @@ members: [person1, person2, person3] mt_federal_income_tax_deduction_for_federal_itemization: 600 output: - mt_itemized_deductions_for_federal_itemization_joint: [2_212, 0, 0] + # Form 4952 lines 3/6/8 = 100/0/0; investment interest contributes zero. + # Head: (100 + 100 + 100) misc + (200 + 200 + 12) medical + # + (300 + 300 + 200) SALT + 600 federal tax = 2,112; others receive zero. + mt_itemized_deductions_for_federal_itemization_joint: [2_112, 0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_allocation.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_allocation.yaml new file mode 100644 index 00000000000..585fde932dd --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_allocation.yaml @@ -0,0 +1,164 @@ +# 2023 Form 2 instructions, line 10: "following the computation on federal +# Form 4952 but include any Montana adjustments to income." +# https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=31 +# 2024 Worksheet A line 1: "Enter your total federal itemized deductions from +# Form 1040, line 12." +# https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=7 +# Federal Form 4952 line 8: "Enter the smaller of line 3 or line 6." +# https://www.irs.gov/pub/irs-pdf/f4952.pdf + +- name: Montana allocates limited interest by each spouse's paid share and handles zero totals + period: 2023 + absolute_error_margin: 0.01 + input: + people: + partial_head: + is_tax_unit_head: true + is_tax_unit_head_or_spouse: true + investment_interest_expense: 300 + taxable_interest_income: 600 + mortgage_interest: 0 + mt_misc_deductions: 0 + mt_medical_expense_deduction_indiv: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + mt_federal_income_tax_deduction_indiv: 0 + mt_federal_income_tax_deduction_for_federal_itemization_indiv: 0 + partial_spouse: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_head_or_spouse: true + investment_interest_expense: 900 + mortgage_interest: 0 + mt_misc_deductions: 0 + mt_medical_expense_deduction_indiv: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + mt_federal_income_tax_deduction_indiv: 0 + mt_federal_income_tax_deduction_for_federal_itemization_indiv: 0 + zero_head: + is_tax_unit_head: true + is_tax_unit_head_or_spouse: true + investment_interest_expense: 0 + mortgage_interest: 0 + mt_misc_deductions: 0 + mt_medical_expense_deduction_indiv: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + mt_federal_income_tax_deduction_indiv: 0 + mt_federal_income_tax_deduction_for_federal_itemization_indiv: 0 + zero_spouse: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_head_or_spouse: true + investment_interest_expense: 0 + mortgage_interest: 0 + mt_misc_deductions: 0 + mt_medical_expense_deduction_indiv: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + mt_federal_income_tax_deduction_indiv: 0 + mt_federal_income_tax_deduction_for_federal_itemization_indiv: 0 + spouse_only_head: + is_tax_unit_head: true + is_tax_unit_head_or_spouse: true + investment_interest_expense: 0 + mortgage_interest: 0 + mt_misc_deductions: 0 + mt_medical_expense_deduction_indiv: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + mt_federal_income_tax_deduction_indiv: 0 + mt_federal_income_tax_deduction_for_federal_itemization_indiv: 0 + spouse_only_spouse: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_head_or_spouse: true + investment_interest_expense: 1_200 + taxable_interest_income: 500 + mortgage_interest: 0 + mt_misc_deductions: 0 + mt_medical_expense_deduction_indiv: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + mt_federal_income_tax_deduction_indiv: 0 + mt_federal_income_tax_deduction_for_federal_itemization_indiv: 0 + tax_units: + partial: + members: [partial_head, partial_spouse] + filing_status: JOINT + charitable_deduction: 0 + mt_federal_income_tax_deduction_unit: 0 + mt_federal_income_tax_deduction_for_federal_itemization: 0 + zero: + members: [zero_head, zero_spouse] + filing_status: JOINT + charitable_deduction: 0 + mt_federal_income_tax_deduction_unit: 0 + mt_federal_income_tax_deduction_for_federal_itemization: 0 + spouse_only: + members: [spouse_only_head, spouse_only_spouse] + filing_status: JOINT + charitable_deduction: 0 + mt_federal_income_tax_deduction_unit: 0 + mt_federal_income_tax_deduction_for_federal_itemization: 0 + households: + household: + members: [partial_head, partial_spouse, zero_head, zero_spouse, spouse_only_head, spouse_only_spouse] + state_code: MT + output: + # Line 3 = [300 + 900, 0 + 0, 0 + 1,200]. + form_4952_total_investment_interest_expense: [1_200, 0, 1_200] + # Line 6 = taxable interest [600, 0, 500] less zero line 5 investment expenses. + form_4952_net_investment_income: [600, 0, 500] + # Line 8 = min(line 3, line 6) = [600, 0, 500]. + investment_interest_expense_deduction: [600, 0, 500] + # Partial: 600 * [300/1,200, 900/1,200] = [150, 450]. + # Zero: zero paid gives zero shares. Spouse-only: 500 * [0/1,200, 1,200/1,200]. + mt_itemized_deductions_indiv: [150, 450, 0, 0, 0, 500] + # Each tax unit's full line 8 is assigned to its head, including spouse interest. + mt_itemized_deductions_joint: [600, 0, 0, 0, 500, 0] + # The federal-itemization allocation uses the same paid shares and limited amounts. + mt_itemized_deductions_for_federal_itemization_indiv: [150, 450, 0, 0, 0, 500] + # Federal-itemization joint totals assign line 8 to the head as well. + mt_itemized_deductions_for_federal_itemization_joint: [600, 0, 0, 0, 500, 0] + +- name: Montana joint federal itemization includes both spouses' fully deductible interest in 2024 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + head: + is_tax_unit_head: true + investment_interest_expense: 300 + taxable_interest_income: 2_000 + mortgage_interest: 0 + mt_misc_deductions: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + spouse: + is_tax_unit_head: false + is_tax_unit_spouse: true + investment_interest_expense: 900 + mortgage_interest: 0 + mt_misc_deductions: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + charitable_deduction: 0 + mt_federal_income_tax_deduction_unit: 0 + mt_federal_income_tax_deduction_for_federal_itemization: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + # Form 4952 line 8 = min(line 3 300 + 900, line 6 2,000) = 1,200. + investment_interest_expense_deduction: 1_200 + # The head receives the tax-unit line 8; other deductions are zero. + mt_itemized_deductions_joint: [1_200, 0] + # Federal-itemization joint totals use the same federal line 8. + mt_itemized_deductions_for_federal_itemization_joint: [1_200, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_indiv.yaml index 2bc390f8ad3..cf7dc483fcf 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_indiv.yaml @@ -1,3 +1,6 @@ +# 26 U.S.C. 163(d)(1): investment interest "shall not exceed the net investment +# income"; Form 4952 line 8 is the smaller of line 3 or line 6. +# The 2023 MT Form 2 instructions, line 10, use the federal Form 4952 computation. - name: Test case 1 # filing separate period: 2021 input: @@ -10,7 +13,10 @@ mt_federal_income_tax_deduction_indiv: 2_000 state_code: MT output: - mt_itemized_deductions_indiv: 9_188 + # Form 4952 lines 3/6/8 = 265/0/0, so investment interest contributes zero. + # 800 / 2 charity + (400 + 123) misc + 1,000 medical + 5,000 SALT + # + 2,000 federal tax = 8,923; the former 265 interest deduction is disallowed. + mt_itemized_deductions_indiv: 8_923 - name: Test case 2 #salt has a cap of 10000 for single, head of household and joint period: 2022 @@ -24,7 +30,9 @@ mt_federal_income_tax_deduction_indiv: 2_000 state_code: MT output: - mt_itemized_deductions_indiv: 16_234 + # Form 4952 lines 3/6/8 = 1,234/0/0. + # 0 charity + 0 misc + 3,000 medical + 10,000 SALT + 2,000 federal tax = 15,000. + mt_itemized_deductions_indiv: 15_000 - name: Test case 3 #sum all terms period: 2021 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml index 6b25795e07a..d4d96b85182 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_itemized_deductions_joint.yaml @@ -1,3 +1,6 @@ +# 26 U.S.C. 163(d)(1): investment interest "shall not exceed the net investment +# income"; Form 4952 line 8 is the smaller of line 3 or line 6. +# The 2023 MT Form 2 instructions, line 10, use the federal Form 4952 computation. - name: Test case 1 # filing separate period: 2021 input: @@ -11,7 +14,10 @@ mt_federal_income_tax_deduction_unit: 2_000 state_code: MT output: - mt_itemized_deductions_joint: 9_588 + # Form 4952 lines 3/6/8 = 265/0/0, so investment interest contributes zero. + # 800 charity + (400 + 123) misc + 1,000 medical + 5,000 SALT + # + 2,000 federal tax = 9,323; the former 265 interest deduction is disallowed. + mt_itemized_deductions_joint: 9_323 - name: Test case 2 #salt has a cap of 10000 for single, head of household and joint period: 2022 @@ -73,4 +79,7 @@ members: [person1, person2, person3] mt_federal_income_tax_deduction_unit: 600 output: - mt_itemized_deductions_joint: [2_212, 0, 0] + # Form 4952 lines 3/6/8 = 100/0/0; investment interest contributes zero. + # Head: (100 + 100 + 100) misc + (200 + 200 + 12) medical + # + (300 + 300 + 200) SALT + 600 federal tax = 2,112; others receive zero. + mt_itemized_deductions_joint: [2_112, 0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemized_deductions_phase_out.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemized_deductions_phase_out.yaml index 07de472c2ad..60c3795d0f3 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemized_deductions_phase_out.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_itemized_deductions_phase_out.yaml @@ -103,7 +103,8 @@ ny_itemized_deductions_max: 100_000 ny_college_tuition_deduction: 0 medical_expense_deduction: 25_000 - investment_interest_expense: 10_000 + # IT-196 line 14 takes federal Schedule A line 9 (Form 4952 line 8). + investment_interest_expense_deduction: 10_000 ny_casualty_loss_deduction: 5_000 adjusted_gross_income: 5_000_000 filing_status: JOINT diff --git a/policyengine_us/variables/gov/states/ca/tax/income/deductions/itemized/ca_itemized_deductions_pre_limitation.py b/policyengine_us/variables/gov/states/ca/tax/income/deductions/itemized/ca_itemized_deductions_pre_limitation.py index ed87c390fac..6992853b35b 100644 --- a/policyengine_us/variables/gov/states/ca/tax/income/deductions/itemized/ca_itemized_deductions_pre_limitation.py +++ b/policyengine_us/variables/gov/states/ca/tax/income/deductions/itemized/ca_itemized_deductions_pre_limitation.py @@ -18,4 +18,5 @@ class ca_itemized_deductions_pre_limitation(Variable): "ca_investment_interest_expense_deduction", "real_estate_taxes", ] - subtracts = ["investment_interest_expense"] + # Replace federal Schedule A line 9 with the California FTB 3526 deduction. + subtracts = ["investment_interest_expense_deduction"] diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py index 5d1cb0c5153..50f12fb2ba7 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py @@ -11,8 +11,22 @@ class mt_itemized_deductions_for_federal_itemization_indiv(Variable): "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-21/section-15-30-2131/", # MT Code § 15-30-2131 (2022) (1) + "https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=31", + "https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=7", ) defined_for = "mt_married_filing_separately_on_same_return_eligible" + documentation = """ + The 2023 Form 2 instructions, line 10, say to determine investment + interest by "following the computation on federal Form 4952 but include + any Montana adjustments to income." Worksheet A line 1 of the 2024 + instructions says, "Enter your total federal itemized deductions from + Form 1040, line 12." + + Investment interest uses federal Form 4952 line 8. Before 2024 this is an + approximation because Montana investment-income adjustments are not + separately modeled. The tax-unit deduction is allocated by each person's + share of investment interest paid for separate filing on the same return. + """ def formula(person, period, parameters): p = parameters(period).gov.irs.deductions @@ -20,8 +34,19 @@ def formula(person, period, parameters): # we will split the value between each spouse charitable_deduction = person.tax_unit("charitable_deduction", period) * 0.5 head_or_spouse = person("is_tax_unit_head_or_spouse", period) - # The interest deduction is the sum of mortagage and investment interest expenses - investment_interest = person("investment_interest_expense", period) + # Allocate federal Form 4952 line 8 by each person's share of interest paid. + interest_paid = person("investment_interest_expense", period) + total_interest_paid = person.tax_unit.sum(interest_paid) + interest_share = np.divide( + interest_paid, + total_interest_paid, + out=np.zeros_like(interest_paid), + where=total_interest_paid > 0, + ) + investment_interest = ( + person.tax_unit("investment_interest_expense_deduction", period) + * interest_share + ) mortgage_interest = person("mortgage_interest", period) interest_ded = investment_interest + mortgage_interest other_deductions = add( diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.py index 3469bb0b4b3..aa10081d4ff 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.py @@ -11,12 +11,28 @@ class mt_itemized_deductions_for_federal_itemization_joint(Variable): "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-21/section-15-30-2131/", # MT Code § 15-30-2131 (2022) (1) + "https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=31", + "https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=7", ) defined_for = StateCode.MT + documentation = """ + The 2023 Form 2 instructions, line 10, say to determine investment + interest by "following the computation on federal Form 4952 but include + any Montana adjustments to income." Worksheet A line 1 of the 2024 + instructions says, "Enter your total federal itemized deductions from + Form 1040, line 12." + + Investment interest uses federal Form 4952 line 8. Before 2024 this is an + approximation because Montana investment-income adjustments are not + separately modeled. For joint filing the whole tax-unit investment + interest deduction, including the spouse's amount, is assigned to the head. + """ def formula(person, period, parameters): charitable_deduction = person.tax_unit("charitable_deduction", period) - investment_interest = person("investment_interest_expense", period) + investment_interest = person.tax_unit( + "investment_interest_expense_deduction", period + ) mortgage_interest = person("mortgage_interest", period) interest_ded = investment_interest + mortgage_interest other_deductions = add( diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py index 7b31319e265..bc4859d728d 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py @@ -11,8 +11,22 @@ class mt_itemized_deductions_indiv(Variable): "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-21/section-15-30-2131/", # MT Code § 15-30-2131 (2022) (1) + "https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=31", + "https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=7", ) defined_for = "mt_married_filing_separately_on_same_return_eligible" + documentation = """ + The 2023 Form 2 instructions, line 10, say to determine investment + interest by "following the computation on federal Form 4952 but include + any Montana adjustments to income." Worksheet A line 1 of the 2024 + instructions says, "Enter your total federal itemized deductions from + Form 1040, line 12." + + Investment interest uses federal Form 4952 line 8. Before 2024 this is an + approximation because Montana investment-income adjustments are not + separately modeled. The tax-unit deduction is allocated by each person's + share of investment interest paid for separate filing on the same return. + """ def formula(person, period, parameters): p = parameters(period).gov.states.mt.tax.income.deductions.itemized @@ -22,8 +36,19 @@ def formula(person, period, parameters): person.tax_unit("charitable_deduction", period) * p.spouse_allocation_rate ) head_or_spouse = person("is_tax_unit_head_or_spouse", period) - # The interest deduction is the sum of mortagage and investment interest expenses - investment_interest = person("investment_interest_expense", period) + # Allocate federal Form 4952 line 8 by each person's share of interest paid. + interest_paid = person("investment_interest_expense", period) + total_interest_paid = person.tax_unit.sum(interest_paid) + interest_share = np.divide( + interest_paid, + total_interest_paid, + out=np.zeros_like(interest_paid), + where=total_interest_paid > 0, + ) + investment_interest = ( + person.tax_unit("investment_interest_expense_deduction", period) + * interest_share + ) mortgage_interest = person("mortgage_interest", period) interest_ded = investment_interest + mortgage_interest other_deductions = add( diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py index a3328b0290d..180e341360b 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py @@ -11,12 +11,28 @@ class mt_itemized_deductions_joint(Variable): "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-21/section-15-30-2131/", # MT Code § 15-30-2131 (2022) (1) + "https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=31", + "https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=7", ) defined_for = StateCode.MT + documentation = """ + The 2023 Form 2 instructions, line 10, say to determine investment + interest by "following the computation on federal Form 4952 but include + any Montana adjustments to income." Worksheet A line 1 of the 2024 + instructions says, "Enter your total federal itemized deductions from + Form 1040, line 12." + + Investment interest uses federal Form 4952 line 8. Before 2024 this is an + approximation because Montana investment-income adjustments are not + separately modeled. For joint filing the whole tax-unit investment + interest deduction, including the spouse's amount, is assigned to the head. + """ def formula(person, period, parameters): charitable_deduction = person.tax_unit("charitable_deduction", period) - investment_interest = person("investment_interest_expense", period) + investment_interest = person.tax_unit( + "investment_interest_expense_deduction", period + ) mortgage_interest = person("mortgage_interest", period) interest_ded = investment_interest + mortgage_interest other_deductions = add( diff --git a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out.py b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out.py index 27565cc41af..528b9f7ec0f 100644 --- a/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out.py +++ b/policyengine_us/variables/gov/states/ny/tax/income/taxable_income/deductions/itemized/phase_out/ny_itemized_deductions_phase_out.py @@ -22,7 +22,7 @@ class ny_itemized_deductions_phase_out(Variable): (b) 80% of the deductions that are subject to the limitation (lines 1-4). Mapping of the worksheet's line-total inputs to PolicyEngine variables - (federal Form IT-196 line numbers in brackets): + (New York Form IT-196 line numbers in brackets): Worksheet line 1 (L1) = form lines 4, 9, 15, 19, 20, 28, 39, i.e. the total itemized deductions before the limitation. In PolicyEngine this is @@ -36,7 +36,7 @@ class ny_itemized_deductions_phase_out(Variable): Worksheet line 2 (L2) = form lines 4, 14, 16a, 20, 29, 30, 37, i.e. the deductions NOT subject to the limitation (26 U.S.C. 68(c)): - line 4 medical/dental -> medical_expense_deduction - - line 14 investment interest -> investment_interest_expense + - line 14 investment interest -> investment_interest_expense_deduction - line 20 casualty/theft loss -> ny_casualty_loss_deduction - lines 16a/29/30/37 (qualified contributions, gambling losses, income-producing-property casualty, federal disaster loss) are not @@ -45,8 +45,9 @@ class ny_itemized_deductions_phase_out(Variable): L3 = L1 - L2 (worksheet line 3) is therefore the mortgage interest, real estate taxes, charitable gifts, and 2%-floor miscellaneous deductions - exactly the 68(c) base subject to the limitation. Because investment - interest appears in both L1 (inside interest_deduction) and L2, it cancels - out of L3, as intended. + interest after the federal Form 4952 limit appears in both L1 (inside + interest_deduction) and L2, it cancels out of L3, as intended. IT-196 line + 14 says to "enter the amount from federal Schedule A, line 9." """ def formula(tax_unit, period, parameters): @@ -63,7 +64,7 @@ def formula(tax_unit, period, parameters): # Worksheet line 2 (L2): deductions not subject to the limitation # (26 U.S.C. 68(c)) - medical, investment interest, casualty/theft. medical = tax_unit("medical_expense_deduction", period) - investment_interest = add(tax_unit, period, ["investment_interest_expense"]) + investment_interest = tax_unit("investment_interest_expense_deduction", period) casualty = tax_unit("ny_casualty_loss_deduction", period) l2 = medical + investment_interest + casualty From 79b96595f5f1b4ab766002fac10ea9e8ae72807b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 4 Oct 2026 07:13:23 -0400 Subject: [PATCH 03/11] Test state investment expenses and clarify Montana approximation --- .../itemized/ar_misc_deduction_joint.yaml | 15 +++++++++++++++ .../itemized/ny_pre_tcja_deductions.yaml | 15 +++++++++++++++ ...ed_deductions_for_federal_itemization_indiv.py | 7 ++++--- .../general/mt_itemized_deductions_indiv.py | 7 ++++--- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/deductions/itemized/ar_misc_deduction_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/deductions/itemized/ar_misc_deduction_joint.yaml index f862cd1d699..65ea639360d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/deductions/itemized/ar_misc_deduction_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/deductions/itemized/ar_misc_deduction_joint.yaml @@ -63,3 +63,18 @@ state_code: AR output: ar_misc_deduction_joint: 5_500 + +- name: 2025 Arkansas miscellaneous pool includes investment expenses + # AR3 instructions line 21: "Brokerage fees (Investment fees) & expenses". + # https://www.dfa.arkansas.gov/wp-content/uploads/2025_AR1000F_and_AR1000NR_Instructions.pdf#page=21 + period: 2025 + input: + state_code: AR + ar_agi_joint: 100_000 + investment_expenses: 3_000 + tax_preparation_fees: 500 + output: + total_misc_deductions: 3_500 # AR3 lines 21-22 = investment expenses 3,000 + tax preparation 500 + ar_misc_deduction_joint: 1_500 # AR3 line 25 = 3,500 - 2% * 100,000 + misc_deduction: 0 # Federal section 67(h) disallows miscellaneous deductions after 2017 + form_4952_investment_expenses: 0 # Form 4952 line 5 = 0 when federal misc.applies is false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_pre_tcja_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_pre_tcja_deductions.yaml index 502aab68f83..008e46e3503 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_pre_tcja_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_pre_tcja_deductions.yaml @@ -84,3 +84,18 @@ # NY Casualty: 15,000 - (100,000 * 0.10) = 5,000 # Total: 5,000 + 2,500 + 12,000 + 2,000 + 5,000 = 26,500 ny_itemized_deductions_max: 26_500 + +- name: 2025 New York miscellaneous pool includes investment expenses + # IT-196 line 24: "Other expenses - investment, safe deposit box, etc." + # https://www.tax.ny.gov/pdf/2025/inc/it196_2025_fill_in.pdf#page=2 + period: 2025 + input: + state_code: NY + employment_income: 100_000 + investment_expenses: 3_000 + tax_preparation_fees: 500 + output: + total_misc_deductions: 3_500 # IT-196 line 25 = line 24 investment 3,000 + line 23 tax preparation 500 + ny_misc_deduction: 1_500 # IT-196 line 28 = max(0, 3,500 - 2% * 100,000) + misc_deduction: 0 # Federal section 67(h) disallows miscellaneous deductions after 2017 + form_4952_investment_expenses: 0 # Form 4952 line 5 = 0 when federal misc.applies is false diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py index 50f12fb2ba7..ffd31bd286c 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py @@ -23,9 +23,10 @@ class mt_itemized_deductions_for_federal_itemization_indiv(Variable): Form 1040, line 12." Investment interest uses federal Form 4952 line 8. Before 2024 this is an - approximation because Montana investment-income adjustments are not - separately modeled. The tax-unit deduction is allocated by each person's - share of investment interest paid for separate filing on the same return. + approximation: the 2023 instructions call for federal Form 4952 + "separately", and Montana investment-income adjustments are not modeled. + The model allocates the tax-unit deduction by each person's share of + investment interest paid for separate filing on the same return. """ def formula(person, period, parameters): diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py index bc4859d728d..9bf61e850fe 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py @@ -23,9 +23,10 @@ class mt_itemized_deductions_indiv(Variable): Form 1040, line 12." Investment interest uses federal Form 4952 line 8. Before 2024 this is an - approximation because Montana investment-income adjustments are not - separately modeled. The tax-unit deduction is allocated by each person's - share of investment interest paid for separate filing on the same return. + approximation: the 2023 instructions call for federal Form 4952 + "separately", and Montana investment-income adjustments are not modeled. + The model allocates the tax-unit deduction by each person's share of + investment interest paid for separate filing on the same return. """ def formula(person, period, parameters): From 40a7fcbca6552bc92c9ad420a1a57ffbb10190af Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 16:21:52 -0400 Subject: [PATCH 04/11] wip(hub snapshot): 163d-build-snapshot after the 2026-10-06 sleep/network loss --- ...st_investment_interest_limit_properties.py | 2 +- .../investment_interest/form_4952.yaml | 2 +- .../mt_investment_interest_allocation.yaml | 2 +- .../sources/investment_income_form_4952.py | 12 +++++-- .../itemizing/interest_deduction.py | 18 +++++++---- .../form_4952_capital_gain_distributions.py | 28 ++++++++++++++++ ..._investment_interest_expense_prior_year.py | 21 ++++++++++++ .../form_4952_elected_investment_income.py | 25 +++++++++------ .../form_4952_gross_investment_income.py | 9 ++---- .../form_4952_investment_expenses.py | 20 ++++++------ .../form_4952_investment_income.py | 2 +- ...m_4952_investment_interest_carryforward.py | 11 ++++--- .../form_4952_net_capital_gain.py | 26 +++++++++------ .../form_4952_net_investment_gain.py | 19 ++++++----- .../form_4952_net_investment_income.py | 2 +- .../form_4952_qualified_dividends.py | 7 ++-- ..._4952_total_investment_interest_expense.py | 24 +++++++++----- .../investment_interest_expense_deduction.py | 8 ++--- .../adjusted_net_capital_gain.py | 12 +++---- .../capital_gains/net_capital_gain.py | 19 +++++------ ...chedule_d_tax_worksheet_form_4952_lines.py | 32 ++++++++----------- ...1_net_capital_gain_other_than_dividends.py | 6 +++- .../section_911_qualified_dividend_income.py | 12 +++---- .../expense/person/investment_expense.py | 2 +- 24 files changed, 198 insertions(+), 123 deletions(-) create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_capital_gain_distributions.py create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_disallowed_investment_interest_expense_prior_year.py diff --git a/policyengine_us/tests/core/test_investment_interest_limit_properties.py b/policyengine_us/tests/core/test_investment_interest_limit_properties.py index 17b054bc140..0b947fd307b 100644 --- a/policyengine_us/tests/core/test_investment_interest_limit_properties.py +++ b/policyengine_us/tests/core/test_investment_interest_limit_properties.py @@ -5,7 +5,7 @@ smaller of line 3 or line 6." Line 7 carries forward the excess of line 3 over line 6. Its line 4g instructions say "don't enter more than the sum of lines 4b and 4e." Sources: https://www.law.cornell.edu/uscode/text/26/163#d and -https://www.irs.gov/pub/irs-pdf/f4952.pdf. +https://www.irs.gov/pub/irs-prior/f4952--2025.pdf. For 2017, line 5 includes the smaller of investment expenses on Schedule A line 23 and Schedule A line 27 (Form 4952 (2017), line 5 instructions, diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml index 0ffff5a18e2..508ab4c45e0 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml @@ -1,6 +1,6 @@ # Sources: 26 U.S.C. 163(d)(1)-(4), https://www.law.cornell.edu/uscode/text/26/163#d # 2025 Form 4952: "Enter the smaller of line 3 or line 6" (line 8). -# https://www.irs.gov/pub/irs-pdf/f4952.pdf +# https://www.irs.gov/pub/irs-prior/f4952--2025.pdf # 2017 Form 4952 line 5: "Include on line 5 the smaller of" investment # expenses on Schedule A line 23 and the total on Schedule A line 27. # https://www.irs.gov/pub/irs-prior/f4952--2017.pdf diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_allocation.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_allocation.yaml index 585fde932dd..069690d7bcc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_allocation.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_allocation.yaml @@ -5,7 +5,7 @@ # Form 1040, line 12." # https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=7 # Federal Form 4952 line 8: "Enter the smaller of line 3 or line 6." -# https://www.irs.gov/pub/irs-pdf/f4952.pdf +# https://www.irs.gov/pub/irs-prior/f4952--2025.pdf - name: Montana allocates limited interest by each spouse's paid share and handles zero totals period: 2023 diff --git a/policyengine_us/variables/gov/irs/income/sources/investment_income_form_4952.py b/policyengine_us/variables/gov/irs/income/sources/investment_income_form_4952.py index 8551304d487..943e9085889 100644 --- a/policyengine_us/variables/gov/irs/income/sources/investment_income_form_4952.py +++ b/policyengine_us/variables/gov/irs/income/sources/investment_income_form_4952.py @@ -7,9 +7,12 @@ class investment_income_form_4952(Variable): label = "Federal Form 4952 line 8 amount for California FTB 3526" documentation = ( "The federal Form 4952 amount that California form FTB 3526 line 9 " - "asks for (federal Form 4952 line 8). The federal Schedule D Tax " - "Worksheet does not read this input; it takes the Form 4952 line 4g " - "election from investment_income_elected_form_4952." + "asks for (federal Form 4952 line 8). Defaults to the model's federal " + "investment_interest_expense_deduction; an input overrides it, for " + "example a federal deduction that includes amounts the model does " + "not compute. The federal Schedule D Tax Worksheet does not read " + "this variable; it takes the Form 4952 line 4g election from " + "form_4952_elected_investment_income." ) unit = USD definition_period = YEAR @@ -17,3 +20,6 @@ class investment_income_form_4952(Variable): title="2021 California form FTB 3526, line 9", href="https://www.ftb.ca.gov/forms/2021/2021-3526.pdf", ) + + def formula(tax_unit, period, parameters): + return tax_unit("investment_interest_expense_deduction", period) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/interest_deduction.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/interest_deduction.py index 9b680c33f25..0aa2f3ef7f5 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/interest_deduction.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/interest_deduction.py @@ -9,22 +9,26 @@ class interest_deduction(Variable): documentation = """ Schedule A line 10: "Add lines 8e and 9." Line 8e is mortgage interest and line 9 is "Investment interest. Attach Form 4952 if required." - Replace investment interest paid in deductible_interest_expense with - the Form 4952 line 8 deduction limited under 163(d). Retain the existing - all-member aggregation and honor direct deductible_interest_expense - inputs by adjusting only the separately reported investment interest. + Start from the existing aggregate of deductible_interest_expense over + every tax unit member, remove all current-year investment interest paid, + and add the Form 4952 line 8 deduction limited under 163(d). The aggregate + contains only current-year interest, so the prior-year carryover on Form + 4952 line 2 enters only through line 8. A dependent's investment interest + leaves the aggregate and stays off the filer's Form 4952. Direct + deductible_interest_expense inputs and mortgage interest keep the + existing aggregation. """ definition_period = YEAR reference = [ "https://www.law.cornell.edu/uscode/text/26/163#d_1", "https://www.irs.gov/pub/irs-prior/f1040sa--2025.pdf", - "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + "https://www.irs.gov/pub/irs-prior/f4952--2025.pdf", ] def formula(tax_unit, period, parameters): interest_paid = add(tax_unit, period, ["deductible_interest_expense"]) - investment_interest_paid = tax_unit( - "form_4952_total_investment_interest_expense", period + investment_interest_paid = add( + tax_unit, period, ["investment_interest_expense"] ) investment_interest_allowed = tax_unit( "investment_interest_expense_deduction", period diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_capital_gain_distributions.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_capital_gain_distributions.py new file mode 100644 index 00000000000..f22084f1bbe --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_capital_gain_distributions.py @@ -0,0 +1,28 @@ +from policyengine_us.model_api import * + + +class form_4952_capital_gain_distributions(Variable): + value_type = float + entity = TaxUnit + label = "Form 4952 capital gain distributions reported without Schedule D" + unit = USD + definition_period = YEAR + documentation = """ + The head's and spouse's capital gain distributions reported without + Schedule D, each floored at zero as in irs_gross_income. The Form 4952 + line 4d instructions include capital gain distributions in the net gain, + and the line 4e instructions say "Capital gain distributions from mutual + funds are treated as long-term capital gains." Distributions reported on + Schedule D are already part of long_term_capital_gains. A tax unit + dependent's distributions belong on the dependent's own return. + """ + reference = [ + "https://www.irs.gov/pub/irs-prior/f4952--2025.pdf#page=3", + "https://www.irs.gov/pub/irs-prior/i1040sd--2025.pdf#page=2", + ] + + def formula(tax_unit, period, parameters): + person = tax_unit.members + filer = ~person("is_tax_unit_dependent", period) + distributions = max_(0, person("non_sch_d_capital_gains", period)) + return tax_unit.sum(filer * distributions) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_disallowed_investment_interest_expense_prior_year.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_disallowed_investment_interest_expense_prior_year.py new file mode 100644 index 00000000000..3ebbc2a6838 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_disallowed_investment_interest_expense_prior_year.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class form_4952_disallowed_investment_interest_expense_prior_year(Variable): + value_type = float + entity = TaxUnit + label = "Form 4952 disallowed investment interest expense from the prior year" + unit = USD + definition_period = YEAR + documentation = """ + Form 4952 line 2: "Disallowed investment interest expense from 2024 Form + 4952, line 7." Section 163(d)(2) treats interest disallowed under the + limit as "investment interest paid or accrued by the taxpayer in the + succeeding taxable year". The model does not carry one year's line 7 into + the next year's simulation, so the prior-year amount is an input. It + defaults to zero. + """ + reference = [ + "https://www.law.cornell.edu/uscode/text/26/163#d_2", + "https://www.irs.gov/pub/irs-prior/f4952--2025.pdf", + ] diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_elected_investment_income.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_elected_investment_income.py index 874cc1b2791..38f68a41cd1 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_elected_investment_income.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_elected_investment_income.py @@ -10,22 +10,29 @@ class form_4952_elected_investment_income(Variable): documentation = """ Form 4952 line 4g: "Enter the amount from lines 4b and 4e that you elect to include in investment income." The instructions say "don't enter - more than the sum of lines 4b and 4e". Sum the existing Person election - inputs across the tax unit, floor at zero, and cap at lines 4b plus 4e. - The line 4g note says the election is "attributable first to net capital - gain" and "then to qualified dividends". No input exists for the - alternative "Elec." allocation next to line 4e, so the default gain-first - allocation applies. This deduction calculation does not change the - separate capital gains tax worksheets. + more than the sum of lines 4b and 4e". Sum the head's and spouse's + investment_income_elected_form_4952, floor at zero, and cap at lines 4b + plus 4e. A tax unit dependent's election belongs on the dependent's own + Form 4952. + + The instructions also have filers enter this amount on "The Schedule D + Tax Worksheet, line 3", so the capital gains tax uses this effective + election. The line 4g note says the election is "attributable first to + net capital gain" and "then to qualified dividends". No input exists for + the alternative "Elec." allocation next to line 4e, so the default + gain-first allocation applies. """ reference = [ "https://www.law.cornell.edu/uscode/text/26/163#d_4_B_iii", - "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + "https://www.irs.gov/pub/irs-prior/f4952--2025.pdf#page=3", ] def formula(tax_unit, period, parameters): election = max_( - 0, add(tax_unit, period, ["investment_income_elected_form_4952"]) + 0, + tax_unit_non_dep_add( + tax_unit, period, ["investment_income_elected_form_4952"] + ), ) qualified_dividends = tax_unit("form_4952_qualified_dividends", period) net_capital_gain = tax_unit("form_4952_net_capital_gain", period) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_gross_investment_income.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_gross_investment_income.py index 53d2fb66f1d..e9a914ce99e 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_gross_investment_income.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_gross_investment_income.py @@ -32,13 +32,10 @@ class form_4952_gross_investment_income(Variable): "https://www.law.cornell.edu/uscode/text/26/163#d_4_B_i", "https://www.law.cornell.edu/uscode/text/26/163#d_5", "https://www.law.cornell.edu/cfr/text/26/1.469-2T", - "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + "https://www.irs.gov/pub/irs-prior/f4952--2025.pdf#page=3", ] def formula(tax_unit, period, parameters): - person = tax_unit.members - not_dependent = ~person("is_tax_unit_dependent", period) - income = add( - person, period, ["taxable_interest_income", "ordinary_dividend_income"] + return tax_unit_non_dep_add( + tax_unit, period, ["taxable_interest_income", "ordinary_dividend_income"] ) - return tax_unit.sum(income * not_dependent) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_expenses.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_expenses.py index dcb65c0f62e..7fb146d02be 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_expenses.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_expenses.py @@ -10,13 +10,15 @@ class form_4952_investment_expenses(Variable): documentation = """ Form 4952 line 5: "Investment expenses (see instructions)". Section 163(d)(4)(C) includes "deductions allowed" other than interest that are - "directly connected with the production of investment income". - The 2017 instructions require "the smaller of: (a) the investment - expenses included on Schedule A (Form 1040), line 23; or (b) the total - on Schedule A (Form 1040), line 27." The model's misc_deduction applies - the 2% AGI floor to the miscellaneous expense pool, which includes - investment_expenses from Schedule A line 23. Aggregate these expenses - with the same all-member membership as that pool. + "directly connected with the production of investment income", and the + instructions include expenses "only if you are allowed a deduction on + your return for the expense." The 2017 instructions require "the smaller + of: (a) the investment expenses included on Schedule A (Form 1040), line + 23; or (b) the total on Schedule A (Form 1040), line 27." The model's + misc_deduction applies the 2% AGI floor to the miscellaneous expense pool, + which includes investment_expenses from Schedule A line 23. Only the + head's and spouse's investment expenses count: a tax unit dependent's + expenses belong on the dependent's own return. When misc.applies is false, these expenses are not allowed deductions and line 5 is zero. Section 67(h), as amended by P.L. 119-21 section 70110, permanently disallows miscellaneous itemized deductions for @@ -27,12 +29,12 @@ class form_4952_investment_expenses(Variable): "https://www.law.cornell.edu/uscode/text/26/163#d_4_C", "https://www.law.cornell.edu/uscode/text/26/67#h", "https://www.irs.gov/pub/irs-prior/f4952--2017.pdf", - "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + "https://www.irs.gov/pub/irs-prior/f4952--2025.pdf#page=4", ] def formula(tax_unit, period, parameters): if not parameters(period).gov.irs.deductions.itemized.misc.applies: return 0 - expenses = add(tax_unit, period, ["investment_expenses"]) + expenses = tax_unit_non_dep_add(tax_unit, period, ["investment_expenses"]) allowed_misc_deduction = tax_unit("misc_deduction", period) return min_(expenses, allowed_misc_deduction) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_income.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_income.py index 9edbd14b258..767ca8e8a0b 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_income.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_income.py @@ -16,7 +16,7 @@ class form_4952_investment_income(Variable): """ reference = [ "https://www.law.cornell.edu/uscode/text/26/163#d_4_B", - "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + "https://www.irs.gov/pub/irs-prior/f4952--2025.pdf", ] def formula(tax_unit, period, parameters): diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_interest_carryforward.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_interest_carryforward.py index d25de4c7883..b56a97a3956 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_interest_carryforward.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_interest_carryforward.py @@ -11,14 +11,15 @@ class form_4952_investment_interest_carryforward(Variable): Form 4952 line 7: "Disallowed investment interest expense to be carried forward to 2026. Subtract line 6 from line 3. If zero or less, enter -0-." Section 163(d)(2) treats disallowed interest as "investment interest - paid or accrued by the taxpayer in the succeeding taxable year". - This variable reports the current-year amount available to carry - forward; prior-year carryforwards (line 2) have no input and do not - automatically enter a later year's calculation. + paid or accrued by the taxpayer in the succeeding taxable year". Line 3 + includes this year's prior-year carryover input (line 2), so lines 7 and + 8 together equal line 3. The amount does not enter the next year's + simulation automatically; supply it there as + form_4952_disallowed_investment_interest_expense_prior_year. """ reference = [ "https://www.law.cornell.edu/uscode/text/26/163#d_2", - "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + "https://www.irs.gov/pub/irs-prior/f4952--2025.pdf", ] def formula(tax_unit, period, parameters): diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_capital_gain.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_capital_gain.py index a70b47026fa..c831d312a9a 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_capital_gain.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_capital_gain.py @@ -14,22 +14,28 @@ class form_4952_net_capital_gain(Variable): capital gain over your net short-term capital loss" and state "Capital gain distributions from mutual funds are treated as long-term capital gains." As on line 4d, every capital asset is treated as property held - for investment and all tax-unit members' gains are summed, matching the - Schedule D Tax Worksheet Form 4952 helper. Schedule D distributions - already belong to long_term_capital_gains; add non_sch_d_capital_gains - once. Capital loss carryovers have no model input and are not modeled. + for investment and only the head's and spouse's gains count. Schedule D + distributions already belong to long_term_capital_gains, so + distributions reported without Schedule D are added once. Capital loss + carryovers have no model input and are not modeled. + + This is also Schedule D Tax Worksheet line 4, so the capital gains tax + takes a Form 4952 line 4g election from the same amount. It does not read + the worksheet, which reads it. """ reference = [ "https://www.law.cornell.edu/uscode/text/26/163#d_4_B_ii_II", - "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + "https://www.irs.gov/pub/irs-prior/f4952--2025.pdf#page=3", + "https://www.irs.gov/pub/irs-prior/i1040sd--2025.pdf#page=15", ] def formula(tax_unit, period, parameters): net_gain = tax_unit("form_4952_net_investment_gain", period) - long_term_gains = add(tax_unit, period, ["long_term_capital_gains"]) - short_term_gains = add(tax_unit, period, ["short_term_capital_gains"]) - distributions = add(tax_unit, period, ["non_sch_d_capital_gains"]) - net_capital_gain = max_( - 0, long_term_gains + distributions - max_(0, -short_term_gains) + long_term_gains = tax_unit_non_dep_add( + tax_unit, period, ["long_term_capital_gains"] + ) + tax_unit("form_4952_capital_gain_distributions", period) + short_term_gains = tax_unit_non_dep_add( + tax_unit, period, ["short_term_capital_gains"] ) + net_capital_gain = max_(0, long_term_gains - max_(0, -short_term_gains)) return min_(net_gain, net_capital_gain) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_gain.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_gain.py index ab69ecdd92d..81aea588df4 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_gain.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_gain.py @@ -13,20 +13,23 @@ class form_4952_net_investment_gain(Variable): gains over your total losses" and include capital gain distributions and capital loss carryovers. The model has no split between investment and other capital assets, so it treats every capital asset as property - held for investment, matching the Schedule D Tax Worksheet Form 4952 - helper's arithmetic and aggregation of all tax-unit members. - long_term_capital_gains already includes Schedule D distributions; - non_sch_d_capital_gains adds distributions reported without Schedule D. + held for investment. Only the head's and spouse's gains count: a tax unit + dependent's gains belong on the dependent's own return, as in + irs_gross_income. long_term_capital_gains already includes Schedule D + distributions; non_sch_d_capital_gains adds distributions reported + without Schedule D, floored at zero for each filer as in irs_gross_income. There is no capital loss carryover input. Form 4797 business-property gains (other_net_gain) are excluded. """ reference = [ "https://www.law.cornell.edu/uscode/text/26/163#d_4_B_ii", "https://www.law.cornell.edu/uscode/text/26/163#d_5", - "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + "https://www.irs.gov/pub/irs-prior/f4952--2025.pdf#page=3", ] def formula(tax_unit, period, parameters): - net_gains = tax_unit("net_capital_gains", period) - distributions = add(tax_unit, period, ["non_sch_d_capital_gains"]) - return max_(0, net_gains + distributions) + gains = tax_unit_non_dep_add( + tax_unit, period, ["long_term_capital_gains", "short_term_capital_gains"] + ) + distributions = tax_unit("form_4952_capital_gain_distributions", period) + return max_(0, gains + distributions) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_income.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_income.py index 5af45596507..5bb5f62f0ee 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_income.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_income.py @@ -15,7 +15,7 @@ class form_4952_net_investment_income(Variable): """ reference = [ "https://www.law.cornell.edu/uscode/text/26/163#d_4_A", - "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + "https://www.irs.gov/pub/irs-prior/f4952--2025.pdf", ] def formula(tax_unit, period, parameters): diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_qualified_dividends.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_qualified_dividends.py index 2bfcae32932..8ad348e51e7 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_qualified_dividends.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_qualified_dividends.py @@ -17,11 +17,8 @@ class form_4952_qualified_dividends(Variable): """ reference = [ "https://www.law.cornell.edu/uscode/text/26/163#d_4_B", - "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + "https://www.irs.gov/pub/irs-prior/f4952--2025.pdf#page=3", ] def formula(tax_unit, period, parameters): - person = tax_unit.members - not_dependent = ~person("is_tax_unit_dependent", period) - dividends = person("qualified_dividend_income", period) - return tax_unit.sum(dividends * not_dependent) + return tax_unit_non_dep_add(tax_unit, period, ["qualified_dividend_income"]) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_total_investment_interest_expense.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_total_investment_interest_expense.py index dcee0fb0052..cd467b0ec1d 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_total_investment_interest_expense.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_total_investment_interest_expense.py @@ -9,16 +9,24 @@ class form_4952_total_investment_interest_expense(Variable): definition_period = YEAR documentation = """ Form 4952 line 3: "Total investment interest expense. Add lines 1 and 2." - Line 1 is "Investment interest expense paid or accrued in 2025". Sum all - tax-unit members' investment_interest_expense, matching the existing - deductible_interest_expense aggregation, which includes dependents. - Prior-year disallowed investment interest (line 2) has no model input - and is not modeled, so line 3 equals current-year line 1. + Line 1 is "Investment interest expense paid or accrued in 2025": the head's + and spouse's investment_interest_expense. A tax unit dependent's interest + belongs on the dependent's own return, as irs_gross_income leaves out the + dependent's income. Line 2 is the prior-year disallowed amount, which + section 163(d)(2) carries forward as interest paid in this year. """ reference = [ - "https://www.law.cornell.edu/uscode/text/26/163#d_3", + "https://www.law.cornell.edu/uscode/text/26/163#d_1", "https://www.law.cornell.edu/uscode/text/26/163#d_2", - "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + "https://www.law.cornell.edu/uscode/text/26/163#d_3", + "https://www.irs.gov/pub/irs-prior/f4952--2025.pdf", ] - adds = ["investment_interest_expense"] + def formula(tax_unit, period, parameters): + # Line 1: interest paid or accrued this year by the head and spouse. + paid = tax_unit_non_dep_add(tax_unit, period, ["investment_interest_expense"]) + # Line 2: interest disallowed last year. + carryover = tax_unit( + "form_4952_disallowed_investment_interest_expense_prior_year", period + ) + return paid + carryover diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/investment_interest_expense_deduction.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/investment_interest_expense_deduction.py index c3526ee1261..6f229d8af1e 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/investment_interest_expense_deduction.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/investment_interest_expense_deduction.py @@ -11,13 +11,13 @@ class investment_interest_expense_deduction(Variable): Form 4952 line 8: "Investment interest expense deduction. Enter the smaller of line 3 or line 6." Section 163(d)(1) says the investment interest deduction "shall not exceed the net investment income of - the taxpayer for the taxable year". The Form 4952 line 8 instructions - direct individuals to Schedule A line 9. Prior-year disallowed - investment interest (line 2) is not modeled. + the taxpayer for the taxable year". Line 3 includes any prior-year + disallowed interest carried forward under section 163(d)(2). The Form + 4952 line 8 instructions direct individuals to Schedule A line 9. """ reference = [ "https://www.law.cornell.edu/uscode/text/26/163#d_1", - "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + "https://www.irs.gov/pub/irs-prior/f4952--2025.pdf#page=4", ] def formula(tax_unit, period, parameters): diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/adjusted_net_capital_gain.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/adjusted_net_capital_gain.py index d37d2d6c75c..341d7e5f270 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/adjusted_net_capital_gain.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/adjusted_net_capital_gain.py @@ -27,13 +27,11 @@ def formula(tax_unit, period, parameters): # definition of 'net capital gain' for the above variable, and 26 U.S. Code § 1(h)(3) for the definition # of adjusted net capital gain (this variable). # Qualified dividend income as defined in paragraph (11), which - # excludes dividends elected as investment income (§ 1(h)(11)(D)(i)). - # net_capital_gain takes a Form 4952 election from the gain before - # the dividends, so the dividends it still holds are the smaller of - # the two amounts. - qualified_dividend_income = min_( - max_(0, add(tax_unit, period, ["qualified_dividend_income"])), - net_capital_gain, + # excludes dividends elected as investment income (§ 1(h)(11)(D)(i)): + # Schedule D Tax Worksheet line 6, the dividends net_capital_gain + # still holds after the Form 4952 line 4g election. + qualified_dividend_income = tax_unit( + "dividend_income_reduced_by_investment_income", period ) unrecaptured_s_1250_gain = tax_unit( "schedule_d_unrecaptured_section_1250_gain", period diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/net_capital_gain.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/net_capital_gain.py index d6011f76864..b2a8bdbf6f7 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/net_capital_gain.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/net_capital_gain.py @@ -46,19 +46,20 @@ def formula(tax_unit, period, parameters): # 26 U.S.C. 1222(11), determined without regard to section 1(h)(11). gain = max_(0, long_term_gain - short_term_loss) # Form 4952 line 4g: the net capital gain and qualified dividends - # elected to be included in investment income (26 U.S.C. 163(d)(4)(B)). + # elected to be included in investment income (26 U.S.C. 163(d)(4)(B)), + # the same effective amount the investment interest deduction uses. # The amount "is generally treated as being attributable first to net # capital gain ... and then to qualified dividends" (line 4g - # instructions). Section 1(h)(2) removes the gain part, the amount - # taken into account under 163(d)(4)(B)(iii); section 1(h)(11)(D)(i) - # removes the rest from qualified dividend income. - election = max_( - 0, add(tax_unit, period, ["investment_income_elected_form_4952"]) - ) - gain_elected = min_(election, gain) + # instructions): first to Form 4952 line 4e (Schedule D Tax Worksheet + # line 8), then to qualified dividends (worksheet line 5). Section + # 1(h)(2) removes the gain part, the amount taken into account under + # 163(d)(4)(B)(iii); section 1(h)(11)(D)(i) removes the rest from + # qualified dividend income. + election = tax_unit("form_4952_elected_investment_income", period) + gain_elected = min_(election, tax_unit("form_4952_net_capital_gain", period)) dividends_elected = election - gain_elected qualified_dividends = max_( 0, add(tax_unit, period, ["qualified_dividend_income"]) - dividends_elected, ) - return gain - gain_elected + qualified_dividends + return max_(0, gain - gain_elected) + qualified_dividends diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952_lines.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952_lines.py index f3eb7b9d7c7..9bf7aa0fad4 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952_lines.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952_lines.py @@ -18,35 +18,29 @@ def schedule_d_tax_worksheet_form_4952_lines(tax_unit, period): your net capital gain from the disposition of property held for investment". Line 7 is "the smaller of line 15 or line 16 of Schedule D". - The model treats every capital asset as property held for investment, so - Form 4952 line 4d is the excess of total gains over total losses and - line 4e's net capital gain is the excess of net long-term capital gain - over net short-term capital loss (2025 Form 4952 instructions, lines 4d - and 4e). It has no input for the "Elec." amount a taxpayer may write next - to line 4e (the worksheet's line 4 footnote), so the election is - attributed first to net capital gain and then to qualified dividends, as - the line 4g instructions do by default. + Lines 3 and 4 are the Form 4952 amounts the investment interest deduction + uses: form_4952_elected_investment_income, the head's and spouse's + election capped at Form 4952 lines 4b plus 4e, and + form_4952_net_capital_gain. Those variables compute their gains directly + and never read this worksheet. The model has no input for the "Elec." + amount a taxpayer may write next to line 4e (the worksheet's line 4 + footnote), so the election is attributed first to net capital gain and + then to qualified dividends, as the line 4g instructions do by default. - Capital gain distributions reported without Schedule D + Line 7 keeps the Schedule D amounts the rest of the capital gains tax + uses. Capital gain distributions reported without Schedule D (non_sch_d_capital_gains) belong on Schedule D line 13 (2025 Instructions for Schedule D, "Capital Gain Distributions"), so they enter lines 15 and - 16, and Form 4952 counts them as long-term capital gains. + 16. """ - election = max_(0, add(tax_unit, period, ["investment_income_elected_form_4952"])) long_term_gains = add(tax_unit, period, ["long_term_capital_gains"]) - short_term_gains = add(tax_unit, period, ["short_term_capital_gains"]) distributions = add(tax_unit, period, ["non_sch_d_capital_gains"]) # Schedule D line 15 (net long-term gain or loss, with line 13) and line # 16 (Schedule D line 7 plus line 15). schedule_d_line_15 = long_term_gains + distributions schedule_d_line_16 = tax_unit("net_capital_gains", period) + distributions - # Form 4952 line 4d: total gains over total losses, if any. - net_gain = max_(0, schedule_d_line_16) - # Net capital gain from property held for investment: net long-term - # capital gain over net short-term capital loss, if any. - net_capital_gain = max_(0, schedule_d_line_15 - max_(0, -short_term_gains)) return ScheduleDTaxWorksheetForm4952Lines( - line_3=election, - line_4=min_(net_gain, net_capital_gain), + line_3=tax_unit("form_4952_elected_investment_income", period), + line_4=tax_unit("form_4952_net_capital_gain", period), line_7=min_(schedule_d_line_15, schedule_d_line_16), ) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_net_capital_gain_other_than_dividends.py b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_net_capital_gain_other_than_dividends.py index c66190cdf30..215193dc59b 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_net_capital_gain_other_than_dividends.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_net_capital_gain_other_than_dividends.py @@ -10,7 +10,11 @@ def section_911_net_capital_gain_other_than_dividends(tax_unit, period): rounded to single precision, which moved capital_gains_tax by fractions of a cent for filers with nothing excluded. """ - qualified_dividends = add(tax_unit, period, ["qualified_dividend_income"]) + # Schedule D Tax Worksheet line 6: the qualified dividends net_capital_gain + # still holds after any Form 4952 line 4g election. + qualified_dividends = tax_unit( + "dividend_income_reduced_by_investment_income", period + ) gain = max_(0, tax_unit("net_capital_gain", period) - qualified_dividends) excess = tax_unit("section_911_capital_gain_excess", period) return where(excess > 0, max_(0, gain - excess), gain) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.py b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.py index 3cfc0d25094..2197da7bad7 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.py @@ -33,13 +33,11 @@ class section_911_qualified_dividend_income(Variable): def formula(tax_unit, period, parameters): net_capital_gain = tax_unit("net_capital_gain", period) # Qualified dividend income as section 1(h)(11) defines it, without - # dividends elected as investment income (§ 1(h)(11)(D)(i)). - # net_capital_gain takes a Form 4952 election from the gain before the - # dividends, so the dividends it still holds are the smaller of the - # two amounts. - qualified_dividends = min_( - max_(0, add(tax_unit, period, ["qualified_dividend_income"])), - net_capital_gain, + # dividends elected as investment income (§ 1(h)(11)(D)(i)): Schedule + # D Tax Worksheet line 6, the dividends net_capital_gain still holds + # after the Form 4952 line 4g election. + qualified_dividends = tax_unit( + "dividend_income_reduced_by_investment_income", period ) excess = tax_unit("section_911_capital_gain_excess", period) # Net capital gain determined without regard to section 1(h)(11). diff --git a/policyengine_us/variables/household/expense/person/investment_expense.py b/policyengine_us/variables/household/expense/person/investment_expense.py index d25c6b92b9d..445f36d15b9 100644 --- a/policyengine_us/variables/household/expense/person/investment_expense.py +++ b/policyengine_us/variables/household/expense/person/investment_expense.py @@ -19,5 +19,5 @@ class investment_expenses(Variable): reference = [ "https://www.law.cornell.edu/uscode/text/26/163#d_4_C", "https://www.law.cornell.edu/uscode/text/26/163#d_4_D", - "https://www.irs.gov/pub/irs-pdf/f4952.pdf", + "https://www.irs.gov/pub/irs-prior/f4952--2025.pdf", ] From 1f7b3445b53e310f485742c3d31e998d39c25f41 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 06:11:18 -0400 Subject: [PATCH 05/11] Exclude dependent investments and compute separate Montana interest limits --- ...st_investment_interest_limit_properties.py | 131 +++++++++++++++--- .../investment_interest/form_4952.yaml | 87 +++++++++++- .../mt_investment_interest_allocation.yaml | 14 +- ...ment_interest_expense_deduction_indiv.yaml | 125 +++++++++++++++++ .../form_4952_investment_expenses.py | 10 +- .../adjusted_net_capital_gain.py | 8 +- ...end_income_reduced_by_investment_income.py | 2 +- .../capital_gains/net_capital_gain.py | 15 +- ...chedule_d_tax_worksheet_form_4952_lines.py | 16 ++- ...1_net_capital_gain_other_than_dividends.py | 11 +- .../section_911_qualified_dividend_income.py | 8 +- ...eductions_for_federal_itemization_indiv.py | 26 ++-- ...eductions_for_federal_itemization_joint.py | 2 +- ...stment_interest_expense_deduction_indiv.py | 79 +++++++++++ .../general/mt_itemized_deductions_indiv.py | 26 ++-- .../general/mt_itemized_deductions_joint.py | 2 +- 16 files changed, 472 insertions(+), 90 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_expense_deduction_indiv.yaml create mode 100644 policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_investment_interest_expense_deduction_indiv.py diff --git a/policyengine_us/tests/core/test_investment_interest_limit_properties.py b/policyengine_us/tests/core/test_investment_interest_limit_properties.py index 0b947fd307b..59939dfc89e 100644 --- a/policyengine_us/tests/core/test_investment_interest_limit_properties.py +++ b/policyengine_us/tests/core/test_investment_interest_limit_properties.py @@ -15,11 +15,9 @@ uses raw inputs, without reading model outputs or parameters. Each case builds one Simulation containing seeded random single and joint -returns, including dependents. Interest and dividends exclude dependents, -matching irs_gross_income; expenses, gains and elections retain the existing -all-member aggregation. Prior-year disallowed investment interest has no -input, so line 3 contains current-year interest only. Comparison cohorts -share one Simulation, avoiding dependence on computed-value invalidation. +returns, including dependents. All Form 4952 amounts belong to filers; +line 3 also includes explicit prior-year carryover. Comparison cohorts share +one Simulation, avoiding computed-value invalidation. """ import numpy as np @@ -40,6 +38,7 @@ "investment_income_elected_form_4952", "investment_interest_expense", "investment_expenses", + "schedule_d_capital_gain_distributions", "unreimbursed_business_employee_expenses", "tax_preparation_fees", "deductible_mortgage_interest", @@ -73,6 +72,7 @@ def _draw(seed): "present": present, "filers": filers, "adjusted_gross_income": np.round(rng.uniform(0, 400_000, N), 2), + "carryover": np.round(rng.uniform(0, 40_000, N), 2), } for name in PERSON_INPUTS: if name in ("long_term_capital_gains", "short_term_capital_gains"): @@ -123,6 +123,9 @@ def _simulate(year, raw): "members": members, "filing_status": {period: "JOINT" if raw["present"][i, 1] else "SINGLE"}, "adjusted_gross_income": {period: float(raw["adjusted_gross_income"][i])}, + "form_4952_disallowed_investment_interest_expense_prior_year": { + period: float(raw["carryover"][i]) + }, } households[f"h{i}"] = {"members": members} simulation = Simulation( @@ -133,14 +136,22 @@ def _simulate(year, raw): "marital_units": marital_units, } ) - outputs = (*LINES.values(), "interest_deduction", "misc_deduction") + outputs = ( + *LINES.values(), + "interest_deduction", + "misc_deduction", + "net_capital_gain", + "dividend_income_reduced_by_investment_income", + "dwks09", + "dwks10", + ) return {name: simulation.calculate(name, period) for name in outputs} def _restatement(year, raw): """Form 4952 lines 3-8, independently restated from person inputs.""" filer = raw["filers"] - line_3 = raw["investment_interest_expense"].sum(axis=1) + line_3 = (raw["investment_interest_expense"] * filer).sum(axis=1) + raw["carryover"] line_4a = ( ( raw["taxable_interest_income"] @@ -150,23 +161,23 @@ def _restatement(year, raw): * filer ).sum(axis=1) line_4b = (raw["qualified_dividend_income"] * filer).sum(axis=1) - long_term = raw["long_term_capital_gains"].sum(axis=1) - short_term = raw["short_term_capital_gains"].sum(axis=1) - distributions = raw["non_sch_d_capital_gains"].sum(axis=1) + long_term = (raw["long_term_capital_gains"] * filer).sum(axis=1) + short_term = (raw["short_term_capital_gains"] * filer).sum(axis=1) + distributions = (np.maximum(0, raw["non_sch_d_capital_gains"]) * filer).sum(axis=1) line_4d = np.maximum(0, long_term + short_term + distributions) line_4e = np.minimum( line_4d, np.maximum(0, long_term + distributions - np.maximum(0, -short_term)), ) line_4g = np.minimum( - np.maximum(0, raw["investment_income_elected_form_4952"].sum(axis=1)), + np.maximum(0, (raw["investment_income_elected_form_4952"] * filer).sum(axis=1)), line_4b + line_4e, ) line_4h = line_4a - line_4b + line_4d - line_4e + line_4g - expenses = raw["investment_expenses"].sum(axis=1) + expenses = (raw["investment_expenses"] * filer).sum(axis=1) if year == 2017: schedule_a_line_24 = ( - expenses + raw["investment_expenses"].sum(axis=1) + raw["unreimbursed_business_employee_expenses"].sum(axis=1) + raw["tax_preparation_fees"].sum(axis=1) ) @@ -174,7 +185,20 @@ def _restatement(year, raw): 0, schedule_a_line_24 - 0.02 * np.maximum(0, raw["adjusted_gross_income"]), ) - line_5 = np.minimum(expenses, schedule_a_line_27) + filer_misc = sum( + (raw[name] * filer).sum(axis=1) + for name in ( + "investment_expenses", + "unreimbursed_business_employee_expenses", + "tax_preparation_fees", + ) + ) + line_5 = np.minimum( + expenses, + np.maximum( + 0, filer_misc - 0.02 * np.maximum(0, raw["adjusted_gross_income"]) + ), + ) else: schedule_a_line_27 = np.zeros_like(expenses) line_5 = np.zeros_like(expenses) @@ -203,8 +227,10 @@ def _restatement(year, raw): def _scale(raw): - return raw["adjusted_gross_income"] + sum( - np.abs(raw[name]).sum(axis=1) for name in PERSON_INPUTS + return ( + raw["adjusted_gross_income"] + + raw["carryover"] + + sum(np.abs(raw[name]).sum(axis=1) for name in PERSON_INPUTS) ) @@ -291,3 +317,76 @@ def test_investment_interest_limit_is_monotone(year): assert np.all(actual[LINES["8"]][cohort] >= base_allowed - slack), name if name == "investment_interest_expense": assert np.all(actual[LINES["7"]][cohort] >= base_carried - slack) + + +@pytest.mark.parametrize("year", YEARS) +def test_dependent_investments_do_not_change_filer_form_4952(year): + base = _draw(seed=year + 30) + changed = {key: value.copy() for key, value in base.items()} + for name in PERSON_INPUTS: + if name != "deductible_mortgage_interest": + changed[name][:, 2] += 50_000 + combined = {key: np.concatenate([base[key], changed[key]], axis=0) for key in base} + actual = _simulate(year, combined) + scale = _scale(combined) + for name in ( + *LINES.values(), + "interest_deduction", + "net_capital_gain", + "dividend_income_reduced_by_investment_income", + "dwks09", + "dwks10", + ): + _close(actual[name][N:], actual[name][:N], scale[:N] + scale[N:], name) + + +@pytest.mark.parametrize("year", YEARS) +def test_carryforward_is_available_and_conserved(year): + base = _draw(seed=year + 40) + base["investment_interest_expense"][:] = 0 + base["carryover"][:] = 0 + changed = {key: value.copy() for key, value in base.items()} + changed["carryover"][:] = 4_000 + combined = {key: np.concatenate([base[key], changed[key]], axis=0) for key in base} + actual = _simulate(year, combined) + expected = _restatement(year, combined) + scale = _scale(combined) + for name in (LINES["3"], LINES["7"], LINES["8"], "interest_deduction"): + _close(actual[name], expected[name], scale, name) + _close( + actual[LINES["7"]] + actual[LINES["8"]], + combined["carryover"], + scale, + "carryover conservation", + ) + assert np.any(actual[LINES["8"]][N:] > 0) + + +@pytest.mark.parametrize("year", YEARS) +def test_effective_election_agrees_across_deduction_and_tax_worksheets(year): + raw = _draw(seed=year + 50) + actual = _simulate(year, raw) + expected = _restatement(year, raw) + scale = _scale(raw) + election = expected[LINES["4g"]] + investment_gain = expected[LINES["4e"]] + residual_dividends = np.maximum( + 0, expected[LINES["4b"]] - np.maximum(0, election - investment_gain) + ) + residual_gain = np.maximum(0, investment_gain - election) + _close( + actual["dividend_income_reduced_by_investment_income"], + residual_dividends, + scale, + "worksheet line 6", + ) + _close(actual["dwks09"], residual_gain, scale, "worksheet line 9") + _close( + actual["dwks10"], residual_gain + residual_dividends, scale, "worksheet line 10" + ) + _close( + actual["net_capital_gain"], + residual_gain + residual_dividends, + scale, + "statutory net gain", + ) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml index 508ab4c45e0..9ff46f972f4 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml @@ -11,7 +11,7 @@ taxable_interest_income: 10_000 investment_interest_expense: 6_000 output: - form_4952_total_investment_interest_expense: 6_000 # line 3 = line 1 6,000 + unmodeled line 2 0 + form_4952_total_investment_interest_expense: 6_000 # line 3 = line 1 6,000 + default line 2 0 form_4952_gross_investment_income: 10_000 # line 4a = taxable interest 10,000 + dividends 0 form_4952_qualified_dividends: 0 # line 4b = qualified dividends 0 form_4952_net_investment_gain: 0 # line 4d = max(0, gains 0) @@ -157,7 +157,7 @@ form_4952_elected_investment_income: 8_000 # line 4g = min(10,000, 4b 0 + 4e 8,000) investment_interest_expense_deduction: 8_000 # line 8 = min(3 12,000, 6 8,000) -- name: Joint income excludes dependent interest and dividends while expenses preserve existing membership +- name: Joint Form 4952 excludes dependent income and investment debt period: 2025 input: people: @@ -182,12 +182,12 @@ unit: members: [head, spouse, child] output: - form_4952_total_investment_interest_expense: 10_500 # line 3 = 6,000 + 4,000 + 500 (all members) + form_4952_total_investment_interest_expense: 10_000 # line 3 = filer debt 6,000 + 4,000 form_4952_gross_investment_income: 12_000 # line 4a = filers' interest 7,000 + ordinary dividends 5,000 form_4952_qualified_dividends: 3_000 # line 4b = filers' qualified dividends 1,000 + 2,000 form_4952_investment_income: 9_000 # line 4c = 4a 12,000 - 4b 3,000; 4f = 4g = 0 - investment_interest_expense_deduction: 9_000 # line 8 = min(3 10,500, 6 9,000) - form_4952_investment_interest_carryforward: 1_500 # line 7 = 3 10,500 - 6 9,000 + investment_interest_expense_deduction: 9_000 # line 8 = min(3 10,000, 6 9,000) + form_4952_investment_interest_carryforward: 1_000 # line 7 = 3 10,000 - 6 9,000 - name: Alaska Permanent Fund dividends and business property gains stay out of line 4a and 4d period: 2025 @@ -309,3 +309,80 @@ # + (120,000 - 103,350) * 24% = 1,192.50 + 4,386 + 12,072.50 + 3,996 = 21,647. # Source: https://www.irs.gov/pub/irs-drop/rp-24-40.pdf#page=6 income_tax: 21_647 # federal tax after credits = regular tax 21,647 + AMT/NIIT 0 - credits 0 + +- name: Carryover adds to the deduction without reducing mortgage interest + period: 2025 + input: + deductible_mortgage_interest: 10_000 + investment_interest_expense: 2_000 + form_4952_disallowed_investment_interest_expense_prior_year: 4_000 + taxable_interest_income: 3_000 + output: + form_4952_total_investment_interest_expense: 6_000 + investment_interest_expense_deduction: 3_000 + form_4952_investment_interest_carryforward: 3_000 + interest_deduction: 13_000 # 12,000 current aggregate - 2,000 current debt + 3,000 allowed + +- name: Carryover alone can be deducted + period: 2026 + input: + form_4952_disallowed_investment_interest_expense_prior_year: 4_000 + taxable_interest_income: 5_000 + output: + investment_interest_expense_deduction: 4_000 + form_4952_investment_interest_carryforward: 0 + interest_deduction: 4_000 + +- name: Dependent investments stay off the parent Form 4952 + period: 2025 + input: + people: + head: + is_tax_unit_head: true + taxable_interest_income: 1_000 + investment_interest_expense: 5_000 + child: + age: 12 + is_tax_unit_dependent: true + taxable_interest_income: 50_000 + qualified_dividend_income: 10_000 + short_term_capital_gains: 9_000 + long_term_capital_gains: 10_000 + investment_income_elected_form_4952: 20_000 + investment_interest_expense: 5_000 + tax_units: + unit: + members: [head, child] + output: + form_4952_total_investment_interest_expense: 5_000 + form_4952_gross_investment_income: 1_000 + form_4952_qualified_dividends: 0 + form_4952_net_investment_gain: 0 + form_4952_net_capital_gain: 0 + form_4952_elected_investment_income: 0 + investment_interest_expense_deduction: 1_000 + form_4952_investment_interest_carryforward: 4_000 + interest_deduction: 1_000 # Remove both members' current raw debt from aggregate. + net_capital_gain: 0 + dividend_income_reduced_by_investment_income: 0 + +- name: Dependent miscellaneous fees do not increase filer Form 4952 line 5 + period: 2017 + input: + people: + head: + is_tax_unit_head: true + investment_expenses: 3_000 + taxable_interest_income: 12_000 + investment_interest_expense: 20_000 + child: + age: 12 + is_tax_unit_dependent: true + investment_expenses: 3_000 + tax_units: + unit: + members: [head, child] + adjusted_gross_income: 100_000 + output: + form_4952_investment_expenses: 1_000 # Filer pool 3,000 - 2% * 100,000. + investment_interest_expense_deduction: 11_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_allocation.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_allocation.yaml index 069690d7bcc..b433c274b59 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_allocation.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_allocation.yaml @@ -1,13 +1,13 @@ # 2023 Form 2 instructions, line 10: "following the computation on federal # Form 4952 but include any Montana adjustments to income." -# https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=31 +# https://mtrevenue.gov/wp-content/uploads/dlm_uploads/2023/12/Form_2_2023_Instructions.pdf#page=31 # 2024 Worksheet A line 1: "Enter your total federal itemized deductions from # Form 1040, line 12." # https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=7 # Federal Form 4952 line 8: "Enter the smaller of line 3 or line 6." # https://www.irs.gov/pub/irs-prior/f4952--2025.pdf -- name: Montana allocates limited interest by each spouse's paid share and handles zero totals +- name: Montana separate investment interest limits handle zero debt and spouse-only debt period: 2023 absolute_error_margin: 0.01 input: @@ -113,13 +113,13 @@ form_4952_net_investment_income: [600, 0, 500] # Line 8 = min(line 3, line 6) = [600, 0, 500]. investment_interest_expense_deduction: [600, 0, 500] - # Partial: 600 * [300/1,200, 900/1,200] = [150, 450]. - # Zero: zero paid gives zero shares. Spouse-only: 500 * [0/1,200, 1,200/1,200]. - mt_itemized_deductions_indiv: [150, 450, 0, 0, 0, 500] + # Separate limits: [min(300, 600), min(900, 0)] = [300, 0]. + # Zero debt remains zero; only the spouse can use the spouse's own income. + mt_itemized_deductions_indiv: [300, 0, 0, 0, 0, 500] # Each tax unit's full line 8 is assigned to its head, including spouse interest. mt_itemized_deductions_joint: [600, 0, 0, 0, 500, 0] - # The federal-itemization allocation uses the same paid shares and limited amounts. - mt_itemized_deductions_for_federal_itemization_indiv: [150, 450, 0, 0, 0, 500] + # Federal-itemization uses the same separate limits. + mt_itemized_deductions_for_federal_itemization_indiv: [300, 0, 0, 0, 0, 500] # Federal-itemization joint totals assign line 8 to the head as well. mt_itemized_deductions_for_federal_itemization_joint: [600, 0, 0, 0, 500, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_expense_deduction_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_expense_deduction_indiv.yaml new file mode 100644 index 00000000000..f7582172129 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_expense_deduction_indiv.yaml @@ -0,0 +1,125 @@ +# Montana 2023 Form 2 instructions, page 31, line 10, require spouses filing +# separately to compute their investment interest deductions separately. +# https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=38 + +- name: Montana separate returns use separate investment income limits + period: 2023 + input: + people: + head: + is_tax_unit_head: true + is_tax_unit_head_or_spouse: true + investment_interest_expense: 300 + taxable_interest_income: 600 + mortgage_interest: 0 + mt_misc_deductions: 0 + mt_medical_expense_deduction_indiv: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + mt_federal_income_tax_deduction_indiv: 0 + mt_federal_income_tax_deduction_for_federal_itemization_indiv: 0 + spouse: + is_tax_unit_head: false + is_tax_unit_spouse: true + is_tax_unit_head_or_spouse: true + investment_interest_expense: 900 + mortgage_interest: 0 + mt_misc_deductions: 0 + mt_medical_expense_deduction_indiv: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + mt_federal_income_tax_deduction_indiv: 0 + mt_federal_income_tax_deduction_for_federal_itemization_indiv: 0 + tax_units: + unit: + members: [head, spouse] + filing_status: JOINT + charitable_deduction: 0 + mt_federal_income_tax_deduction_unit: 0 + mt_federal_income_tax_deduction_for_federal_itemization: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + mt_itemized_deductions_indiv: [300, 0] + mt_itemized_deductions_for_federal_itemization_indiv: [300, 0] + mt_itemized_deductions_joint: [600, 0] + mt_itemized_deductions_for_federal_itemization_joint: [600, 0] + # Separate: min(300,600)=300; min(900,0)=0. + # Joint: min(300+900,600)=600. + +- name: Montana separate limits net each spouse's gains and cap only their own election + period: 2023 + input: + people: + head: + is_tax_unit_head: true + investment_interest_expense: 1_000 + taxable_interest_income: 200 + qualified_dividend_income: 40 + short_term_capital_gains: -100 + long_term_capital_gains: 500 + non_sch_d_capital_gains: -300 + investment_income_elected_form_4952: 50 + investment_expenses: 1_000 + spouse: + is_tax_unit_spouse: true + investment_interest_expense: 200 + qualified_dividend_income: 10 + short_term_capital_gains: 100 + long_term_capital_gains: -20 + investment_income_elected_form_4952: 500 + child: + age: 12 + is_tax_unit_dependent: true + investment_interest_expense: 9_000 + taxable_interest_income: 50_000 + qualified_dividend_income: 10_000 + short_term_capital_gains: 9_000 + long_term_capital_gains: 10_000 + investment_income_elected_form_4952: 20_000 + tax_units: + unit: + members: [head, spouse, child] + filing_status: JOINT + households: + household: + members: [head, spouse, child] + state_code: MT + output: + # Head: negative distributions are floored; the 400 net long-term gain + # counts only through the 50 election. Income is 200+50=250. Investment + # fees are not deductible under the modeled federal post-2017 rules. + # Spouse: 80 net short-term gain counts without election; the 500 + # election is capped at 10 qualified dividends, giving income of 90. + mt_investment_interest_expense_deduction_indiv: [250, 90, 0] + +- name: Montana 2024 uses the pooled federal limit and excludes dependent debt from shares + period: 2024 + absolute_error_margin: 0.01 + input: + people: + head: + is_tax_unit_head: true + investment_interest_expense: 300 + taxable_interest_income: 600 + spouse: + is_tax_unit_spouse: true + investment_interest_expense: 900 + child: + age: 12 + is_tax_unit_dependent: true + investment_interest_expense: 9_000 + taxable_interest_income: 50_000 + tax_units: + unit: + members: [head, spouse, child] + filing_status: JOINT + households: + household: + members: [head, spouse, child] + state_code: MT + output: + investment_interest_expense_deduction: 600 + mt_investment_interest_expense_deduction_indiv: [150, 450, 0] diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_expenses.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_expenses.py index 7fb146d02be..fbd978f1227 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_expenses.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_expenses.py @@ -36,5 +36,13 @@ def formula(tax_unit, period, parameters): if not parameters(period).gov.irs.deductions.itemized.misc.applies: return 0 expenses = tax_unit_non_dep_add(tax_unit, period, ["investment_expenses"]) - allowed_misc_deduction = tax_unit("misc_deduction", period) + p = parameters(period).gov.irs.deductions.itemized.misc + # Dependent expenses cannot increase the filer's Schedule A line 27. + filer_misc_expenses = tax_unit_non_dep_add(tax_unit, period, p.sources) + filer_misc_deduction = max_( + 0, filer_misc_expenses - p.floor * tax_unit("positive_agi", period) + ) + allowed_misc_deduction = min_( + tax_unit("misc_deduction", period), filer_misc_deduction + ) return min_(expenses, allowed_misc_deduction) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/adjusted_net_capital_gain.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/adjusted_net_capital_gain.py index 341d7e5f270..ded6624b604 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/adjusted_net_capital_gain.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/adjusted_net_capital_gain.py @@ -29,9 +29,11 @@ def formula(tax_unit, period, parameters): # Qualified dividend income as defined in paragraph (11), which # excludes dividends elected as investment income (§ 1(h)(11)(D)(i)): # Schedule D Tax Worksheet line 6, the dividends net_capital_gain - # still holds after the Form 4952 line 4g election. - qualified_dividend_income = tax_unit( - "dividend_income_reduced_by_investment_income", period + # still holds after the Form 4952 line 4g election. Cap this part at + # net_capital_gain to preserve a directly supplied net gain amount. + qualified_dividend_income = min_( + max_(0, tax_unit("dividend_income_reduced_by_investment_income", period)), + net_capital_gain, ) unrecaptured_s_1250_gain = tax_unit( "schedule_d_unrecaptured_section_1250_gain", period diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dividend_income_reduced_by_investment_income.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dividend_income_reduced_by_investment_income.py index c1942cc2082..830bd31aabc 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dividend_income_reduced_by_investment_income.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dividend_income_reduced_by_investment_income.py @@ -28,7 +28,7 @@ class dividend_income_reduced_by_investment_income(Variable): def formula(tax_unit, period, parameters): # Line 2: qualified dividends (Form 1040 line 3a). - line_2 = add(tax_unit, period, ["qualified_dividend_income"]) + line_2 = tax_unit("form_4952_qualified_dividends", period) lines = schedule_d_tax_worksheet_form_4952_lines(tax_unit, period) # Line 5: line 3 minus line 4; if zero or less, zero. The part of the # election that net capital gain from investment property cannot diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/net_capital_gain.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/net_capital_gain.py index b2a8bdbf6f7..1ed7f4b0c75 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/net_capital_gain.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/net_capital_gain.py @@ -36,13 +36,14 @@ class net_capital_gain(Variable): def formula(tax_unit, period, parameters): # Capital gain distributions, including those reported without # Schedule D (Form 1040 line 7 with the box checked), are long-term - # capital gains under IRC 852(b)(3)(B). - long_term_gain = add( - tax_unit, - period, - ["long_term_capital_gains", "non_sch_d_capital_gains"], + # capital gains under IRC 852(b)(3)(B). Only the head's and spouse's + # gains belong on this return, as on their Form 4952. + long_term_gain = tax_unit_non_dep_add( + tax_unit, period, ["long_term_capital_gains"] + ) + tax_unit("form_4952_capital_gain_distributions", period) + short_term_loss = max_( + 0, -tax_unit_non_dep_add(tax_unit, period, ["short_term_capital_gains"]) ) - short_term_loss = max_(0, -add(tax_unit, period, ["short_term_capital_gains"])) # 26 U.S.C. 1222(11), determined without regard to section 1(h)(11). gain = max_(0, long_term_gain - short_term_loss) # Form 4952 line 4g: the net capital gain and qualified dividends @@ -60,6 +61,6 @@ def formula(tax_unit, period, parameters): dividends_elected = election - gain_elected qualified_dividends = max_( 0, - add(tax_unit, period, ["qualified_dividend_income"]) - dividends_elected, + tax_unit("form_4952_qualified_dividends", period) - dividends_elected, ) return max_(0, gain - gain_elected) + qualified_dividends diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952_lines.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952_lines.py index 9bf7aa0fad4..4ec8af279f7 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952_lines.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952_lines.py @@ -27,18 +27,24 @@ def schedule_d_tax_worksheet_form_4952_lines(tax_unit, period): footnote), so the election is attributed first to net capital gain and then to qualified dividends, as the line 4g instructions do by default. - Line 7 keeps the Schedule D amounts the rest of the capital gains tax - uses. Capital gain distributions reported without Schedule D + Line 7 keeps the head's and spouse's Schedule D amounts the rest of the + capital gains tax uses, independently of Form 4952 line 4e. Capital gain + distributions reported without Schedule D (non_sch_d_capital_gains) belong on Schedule D line 13 (2025 Instructions for Schedule D, "Capital Gain Distributions"), so they enter lines 15 and 16. """ - long_term_gains = add(tax_unit, period, ["long_term_capital_gains"]) - distributions = add(tax_unit, period, ["non_sch_d_capital_gains"]) + long_term_gains = tax_unit_non_dep_add( + tax_unit, period, ["long_term_capital_gains"] + ) + short_term_gains = tax_unit_non_dep_add( + tax_unit, period, ["short_term_capital_gains"] + ) + distributions = tax_unit("form_4952_capital_gain_distributions", period) # Schedule D line 15 (net long-term gain or loss, with line 13) and line # 16 (Schedule D line 7 plus line 15). schedule_d_line_15 = long_term_gains + distributions - schedule_d_line_16 = tax_unit("net_capital_gains", period) + distributions + schedule_d_line_16 = schedule_d_line_15 + short_term_gains return ScheduleDTaxWorksheetForm4952Lines( line_3=tax_unit("form_4952_elected_investment_income", period), line_4=tax_unit("form_4952_net_capital_gain", period), diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_net_capital_gain_other_than_dividends.py b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_net_capital_gain_other_than_dividends.py index 215193dc59b..8125bf1b189 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_net_capital_gain_other_than_dividends.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_net_capital_gain_other_than_dividends.py @@ -11,10 +11,13 @@ def section_911_net_capital_gain_other_than_dividends(tax_unit, period): of a cent for filers with nothing excluded. """ # Schedule D Tax Worksheet line 6: the qualified dividends net_capital_gain - # still holds after any Form 4952 line 4g election. - qualified_dividends = tax_unit( - "dividend_income_reduced_by_investment_income", period + # still holds after any Form 4952 line 4g election, bounded by any direct + # net_capital_gain input. + net_capital_gain = tax_unit("net_capital_gain", period) + qualified_dividends = min_( + max_(0, tax_unit("dividend_income_reduced_by_investment_income", period)), + net_capital_gain, ) - gain = max_(0, tax_unit("net_capital_gain", period) - qualified_dividends) + gain = max_(0, net_capital_gain - qualified_dividends) excess = tax_unit("section_911_capital_gain_excess", period) return where(excess > 0, max_(0, gain - excess), gain) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.py b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.py index 2197da7bad7..fba97bdaca9 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.py @@ -35,9 +35,11 @@ def formula(tax_unit, period, parameters): # Qualified dividend income as section 1(h)(11) defines it, without # dividends elected as investment income (§ 1(h)(11)(D)(i)): Schedule # D Tax Worksheet line 6, the dividends net_capital_gain still holds - # after the Form 4952 line 4g election. - qualified_dividends = tax_unit( - "dividend_income_reduced_by_investment_income", period + # after the Form 4952 line 4g election. A directly supplied net gain + # can contain fewer dividends than this independently computed line. + qualified_dividends = min_( + max_(0, tax_unit("dividend_income_reduced_by_investment_income", period)), + net_capital_gain, ) excess = tax_unit("section_911_capital_gain_excess", period) # Net capital gain determined without regard to section 1(h)(11). diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py index ffd31bd286c..d41aaaa7794 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.py @@ -11,7 +11,7 @@ class mt_itemized_deductions_for_federal_itemization_indiv(Variable): "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-21/section-15-30-2131/", # MT Code § 15-30-2131 (2022) (1) - "https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=31", + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=38", "https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=7", ) defined_for = "mt_married_filing_separately_on_same_return_eligible" @@ -22,11 +22,11 @@ class mt_itemized_deductions_for_federal_itemization_indiv(Variable): instructions says, "Enter your total federal itemized deductions from Form 1040, line 12." - Investment interest uses federal Form 4952 line 8. Before 2024 this is an - approximation: the 2023 instructions call for federal Form 4952 - "separately", and Montana investment-income adjustments are not modeled. - The model allocates the tax-unit deduction by each person's share of - investment interest paid for separate filing on the same return. + Before 2024, each spouse computes the investment interest limit + separately using that spouse's investment income, gains, and election. + Montana investment-income adjustments and separate-return carryovers + are not modeled. From 2024, investment interest follows federal Form + 4952 line 8. """ def formula(person, period, parameters): @@ -35,18 +35,8 @@ def formula(person, period, parameters): # we will split the value between each spouse charitable_deduction = person.tax_unit("charitable_deduction", period) * 0.5 head_or_spouse = person("is_tax_unit_head_or_spouse", period) - # Allocate federal Form 4952 line 8 by each person's share of interest paid. - interest_paid = person("investment_interest_expense", period) - total_interest_paid = person.tax_unit.sum(interest_paid) - interest_share = np.divide( - interest_paid, - total_interest_paid, - out=np.zeros_like(interest_paid), - where=total_interest_paid > 0, - ) - investment_interest = ( - person.tax_unit("investment_interest_expense_deduction", period) - * interest_share + investment_interest = person( + "mt_investment_interest_expense_deduction_indiv", period ) mortgage_interest = person("mortgage_interest", period) interest_ded = investment_interest + mortgage_interest diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.py index aa10081d4ff..175d6a6afbe 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/federal_itemization/mt_itemized_deductions_for_federal_itemization_joint.py @@ -11,7 +11,7 @@ class mt_itemized_deductions_for_federal_itemization_joint(Variable): "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-21/section-15-30-2131/", # MT Code § 15-30-2131 (2022) (1) - "https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=31", + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=38", "https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=7", ) defined_for = StateCode.MT diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_investment_interest_expense_deduction_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_investment_interest_expense_deduction_indiv.py new file mode 100644 index 00000000000..921f38a0f72 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_investment_interest_expense_deduction_indiv.py @@ -0,0 +1,79 @@ +from policyengine_us.model_api import * + + +class mt_investment_interest_expense_deduction_indiv(Variable): + value_type = float + entity = Person + label = "Montana investment interest deduction for separate returns" + unit = USD + definition_period = YEAR + defined_for = StateCode.MT + reference = [ + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=38", + "https://www.irs.gov/pub/irs-prior/f4952--2023.pdf", + "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=7", + ] + documentation = """ + Before 2024, Montana's Form 2 line 10 requires spouses filing separately + to compute Form 4952 separately. Each spouse's current interest is + limited to that spouse's net investment income, with the same modeled + property classification, gain netting, and capped election as the federal + form. Dependents' amounts do not enter either spouse's computation. + + Montana investment-income adjustments and separate-return carryovers + are not modeled. The federal prior-year carryover input is at tax-unit + level and cannot identify the carryover belonging to each spouse. + Investment expenses follow the modeled federal miscellaneous deduction + rules, applying any allowed expense pool and AGI floor separately. + + From 2024, Montana uses federal itemized deductions. This variable + preserves the allocation of the federal deduction by filer interest + shares; Montana no longer permits separate filing on the same return. + """ + + def formula(person, period, parameters): + filer = person("is_tax_unit_head_or_spouse", period) + interest = max_(0, person("investment_interest_expense", period)) + qualified_dividends = person("qualified_dividend_income", period) + distributions = max_(0, person("non_sch_d_capital_gains", period)) + long_term_gains = person("long_term_capital_gains", period) + distributions + short_term_gains = person("short_term_capital_gains", period) + net_gain = max_(0, long_term_gains + short_term_gains) + net_capital_gain = min_( + net_gain, max_(0, long_term_gains - max_(0, -short_term_gains)) + ) + election = min_( + max_(0, person("investment_income_elected_form_4952", period)), + qualified_dividends + net_capital_gain, + ) + gross_income = add( + person, period, ["taxable_interest_income", "ordinary_dividend_income"] + ) + investment_income = ( + gross_income - qualified_dividends + net_gain - net_capital_gain + election + ) + p = parameters(period).gov.irs.deductions.itemized.misc + if p.applies: + misc_expenses = add(person, period, p.sources) + own_agi = max_(0, person("adjusted_gross_income_person", period)) + allowed_misc = max_(0, misc_expenses - p.floor * own_agi) + expenses = min_(person("investment_expenses", period), allowed_misc) + else: + expenses = 0 + net_investment_income = max_(0, investment_income - expenses) + return filer * min_(interest, net_investment_income) + + def formula_2024(person, period, parameters): + filer = person("is_tax_unit_head_or_spouse", period) + interest = filer * max_(0, person("investment_interest_expense", period)) + total_interest = person.tax_unit.sum(interest) + interest_share = np.divide( + interest, + total_interest, + out=np.zeros_like(interest), + where=total_interest > 0, + ) + return ( + person.tax_unit("investment_interest_expense_deduction", period) + * interest_share + ) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py index 9bf61e850fe..dd2f976122f 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_indiv.py @@ -11,7 +11,7 @@ class mt_itemized_deductions_indiv(Variable): "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-21/section-15-30-2131/", # MT Code § 15-30-2131 (2022) (1) - "https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=31", + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=38", "https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=7", ) defined_for = "mt_married_filing_separately_on_same_return_eligible" @@ -22,11 +22,11 @@ class mt_itemized_deductions_indiv(Variable): instructions says, "Enter your total federal itemized deductions from Form 1040, line 12." - Investment interest uses federal Form 4952 line 8. Before 2024 this is an - approximation: the 2023 instructions call for federal Form 4952 - "separately", and Montana investment-income adjustments are not modeled. - The model allocates the tax-unit deduction by each person's share of - investment interest paid for separate filing on the same return. + Before 2024, each spouse computes the investment interest limit + separately using that spouse's investment income, gains, and election. + Montana investment-income adjustments and separate-return carryovers + are not modeled. From 2024, investment interest follows federal Form + 4952 line 8. """ def formula(person, period, parameters): @@ -37,18 +37,8 @@ def formula(person, period, parameters): person.tax_unit("charitable_deduction", period) * p.spouse_allocation_rate ) head_or_spouse = person("is_tax_unit_head_or_spouse", period) - # Allocate federal Form 4952 line 8 by each person's share of interest paid. - interest_paid = person("investment_interest_expense", period) - total_interest_paid = person.tax_unit.sum(interest_paid) - interest_share = np.divide( - interest_paid, - total_interest_paid, - out=np.zeros_like(interest_paid), - where=total_interest_paid > 0, - ) - investment_interest = ( - person.tax_unit("investment_interest_expense_deduction", period) - * interest_share + investment_interest = person( + "mt_investment_interest_expense_deduction_indiv", period ) mortgage_interest = person("mortgage_interest", period) interest_ded = investment_interest + mortgage_interest diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py index 180e341360b..7a3ada9c30f 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py @@ -11,7 +11,7 @@ class mt_itemized_deductions_joint(Variable): "https://revenuefiles.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2022_Montana_Individual_Income_Tax_Return_Form_2.pdf#page=7", "https://law.justia.com/codes/montana/2022/title-15/chapter-30/part-21/section-15-30-2131/", # MT Code § 15-30-2131 (2022) (1) - "https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=31", + "https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=38", "https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=7", ) defined_for = StateCode.MT From fbd0b175343ed05a114562ea02b7d569316bd846 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 06:14:40 -0400 Subject: [PATCH 06/11] Clarify gain carryover conventions and verified Montana references --- .../deductions/itemized/mt_investment_interest_allocation.yaml | 2 +- .../investment_interest/form_4952_net_investment_gain.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_allocation.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_allocation.yaml index b433c274b59..0d30504e2d7 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_allocation.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_allocation.yaml @@ -1,6 +1,6 @@ # 2023 Form 2 instructions, line 10: "following the computation on federal # Form 4952 but include any Montana adjustments to income." -# https://mtrevenue.gov/wp-content/uploads/dlm_uploads/2023/12/Form_2_2023_Instructions.pdf#page=31 +# https://revenue.mt.gov/files/forms/Montana-Individual-Income-Tax-Return-Form-2-Instructions/2023_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=38 # 2024 Worksheet A line 1: "Enter your total federal itemized deductions from # Form 1040, line 12." # https://revenue.mt.gov/files/Forms/Montana-Individual-Income-Tax-Return-Form-2/2024_Montana_Individual_Income_Tax_Return_Form_2_Instructions.pdf#page=7 diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_gain.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_gain.py index 81aea588df4..105bf3c0ea1 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_gain.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_gain.py @@ -18,7 +18,8 @@ class form_4952_net_investment_gain(Variable): irs_gross_income. long_term_capital_gains already includes Schedule D distributions; non_sch_d_capital_gains adds distributions reported without Schedule D, floored at zero for each filer as in irs_gross_income. - There is no capital loss carryover input. Form 4797 business-property + The long_term_capital_loss_carryover input is a memo amount whose loss + is already included in long_term_capital_gains; it is not subtracted again. Form 4797 business-property gains (other_net_gain) are excluded. """ reference = [ From 636342788add0ce64951409147cd267e0cfe6307 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 06:20:33 -0400 Subject: [PATCH 07/11] Preserve carryover-only Montana reporting and federal line 8 alias --- .../investment-interest-163d-limit.fixed.md | 2 +- .../investment_interest/form_4952.yaml | 1 + ...ment_interest_expense_deduction_indiv.yaml | 39 +++++++++++++++++++ .../form_4952_net_capital_gain.py | 4 +- ...stment_interest_expense_deduction_indiv.py | 11 +++++- 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/changelog.d/investment-interest-163d-limit.fixed.md b/changelog.d/investment-interest-163d-limit.fixed.md index ee7167ffb8e..80f52b0af2c 100644 --- a/changelog.d/investment-interest-163d-limit.fixed.md +++ b/changelog.d/investment-interest-163d-limit.fixed.md @@ -1 +1 @@ -Limit the itemized investment interest deduction to net investment income under 26 U.S.C. 163(d)(1) via Form 4952 and update California, New York, Virginia, and Montana to use the limited federal deduction. +Limit itemized investment interest under 26 U.S.C. 163(d) through Form 4952, support explicit carryovers, exclude dependent investments, and align California, New York, Virginia, and Montana deductions, including Montana separate-return limits before 2024. diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml index 9ff46f972f4..c5c404bded9 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml @@ -319,6 +319,7 @@ taxable_interest_income: 3_000 output: form_4952_total_investment_interest_expense: 6_000 + investment_income_form_4952: 3_000 # Legacy CA federal line 8 alias defaults to allowed interest. investment_interest_expense_deduction: 3_000 form_4952_investment_interest_carryforward: 3_000 interest_deduction: 13_000 # 12,000 current aggregate - 2,000 current debt + 3,000 allowed diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_expense_deduction_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_expense_deduction_indiv.yaml index f7582172129..5deb09e3b0f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_expense_deduction_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/deductions/itemized/mt_investment_interest_expense_deduction_indiv.yaml @@ -123,3 +123,42 @@ output: investment_interest_expense_deduction: 600 mt_investment_interest_expense_deduction_indiv: [150, 450, 0] + +- name: Montana 2024 preserves a carryover-only federal deduction on the head + period: 2024 + input: + people: + head: + is_tax_unit_head: true + investment_interest_expense: 0 + taxable_interest_income: 5_000 + mortgage_interest: 0 + mt_misc_deductions: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + spouse: + is_tax_unit_spouse: true + investment_interest_expense: 0 + mortgage_interest: 0 + mt_misc_deductions: 0 + mt_medical_expense_deduction_joint: 0 + mt_salt_deduction: 0 + tax_units: + unit: + members: [head, spouse] + filing_status: JOINT + form_4952_disallowed_investment_interest_expense_prior_year: 4_000 + charitable_deduction: 0 + mt_federal_income_tax_deduction_unit: 0 + mt_federal_income_tax_deduction_for_federal_itemization: 0 + households: + household: + members: [head, spouse] + state_code: MT + output: + investment_interest_expense_deduction: 4_000 + mt_investment_interest_expense_deduction_indiv: [4_000, 0] + mt_itemized_deductions_joint: [4_000, 0] + mt_itemized_deductions_for_federal_itemization_joint: [4_000, 0] + # The head allocation is reporting bookkeeping for the federal total; + # Montana no longer computes separate limits on a joint federal return. diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_capital_gain.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_capital_gain.py index c831d312a9a..6096dfc0a57 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_capital_gain.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_capital_gain.py @@ -16,8 +16,8 @@ class form_4952_net_capital_gain(Variable): gains." As on line 4d, every capital asset is treated as property held for investment and only the head's and spouse's gains count. Schedule D distributions already belong to long_term_capital_gains, so - distributions reported without Schedule D are added once. Capital loss - carryovers have no model input and are not modeled. + distributions reported without Schedule D are added once. The long_term_capital_loss_carryover input is a memo amount already + reflected in long_term_capital_gains; it is not subtracted again. This is also Schedule D Tax Worksheet line 4, so the capital gains tax takes a Form 4952 line 4g election from the same amount. It does not read diff --git a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_investment_interest_expense_deduction_indiv.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_investment_interest_expense_deduction_indiv.py index 921f38a0f72..20dce55bd99 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_investment_interest_expense_deduction_indiv.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_investment_interest_expense_deduction_indiv.py @@ -28,7 +28,9 @@ class mt_investment_interest_expense_deduction_indiv(Variable): From 2024, Montana uses federal itemized deductions. This variable preserves the allocation of the federal deduction by filer interest - shares; Montana no longer permits separate filing on the same return. + shares. When no current interest was paid, the head carries any allowed + federal carryover, matching the joint-return reporting convention. + Montana no longer permits separate filing on the same return. """ def formula(person, period, parameters): @@ -73,6 +75,13 @@ def formula_2024(person, period, parameters): out=np.zeros_like(interest), where=total_interest > 0, ) + # Preserve a carryover-only federal deduction using the existing + # joint-return reporting convention, without assigning ownership. + interest_share = where( + total_interest > 0, + interest_share, + person("is_tax_unit_head", period), + ) return ( person.tax_unit("investment_interest_expense_deduction", period) * interest_share From 045f7aef3fd7e36668188f4d09c93d890a741b6f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 06:47:27 -0400 Subject: [PATCH 08/11] Keep dependent investment fees out of filer miscellaneous deductions --- ...st_investment_interest_limit_properties.py | 2 +- .../investment_interest/form_4952.yaml | 2 ++ .../itemized/ar_misc_deduction_joint.yaml | 24 ++++++++++++++++++ .../itemized/ny_misc_deduction.yaml | 25 +++++++++++++++++++ .../itemizing/total_misc_deductions.py | 13 +++++++++- 5 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_misc_deduction.yaml diff --git a/policyengine_us/tests/core/test_investment_interest_limit_properties.py b/policyengine_us/tests/core/test_investment_interest_limit_properties.py index 59939dfc89e..f5a48bc4fda 100644 --- a/policyengine_us/tests/core/test_investment_interest_limit_properties.py +++ b/policyengine_us/tests/core/test_investment_interest_limit_properties.py @@ -177,7 +177,7 @@ def _restatement(year, raw): expenses = (raw["investment_expenses"] * filer).sum(axis=1) if year == 2017: schedule_a_line_24 = ( - raw["investment_expenses"].sum(axis=1) + expenses + raw["unreimbursed_business_employee_expenses"].sum(axis=1) + raw["tax_preparation_fees"].sum(axis=1) ) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml index c5c404bded9..e62f15dd259 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml @@ -385,5 +385,7 @@ members: [head, child] adjusted_gross_income: 100_000 output: + total_misc_deductions: 3_000 # Only the filer investment fees belong on Schedule A. + misc_deduction: 1_000 form_4952_investment_expenses: 1_000 # Filer pool 3,000 - 2% * 100,000. investment_interest_expense_deduction: 11_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/deductions/itemized/ar_misc_deduction_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/deductions/itemized/ar_misc_deduction_joint.yaml index 65ea639360d..bb6e14f6558 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/deductions/itemized/ar_misc_deduction_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/deductions/itemized/ar_misc_deduction_joint.yaml @@ -78,3 +78,27 @@ ar_misc_deduction_joint: 1_500 # AR3 line 25 = 3,500 - 2% * 100,000 misc_deduction: 0 # Federal section 67(h) disallows miscellaneous deductions after 2017 form_4952_investment_expenses: 0 # Form 4952 line 5 = 0 when federal misc.applies is false + +- name: AR excludes dependent investment fees from the filer miscellaneous pool + period: 2025 + input: + people: + head: + age: 45 + is_tax_unit_head: true + investment_expenses: 3_000 + child: + age: 12 + is_tax_unit_dependent: true + investment_expenses: 50_000 + tax_units: + unit: + members: [head, child] + ar_agi_joint: 100_000 + households: + household: + members: [head, child] + state_code: AR + output: + total_misc_deductions: 3_000 + ar_misc_deduction_joint: 1_000 # Filer fees 3,000 - 2% * AGI 100,000. diff --git a/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_misc_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_misc_deduction.yaml new file mode 100644 index 00000000000..7d119f000cf --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ny/tax/income/taxable_income/deductions/itemized/ny_misc_deduction.yaml @@ -0,0 +1,25 @@ +# Investment fees belong only to the filer whose income they produce. + +- name: NY excludes dependent investment fees from the filer miscellaneous pool + period: 2025 + input: + people: + head: + age: 45 + is_tax_unit_head: true + investment_expenses: 3_000 + child: + age: 12 + is_tax_unit_dependent: true + investment_expenses: 50_000 + tax_units: + unit: + members: [head, child] + adjusted_gross_income: 100_000 + households: + household: + members: [head, child] + state_code: NY + output: + total_misc_deductions: 3_000 + ny_misc_deduction: 1_000 # Filer fees 3,000 - 2% * AGI 100,000. diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/total_misc_deductions.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/total_misc_deductions.py index cf6ed57e03b..f4af65bd6f3 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/total_misc_deductions.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/total_misc_deductions.py @@ -9,4 +9,15 @@ class total_misc_deductions(Variable): definition_period = YEAR reference = "https://www.law.cornell.edu/uscode/text/26/67#b" - adds = "gov.irs.deductions.itemized.misc.sources" + def formula(tax_unit, period, parameters): + sources = parameters(period).gov.irs.deductions.itemized.misc.sources + # Investment fees belong to the filer's return; preserve the existing + # aggregation of the other miscellaneous expense sources. + return tax_unit_non_dep_add( + tax_unit, + period, + sources, + include_dependents=tuple( + source for source in sources if source != "investment_expenses" + ), + ) From e0bf2023b68bea98876eb478ecc5085d2ae4d20d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 07:09:32 -0400 Subject: [PATCH 09/11] Cover allowed investment interest in Virginia deduction limits --- .../va_reduced_itemized_deductions.yaml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/va_reduced_itemized_deductions.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/va_reduced_itemized_deductions.yaml index 80257f26b5c..ddbf71b9103 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/va_reduced_itemized_deductions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/va_reduced_itemized_deductions.yaml @@ -40,3 +40,29 @@ state_code: VA output: va_reduced_itemized_deductions: 35_282 + +- name: Virginia excludes allowed investment interest from its 80 percent limitation base + period: 2021 + absolute_error_margin: 0.01 + input: + state_code: VA + filing_status: SINGLE + adjusted_gross_income: 5_000_000 + investment_interest_expense: 10_000 + taxable_interest_income: 2_000 + deductible_mortgage_interest: 20_000 + state_and_local_sales_or_income_tax: 0 + real_estate_taxes: 0 + medical_expense_deduction: 0 + charitable_deduction: 0 + casualty_loss_deduction: 0 + gambling_losses: 0 + output: + investment_interest_expense_deduction: 2_000 + interest_deduction: 22_000 + # Schedule A worksheet line 2 excludes allowed investment interest from + # line 9; line 4 limits the reduction to 80% of the remaining deductions. + # https://www.tax.virginia.gov/sites/default/files/taxforms/individual-income-tax/2021/schedule-2021.pdf#page=2 + # 22,000 - 80% * (22,000 - 2,000) = 6,000; the 3% AGI limb is larger. + # Excluding raw 10,000 paid interest would incorrectly yield 12,400. + va_reduced_itemized_deductions: 6_000 From 05228167642374813d0afbdf4c0aa2cf559de7aa Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 07:26:36 -0400 Subject: [PATCH 10/11] Place Arkansas AGI on the filer in dependent fee regression --- .../tax/income/deductions/itemized/ar_misc_deduction_joint.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/deductions/itemized/ar_misc_deduction_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/deductions/itemized/ar_misc_deduction_joint.yaml index bb6e14f6558..affda507fab 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/deductions/itemized/ar_misc_deduction_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/deductions/itemized/ar_misc_deduction_joint.yaml @@ -87,6 +87,7 @@ age: 45 is_tax_unit_head: true investment_expenses: 3_000 + ar_agi_joint: 100_000 child: age: 12 is_tax_unit_dependent: true @@ -94,7 +95,6 @@ tax_units: unit: members: [head, child] - ar_agi_joint: 100_000 households: household: members: [head, child] From a0d4dce2e4fcc149da257978e643f1a32061d5f8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 08:59:50 -0400 Subject: [PATCH 11/11] Exclude dependent specialty gains and losses from filer worksheets --- .../investment-interest-163d-limit.fixed.md | 2 +- ...st_investment_interest_limit_properties.py | 179 +++++++++++++++++- .../capital_gains_28_percent_rate_gain.py | 16 +- 3 files changed, 187 insertions(+), 10 deletions(-) diff --git a/changelog.d/investment-interest-163d-limit.fixed.md b/changelog.d/investment-interest-163d-limit.fixed.md index 80f52b0af2c..071be49db1d 100644 --- a/changelog.d/investment-interest-163d-limit.fixed.md +++ b/changelog.d/investment-interest-163d-limit.fixed.md @@ -1 +1 @@ -Limit itemized investment interest under 26 U.S.C. 163(d) through Form 4952, support explicit carryovers, exclude dependent investments, and align California, New York, Virginia, and Montana deductions, including Montana separate-return limits before 2024. +Limit itemized investment interest under 26 U.S.C. 163(d) through Form 4952, support explicit carryovers, exclude dependent investments, and align California, New York, Virginia, and Montana deductions, including Montana separate-return limits before 2024. Exclude dependents' specialty capital gains and losses from filers' Schedule D rate worksheets. diff --git a/policyengine_us/tests/core/test_investment_interest_limit_properties.py b/policyengine_us/tests/core/test_investment_interest_limit_properties.py index f5a48bc4fda..04ecb8c170b 100644 --- a/policyengine_us/tests/core/test_investment_interest_limit_properties.py +++ b/policyengine_us/tests/core/test_investment_interest_limit_properties.py @@ -43,6 +43,13 @@ "tax_preparation_fees", "deductible_mortgage_interest", ) +SPECIALTY_PERSON_INPUTS = ( + "long_term_capital_gains_on_collectibles", + "long_term_capital_gains_on_small_business_stock", + "collectibles_gain_or_loss", + "section_1202_gain", + "long_term_capital_loss_carryover", +) LINES = { "3": "form_4952_total_investment_interest_expense", "4a": "form_4952_gross_investment_income", @@ -97,8 +104,12 @@ def _draw(seed): return raw -def _simulate(year, raw): +def _simulate(year, raw, extra_outputs=()): period = str(year) + person_inputs = ( + *PERSON_INPUTS, + *(name for name in SPECIALTY_PERSON_INPUTS if name in raw), + ) people, tax_units, households, marital_units = {}, {}, {}, {} for i in range(len(raw["adjusted_gross_income"])): members, spouses = [], [] @@ -112,7 +123,7 @@ def _simulate(year, raw): "is_tax_unit_head": {period: role == "head"}, "is_tax_unit_spouse": {period: role == "spouse"}, "is_tax_unit_dependent": {period: role == "dependent"}, - **{name: {period: float(raw[name][i, j])} for name in PERSON_INPUTS}, + **{name: {period: float(raw[name][i, j])} for name in person_inputs}, } if role == "dependent": marital_units[f"m{i}_dependent"] = {"members": [person]} @@ -127,6 +138,10 @@ def _simulate(year, raw): period: float(raw["carryover"][i]) }, } + if "unrecaptured_section_1250_gain_before_losses" in raw: + tax_units[f"t{i}"]["unrecaptured_section_1250_gain_before_losses"] = { + period: float(raw["unrecaptured_section_1250_gain_before_losses"][i]) + } households[f"h{i}"] = {"members": members} simulation = Simulation( situation={ @@ -144,6 +159,7 @@ def _simulate(year, raw): "dividend_income_reduced_by_investment_income", "dwks09", "dwks10", + *extra_outputs, ) return {name: simulation.calculate(name, period) for name in outputs} @@ -230,7 +246,11 @@ def _scale(raw): return ( raw["adjusted_gross_income"] + raw["carryover"] - + sum(np.abs(raw[name]).sum(axis=1) for name in PERSON_INPUTS) + + sum( + np.abs(raw[name]).sum(axis=1) + for name in (*PERSON_INPUTS, *SPECIALTY_PERSON_INPUTS) + if name in raw + ) ) @@ -327,18 +347,169 @@ def test_dependent_investments_do_not_change_filer_form_4952(year): if name != "deductible_mortgage_interest": changed[name][:, 2] += 50_000 combined = {key: np.concatenate([base[key], changed[key]], axis=0) for key in base} - actual = _simulate(year, combined) + actual = _simulate(year, combined, extra_outputs=("adjusted_net_capital_gain",)) scale = _scale(combined) for name in ( *LINES.values(), "interest_deduction", "net_capital_gain", + "adjusted_net_capital_gain", "dividend_income_reduced_by_investment_income", "dwks09", "dwks10", ): _close(actual[name][N:], actual[name][:N], scale[:N] + scale[N:], name) + # Isolate each specialty worksheet component from the miscellaneous + # Schedule A expenses above. Both single and joint filers have the same + # aggregate amounts, including spouse gains offsetting the head's losses. + dependent_cases = ( + ("long_term_capital_gains_on_collectibles", 10_000), + ("long_term_capital_gains_on_small_business_stock", 10_000), + ("collectibles_gain_or_loss", 10_000), + ("collectibles_gain_or_loss", -10_000), + ("section_1202_gain", 10_000), + ("short_term_capital_gains", -10_000), + ("short_term_capital_gains", 10_000), + ("long_term_capital_loss_carryover", 10_000), + ) + joint = np.tile([False, True], 2 * len(dependent_cases)) + excess_losses = np.repeat([False, True], 2 * len(dependent_cases)) + size = len(joint) + present = np.column_stack( + (np.ones(size, dtype=bool), joint, np.ones(size, dtype=bool)) + ) + filers = present.copy() + filers[:, 2] = False + specialty_base = { + "present": present, + "filers": filers, + "adjusted_gross_income": np.full(size, 150_000), + "carryover": np.zeros(size), + "unrecaptured_section_1250_gain_before_losses": np.full(size, 10_000), + **{ + name: np.zeros((size, len(ROLES))) + for name in (*PERSON_INPUTS, *SPECIALTY_PERSON_INPUTS) + }, + } + for name, single, head, spouse in ( + ("long_term_capital_gains", 30_000, 20_000, 10_000), + ("short_term_capital_gains", -2_000, -4_000, 2_000), + ("collectibles_gain_or_loss", 7_000, 10_000, -3_000), + ("section_1202_gain", 4_000, 3_000, 1_000), + ("long_term_capital_loss_carryover", 2_000, 1_000, 1_000), + ): + specialty_base[name][:, 0] = np.where(joint, head, single) + specialty_base[name][:, 1] = np.where(joint, spouse, 0) + # The second regime has a 3,000 excess loss that reduces section 1250 + # gain. A dependent's positive gains cannot absorb the filer's loss. + specialty_base["collectibles_gain_or_loss"][excess_losses, 0] -= 10_000 + specialty_changed = {key: value.copy() for key, value in specialty_base.items()} + for regime in range(2): + for i, (name, amount) in enumerate(dependent_cases): + start = 2 * (regime * len(dependent_cases) + i) + rows = slice(start, start + 2) + specialty_changed[name][rows, 2] = amount + if name not in ( + "short_term_capital_gains", + "long_term_capital_loss_carryover", + ): + specialty_changed["long_term_capital_gains"][rows, 2] = amount + specialty_combined = { + key: np.concatenate([specialty_base[key], specialty_changed[key]], axis=0) + for key in specialty_base + } + specialty_actual = _simulate( + year, + specialty_combined, + extra_outputs=( + "capital_gains_28_percent_rate_gain", + "schedule_d_unrecaptured_section_1250_gain", + "adjusted_net_capital_gain", + "income_tax_main_rates", + "capital_gains_tax", + ), + ) + specialty_scale = _scale(specialty_combined) + # Schedule D line 18: 7,000 + 4,000 - 2,000 - 2,000 = 7,000. + # Line 19 remains 10,000; adjusted net capital gain is + # 30,000 - 2,000 - 7,000 - 10,000 = 11,000 for either filing status. + # With collectibles lower by 10,000, line 18 is zero, line 19 is + # 10,000 - 3,000 = 7,000, and adjusted net capital gain is 21,000. + for name, expected in ( + ("capital_gains_28_percent_rate_gain", np.where(excess_losses, 0, 7_000)), + ( + "schedule_d_unrecaptured_section_1250_gain", + np.where(excess_losses, 7_000, 10_000), + ), + ("adjusted_net_capital_gain", np.where(excess_losses, 21_000, 11_000)), + ): + _close( + specialty_actual[name], + np.tile(expected, 2), + specialty_scale, + name, + ) + regular_tax = ( + specialty_actual["income_tax_main_rates"] + + specialty_actual["capital_gains_tax"] + ) + _close( + regular_tax[size:], + regular_tax[:size], + specialty_scale[:size] + specialty_scale[size:], + "regular tax with dependent specialty gains or losses", + ) + + +def test_dependent_collectibles_do_not_change_2025_filer_regular_tax(): + simulation = Simulation( + situation={ + "people": { + "filer": { + "age": {"2025": 45}, + "is_tax_unit_head": {"2025": True}, + "employment_income": {"2025": 140_000}, + "long_term_capital_gains": {"2025": 10_000}, + }, + "dependent": { + "age": {"2025": 12}, + "is_tax_unit_dependent": {"2025": True}, + "long_term_capital_gains": {"2025": 10_000}, + "long_term_capital_gains_on_collectibles": {"2025": 10_000}, + }, + }, + "tax_units": { + "return": { + "members": ["filer", "dependent"], + "filing_status": {"2025": "SINGLE"}, + } + }, + "households": {"household": {"members": ["filer", "dependent"]}}, + "marital_units": { + "filer": {"members": ["filer"]}, + "dependent": {"members": ["dependent"]}, + }, + } + ) + regular_tax = ( + simulation.calculate("income_tax_main_rates", "2025")[0] + + simulation.calculate("capital_gains_tax", "2025")[0] + ) + np.testing.assert_allclose( + [ + regular_tax, + simulation.calculate("adjusted_gross_income", "2025")[0], + simulation.calculate("standard_deduction", "2025")[0], + simulation.calculate("taxable_income", "2025")[0], + simulation.calculate("adjusted_net_capital_gain", "2025")[0], + simulation.calculate("alternative_minimum_tax", "2025")[0], + ], + [24_167, 150_000, 15_750, 134_250, 10_000, 0], + rtol=RTOL, + atol=ATOL, + ) + @pytest.mark.parametrize("year", YEARS) def test_carryforward_is_available_and_conserved(year): diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/capital_gains_28_percent_rate_gain.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/capital_gains_28_percent_rate_gain.py index 596b96ba092..efa177d7dd2 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/capital_gains_28_percent_rate_gain.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/capital_gains_28_percent_rate_gain.py @@ -13,15 +13,21 @@ def rate_gains_less_losses(tax_unit, period): Worksheet combined. Amounts entered already netted, as Schedule D lines 18 and 19 report them, are not part of it. """ + # These taxpayer-specific worksheets include only the head and spouse's + # gains and losses, as on their Schedule D and Form 4952. # Collectibles gain or (loss); a net collectibles loss is negative. - collectibles = add(tax_unit, period, ["collectibles_gain_or_loss"]) - section_1202_gain = max_(0, add(tax_unit, period, ["section_1202_gain"])) + collectibles = tax_unit_non_dep_add(tax_unit, period, ["collectibles_gain_or_loss"]) + section_1202_gain = max_( + 0, tax_unit_non_dep_add(tax_unit, period, ["section_1202_gain"]) + ) # 26 U.S.C. 1222(6), the same net short-term capital loss net_capital_gain # subtracts: Schedule D line 7, if a loss. net_short_term_capital_loss = max_( - 0, -add(tax_unit, period, ["short_term_capital_gains"]) + 0, -tax_unit_non_dep_add(tax_unit, period, ["short_term_capital_gains"]) + ) + carryover = max_( + 0, tax_unit_non_dep_add(tax_unit, period, ["long_term_capital_loss_carryover"]) ) - carryover = max_(0, add(tax_unit, period, ["long_term_capital_loss_carryover"])) return collectibles + section_1202_gain - net_short_term_capital_loss - carryover @@ -51,7 +57,7 @@ class capital_gains_28_percent_rate_gain(Variable): ] def formula(tax_unit, period, parameters): - reported = add( + reported = tax_unit_non_dep_add( tax_unit, period, [