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/feie-tax-stacking-911f.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Tax the income of filers who exclude foreign earned income at the rates it would face on top of the excluded amount, in the regular tax, the capital gains rates and the alternative minimum tax (26 U.S.C. 911(f), the Foreign Earned Income Tax Worksheet).
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from policyengine_us.model_api import *
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 (
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 (
schedule_d_tax_worksheet_after_capital_gain_excess,
)


def create_additional_tax_bracket() -> Reform:
Expand All @@ -15,14 +21,19 @@ class regular_tax_before_credits(Variable):
def formula(tax_unit, period, parameters):
p = parameters(period).gov.irs
filing_status = tax_unit("filing_status", period)
dwks1 = tax_unit("taxable_income", period)
# As in the baseline formula, a Form 2555 filer stacks taxable
# income on the excluded amount (26 U.S.C. 911(f)).
dwks1 = tax_unit("taxable_income_plus_section_911_exclusion", period)
worksheet = schedule_d_tax_worksheet_after_capital_gain_excess(
tax_unit, period, tax_unit("taxable_income", period)
)

dwks16 = min_(p.capital_gains.thresholds["1"][filing_status], dwks1)
dwks17 = min_(tax_unit("dwks14", period), dwks16)
dwks20 = dwks16 - dwks17
lowest_rate_tax = p.capital_gains.rates["1"] * dwks20
# Break in worksheet lines
dwks13 = tax_unit("dwks13", period)
dwks13 = worksheet.line_13
dwks21 = min_(dwks1, dwks13)
dwks22 = dwks20
dwks23 = max_(0, dwks21 - dwks22)
Expand All @@ -36,11 +47,8 @@ def formula(tax_unit, period, parameters):
dwks31 = dwks21 - dwks30
dwks32 = p.capital_gains.rates["3"] * dwks31
# Break in worksheet lines
dwks33 = min_(
tax_unit("dwks09", period),
add(tax_unit, period, ["unrecaptured_section_1250_gain"]),
)
dwks10 = tax_unit("dwks10", period)
dwks33 = min_(worksheet.line_9, worksheet.unrecaptured_section_1250_gain)
dwks10 = worksheet.line_10
dwks34 = dwks10 + dwks19
dwks36 = max_(0, dwks34 - dwks1)
dwks37 = max_(0, dwks33 - dwks36)
Expand All @@ -54,25 +62,18 @@ def formula(tax_unit, period, parameters):
dwks41 = p.amt.brackets.rates[-1] * dwks40

# Compute regular tax using bracket rates and thresholds
# The shared schedule clamps inverted brackets as in the
# baseline formulas (#9084).
reg_taxinc = max_(0, dwks19)
p_reform = parameters(period).gov.contrib.additional_tax_bracket
bracket_tops = p_reform.bracket.thresholds
bracket_rates = p_reform.bracket.rates
reg_tax = 0
bracket_bottom = 0
for i in range(1, len(list(bracket_rates.__iter__())) + 1):
b = str(i)
# Clamp as in the baseline formulas (#9084): an
# inverted bracket contributes zero width.
bracket_top = max_(bracket_bottom, bracket_tops[b][filing_status])
reg_tax += bracket_rates[b] * amount_between(
reg_taxinc, bracket_bottom, bracket_top
)
bracket_bottom = bracket_top
bracket = parameters(period).gov.contrib.additional_tax_bracket.bracket
reg_tax = tax_at_main_rates(reg_taxinc, filing_status, bracket)

# Return to worksheet lines
dwks42 = reg_tax
dwks43 = dwks29 + dwks32 + dwks38 + dwks41 + dwks42 + lowest_rate_tax
excluded = max_(0, tax_unit("foreign_earned_income_exclusion", period))
tax_on_excluded = tax_at_main_rates(excluded, filing_status, bracket)
dwks43 = where(excluded > 0, max_(0, dwks43 - tax_on_excluded), dwks43)
dwks44 = tax_unit("income_tax_main_rates", period)
dwks45 = min_(dwks43, dwks44)
return where(tax_unit("has_qdiv_or_ltcg", period), dwks45, dwks44)
Expand All @@ -82,33 +83,31 @@ class income_tax_main_rates(Variable):
entity = TaxUnit
definition_period = YEAR
label = "Income tax main rates"
reference = "https://www.law.cornell.edu/uscode/text/26/1"
reference = [
"https://www.law.cornell.edu/uscode/text/26/1",
"https://www.law.cornell.edu/uscode/text/26/911#f_1_A",
]
unit = USD

def formula(tax_unit, period, parameters):
# compute taxable income that is taxed at the main rates
full_taxable_income = tax_unit("taxable_income", period)
# compute taxable income that is taxed at the main rates; as in
# the baseline formula, a taxpayer excluding foreign earned
# income adds the excluded amount back (26 U.S.C. 911(f)(1)(A))
full_taxable_income = tax_unit(
"taxable_income_plus_section_911_exclusion", period
)
cg_exclusion = tax_unit(
"capital_gains_excluded_from_taxable_income", period
)
taxinc = max_(0, full_taxable_income - cg_exclusion)
# compute tax using bracket rates and thresholds
p = parameters(period).gov.contrib.additional_tax_bracket
bracket_tops = p.bracket.thresholds
bracket_rates = p.bracket.rates
bracket = parameters(period).gov.contrib.additional_tax_bracket.bracket
filing_status = tax_unit("filing_status", period)
tax = 0
bracket_bottom = 0
for i in range(1, len(list(bracket_rates.__iter__())) + 1):
b = str(i)
# Clamp as in the baseline formulas (#9084): an
# inverted bracket contributes zero width.
bracket_top = max_(bracket_bottom, bracket_tops[b][filing_status])
tax += bracket_rates[b] * amount_between(
taxinc, bracket_bottom, bracket_top
)
bracket_bottom = bracket_top
return tax
tax = tax_at_main_rates(taxinc, filing_status, bracket)
# less the tax on the excluded amount alone
excluded = max_(0, tax_unit("foreign_earned_income_exclusion", period))
tax_on_excluded = tax_at_main_rates(excluded, filing_status, bracket)
return where(excluded > 0, max_(0, tax - tax_on_excluded), tax)

class reform(Reform):
def apply(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from policyengine_us.model_api import *
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,
)


def create_capital_gains_tax_increase() -> Reform:
Expand All @@ -10,10 +13,16 @@ class capital_gains_tax(Variable):
definition_period = YEAR

def formula(tax_unit, period, parameters):
net_cg = tax_unit("net_capital_gain", period)
taxable_income = tax_unit("taxable_income", period)
# As in the baseline formula, a taxpayer excluding foreign earned
# income applies the rates to taxable income plus the excluded
# amount, with gains reduced by any capital gain excess
# (26 U.S.C. 911(f)).
net_cg = tax_unit("section_911_net_capital_gain", period)
taxable_income = tax_unit(
"taxable_income_plus_section_911_exclusion", period
)
adjusted_net_cg = min_(
tax_unit("adjusted_net_capital_gain", period),
tax_unit("section_911_adjusted_net_capital_gain", period),
taxable_income,
) # ANCG is referred to in all cases as ANCG or taxable income if less.

Expand Down Expand Up @@ -42,7 +51,12 @@ def formula(tax_unit, period, parameters):

p_reform = parameters(period).gov.contrib.biden.budget_2025.capital_gains
income_threshold = p_reform.income_threshold[filing_status]
excess_income = max_(0, taxable_income - income_threshold)
# The threshold tests the filer's own income, which leaves out an
# amount excluded under section 911, so it uses taxable income
# before the excluded amount is added back; the rates still stack.
excess_income = max_(
0, tax_unit("taxable_income", period) - income_threshold
)

cg_in_top_bracket = min_(adjusted_net_cg, excess_income)

Expand Down Expand Up @@ -90,14 +104,18 @@ def formula(tax_unit, period, parameters):
)

