From a066f38fcda88415d9f2ee3d144ef18434d26787 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 07:34:12 -0400 Subject: [PATCH 1/7] Use the filers' Schedule D for the tax on net capital gain Net capital gain (26 U.S.C. 1222(11), 1(h)(11)), the Schedule D Tax Worksheet and the 28% rate gain summed long- and short-term gains, capital gain distributions, qualified dividends, collectibles gain and Form 4952 elections over every tax unit member. Adjusted gross income leaves out tax unit dependents, whose items are on their own returns (2025 Form 8814, line 4), so a dependent's gain or dividends let the filer's ordinary income be taxed at capital gain rates. The rate side also added a negative capital gain distributions input, which AGI floors at zero (Form 1099-DIV box 2a). A new helper, filer_schedule_d_lines, gives the head and spouse's Schedule D lines 7, 15 and 16 with distributions floored per person. It now feeds filer_loss_limited_net_capital_gains (Form 1040 line 7a), has_qdiv_or_ltcg, net_capital_gain and the worksheet's Form 4952 lines, so the AGI side and the rate side read the same lines. Qualified dividends, the Form 4952 election and the 28% rate gain inputs are summed over the head and spouse with tax_unit_non_dep_add. Co-Authored-By: Claude Opus 5.5 --- ...g-filer-gains-floor-distributions.fixed.md | 1 + .../capital_gains_28_percent_rate_gain.yaml | 59 +++ .../capital_gains/net_capital_gain.yaml | 231 +++++++++++ .../schedule_d_tax_worksheet_form_4952.yaml | 26 ++ ...dule_d_unrecaptured_section_1250_gain.yaml | 19 + .../test_schedule_d_filer_net_capital_gain.py | 366 ++++++++++++++++++ .../filer_loss_limited_net_capital_gains.py | 20 +- .../irs_gross_income/has_qdiv_or_ltcg.py | 32 +- .../adjusted_net_capital_gain.py | 8 +- .../capital_gains_28_percent_rate_gain.py | 21 +- ...end_income_reduced_by_investment_income.py | 5 +- .../capital_gains/filer_schedule_d_lines.py | 49 +++ .../capital_gains/net_capital_gain.py | 36 +- ...chedule_d_tax_worksheet_form_4952_lines.py | 25 +- ...1_net_capital_gain_other_than_dividends.py | 5 +- .../section_911_qualified_dividend_income.py | 8 +- .../capital_gains/non_sch_d_capital_gains.py | 4 +- 17 files changed, 848 insertions(+), 67 deletions(-) create mode 100644 changelog.d/ncg-filer-gains-floor-distributions.fixed.md create mode 100644 policyengine_us/tests/test_schedule_d_filer_net_capital_gain.py create mode 100644 policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py diff --git a/changelog.d/ncg-filer-gains-floor-distributions.fixed.md b/changelog.d/ncg-filer-gains-floor-distributions.fixed.md new file mode 100644 index 00000000000..9ab3acf7f75 --- /dev/null +++ b/changelog.d/ncg-filer-gains-floor-distributions.fixed.md @@ -0,0 +1 @@ +Figure net capital gain, the Schedule D Tax Worksheet and the 28% rate gain from the head and spouse's Schedule D and qualified dividends only, leaving out tax unit dependents' gains, losses, dividends and collectibles gain as adjusted gross income does, and treat a negative capital gain distributions input as zero there too. diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/capital_gains_28_percent_rate_gain.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/capital_gains_28_percent_rate_gain.yaml index 9d1375d0ee1..533f0e4c682 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/capital_gains_28_percent_rate_gain.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/capital_gains_28_percent_rate_gain.yaml @@ -179,3 +179,62 @@ capital_gains_28_percent_rate_gain: 610_988.38 schedule_d_unrecaptured_section_1250_gain: 208_514.11 adjusted_net_capital_gain: 290_510.39 + +- name: A dependent's collectibles gain is on the dependent's own return + period: 2025 + input: + people: + head: + age: 45 + long_term_capital_gains: 10_000 + child: + age: 12 + is_tax_unit_dependent: true + long_term_capital_gains: 5_000 + collectibles_gain_or_loss: 5_000 + tax_units: + tax_unit: + members: [head, child] + output: + # The head's worksheet has no collectibles gain. + capital_gains_28_percent_rate_gain: 0 + net_capital_gain: 10_000 + adjusted_net_capital_gain: 10_000 + +- name: A dependent's Schedule D line 18 amounts are on the dependent's own return + period: 2025 + input: + people: + head: + age: 45 + long_term_capital_gains: 10_000 + child: + age: 12 + is_tax_unit_dependent: true + long_term_capital_gains: 5_000 + long_term_capital_gains_on_collectibles: 2_000 + long_term_capital_gains_on_small_business_stock: 1_000 + tax_units: + tax_unit: + members: [head, child] + output: + capital_gains_28_percent_rate_gain: 0 + +- name: A dependent's short-term loss does not reduce the head's 28 percent rate gain + period: 2025 + input: + people: + head: + age: 45 + long_term_capital_gains: 50_000 + collectibles_gain_or_loss: 10_000 + child: + age: 12 + is_tax_unit_dependent: true + short_term_capital_gains: -8_000 + tax_units: + tax_unit: + members: [head, child] + output: + # Line 6 is the head's Schedule D line 7 loss: none. + capital_gains_28_percent_rate_gain: 10_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/net_capital_gain.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/net_capital_gain.yaml index 734071018ad..2b21b868a8b 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/net_capital_gain.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/net_capital_gain.yaml @@ -12,3 +12,234 @@ members: [person1] output: net_capital_gain: 10_500 + +# 26 U.S.C. 1222(11): net capital gain is the net long-term capital gain over +# the net short-term capital loss, Schedule D lines 15 and 7 of the 2025 +# Schedule D. Section 1(h)(11)(A) adds qualified dividend income. These are +# amounts on the head and spouse's return. A tax unit dependent's gains, +# losses and dividends are on the dependent's own return (2025 Form 8814, line +# 4), as in adjusted gross income. + +- name: A dependent's long-term gain is not on the head's Schedule D + period: 2025 + input: + people: + head: + age: 40 + employment_income: 60_000 + long_term_capital_gains: 10_000 + child: + age: 10 + is_tax_unit_dependent: true + long_term_capital_gains: 20_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: TX + output: + # Schedule D line 15 = 10,000 (the head's), line 16 = 10,000. + net_capital_gain: 10_000 + adjusted_net_capital_gain: 10_000 + dwks09: 10_000 + dwks10: 10_000 + has_qdiv_or_ltcg: true + # The child's gain is not in gross income either: 60,000 + 10,000. + adjusted_gross_income: 70_000 + # Head of household: 70,000 - 23,625 standard deduction. + taxable_income: 46_375 + # Ordinary income 36,375: 10% x 17,000 + 12% x 19,375 = 1,700 + 2,325. + # The 10,000 gain is below the 64,750 zero-rate maximum, so 0% on it. + # Counting the child's gain too (30,000) gave 1,637.50. + income_tax_before_credits: 4_025 + +- name: A dependent's long-term gain, filing single + period: 2025 + input: + people: + head: + age: 40 + employment_income: 60_000 + long_term_capital_gains: 10_000 + child: + age: 10 + is_tax_unit_dependent: true + long_term_capital_gains: 20_000 + tax_units: + tax_unit: + members: [head, child] + filing_status: SINGLE + households: + household: + members: [head, child] + state_code: TX + output: + net_capital_gain: 10_000 + # 70,000 - 15,750 standard deduction. + taxable_income: 54_250 + # Ordinary income 44,250: 10% x 11,925 + 12% x 32,325 = 1,192.50 + + # 3,879. Of the 10,000 gain, 48,350 - 44,250 = 4,100 is at 0% and 5,900 + # at 15% (885). Counting the child's gain too (30,000) gave 3,556.50. + income_tax_before_credits: 5_956.5 + +- name: A dependent's short-term loss does not reduce the head's net capital gain + period: 2025 + input: + people: + head: + age: 40 + long_term_capital_gains: 10_000 + child: + age: 10 + is_tax_unit_dependent: true + short_term_capital_gains: -8_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: TX + output: + # The head's Schedule D line 7 is 0 and line 15 is 10,000. + net_capital_gain: 10_000 + dwks09: 10_000 + adjusted_gross_income: 10_000 + +- name: Spouses' gains and losses net, and a dependent's stay off the return + period: 2025 + input: + people: + head: + age: 40 + long_term_capital_gains: 10_000 + spouse: + age: 40 + short_term_capital_gains: -4_000 + child: + age: 10 + is_tax_unit_dependent: true + long_term_capital_gains: 20_000 + short_term_capital_gains: -1_000 + non_sch_d_capital_gains: 1_000 + tax_units: + tax_unit: + members: [head, spouse, child] + households: + household: + members: [head, spouse, child] + state_code: TX + output: + # Joint Schedule D: line 7 = (4,000), line 15 = 10,000, line 16 = 6,000. + # 1222(11): 10,000 - 4,000. + net_capital_gain: 6_000 + dwks09: 6_000 + adjusted_gross_income: 6_000 + +- name: A dependent's qualified dividends are not on the head's return + period: 2025 + input: + people: + head: + age: 45 + employment_income: 150_000 + long_term_capital_gains: 10_000 + child: + age: 12 + is_tax_unit_dependent: true + qualified_dividend_income: 20_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: TX + output: + # Form 1040 line 3a is the head's: 0. + dividend_income_reduced_by_investment_income: 0 + net_capital_gain: 10_000 + adjusted_net_capital_gain: 10_000 + adjusted_gross_income: 160_000 + # Head of household: 160,000 - 23,625 = 136,375; ordinary income 126,375: + # 1,700 + 12% x 47,850 + 22% x 38,500 + 24% x 23,025 = 1,700 + 5,742 + + # 8,470 + 5,526; the 10,000 gain at 15% (1,500). Counting the child's + # dividends too taxed 20,000 of the head's income at 15% rather than 24% + # (21,138). + income_tax_before_credits: 22_938 + +- name: A dependent's qualified dividends alone do not call for the worksheet + period: 2025 + input: + people: + head: + age: 45 + employment_income: 60_000 + child: + age: 12 + is_tax_unit_dependent: true + qualified_dividend_income: 5_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: TX + output: + # No qualified dividends on Form 1040 line 3a and no Schedule D. + has_qdiv_or_ltcg: false + net_capital_gain: 0 + +- name: A dependent's investment income election is on the dependent's own Form 4952 + period: 2025 + input: + people: + head: + age: 45 + long_term_capital_gains: 10_000 + qualified_dividend_income: 2_000 + child: + age: 12 + is_tax_unit_dependent: true + investment_income_elected_form_4952: 500 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: TX + output: + # The head elects nothing: 10,000 + 2,000. + net_capital_gain: 12_000 + +- name: A negative distributions input counts as zero in net capital gain + period: 2025 + input: + people: + head: + age: 45 + employment_income: 100_000 + long_term_capital_gains: 10_000 + non_sch_d_capital_gains: -2_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: TX + output: + # Form 1099-DIV box 2a, total capital gain distributions, is not + # negative. Schedule D line 13 = 0, line 15 = 10,000, as in AGI. + adjusted_gross_income: 110_000 + net_capital_gain: 10_000 + dwks09: 10_000 + # Taxable income 110,000 - 15,750 = 94,250; ordinary income 84,250: + # 1,192.50 + 12% x 36,550 + 22% x 35,775 = 1,192.50 + 4,386 + 7,870.50; + # the 10,000 gain at 15% (1,500). With the input subtracted (8,000) it + # was 15,089. + income_tax_before_credits: 14_949 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml index 55bc9ed9711..88e746089ee 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml @@ -293,3 +293,29 @@ # 48,350; line 29 = 533,400 - 48,350 = 485,050; line 30 = 197,800; line # 31 = 29,670; line 38 = 29,670. Before the fix: 24,532.50. amt_tax_including_cg: 29_670 + +- name: A dependent's gains are not in the head's worksheet or Form 4952 + period: 2025 + input: + people: + head: + age: 45 + long_term_capital_gains: 2_000 + qualified_dividend_income: 6_000 + investment_income_elected_form_4952: 5_000 + child: + age: 12 + is_tax_unit_dependent: true + long_term_capital_gains: 10_000 + qualified_dividend_income: 1_000 + tax_units: + tax_unit: + members: [head, child] + output: + # Line 2 = 6,000; line 3 = 5,000; line 4 = min(2,000, 2,000) = 2,000; + # line 5 = 3,000; line 6 = 3,000. + dividend_income_reduced_by_investment_income: 3_000 + # Line 7 = min(2,000, 2,000); line 8 = min(5,000, 2,000); line 9 = 0. + dwks09: 0 + dwks10: 3_000 + net_capital_gain: 3_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_unrecaptured_section_1250_gain.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_unrecaptured_section_1250_gain.yaml index 768fd73f9a1..34895c401af 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_unrecaptured_section_1250_gain.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_unrecaptured_section_1250_gain.yaml @@ -138,3 +138,22 @@ output: capital_gains_28_percent_rate_gain: 0 schedule_d_unrecaptured_section_1250_gain: 20_000 + +- name: A dependent's short-term loss does not reduce the head's line 19 + period: 2025 + input: + people: + head: + age: 45 + long_term_capital_gains: 50_000 + child: + age: 12 + is_tax_unit_dependent: true + short_term_capital_gains: -6_000 + tax_units: + tax_unit: + members: [head, child] + unrecaptured_section_1250_gain_before_losses: 20_000 + output: + # Line 15 is the head's Schedule D line 7 loss: none, so line 18 = 20,000. + schedule_d_unrecaptured_section_1250_gain: 20_000 diff --git a/policyengine_us/tests/test_schedule_d_filer_net_capital_gain.py b/policyengine_us/tests/test_schedule_d_filer_net_capital_gain.py new file mode 100644 index 00000000000..0af83ab9b64 --- /dev/null +++ b/policyengine_us/tests/test_schedule_d_filer_net_capital_gain.py @@ -0,0 +1,366 @@ +"""Property tests: the tax on net capital gain uses the filers' Schedule D. + +26 U.S.C. 1(h) taxes a net capital gain at lower rates. Net capital gain is +the net long-term capital gain over the net short-term capital loss (26 U.S.C. +1222(11)), plus qualified dividend income (1(h)(11)(A)). On the return these +are Schedule D lines 15 and 16 (2025 Schedule D) and Form 1040 line 3a, and +the Schedule D Tax Worksheet starts from "the smaller of line 15 or line 16" +(2025 Instructions for Schedule D, page 15). Capital gain distributions +reported without Schedule D go on line 13, inside line 15 (2025 Instructions +for Schedule D, "Capital Gain Distributions"). Form 1099-DIV box 2a reports +them, so they are never negative. + +A tax unit dependent's gains, losses and dividends are on the dependent's own +return (2025 Form 8814, line 4), and adjusted gross income leaves them out. +So for every combination of the filers' long-term (LT) and short-term (ST) +gains and losses, distributions (D) and qualified dividends (Q), and anything +the tax unit's dependents have: + +1. net_capital_gain = max(0, min(line 15, line 16)) + Q, where line 15 is + LT + max(0, D) and line 16 is line 15 + ST, over the head and spouse. +2. The Schedule D Tax Worksheet agrees: line 9 (dwks09) is + max(0, min(line 15, line 16)), line 6 is Q, and line 10 (dwks10) equals + net_capital_gain, which figures the same amount from 1222(11). This is a + differential check between the two paths. +3. has_qdiv_or_ltcg is true exactly when Q > 0 or lines 15 and 16 are both + more than zero (Form 1040 line 16 instructions). +4. Form 1040 line 7a (filer_loss_limited_net_capital_gains) is line 16, or + the loss limit if line 16 is a larger loss, from the same lines. +5. Changing a dependent's capital gains, losses, distributions, dividends, + collectibles gain or Form 4952 election changes none of the filers' tax + amounts. +6. A negative distributions input gives the same results as zero. +7. Net capital gain never falls when a filer's LT, ST, D or Q rises, and + rises by no more than the increase. + +Amounts are whole dollars of at most $1,000,000, so every sum is exact in +single precision and the comparisons are exact. +""" + +import itertools + +import numpy as np +import pytest + +from policyengine_us import Simulation + +YEAR = 2025 + +FILER_INPUTS = { + "long_term": "long_term_capital_gains", + "short_term": "short_term_capital_gains", + "distributions": "non_sch_d_capital_gains", + "dividends": "qualified_dividend_income", +} +DEPENDENT_INPUTS = { + **FILER_INPUTS, + "collectibles": "collectibles_gain_or_loss", + "line_18_collectibles": "long_term_capital_gains_on_collectibles", + "election": "investment_income_elected_form_4952", +} + +# The head and spouse's amounts, which must not depend on a dependent's. +TAX_OUTPUTS = [ + "net_capital_gain", + "adjusted_net_capital_gain", + "dwks09", + "dwks10", + "dividend_income_reduced_by_investment_income", + "has_qdiv_or_ltcg", + "capital_gains_28_percent_rate_gain", + "schedule_d_unrecaptured_section_1250_gain", + "filer_loss_limited_net_capital_gains", + "adjusted_gross_income", + "taxable_income", + "capital_gains_tax", + "income_tax_main_rates", + "alternative_minimum_tax", + "income_tax_before_credits", +] + + +def schedule_d_lines(h): + """Schedule D lines 15 and 16 and Form 1040 line 3a of the filers.""" + filers = h["filers"] + line_15 = sum(p["long_term"] + max(0, p["distributions"]) for p in filers) + line_16 = line_15 + sum(p["short_term"] for p in filers) + dividends = sum(p["dividends"] for p in filers) + return line_15, line_16, dividends + + +def build_situation(households): + """One tax unit per household: a head, an optional spouse and any + dependents, each dependent in a marital unit of their own.""" + people, tax_units, marital_units, households_out = {}, {}, {}, {} + for i, h in enumerate(households): + filers, members = [], [] + for j, p in enumerate(h["filers"]): + name = f"filer_{i}_{j}" + people[name] = { + "age": {YEAR: 45}, + "is_tax_unit_head": {YEAR: j == 0}, + "is_tax_unit_spouse": {YEAR: j == 1}, + "is_tax_unit_dependent": {YEAR: False}, + "employment_income": {YEAR: h["wages"] if j == 0 else 0}, + **{v: {YEAR: p[k]} for k, v in FILER_INPUTS.items()}, + } + filers.append(name) + for j, p in enumerate(h["dependents"]): + name = f"dependent_{i}_{j}" + people[name] = { + "age": {YEAR: 10}, + "is_tax_unit_head": {YEAR: False}, + "is_tax_unit_spouse": {YEAR: False}, + "is_tax_unit_dependent": {YEAR: True}, + "employment_income": {YEAR: 0}, + **{v: {YEAR: p.get(k, 0)} for k, v in DEPENDENT_INPUTS.items()}, + } + marital_units[f"dependent_marital_unit_{i}_{j}"] = {"members": [name]} + members.append(name) + members = filers + members + tax_units[f"tax_unit_{i}"] = {"members": members} + marital_units[f"marital_unit_{i}"] = {"members": filers} + households_out[f"household_{i}"] = { + "members": members, + "state_code": {YEAR: "TX"}, + } + return { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + "households": households_out, + } + + +def calculate(households): + simulation = Simulation(situation=build_situation(households)) + results = {v: np.asarray(simulation.calculate(v, YEAR)) for v in TAX_OUTPUTS} + results["filing_status"] = np.asarray( + simulation.calculate("filing_status", YEAR).decode_to_str() + ) + return results + + +def assert_properties(households): + model = calculate(households) + for i, h in enumerate(households): + line_15, line_16, dividends = schedule_d_lines(h) + gain = max(0, min(line_15, line_16)) + # 1. 26 U.S.C. 1222(11) and 1(h)(11)(A) on the filers' return. + assert model["net_capital_gain"][i] == gain + dividends, h + # 2. The Schedule D Tax Worksheet, lines 6, 9 and 10. + assert model["dividend_income_reduced_by_investment_income"][i] == ( + dividends + ), h + assert model["dwks09"][i] == gain, h + assert model["dwks10"][i] == model["net_capital_gain"][i], h + # 3. Form 1040 line 16 routing. + assert bool(model["has_qdiv_or_ltcg"][i]) == ( + dividends > 0 or (line_15 > 0 and line_16 > 0) + ), h + # 4. Form 1040 line 7a, Schedule D line 16 or line 21. + limit = 1_500 if model["filing_status"][i] == "SEPARATE" else 3_000 + assert model["filer_loss_limited_net_capital_gains"][i] == max( + -limit, line_16 + ), h + # No collectibles, section 1202 or section 1250 gain on the return. + assert model["adjusted_net_capital_gain"][i] == model["net_capital_gain"][i], h + + +def without_dependents_amounts(households): + return [{**h, "dependents": [{} for _ in h["dependents"]]} for h in households] + + +def assert_dependents_change_nothing(households): + """5. The filers' amounts with the dependents' inputs and without them.""" + with_amounts = calculate(households) + without_amounts = calculate(without_dependents_amounts(households)) + for variable in TAX_OUTPUTS: + assert np.array_equal(with_amounts[variable], without_amounts[variable]), ( + variable, + households, + ) + + +# --------------------------------------------------------------------------- +# A deterministic grid (runs without Hypothesis). +# --------------------------------------------------------------------------- + +GRID_GAINS = [-12_000, -3_000, 0, 4_000, 25_000] +GRID_DISTRIBUTIONS = [-2_000, 0, 3_000] +GRID_DEPENDENT = { + "long_term": 20_000, + "short_term": -8_000, + "distributions": 1_000, + "dividends": 5_000, + "collectibles": 4_000, + "line_18_collectibles": 2_000, + "election": 3_000, +} + + +def grid_households(): + households = [] + for lt, st_, d, q in itertools.product( + GRID_GAINS, GRID_GAINS, GRID_DISTRIBUTIONS, [0, 2_000] + ): + filer = {"long_term": lt, "short_term": st_, "distributions": d, "dividends": q} + none = {"long_term": 0, "short_term": 0, "distributions": 0, "dividends": 0} + households.append( + {"wages": 60_000, "filers": [filer], "dependents": [GRID_DEPENDENT]} + ) + # The same totals split between spouses, with two dependents. + households.append( + { + "wages": 150_000, + "filers": [ + {**none, "long_term": lt, "distributions": d}, + {**none, "short_term": st_, "dividends": q}, + ], + "dependents": [GRID_DEPENDENT, {"long_term": -5_000}], + } + ) + return households + + +def test_grid(): + households = grid_households() + assert_properties(households) + assert_dependents_change_nothing(households) + + +def test_reported_case(): + """A single filer with wages 60,000 and a 10,000 long-term gain, and a + dependent child with a 20,000 long-term gain: the child's gain is on the + child's own return, so net capital gain is 10,000, not 30,000.""" + h = { + "wages": 60_000, + "filers": [ + {"long_term": 10_000, "short_term": 0, "distributions": 0, "dividends": 0} + ], + "dependents": [{"long_term": 20_000}], + } + model = calculate([h]) + assert model["net_capital_gain"][0] == 10_000 + assert model["dwks10"][0] == 10_000 + assert model["adjusted_gross_income"][0] == 70_000 + + +# --------------------------------------------------------------------------- +# Hypothesis. +# --------------------------------------------------------------------------- + +# Hypothesis is a dev extra; skip rather than fail collection without it. +hypothesis = pytest.importorskip("hypothesis") +st = pytest.importorskip("hypothesis.strategies") + +AMOUNT = st.integers(min_value=-1_000_000, max_value=1_000_000) +SMALL = st.integers(min_value=-20_000, max_value=20_000) +NON_NEGATIVE = st.one_of( + st.just(0), + st.integers(min_value=0, max_value=20_000), + st.integers(min_value=0, max_value=1_000_000), +) +# Distributions inputs, including negative ones, which count as zero. +DISTRIBUTIONS = st.one_of(NON_NEGATIVE, st.integers(min_value=-50_000, max_value=-1)) +WAGES = st.sampled_from([0, 40_000, 150_000, 600_000]) + + +@st.composite +def filer(draw): + amounts = draw(st.sampled_from([AMOUNT, SMALL])) + return { + "long_term": draw(st.one_of(st.just(0), amounts)), + "short_term": draw(st.one_of(st.just(0), amounts)), + "distributions": draw(DISTRIBUTIONS), + "dividends": draw(NON_NEGATIVE), + } + + +@st.composite +def dependent(draw): + return { + "long_term": draw(st.one_of(st.just(0), SMALL, AMOUNT)), + "short_term": draw(st.one_of(st.just(0), SMALL, AMOUNT)), + "distributions": draw(DISTRIBUTIONS), + "dividends": draw(NON_NEGATIVE), + "collectibles": draw(st.one_of(st.just(0), SMALL)), + "line_18_collectibles": draw(st.one_of(st.just(0), NON_NEGATIVE)), + "election": draw(st.one_of(st.just(0), NON_NEGATIVE)), + } + + +@st.composite +def household(draw): + return { + "wages": draw(WAGES), + "filers": [draw(filer()) for _ in range(draw(st.integers(1, 2)))], + "dependents": [draw(dependent()) for _ in range(draw(st.integers(0, 2)))], + } + + +@hypothesis.settings(max_examples=40, deadline=None) +@hypothesis.given(st.lists(household(), min_size=1, max_size=8)) +def test_properties(households): + assert_properties(households) + + +@hypothesis.settings(max_examples=30, deadline=None) +@hypothesis.given(st.lists(household(), min_size=1, max_size=8)) +def test_dependents_change_nothing_on_the_filers_return(households): + assert_dependents_change_nothing(households) + + +@hypothesis.settings(max_examples=30, deadline=None) +@hypothesis.given(st.lists(household(), min_size=1, max_size=8)) +def test_negative_distributions_count_as_zero(households): + """6. Flooring each filer's distributions at zero changes nothing.""" + floored = [ + { + **h, + "filers": [ + {**p, "distributions": max(0, p["distributions"])} for p in h["filers"] + ], + } + for h in households + ] + model = calculate(households) + reference = calculate(floored) + for variable in TAX_OUTPUTS: + assert np.array_equal(model[variable], reference[variable]), variable + + +@hypothesis.settings(max_examples=30, deadline=None) +@hypothesis.given( + st.lists(household(), min_size=1, max_size=8), + st.sampled_from(sorted(FILER_INPUTS)), + st.integers(min_value=1, max_value=50_000), +) +def test_net_capital_gain_rises_with_the_filers_gains(households, item, extra): + """7. Net capital gain is non-decreasing in each of the head's LT, ST, + D (once it is not negative) and Q, and rises by at most the increase.""" + households = [ + { + **h, + "filers": [ + { + **h["filers"][0], + "distributions": max(0, h["filers"][0]["distributions"]), + } + ] + + h["filers"][1:], + } + for h in households + ] + raised = [ + { + **h, + "filers": [{**h["filers"][0], item: h["filers"][0][item] + extra}] + + h["filers"][1:], + } + for h in households + ] + base = calculate(households)["net_capital_gain"] + more = calculate(raised)["net_capital_gain"] + assert (more >= base).all() + assert (more - base <= extra).all() diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/filer_loss_limited_net_capital_gains.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/filer_loss_limited_net_capital_gains.py index 120ca814d65..26a6f3fca81 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/filer_loss_limited_net_capital_gains.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/filer_loss_limited_net_capital_gains.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.irs.tax.federal_income.capital_gains.filer_schedule_d_lines import ( + filer_schedule_d_lines, +) class filer_loss_limited_net_capital_gains(Variable): @@ -25,15 +28,8 @@ def formula(tax_unit, period, parameters): p = parameters(period).gov.irs filing_status = tax_unit("filing_status", period) loss_limit = p.capital_gains.loss_limit[filing_status] - net_capital_gains = tax_unit_non_dep_add( - tax_unit, period, ["long_term_capital_gains", "short_term_capital_gains"] - ) - # Capital gain distributions go on Schedule D line 13 with the other - # gains and losses, so they net before the limit. Each filer's input - # is floored at zero, as in irs_gross_income. - person = tax_unit.members - not_dependent = ~person("is_tax_unit_dependent", period) - distributions = tax_unit.sum( - not_dependent * max_(0, person("non_sch_d_capital_gains", period)) - ) - return max_(-loss_limit, net_capital_gains + distributions) + # Schedule D line 16 of the head and spouse. Capital gain + # distributions go on line 13 with the other gains and losses, so + # they net before the limit (line 21). + lines = filer_schedule_d_lines(tax_unit, period) + return max_(-loss_limit, lines.line_16) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/has_qdiv_or_ltcg.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/has_qdiv_or_ltcg.py index ac66f03c566..2109e5f3861 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/has_qdiv_or_ltcg.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/has_qdiv_or_ltcg.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.irs.tax.federal_income.capital_gains.filer_schedule_d_lines import ( + filer_schedule_d_lines, +) class has_qdiv_or_ltcg(Variable): @@ -7,8 +10,8 @@ class has_qdiv_or_ltcg(Variable): label = "Has qualified dividends or long-term capital gains" documentation = ( "Whether this tax unit figures its tax with the Qualified Dividends " - "and Capital Gain Tax Worksheet or the Schedule D Tax Worksheet: it " - "has qualified dividends, or the head and spouse's Schedule D lines " + "and Capital Gain Tax Worksheet or the Schedule D Tax Worksheet: the " + "head and spouse have qualified dividends, or their Schedule D lines " "15 and 16 are both more than zero. Capital gain distributions are on " "Schedule D line 13, so a filer who reports them without Schedule D " "has lines 15 and 16 equal to them." @@ -26,22 +29,11 @@ class has_qdiv_or_ltcg(Variable): ) def formula(tax_unit, period, parameters): - qualified_dividends = add(tax_unit, period, ["qualified_dividend_income"]) - # Schedule D lines 15 and 16 of the head and spouse; a tax unit - # dependent's gains and losses are on the dependent's own return. - person = tax_unit.members - not_dependent = ~person("is_tax_unit_dependent", period) - distributions = tax_unit.sum( - not_dependent * max_(0, person("non_sch_d_capital_gains", period)) + # Form 1040 line 3a and Schedule D lines 15 and 16 of the head and + # spouse; a tax unit dependent's dividends, gains and losses are on + # the dependent's own return. + qualified_dividends = tax_unit_non_dep_add( + tax_unit, period, ["qualified_dividend_income"] ) - # Line 15: net long-term capital gain or loss, including capital gain - # distributions (line 13). - line_15 = ( - tax_unit_non_dep_add(tax_unit, period, ["long_term_capital_gains"]) - + distributions - ) - # Line 16: line 15 plus the net short-term gain or loss. - line_16 = line_15 + tax_unit_non_dep_add( - tax_unit, period, ["short_term_capital_gains"] - ) - return (qualified_dividends > 0) | ((line_15 > 0) & (line_16 > 0)) + lines = filer_schedule_d_lines(tax_unit, period) + return (qualified_dividends > 0) | ((lines.line_15 > 0) & (lines.line_16 > 0)) 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..54c784f8aa2 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 @@ -30,9 +30,13 @@ def formula(tax_unit, period, parameters): # 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. + # the two amounts. As in net_capital_gain, a tax unit dependent's + # dividends are on the dependent's own return. qualified_dividend_income = min_( - max_(0, add(tax_unit, period, ["qualified_dividend_income"])), + max_( + 0, + tax_unit_non_dep_add(tax_unit, period, ["qualified_dividend_income"]), + ), 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..093ee4ee01a 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 @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.irs.tax.federal_income.capital_gains.filer_schedule_d_lines import ( + filer_schedule_d_lines, +) def rate_gains_less_losses(tax_unit, period): @@ -12,16 +15,24 @@ def rate_gains_less_losses(tax_unit, period): The same amount is lines 14 to 16 of the Unrecaptured Section 1250 Gain Worksheet combined. Amounts entered already netted, as Schedule D lines 18 and 19 report them, are not part of it. + + The worksheets are the head and spouse's: a tax unit dependent's gains + and losses are on the dependent's own return. """ # 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, -filer_schedule_d_lines(tax_unit, period).line_7 + ) + 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 +62,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..bb0af428d9d 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 @@ -27,8 +27,9 @@ 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: qualified dividends (Form 1040 line 3a), the head and + # spouse's; a tax unit dependent's are on the dependent's own return. + line_2 = tax_unit_non_dep_add(tax_unit, period, ["qualified_dividend_income"]) 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/filer_schedule_d_lines.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py new file mode 100644 index 00000000000..5f534abf533 --- /dev/null +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py @@ -0,0 +1,49 @@ +from collections import namedtuple + +from policyengine_us.model_api import * + +FilerScheduleDLines = namedtuple( + "FilerScheduleDLines", ["line_7", "line_15", "line_16"] +) + + +def filer_schedule_d_lines(tax_unit, period): + """Schedule D (Form 1040) lines 7, 15 and 16 of the head and spouse, shared + by the capital loss limit (filer_loss_limited_net_capital_gains), the + Form 1040 line 16 routing (has_qdiv_or_ltcg), net_capital_gain and the + Schedule D Tax Worksheet. + + Line 7 is the net short-term capital gain or loss. Line 15 is the net + long-term capital gain or loss, including capital gain distributions + (line 13). Line 16 is lines 7 and 15 combined (2025 Schedule D, lines 7, + 13, 15 and 16). + + A tax unit dependent's gains, losses and distributions belong on the + dependent's own return, so they are left out, as irs_gross_income leaves + them out of adjusted gross income. + + Capital gain distributions reported without Schedule D + (non_sch_d_capital_gains) are long-term capital gains (26 U.S.C. + 852(b)(3)(B)) and go on line 13 when the filer files Schedule D (2025 + Instructions for Schedule D, "Capital Gain Distributions"). They are Form + 1099-DIV box 2a amounts, which are not negative, so each filer's input is + floored at zero here, as irs_gross_income floors it. + """ + person = tax_unit.members + not_dependent = ~person("is_tax_unit_dependent", period) + distributions = tax_unit.sum( + not_dependent * max_(0, person("non_sch_d_capital_gains", period)) + ) + line_7 = tax_unit_non_dep_add(tax_unit, period, ["short_term_capital_gains"]) + line_15 = ( + tax_unit_non_dep_add(tax_unit, period, ["long_term_capital_gains"]) + + distributions + ) + # Line 16: lines 7 and 15 combined. + line_16 = ( + tax_unit_non_dep_add( + tax_unit, period, ["long_term_capital_gains", "short_term_capital_gains"] + ) + + distributions + ) + return FilerScheduleDLines(line_7=line_7, line_15=line_15, line_16=line_16) 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..d75a8d36ee4 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 @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.irs.tax.federal_income.capital_gains.filer_schedule_d_lines import ( + filer_schedule_d_lines, +) class net_capital_gain(Variable): @@ -31,18 +34,25 @@ class net_capital_gain(Variable): title="2025 Form 4952, line 4g instructions", href="https://www.irs.gov/pub/irs-prior/f4952--2025.pdf#page=4", ), + dict( + title="2025 Schedule D (Form 1040), lines 7, 13 and 15", + href="https://www.irs.gov/pub/irs-prior/f1040sd--2025.pdf#page=1", + ), + dict( + title="2025 Form 8814, line 4 (a child's income is on the child's own return)", + href="https://www.irs.gov/pub/irs-prior/f8814--2025.pdf#page=1", + ), ] 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"], - ) - short_term_loss = max_(0, -add(tax_unit, period, ["short_term_capital_gains"])) + # The head and spouse's Schedule D. Capital gain distributions, + # including those reported without Schedule D (Form 1040 line 7a), are + # long-term capital gains under IRC 852(b)(3)(B), on line 15. A tax + # unit dependent's gains, losses and dividends are on the dependent's + # own return. + lines = filer_schedule_d_lines(tax_unit, period) + long_term_gain = lines.line_15 + short_term_loss = max_(0, -lines.line_7) # 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 @@ -53,12 +63,16 @@ def formula(tax_unit, period, parameters): # 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"]) + 0, + tax_unit_non_dep_add( + tax_unit, period, ["investment_income_elected_form_4952"] + ), ) gain_elected = min_(election, gain) dividends_elected = election - gain_elected qualified_dividends = max_( 0, - add(tax_unit, period, ["qualified_dividend_income"]) - dividends_elected, + tax_unit_non_dep_add(tax_unit, period, ["qualified_dividend_income"]) + - dividends_elected, ) return 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..0bdbdbf6d05 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 @@ -1,6 +1,9 @@ from collections import namedtuple from policyengine_us.model_api import * +from policyengine_us.variables.gov.irs.tax.federal_income.capital_gains.filer_schedule_d_lines import ( + filer_schedule_d_lines, +) ScheduleDTaxWorksheetForm4952Lines = namedtuple( "ScheduleDTaxWorksheetForm4952Lines", ["line_3", "line_4", "line_7"] @@ -31,22 +34,22 @@ def schedule_d_tax_worksheet_form_4952_lines(tax_unit, period): (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. + + The worksheet and Form 4952 are the head and spouse's: a tax unit + dependent's gains, losses and election are on the dependent's own return. """ - election = max_(0, add(tax_unit, period, ["investment_income_elected_form_4952"])) - long_term_gains = add(tax_unit, period, ["long_term_capital_gains"]) - short_term_gains = add(tax_unit, period, ["short_term_capital_gains"]) - distributions = add(tax_unit, period, ["non_sch_d_capital_gains"]) - # Schedule D line 15 (net long-term gain or loss, with line 13) and line - # 16 (Schedule D line 7 plus line 15). - schedule_d_line_15 = long_term_gains + distributions - schedule_d_line_16 = tax_unit("net_capital_gains", period) + distributions + election = max_( + 0, + tax_unit_non_dep_add(tax_unit, period, ["investment_income_elected_form_4952"]), + ) + lines = filer_schedule_d_lines(tax_unit, period) # Form 4952 line 4d: total gains over total losses, if any. - net_gain = max_(0, schedule_d_line_16) + net_gain = max_(0, lines.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)) + net_capital_gain = max_(0, lines.line_15 - max_(0, -lines.line_7)) return ScheduleDTaxWorksheetForm4952Lines( line_3=election, line_4=min_(net_gain, net_capital_gain), - line_7=min_(schedule_d_line_15, schedule_d_line_16), + line_7=min_(lines.line_15, lines.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..6ff53f5835f 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,10 @@ 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"]) + # The head and spouse's qualified dividends, as in net_capital_gain. + qualified_dividends = tax_unit_non_dep_add( + tax_unit, period, ["qualified_dividend_income"] + ) gain = max_(0, tax_unit("net_capital_gain", period) - qualified_dividends) excess = tax_unit("section_911_capital_gain_excess", period) return where(excess > 0, max_(0, gain - excess), gain) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.py b/policyengine_us/variables/gov/irs/tax/federal_income/foreign_earned_income_exclusion/section_911_qualified_dividend_income.py index 3cfc0d25094..926cb5265d0 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 @@ -36,9 +36,13 @@ def formula(tax_unit, period, parameters): # 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. + # two amounts. A tax unit dependent's dividends are on the + # dependent's own return. qualified_dividends = min_( - max_(0, add(tax_unit, period, ["qualified_dividend_income"])), + max_( + 0, + tax_unit_non_dep_add(tax_unit, period, ["qualified_dividend_income"]), + ), net_capital_gain, ) excess = tax_unit("section_911_capital_gain_excess", period) diff --git a/policyengine_us/variables/household/income/person/capital_gains/non_sch_d_capital_gains.py b/policyengine_us/variables/household/income/person/capital_gains/non_sch_d_capital_gains.py index ad4d944c6b7..01dcc3e90fe 100644 --- a/policyengine_us/variables/household/income/person/capital_gains/non_sch_d_capital_gains.py +++ b/policyengine_us/variables/household/income/person/capital_gains/non_sch_d_capital_gains.py @@ -12,7 +12,9 @@ class non_sch_d_capital_gains(Variable): "or other capital gains enters them on Schedule D line 13, so the " "model nets them with those gains and losses before the capital loss " "limit (loss_limited_net_capital_gains, " - "capital_losses_allowed_against_gains and limited_capital_loss)." + "capital_losses_allowed_against_gains and limited_capital_loss). " + "Form 1099-DIV box 2a is never negative, so adjusted gross income and " + "the tax on net capital gain treat a negative entry as zero." ) reference = dict( title="2025 Instructions for Schedule D (Form 1040), Capital Gain Distributions", From 7591596170b04eba613f00fdfde515e9d30f5e2b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 07:42:04 -0400 Subject: [PATCH 2/7] Trim the Schedule D property tests' Hypothesis examples Keeps the remaining pytest group's added time small: the file now takes about 50 s of CPU locally (6 tests, 1.4 GB peak footprint). Co-Authored-By: Claude Opus 5.5 --- .../tests/test_schedule_d_filer_net_capital_gain.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/policyengine_us/tests/test_schedule_d_filer_net_capital_gain.py b/policyengine_us/tests/test_schedule_d_filer_net_capital_gain.py index 0af83ab9b64..bc4ab6ff3f9 100644 --- a/policyengine_us/tests/test_schedule_d_filer_net_capital_gain.py +++ b/policyengine_us/tests/test_schedule_d_filer_net_capital_gain.py @@ -299,19 +299,19 @@ def household(draw): } -@hypothesis.settings(max_examples=40, deadline=None) +@hypothesis.settings(max_examples=30, deadline=None) @hypothesis.given(st.lists(household(), min_size=1, max_size=8)) def test_properties(households): assert_properties(households) -@hypothesis.settings(max_examples=30, deadline=None) +@hypothesis.settings(max_examples=20, deadline=None) @hypothesis.given(st.lists(household(), min_size=1, max_size=8)) def test_dependents_change_nothing_on_the_filers_return(households): assert_dependents_change_nothing(households) -@hypothesis.settings(max_examples=30, deadline=None) +@hypothesis.settings(max_examples=20, deadline=None) @hypothesis.given(st.lists(household(), min_size=1, max_size=8)) def test_negative_distributions_count_as_zero(households): """6. Flooring each filer's distributions at zero changes nothing.""" @@ -330,7 +330,7 @@ def test_negative_distributions_count_as_zero(households): assert np.array_equal(model[variable], reference[variable]), variable -@hypothesis.settings(max_examples=30, deadline=None) +@hypothesis.settings(max_examples=20, deadline=None) @hypothesis.given( st.lists(household(), min_size=1, max_size=8), st.sampled_from(sorted(FILER_INPUTS)), From 2b7bf4f050d67026cb1a9fc691bd7c35dd5a18a7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 08:02:30 -0400 Subject: [PATCH 3/7] Keep a supplied net_capital_gains in Schedule D line 16 Line 16 of the filers' Schedule D now starts from the tax unit net_capital_gains and takes out dependents' person-level gains and losses, as #9796 does for Form 1040 line 7a. The Schedule D Tax Worksheet read net_capital_gains on main, so a tax unit amount supplied there keeps reaching worksheet line 7. filer_loss_limited_net_capital_gains goes back to main's formula, so this PR no longer edits the file #9796 and #9840 change. Co-Authored-By: Claude Opus 5.5 --- .../filer_loss_limited_net_capital_gains.py | 20 +++++++++------- .../capital_gains/filer_schedule_d_lines.py | 24 +++++++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/filer_loss_limited_net_capital_gains.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/filer_loss_limited_net_capital_gains.py index 26a6f3fca81..120ca814d65 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/filer_loss_limited_net_capital_gains.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/filer_loss_limited_net_capital_gains.py @@ -1,7 +1,4 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.irs.tax.federal_income.capital_gains.filer_schedule_d_lines import ( - filer_schedule_d_lines, -) class filer_loss_limited_net_capital_gains(Variable): @@ -28,8 +25,15 @@ def formula(tax_unit, period, parameters): p = parameters(period).gov.irs filing_status = tax_unit("filing_status", period) loss_limit = p.capital_gains.loss_limit[filing_status] - # Schedule D line 16 of the head and spouse. Capital gain - # distributions go on line 13 with the other gains and losses, so - # they net before the limit (line 21). - lines = filer_schedule_d_lines(tax_unit, period) - return max_(-loss_limit, lines.line_16) + net_capital_gains = tax_unit_non_dep_add( + tax_unit, period, ["long_term_capital_gains", "short_term_capital_gains"] + ) + # Capital gain distributions go on Schedule D line 13 with the other + # gains and losses, so they net before the limit. Each filer's input + # is floored at zero, as in irs_gross_income. + person = tax_unit.members + not_dependent = ~person("is_tax_unit_dependent", period) + distributions = tax_unit.sum( + not_dependent * max_(0, person("non_sch_d_capital_gains", period)) + ) + return max_(-loss_limit, net_capital_gains + distributions) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py index 5f534abf533..a0ceb6be6f3 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py @@ -9,9 +9,8 @@ def filer_schedule_d_lines(tax_unit, period): """Schedule D (Form 1040) lines 7, 15 and 16 of the head and spouse, shared - by the capital loss limit (filer_loss_limited_net_capital_gains), the - Form 1040 line 16 routing (has_qdiv_or_ltcg), net_capital_gain and the - Schedule D Tax Worksheet. + by the Form 1040 line 16 routing (has_qdiv_or_ltcg), net_capital_gain, the + Schedule D Tax Worksheet and the 28% Rate Gain Worksheet. Line 7 is the net short-term capital gain or loss. Line 15 is the net long-term capital gain or loss, including capital gain distributions @@ -20,7 +19,10 @@ def filer_schedule_d_lines(tax_unit, period): A tax unit dependent's gains, losses and distributions belong on the dependent's own return, so they are left out, as irs_gross_income leaves - them out of adjusted gross income. + them out of adjusted gross income. Line 16 starts from net_capital_gains, + so a tax unit amount supplied there is kept; that amount is read as + covering every member, and any dependent's person-level gains and losses + are then taken out. Capital gain distributions reported without Schedule D (non_sch_d_capital_gains) are long-term capital gains (26 U.S.C. @@ -30,9 +32,9 @@ def filer_schedule_d_lines(tax_unit, period): floored at zero here, as irs_gross_income floors it. """ person = tax_unit.members - not_dependent = ~person("is_tax_unit_dependent", period) + dependent = person("is_tax_unit_dependent", period) distributions = tax_unit.sum( - not_dependent * max_(0, person("non_sch_d_capital_gains", period)) + ~dependent * max_(0, person("non_sch_d_capital_gains", period)) ) line_7 = tax_unit_non_dep_add(tax_unit, period, ["short_term_capital_gains"]) line_15 = ( @@ -40,10 +42,12 @@ def filer_schedule_d_lines(tax_unit, period): + distributions ) # Line 16: lines 7 and 15 combined. - line_16 = ( - tax_unit_non_dep_add( - tax_unit, period, ["long_term_capital_gains", "short_term_capital_gains"] + dependent_gains = tax_unit.sum( + dependent + * ( + person("long_term_capital_gains", period) + + person("short_term_capital_gains", period) ) - + distributions ) + line_16 = tax_unit("net_capital_gains", period) - dependent_gains + distributions return FilerScheduleDLines(line_7=line_7, line_15=line_15, line_16=line_16) From cf9a4037cf4a594cd02042a50a234e87378f42ea Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 08:30:49 -0400 Subject: [PATCH 4/7] Use the filers' amounts in Vermont's and Hawaii's capital gain lines Review r1 found that Vermont's flat capital gains exclusion took every member's qualified dividends out of the now filer-only adjusted_net_capital_gain. A dependent's 3,000 of dividends cut a 4,000 exclusion to 1,000 (Vermont tax 936.33 to 1,036.82). The dividends are now the head and spouse's, as in the federal amount. Hawaii's Tax on Capital Gains Worksheet took the smaller of the filers' net capital gain (line 7) and every member's long-term gains (line 4), so a dependent's loss shrank the head's line 8. Line 4 is now the head and spouse's too. The property tests also give dependents section 1202 gain, a long-term loss carryover and Schedule D line 18 small business stock gain, and no longer claim the grid runs without Hypothesis. Co-Authored-By: Claude Opus 5.5 --- ...hi_taxable_income_for_alternative_tax.yaml | 25 ++++++++++++++++++ .../agi/vt_capital_gains_exclusion.yaml | 26 +++++++++++++++++++ .../test_schedule_d_filer_net_capital_gain.py | 14 ++++++++-- .../hi_taxable_income_for_alternative_tax.py | 7 +++-- .../vt_capital_gains_exclusion.py | 8 ++++-- 5 files changed, 74 insertions(+), 6 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/alternative_tax/hi_taxable_income_for_alternative_tax.yaml b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/alternative_tax/hi_taxable_income_for_alternative_tax.yaml index 30407465981..be4e9312c13 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/alternative_tax/hi_taxable_income_for_alternative_tax.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/alternative_tax/hi_taxable_income_for_alternative_tax.yaml @@ -41,3 +41,28 @@ net_capital_gain: 50_000 output: hi_taxable_income_for_alternative_tax: 48_000 + +- name: A dependent's long-term loss does not reduce the head's worksheet line 4 + period: 2025 + input: + people: + head: + age: 45 + long_term_capital_gains: 10_000 + child: + age: 10 + is_tax_unit_dependent: true + long_term_capital_gains: -5_000 + tax_units: + tax_unit: + members: [head, child] + filing_status: SINGLE + hi_taxable_income: 200_000 + households: + household: + members: [head, child] + state_code: HI + output: + # Line 4 = 10,000 (the head's) and line 7 = 10,000, so line 8 = 10,000: + # 200,000 - 10,000, above the capped taxable income. + hi_taxable_income_for_alternative_tax: 190_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/vt_capital_gains_exclusion.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/vt_capital_gains_exclusion.yaml index 435ba557c67..403e00d94bc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/vt_capital_gains_exclusion.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/vt_capital_gains_exclusion.yaml @@ -84,3 +84,29 @@ output: # Only flat $5,000 applies (financial instruments are NOT eligible for 40%) vt_capital_gains_exclusion: 5_000 + +- name: A dependent's qualified dividends do not reduce the head's flat exclusion + period: 2025 + input: + people: + head: + age: 45 + employment_income: 50_000 + long_term_capital_gains: 4_000 + child: + age: 10 + is_tax_unit_dependent: true + qualified_dividend_income: 3_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: VT + output: + # The head's adjusted net capital gain is 4,000 and has no qualified + # dividends; the child's 3,000 is on the child's own return. + adjusted_net_capital_gain: 4_000 + # min(4,000, 5,000); 40% of taxable income (54,000 - 23,625) is 12,150. + vt_capital_gains_exclusion: 4_000 diff --git a/policyengine_us/tests/test_schedule_d_filer_net_capital_gain.py b/policyengine_us/tests/test_schedule_d_filer_net_capital_gain.py index bc4ab6ff3f9..373e3a759d5 100644 --- a/policyengine_us/tests/test_schedule_d_filer_net_capital_gain.py +++ b/policyengine_us/tests/test_schedule_d_filer_net_capital_gain.py @@ -27,7 +27,8 @@ 4. Form 1040 line 7a (filer_loss_limited_net_capital_gains) is line 16, or the loss limit if line 16 is a larger loss, from the same lines. 5. Changing a dependent's capital gains, losses, distributions, dividends, - collectibles gain or Form 4952 election changes none of the filers' tax + collectibles and section 1202 gain, long-term loss carryover, Schedule D + line 18 amounts or Form 4952 election changes none of the filers' tax amounts. 6. A negative distributions input gives the same results as zero. 7. Net capital gain never falls when a filer's LT, ST, D or Q rises, and @@ -56,6 +57,9 @@ **FILER_INPUTS, "collectibles": "collectibles_gain_or_loss", "line_18_collectibles": "long_term_capital_gains_on_collectibles", + "line_18_small_business_stock": "long_term_capital_gains_on_small_business_stock", + "section_1202": "section_1202_gain", + "carryover": "long_term_capital_loss_carryover", "election": "investment_income_elected_form_4952", } @@ -183,7 +187,7 @@ def assert_dependents_change_nothing(households): # --------------------------------------------------------------------------- -# A deterministic grid (runs without Hypothesis). +# A deterministic grid. # --------------------------------------------------------------------------- GRID_GAINS = [-12_000, -3_000, 0, 4_000, 25_000] @@ -195,6 +199,9 @@ def assert_dependents_change_nothing(households): "dividends": 5_000, "collectibles": 4_000, "line_18_collectibles": 2_000, + "line_18_small_business_stock": 1_000, + "section_1202": 3_000, + "carryover": 2_000, "election": 3_000, } @@ -286,6 +293,9 @@ def dependent(draw): "dividends": draw(NON_NEGATIVE), "collectibles": draw(st.one_of(st.just(0), SMALL)), "line_18_collectibles": draw(st.one_of(st.just(0), NON_NEGATIVE)), + "line_18_small_business_stock": draw(st.one_of(st.just(0), NON_NEGATIVE)), + "section_1202": draw(st.one_of(st.just(0), NON_NEGATIVE)), + "carryover": draw(st.one_of(st.just(0), NON_NEGATIVE)), "election": draw(st.one_of(st.just(0), NON_NEGATIVE)), } diff --git a/policyengine_us/variables/gov/states/hi/tax/income/alternative_tax/hi_taxable_income_for_alternative_tax.py b/policyengine_us/variables/gov/states/hi/tax/income/alternative_tax/hi_taxable_income_for_alternative_tax.py index ad3ac9b491c..1d462080048 100644 --- a/policyengine_us/variables/gov/states/hi/tax/income/alternative_tax/hi_taxable_income_for_alternative_tax.py +++ b/policyengine_us/variables/gov/states/hi/tax/income/alternative_tax/hi_taxable_income_for_alternative_tax.py @@ -15,8 +15,11 @@ def formula(tax_unit, period, parameters): # line 1 of Hawaii Tax on Capital Gains Worksheet # on Hawaii Resident Income Tax Instructions N-11 Rev.2022, page=33 taxable_income = tax_unit("hi_taxable_income", period) - # line 4 - net_lt_capital_gain = add(tax_unit, period, ["long_term_capital_gains"]) + # line 4; the head and spouse's, as in net_capital_gain (line 7). A tax + # unit dependent's gains and losses are on the dependent's own return. + net_lt_capital_gain = tax_unit_non_dep_add( + tax_unit, period, ["long_term_capital_gains"] + ) # line 7 net_capital_gain = tax_unit("net_capital_gain", period) # line 8 diff --git a/policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/subtractions/vt_capital_gain_exclusion/vt_capital_gains_exclusion.py b/policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/subtractions/vt_capital_gain_exclusion/vt_capital_gains_exclusion.py index 0c37696c4cc..f178a6c1207 100644 --- a/policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/subtractions/vt_capital_gain_exclusion/vt_capital_gains_exclusion.py +++ b/policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/subtractions/vt_capital_gain_exclusion/vt_capital_gains_exclusion.py @@ -14,9 +14,13 @@ class vt_capital_gains_exclusion(Variable): ) def formula(tax_unit, period, parameters): - # Get adjusted net capital gains (used for flat exclusion) + # Get adjusted net capital gains (used for flat exclusion). Like the + # federal amount, the qualified dividends taken out of it are the head + # and spouse's; a tax unit dependent's are on the dependent's own return. adjusted_net_capital_gain = tax_unit("adjusted_net_capital_gain", period) - qualified_dividend_income = add(tax_unit, period, ["qualified_dividend_income"]) + qualified_dividend_income = tax_unit_non_dep_add( + tax_unit, period, ["qualified_dividend_income"] + ) reduced_adjusted_net_capital_gain = max_( adjusted_net_capital_gain - qualified_dividend_income, 0 ) From 3f1eade100228aea0917838f4ed07603325baaf9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 08:50:22 -0400 Subject: [PATCH 5/7] Test that a supplied net_capital_gains reaches Schedule D line 16 Review r2 asked for a case covering 2b7bf4f050: distributions of 1,000 and a tax unit net_capital_gains input of -5,000 make line 16 a loss of 4,000, so has_qdiv_or_ltcg is false and worksheet line 9 is 0. Main turned the flag on there. The helper's line 16 comment now says it is figured from net_capital_gains. Co-Authored-By: Claude Opus 5.5 --- .../schedule_d_tax_worksheet_form_4952.yaml | 22 +++++++++++++++++++ .../capital_gains/filer_schedule_d_lines.py | 3 ++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml index 88e746089ee..3f92fda7910 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml @@ -319,3 +319,25 @@ dwks09: 0 dwks10: 3_000 net_capital_gain: 3_000 + +- name: A tax unit net_capital_gains input reaches Schedule D line 16 + period: 2025 + input: + people: + person1: + age: 45 + non_sch_d_capital_gains: 1_000 + tax_units: + tax_unit: + members: [person1] + net_capital_gains: -5_000 + households: + household: + members: [person1] + state_code: TX + output: + # Schedule D line 15 = 1,000 (line 13); line 16 = -5,000 + 1,000 = + # -4,000, a loss, so neither tax worksheet applies (2025 Instructions + # for Form 1040, line 16) and worksheet line 7 = min(1,000, -4,000). + has_qdiv_or_ltcg: false + dwks09: 0 diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py index a0ceb6be6f3..6bb223e8af3 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py @@ -41,7 +41,8 @@ def filer_schedule_d_lines(tax_unit, period): tax_unit_non_dep_add(tax_unit, period, ["long_term_capital_gains"]) + distributions ) - # Line 16: lines 7 and 15 combined. + # Line 16: lines 7 and 15 combined, figured from net_capital_gains so a + # supplied tax unit amount is kept. dependent_gains = tax_unit.sum( dependent * ( From 63e4df4ab8aedb672e9acfaf87d79285c5b32ec3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 8 Oct 2026 20:13:55 -0400 Subject: [PATCH 6/7] Clarify Schedule D worksheet scope and aggregate input limitation --- .../schedule_d_tax_worksheet_form_4952.yaml | 10 ++++++++-- .../capital_gains/filer_schedule_d_lines.py | 10 ++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml index 3f92fda7910..4a9bee2064e 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_tax_worksheet_form_4952.yaml @@ -11,8 +11,9 @@ # 9. line 7 - line 8, not below zero # 10. line 6 + line 9 # Line 6 is dividend_income_reduced_by_investment_income, line 9 dwks09 and -# line 10 dwks10. Line 10 equals net_capital_gain, which applies 26 U.S.C. -# 1(h)(2) and 1(h)(11)(D)(i) to the same inputs. +# line 10 dwks10. With component-derived inputs, line 10 equals +# net_capital_gain, which applies 26 U.S.C. 1(h)(2) and 1(h)(11)(D)(i). +# The supplied-aggregate case below records a preexisting disagreement. - name: An election within the gain comes off the gain period: 2025 @@ -341,3 +342,8 @@ # for Form 1040, line 16) and worksheet line 7 = min(1,000, -4,000). has_qdiv_or_ltcg: false dwks09: 0 + dwks10: 0 + # Preserve the preexisting aggregate/component split: the statutory + # path uses person-level lines 15 and 7, rather than supplied line 16. + net_capital_gain: 1_000 + adjusted_net_capital_gain: 1_000 diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py index 6bb223e8af3..2fc02a56423 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py @@ -10,19 +10,21 @@ def filer_schedule_d_lines(tax_unit, period): """Schedule D (Form 1040) lines 7, 15 and 16 of the head and spouse, shared by the Form 1040 line 16 routing (has_qdiv_or_ltcg), net_capital_gain, the - Schedule D Tax Worksheet and the 28% Rate Gain Worksheet. + Schedule D Tax Worksheet. Line 7 is the net short-term capital gain or loss. Line 15 is the net long-term capital gain or loss, including capital gain distributions - (line 13). Line 16 is lines 7 and 15 combined (2025 Schedule D, lines 7, - 13, 15 and 16). + (line 13). With component-derived inputs, line 16 is lines 7 and 15 + combined (2025 Schedule D, lines 7, 13, 15 and 16). A tax unit dependent's gains, losses and distributions belong on the dependent's own return, so they are left out, as irs_gross_income leaves them out of adjusted gross income. Line 16 starts from net_capital_gains, so a tax unit amount supplied there is kept; that amount is read as covering every member, and any dependent's person-level gains and losses - are then taken out. + are then taken out. An explicit aggregate override can make line 16 + differ from lines 7 and 15 combined, and the worksheet and statutory + net-capital-gain paths can then disagree. Capital gain distributions reported without Schedule D (non_sch_d_capital_gains) are long-term capital gains (26 U.S.C. From 982378520b210525938d64141a983434c183a532 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 01:35:33 -0400 Subject: [PATCH 7/7] Match the changelog and helper docstring to the PR's scope The 28% Rate Gain Worksheet was already filer-only on main (#9988), so the fragment no longer claims this PR changes it, and it now names Vermont's dividend subtraction. The helper docstring sentence was missing an "and". Co-Authored-By: Claude Opus 5.5 --- changelog.d/ncg-filer-gains-floor-distributions.fixed.md | 2 +- .../federal_income/capital_gains/filer_schedule_d_lines.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/changelog.d/ncg-filer-gains-floor-distributions.fixed.md b/changelog.d/ncg-filer-gains-floor-distributions.fixed.md index 46cf56abd69..16c116afe33 100644 --- a/changelog.d/ncg-filer-gains-floor-distributions.fixed.md +++ b/changelog.d/ncg-filer-gains-floor-distributions.fixed.md @@ -1 +1 @@ -Figure net capital gain, the Schedule D Tax Worksheet and the 28% rate gain from the head and spouse's Schedule D and qualified dividends only, leaving out tax unit dependents' gains, losses, dividends and collectibles gain as adjusted gross income does, and treat a negative capital gain distributions input as zero there too. Keep that filer-only scope in Hawaii's alternative capital gains tax worksheet. +Figure net capital gain and the Schedule D Tax Worksheet from the head and spouse's Schedule D, qualified dividends and Form 4952 election only, leaving out tax unit dependents' amounts as adjusted gross income does, and treat each filer's negative capital gain distributions input as zero. Apply the same filer-only scope in Hawaii's alternative capital gains tax worksheet and in the qualified dividends Vermont's flat capital gains exclusion subtracts. diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py index 2fc02a56423..a0e3654534f 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/filer_schedule_d_lines.py @@ -9,8 +9,8 @@ def filer_schedule_d_lines(tax_unit, period): """Schedule D (Form 1040) lines 7, 15 and 16 of the head and spouse, shared - by the Form 1040 line 16 routing (has_qdiv_or_ltcg), net_capital_gain, the - Schedule D Tax Worksheet. + by the Form 1040 line 16 routing (has_qdiv_or_ltcg), net_capital_gain and + the Schedule D Tax Worksheet. Line 7 is the net short-term capital gain or loss. Line 15 is the net long-term capital gain or loss, including capital gain distributions