Files
stack/tests/lead/test_settlement.py
kert cb9976e160
All checks were successful
CI / lint (push) Successful in 44s
CI / notebooks-smoke (push) Successful in 1m33s
Deploy / changes (push) Successful in 14s
CI / test (push) Successful in 3m9s
Infra CI / zotero (push) Successful in 14s
Infra CI / notebooks (push) Successful in 55s
Infra CI / api (push) Successful in 1m9s
Infra CI / docs (push) Successful in 1m23s
Infra CI / llm (push) Successful in 1m1s
Infra CI / mc (push) Successful in 16s
Infra CI / bcda (push) Successful in 1m43s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Package Supply Chain / pkg-supply-chain (push) Successful in 1m30s
Deploy / api (push) Successful in 1m17s
Deploy / llm (push) Successful in 1m18s
Deploy / mc (push) Has been skipped
Deploy / bcda (push) Successful in 1m42s
Deploy / report (push) Successful in 1m36s
feat(lead): LEAD PY2027 financial methodology — settlement and benchmark engines
Ports the public CMS "LEAD Model PY 2027 Alignment and Financial
Methodology Paper" v1.2 (bib E4PEU3HV) as generic engines: parameters
come from the paper, everything CMS computes per ACO or per year comes
from the caller. No participant data, no organization-specific rules.

- lead.params: Table 5.2.1 discounts, 3% Quality Withhold, 2% retention
  incentive, Box 5.3.3 Stop-Loss bands, Tables 5.4.1/5.4.3 corridors, 2%
  sequestration, base-year weights, Table 3.5.1 ACPT guardrails,
  risk-score growth caps, REA/PSA shares and caps, voluntary guardrail
  and 500 minimum, Table 2.6.1 base-year alignment minimums.
- lead.settlement: Final Financial Settlement, Table 5.6.1 line by line
  (settle(SettlementInputs) -> SettlementLines).
- lead.stop_loss: Box 5.3.3 charge, banded payout, neutrality factor,
  application to PY expenditure.
- lead.monies_owed: Table 5.5.1 Total Monies Owed, APO reconciliation.
- lead.timing: run-out end, settlement-basis rule, settlement-stage inputs.
- lead.benchmark: §3 formulas — risk standardization, two-/three-way
  trend, base-year weights and sufficiency, voluntary guardrail,
  regional FFS, spending designation, REA, PSA, risk-score cap, hybrid
  alignment dollars, category blend.
- lead.express: the same as narwhals Exprs; express.settlement.settle
  settles a frame of scenarios on any backend.

Rounding: every dollar line rounds to the cent (half away from zero) as
it is produced; PBPMs, rates and factors never do. It is the only rule
under which Tables 3.3.3, 3.5.3.2, 3.5.4, 5.5.1 and 5.6.1 all reproduce
(3.5.4 line 24 sums cent-rounded cells; 3.3.3 carries unrounded PBPMs).
The Expr cents snaps its ÷100 with round(2): polars 2.0 divides a column
by a literal as a reciprocal multiply (k × 0.01), one ulp off k/100 for
~15% of values.

Missing inputs stay missing (None/null or a refused input model); the
only zeros are the paper's own (Professional "N/A" discount, no
Stop-Loss election, sequestration on losses, ineligible REA/PSA).

Paper inconsistencies recorded in tests: Table 5.5.1 line 2 is a cent
below Table 5.6.1 line 28 (.43 vs .44); Box 3.3.2 prints 1.104 then says
1.103; §3.5.1 prose widens the ACPT lower bound a year later than
Table 3.5.1 (the table is followed).

Tests: 283 in tests/lead — every published line to the cent, Expr
builders on polars and DuckDB, bit-for-bit scalar/frame parity over 160
scenarios; 100% of src/lead.
2026-10-27 14:11:10 -04:00

285 lines
10 KiB
Python