unrecaptured_s_1250_gain = tax_unit(
"unrecaptured_section_1250_gain", period
"section_911_unrecaptured_section_1250_gain", period
)

qualified_dividends = add(tax_unit, period, ["qualified_dividend_income"])

# Net capital gain determined without regard to section 1(h)(11),
# reduced first by any capital gain excess (26 U.S.C.
# 911(f)(2)(A)(i)).
net_cg_other_than_dividends = (
section_911_net_capital_gain_other_than_dividends(tax_unit, period)
)
max_taxable_unrecaptured_gain = min_(
unrecaptured_s_1250_gain,
max_(0, net_cg - qualified_dividends),
net_cg_other_than_dividends,
)
unrecaptured_gain_deduction = max_(
non_cg_taxable_income + net_cg - taxable_income,
Expand All @@ -113,8 +131,7 @@ def formula(tax_unit, period, parameters):
)

remaining_cg_tax = (
tax_unit("capital_gains_28_percent_rate_gain", period)
* cg.other_cg_rate
tax_unit("section_911_28_percent_rate_gain", period) * cg.other_cg_rate
)
return main_cg_tax + unrecaptured_gain_tax + remaining_cg_tax

Expand Down
8 changes: 6 additions & 2 deletions policyengine_us/reforms/crfb/non_refundable_ss_credit.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ class highest_tax_rate(Variable):
unit = "/1"

def formula(tax_unit, period, parameters):
# compute taxable income that is taxed at the main rates
full_taxable_income = tax_unit("taxable_income", period)
# compute taxable income that is taxed at the main rates; a
# taxpayer excluding foreign earned income adds the excluded
# amount back (26 U.S.C. 911(f)(1)(A))
full_taxable_income = tax_unit(
"taxable_income_plus_section_911_exclusion", period
)
cg_exclusion = tax_unit(
"capital_gains_excluded_from_taxable_income", period
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from policyengine_us.model_api import *
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,
)


def create_harris_capital_gains() -> Reform:
Expand All @@ -10,10 +13,16 @@ class capital_gains_tax(Variable):
definition_period = YEAR

