Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fix-amt-cg-bracket-overflow.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed the AMT capital-gains calculation to read the regular-tax ordinary-income portion (Schedule D Tax Worksheet line 14 / line 21) for Form 6251 Part III line 27, so that filers whose AMTI minus exemption is below the 20% LTCG bracket threshold no longer overflow gains into the 20% bracket.
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,40 @@
capital_gains_tax: 0
output:
alternative_minimum_tax: 1_000

# Locks in the fix for taxsim #424 / issue #8292. Pre-fix, Form 6251
# Part III Line 27 read `loss_limited_net_capital_gains` (the LTCG
# amount) instead of `dwks14` (the regular-tax ordinary-income portion),
# which shrank the 15% bracket window and overflowed gains into the 20%
# bracket even though AMTI - exemption sat well below the $583,750 MFJ
# 20% threshold. The reported $8,634 AMT add-on should be $0 for any
# year the bug existed (2018-2025).
- name: AMT add-on is zero for MFJ filer whose AMTI minus exemption is below the 20% cap gains threshold (taxsim #424)
absolute_error_margin: 50
period: 2024
input:
people:
head:
age: 56
qualified_dividend_income: 65_148.79
taxable_interest_income: 20_301.98
long_term_capital_gains: 390_720.38
taxable_pension_income: 33_581.32
is_tax_unit_head: true
spouse:
age: 20
is_tax_unit_spouse: true
tax_units:
tax_unit:
members: [head, spouse]
filing_status: JOINT
tax_unit_itemizes: false
spm_units:
spm_unit:
members: [head, spouse]
households:
household:
members: [head, spouse]
state_code: NM
output:
alternative_minimum_tax: 0
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
dwks14: 0
unrecaptured_section_1250_gain: 0
output:
amt_tax_including_cg: 22_700
# AMTI - exemption = $100k; ordinary AMTI = $70k * 26% = $18,200.
# Preferential base = min($100k, $30k) = $30k, all in the 0% bracket
# (room = $44,625 single, ordinary regular income = $0).
amt_tax_including_cg: 18_200

- name: AMT with capital gains in second bracket
period: 2023
Expand All @@ -44,7 +47,11 @@
dwks14: 0
unrecaptured_section_1250_gain: 0
output:
amt_tax_including_cg: 144_086
# Ordinary AMTI tax: 26% * $220,700 + 28% * $229,300 = $121,586.
# Preferential base $150k: 0% bracket consumes $44,625; remaining
# $105,375 at 15% = $15,806.25; nothing reaches the 20% bracket
# (room = $492,300 - $44,625 = $447,675).
amt_tax_including_cg: 137_392.25

- name: AMT with unrecaptured section 1250 gains
period: 2023
Expand All @@ -56,4 +63,96 @@
dwks14: 0
unrecaptured_section_1250_gain: 200_000
output:
amt_tax_including_cg: 230_586
# Ordinary AMTI tax: 26% * $220,700 + 28% * $279,300 = $135,586.
# Preferential base $300k: 0% bracket consumes $44,625; remaining
# $255,375 at 15% = $38,306.25. The $200k unrecaptured section 1250
# gain stays in line 36 = $1M - ($500k + $300k) = $200k at 25% =
# $50,000.
amt_tax_including_cg: 223_892.25

# Boundary at the 0% LTCG threshold ($44,625 single 2023). All
# preferential base falls inside the 0% bracket, so the entire
# computation should collapse to ordinary AMTI * 26%.
- name: All CG, preferential base exactly at the 0% bracket threshold
period: 2023
input:
filing_status: SINGLE
amt_income_less_exemptions: 44_625
dwks10: 44_625
dwks13: 44_625
dwks14: 0
unrecaptured_section_1250_gain: 0
output:
# L22 = L23 = $44,625; L24 = $0; nothing in 15% or 20%; L17 = $0.
amt_tax_including_cg: 0

# Exercises the 15%/20% boundary: AMTI - exemption equals the single
# 20% bracket threshold ($492,300). Everything above $44,625 is in
# the 15% bracket, nothing in the 20% bracket.
- name: All CG, preferential base exactly at the 20% bracket threshold
period: 2023
input:
filing_status: SINGLE
amt_income_less_exemptions: 492_300
dwks10: 492_300
dwks13: 492_300
dwks14: 0
unrecaptured_section_1250_gain: 0
output:
# 0% bracket consumes $44,625; remaining $447,675 at 15% =
# $67,151.25; nothing reaches 20%.
amt_tax_including_cg: 67_151.25

# Exercises the actual 20% bracket (regression for the L27 fix —
# pre-fix this case would have over-allocated to the 20% bracket).
- name: All CG, preferential base above the 20% bracket threshold
period: 2023
input:
filing_status: SINGLE
amt_income_less_exemptions: 600_000
dwks10: 600_000
dwks13: 600_000
dwks14: 0
unrecaptured_section_1250_gain: 0
output:
# 0%: $44,625; 15%: $447,675 (= $492,300 - $44,625) * 15% =
# $67,151.25; 20%: $107,700 (= $600,000 - $44,625 - $447,675)
# * 20% = $21,540. Total $88,691.25.
amt_tax_including_cg: 88_691.25

# Locks in the L27 fix specifically: with non-zero regular-tax
# ordinary income, the 0% bracket room shrinks and the 15% bracket
# starts higher. Pre-fix would have used LTCG amount instead of
# ordinary income on L27 and shrunk the 15% window incorrectly.
- name: Mixed preferential + regular ordinary income, AMTI below 20% threshold
period: 2023
input:
filing_status: SINGLE
amt_income_less_exemptions: 300_000
dwks10: 100_000
dwks13: 100_000
dwks14: 50_000
unrecaptured_section_1250_gain: 0
output:
# Ordinary AMTI tax: 26% * $200,000 = $52,000.
# 0% room: max(0, $44,625 - $50,000) = $0 -> nothing at 0%.
# 15% room: $492,300 - $50,000 = $442,300; cap gain $100,000
# entirely at 15% = $15,000.
# Total: $52,000 + $15,000 = $67,000.
amt_tax_including_cg: 67_000

# Joint filer using the joint 20% bracket threshold ($553,850 in
# 2023). Same logic as the issue 8292 reproducer.
- name: Joint filer with substantial cap gains below the joint 20% threshold
period: 2023
input:
filing_status: JOINT
amt_income_less_exemptions: 400_000
dwks10: 400_000
dwks13: 400_000
dwks14: 0
unrecaptured_section_1250_gain: 0
output:
# 0%: $89,250 (joint); 15%: $310,750 * 15% = $46,612.50;
# 20%: $0.
amt_tax_including_cg: 46_612.50
Original file line number Diff line number Diff line change
Expand Up @@ -11,76 +11,82 @@ class amt_tax_including_cg(Variable):
reference = "https://www.irs.gov/pub/irs-pdf/f6251.pdf"

def formula(tax_unit, period, parameters):
# Line 12
# Form 6251 Part III, Line 12: AMTI minus exemption
reduced_income = tax_unit("amt_income_less_exemptions", period)
# Line 13 - schedule D line 13
# Line 13: amount from QDCG Worksheet line 4 or Schedule D Tax
# Worksheet line 13 (qualified dividends + LTCG net of unrecaptured
# section 1250 and 28%-rate gains).
cg_distributions = tax_unit("dwks13", period)
# Line 14 - schedule D line 19
# Line 14: Schedule D line 19 (unrecaptured section 1250 gain).
section_1250_gain_worksheet = tax_unit("unrecaptured_section_1250_gain", period)
# Line 15 - smaller of the sum of Line 13 and Line 14 or Schedule D line 10
# Line 15: smaller of (Line 13 + Line 14) or Schedule D Tax Worksheet
# line 10.
total_transactions_reported = tax_unit("dwks10", period)
capped_capital_gains = min_(
cg_distributions + section_1250_gain_worksheet,
total_transactions_reported,
)
# Line 16 - smaller of Line 15 or Line 12
# Line 16: smaller of Line 12 or Line 15.
capped_income = min_(capped_capital_gains, reduced_income)
# Line 17 - Line 12 minus Line 16
# Line 17: Line 12 minus Line 16 (ordinary AMTI).
excess_income = max_(0, reduced_income - capped_income)
# Line 18 - Apply CG tax rates to Line 17
# Line 18: apply the 26%/28% AMT bracket to Line 17.
p = parameters(period).gov.irs
income_taxes_at_amt_rates = p.income.amt.brackets.calc(excess_income)
# Line 19 - First CG tax bracket threshold
# Line 19: 0% LTCG bracket threshold for the filing status.
filing_status = tax_unit("filing_status", period)
cg_bracket = p.capital_gains.thresholds["1"][filing_status]
# Line 20 - Schedule D Line 14
lt_capital_loss_carryover = tax_unit("dwks14", period)
# Line 21 - Line 20 minus Line 19
reduced_cg_bracket = max_(0, cg_bracket - lt_capital_loss_carryover)
# Line 22 - smaller of Line 12 or Line 13
# 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.
regular_ordinary_income = 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)
# Line 22: smaller of Line 12 or Line 13.
smaller_of_income_or_cg = min_(reduced_income, cg_distributions)
# Line 23 - smaller of Line 22 or Line 21 (amount is taxed at 0%)
# Line 23: smaller of Line 21 or Line 22. This is the amount taxed at
# 0% (not the tax — the form re-uses this amount in Line 24 and Line
# 32, so we keep the quantity here rather than collapsing to the tax).
amount_at_first_rate = min_(smaller_of_income_or_cg, reduced_cg_bracket)
cg_first_rate = p.capital_gains.rates["1"]
disregarded_gains = (
min_(smaller_of_income_or_cg, reduced_cg_bracket) * cg_first_rate
cg_first_bracket_tax = amount_at_first_rate * cg_first_rate
# Line 24: Line 22 minus Line 23 (preferential-rate base remaining
# after the 0% bracket).
income_after_first_rate = max_(
0, smaller_of_income_or_cg - amount_at_first_rate
)
# Line 24 - Line 22 minus Line 23
taxable_income_including_cg = max_(
smaller_of_income_or_cg - disregarded_gains, 0
)
# Line 25 - Second CG tax bracket threshold
# Line 25: 20% LTCG bracket threshold for the filing status.
second_cg_bracket = p.capital_gains.thresholds["2"][filing_status]
# Line 26 - same as line 21
# Line 27 - Schedule D Line 21
loss_limited_net_capital_gains = tax_unit(
"loss_limited_net_capital_gains", period
)
# Line 28 - Line 26 plus Line 27
first_cg_bracket_increased_by_loss = (
loss_limited_net_capital_gains + reduced_cg_bracket
# 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.
# Line 28: Line 26 plus Line 27.
first_cg_bracket_increased_by_ordinary = (
reduced_cg_bracket + regular_ordinary_income
)
# Line 29 Line 25 minus Line 28
# Line 29: Line 25 minus Line 28 (room left in the 15% bracket).
reduced_second_cg_bracket = max_(
0, second_cg_bracket - first_cg_bracket_increased_by_loss
)
# Line 30 - smaller of Line 24 or Line 29
capped_income_including_cg = min_(
taxable_income_including_cg, reduced_second_cg_bracket
0, second_cg_bracket - first_cg_bracket_increased_by_ordinary
)
# Line 31 - multiply Line 30by second CG tax rate
cg_second_bracket_tax = capped_income_including_cg * p.capital_gains.rates["2"]
# Line 32 - Line 23 plus Line 30
taxed_gains = disregarded_gains + capped_income_including_cg
# Line 33 - Line 22 minus Line 32
excess_taxed_gains = max_(0, smaller_of_income_or_cg - taxed_gains)
# Line 34 - multiply Line 33 by third CG tax rate
# Line 30: smaller of Line 24 or Line 29 (amount taxed at 15%).
amount_at_second_rate = min_(income_after_first_rate, reduced_second_cg_bracket)
# Line 31: Line 30 times the 15% LTCG rate.
cg_second_bracket_tax = amount_at_second_rate * p.capital_gains.rates["2"]
# Line 32: Line 23 plus Line 30 (cumulative preferential amounts).
taxed_gains_amount = amount_at_first_rate + amount_at_second_rate
# Line 33: Line 22 minus Line 32 (amount taxed at 20%).
excess_taxed_gains = max_(0, smaller_of_income_or_cg - taxed_gains_amount)
# Line 34: Line 33 times the 20% LTCG rate.
cg_third_bracket_tax = excess_taxed_gains * p.capital_gains.rates["3"]
# Line 35 - sum of Line 17, Line 32, Line 33
final_taxed_income = excess_income + taxed_gains + excess_taxed_gains
# Line 36 Line 12 minus Line 35
# Line 35: Line 17 plus Line 32 plus Line 33.
final_taxed_income = excess_income + taxed_gains_amount + excess_taxed_gains
# Line 36: Line 12 minus Line 35 (unrecaptured section 1250 portion).
final_excess = max_(0, reduced_income - final_taxed_income)
# Line 37 - Multiply Line 36 by the AMT specific capital gain excess tax rate
# The increased tax does not applied if no recaptured section 1250 gains
# Line 37: Line 36 times the 25% rate, only when unrecaptured section
# 1250 gains exist (per the form's "skip lines 35-37 if line 14 is
# zero" instruction).
unrecaptured_section_1250_gain = tax_unit(
"unrecaptured_section_1250_gain", period
)
Expand All @@ -89,9 +95,12 @@ def formula(tax_unit, period, parameters):
0,
final_excess * p.income.amt.capital_gains.capital_gain_excess_tax_rate,
)
# Line 38 - sum of Line 18, Line 31, Line 34, and Line 37
# Line 38: sum of Lines 18, 31, 34, and 37 (plus Line 23's 0%
# contribution, which is always zero with a 0% bracket rate but kept
# in the sum for structural clarity).
return (
income_taxes_at_amt_rates
+ cg_first_bracket_tax
+ cg_second_bracket_tax
+ cg_third_bracket_tax
+ excess_tax
Expand Down
Loading