"""lead.settlement reproduces Table 5.6.1 line by line and keeps the
paper's edge cases: no Professional discount, the "N/A" discount, losses,
corridor boundaries, the retention incentive, and Stop-Loss elections."""
from __future__ import annotations
from dataclasses import asdict, replace
import pytest
from pydantic import ValidationError
from lead.params import DISCOUNT_RATES, RISK_CORRIDORS
from lead.settlement import (
SettlementInputs,
adjust_benchmark,
apply_discount,
benchmark_discount_rate,
corridor_exposure,
gross_savings_losses,
py_expenditure,
quality_earn_back,
quality_withhold,
retention_incentive_adjustment,
sequestration_amount,
settle,
shared_savings_losses,
)
from tests.lead.paper import TABLE_5_6_1, TABLE_5_6_1_INPUTS, TABLE_5_6_1_LINES
def table_5_6_1(risk_option: str, **changes) -> SettlementInputs:
return SettlementInputs(
**{**TABLE_5_6_1_INPUTS, "risk_option": risk_option, **changes}
)
# ── Table 5.6.1 ───────────────────────────────────────────────────────────
@pytest.mark.parametrize("risk_option", ["Global", "Professional"])
@pytest.mark.parametrize(
"line,field", TABLE_5_6_1_LINES, ids=[f"line {n}" for n, _ in TABLE_5_6_1_LINES]
)
def test_table_5_6_1_every_line_to_the_cent(risk_option, line, field):
lines = asdict(settle(table_5_6_1(risk_option)))
assert lines[field] == TABLE_5_6_1[risk_option][line]
def test_table_5_2_2_is_lines_1_to_9_of_table_5_6_1():
# Table 5.2.2 prints the same benchmark adjustments, reductions positive.
for option, rate in (("Global", 0.03), ("Professional", 0.0)):
adjusted = adjust_benchmark(150_000_000.0, rate, 0.95)
assert adjusted.benchmark_after_adjustments == TABLE_5_6_1[option]["9"]
assert adjusted.net_quality_withhold == 225_000.00
def test_table_5_3_2_py_expenditure():
expenditure = py_expenditure(10_000_000, 1_003_442, 33_435_084, 91_355_457)
assert (expenditure.total_ffs_payments, expenditure.py_expenditure) == (
125_793_983.00,
135_793_983.00,
)
def test_tables_5_4_2_and_5_4_4_corridor_lines():
global_ = shared_savings_losses(9_229_682.72, 145_275_000.0, "Global")
professional = shared_savings_losses(13_729_682.72, 149_775_000.0, "Professional")
assert global_.retained_by_corridor == (9_229_682.72, 0.0, 0.0, 0.0)
assert professional.retained_by_corridor == (8_237_809.63, 0.0, 0.0, 0.0)
assert (global_.scenario, professional.scenario) == ("savings", "savings")
# ── §5.2 benchmark adjustments ────────────────────────────────────────────
@pytest.mark.parametrize(
"designation,year,rate",
[(d, y, r) for d, by_year in DISCOUNT_RATES.items() for y, r in by_year.items()],
)
def test_table_5_2_1_discount_schedule(designation, year, rate):
assert benchmark_discount_rate("Global", designation, year) == rate
def test_discount_schedule_values():
higher = DISCOUNT_RATES["Higher-spending"]
assert [higher[y] for y in range(2027, 2033)] == [
0.0175,
0.02,
0.0225,
0.025,
0.0275,
0.03,
]
assert set(DISCOUNT_RATES["Lower-spending"].values()) == {0.03}
def test_professional_option_has_no_discount_whatever_the_designation():
for designation in ("Lower-spending", "Higher-spending", None):
assert benchmark_discount_rate("Professional", designation, 2031) == 0.0
def test_higher_spending_2027_discount_dollars():
lines = settle(table_5_6_1("Global", spending_designation="Higher-spending"))
assert (lines.discount_rate, lines.discount_amount) == (0.0175, 2_625_000.00)
assert lines.benchmark_after_adjustments == 147_150_000.00
def test_discount_rate_refuses_what_it_cannot_look_up():
with pytest.raises(ValueError, match="spending designation"):
benchmark_discount_rate("Global", None, 2027)
with pytest.raises(ValueError, match="outside the LEAD model"):
benchmark_discount_rate("Global", "Lower-spending", 2037)
with pytest.raises(ValueError, match="unknown risk option"):
benchmark_discount_rate("global", "Lower-spending", 2027)
def test_missing_discount_rate_is_the_papers_na_and_removes_nothing():
discount = apply_discount(150_000_000.0, None)
assert (discount.discount_rate, discount.discount_amount) == (0.0, 0.0)
assert discount.benchmark_after_discount == 150_000_000.0
def test_quality_withhold_is_on_the_benchmark_before_the_discount():
assert quality_withhold(150_000_000.0) == 4_500_000.00
assert quality_earn_back(4_500_000.0, 0.0) == 0.0
assert quality_earn_back(4_500_000.0, 1.0) == 4_500_000.0
with pytest.raises(ValueError, match="quality score"):
quality_earn_back(4_500_000.0, 1.2)
def test_retention_incentive_reduces_the_benchmark_by_two_percent():
assert retention_incentive_adjustment(150_000_000.0, False) == 0.0
lines = settle(table_5_6_1("Global", retention_incentive_applies=True))
assert lines.retention_incentive_adjustment == 3_000_000.00
assert lines.benchmark_after_adjustments == 145_275_000.00 - 3_000_000.00
# ── §5.4 corridors, losses, sequestration ─────────────────────────────────
B = 100_000_000.0
@pytest.mark.parametrize(
"option,share,expected",
[
# Global: 100% to 15%, 50% to 35%, 25% to 50%, 10% beyond.
("Global", 0.15, 15_000_000.00),
("Global", 0.35, 15_000_000.00 + 10_000_000.00),
("Global", 0.50, 25_000_000.00 + 3_750_000.00),
("Global", 0.60, 28_750_000.00 + 1_000_000.00),
# Professional savings: 60% to 10%, 35% to 15%, 15% to 20%, 5% beyond.
("Professional", 0.10, 6_000_000.00),
("Professional", 0.15, 6_000_000.00 + 1_750_000.00),
("Professional", 0.20, 7_750_000.00 + 750_000.00),
("Professional", 0.30, 8_500_000.00 + 500_000.00),
# Professional losses: corridor 1 shares 50%.
("Professional", -0.10, -5_000_000.00),
(
"Professional",
-0.30,
-(5_000_000.00 + 1_750_000.00 + 750_000.00 + 500_000.00),
),
("Global", -0.60, -29_750_000.00),
],
)
def test_corridor_boundaries(option, share, expected):
shared = shared_savings_losses(B * share, B, option)
assert shared.shared_savings_losses == expected
assert shared.scenario == ("savings" if share > 0 else "losses")
assert sum(shared.retained_by_corridor) == pytest.approx(expected)
def test_each_corridor_takes_only_its_band():
shared = shared_savings_losses(B * 0.60, B, "Global")
assert shared.retained_by_corridor == (
15_000_000.0,
10_000_000.0,
3_750_000.0,
1_000_000.0,
)
bands = RISK_CORRIDORS[("Global", "savings")]
assert [corridor_exposure(B * 0.2, B, c.lower, c.upper) for c in bands] == [
15_000_000.0,
5_000_000.0,
0.0,
0.0,
]
def test_unknown_risk_option_is_refused():
with pytest.raises(ValueError, match="unknown risk option"):
shared_savings_losses(1.0, B, "Hybrid")
def test_shared_losses_are_not_sequestered():
assert sequestration_amount(-1_000_000.0) == 0.0
assert sequestration_amount(0.0) == 0.0
assert sequestration_amount(9_229_682.72) == 184_593.65
def test_loss_scenario_end_to_end():
# Expenditure 20 percent above a Global benchmark after adjustments.
lines = settle(
table_5_6_1(
"Global",
non_aco_provider_claims=91_355_457.00
+ 0.20 * 145_275_000.00
+ 9_229_682.72,
)
)
assert lines.gross_savings_losses == -29_055_000.00
# 15% at 100% (21,791,250) + 5% at 50% (3,631,875).
assert lines.retained_corridor_1 == -21_791_250.00
assert lines.retained_corridor_2 == -3_631_875.00
assert lines.shared_savings_losses == -25_423_125.00
assert lines.sequestration_amount == 0.0
assert lines.shared_savings_losses_after_sequestration == -25_423_125.00
def test_break_even_shares_nothing():
assert gross_savings_losses(100.0, 100.0) == 0.0
shared = shared_savings_losses(0.0, B, "Professional")
assert (shared.scenario, shared.shared_savings_losses) == ("savings", 0.0)
# ── Stop-Loss elections and input completeness ───────────────────────────
def test_no_stop_loss_election_leaves_expenditure_unchanged():
lines = settle(
table_5_6_1(
"Global",
stop_loss_elected=False,
stop_loss_charge=None,
stop_loss_payout=None,
stop_loss_neutrality_factor=None,
)
)
assert (lines.stop_loss_charge, lines.adjusted_stop_loss_payout) == (0.0, 0.0)
assert lines.stop_loss_neutrality_factor is None
assert lines.net_stop_loss_impact == 0.0
assert (
lines.py_expenditure_after_stop_loss == lines.py_expenditure == 135_793_983.00
)
assert lines.gross_savings_losses == 145_275_000.00 - 135_793_983.00
def test_zero_payout_needs_no_neutrality_factor():
lines = settle(
table_5_6_1("Global", stop_loss_payout=0.0, stop_loss_neutrality_factor=None)
)
assert lines.adjusted_stop_loss_payout == 0.0
assert lines.net_stop_loss_impact == 2_948_334.28
@pytest.mark.parametrize(
"changes,message",
[
({"spending_designation": None}, "spending designation"),
({"performance_year": 2026}, "outside the LEAD model"),
({"stop_loss_charge": None}, "charge and payout are required"),
({"stop_loss_neutrality_factor": None}, "needs the neutrality factor"),
({"stop_loss_elected": False}, "not elected"),
({"quality_score": 1.5}, "less than or equal to 1"),
],
)
def test_incomplete_scenarios_are_refused(changes, message):
with pytest.raises(ValidationError, match=message):
table_5_6_1("Global", **changes)
def test_professional_needs_no_designation():
lines = settle(table_5_6_1("Professional", spending_designation=None))
assert (
lines.shared_savings_losses_after_sequestration
== TABLE_5_6_1["Professional"]["28"]
)
def test_settlement_lines_are_frozen():
lines = settle(table_5_6_1("Global"))
with pytest.raises(AttributeError):
lines.py_benchmark = 0.0 # type: ignore[misc]
assert replace(lines, py_benchmark=1.0).py_benchmark == 1.0