Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
description: Whether long-term capital gains cost basis is indexed to inflation.
values:
2013-01-01: false
metadata:
unit: bool
period: year
label: Capital gains basis indexation applies
reference:
- title: Yale Budget Lab, Indexing Capital Gains to Inflation
href: https://budgetlab.yale.edu/research/indexing-capital-gains-inflation
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
description: Whether basis indexation is capped so it cannot turn a long-term capital gain into a loss.
values:
2013-01-01: true
metadata:
unit: bool
period: year
label: Capital gains basis indexation no-loss cap
reference:
- title: Yale Budget Lab, Indexing Capital Gains to Inflation
href: https://budgetlab.yale.edu/research/indexing-capital-gains-inflation
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
description: Capital gains basis indexation uses chained CPI-U from this calendar year onward.
values:
2013-01-01: 2017
metadata:
unit: year
period: year
label: Capital gains basis indexation chained CPI transition year
reference:
- title: Yale Budget Lab, Indexing Capital Gains to Inflation
href: https://budgetlab.yale.edu/research/indexing-capital-gains-inflation

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
description: Minimum holding period required for long-term capital gains basis indexation.
values:
2013-01-01: 1
metadata:
unit: year
period: year
label: Capital gains basis indexation minimum holding period
reference:
- title: Yale Budget Lab, Indexing Capital Gains to Inflation
href: https://budgetlab.yale.edu/research/indexing-capital-gains-inflation
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
description: Assets must be purchased after this calendar year to qualify for long-term capital gains basis indexation.
values:
2013-01-01: 0
metadata:
unit: year
period: year
label: Capital gains basis indexation purchase year threshold
reference:
- title: Yale Budget Lab, Indexing Capital Gains to Inflation
href: https://budgetlab.yale.edu/research/indexing-capital-gains-inflation
44 changes: 44 additions & 0 deletions policyengine_us/reforms/capital_gains_indexation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from policyengine_us.model_api import *
from policyengine_core.periods import instant


def create_capital_gains_indexation_reform(
purchase_year_threshold: int,
minimum_holding_period: int = 1,
cap_adjustment_to_gain: bool = True,
):
def modify_parameters(parameters):
indexation = parameters.gov.irs.capital_gains.indexation
start = instant("2026-01-01")
stop = instant("2100-12-31")
indexation.applies.update(start=start, stop=stop, value=True)
indexation.minimum_holding_period.update(
start=start, stop=stop, value=minimum_holding_period
)
indexation.purchase_year_threshold.update(
start=start, stop=stop, value=purchase_year_threshold
)
indexation.cap_adjustment_to_gain.update(
start=start, stop=stop, value=cap_adjustment_to_gain
)
return parameters

class reform(Reform):
def apply(self):
self.modify_parameters(modify_parameters)

return reform


capital_gains_indexation_prospective = create_capital_gains_indexation_reform(
purchase_year_threshold=2025
)
capital_gains_indexation_retrospective = create_capital_gains_indexation_reform(
purchase_year_threshold=0
)
capital_gains_indexation_retrospective_losses_allowed = (
create_capital_gains_indexation_reform(
purchase_year_threshold=0,
cap_adjustment_to_gain=False,
)
)
123 changes: 123 additions & 0 deletions policyengine_us/tests/policy/contrib/capital_gains_indexation.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
- name: Case 1, inactive capital gains basis indexation leaves long-term gains unchanged.
period: 2026
input:
people:
person1:
long_term_capital_gains_before_response: 5_000
long_term_capital_gains_basis: 10_000
long_term_capital_gains_years_held: 10
tax_units:
tax_unit:
members: [person1]
output:
tax_unit_long_term_capital_gains_indexation_adjustment: 0
long_term_capital_gains_indexation_adjustment: [0]
long_term_capital_gains: [5_000]

- name: Case 2, retrospective indexation applies the no-loss cap.
period: 2026
reforms: policyengine_us.reforms.capital_gains_indexation.capital_gains_indexation_retrospective
input:
people:
person1:
long_term_capital_gains_before_response: 100
long_term_capital_gains_basis: 1_000_000
long_term_capital_gains_years_held: 10
tax_units:
tax_unit:
members: [person1]
output:
tax_unit_long_term_capital_gains_indexation_adjustment: 100
long_term_capital_gains_indexation_adjustment: [100]
long_term_capital_gains: [0]