def formula(tax_unit, period, parameters):
net_cg = tax_unit("net_capital_gain", period)
taxable_income = tax_unit("taxable_income", period)
# As in the baseline formula, a taxpayer excluding foreign earned
# income applies the rates to taxable income plus the excluded
# amount, with gains reduced by any capital gain excess
# (26 U.S.C. 911(f)).
net_cg = tax_unit("section_911_net_capital_gain", period)
taxable_income = tax_unit(
"taxable_income_plus_section_911_exclusion", period
)
adjusted_net_cg = min_(
tax_unit("adjusted_net_capital_gain", period),
tax_unit("section_911_adjusted_net_capital_gain", period),
taxable_income,
) # ANCG is referred to in all cases as ANCG or taxable income if less.

Expand Down Expand Up @@ -85,12 +94,17 @@ def formula(tax_unit, period, parameters):
)

unrecaptured_s_1250_gain = tax_unit(
"unrecaptured_section_1250_gain", period
"section_911_unrecaptured_section_1250_gain", period
)
# Net capital gain determined without regard to section 1(h)(11),
# reduced first by any capital gain excess (26 U.S.C.
# 911(f)(2)(A)(i)).
net_cg_other_than_dividends = (
section_911_net_capital_gain_other_than_dividends(tax_unit, period)
)
qualified_dividends = add(tax_unit, period, ["qualified_dividend_income"])
max_taxable_unrecaptured_gain = min_(
unrecaptured_s_1250_gain,
max_(0, net_cg - qualified_dividends),
net_cg_other_than_dividends,
)
unrecaptured_gain_deduction = max_(
non_cg_taxable_income + net_cg - taxable_income,
Expand All @@ -106,8 +120,7 @@ def formula(tax_unit, period, parameters):
)

remaining_cg_tax = (
tax_unit("capital_gains_28_percent_rate_gain", period)
* cg.other_cg_rate
tax_unit("section_911_28_percent_rate_gain", period) * cg.other_cg_rate
)

return main_cg_tax + unrecaptured_gain_tax + remaining_cg_tax
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,34 @@
output:
# First 100,000 taxed at 20%
# Last 100,000 taxed at 37%
capital_gains_tax: 57_000 # 15,000 + 37,000
capital_gains_tax: 57_000 # 20,000 + 37,000

- name: A section 911 exclusion stacks the rates but not the $1 million test
period: 2024
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: 36
qualified_dividend_income: 0
long_term_capital_gains: 80_000
spouse:
age: 36
qualified_dividend_income: 120_000
long_term_capital_gains: 0
tax_units:
tax_unit:
members: [head, spouse]
taxable_income: 1_100_000
filing_status: JOINT
foreign_earned_income_exclusion: 300_000
output:
# The threshold is tested on taxable income without the excluded amount,
# so as in the first case 100,000 of the gains is over it: 100,000 at
# 20% and 100,000 at 37%. Testing it on taxable income plus the excluded
# amount would put all 200,000 at 37%.
capital_gains_tax: 57_000 # 20,000 + 37,000

- name: Reform not applied
period: 2024
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,48 @@
# Section 911 filer with positive taxable income: the CTC's tax limit sees the
# tax figured on the Foreign Earned Income Tax Worksheet (26 U.S.C. 911(f)).
- name: Head of household with two children, positive taxable income and a section 911 exclusion in 2025.
period: 2025
absolute_error_margin: 0.01
input:
people:
parent:
age: 40
# Wages included in gross income; $40,000 of foreign wages is
# excluded on Form 2555.
employment_income: 60_000
child1:
age: 5
child2:
age: 8
tax_units:
tax_unit:
members: [parent, child1, child2]
foreign_earned_income_exclusion: 40_000
households:
household:
members: [parent, child1, child2]
state_code: TX
output:
filing_status: HEAD_OF_HOUSEHOLD
# $60,000 less the $23,625 head of household standard deduction.
taxable_income: 36_375
# Foreign Earned Income Tax Worksheet (Form 1040 line 16): line 3 =
# $76,375, line 4 = $9,977.50 (Tax Table $9,978), line 5 = tax on
# $40,000 = $4,460 (Tax Table $4,463), line 6 = $5,517.50 (Tax Table
# $5,515). Taxed from the bottom bracket it would be $4,025.
income_tax_before_credits: 5_517.50
eitc: 0
# Schedule 8812 line 12: 2 x $2,200.
ctc: 4_400
# Credit Limit Worksheet A sees the stacked tax, which covers the whole
# credit, so nothing is left to refund (section 24(d)(3) would bar the
# refund in any case).
ctc_limiting_tax_liability: 5_517.50
refundable_ctc: 0
non_refundable_ctc: 4_400
income_tax_before_refundable_credits: 1_117.50
income_tax: 1_117.50

- name: Single parent of 10 children aged 6 to 17, $200k income during ARP.
period: 2021
absolute_error_margin: 0
Expand Down
Loading
Loading