diff --git a/changelog.d/split-capital-loss-carryover-k1.added.md b/changelog.d/split-capital-loss-carryover-k1.added.md new file mode 100644 index 00000000000..07a373c11b4 --- /dev/null +++ b/changelog.d/split-capital-loss-carryover-k1.added.md @@ -0,0 +1 @@ +Split the capital loss carryover memo inputs into the person's own carryover and one from a terminated estate or trust (Schedule K-1 (Form 1041), box 11, codes C and D), add a short-term carryover memo, and add the short-term carryover back in Arizona property tax credit household income (ITR 12-1 item (7)). diff --git a/policyengine_us/data/economic_assumptions.py b/policyengine_us/data/economic_assumptions.py index 14347e5fb70..184a97f223f 100644 --- a/policyengine_us/data/economic_assumptions.py +++ b/policyengine_us/data/economic_assumptions.py @@ -30,6 +30,10 @@ "foreign_tax_credit": DEFAULT_MICRODATA_UPRATING, "interest_deduction": DEFAULT_MICRODATA_UPRATING, "long_term_capital_gains": "calibration.gov.irs.soi.long_term_capital_gains", + # long_term_capital_loss_carryover and short_term_capital_loss_carryover + # add their own and estate-or-trust leaves. A dataset that carries either + # sum keeps the series its leaves follow. + "long_term_capital_loss_carryover": "calibration.gov.irs.soi.long_term_capital_gains", "misc_deduction": DEFAULT_MICRODATA_UPRATING, "partnership_income": "calibration.gov.irs.soi.partnership_s_corp_income", "partnership_s_corp_income": "calibration.gov.irs.soi.partnership_s_corp_income", @@ -41,6 +45,7 @@ "self_employment_income": "calibration.gov.irs.soi.self_employment_income", "self_employed_health_insurance_ald": DEFAULT_MICRODATA_UPRATING, "self_employed_pension_contribution_ald": DEFAULT_MICRODATA_UPRATING, + "short_term_capital_loss_carryover": "calibration.gov.irs.soi.short_term_capital_gains", "social_security": "calibration.gov.irs.soi.social_security", "s_corp_income": "calibration.gov.irs.soi.partnership_s_corp_income", "spm_unit_weight": "calibration.gov.census.populations.total", diff --git a/policyengine_us/system.py b/policyengine_us/system.py index c4c90688ea2..19032de7bc9 100644 --- a/policyengine_us/system.py +++ b/policyengine_us/system.py @@ -1,6 +1,8 @@ import hashlib from functools import lru_cache from pathlib import Path + +import numpy as np from policyengine_core.taxbenefitsystems import TaxBenefitSystem from policyengine_us.entities import * from policyengine_us.parameters.gov.irs.uprating import ( @@ -217,6 +219,63 @@ def _backfill_state_code_from_str(simulation): state_code_str.delete_arrays(known_period) +CAPITAL_LOSS_CARRYOVER_SUMS = { + "long_term_capital_loss_carryover": ( + "own_long_term_capital_loss_carryover", + "estate_or_trust_long_term_capital_loss_carryover", + ), + "short_term_capital_loss_carryover": ( + "own_short_term_capital_loss_carryover", + "estate_or_trust_short_term_capital_loss_carryover", + ), +} + + +def _file_capital_loss_carryover_sums(simulation): + """File an entered capital loss carryover sum as the person's own carryover. + + Each sum adds the person's own carryover and one from a terminated estate + or trust. Core does not uprate a variable that adds others, and a sum + entered for anyone in a simulation would otherwise be an input for + everyone in it. So, mirroring the ``employment_income`` -> + ``employment_income_before_lsr`` moves, a sum entered for a period + replaces the leaves entered for that period for each person it is + nonzero for: their own carryover becomes the sum, which uprates to later + periods as that leaf does, and an estate or trust carryover entered for + the period becomes zero. Where the sum is zero, the leaves entered for the + period stay, and without any the person's own carryover is zero, as the + entered sum was. Leaves entered for other periods are left alone. The sum + is then computed from its leaves. + """ + for total, (own, estate_or_trust) in CAPITAL_LOSS_CARRYOVER_SUMS.items(): + holder = simulation.get_holder(total) + for known_period in holder.get_known_periods(): + amount = holder.get_array(known_period) + entered = amount != 0 + for leaf, value in [(own, amount), (estate_or_trust, 0)]: + existing = simulation.get_holder(leaf).get_array(known_period) + if existing is None and leaf == estate_or_trust: + continue + simulation.set_input( + leaf, + known_period, + np.where(entered, value, 0 if existing is None else existing), + ) + holder.delete_arrays(known_period) + if getattr(simulation, "input_variables", None) is not None: + names = [ + name + for name in simulation.input_variables + if name not in CAPITAL_LOSS_CARRYOVER_SUMS + ] + for leaves in CAPITAL_LOSS_CARRYOVER_SUMS.values(): + for leaf in leaves: + known = simulation.get_holder(leaf).get_known_periods() + if known and leaf not in names: + names.append(leaf) + simulation.input_variables = names + + class Simulation(SPMSimulationMixin, CoreSimulation): """ A simulation of the tax-benefit system for the United States, @@ -296,6 +355,8 @@ def __init__(self, *args, **kwargs): ) cg_holder.delete_arrays(known_period) + _file_capital_loss_carryover_sums(self) + # Geography backfill: state_code_str-only input -> state_code enum. _backfill_state_code_from_str(self) @@ -556,6 +617,8 @@ def __init__(self, *args, **kwargs): ) cg_holder.delete_arrays(known_period) + _file_capital_loss_carryover_sums(self) + # Geography backfill: state_code_str-only input -> state_code enum. # Datasets carry state_fips, so this only fires for situations that # explicitly supply state_code_str. diff --git a/policyengine_us/tests/core/test_capital_gains_rate_worksheet_properties.py b/policyengine_us/tests/core/test_capital_gains_rate_worksheet_properties.py index bdd22cb4d58..479959dcf08 100644 --- a/policyengine_us/tests/core/test_capital_gains_rate_worksheet_properties.py +++ b/policyengine_us/tests/core/test_capital_gains_rate_worksheet_properties.py @@ -7,7 +7,11 @@ Port the specialty dependent cohorts from PR #9977's investment-interest properties without its Form 4952 calculations. YAML covers policy examples and regular tax; Hypothesis covers arbitrary vectorized populations, changes -to dependents' inputs, and redistribution between the head and spouse. +to dependents' inputs, and redistribution between the head and spouse. A +differential checks that entering long_term_capital_loss_carryover as its two +leaves, the person's own carryover and one from a terminated estate or trust +(Schedule K-1 (Form 1041), box 11, code D), gives the same worksheet lines as +entering their sum, as older callers and datasets do. Each example constructs a small Core system containing the production worksheet classes and their inputs, without reconstructing the full US @@ -38,6 +42,9 @@ from policyengine_us.variables.household.income.person.capital_gains.collectibles_gain_or_loss import ( collectibles_gain_or_loss, ) +from policyengine_us.variables.household.income.person.capital_gains.estate_or_trust_long_term_capital_loss_carryover import ( + estate_or_trust_long_term_capital_loss_carryover, +) from policyengine_us.variables.household.income.person.capital_gains.long_term_capital_gains_on_collectibles import ( long_term_capital_gains_on_collectibles, ) @@ -47,6 +54,9 @@ from policyengine_us.variables.household.income.person.capital_gains.long_term_capital_loss_carryover import ( long_term_capital_loss_carryover, ) +from policyengine_us.variables.household.income.person.capital_gains.own_long_term_capital_loss_carryover import ( + own_long_term_capital_loss_carryover, +) from policyengine_us.variables.household.income.person.capital_gains.section_1202_gain import ( section_1202_gain, ) @@ -70,6 +80,11 @@ long_term_capital_gains_on_small_business_stock, ) INPUTS = tuple(variable.__name__ for variable in INPUT_CLASSES) +# long_term_capital_loss_carryover adds these; an entered sum is used instead. +CARRYOVER_LEAVES = ( + own_long_term_capital_loss_carryover, + estate_or_trust_long_term_capital_loss_carryover, +) OUTPUTS = ( "rate_gains_less_losses", "capital_gains_28_percent_rate_gain", @@ -83,6 +98,7 @@ def _simulate(units, *, reverse_members=False): system = TaxBenefitSystem(entities=[Person, TaxUnit]) system.add_variables( *INPUT_CLASSES, + *CARRYOVER_LEAVES, is_tax_unit_dependent, unrecaptured_section_1250_gain, unrecaptured_section_1250_gain_before_losses, @@ -124,6 +140,10 @@ def _simulate(units, *, reverse_members=False): for name in OUTPUTS if name != "rate_gains_less_losses" }, + # Per person, in the order the units list them. + "long_term_capital_loss_carryover": simulation.calculate( + "long_term_capital_loss_carryover", PERIOD + ), } @@ -262,3 +282,34 @@ def test_vectorized_worksheets_depend_only_on_non_dependents(units): np.testing.assert_array_equal( results[name][size : 2 * size], expected, err_msg=name ) + + +@settings(max_examples=60, deadline=None) +@given(units=UNITS, data=st.data()) +def test_carryover_leaves_give_the_same_worksheets_as_their_sum(units, data): + """Differential: each person's long-term carryover entered as its sum, or + split at random between the person's own carryover and one from a + terminated estate or trust, gives the same carryover and the same worksheet + lines. Dependents' carryovers are split too; the worksheets leave them out + either way. The two cohorts run in separate simulations: a sum entered for + anyone in a simulation is an input for everyone in it.""" + as_leaves = deepcopy(units) + totals = [] + for unit in as_leaves: + people = [unit["head"], *unit["dependents"]] + if unit["spouse"] is not None: + people.insert(1, unit["spouse"]) + for person in people: + total = person.pop("long_term_capital_loss_carryover") + own = data.draw(st.integers(min_value=0, max_value=total)) + person["own_long_term_capital_loss_carryover"] = own + person["estate_or_trust_long_term_capital_loss_carryover"] = total - own + totals.append(total) + + summed, leaves = _simulate(units), _simulate(as_leaves) + for results in (summed, leaves): + np.testing.assert_array_equal( + results["long_term_capital_loss_carryover"], totals + ) + for name in OUTPUTS: + np.testing.assert_array_equal(leaves[name], summed[name], err_msg=name) diff --git a/policyengine_us/tests/core/test_capital_loss_carryover_entered_sums.py b/policyengine_us/tests/core/test_capital_loss_carryover_entered_sums.py new file mode 100644 index 00000000000..a548b27b3b0 --- /dev/null +++ b/policyengine_us/tests/core/test_capital_loss_carryover_entered_sums.py @@ -0,0 +1,144 @@ +"""A capital loss carryover sum entered as an input is filed as the person's +own carryover when a Simulation is built, as employment_income is filed as +employment_income_before_lsr (``policyengine_us.system``). YAML tests build +Core simulations, which skip that step, so these run through +``policyengine_us.Simulation``. + +Invariants: + +1. Uprating: a sum entered for 2024 grows to later years by the series its + leaves follow, as it did when the sum was an uprated input, and gives the + same results as entering the person's own carryover for 2024. +2. People: a sum entered for one person leaves another person's leaves for + the same year alone. +3. Replacement: a nonzero sum entered for a person replaces the leaves entered + for that person for the same year; a zero sum leaves them. +""" + +import numpy as np +import pytest +from policyengine_core.parameters.operations.get_parameter import get_parameter + +from policyengine_us import Simulation +from policyengine_us.system import system +from policyengine_us.tools.per_capita_uprating import per_capita_path + +BASE_YEAR, LATER_YEARS = 2024, (2025, 2026, 2030) +TERMS = { + "long": per_capita_path("calibration.gov.irs.soi.long_term_capital_gains"), + "short": per_capita_path("calibration.gov.irs.soi.short_term_capital_gains"), +} + + +def _growth(path, year): + parameter = get_parameter(system.parameters, path) + return parameter(f"{year}-01-01") / parameter(f"{BASE_YEAR}-01-01") + + +def _renter(carryovers: dict, year=BASE_YEAR) -> dict: + """An Arizona renter aged 70 whose gains are net of the carryovers.""" + person = { + "age": {year: 70}, + "rent": {year: 5_000}, + "long_term_capital_gains": {year: 1_000}, + "short_term_capital_gains": {year: -500}, + } + person.update({name: {year: amount} for name, amount in carryovers.items()}) + return { + "people": {"head": person}, + "tax_units": {"tax_unit": {"members": ["head"]}}, + "households": {"household": {"members": ["head"], "state_code": {year: "AZ"}}}, + } + + +@pytest.mark.parametrize("term", TERMS) +def test_entered_sum_uprates_as_its_own_leaf(term): + sum_entered = Simulation( + situation=_renter({f"{term}_term_capital_loss_carryover": 3_000}) + ) + own_entered = Simulation( + situation=_renter({f"own_{term}_term_capital_loss_carryover": 3_000}) + ) + for year in LATER_YEARS: + np.testing.assert_allclose( + sum_entered.calculate(f"{term}_term_capital_loss_carryover", year), + [3_000 * _growth(TERMS[term], year)], + rtol=1e-6, + ) + for name in [ + f"{term}_term_capital_loss_carryover", + "az_property_tax_credit_capital_gains", + "az_property_tax_credit", + "capital_gains_28_percent_rate_gain", + "income_tax", + ]: + np.testing.assert_array_equal( + sum_entered.calculate(name, year), + own_entered.calculate(name, year), + err_msg=f"{name} {year}", + ) + # The sum is no longer an input; the own leaf holds it. + assert f"{term}_term_capital_loss_carryover" not in sum_entered.input_variables + assert f"own_{term}_term_capital_loss_carryover" in sum_entered.input_variables + + +@pytest.mark.parametrize("term", TERMS) +def test_entered_sum_leaves_other_peoples_leaves(term): + year = str(BASE_YEAR) + simulation = Simulation( + situation={ + "people": { + "head": { + "age": {year: 40}, + f"{term}_term_capital_loss_carryover": {year: 1_000}, + }, + "spouse": { + "age": {year: 40}, + f"estate_or_trust_{term}_term_capital_loss_carryover": { + year: 2_000 + }, + }, + "child": { + "age": {year: 10}, + f"own_{term}_term_capital_loss_carryover": {year: 300}, + }, + } + } + ) + np.testing.assert_array_equal( + simulation.calculate(f"{term}_term_capital_loss_carryover", year), + [1_000, 2_000, 300], + ) + + +@pytest.mark.parametrize("term", TERMS) +def test_entered_sum_replaces_the_persons_leaves(term): + year = str(BASE_YEAR) + leaves = { + f"own_{term}_term_capital_loss_carryover": {year: 1_000}, + f"estate_or_trust_{term}_term_capital_loss_carryover": {year: 500}, + } + simulation = Simulation( + situation={ + "people": { + "replaced": { + "age": {year: 40}, + f"{term}_term_capital_loss_carryover": {year: 4_000}, + **leaves, + }, + "kept": { + "age": {year: 40}, + f"{term}_term_capital_loss_carryover": {year: 0}, + **leaves, + }, + } + } + ) + for name, expected in [ + (f"{term}_term_capital_loss_carryover", [4_000, 1_500]), + (f"own_{term}_term_capital_loss_carryover", [4_000, 1_000]), + (f"estate_or_trust_{term}_term_capital_loss_carryover", [0, 500]), + ]: + np.testing.assert_array_equal( + simulation.calculate(name, year), expected, err_msg=name + ) diff --git a/policyengine_us/tests/core/test_uprating_series.py b/policyengine_us/tests/core/test_uprating_series.py index 7a673d48992..c6ae40bce73 100644 --- a/policyengine_us/tests/core/test_uprating_series.py +++ b/policyengine_us/tests/core/test_uprating_series.py @@ -16,6 +16,9 @@ EMPLOYMENT = per_capita_path("calibration.gov.irs.soi.employment_income") SELF_EMPLOYMENT = per_capita_path("calibration.gov.irs.soi.self_employment_income") CAPITAL_GAINS = per_capita_path("calibration.gov.irs.soi.long_term_capital_gains") +SHORT_TERM_CAPITAL_GAINS = per_capita_path( + "calibration.gov.irs.soi.short_term_capital_gains" +) ALIMONY = per_capita_path("calibration.gov.irs.soi.alimony_income") UNEMPLOYMENT = per_capita_path("calibration.gov.irs.soi.unemployment_compensation") DEFAULT = per_capita_path("calibration.gov.cbo.income_by_source.adjusted_gross_income") @@ -49,7 +52,10 @@ "self_employed_pension_contributions_desired": SELF_EMPLOYMENT, # Components of capital gains. "long_term_capital_gains_on_collectibles": CAPITAL_GAINS, - "long_term_capital_loss_carryover": CAPITAL_GAINS, + "own_long_term_capital_loss_carryover": CAPITAL_GAINS, + "estate_or_trust_long_term_capital_loss_carryover": CAPITAL_GAINS, + "own_short_term_capital_loss_carryover": SHORT_TERM_CAPITAL_GAINS, + "estate_or_trust_short_term_capital_loss_carryover": SHORT_TERM_CAPITAL_GAINS, "collectibles_gain_or_loss": CAPITAL_GAINS, "section_1202_gain": CAPITAL_GAINS, "unrecaptured_section_1250_gain_before_losses": CAPITAL_GAINS, @@ -158,6 +164,20 @@ def test_rent_override_matches_the_rent_input(): assert system.variables["pre_subsidy_rent"].uprating == CPI_U +@pytest.mark.parametrize( + "total", ["long_term_capital_loss_carryover", "short_term_capital_loss_carryover"] +) +def test_carryover_sum_override_matches_its_leaves(total): + """A carryover sum adds its leaves, so Core cannot uprate it; a dataset + column holding the sum follows the series its leaves follow.""" + leaves = system.variables[total].adds + assert len(leaves) == 2 + for leaf in leaves: + assert system.variables[leaf].uprating == per_capita_path( + MICRODATA_UPRATING_OVERRIDES[total] + ) + + METHODOLOGY_PAGE = ( Path(__file__).parents[3] / "docs-quarto" / "methodology" / "spm-poverty.qmd" ) diff --git a/policyengine_us/tests/microsimulation/data/test_capital_loss_carryover_dataset_inputs.py b/policyengine_us/tests/microsimulation/data/test_capital_loss_carryover_dataset_inputs.py new file mode 100644 index 00000000000..52e899f2ff1 --- /dev/null +++ b/policyengine_us/tests/microsimulation/data/test_capital_loss_carryover_dataset_inputs.py @@ -0,0 +1,147 @@ +""" +Datasets can carry ``long_term_capital_loss_carryover`` or +``short_term_capital_loss_carryover`` as a person column. Each now adds two +leaves: the person's own carryover and one from a terminated estate or trust +(Schedule K-1 (Form 1041), box 11, codes D and C). Core does not uprate a +variable that adds others, so ``MICRODATA_UPRATING_OVERRIDES`` gives the sums +the series their leaves follow, and the simulation files each year's sums as +the person's own carryover. A dataset carrying the sums must keep them in +every extended year and give the same results as one carrying the leaves. +""" + +import numpy as np +import pandas as pd +import pytest +from policyengine_core.parameters.operations.get_parameter import get_parameter + +from policyengine_us import Microsimulation +from policyengine_us.data.dataset_schema import USSingleYearDataset +from policyengine_us.system import system +from policyengine_us.tools.per_capita_uprating import per_capita_path + +BASE_YEAR, LATER_YEAR = 2024, 2026 +SERIES = { + "long": per_capita_path("calibration.gov.irs.soi.long_term_capital_gains"), + "short": per_capita_path("calibration.gov.irs.soi.short_term_capital_gains"), +} +# Per person: the carryover sums and the own part of each. +CARRYOVERS = { + "long": ([3_000.0, 0.0, 1_200.0], [1_000.0, 0.0, 1_200.0]), + "short": ([1_000.0, 0.0, 400.0], [0.0, 0.0, 100.0]), +} +OUTPUTS = [ + "az_property_tax_credit_capital_gains", + "az_property_tax_credit_income", + "az_property_tax_credit", + "adjusted_gross_income", + "income_tax", +] + + +def _dataset(carryover_columns: dict) -> USSingleYearDataset: + """Two Arizona households: a renter aged 70 whose gains are net of + carryovers, and a renter aged 68 with a dependent grandchild who has + carryovers.""" + return USSingleYearDataset( + person=pd.DataFrame( + { + "person_id": [1, 2, 3], + "person_household_id": [1, 2, 2], + "person_tax_unit_id": [1, 2, 2], + "person_spm_unit_id": [1, 2, 2], + "person_family_id": [1, 2, 2], + "person_marital_unit_id": [1, 2, 3], + "age": [70.0, 68.0, 15.0], + "is_tax_unit_dependent": [False, False, True], + "rent": [5_000.0, 6_000.0, 0.0], + "employment_income": [0.0, 2_000.0, 0.0], + # Schedule D lines 15 and 7, net of the carryovers. + "long_term_capital_gains_before_response": [1_000.0, 0.0, -1_200.0], + "short_term_capital_gains": [-500.0, 0.0, -400.0], + **carryover_columns, + } + ), + household=pd.DataFrame( + { + "household_id": [1, 2], + "household_weight": [1.0, 1.0], + "state_fips": [4, 4], + } + ), + tax_unit=pd.DataFrame({"tax_unit_id": [1, 2]}), + spm_unit=pd.DataFrame({"spm_unit_id": [1, 2]}), + family=pd.DataFrame({"family_id": [1, 2]}), + marital_unit=pd.DataFrame({"marital_unit_id": [1, 2, 3]}), + time_period=BASE_YEAR, + ) + + +def _sums() -> dict: + return { + f"{term}_term_capital_loss_carryover": totals + for term, (totals, _) in CARRYOVERS.items() + } + + +def _leaves() -> dict: + columns = {} + for term, (totals, own) in CARRYOVERS.items(): + columns[f"own_{term}_term_capital_loss_carryover"] = own + columns[f"estate_or_trust_{term}_term_capital_loss_carryover"] = [ + total - part for total, part in zip(totals, own) + ] + return columns + + +def _growth(path: str, year: int) -> float: + parameter = get_parameter(system.parameters, path) + return parameter(f"{year}-01-01") / parameter(f"{BASE_YEAR}-01-01") + + +@pytest.fixture(scope="module") +def simulations(): + return { + name: Microsimulation(dataset=_dataset(columns), dataset_end_year=LATER_YEAR) + for name, columns in [("sums", _sums()), ("leaves", _leaves())] + } + + +@pytest.mark.parametrize("year", [BASE_YEAR, LATER_YEAR]) +def test_carryover_sums_from_a_dataset_uprate_as_their_leaves(simulations, year): + for term, (totals, _) in CARRYOVERS.items(): + expected = np.array(totals) * _growth(SERIES[term], year) + for simulation in simulations.values(): + np.testing.assert_allclose( + simulation.calculate( + f"{term}_term_capital_loss_carryover", year + ).values, + expected, + rtol=1e-6, + ) + + +@pytest.mark.parametrize("year", [BASE_YEAR, LATER_YEAR]) +def test_dataset_sums_and_leaves_give_the_same_results(simulations, year): + for name in OUTPUTS: + np.testing.assert_allclose( + simulations["leaves"].calculate(name, year).values, + simulations["sums"].calculate(name, year).values, + atol=0.01, + err_msg=name, + ) + + +def test_line_d_adds_back_dataset_carryovers(simulations): + # ITR 12-1 item (7): the year's own result counts on line D. The first + # renter: 1,000 + 3,000 - 500 + 1,000. The grandchild: + # -1,200 + 1,200 - 400 + 400. + for simulation in simulations.values(): + line_d = simulation.calculate("az_property_tax_credit_capital_gains", BASE_YEAR) + np.testing.assert_allclose(line_d.values, [4_500, 0, 0], atol=0.01) + + +def test_dataset_sums_are_filed_as_own_carryovers(simulations): + inputs = simulations["sums"].input_variables + for term in CARRYOVERS: + assert f"{term}_term_capital_loss_carryover" not in inputs + assert f"own_{term}_term_capital_loss_carryover" in inputs diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/capital_gains_28_percent_rate_gain.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/capital_gains_28_percent_rate_gain.yaml index 9444db22c4e..784ff6407aa 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/capital_gains_28_percent_rate_gain.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/capital_gains_28_percent_rate_gain.yaml @@ -68,6 +68,75 @@ # 6,000. capital_gains_28_percent_rate_gain: 6_000 +# Line 5 is "your long-term capital loss carryovers from Schedule D, line 14; +# and Schedule K-1 (Form 1041), box 11, code D". A carryover from a terminated +# estate or trust (26 U.S.C. 642(h)(1), Reg. 1.642(h)-1) reduces the gain as +# the person's own carryover does. +- name: A terminated estate or trust's long-term carryover reduces it + period: 2025 + input: + long_term_capital_gains: 47_000 + collectibles_gain_or_loss: 10_000 + estate_or_trust_long_term_capital_loss_carryover: 3_000 + output: + # Line 5 = (3,000), line 7 = 7,000, as with a 3,000 carryover of the + # person's own. + capital_gains_28_percent_rate_gain: 7_000 + +- name: Own and estate or trust long-term carryovers combine on line 5 + period: 2025 + input: + long_term_capital_gains: 47_000 + collectibles_gain_or_loss: 10_000 + own_long_term_capital_loss_carryover: 1_000 + estate_or_trust_long_term_capital_loss_carryover: 2_000 + output: + # Line 5 = (1,000 + 2,000), line 7 = 7,000. + capital_gains_28_percent_rate_gain: 7_000 + +- name: A short-term carryover memo does not reduce it again + period: 2025 + input: + long_term_capital_gains: 50_000 + collectibles_gain_or_loss: 10_000 + # Schedule D line 7: the year's net short-term loss of 5,000 plus a 3,000 + # carryover, 2,000 of it the person's own (line 6) and 1,000 from a + # terminated trust's Schedule K-1 (Form 1041), box 11, code C (line 5). + short_term_capital_gains: -8_000 + own_short_term_capital_loss_carryover: 2_000 + estate_or_trust_short_term_capital_loss_carryover: 1_000 + output: + # Line 6 is the Schedule D line 7 loss, which already holds the + # carryovers: 10,000 - 8,000 = 2,000. + capital_gains_28_percent_rate_gain: 2_000 + +- name: Each spouse's carryover leaves count on a joint return + period: 2025 + input: + people: + head: + age: 45 + is_tax_unit_head: true + collectibles_gain_or_loss: 10_000 + own_long_term_capital_loss_carryover: 1_000 + spouse: + age: 45 + is_tax_unit_spouse: true + estate_or_trust_long_term_capital_loss_carryover: 2_000 + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + marital_units: + filers: + members: [head, spouse] + households: + household: + members: [head, spouse] + output: + # Line 5 = (1,000 + 2,000), line 7 = 10,000 - 3,000. + capital_gains_28_percent_rate_gain: 7_000 + - name: A collectibles loss offsets section 1202 gain period: 2025 input: diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_unrecaptured_section_1250_gain.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_unrecaptured_section_1250_gain.yaml index c84287acaea..93a0ec1e53a 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_unrecaptured_section_1250_gain.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/schedule_d_unrecaptured_section_1250_gain.yaml @@ -83,6 +83,19 @@ # Line 15 = (6,000), line 16 = (1,000), line 17 = 7,000, line 18 = 13,000. schedule_d_unrecaptured_section_1250_gain: 13_000 +- name: An estate or trust carryover and a short-term loss together + period: 2025 + input: + long_term_capital_gains: 49_000 + unrecaptured_section_1250_gain_before_losses: 20_000 + short_term_capital_gains: -6_000 + # Schedule K-1 (Form 1041), box 11, code D, which line 16 takes with + # Schedule D line 14. + estate_or_trust_long_term_capital_loss_carryover: 1_000 + output: + # Line 15 = (6,000), line 16 = (1,000), line 17 = 7,000, line 18 = 13,000. + schedule_d_unrecaptured_section_1250_gain: 13_000 + - name: A collectibles loss reduces it period: 2025 input: diff --git a/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/credits/az_property_tax_credit_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/credits/az_property_tax_credit_income.yaml index f1f7d8f85e2..eee2ed64944 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/credits/az_property_tax_credit_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/az/tax/income/credits/az_property_tax_credit_income.yaml @@ -1033,3 +1033,147 @@ # Line 11b: Schedule 2, 3,851 - 4,000 -> 279. # Line 13: 750. Line 15: smaller of 279 and 750. az_property_tax_credit: 279 + +# The carryover memos are sums of leaf inputs: the person's own carryover and +# one from a terminated estate or trust. Entering the leaves gives the same +# line D as entering their sum. +- name: A terminating trust capital loss entered as its own input remains a carryover + period: 2025 + input: + people: + head: + age: 70 + # Own gains 4,000 less the trust's 3,000 loss transferred in 2025. + long_term_capital_gains: 1_000 + # Final Form 1041 K-1 box 11 code D; d1107 keeps it a carryover. + estate_or_trust_long_term_capital_loss_carryover: 3_000 + rent: 5_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: AZ + output: + adjusted_gross_income: 1_000 + long_term_capital_loss_carryover: [3_000] + az_property_tax_credit_capital_gains: [4_000] + az_property_tax_credit_agi: 4_000 + az_property_tax_credit_income: 4_000 + az_property_tax_credit: 0 + +- name: Own and terminating trust long-term carryovers are both added back + period: 2025 + input: + people: + head: + age: 70 + # The year's long-term gain 4,000, net of a 1,000 own carryover and a + # 2,000 carryover from a terminated trust. + long_term_capital_gains: 1_000 + own_long_term_capital_loss_carryover: 1_000 + estate_or_trust_long_term_capital_loss_carryover: 2_000 + rent: 5_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: AZ + output: + adjusted_gross_income: 1_000 + # Line D: the year's gain 4,000. Line J: 4,000. + az_property_tax_credit_capital_gains: [4_000] + az_property_tax_credit_income: 4_000 + az_property_tax_credit: 0 + +# ITR 12-1 item (7) bars any prior-year carryover, short-term as well as +# long-term. short_term_capital_gains is Schedule D line 7, net of the +# short-term carryover on line 6 (own) or line 5 (Schedule K-1 (Form 1041), +# box 11, code C), so line D adds the memo back. +- name: A short-term capital loss carryover does not reduce line D + period: 2025 + input: + people: + head: + age: 70 + # The year's short-term gain is 4,000; a 3,000 carryover nets it to + # 1,000 on Schedule D line 7. + short_term_capital_gains: 1_000 + own_short_term_capital_loss_carryover: 3_000 + rent: 5_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: AZ + output: + # Federal AGI uses the carryover: 1,000. + adjusted_gross_income: 1_000 + # Line D: the year's gain 4,000. Line J: 4,000. + short_term_capital_loss_carryover: [3_000] + az_property_tax_credit_capital_gains: [4_000] + az_property_tax_credit_income: 4_000 + # Line 11a: Schedule 1 pays nothing at 3,751 or more. + az_property_tax_credit: 0 + +- name: A terminating trust short-term loss remains a carryover for line D + period: 2025 + input: + people: + head: + age: 70 + long_term_capital_gains: 4_000 + # Final Form 1041 K-1 box 11 code C, on Schedule D line 5: the year's + # short-term result is that 3,000 loss alone. Treated like code D + # under d1107, pending Max's confirmation (d1512). + short_term_capital_gains: -3_000 + estate_or_trust_short_term_capital_loss_carryover: 3_000 + rent: 5_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: AZ + output: + # Federal AGI: 4,000 - 3,000 = 1,000. + adjusted_gross_income: 1_000 + # Line D: 4,000 - 3,000 + 3,000 = 4,000. Line J: 4,000. + az_property_tax_credit_capital_gains: [4_000] + az_property_tax_credit_income: 4_000 + az_property_tax_credit: 0 + +- name: A dependent's short-term carryover is not a line D loss + period: 2025 + input: + people: + head: + age: 70 + long_term_capital_gains: 4_000 + rent: 5_000 + child: + age: 17 + is_tax_unit_dependent: true + short_term_capital_gains: -3_000 + own_short_term_capital_loss_carryover: 3_000 + tax_units: + tax_unit: + members: [head, child] + households: + household: + members: [head, child] + state_code: AZ + output: + # Line D: column 1 gain 4,000; column 3 the year's net 0. Line J: 4,000. + az_property_tax_credit_capital_gains: [4_000, 0] + az_property_tax_credit_dependent_income: 0 + az_property_tax_credit_income: 4_000 + # Line 11b: Schedule 2, 3,851 - 4,000 -> 279. + # Line 13: 750. Line 15: smaller of 279 and 750. + az_property_tax_credit: 279 diff --git a/policyengine_us/tests/policy/baseline/household/income/person/capital_gains/long_term_capital_loss_carryover.yaml b/policyengine_us/tests/policy/baseline/household/income/person/capital_gains/long_term_capital_loss_carryover.yaml new file mode 100644 index 00000000000..00cc89238b8 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/income/person/capital_gains/long_term_capital_loss_carryover.yaml @@ -0,0 +1,55 @@ +# 26 U.S.C. 1212(b)(1)(B) and 642(h)(1): the long-term capital loss carried to +# the year is the person's own carryover (Schedule D line 14) plus any +# carryover from a terminated estate or trust (Schedule K-1 (Form 1041), box +# 11, code D, part of Schedule D line 12). +- name: No carryover unless entered + period: 2025 + input: + age: 40 + output: + long_term_capital_loss_carryover: 0 + +- name: The person's own carryover + period: 2025 + input: + own_long_term_capital_loss_carryover: 3_000 + output: + long_term_capital_loss_carryover: 3_000 + +- name: A carryover from a terminated estate or trust + period: 2025 + input: + estate_or_trust_long_term_capital_loss_carryover: 2_000 + output: + long_term_capital_loss_carryover: 2_000 + +- name: Own and estate or trust carryovers add + period: 2025 + input: + own_long_term_capital_loss_carryover: 3_000 + estate_or_trust_long_term_capital_loss_carryover: 2_000 + output: + long_term_capital_loss_carryover: 5_000 + +- name: An amount entered as the sum is used for that year + period: 2025 + input: + own_long_term_capital_loss_carryover: 1_000 + long_term_capital_loss_carryover: 4_000 + output: + # Callers and datasets that enter the sum keep their amount. + long_term_capital_loss_carryover: 4_000 + +- name: Each person's leaves add separately + period: 2025 + input: + people: + head: + own_long_term_capital_loss_carryover: 1_000 + estate_or_trust_long_term_capital_loss_carryover: 500 + spouse: + estate_or_trust_long_term_capital_loss_carryover: 2_000 + child: + age: 10 + output: + long_term_capital_loss_carryover: [1_500, 2_000, 0] diff --git a/policyengine_us/tests/policy/baseline/household/income/person/capital_gains/short_term_capital_loss_carryover.yaml b/policyengine_us/tests/policy/baseline/household/income/person/capital_gains/short_term_capital_loss_carryover.yaml new file mode 100644 index 00000000000..a70b3cc58be --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/income/person/capital_gains/short_term_capital_loss_carryover.yaml @@ -0,0 +1,54 @@ +# 26 U.S.C. 1212(b)(1)(A) and 642(h)(1): the short-term capital loss carried to +# the year is the person's own carryover (Schedule D line 6) plus any +# carryover from a terminated estate or trust (Schedule K-1 (Form 1041), box +# 11, code C, part of Schedule D line 5). +- name: No carryover unless entered + period: 2025 + input: + age: 40 + output: + short_term_capital_loss_carryover: 0 + +- name: The person's own carryover + period: 2025 + input: + own_short_term_capital_loss_carryover: 3_000 + output: + short_term_capital_loss_carryover: 3_000 + +- name: A carryover from a terminated estate or trust + period: 2025 + input: + estate_or_trust_short_term_capital_loss_carryover: 2_000 + output: + short_term_capital_loss_carryover: 2_000 + +- name: Own and estate or trust carryovers add + period: 2025 + input: + own_short_term_capital_loss_carryover: 3_000 + estate_or_trust_short_term_capital_loss_carryover: 2_000 + output: + short_term_capital_loss_carryover: 5_000 + +- name: An amount entered as the sum is used for that year + period: 2025 + input: + estate_or_trust_short_term_capital_loss_carryover: 1_000 + short_term_capital_loss_carryover: 4_000 + output: + short_term_capital_loss_carryover: 4_000 + +- name: Each person's leaves add separately + period: 2025 + input: + people: + head: + own_short_term_capital_loss_carryover: 1_000 + estate_or_trust_short_term_capital_loss_carryover: 500 + spouse: + estate_or_trust_short_term_capital_loss_carryover: 2_000 + child: + age: 10 + output: + short_term_capital_loss_carryover: [1_500, 2_000, 0] diff --git a/policyengine_us/tests/test_az_property_tax_credit_income.py b/policyengine_us/tests/test_az_property_tax_credit_income.py index b95ffe1b383..dcd1ae23156 100644 --- a/policyengine_us/tests/test_az_property_tax_credit_income.py +++ b/policyengine_us/tests/test_az_property_tax_credit_income.py @@ -33,6 +33,13 @@ 4. Schedule floor: the credit depends on line J only through max(line J, 0), so a negative line J gets the zero-income schedule amount. 5. The income does not depend on capital_gains_excluded_from_taxable_income. +6. Carryover leaves: moving part of a member's short-term result into the + short-term carryover memo, which short_term_capital_gains then nets as + Schedule D line 7 does, leaves line J unchanged (ITR 12-1 item (7)). + Entering each carryover memo as its leaves, split at random between the + member's own carryover and one from a terminated estate or trust (Schedule + K-1 (Form 1041), box 11, codes C and D), gives the same Arizona and federal + results as entering the sum. """ import numpy as np @@ -492,3 +499,78 @@ def formula(tax_unit, period, parameters): np.testing.assert_allclose( sim.calculate("az_property_tax_credit", YEAR), [502], atol=0.01 ) + + +def _with_short_term_carryovers(units: list, rng: np.random.Generator) -> list: + """The units with part of some members' short-term result entered as a + short-term capital loss carryover, which short_term_capital_gains nets, as + Schedule D line 7 does. Each member's year's own net gain is unchanged.""" + changed = [] + for unit in units: + members = [] + for member in unit["members"]: + member = dict(member) + carryover = round(rng.uniform(0, 4_000), 2) if rng.random() < 0.3 else 0.0 + member["short_term_capital_gains"] = round( + member["short_term_capital_gains"] - carryover, 2 + ) + member["short_term_capital_loss_carryover"] = carryover + members.append(member) + changed.append({**unit, "members": members}) + return changed + + +def _as_leaves(units: list, rng: np.random.Generator) -> list: + """The units with each carryover memo entered as its leaves: part the + member's own carryover, the rest from a terminated estate or trust.""" + changed = [] + for unit in units: + members = [] + for member in unit["members"]: + member = dict(member) + for term in ["long", "short"]: + total = member.pop(f"{term}_term_capital_loss_carryover") + own = round(total * rng.random(), 2) + member[f"own_{term}_term_capital_loss_carryover"] = own + member[f"estate_or_trust_{term}_term_capital_loss_carryover"] = round( + total - own, 2 + ) + members.append(member) + changed.append({**unit, "members": members}) + return changed + + +def test_carryover_leaves_match_their_sums(): + rng = np.random.default_rng(SEED + 2) + summed_units = _with_short_term_carryovers(UNITS, rng) + leaf_units = _as_leaves(summed_units, rng) + members = [m for unit in summed_units for m in unit["members"]] + assert sum(m["short_term_capital_loss_carryover"] > 0 for m in members) >= 20 + assert any( + m["short_term_capital_loss_carryover"] > 0 and m["role"] == "dependent" + for m in members + ) + # The two cohorts run in separate simulations: a sum entered for anyone in + # a simulation is an input for everyone in it. + summed = Simulation(situation=_situation(summed_units)) + leaves = Simulation(situation=_situation(leaf_units)) + # The short-term memo is added back, so line J is the year's own result. + np.testing.assert_allclose( + summed.calculate("az_property_tax_credit_income", YEAR), LINE_J, atol=0.01 + ) + for name in [ + "long_term_capital_loss_carryover", + "short_term_capital_loss_carryover", + "az_property_tax_credit_capital_gains", + "az_property_tax_credit_income", + "az_property_tax_credit", + "adjusted_gross_income", + "capital_gains_28_percent_rate_gain", + "income_tax", + ]: + np.testing.assert_allclose( + leaves.calculate(name, YEAR), + summed.calculate(name, YEAR), + atol=0.01, + err_msg=name, + ) diff --git a/policyengine_us/variables/gov/states/az/tax/income/credits/property_tax_credit/az_property_tax_credit_capital_gains.py b/policyengine_us/variables/gov/states/az/tax/income/credits/property_tax_credit/az_property_tax_credit_capital_gains.py index f0d2ca0e313..dabd2f4bf38 100644 --- a/policyengine_us/variables/gov/states/az/tax/income/credits/property_tax_credit/az_property_tax_credit_capital_gains.py +++ b/policyengine_us/variables/gov/states/az/tax/income/credits/property_tax_credit/az_property_tax_credit_capital_gains.py @@ -12,11 +12,10 @@ class az_property_tax_credit_capital_gains(Variable): "property for the Arizona property tax credit (Form 140PTC Part 1 line D): " "the year's gains and losses combined, including capital gain " "distributions, with a net loss counted only up to the per-member limit. " - "No prior-year capital loss carryover is used: the long-term carryover " - "that long_term_capital_gains nets is added back, including unused " - "losses transferred by a terminating estate or trust. Short-term inputs " - "must contain only the current year's net gains and losses, without " - "Schedule D line 6 carryovers; no short-term carryover memo is modeled." + "No prior-year capital loss carryover is used: the long-term and " + "short-term carryovers that long_term_capital_gains and " + "short_term_capital_gains net are added back, including unused losses " + "transferred by a terminating estate or trust." ) reference = [ "https://www.law.cornell.edu/regulations/arizona/Ariz-Admin-Code-SS-R15-2C-502", @@ -38,13 +37,20 @@ def formula(person, period, parameters): # capital gains (26 U.S.C. 852(b)(3)(B)), so they are combined with the # member's other gains and losses before the limit. # ITR 12-1 item (7): a prior-year capital loss carryover cannot reduce - # the year's gains. long_term_capital_gains includes the long-term - # carryover (26 U.S.C. 1212(b)(1)(B)) as a loss, as Schedule D line 15 - # does, so it is added back to give the year's own net gain or loss. - # Reg. 1.642(h)-1(a) transfers a terminating estate/trust's unused - # capital loss as a carryover. Max's d1107 ruling (2026-10-09) keeps - # final Form 1041 K-1 box 11 code D losses in this addback. - carryover = max_(0, person("long_term_capital_loss_carryover", period)) + # the year's gains. long_term_capital_gains and short_term_capital_gains + # include the carryovers as losses, as Schedule D lines 15 and 7 do: + # the person's own (26 U.S.C. 1212(b)(1)) and any allowed under 26 + # U.S.C. 642(h)(1) from a terminated estate or trust. So they are added + # back to give the year's own net gain or loss. Reg. 1.642(h)-1(a) + # transfers a terminating estate/trust's unused capital loss as a + # carryover. Max's d1107 ruling (2026-10-09) keeps final Form 1041 K-1 + # box 11 code D losses in this addback. Code C, the short-term + # counterpart under the same regulation, is treated the same pending + # his confirmation (d1512). The estate_or_trust_* leaves hold those + # losses apart from the person's own carryovers. + carryover = max_(0, person("long_term_capital_loss_carryover", period)) + max_( + 0, person("short_term_capital_loss_carryover", period) + ) net_capital_gain = ( add(person, period, ["capital_gains", "non_sch_d_capital_gains"]) + carryover diff --git a/policyengine_us/variables/household/income/person/capital_gains/estate_or_trust_long_term_capital_loss_carryover.py b/policyengine_us/variables/household/income/person/capital_gains/estate_or_trust_long_term_capital_loss_carryover.py new file mode 100644 index 00000000000..fe7f88e88a1 --- /dev/null +++ b/policyengine_us/variables/household/income/person/capital_gains/estate_or_trust_long_term_capital_loss_carryover.py @@ -0,0 +1,39 @@ +from policyengine_us.model_api import * + + +class estate_or_trust_long_term_capital_loss_carryover(Variable): + value_type = float + entity = Person + label = "Long-term capital loss carryover from a terminated estate or trust" + unit = USD + documentation = ( + "Long-term capital loss carryover a beneficiary succeeds to when an " + "estate or trust terminates, as a positive amount: Schedule K-1 (Form " + "1041), box 11, code D, from the estate or trust's final year. Under " + "26 U.S.C. 642(h)(1) and Reg. 1.642(h)-1(a), a capital loss carryover " + "the estate or trust could have used after that year but for the " + "termination is allowed to the beneficiaries succeeding to its " + "property. Reg. 1.642(h)-1(b) keeps it long-term for a beneficiary " + "that is not a corporation. The beneficiary reports it on Schedule D " + "line 12, on line 5 of the 28% Rate Gain Worksheet and on line 16 of " + "the Unrecaptured Section 1250 Gain Worksheet. The person's own " + "carryover goes in own_long_term_capital_loss_carryover instead. Memo " + "item: long_term_capital_gains already includes it as a loss, as " + "Schedule D line 15 does. Zero unless entered." + ) + definition_period = YEAR + uprating = "calibration.gov.irs.soi.long_term_capital_gains" + reference = [ + dict( + title="26 U.S. Code § 642(h)(1)", + href="https://www.law.cornell.edu/uscode/text/26/642#h_1", + ), + dict( + title="26 CFR § 1.642(h)-1", + href="https://www.law.cornell.edu/cfr/text/26/1.642(h)-1", + ), + dict( + title="2025 Instructions for Schedule K-1 (Form 1041) for a Beneficiary Filing Form 1040 or 1040-SR, Box 11, Codes C and D", + href="https://www.irs.gov/pub/irs-prior/i1041sk1--2025.pdf#page=3", + ), + ] diff --git a/policyengine_us/variables/household/income/person/capital_gains/estate_or_trust_short_term_capital_loss_carryover.py b/policyengine_us/variables/household/income/person/capital_gains/estate_or_trust_short_term_capital_loss_carryover.py new file mode 100644 index 00000000000..084767bc991 --- /dev/null +++ b/policyengine_us/variables/household/income/person/capital_gains/estate_or_trust_short_term_capital_loss_carryover.py @@ -0,0 +1,37 @@ +from policyengine_us.model_api import * + + +class estate_or_trust_short_term_capital_loss_carryover(Variable): + value_type = float + entity = Person + label = "Short-term capital loss carryover from a terminated estate or trust" + unit = USD + documentation = ( + "Short-term capital loss carryover a beneficiary succeeds to when an " + "estate or trust terminates, as a positive amount: Schedule K-1 (Form " + "1041), box 11, code C, from the estate or trust's final year. Under " + "26 U.S.C. 642(h)(1) and Reg. 1.642(h)-1(a), a capital loss carryover " + "the estate or trust could have used after that year but for the " + "termination is allowed to the beneficiaries succeeding to its " + "property. Reg. 1.642(h)-1(b) keeps it short-term. The beneficiary " + "reports it on Schedule D line 5. The person's own carryover goes in " + "own_short_term_capital_loss_carryover instead. Memo item: " + "short_term_capital_gains already includes it as a loss, as Schedule " + "D line 7 does. Zero unless entered." + ) + definition_period = YEAR + uprating = "calibration.gov.irs.soi.short_term_capital_gains" + reference = [ + dict( + title="26 U.S. Code § 642(h)(1)", + href="https://www.law.cornell.edu/uscode/text/26/642#h_1", + ), + dict( + title="26 CFR § 1.642(h)-1", + href="https://www.law.cornell.edu/cfr/text/26/1.642(h)-1", + ), + dict( + title="2025 Instructions for Schedule K-1 (Form 1041) for a Beneficiary Filing Form 1040 or 1040-SR, Box 11, Codes C and D", + href="https://www.irs.gov/pub/irs-prior/i1041sk1--2025.pdf#page=3", + ), + ] diff --git a/policyengine_us/variables/household/income/person/capital_gains/long_term_capital_gains.py b/policyengine_us/variables/household/income/person/capital_gains/long_term_capital_gains.py index 86d1e3e9d59..bbba9e7ef14 100644 --- a/policyengine_us/variables/household/income/person/capital_gains/long_term_capital_gains.py +++ b/policyengine_us/variables/household/income/person/capital_gains/long_term_capital_gains.py @@ -6,7 +6,12 @@ class long_term_capital_gains(Variable): entity = Person label = "long-term capital gains" unit = USD - documentation = "Net gains made from sales of assets held for more than one year (losses are expressed as negative gains)." + documentation = ( + "Net gains made from sales of assets held for more than one year (losses " + "are expressed as negative gains): Schedule D line 15, net of any " + "long-term capital loss carryover, which long_term_capital_loss_carryover " + "records as a memo." + ) definition_period = YEAR reference = dict( title="26 U.S. Code § 1222(3)", diff --git a/policyengine_us/variables/household/income/person/capital_gains/long_term_capital_loss_carryover.py b/policyengine_us/variables/household/income/person/capital_gains/long_term_capital_loss_carryover.py index ae642e1df84..3c5aba0cddb 100644 --- a/policyengine_us/variables/household/income/person/capital_gains/long_term_capital_loss_carryover.py +++ b/policyengine_us/variables/household/income/person/capital_gains/long_term_capital_loss_carryover.py @@ -7,20 +7,28 @@ class long_term_capital_loss_carryover(Variable): label = "Long-term capital loss carryover" unit = USD documentation = ( - "Long-term capital loss carried to the year under 26 U.S.C. " - "1212(b)(1)(B), as a positive amount: Schedule D line 14 plus any " - "carryover on Schedule K-1 (Form 1041), box 11, code D, from the " - "final year of an estate or trust (line 5 of the 28% Rate Gain " - "Worksheet and line 16 of the Unrecaptured Section 1250 Gain " - "Worksheet). Memo item: long_term_capital_gains already includes it " - "as a loss, as Schedule D line 15 does, so it does not change gross " - "income. It reduces collectibles_gain_or_loss and section_1202_gain " - "in the 28-percent rate gain, and what they do not absorb reduces " - "unrecaptured_section_1250_gain_before_losses. Zero, no carryover, " - "unless entered." + "Long-term capital loss carried to the year, as a positive amount: the " + "person's own carryover under 26 U.S.C. 1212(b)(1)(B), on Schedule D " + "line 14, plus any carryover allowed under 26 U.S.C. 642(h)(1) from " + "the final year of an estate or trust, on Schedule K-1 (Form 1041), " + "box 11, code D, which is part of Schedule D line 12. Together they " + "are line 5 of the 28% Rate Gain Worksheet and line 16 of the " + "Unrecaptured Section 1250 Gain Worksheet. The sum of " + "own_long_term_capital_loss_carryover and " + "estate_or_trust_long_term_capital_loss_carryover. A nonzero amount " + "entered here for a year is filed as the person's own carryover when " + "a Simulation or Microsimulation is built, in place of the leaves " + "entered for that year. Memo item: long_term_capital_gains already " + "includes it as a loss, as Schedule D line 15 does, so it does not " + "change gross income. It reduces collectibles_gain_or_loss and " + "section_1202_gain in the 28-percent rate gain, and what they do not " + "absorb reduces unrecaptured_section_1250_gain_before_losses." ) definition_period = YEAR - uprating = "calibration.gov.irs.soi.long_term_capital_gains" + adds = [ + "own_long_term_capital_loss_carryover", + "estate_or_trust_long_term_capital_loss_carryover", + ] reference = [ dict( title="26 U.S. Code § 1(h)(4)(B)(iii)", @@ -31,7 +39,15 @@ class long_term_capital_loss_carryover(Variable): href="https://www.law.cornell.edu/uscode/text/26/1212#b_1_B", ), dict( - title="2025 Instructions for Schedule D (Form 1040), 28% Rate Gain Worksheet—Line 18, line 5", - href="https://www.irs.gov/pub/irs-prior/i1040sd--2025.pdf#page=11", + title="26 U.S. Code § 642(h)(1)", + href="https://www.law.cornell.edu/uscode/text/26/642#h_1", + ), + dict( + title="2025 Instructions for Schedule D (Form 1040), Capital Loss Carryover Worksheet, 28% Rate Gain Worksheet and Unrecaptured Section 1250 Gain Worksheet #page 10-11,13", + href="https://www.irs.gov/pub/irs-prior/i1040sd--2025.pdf#page=10", + ), + dict( + title="2025 Instructions for Schedule K-1 (Form 1041) for a Beneficiary Filing Form 1040 or 1040-SR, Box 11, Codes C and D", + href="https://www.irs.gov/pub/irs-prior/i1041sk1--2025.pdf#page=3", ), ] diff --git a/policyengine_us/variables/household/income/person/capital_gains/own_long_term_capital_loss_carryover.py b/policyengine_us/variables/household/income/person/capital_gains/own_long_term_capital_loss_carryover.py new file mode 100644 index 00000000000..fcaf2f13943 --- /dev/null +++ b/policyengine_us/variables/household/income/person/capital_gains/own_long_term_capital_loss_carryover.py @@ -0,0 +1,29 @@ +from policyengine_us.model_api import * + + +class own_long_term_capital_loss_carryover(Variable): + value_type = float + entity = Person + label = "Own long-term capital loss carryover" + unit = USD + documentation = ( + "Long-term capital loss carried to the year from the person's own " + "earlier years under 26 U.S.C. 1212(b)(1)(B), as a positive amount: " + "Schedule D line 14, from line 13 of the Capital Loss Carryover " + "Worksheet. A carryover from a terminated estate or trust goes in " + "estate_or_trust_long_term_capital_loss_carryover instead. Memo item: " + "long_term_capital_gains already includes it as a loss, as Schedule D " + "line 15 does. Zero, no carryover, unless entered." + ) + definition_period = YEAR + uprating = "calibration.gov.irs.soi.long_term_capital_gains" + reference = [ + dict( + title="26 U.S. Code § 1212(b)(1)(B)", + href="https://www.law.cornell.edu/uscode/text/26/1212#b_1_B", + ), + dict( + title="2025 Instructions for Schedule D (Form 1040), Capital Loss Carryover Worksheet—Lines 6 and 14", + href="https://www.irs.gov/pub/irs-prior/i1040sd--2025.pdf#page=10", + ), + ] diff --git a/policyengine_us/variables/household/income/person/capital_gains/own_short_term_capital_loss_carryover.py b/policyengine_us/variables/household/income/person/capital_gains/own_short_term_capital_loss_carryover.py new file mode 100644 index 00000000000..5a3d05a8fd2 --- /dev/null +++ b/policyengine_us/variables/household/income/person/capital_gains/own_short_term_capital_loss_carryover.py @@ -0,0 +1,29 @@ +from policyengine_us.model_api import * + + +class own_short_term_capital_loss_carryover(Variable): + value_type = float + entity = Person + label = "Own short-term capital loss carryover" + unit = USD + documentation = ( + "Short-term capital loss carried to the year from the person's own " + "earlier years under 26 U.S.C. 1212(b)(1)(A), as a positive amount: " + "Schedule D line 6, from line 8 of the Capital Loss Carryover " + "Worksheet. A carryover from a terminated estate or trust goes in " + "estate_or_trust_short_term_capital_loss_carryover instead. Memo item: " + "short_term_capital_gains already includes it as a loss, as Schedule D " + "line 7 does. Zero, no carryover, unless entered." + ) + definition_period = YEAR + uprating = "calibration.gov.irs.soi.short_term_capital_gains" + reference = [ + dict( + title="26 U.S. Code § 1212(b)(1)(A)", + href="https://www.law.cornell.edu/uscode/text/26/1212#b_1_A", + ), + dict( + title="2025 Instructions for Schedule D (Form 1040), Capital Loss Carryover Worksheet—Lines 6 and 14", + href="https://www.irs.gov/pub/irs-prior/i1040sd--2025.pdf#page=10", + ), + ] diff --git a/policyengine_us/variables/household/income/person/capital_gains/short_term_capital_gains.py b/policyengine_us/variables/household/income/person/capital_gains/short_term_capital_gains.py index fc7cf2ba8ea..db9cd997e45 100644 --- a/policyengine_us/variables/household/income/person/capital_gains/short_term_capital_gains.py +++ b/policyengine_us/variables/household/income/person/capital_gains/short_term_capital_gains.py @@ -6,7 +6,12 @@ class short_term_capital_gains(Variable): entity = Person label = "short-term capital gains" unit = USD - documentation = "Net gains made from sales of assets held for one year or less(losses are expressed as negative gains)." + documentation = ( + "Net gains made from sales of assets held for one year or less (losses " + "are expressed as negative gains): Schedule D line 7, net of any " + "short-term capital loss carryover, which short_term_capital_loss_carryover " + "records as a memo." + ) definition_period = YEAR reference = dict( title="26 U.S. Code § 1222(1)", diff --git a/policyengine_us/variables/household/income/person/capital_gains/short_term_capital_loss_carryover.py b/policyengine_us/variables/household/income/person/capital_gains/short_term_capital_loss_carryover.py new file mode 100644 index 00000000000..ea6b7d90428 --- /dev/null +++ b/policyengine_us/variables/household/income/person/capital_gains/short_term_capital_loss_carryover.py @@ -0,0 +1,47 @@ +from policyengine_us.model_api import * + + +class short_term_capital_loss_carryover(Variable): + value_type = float + entity = Person + label = "Short-term capital loss carryover" + unit = USD + documentation = ( + "Short-term capital loss carried to the year, as a positive amount: " + "the person's own carryover under 26 U.S.C. 1212(b)(1)(A), on " + "Schedule D line 6, plus any carryover allowed under 26 U.S.C. " + "642(h)(1) from the final year of an estate or trust, on Schedule K-1 " + "(Form 1041), box 11, code C, which is part of Schedule D line 5. The " + "sum of own_short_term_capital_loss_carryover and " + "estate_or_trust_short_term_capital_loss_carryover. A nonzero amount " + "entered here for a year is filed as the person's own carryover when " + "a Simulation or Microsimulation is built, in place of the leaves " + "entered for that year. Memo item: short_term_capital_gains already " + "includes it as a loss, as Schedule D line 7 does, so it does not " + "change gross income or the net short-term capital loss on line 6 of " + "the 28% Rate Gain Worksheet. The Arizona property tax credit adds it " + "back." + ) + definition_period = YEAR + adds = [ + "own_short_term_capital_loss_carryover", + "estate_or_trust_short_term_capital_loss_carryover", + ] + reference = [ + dict( + title="26 U.S. Code § 1212(b)(1)(A)", + href="https://www.law.cornell.edu/uscode/text/26/1212#b_1_A", + ), + dict( + title="26 U.S. Code § 642(h)(1)", + href="https://www.law.cornell.edu/uscode/text/26/642#h_1", + ), + dict( + title="2025 Instructions for Schedule D (Form 1040), Capital Loss Carryover Worksheet and 28% Rate Gain Worksheet #page 10-11", + href="https://www.irs.gov/pub/irs-prior/i1040sd--2025.pdf#page=10", + ), + dict( + title="2025 Instructions for Schedule K-1 (Form 1041) for a Beneficiary Filing Form 1040 or 1040-SR, Box 11, Codes C and D", + href="https://www.irs.gov/pub/irs-prior/i1041sk1--2025.pdf#page=3", + ), + ]