diff --git a/changelog.d/amt-part-iii-line-27-worksheet-line-21.fixed.md b/changelog.d/amt-part-iii-line-27-worksheet-line-21.fixed.md new file mode 100644 index 00000000000..0697a3215c7 --- /dev/null +++ b/changelog.d/amt-part-iii-line-27-worksheet-line-21.fixed.md @@ -0,0 +1 @@ +Tax 28 percent rate and unrecaptured section 1250 gain at the regular rates on taxable income up to the top of the 24 percent bracket, the amount taxed below 25 percent under 26 U.S.C. 1(h)(1)(A), and enter Schedule D Tax Worksheet line 21 on Form 6251 Part III line 27. diff --git a/policyengine_us/parameters/gov/irs/capital_gains/regular_rate_limit.yaml b/policyengine_us/parameters/gov/irs/capital_gains/regular_rate_limit.yaml new file mode 100644 index 00000000000..6493436a559 --- /dev/null +++ b/policyengine_us/parameters/gov/irs/capital_gains/regular_rate_limit.yaml @@ -0,0 +1,13 @@ +description: The IRS taxes capital gain at the regular rates on taxable income that the regular rate schedule taxes below this rate, up to taxable income less adjusted net capital gain. +values: + # Pub. L. 107-16, section 101(c)(2)(A), substituted "25 percent" for + # "28 percent". + 2001-01-01: 0.25 +metadata: + unit: /1 + label: Capital gains regular rate limit + reference: + - title: 26 U.S.C. § 1(h)(1)(A)(ii)(I) + href: https://www.law.cornell.edu/uscode/text/26/1#h_1_A_ii_I + - title: 2025 Instructions for Schedule D (Form 1040), Schedule D Tax Worksheet, line 19 + href: https://www.irs.gov/pub/irs-pdf/i1040sd.pdf#page=15 diff --git a/policyengine_us/reforms/additional_tax_bracket/additional_tax_bracket_reform.py b/policyengine_us/reforms/additional_tax_bracket/additional_tax_bracket_reform.py index effd9e5564b..13fad485592 100644 --- a/policyengine_us/reforms/additional_tax_bracket/additional_tax_bracket_reform.py +++ b/policyengine_us/reforms/additional_tax_bracket/additional_tax_bracket_reform.py @@ -2,6 +2,7 @@ from policyengine_core.periods import period as period_ from policyengine_core.periods import instant from policyengine_us.variables.gov.irs.tax.federal_income.before_credits.tax_at_main_rates import ( + amount_taxed_below_rate, tax_at_main_rates, ) from policyengine_us.variables.gov.irs.tax.federal_income.foreign_earned_income_exclusion.schedule_d_tax_worksheet_after_capital_gain_excess import ( @@ -109,10 +110,33 @@ def formula(tax_unit, period, parameters): tax_on_excluded = tax_at_main_rates(excluded, filing_status, bracket) return where(excluded > 0, max_(0, tax - tax_on_excluded), tax) + class taxable_income_taxed_below_25_percent(Variable): + value_type = float + entity = TaxUnit + label = "Taxable income taxed at a rate below 25 percent" + unit = USD + definition_period = YEAR + reference = "https://www.law.cornell.edu/uscode/text/26/1#h_1_A_ii_I" + + def formula(tax_unit, period, parameters): + # 26 U.S.C. 1(h)(1)(A)(ii)(I) on the reform's rate schedule, as + # in the baseline formula on the baseline schedule. + taxable_income = tax_unit( + "taxable_income_plus_section_911_exclusion", period + ) + filing_status = tax_unit("filing_status", period) + return amount_taxed_below_rate( + taxable_income, + filing_status, + parameters(period).gov.contrib.additional_tax_bracket.bracket, + parameters(period).gov.irs.capital_gains.regular_rate_limit, + ) + class reform(Reform): def apply(self): self.update_variable(income_tax_main_rates) self.update_variable(regular_tax_before_credits) + self.update_variable(taxable_income_taxed_below_25_percent) return reform diff --git a/policyengine_us/reforms/biden/budget_2025/capital_gains_tax_increase.py b/policyengine_us/reforms/biden/budget_2025/capital_gains_tax_increase.py index b2d5e63903f..f0233f9e3cd 100644 --- a/policyengine_us/reforms/biden/budget_2025/capital_gains_tax_increase.py +++ b/policyengine_us/reforms/biden/budget_2025/capital_gains_tax_increase.py @@ -2,6 +2,9 @@ from policyengine_us.variables.gov.irs.tax.federal_income.foreign_earned_income_exclusion.section_911_net_capital_gain_other_than_dividends import ( section_911_net_capital_gain_other_than_dividends, ) +from policyengine_us.variables.gov.irs.tax.federal_income.capital_gains.capital_gains_tax import ( + rate_gain_taxed_at_28_percent, +) def create_capital_gains_tax_increase() -> Reform: @@ -130,8 +133,16 @@ def formula(tax_unit, period, parameters): cg.unrecaptured_s_1250_rate * taxable_unrecaptured_gain ) - remaining_cg_tax = ( - tax_unit("section_911_28_percent_rate_gain", period) * cg.other_cg_rate + # As in the baseline formula, 28 percent of the rest of taxable + # income (26 U.S.C. 1(h)(1)(F)): the 28 percent rate gain less + # any of it taxed at the regular rates. + remaining_cg_tax = cg.other_cg_rate * rate_gain_taxed_at_28_percent( + tax_unit, + period, + taxable_income, + non_cg_taxable_income, + adjusted_net_cg, + taxable_unrecaptured_gain, ) return main_cg_tax + unrecaptured_gain_tax + remaining_cg_tax diff --git a/policyengine_us/reforms/harris/capital_gains/harris_capital_gains.py b/policyengine_us/reforms/harris/capital_gains/harris_capital_gains.py index c51f1339e83..fdbc86ebcba 100644 --- a/policyengine_us/reforms/harris/capital_gains/harris_capital_gains.py +++ b/policyengine_us/reforms/harris/capital_gains/harris_capital_gains.py @@ -2,6 +2,9 @@ from policyengine_us.variables.gov.irs.tax.federal_income.foreign_earned_income_exclusion.section_911_net_capital_gain_other_than_dividends import ( section_911_net_capital_gain_other_than_dividends, ) +from policyengine_us.variables.gov.irs.tax.federal_income.capital_gains.capital_gains_tax import ( + rate_gain_taxed_at_28_percent, +) def create_harris_capital_gains() -> Reform: @@ -119,8 +122,16 @@ def formula(tax_unit, period, parameters): cg.unrecaptured_s_1250_rate * taxable_unrecaptured_gain ) - remaining_cg_tax = ( - tax_unit("section_911_28_percent_rate_gain", period) * cg.other_cg_rate + # As in the baseline formula, 28 percent of the rest of taxable + # income (26 U.S.C. 1(h)(1)(F)): the 28 percent rate gain less + # any of it taxed at the regular rates. + remaining_cg_tax = cg.other_cg_rate * rate_gain_taxed_at_28_percent( + tax_unit, + period, + taxable_income, + non_cg_taxable_income, + adjusted_net_cg, + taxable_unrecaptured_gain, ) return main_cg_tax + unrecaptured_gain_tax + remaining_cg_tax diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.yaml index 4157db7e966..fd894743583 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.yaml @@ -6,6 +6,7 @@ dwks10: 0 dwks13: 0 dwks14: 0 + dwks19: 0 unrecaptured_section_1250_gain: 0 output: amt_tax_including_cg: 13_000 @@ -18,6 +19,7 @@ dwks10: 0 dwks13: 0 dwks14: 0 + dwks19: 0 unrecaptured_section_1250_gain: 0 output: amt_tax_including_cg: 26_000 @@ -30,6 +32,7 @@ dwks10: 50_000 dwks13: 30_000 dwks14: 0 + dwks19: 0 unrecaptured_section_1250_gain: 0 output: # AMTI - exemption = $100k; ordinary AMTI = $70k * 26% = $18,200. @@ -45,6 +48,7 @@ dwks10: 200_000 dwks13: 150_000 dwks14: 0 + dwks19: 0 unrecaptured_section_1250_gain: 0 output: # Ordinary AMTI tax: 26% * $220,700 + 28% * $229,300 = $121,586. @@ -61,6 +65,7 @@ dwks10: 500_000 dwks13: 300_000 dwks14: 0 + dwks19: 0 unrecaptured_section_1250_gain: 200_000 output: # Ordinary AMTI tax: 26% * $220,700 + 28% * $279,300 = $135,586. @@ -81,6 +86,7 @@ dwks10: 44_625 dwks13: 44_625 dwks14: 0 + dwks19: 0 unrecaptured_section_1250_gain: 0 output: # L22 = L23 = $44,625; L24 = $0; nothing in 15% or 20%; L17 = $0. @@ -97,6 +103,7 @@ dwks10: 492_300 dwks13: 492_300 dwks14: 0 + dwks19: 0 unrecaptured_section_1250_gain: 0 output: # 0% bracket consumes $44,625; remaining $447,675 at 15% = @@ -113,6 +120,7 @@ dwks10: 600_000 dwks13: 600_000 dwks14: 0 + dwks19: 0 unrecaptured_section_1250_gain: 0 output: # 0%: $44,625; 15%: $447,675 (= $492,300 - $44,625) * 15% = @@ -132,6 +140,7 @@ dwks10: 100_000 dwks13: 100_000 dwks14: 50_000 + dwks19: 50_000 unrecaptured_section_1250_gain: 0 output: # Ordinary AMTI tax: 26% * $200,000 = $52,000. @@ -151,6 +160,7 @@ dwks10: 400_000 dwks13: 400_000 dwks14: 0 + dwks19: 0 unrecaptured_section_1250_gain: 0 output: # 0%: $89,250 (joint); 15%: $310,750 * 15% = $46,612.50; @@ -173,6 +183,7 @@ dwks10: 925_000 dwks13: 925_000 dwks14: 136_000 + dwks19: 136_000 unrecaptured_section_1250_gain: 0 output: # Line 12: $1,061,000. Line 13: $925,000 ($25,000 of qualified @@ -197,6 +208,7 @@ dwks10: 50_000 dwks13: 50_000 dwks14: 200_000 + dwks19: 200_000 unrecaptured_section_1250_gain: 0 output: # Line 12: $250,000. Lines 13, 15 and 16: $50,000. Line 17: $200,000. @@ -215,6 +227,7 @@ dwks10: 50_000 dwks13: 50_000 dwks14: 119_550 + dwks19: 119_550 unrecaptured_section_1250_gain: 0 output: # Line 17: $119,550, which is "$119,550 or less", so line 18 is 26% * @@ -231,6 +244,7 @@ dwks10: 50_000 dwks13: 50_000 dwks14: 119_551 + dwks19: 119_551 unrecaptured_section_1250_gain: 0 output: # Line 17: $119,551. Line 18: 28% * $119,551 - $2,391 = $31,083.28 @@ -246,6 +260,7 @@ dwks10: 100_000 dwks13: 100_000 dwks14: 300_000 + dwks19: 300_000 unrecaptured_section_1250_gain: 0 output: # Line 17: $300,000. Line 18: 28% * $300,000 - $2,391 = $81,609 (the @@ -263,18 +278,25 @@ dwks10: 300_000 dwks13: 200_000 dwks14: 200_000 + # Schedule D Tax Worksheet with taxable income (line 1) of $400,000: + # line 18 = $400,000 - $300,000 = $100,000; line 19 = $197,300 (the + # smaller of line 1 or $197,300); line 20 = the smaller of line 14 or + # line 19 = $197,300; line 21 = the larger of line 18 or line 20. + dwks19: 197_300 unrecaptured_section_1250_gain: 100_000 output: # Line 12: $500,000. Line 13: $200,000. Line 14: $100,000. # Line 15: min($200,000 + $100,000, $300,000) = $300,000. Line 16: # $300,000. Line 17: $200,000. Line 18: 28% * $200,000 - $2,391 = - # $53,609. Line 21: $0. Line 22: $200,000. Line 23: $0. Line 24: - # $200,000. Line 29: $300,000 - $200,000 = $100,000. Line 30: $100,000. - # Line 31: $15,000. Line 32: $100,000. Line 33: $100,000. Line 34: - # $20,000. Line 35: $200,000 + $100,000 + $100,000 = $400,000. - # Line 36: $100,000. Line 37: 25% = $25,000. - # Line 38: $53,609 + $15,000 + $20,000 + $25,000 = $113,609. - amt_tax_including_cg: 113_609 + # $53,609. Line 20: $200,000. Line 21: $0. Line 22: $200,000. Line 23: + # $0. Line 24: $200,000. Line 27: $197,300 (worksheet line 21, not + # line 14). Line 28: $197,300. Line 29: $300,000 - $197,300 = + # $102,700. Line 30: $102,700. Line 31: $15,405. Line 32: $102,700. + # Line 33: $97,300. Line 34: $19,460. Line 35: $200,000 + $102,700 + + # $97,300 = $400,000. Line 36: $100,000. Line 37: 25% = $25,000. + # Line 38: $53,609 + $15,405 + $19,460 + $25,000 = $113,474. (Line 14 + # of the worksheet on line 27 would give $113,609.) + amt_tax_including_cg: 113_474 - name: Single filer with the amounts of the first separate case keeps the full breakpoint period: 2025 @@ -284,6 +306,7 @@ dwks10: 925_000 dwks13: 925_000 dwks14: 136_000 + dwks19: 136_000 unrecaptured_section_1250_gain: 0 output: # Line 17: $136,000 <= $239,100, so line 18 is 26% * $136,000 = @@ -301,6 +324,7 @@ dwks10: 0 dwks13: 0 dwks14: 136_000 + dwks19: 136_000 unrecaptured_section_1250_gain: 0 output: # Rev. Proc. 2025-32, section 3.10: for 2026 the 28 percent rate applies @@ -334,6 +358,9 @@ # Line 22 = 211,900, line 23 = 0, line 29 = 533,400 - 100,000 = 433,400, # line 30 = 211,900, line 31 = 31,785, line 33 = 0. Line 38 = 57,785. dwks14: 100_000 + # Line 27: worksheet line 21, also 100,000 with no 28 percent rate or + # unrecaptured section 1250 gain. + dwks19: 100_000 amt_tax_including_cg: 57_785 - name: The same gains without the exclusion diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/integration.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/integration.yaml index 50790f35b22..23a7ed89800 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/alternative_minimum_tax/integration.yaml @@ -73,3 +73,138 @@ # would give $25,285.25). alternative_minimum_tax: 26_767.5 income_tax_before_credits: 228_547 + +# Unrecaptured section 1250 gain, 2025, single. Worked by hand from the 2025 +# Instructions for Schedule D (Schedule D Tax Worksheet), the 2025 Form 1040 +# instructions (Tax Computation Worksheet) and the 2025 Form 6251. +# 26 U.S.C. 1(h)(1)(A) taxes at the regular rates the greater of taxable +# income less net capital gain, or the lesser of the taxable income taxed at +# a rate below 25 percent and taxable income less adjusted net capital gain +# (worksheet line 21); Form 6251 line 27 is that line 21. +- name: Single with unrecaptured section 1250 gain, 2025 + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 45 + employment_income: 200_000 + long_term_capital_gains: 400_000 + tax_units: + tax_unit: + members: [person] + unrecaptured_section_1250_gain: 100_000 + households: + household: + members: [person] + state_code: TX + output: + # Taxable income: $600,000 - $15,750 = $584,250. + # Schedule D Tax Worksheet: lines 9 and 10 $400,000; line 11 $100,000 + # (Schedule D line 19); line 12 $100,000; line 13 $300,000; line 14 + # $284,250; line 18 $184,250; line 19 $197,300; line 20 $197,300; line + # 21 $197,300; line 22 $0; line 23 $300,000; line 27 $533,400; line 28 + # $197,300; line 29 $336,100; line 30 $300,000; line 31 $45,000; line 33 + # $0; line 35 $100,000; line 36 $597,300; line 38 $13,050; line 39 + # $86,950; line 40 $21,737.50; line 44 (tax on $197,300) 24% * $197,300 + # - $7,153 = $40,199; line 45 $106,936.50; line 46 $174,034.75; line 47 + # $106,936.50. + taxable_income: 584_250 + dwks19: 197_300 + income_tax_main_rates: 40_199 + capital_gains_tax: 66_737.5 + # Form 6251: line 4 $600,000; exemption $88,100; line 12 $511,900; + # line 13 $300,000; line 14 $100,000; line 15 $400,000; line 16 + # $400,000; line 17 $111,900; line 18 26% = $29,094; line 19 $48,350; + # line 20 $284,250; line 21 $0; line 22 $300,000; line 23 $0; line 24 + # $300,000; line 25 $533,400; line 27 $197,300; line 28 $197,300; line + # 29 $336,100; line 30 $300,000; line 31 $45,000; line 33 $0; line 35 + # $411,900; line 36 $100,000; line 37 $25,000; line 38 $99,094; line 39 + # 28% * $511,900 - $4,782 = $138,550; line 40 $99,094. Line 10: + # $106,936.50, so line 11 is $0. + amt_tax_including_cg: 99_094 + amt_base_tax: 138_550 + alternative_minimum_tax: 0 + income_tax_before_credits: 106_936.5 + +# The same with $900,000 of long-term gain, $225,000 of it unrecaptured +# section 1250 gain, and no wages. Both the regular tax (line 10) and Part III +# change, and the AMT is positive. +- name: Single with unrecaptured section 1250 gain owing AMT, 2025 + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 45 + long_term_capital_gains: 900_000 + tax_units: + tax_unit: + members: [person] + unrecaptured_section_1250_gain: 225_000 + households: + household: + members: [person] + state_code: TX + output: + # Taxable income: $900,000 - $15,750 = $884,250. + # Schedule D Tax Worksheet: line 10 $900,000; line 12 $225,000; line 13 + # $675,000; line 14 $209,250; line 18 $0; line 19 $197,300; line 21 + # $197,300; line 22 $0; line 25 $675,000; line 29 $336,100; line 30 + # $336,100; line 31 $50,415; line 33 $338,900; line 34 $67,780; line 35 + # $225,000; line 36 $1,097,300; line 38 $213,050; line 39 $11,950; line + # 40 $2,987.50; line 44 $40,199; line 45 $161,381.50. + dwks14: 209_250 + dwks19: 197_300 + income_tax_main_rates: 40_199 + capital_gains_tax: 121_182.5 + # Form 6251: line 4 $900,000; exemption $88,100 - 25% * ($900,000 - + # $626,350) = $19,687.50; line 12 $880,312.50; line 13 $675,000; line + # 14 $225,000; line 15 $900,000; line 16 $880,312.50; line 17 $0; line + # 20 $209,250; line 21 $0; line 22 $675,000; line 24 $675,000; line 27 + # $197,300; line 29 $336,100; line 30 $336,100; line 31 $50,415; line 33 + # $338,900; line 34 $67,780; line 35 $675,000; line 36 $205,312.50; line + # 37 $51,328.13; line 38 $169,523.13; line 39 $241,705.50; line 40 + # $169,523.13. Line 11: $169,523.13 - $161,381.50 = $8,141.63. + amt_income_less_exemptions: 880_312.5 + amt_tax_including_cg: 169_523.125 + alternative_minimum_tax: 8_141.625 + +# Collectibles gain (28 percent rate gain), 2025, single. The part of the 28 +# percent rate gain that falls in the brackets below 25 percent is taxed at +# the regular rates (worksheet line 21); only the rest is taxed at 28 percent +# (worksheet lines 41 to 43, 26 U.S.C. 1(h)(1)(F)). +- name: Single with collectibles gain, 2025 + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 45 + long_term_capital_gains: 900_000 + long_term_capital_gains_on_collectibles: 270_000 + tax_units: + tax_unit: + members: [person] + households: + household: + members: [person] + state_code: TX + output: + # Schedule D Tax Worksheet: line 10 $900,000; line 11 $270,000; line 13 + # $630,000; line 14 $254,250; line 18 $0; line 21 $197,300; line 22 $0; + # line 23 $630,000; line 29 $336,100; line 30 $336,100; line 31 + # $50,415; line 33 $293,900; line 34 $58,780; Schedule D line 19 is zero, + # so lines 35 to 40 are skipped; line 41 $827,300; line 42 $56,950; line + # 43 $15,946; line 44 $40,199; line 45 $165,340. + dwks19: 197_300 + capital_gains_tax: 125_141 + income_tax_main_rates: 40_199 + # Form 6251: line 12 $880,312.50; line 13 $630,000; line 14 $0; line 15 + # $630,000; line 17 $250,312.50; line 18 28% - $4,782 = $65,305.50; line + # 20 $254,250; line 22 $630,000; line 27 $197,300; line 29 $336,100; + # line 30 $336,100; line 31 $50,415; line 33 $293,900; line 34 $58,780; + # line 38 $174,500.50; line 39 $241,705.50. Line 11: $174,500.50 - + # $165,340 = $9,160.50. + amt_tax_including_cg: 174_500.5 + alternative_minimum_tax: 9_160.5 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/capital_gains_excluded_from_taxable_income.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/capital_gains_excluded_from_taxable_income.yaml index 149e1fb3c5d..90a30fd354f 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/capital_gains_excluded_from_taxable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/capital_gains_excluded_from_taxable_income.yaml @@ -36,3 +36,32 @@ # Excess 15,750. Worksheet: line 1 = 134,250, line 3 = 34,250, line 5 = # 100,000, so the whole excluded amount stays in the main-rates base. capital_gains_excluded_from_taxable_income: 34_250 + +# Section 1(h)(1)(A)(ii)(I) is "the amount of taxable income taxed at a rate +# below 25 percent": taxable income up to the top of the 24 percent bracket +# ($197,300 single in 2025), not the 0 percent capital gains threshold. +- name: Unrecaptured section 1250 gain in the 24 percent bracket + period: 2025 + input: + filing_status: SINGLE + taxable_income: 584_250 + net_capital_gain: 400_000 + adjusted_net_capital_gain: 300_000 + foreign_earned_income_exclusion: 0 + output: + # The amount taxed at the regular rates is the greater of 184,250 + # (taxable income less net capital gain) or the lesser of 197,300 and + # 284,250 (taxable income less adjusted net capital gain): 197,300. + capital_gains_excluded_from_taxable_income: 386_950 + +- name: Gains above the 24 percent bracket leave taxable income less net capital gain + period: 2025 + input: + filing_status: SINGLE + taxable_income: 700_000 + net_capital_gain: 400_000 + adjusted_net_capital_gain: 300_000 + foreign_earned_income_exclusion: 0 + output: + # The greater of 300,000 or the lesser of 197,300 and 400,000: 300,000. + capital_gains_excluded_from_taxable_income: 400_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/dwks19.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/dwks19.yaml index 2b4a3a4ec3d..7b79fcc2386 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/dwks19.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/dwks19.yaml @@ -31,3 +31,90 @@ foreign_earned_income_exclusion: 100_000 output: dwks19: 100_000 + +# With 28 percent rate gain or unrecaptured section 1250 gain, line 21 is the +# larger of line 18 (line 1 less line 10) or line 20 (the smaller of line 14 +# or line 19), where line 19 is the smaller of line 1 or the top of the 24 +# percent bracket ($197,300 single, $394,600 joint in 2025), the taxable +# income taxed at a rate below 25 percent (26 U.S.C. 1(h)(1)(A)(ii)(I)). +- name: Unrecaptured section 1250 gain, line 19 caps line 14 + period: 2025 + input: + filing_status: SINGLE + taxable_income: 584_250 + long_term_capital_gains: 400_000 + unrecaptured_section_1250_gain: 100_000 + output: + # Lines 9 and 10: 400,000. Line 11: 100,000. Line 12: 100,000. Line 13: + # 300,000. Line 14: 284,250. Line 18: 184,250. Line 19: 197,300. Line + # 20: 197,300. Line 21: 197,300. + dwks14: 284_250 + dwks19: 197_300 + +- name: Unrecaptured section 1250 gain, line 18 above line 20 + period: 2025 + input: + filing_status: SINGLE + taxable_income: 700_000 + long_term_capital_gains: 400_000 + unrecaptured_section_1250_gain: 100_000 + output: + # Line 13: 300,000. Line 14: 400,000. Line 18: 300,000. Line 20: + # 197,300. Line 21: 300,000. + dwks19: 300_000 + +- name: 28 percent rate and unrecaptured gain, line 14 within line 19 + period: 2025 + input: + filing_status: SINGLE + taxable_income: 414_250 + long_term_capital_gains: 400_000 + long_term_capital_gains_on_collectibles: 80_000 + unrecaptured_section_1250_gain: 50_000 + output: + # Line 10: 400,000. Line 11: 130,000. Line 13: 270,000. Line 14: + # 144,250. Line 18: 14,250. Line 19: 197,300. Line 20: 144,250. Line + # 21: 144,250. + dwks19: 144_250 + +- name: Joint filer with unrecaptured section 1250 gain + period: 2025 + input: + filing_status: JOINT + taxable_income: 900_000 + long_term_capital_gains: 600_000 + unrecaptured_section_1250_gain: 200_000 + output: + # Line 13: 400,000. Line 14: 500,000. Line 18: 300,000. Line 19: + # 394,600. Line 20: 394,600. Line 21: 394,600. + dwks19: 394_600 + +- name: A Form 2555 filer with unrecaptured section 1250 gain + period: 2025 + input: + filing_status: SINGLE + taxable_income: 200_000 + long_term_capital_gains: 150_000 + unrecaptured_section_1250_gain: 50_000 + foreign_earned_income_exclusion: 100_000 + output: + # Line 1: 300,000 (Foreign Earned Income Tax Worksheet line 3). No + # capital gain excess. Line 10: 150,000. Line 13: 100,000. Line 14: + # 200,000. Line 18: 150,000. Line 19: 197,300. Line 21: 197,300. + dwks19: 197_300 + +- name: A Form 2555 filer with a capital gain excess and unrecaptured gain + period: 2025 + input: + filing_status: SINGLE + taxable_income: 34_250 + long_term_capital_gains: 50_000 + unrecaptured_section_1250_gain: 20_000 + foreign_earned_income_exclusion: 100_000 + output: + # Capital gain excess: 50,000 - 34,250 = 15,750. Second worksheet: line + # 9 = 34,250, line 10 = 34,250; Schedule D line 19 = 20,000 - 15,750 = + # 4,250, so line 12 = 4,250 and line 13 = 30,000. Line 1: 134,250. Line + # 14: 104,250. Line 18: 100,000. Line 19: 134,250. Line 20: 104,250. + # Line 21: 104,250. + dwks19: 104_250 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/taxable_income_taxed_below_25_percent.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/taxable_income_taxed_below_25_percent.yaml new file mode 100644 index 00000000000..5ec59d16119 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/taxable_income_taxed_below_25_percent.yaml @@ -0,0 +1,91 @@ +# 26 U.S.C. 1(h)(1)(A)(ii)(I): "the amount of taxable income taxed at a rate +# below 25 percent". 2025 Schedule D Tax Worksheet line 19: "Enter the smaller +# of line 1 or: $197,300 if single or married filing separately; $394,600 if +# married filing jointly or qualifying surviving spouse; or $197,300 if head +# of household." Line 1 is taxable income, or for a Form 2555 filer line 3 of +# the Foreign Earned Income Tax Worksheet. +- name: Single, taxable income above the top of the 24 percent bracket + period: 2025 + input: + filing_status: SINGLE + taxable_income: 584_250 + output: + taxable_income_taxed_below_25_percent: 197_300 + +- name: Single, taxable income within the 24 percent bracket + period: 2025 + input: + filing_status: SINGLE + taxable_income: 150_000 + output: + taxable_income_taxed_below_25_percent: 150_000 + +- name: Joint + period: 2025 + input: + filing_status: JOINT + taxable_income: 500_000 + output: + taxable_income_taxed_below_25_percent: 394_600 + +- name: Qualifying surviving spouse + period: 2025 + input: + filing_status: SURVIVING_SPOUSE + taxable_income: 400_000 + output: + taxable_income_taxed_below_25_percent: 394_600 + +- name: Married filing separately + period: 2025 + input: + filing_status: SEPARATE + taxable_income: 250_000 + output: + taxable_income_taxed_below_25_percent: 197_300 + +- name: Head of household + period: 2025 + input: + filing_status: HEAD_OF_HOUSEHOLD + taxable_income: 300_000 + output: + taxable_income_taxed_below_25_percent: 197_300 + +- name: No taxable income + period: 2025 + input: + filing_status: SINGLE + taxable_income: 0 + output: + taxable_income_taxed_below_25_percent: 0 + +- name: A Form 2555 filer adds the excluded amount + period: 2025 + input: + filing_status: SINGLE + taxable_income: 150_000 + foreign_earned_income_exclusion: 100_000 + output: + # Foreign Earned Income Tax Worksheet line 3: 250,000. + taxable_income_taxed_below_25_percent: 197_300 + +- name: Single, 2026 + period: 2026 + input: + filing_status: SINGLE + taxable_income: 300_000 + output: + # Rev. Proc. 2025-32, section 4.01, Table 3: the 24 percent bracket ends + # at $201,775 for unmarried individuals. + taxable_income_taxed_below_25_percent: 201_775 + +- name: A 25 percent fourth bracket leaves only the first three brackets + period: 2025 + input: + gov.irs.income.bracket.rates.4: 0.25 + filing_status: SINGLE + taxable_income: 584_250 + output: + # The 22 percent bracket ends at $103,350. + taxable_income_taxed_below_25_percent: 103_350 diff --git a/policyengine_us/tests/policy/contrib/additional_tax_bracket/additional_tax_bracket_reform.yaml b/policyengine_us/tests/policy/contrib/additional_tax_bracket/additional_tax_bracket_reform.yaml index eba39e2b714..69b8fdc532f 100644 --- a/policyengine_us/tests/policy/contrib/additional_tax_bracket/additional_tax_bracket_reform.yaml +++ b/policyengine_us/tests/policy/contrib/additional_tax_bracket/additional_tax_bracket_reform.yaml @@ -75,3 +75,38 @@ output: regular_tax_before_credits: 145_893 income_tax_main_rates: 145_893 + +# 26 U.S.C. 1(h)(1)(A)(ii)(I), "the amount of taxable income taxed at a rate +# below 25 percent", follows the rate schedule in force. From 2026 the +# reform's schedule is 10, 15, 25, 28, 33, 35 and 39.6 percent, so the +# amount ends at the top of its 15 percent bracket ($49,300 single), not at +# the top of the baseline 24 percent bracket ($201,775). +- name: Additional tax bracket reform's own amount taxed below 25 percent + period: 2026 + reforms: policyengine_us.reforms.additional_tax_bracket.additional_tax_bracket_reform.additional_tax_bracket + input: + gov.contrib.additional_tax_bracket.in_effect: true + filing_status: SINGLE + taxable_income: 300_000 + long_term_capital_gains: 200_000 + unrecaptured_section_1250_gain: 100_000 + output: + taxable_income_taxed_below_25_percent: 49_300 + # Schedule D Tax Worksheet: line 13 100,000; line 14 200,000; line 18 + # 100,000; line 19 49,300; line 20 49,300; line 21 100,000. + dwks19: 100_000 + # Taxed at the regular rates: the greater of 100,000 or the lesser of + # 49,300 and 200,000. + capital_gains_excluded_from_taxable_income: 200_000 + +- name: The same household without the reform + period: 2026 + input: + filing_status: SINGLE + taxable_income: 300_000 + long_term_capital_gains: 200_000 + unrecaptured_section_1250_gain: 100_000 + output: + taxable_income_taxed_below_25_percent: 201_775 + dwks19: 200_000 + capital_gains_excluded_from_taxable_income: 100_000 diff --git a/policyengine_us/tests/policy/contrib/biden/budget_2025/capital_gains_tax_increase.yaml b/policyengine_us/tests/policy/contrib/biden/budget_2025/capital_gains_tax_increase.yaml new file mode 100644 index 00000000000..8ef857c3beb --- /dev/null +++ b/policyengine_us/tests/policy/contrib/biden/budget_2025/capital_gains_tax_increase.yaml @@ -0,0 +1,30 @@ +# 26 U.S.C. 1(h)(1)(F) taxes at 28 percent only the taxable income left after +# the regular rate amount, the adjusted net capital gain and the unrecaptured +# section 1250 gain. 2025, single, $200,000 of wages and $400,000 of +# long-term gain, $100,000 of it collectibles gain, below the reform's $1 +# million income threshold: taxable income $584,250; the regular rate amount +# is $197,300; the adjusted net capital gain $300,000 is taxed at 15 percent +# ($45,000); the 28 percent amount is $584,250 - $197,300 - $300,000 = +# $86,950 ($24,346), not all $100,000. +- name: Reform active, collectibles gain partly taxed at the regular rates + period: 2025 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.biden.budget_2025.capital_gains_tax_increase.capital_gains_tax_increase + input: + gov.contrib.biden.budget_2025.capital_gains.active: true + people: + head: + age: 45 + employment_income: 200_000 + long_term_capital_gains: 400_000 + long_term_capital_gains_on_collectibles: 100_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: TX + output: + taxable_income: 584_250 + capital_gains_tax: 69_346 diff --git a/policyengine_us/tests/policy/contrib/harris/capital_gains/harris_capital_gains.yaml b/policyengine_us/tests/policy/contrib/harris/capital_gains/harris_capital_gains.yaml index 941c3c742bc..6cd59e14ac0 100644 --- a/policyengine_us/tests/policy/contrib/harris/capital_gains/harris_capital_gains.yaml +++ b/policyengine_us/tests/policy/contrib/harris/capital_gains/harris_capital_gains.yaml @@ -85,3 +85,35 @@ filing_status: JOINT output: capital_gains_tax: 892.5 + +# 26 U.S.C. 1(h)(1)(F) taxes at 28 percent only the taxable income left after +# the regular rate amount, the adjusted net capital gain and the unrecaptured +# section 1250 gain. 2025, single, $200,000 of wages and $400,000 of +# long-term gain, $100,000 of it collectibles gain: taxable income $584,250; +# the regular rate amount is the greater of $184,250 or the lesser of +# $197,300 and $284,250, $197,300; the adjusted net capital gain $300,000 is +# all within the reform's 15 percent band ($45,000); the 28 percent amount is +# $584,250 - $197,300 - $300,000 = $86,950 ($24,346), not all $100,000. +- name: Reform applied, collectibles gain partly taxed at the regular rates + period: 2025 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.harris.capital_gains.harris_capital_gains.harris_capital_gains + input: + gov.contrib.harris.capital_gains.in_effect: true + people: + head: + age: 45 + employment_income: 200_000 + long_term_capital_gains: 400_000 + long_term_capital_gains_on_collectibles: 100_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: TX + output: + taxable_income: 584_250 + capital_gains_excluded_from_taxable_income: 386_950 + capital_gains_tax: 69_346 diff --git a/policyengine_us/tests/test_form_6251_part_iii.py b/policyengine_us/tests/test_form_6251_part_iii.py index 91013165611..eb25558d626 100644 --- a/policyengine_us/tests/test_form_6251_part_iii.py +++ b/policyengine_us/tests/test_form_6251_part_iii.py @@ -32,8 +32,21 @@ scale applied with no factor, bit for bit: their multiplier is 1. Households with 28-percent rate or unrecaptured section 1250 gain go through -the Schedule D Tax Worksheet, which is not transcribed here; they are -checked only against the properties. YAML unit tests cover those lines. +the Schedule D Tax Worksheet. Its 2025 lines 1 to 47 are transcribed below +too, and for those households the model is compared with: + +- line 47, the regular tax (`income_tax_main_rates` plus + `capital_gains_tax`), which taxes at the regular rates the greater of line + 18 or line 20 (line 21), where line 19 is the taxable income taxed at a + rate below 25 percent (26 U.S.C. 1(h)(1)(A)(ii)(I)); +- Form 6251 lines 38 to 40 and 11, with line 14 from Schedule D line 19, + line 15 capped by worksheet line 10, line 27 from worksheet line 21 and + lines 35 to 37 taxing the rest of the unrecaptured gain at 25 percent. + +Further properties for those households: worksheet line 21 lies between +lines 18 and 14, and equals line 14 without either kind of gain (so line 27 +is then line 20, line 5 of the capital gains worksheet); the regular tax +never exceeds the tax on all taxable income at the regular rates (line 46). """ import numpy as np @@ -77,14 +90,29 @@ def amt_rates_2025(amount, status): return 0.26 * amount if amount <= 239_100 else 0.28 * amount - 4_782 -def form_6251_part_iii_2025(line_12, line_13, line_20, status): - """2025 Form 6251, lines 12 to 40, with no Schedule D Tax Worksheet. +def form_6251_part_iii_2025( + line_12, + line_13, + line_20, + status, + line_14=0, + worksheet_line_10=None, + line_27=None, +): + """2025 Form 6251, lines 12 to 40. With no Schedule D Tax Worksheet, line 14 is zero, line 15 is line 13 - and line 27 is line 20 (line 5 of the capital gains worksheet). + and line 27 is line 20 (line 5 of the capital gains worksheet). With + one, pass line 14 (Schedule D line 19), the worksheet's line 10 and its + line 21 for line 27. Returns lines 17, 38, 39 and 40. """ - line_15 = line_13 + if worksheet_line_10 is None: + line_15 = line_13 + else: + line_15 = min(line_13 + line_14, worksheet_line_10) + if line_27 is None: + line_27 = line_20 line_16 = min(line_12, line_15) line_17 = line_12 - line_16 line_18 = amt_rates_2025(line_17, status) @@ -92,18 +120,166 @@ def form_6251_part_iii_2025(line_12, line_13, line_20, status): line_22 = min(line_12, line_13) line_23 = min(line_21, line_22) line_24 = line_22 - line_23 - line_28 = line_21 + line_20 + line_26 = line_21 + line_28 = line_26 + line_27 line_29 = max(0, FIFTEEN_PERCENT_RATE_AMOUNT_2025[status] - line_28) line_30 = min(line_24, line_29) line_31 = 0.15 * line_30 line_32 = line_23 + line_30 - line_33 = line_22 - line_32 - line_34 = 0.20 * line_33 - line_38 = line_18 + line_31 + line_34 + line_33 = line_34 = line_37 = 0 + # "If lines 32 and 12 are the same, skip lines 33 through 37." + if line_32 != line_12: + line_33 = line_22 - line_32 + line_34 = 0.20 * line_33 + # "If line 14 is zero or blank, skip lines 35 through 37." + if line_14 > 0: + line_35 = line_17 + line_32 + line_33 + line_36 = line_12 - line_35 + line_37 = 0.25 * line_36 + line_38 = line_18 + line_31 + line_34 + line_37 line_39 = amt_rates_2025(line_12, status) return line_17, line_38, line_39, min(line_38, line_39) +# The 2025 Schedule D Tax Worksheet (Instructions for Schedule D (Form 1040)). +# Line 19: the smaller of line 1 or these amounts, the top of the 24 percent +# bracket. +BELOW_25_PERCENT_AMOUNT_2025 = { + "SINGLE": 197_300, + "SEPARATE": 197_300, + "JOINT": 394_600, + "SURVIVING_SPOUSE": 394_600, + "HEAD_OF_HOUSEHOLD": 197_300, +} + +# Lines 44 and 46 tax amounts at the regular rates. These are the rows of the +# 2025 Tax Computation Worksheet (2025 Instructions for Form 1040, page 80): +# (top of the row, multiplication amount, subtraction amount). The model taxes +# amounts under $100,000 at the same rates rather than at the Tax Table's +# midpoints, and so does this transcription. The 10 and 12 percent rows' +# tops are the lower brackets; each matches the Tax Table row containing it +# (for example, single $48,450-$48,500: $5,579, the tax on $48,475 rounded) +# and the 22 percent row's subtraction amount (for single, 10% of $48,475 +# plus 2% of $11,925 is $5,086). +_UNMARRIED_ROWS = [ + (11_925, 0.10, 0), + (48_475, 0.12, 238.50), + (103_350, 0.22, 5_086), + (197_300, 0.24, 7_153), + (250_525, 0.32, 22_937), +] +TAX_COMPUTATION_ROWS_2025 = { + "SINGLE": _UNMARRIED_ROWS + + [(626_350, 0.35, 30_452.75), (float("inf"), 0.37, 42_979.75)], + "SEPARATE": _UNMARRIED_ROWS + + [(375_800, 0.35, 30_452.75), (float("inf"), 0.37, 37_968.75)], + "JOINT": [ + (23_850, 0.10, 0), + (96_950, 0.12, 477), + (206_700, 0.22, 10_172), + (394_600, 0.24, 14_306), + (501_050, 0.32, 45_874), + (751_600, 0.35, 60_905.50), + (float("inf"), 0.37, 75_937.50), + ], + "HEAD_OF_HOUSEHOLD": [ + (17_000, 0.10, 0), + (64_850, 0.12, 340), + (103_350, 0.22, 6_825), + (197_300, 0.24, 8_892), + (250_500, 0.32, 24_676), + (626_350, 0.35, 32_191), + (float("inf"), 0.37, 44_718), + ], +} +TAX_COMPUTATION_ROWS_2025["SURVIVING_SPOUSE"] = TAX_COMPUTATION_ROWS_2025["JOINT"] + + +def regular_tax_2025(amount, status): + """The 2025 tax on an amount at the regular rates.""" + if amount <= 0: + return 0 + for top, rate, subtraction in TAX_COMPUTATION_ROWS_2025[status]: + if amount <= top: + return rate * amount - subtraction + + +def schedule_d_tax_worksheet_2025(household, line_1, status): + """2025 Schedule D Tax Worksheet, lines 1 to 47, with no Form 4952. + + Schedule D lines 18 and 19 are the household's 28 percent rate gain + (collectibles) and unrecaptured section 1250 gain. Returns the lines. + """ + long_term = household["long_term_gains"] + schedule_d_18 = max(0, long_term) * household.get("collectibles_share", 0) / 100 + schedule_d_19 = max(0, long_term) * household.get("section_1250_share", 0) / 100 + line = {1: line_1} + line[2] = household["qualified_dividends"] + line[3] = line[4] = line[5] = 0 + line[6] = max(0, line[2] - line[5]) + # Schedule D line 15 is the long-term gain, line 16 the net gain. + line[7] = min(long_term, long_term + household["short_term_gains"]) + line[8] = min(line[3], line[4]) + line[9] = max(0, line[7] - line[8]) + line[10] = line[6] + line[9] + line[11] = schedule_d_18 + schedule_d_19 + line[12] = min(line[9], line[11]) + line[13] = line[10] - line[12] + line[14] = max(0, line[1] - line[13]) + line[15] = ZERO_RATE_AMOUNT_2025[status] + line[16] = min(line[1], line[15]) + line[17] = min(line[14], line[16]) + line[18] = max(0, line[1] - line[10]) + line[19] = min(line[1], BELOW_25_PERCENT_AMOUNT_2025[status]) + line[20] = min(line[14], line[19]) + line[21] = max(line[18], line[20]) + line[22] = line[16] - line[17] + for i in range(23, 44): + line[i] = 0 + # "If lines 1 and 16 are the same, skip lines 23 through 43." + if line[1] != line[16]: + line[23] = min(line[1], line[13]) + line[24] = line[22] + line[25] = max(0, line[23] - line[24]) + line[26] = FIFTEEN_PERCENT_RATE_AMOUNT_2025[status] + line[27] = min(line[1], line[26]) + line[28] = line[21] + line[22] + line[29] = max(0, line[27] - line[28]) + line[30] = min(line[25], line[29]) + line[31] = 0.15 * line[30] + line[32] = line[24] + line[30] + # "If lines 1 and 32 are the same, skip lines 33 through 43." + if line[1] != line[32]: + line[33] = line[23] - line[32] + line[34] = 0.20 * line[33] + # "If Schedule D, line 19, is zero or blank, skip lines 35 + # through 40." + if schedule_d_19 > 0: + line[35] = min(line[9], schedule_d_19) + line[36] = line[10] + line[21] + line[37] = line[1] + line[38] = max(0, line[36] - line[37]) + line[39] = max(0, line[35] - line[38]) + line[40] = 0.25 * line[39] + # "If Schedule D, line 18, is zero or blank, skip lines 41 + # through 43." + if schedule_d_18 > 0: + line[41] = line[21] + line[22] + line[30] + line[33] + line[39] + line[42] = line[1] - line[41] + line[43] = 0.28 * line[42] + if line[1] > 0: + # Lines 21, 22, 30, 33, 39 and 42 split line 1: the amounts taxed at + # the regular rates and at 0, 15, 20, 25 and 28 percent. + split = line[21] + line[22] + line[30] + line[33] + line[39] + line[42] + assert split == pytest.approx(line[1], abs=0.01), (household, line) + line[44] = regular_tax_2025(line[21], status) + line[45] = line[31] + line[34] + line[40] + line[43] + line[44] + line[46] = regular_tax_2025(line[1], status) + line[47] = min(line[45], line[46]) + line["schedule_d_19"] = schedule_d_19 + return line + + def capital_gains_worksheet_line_4(household): """Qualified Dividends and Capital Gain Tax Worksheet, lines 2 to 4. @@ -122,6 +298,11 @@ def capital_gains_worksheet_line_4(household): OUTPUTS = [ "filing_status", "taxable_income", + "has_qdiv_or_ltcg", + "dwks10", + "dwks14", + "dwks19", + "income_tax_main_rates", "regular_tax_before_credits", "capital_gains_tax", "amt_income_less_exemptions", @@ -248,6 +429,93 @@ def assert_properties(households, law): assert np.all(np.abs(part_iii - base)[no_gains] <= tolerance(line_12)[no_gains]) +def assert_matches_schedule_d_tax_worksheet(households, law): + """The 2025 model against the transcribed 2025 Schedule D Tax Worksheet. + + For households with no short-term loss, so that Schedule D lines 18 and + 19 are the 28 percent rate gain and unrecaptured section 1250 gain as + entered (a short-term loss would reduce them on the 28% Rate Gain and + Unrecaptured Section 1250 Gain Worksheets, which the model does not do). + """ + for i, h in enumerate(households): + assert h["short_term_gains"] >= 0, h + status = law["filing_status"][i] + assert status == h["status"], (i, status) + line_1 = float(law["taxable_income"][i]) + regular_tax = float(law["income_tax_main_rates"][i]) + float( + law["capital_gains_tax"][i] + ) + if line_1 <= 0: + assert regular_tax == 0, (i, h) + continue + worksheet = schedule_d_tax_worksheet_2025(h, line_1, status) + if law["has_qdiv_or_ltcg"][i]: + for model_line, worksheet_line in ( + ("dwks10", 10), + ("dwks14", 14), + ("dwks19", 21), + ): + assert float(law[model_line][i]) == pytest.approx( + worksheet[worksheet_line], abs=tolerance(line_1) + ), (i, h, model_line) + # Line 45. The model does not take the smaller of lines 45 and 46, + # which differ only when a little gain taxed at 15 percent sits where + # the regular rate is 12 percent. + assert regular_tax == pytest.approx(worksheet[45], abs=tolerance(line_1)), ( + i, + h, + worksheet, + ) + # Form 6251 Part III, with lines 14, 15 and 27 from the worksheet. + line_12 = float(law["amt_income_less_exemptions"][i]) + _, line_38, line_39, line_40 = form_6251_part_iii_2025( + line_12, + worksheet[13], + worksheet[14], + status, + line_14=worksheet["schedule_d_19"], + worksheet_line_10=worksheet[10], + line_27=worksheet[21], + ) + assert float(law["amt_tax_including_cg"][i]) == pytest.approx( + line_38, abs=tolerance(line_12) + ), (i, h) + assert float(law["amt_base_tax"][i]) == pytest.approx( + line_39, abs=tolerance(line_12) + ), (i, h) + # Line 11, with line 10 the worksheet's regular tax (line 45, as + # above) and no foreign tax credit. + amt = max(0, line_40 - worksheet[45]) + assert float(law["alternative_minimum_tax"][i]) == pytest.approx( + amt, abs=tolerance(line_12, line_1) + ), (i, h) + + +def assert_schedule_d_properties(households, law): + """Schedule D Tax Worksheet line 21 (`dwks19`), for every year.""" + line_1 = law["taxable_income"].astype(float) + line_10 = law["dwks10"].astype(float) + line_14 = law["dwks14"].astype(float) + line_21 = law["dwks19"].astype(float) + has_gains = law["has_qdiv_or_ltcg"].astype(bool) + tol = tolerance(line_1) + # Line 18 <= line 21 <= line 14: line 21 is the larger of line 18 and + # the smaller of line 14 and line 19, and line 18 is at most line 14. + line_18 = np.where(has_gains, np.maximum(0, line_1 - line_10), 0) + assert (line_18 <= line_21 + tol).all() + assert (line_21 <= line_14 + tol).all() + # Without 28 percent rate or unrecaptured section 1250 gain, line 13 is + # line 10, so line 21 is line 14 (QDCG Worksheet line 5), bit for bit, + # and Form 6251 line 27 is line 20. + plain = np.array( + [ + h.get("collectibles_share", 0) == 0 and h.get("section_1250_share", 0) == 0 + for h in households + ] + ) + assert np.array_equal(line_21[plain], line_14[plain]) + + # --------------------------------------------------------------------------- # Line 18 as a schedule. # --------------------------------------------------------------------------- @@ -370,6 +638,117 @@ def test_separate_filer_with_line_17_of_136_162_50(): assert law["alternative_minimum_tax"][0] > 0 +# --------------------------------------------------------------------------- +# Households with 28 percent rate or unrecaptured section 1250 gain. +# --------------------------------------------------------------------------- + + +def test_unrecaptured_gain_household_by_hand(): + # 2025, single: $200,000 of wages and $400,000 of long-term gain, of + # which $100,000 is unrecaptured section 1250 gain. Taxable income is + # $584,250 (the standard deduction is $15,750). + # Schedule D Tax Worksheet: line 10 $400,000; line 13 $300,000; line 14 + # $284,250; line 18 $184,250; line 19 $197,300; line 20 $197,300; line + # 21 $197,300; line 22 $0; line 30 $300,000 (15%: $45,000); line 33 $0; + # line 38 $400,000 + $197,300 - $584,250 = $13,050, so line 39 is + # $86,950 (25%: $21,737.50); line 44, the tax on $197,300, is $40,199. + # Line 45: $106,936.50. Taxing line 18 ($184,250) at the regular rates + # and all $100,000 at 25 percent instead gives $107,067. + # Form 6251: line 12 $511,900 ($600,000 less the $88,100 exemption); + # lines 13 to 17: $300,000, $100,000, $400,000, $400,000, $111,900; + # line 18 $29,094; line 20 $284,250; line 21 $0; line 22 $300,000; line + # 27 $197,300 (worksheet line 21); line 29 $336,100; line 30 $300,000 + # (15%: $45,000); line 33 $0; line 36 $100,000 (25%: $25,000). Line 38: + # $99,094. Worksheet line 14 on line 27 would leave $249,150 at 15 + # percent and $50,850 at 20 percent: $101,636.50. + household = { + "status": "SINGLE", + "wages": 200_000, + "qualified_dividends": 0, + "long_term_gains": 400_000, + "short_term_gains": 0, + "section_1250_share": 25, + } + law = calculate([household], 2025) + assert law["taxable_income"][0] == 584_250 + assert law["dwks14"][0] == 284_250 + assert law["dwks19"][0] == 197_300 + regular_tax = law["income_tax_main_rates"][0] + law["capital_gains_tax"][0] + assert regular_tax == pytest.approx(106_936.50, abs=0.01) + assert law["amt_income_less_exemptions"][0] == 511_900 + assert law["amt_tax_including_cg"][0] == pytest.approx(99_094, abs=0.01) + assert law["alternative_minimum_tax"][0] == 0 + assert_matches_schedule_d_tax_worksheet([household], law) + + +def test_rate_gain_household_by_hand(): + # 2025, single: $200,000 of wages and $400,000 of long-term gain, of + # which $100,000 is collectibles gain (28 percent rate gain). Worksheet + # lines 10 to 22 are as in the previous test. Schedule D line 19 is zero, + # so lines 35 to 40 are skipped. Line 41: $197,300 + $0 + $300,000 + $0 + # + $0 = $497,300. Line 42: $86,950 (28%: $24,346). Line 45: $45,000 + + # $24,346 + $40,199 = $109,545. Taxing the whole $100,000 at 28 percent + # on top would tax $13,050 of it twice. + household = { + "status": "SINGLE", + "wages": 200_000, + "qualified_dividends": 0, + "long_term_gains": 400_000, + "short_term_gains": 0, + "collectibles_share": 25, + } + law = calculate([household], 2025) + assert law["dwks19"][0] == 197_300 + regular_tax = law["income_tax_main_rates"][0] + law["capital_gains_tax"][0] + assert regular_tax == pytest.approx(109_545, abs=0.01) + assert_matches_schedule_d_tax_worksheet([household], law) + + +SCHEDULE_D_GRID = [ + { + "status": status, + "wages": wages, + "qualified_dividends": dividends, + "long_term_gains": long_term, + "short_term_gains": short_term, + "section_1250_share": section_1250, + "collectibles_share": collectibles, + "real_estate_taxes": real_estate_taxes, + } + for status in STATUSES + for wages in (0, 60_000, 150_000, 260_000, 700_000) + for real_estate_taxes in (0, 40_000) + for ( + dividends, + long_term, + short_term, + section_1250, + collectibles, + ) in ( + (0, 400_000, 0, 25, 0), + (0, 400_000, 0, 0, 25), + (10_000, 300_000, 20_000, 30, 20), + (0, 100_000, 0, 50, 60), + (25_000, 900_000, 0, 10, 10), + (0, 50_000, 5_000, 100, 0), + (3_000, 0, 0, 0, 0), + (0, 0, 0, 0, 0), + ) +] + + +def test_schedule_d_grid_matches_2025(): + law = calculate(SCHEDULE_D_GRID, 2025) + assert_matches_schedule_d_tax_worksheet(SCHEDULE_D_GRID, law) + assert_properties(SCHEDULE_D_GRID, law) + assert_schedule_d_properties(SCHEDULE_D_GRID, law) + # The grid reaches worksheet line 21 below line 14 (line 19 binding), + # and the alternative minimum tax for such a household. + below = law["dwks19"] < law["dwks14"] + assert below.any() + assert (below & (law["alternative_minimum_tax"] > 0)).any() + + household_strategy = st.fixed_dictionaries( { "status": st.sampled_from(STATUSES), @@ -448,4 +827,32 @@ def test_random_households_keep_the_properties(households, year): for h in households ] households = households + no_gains - assert_properties(households, calculate(households, year)) + law = calculate(households, year) + assert_properties(households, law) + assert_schedule_d_properties(households, law) + + +# Households with 28 percent rate or unrecaptured section 1250 gain and no +# short-term loss (see assert_matches_schedule_d_tax_worksheet). +schedule_d_worksheet_household_strategy = st.builds( + lambda h, short_term, collectibles, section_1250: { + **h, + "short_term_gains": short_term, + "collectibles_share": collectibles, + "section_1250_share": section_1250, + }, + household_strategy, + st.one_of(st.just(0), st.integers(1, 300_000)), + st.one_of(st.just(0), st.integers(1, 100)), + st.one_of(st.just(0), st.integers(1, 100)), +) + + +@hypothesis.settings(**SETTINGS) +@hypothesis.given( + st.lists(schedule_d_worksheet_household_strategy, min_size=1, max_size=25) +) +def test_random_households_match_schedule_d_tax_worksheet_2025(households): + law = calculate(households, 2025) + assert_matches_schedule_d_tax_worksheet(households, law) + assert_schedule_d_properties(households, law) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.py b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.py index 371c3846001..2ec9cec3888 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.py @@ -53,14 +53,14 @@ def formula(tax_unit, period, parameters): # Line 19: 0% LTCG bracket threshold for the filing status. cg_bracket = p.capital_gains.thresholds["1"][filing_status] # Line 20: amount from QDCG Worksheet line 5 or Schedule D Tax - # Worksheet line 14 (as figured for the regular tax). This is the - # ordinary-income portion of taxable income. For a Form 2555 filer + # Worksheet line 14 (as figured for the regular tax): taxable income + # less the gains taxed at 0, 15 and 20 percent. For a Form 2555 filer # it reflects the regular tax capital gain excess, not the AMT one # (26 U.S.C. 911(f)(2)(B)(ii)). - regular_ordinary_income = tax_unit("dwks14", period) + regular_tax_income_less_gains = tax_unit("dwks14", period) # Line 21: Line 19 minus Line 20. Room left in the 0% bracket after # accounting for ordinary income already filling it. - reduced_cg_bracket = max_(0, cg_bracket - regular_ordinary_income) + reduced_cg_bracket = max_(0, cg_bracket - regular_tax_income_less_gains) # Line 22: smaller of Line 12 or Line 13. smaller_of_income_or_cg = min_(reduced_income, cg_distributions) # Line 23: smaller of Line 21 or Line 22. This is the amount taxed at @@ -78,11 +78,18 @@ def formula(tax_unit, period, parameters): second_cg_bracket = p.capital_gains.thresholds["2"][filing_status] # Line 26: same as Line 21. # Line 27: amount from QDCG Worksheet line 5 or Schedule D Tax - # Worksheet line 21 (as figured for the regular tax). For the QDCG - # path this is the same ordinary-income value used on Line 20. + # Worksheet line 21 (as figured for the regular tax), the amount the + # regular tax taxes at the regular rates (26 U.S.C. 1(h)(1)(A)). + # With 28 percent rate or unrecaptured section 1250 gain, Schedule D + # Tax Worksheet line 21 caps line 14 at the top of the 24 percent + # bracket (but not below line 18), so it can be less than Line 20. + # Without them, it is line 14, the same as QDCG Worksheet line 5 and + # Line 20. For a Form 2555 filer it reflects the regular tax capital + # gain excess, not the AMT one. + regular_tax_ordinary_income = tax_unit("dwks19", period) # Line 28: Line 26 plus Line 27. first_cg_bracket_increased_by_ordinary = ( - reduced_cg_bracket + regular_ordinary_income + reduced_cg_bracket + regular_tax_ordinary_income ) # Line 29: Line 25 minus Line 28 (room left in the 15% bracket). reduced_second_cg_bracket = max_( diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/before_credits/tax_at_main_rates.py b/policyengine_us/variables/gov/irs/tax/federal_income/before_credits/tax_at_main_rates.py index ccdad76e0d8..3872a6a46ed 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/before_credits/tax_at_main_rates.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/before_credits/tax_at_main_rates.py @@ -23,3 +23,21 @@ def tax_at_main_rates(taxable_amount, filing_status, bracket): ) bracket_bottom = bracket_top return tax + + +def amount_taxed_below_rate(taxable_amount, filing_status, bracket, rate): + """The part of an amount that the ordinary rate schedule taxes below a rate. + + For example, 26 U.S.C. 1(h)(1)(A)(ii)(I) refers to "the amount of taxable + income taxed at a rate below 25 percent". Brackets are walked and clamped + as in tax_at_main_rates, so the amount is consistent with the tax. + """ + amount = 0 + bracket_bottom = 0 + for i in range(1, len(list(bracket.rates.__iter__())) + 1): + b = str(i) + bracket_top = max_(bracket_bottom, bracket.thresholds[b][filing_status]) + if bracket.rates[b] < rate: + amount += amount_between(taxable_amount, bracket_bottom, bracket_top) + bracket_bottom = bracket_top + return amount diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/capital_gains_excluded_from_taxable_income.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/capital_gains_excluded_from_taxable_income.py index e367ee9c63c..54a2e086e06 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/capital_gains_excluded_from_taxable_income.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/capital_gains_excluded_from_taxable_income.py @@ -20,7 +20,6 @@ class capital_gains_excluded_from_taxable_income(Variable): ] def formula(tax_unit, period, parameters): - p = parameters(period).gov.irs.capital_gains # A taxpayer excluding foreign earned income applies section 1(h) to # taxable income plus the excluded amount, with gains reduced by any # capital gain excess (26 U.S.C. 911(f)). Everyone else's amounts are @@ -30,14 +29,20 @@ def formula(tax_unit, period, parameters): "section_911_adjusted_net_capital_gain", period ) taxable_income = tax_unit("taxable_income_plus_section_911_exclusion", period) - filing_status = tax_unit("filing_status", period) - income_taxed_below_first_rate = clip( - taxable_income, 0, p.thresholds["1"][filing_status] + # Section 1(h)(1)(A) taxes at the regular rates the greater of (i) + # taxable income less net capital gain or (ii) the lesser of (I) the + # taxable income taxed at a rate below 25 percent (the top of the 24 + # percent bracket, not the 0 percent capital gains threshold) or (II) + # taxable income less adjusted net capital gain. Clause (ii) taxes + # the part of any 28 percent rate or unrecaptured section 1250 gain + # that falls in the brackets below 25 percent at the regular rates. + income_taxed_below_25_percent = tax_unit( + "taxable_income_taxed_below_25_percent", period ) reduced_taxable_income = max_( taxable_income - net_capital_gain, min_( - income_taxed_below_first_rate, + income_taxed_below_25_percent, taxable_income - adjusted_net_capital_gain, ), ) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/capital_gains_tax.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/capital_gains_tax.py index 478968aa924..9bef17774d5 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/capital_gains_tax.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/capital_gains_tax.py @@ -5,6 +5,39 @@ ) +def rate_gain_taxed_at_28_percent( + tax_unit, + period, + taxable_income, + regular_rate_income, + adjusted_net_capital_gain, + taxed_unrecaptured_gain, +): + """The amount 26 U.S.C. 1(h)(1)(F) taxes at 28 percent. + + Subparagraph (F) taxes "the amount of taxable income in excess of the + sum of the amounts on which tax is determined under the preceding + subparagraphs" (Schedule D Tax Worksheet lines 41 and 42): taxable + income less the regular rate amount (A), the adjusted net capital gain or + taxable income if less, which (B) to (D) tax between them, and the + unrecaptured section 1250 gain (E) taxes. That is the 28 percent rate gain + less any of it (A) taxes at the regular rates, which happens when it falls + in the brackets below 25 percent. The excess is never more than the 28 + percent rate gain; taking the smaller also keeps rounding from taxing a + household without it. + """ + return min_( + tax_unit("section_911_28_percent_rate_gain", period), + max_( + 0, + taxable_income + - regular_rate_income + - adjusted_net_capital_gain + - taxed_unrecaptured_gain, + ), + ) + + class capital_gains_tax(Variable): value_type = float entity = TaxUnit @@ -95,8 +128,14 @@ def formula(tax_unit, period, parameters): unrecaptured_gain_tax = cg.unrecaptured_s_1250_rate * taxable_unrecaptured_gain - remaining_cg_tax = ( - tax_unit("section_911_28_percent_rate_gain", period) * cg.other_cg_rate + # 26 U.S.C. 1(h)(1)(F): 28 percent of the rest of taxable income. + remaining_cg_tax = cg.other_cg_rate * rate_gain_taxed_at_28_percent( + tax_unit, + period, + taxable_income, + non_cg_taxable_income, + adjusted_net_cg, + taxable_unrecaptured_gain, ) return main_cg_tax + unrecaptured_gain_tax + remaining_cg_tax diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dwks19.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dwks19.py index 6a63ea5dbdd..4e3e4d98268 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dwks19.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dwks19.py @@ -9,20 +9,41 @@ class dwks19(Variable): entity = TaxUnit definition_period = YEAR label = "IRS Form 1040 Schedule D worksheet (part 6 of 6)" + documentation = ( + "Schedule D Tax Worksheet line 21 (line 19 of the 2017 and 2018 " + "worksheets): the part of taxable income taxed at the regular rates, " + "26 U.S.C. 1(h)(1)(A). Form 6251 Part III line 27 uses it." + ) unit = USD + reference = [ + dict( + title="26 U.S. Code § 1(h)(1)(A)", + href="https://www.law.cornell.edu/uscode/text/26/1#h_1_A", + ), + dict( + title="2025 Instructions for Schedule D (Form 1040), Schedule D Tax Worksheet, lines 18 to 21", + href="https://www.irs.gov/pub/irs-pdf/i1040sd.pdf#page=15", + ), + ] def formula(tax_unit, period, parameters): - p = parameters(period).gov.irs.capital_gains dwks14 = tax_unit("dwks14", period) - filing_status = tax_unit("filing_status", period) # Line 1 is taxable income, or for a Form 2555 filer line 3 of the # Foreign Earned Income Tax Worksheet, and line 10 reflects any # capital gain excess. dwks01 = tax_unit("taxable_income_plus_section_911_exclusion", period) - dwks16 = min_(p.thresholds["1"][filing_status], dwks01) - dwks17 = min_(dwks14, dwks16) dwks10 = schedule_d_tax_worksheet_after_capital_gain_excess( tax_unit, period, tax_unit("taxable_income", period) ).line_10 - dwks18 = max_(0, dwks01 - dwks10) - return max_(dwks17, dwks18) * tax_unit("has_qdiv_or_ltcg", period) + # Line 18: line 1 less line 10 (section 1(h)(1)(A)(i)). + line_18 = max_(0, dwks01 - dwks10) + # Line 19: the smaller of line 1 or the top of the 24 percent + # bracket, the taxable income taxed at a rate below 25 percent + # (section 1(h)(1)(A)(ii)(I)). + line_19 = tax_unit("taxable_income_taxed_below_25_percent", period) + # Line 20: the smaller of line 14 or line 19 (section 1(h)(1)(A)(ii)). + line_20 = min_(dwks14, line_19) + # Line 21: the larger of line 18 or line 20. Without 28 percent rate + # or unrecaptured section 1250 gain, line 13 is line 10, so lines 18 + # and 21 are line 14. + return max_(line_18, line_20) * tax_unit("has_qdiv_or_ltcg", period) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/taxable_income_taxed_below_25_percent.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/taxable_income_taxed_below_25_percent.py new file mode 100644 index 00000000000..41e8ee7d39c --- /dev/null +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/taxable_income_taxed_below_25_percent.py @@ -0,0 +1,48 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.irs.tax.federal_income.before_credits.tax_at_main_rates import ( + amount_taxed_below_rate, +) + + +class taxable_income_taxed_below_25_percent(Variable): + value_type = float + entity = TaxUnit + label = "Taxable income taxed at a rate below 25 percent" + unit = USD + documentation = ( + "The amount of taxable income that the regular rate schedule taxes at " + "a rate below 25 percent: under the 10, 12, 22, 24, 32, 35 and 37 " + "percent schedule, taxable income up to the top of the 24 percent " + "bracket. Section 1(h)(1)(A) taxes capital gain at the regular rates " + "up to this amount, or taxable income less adjusted net capital gain " + "if less. Schedule D Tax Worksheet line 19. For a taxpayer excluding " + "foreign earned income, taxable income includes the excluded amount." + ) + definition_period = YEAR + reference = [ + dict( + title="26 U.S. Code § 1(h)(1)(A)(ii)(I)", + href="https://www.law.cornell.edu/uscode/text/26/1#h_1_A_ii_I", + ), + dict( + title="26 U.S. Code § 911(f)", + href="https://www.law.cornell.edu/uscode/text/26/911#f", + ), + dict( + title="2025 Instructions for Schedule D (Form 1040), Schedule D Tax Worksheet, line 19", + href="https://www.irs.gov/pub/irs-pdf/i1040sd.pdf#page=15", + ), + ] + + def formula(tax_unit, period, parameters): + p = parameters(period).gov.irs + # Worksheet line 1: taxable income, or for a Form 2555 filer line 3 + # of the Foreign Earned Income Tax Worksheet (26 U.S.C. 911(f)). + taxable_income = tax_unit("taxable_income_plus_section_911_exclusion", period) + filing_status = tax_unit("filing_status", period) + return amount_taxed_below_rate( + taxable_income, + filing_status, + p.income.bracket, + p.capital_gains.regular_rate_limit, + )