- name: Case 3, prospective indexation excludes assets purchased before 2026.
period: 2026
reforms: policyengine_us.reforms.capital_gains_indexation.capital_gains_indexation_prospective
input:
people:
person1:
long_term_capital_gains_before_response: 5_000
long_term_capital_gains_basis: 10_000
long_term_capital_gains_years_held: 10
tax_units:
tax_unit:
members: [person1]
output:
tax_unit_long_term_capital_gains_indexation_adjustment: 0
long_term_capital_gains_indexation_adjustment: [0]
long_term_capital_gains: [5_000]

- name: Case 4, prospective indexation applies to assets purchased after 2025.
period: 2027
reforms: policyengine_us.reforms.capital_gains_indexation.capital_gains_indexation_prospective
input:
people:
person1:
long_term_capital_gains_before_response: 100
long_term_capital_gains_basis: 1_000_000
long_term_capital_gains_years_held: 1.1
tax_units:
tax_unit:
members: [person1]
output:
tax_unit_long_term_capital_gains_indexation_adjustment: 100
long_term_capital_gains_indexation_adjustment: [100]
long_term_capital_gains: [0]

- name: Case 5, tax-unit indexation allocates a capped mixed-spouse adjustment to the gain holder.
period: 2026
reforms: policyengine_us.reforms.capital_gains_indexation.capital_gains_indexation_retrospective
input:
people:
person1:
long_term_capital_gains_before_response: 500
long_term_capital_gains_basis: 800_000
long_term_capital_gains_years_held: 10
person2:
long_term_capital_gains_before_response: -200
long_term_capital_gains_basis: 200_000
long_term_capital_gains_years_held: 10
tax_units:
tax_unit:
members: [person1, person2]
output:
tax_unit_long_term_capital_gains_indexation_adjustment: 300
long_term_capital_gains_indexation_adjustment: [300, 0]
long_term_capital_gains: [200, -200]
net_capital_gains: 0

- name: Case 6, no-loss cap prevents larger net long-term losses.
period: 2026
reforms: policyengine_us.reforms.capital_gains_indexation.capital_gains_indexation_retrospective
input:
people:
person1:
long_term_capital_gains_before_response: -500
long_term_capital_gains_basis: 1_000_000
long_term_capital_gains_years_held: 10
tax_units:
tax_unit:
members: [person1]
output:
tax_unit_long_term_capital_gains_indexation_adjustment: 0
long_term_capital_gains_indexation_adjustment: [0]
long_term_capital_gains: [-500]

- name: Case 7, losses-allowed benchmark can increase net long-term losses.
period: 2026
absolute_error_margin: 0.01
reforms: policyengine_us.reforms.capital_gains_indexation.capital_gains_indexation_retrospective_losses_allowed
input:
people:
person1:
long_term_capital_gains_before_response: -500
long_term_capital_gains_basis: 1_000_000
long_term_capital_gains_years_held: 10
tax_units:
tax_unit:
members: [person1]
output:
tax_unit_long_term_capital_gains_indexation_adjustment: 334_063.66
long_term_capital_gains_indexation_adjustment: [334_063.66]
long_term_capital_gains: [-334_563.66]
11 changes: 11 additions & 0 deletions policyengine_us/tests/test_system_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,14 @@ def test_computed_default_uprated_variables_have_microdata_overrides():
missing_overrides.append(name)

assert missing_overrides == []


def test_capital_gains_indexation_inputs_have_expected_uprating():
from policyengine_us.system import CountryTaxBenefitSystem

system = CountryTaxBenefitSystem()
assert (
system.variables["long_term_capital_gains_basis"].uprating
== "calibration.gov.irs.soi.long_term_capital_gains"
)
assert system.variables["long_term_capital_gains_years_held"].uprating is None
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
- name: Long-term capital gains basis is a person-level input
period: 2026
input:
people:
person1:
long_term_capital_gains_basis: 80_000
person2:
long_term_capital_gains_basis: 20_000
output:
long_term_capital_gains_basis: [80_000, 20_000]

