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..071be49db1d --- /dev/null +++ b/changelog.d/investment-interest-163d-limit.fixed.md @@ -0,0 +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. Exclude dependents' specialty capital gains and losses from filers' Schedule D rate worksheets. 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/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/core/test_investment_interest_limit_properties.py b/policyengine_us/tests/core/test_investment_interest_limit_properties.py new file mode 100644 index 00000000000..04ecb8c170b --- /dev/null +++ b/policyengine_us/tests/core/test_investment_interest_limit_properties.py @@ -0,0 +1,563 @@ +"""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-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, +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. 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 +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", + "schedule_d_capital_gain_distributions", + "unreimbursed_business_employee_expenses", + "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", + "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), + "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"): + 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, 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 = [], [] + 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])}, + "form_4952_disallowed_investment_interest_expense_prior_year": { + 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={ + "people": people, + "tax_units": tax_units, + "households": households, + "marital_units": marital_units, + } + ) + outputs = ( + *LINES.values(), + "interest_deduction", + "misc_deduction", + "net_capital_gain", + "dividend_income_reduced_by_investment_income", + "dwks09", + "dwks10", + *extra_outputs, + ) + 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"] * filer).sum(axis=1) + raw["carryover"] + 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"] * 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"] * filer).sum(axis=1)), + line_4b + line_4e, + ) + line_4h = line_4a - line_4b + line_4d - line_4e + line_4g + expenses = (raw["investment_expenses"] * filer).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"]), + ) + 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) + 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"] + + raw["carryover"] + + sum( + np.abs(raw[name]).sum(axis=1) + for name in (*PERSON_INPUTS, *SPECIALTY_PERSON_INPUTS) + if name in raw + ) + ) + + +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) + + +@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, 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): + 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 new file mode 100644 index 00000000000..e62f15dd259 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952.yaml @@ -0,0 +1,391 @@ +# 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-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 + +- 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 + 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) + 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 Form 4952 excludes dependent income and investment debt + 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_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,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 + 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 + +- 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_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 + +- 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: + 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 f862cd1d699..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 @@ -63,3 +63,42 @@ 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 + +- 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 + ar_agi_joint: 100_000 + child: + age: 12 + is_tax_unit_dependent: true + investment_expenses: 50_000 + tax_units: + unit: + members: [head, child] + 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/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..0d30504e2d7 --- /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=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 +# 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 separate investment interest limits handle zero debt and spouse-only debt + 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] + # 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] + # 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] + +- 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_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..5deb09e3b0f --- /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,164 @@ +# 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] + +- 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/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/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/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/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 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 effde0252ef..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 @@ -6,8 +6,31 @@ 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." + 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" + 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-prior/f4952--2025.pdf", + ] - adds = ["deductible_interest_expense"] + def formula(tax_unit, period, parameters): + interest_paid = add(tax_unit, period, ["deductible_interest_expense"]) + investment_interest_paid = add( + tax_unit, period, ["investment_interest_expense"] + ) + 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_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 new file mode 100644 index 00000000000..38f68a41cd1 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_elected_investment_income.py @@ -0,0 +1,39 @@ +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 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-prior/f4952--2025.pdf#page=3", + ] + + def formula(tax_unit, period, parameters): + election = max_( + 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) + 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..e9a914ce99e --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_gross_investment_income.py @@ -0,0 +1,41 @@ +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-prior/f4952--2025.pdf#page=3", + ] + + def formula(tax_unit, period, parameters): + return tax_unit_non_dep_add( + tax_unit, period, ["taxable_interest_income", "ordinary_dividend_income"] + ) 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..fbd978f1227 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_expenses.py @@ -0,0 +1,48 @@ +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", 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 + 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-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 = tax_unit_non_dep_add(tax_unit, period, ["investment_expenses"]) + 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/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..767ca8e8a0b --- /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-prior/f4952--2025.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..b56a97a3956 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_investment_interest_carryforward.py @@ -0,0 +1,28 @@ +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". 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-prior/f4952--2025.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..6096dfc0a57 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_capital_gain.py @@ -0,0 +1,41 @@ +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 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. 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 + 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-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 = 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 new file mode 100644 index 00000000000..105bf3c0ea1 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_net_investment_gain.py @@ -0,0 +1,36 @@ +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. 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. + 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 = [ + "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-prior/f4952--2025.pdf#page=3", + ] + + def formula(tax_unit, period, parameters): + 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 new file mode 100644 index 00000000000..5bb5f62f0ee --- /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-prior/f4952--2025.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..8ad348e51e7 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/itemizing/investment_interest/form_4952_qualified_dividends.py @@ -0,0 +1,24 @@ +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-prior/f4952--2025.pdf#page=3", + ] + + def formula(tax_unit, period, parameters): + 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 new file mode 100644 index 00000000000..cd467b0ec1d --- /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,32 @@ +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": 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_1", + "https://www.law.cornell.edu/uscode/text/26/163#d_2", + "https://www.law.cornell.edu/uscode/text/26/163#d_3", + "https://www.irs.gov/pub/irs-prior/f4952--2025.pdf", + ] + + 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 new file mode 100644 index 00000000000..6f229d8af1e --- /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". 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-prior/f4952--2025.pdf#page=4", + ] + + 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/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" + ), + ) 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..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 @@ -27,12 +27,12 @@ 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. + # 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. Cap this part at + # net_capital_gain to preserve a directly supplied net gain amount. qualified_dividend_income = min_( - max_(0, add(tax_unit, period, ["qualified_dividend_income"])), + max_(0, tax_unit("dividend_income_reduced_by_investment_income", period)), net_capital_gain, ) unrecaptured_s_1250_gain = tax_unit( 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, [ 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 d6011f76864..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,29 +36,31 @@ 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 - # 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, + tax_unit("form_4952_qualified_dividends", period) - 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..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 @@ -18,35 +18,35 @@ 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 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, 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"]) + 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 - # 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)) + schedule_d_line_16 = schedule_d_line_15 + 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..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 @@ -10,7 +10,14 @@ 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"]) - gain = max_(0, tax_unit("net_capital_gain", period) - qualified_dividends) + # Schedule D Tax Worksheet line 6: the qualified dividends net_capital_gain + # 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, 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 3cfc0d25094..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 @@ -33,12 +33,12 @@ 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. + # 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. A directly supplied net gain + # can contain fewer dividends than this independently computed line. qualified_dividends = min_( - max_(0, add(tax_unit, period, ["qualified_dividend_income"])), + max_(0, tax_unit("dividend_income_reduced_by_investment_income", period)), net_capital_gain, ) excess = tax_unit("section_911_capital_gain_excess", period) 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..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,8 +11,23 @@ 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=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" + 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." + + 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): p = parameters(period).gov.irs.deductions @@ -20,8 +35,9 @@ 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) + investment_interest = person( + "mt_investment_interest_expense_deduction_indiv", 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/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..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,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=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 + 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_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..20dce55bd99 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_investment_interest_expense_deduction_indiv.py @@ -0,0 +1,88 @@ +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. 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): + 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, + ) + # 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 + ) 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..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,8 +11,23 @@ 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=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" + 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." + + 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): p = parameters(period).gov.states.mt.tax.income.deductions.itemized @@ -22,8 +37,9 @@ 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) + investment_interest = person( + "mt_investment_interest_expense_deduction_indiv", 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_joint.py b/policyengine_us/variables/gov/states/mt/tax/income/deductions/itemized/general/mt_itemized_deductions_joint.py index a3328b0290d..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,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=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 + 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 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..445f36d15b9 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-prior/f4952--2025.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