- name: Long-term capital gains basis defaults to zero
period: 2026
input:
people:
person1: {}
output:
long_term_capital_gains_basis: [0]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
- name: Long-term capital gains years held is a person-level input
period: 2026
input:
people:
person1:
long_term_capital_gains_years_held: 8.5
person2:
long_term_capital_gains_years_held: 21.25
output:
long_term_capital_gains_years_held: [8.5, 21.25]

- name: Long-term capital gains years held defaults to zero
period: 2026
input:
people:
person1: {}
output:
long_term_capital_gains_years_held: [0]
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from policyengine_us.model_api import *


FIRST_CPI_U_YEAR = 1913


def capital_gains_indexation_price_level(parameters, year, transition_year):
if year <= transition_year:
return parameters(f"{year}-01-01").gov.bls.cpi.cpi_u

cpi_u_transition = parameters(f"{transition_year}-01-01").gov.bls.cpi.cpi_u
chained_cpi_transition = parameters(f"{transition_year}-01-01").gov.bls.cpi.c_cpi_u
chained_cpi_year = parameters(f"{year}-01-01").gov.bls.cpi.c_cpi_u
return cpi_u_transition * chained_cpi_year / chained_cpi_transition


class tax_unit_long_term_capital_gains_indexation_adjustment(Variable):
value_type = float
entity = TaxUnit
label = "tax unit long-term capital gains basis indexation adjustment"
unit = USD
documentation = (
"Reduction in taxable long-term capital gains from indexing basis to inflation."
)
definition_period = YEAR

def formula(tax_unit, period, parameters):
p = parameters(period).gov.irs.capital_gains.indexation
if not p.applies:
return np.zeros(tax_unit.count)

person = tax_unit.members
year = period.start.year
gains = add(tax_unit, period, ["long_term_capital_gains_before_response"])
basis = add(tax_unit, period, ["long_term_capital_gains_basis"])

person_gains = person("long_term_capital_gains_before_response", period)
person_years_held = person("long_term_capital_gains_years_held", period)
person_weights = abs(person_gains)
weight_sum = tax_unit.sum(person_weights)
weighted_years_held = tax_unit.sum(person_years_held * person_weights)
years_held = np.divide(
weighted_years_held,
weight_sum,
out=np.zeros_like(weighted_years_held),
where=weight_sum != 0,
)

rounded_years_held = np.rint(np.nan_to_num(years_held)).astype(int)
acquisition_year = year - rounded_years_held

transition_year = p.chained_cpi_transition_year
current_price_level = capital_gains_indexation_price_level(
parameters, year, transition_year
)
indexation_ratio = np.ones(tax_unit.count)
for purchase_year in np.unique(acquisition_year):
if purchase_year < FIRST_CPI_U_YEAR or purchase_year > year:
continue
purchase_price_level = capital_gains_indexation_price_level(
parameters, int(purchase_year), transition_year
)
indexation_ratio = where(
acquisition_year == purchase_year,
current_price_level / purchase_price_level,
indexation_ratio,
)

eligible = (
(basis > 0)
& (years_held > p.minimum_holding_period)
& (acquisition_year > p.purchase_year_threshold)
)
adjustment = basis * max_(0, indexation_ratio - 1)

if p.cap_adjustment_to_gain:
adjustment = where(gains > 0, min_(adjustment, gains), 0)

return where(eligible, adjustment, 0)
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ class long_term_capital_gains(Variable):
title="26 U.S. Code § 1222(3)",
href="https://www.law.cornell.edu/uscode/text/26/1222#3",
)
adds = [
"long_term_capital_gains_before_response",
"capital_gains_behavioral_response",
]

def formula(person, period, parameters):
gains = person("long_term_capital_gains_before_response", period)
response = person("capital_gains_behavioral_response", period)
indexation_adjustment = person(
"long_term_capital_gains_indexation_adjustment", period
)
return gains + response - indexation_adjustment
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from policyengine_us.model_api import *


class long_term_capital_gains_basis(Variable):
value_type = float
entity = Person
label = "long-term capital gains basis"
unit = USD
documentation = (
"Cost basis associated with realized long-term capital gains, "
"stored for capital-gains basis indexation analysis."
)
definition_period = YEAR
uprating = "calibration.gov.irs.soi.long_term_capital_gains"
Loading