Resettlement & True-Up Processing
A validated meter file lands ninety days after the trading day, correcting a single revenue-quality channel that spent the preliminary run on an estimate — and the question is not whether the charge changes but which line items are allowed to move, by how much, and how the difference gets recorded without disturbing the thousands of intervals that were always correct. Answer it carelessly and the run either restates the whole day (destroying the audit trail that ties every dollar to a version) or silently overwrites the original figure (destroying the ability to prove what was invoiced). This component of the Settlement Calculation & Validation Engines framework governs that transition: it takes a trading day through its preliminary, final true-up, and any resettlement runs, recomputing only the intervals whose inputs actually changed, computing a signed delta against the prior run, and appending an adjustment record rather than editing history. The run identifiers and cycle windows it keys off are defined upstream by Settlement Cycle Mapping, and the deviation math it re-runs on corrected schedules is the same logic documented in Imbalance Allocation Algorithms.
The diagram traces the internal stages every restatement passes through: a change-detection step compares the newly arrived input snapshot against the snapshot the prior run pinned, scopes the affected (settlement_point, interval_start) keys, recomputes only those, diffs the result against the prior ledger, and appends the signed adjustment.
The rule that makes this defensible is simple to state and unforgiving to violate: a restatement never mutates a prior run in place. The preliminary ledger is frozen the moment it is invoiced; the final and resettlement runs are new, separately identified ledgers, and the adjustment record is the arithmetic bridge between them. The sections below fix the standards that bound what may move, the production steps that scope and compute the delta, the edge cases that corrupt a naive restatement, and the reconciliation controls that prove the true-up is correct.
Specification & Standards
Resettlement is not a discretionary correction — its timing, its permitted triggers, and its dispute windows are fixed by the tariff of the market being settled and by the federal rules layered above it. Each Regional Transmission Organization (RTO) and Independent System Operator (ISO) publishes a settlements business practice manual that enumerates the settlement runs for a trading day, the calendar offset of each, and the window inside which a settled charge may be reopened. PJM’s settlements timeline, MISO BPM-005, CAISO’s Settlements and Billing Business Practice Manual, and ERCOT Nodal Protocol Section 9 each name their runs differently, but all share the same skeleton: an initial or preliminary statement, one or more true-up statements as validated meter data replaces estimates, and a bounded resettlement or recalculation window for disputes and data corrections.
Three standards constrain the engine directly:
- FERC traceability and true-up obligation. Under the FERC Uniform System of Accounts and market-monitoring rules, every settled dollar — including every adjustment dollar — must trace to a metered interval, a published price version, and a tariff provision. A true-up that cannot show why a figure moved between the preliminary and final run is not compliant, which is why the engine persists the signed delta and its cause, not merely the new total.
- Dispute-window bounding. Tariffs fix the number of business days (commonly measured from statement issuance) inside which a participant may dispute a charge and inside which the ISO itself may resettle. Corrections arriving after the window are not silently absorbed; they route to a governed exception path, because reopening a closed period changes the accounting treatment.
- NAESB interchange reference points. NAESB Wholesale Electric Quadrant business practices define how scheduled quantities and tags are expressed. When a resettlement is driven by a corrected schedule rather than corrected meter data, those NAESB-standard quantities are the baseline the recomputed imbalance is measured against.
Because tariffs and dispute windows are revised continuously, the engine treats each as versioned configuration mapped per market. A resettlement run pins the tariff revision, price publication, and loss-factor publication that were in force for the trading day being restated — never today’s rules applied retroactively — so that a corrected interval is recomputed under exactly the regime it was originally settled under, with only the corrected input allowed to change.
The run taxonomy the rest of this component keys off is fixed by the following table. The calendar offsets are illustrative of a typical two-settlement RTO and are themselves configuration; the invariant is the ordering and the substitution policy of each run.
| Run type | Typical timing | Input snapshot | Intervals recomputed | Financial effect |
|---|---|---|---|---|
| Preliminary | T+1 to T+7 | SCADA / initial MDM export, estimates flagged | All intervals of the day | Initial invoice / margin |
| Final (true-up) | T+55 to T+90 | Validated MDM (VEE) meter data | Only intervals whose input changed vs preliminary | Final charge, delta vs preliminary |
| Resettlement | Per tariff dispute window | Corrected meter or price data | Only the disputed / corrected intervals | Adjustment charge, delta vs prior run |
| Second resettlement | Late dispute / ISO recalc | Further corrected data | Only intervals changed again | Adjustment vs most recent run |
Each row after the preliminary reads its baseline from the immediately preceding run, so a chain of restatements composes: the sum of a preliminary charge and every subsequent signed delta always equals the current settled position for that interval.
Step-by-Step Implementation
A restatement is a disciplined sequence: pin the prior run, detect which inputs changed, scope the affected keys, recompute only those, and diff. Skipping the scoping step and recomputing the whole day is the single most common design error — it works numerically but destroys the ability to attribute a change to a cause and inflates the audit trail with unchanged rows.
Step 1 — Pin the prior run and its input fingerprint. Every run persists not only its ledger but a content hash of each interval’s inputs (meter reading, price, loss factor, schedule). The restatement begins by loading the prior run’s ledger and its per-interval input fingerprints. This is what makes change detection cheap and exact rather than a full recompute-and-compare.
from dataclasses import dataclass
from decimal import Decimal
import hashlib
import json
@dataclass(frozen=True)
class IntervalInputs:
settlement_point: str
interval_start: str # ISO-8601, timezone-aware
metered_mwh: Decimal
price_per_mwh: Decimal
loss_factor: Decimal
price_version: str
def input_fingerprint(inputs: IntervalInputs) -> str:
"""Stable SHA-256 over the canonical inputs of one settlement interval."""
canonical = json.dumps(
{
"settlement_point": inputs.settlement_point,
"interval_start": inputs.interval_start,
"metered_mwh": str(inputs.metered_mwh),
"price_per_mwh": str(inputs.price_per_mwh),
"loss_factor": str(inputs.loss_factor),
"price_version": inputs.price_version,
},
sort_keys=True,
)
return hashlib.sha256(canonical.encode()).hexdigest()
Step 2 — Detect changed keys. Fingerprint the newly arrived snapshot and compare, key by key, against the prior run’s fingerprints. Only keys whose fingerprint differs — or keys that are new — enter the affected set. A key that is present in the prior run but absent from the new snapshot is a distinct case (a withdrawn reading) and is flagged rather than dropped.
def scope_affected_keys(
prior_fingerprints: dict[tuple[str, str], str],
new_inputs: dict[tuple[str, str], IntervalInputs],
) -> set[tuple[str, str]]:
"""Return only the (settlement_point, interval_start) keys whose inputs changed."""
affected: set[tuple[str, str]] = set()
for key, inputs in new_inputs.items():
if prior_fingerprints.get(key) != input_fingerprint(inputs):
affected.add(key) # changed value or brand-new key
withdrawn = set(prior_fingerprints) - set(new_inputs)
affected |= withdrawn # a disappeared reading is a change too
return affected
Step 3 — Recompute only the affected intervals. The recompute reuses the exact same pure calculation the preliminary run used — pricing, loss, imbalance — under the trading day’s pinned tariff and price versions. Only the affected keys are fed in, so the cost of a true-up scales with how much data actually changed, not with the size of the day. The money math stays in Decimal end to end.
def recompute_line_item(inputs: IntervalInputs) -> Decimal:
"""Settle one interval's delivered energy at its nodal price. Decimal throughout."""
delivered_mwh = (inputs.metered_mwh * inputs.loss_factor)
amount = (delivered_mwh * inputs.price_per_mwh).quantize(Decimal("0.01"))
return amount
Step 4 — Diff against the prior run and emit a signed adjustment. For each affected key, subtract the prior amount from the recomputed amount. A positive delta increases the settled charge; a negative delta refunds. The adjustment row carries both the prior and new values, the delta, the cause, and the run identifiers, so the bridge from one run to the next is explicit and replayable. The concrete diff-and-ledger emitter is worked end to end in Computing preliminary-to-final settlement deltas.
def emit_adjustment(
key: tuple[str, str],
prior_amount: Decimal,
new_amount: Decimal,
prior_run_id: str,
run_id: str,
cause: str,
) -> dict:
"""One signed restatement row bridging prior_run_id -> run_id for a single key."""
delta = (new_amount - prior_amount).quantize(Decimal("0.01"))
settlement_point, interval_start = key
return {
"settlement_point": settlement_point,
"interval_start": interval_start,
"prior_run_id": prior_run_id,
"run_id": run_id,
"prior_amount": str(prior_amount),
"new_amount": str(new_amount),
"delta_amount": str(delta), # signed: + is a charge, - is a refund
"cause": cause, # e.g. "vee_meter_replaced_estimate"
}
Step 5 — Append, never overwrite. The adjustment rows append to the ledger under the new run_id; the prior run’s rows are left untouched. Persistence is idempotent on (run_id, settlement_point, interval_start), so re-executing a partial restatement after a crash replaces exactly the rows for the current run and nothing from any prior run. This is the property that lets an auditor reconstruct the settled position at any point in the day’s history.
Edge Cases & Failure Modes
The naive version of this component — recompute the day, overwrite the ledger — passes a happy-path test and fails every real correction. The failure modes below are the ones that corrupt a restatement in production; each needs explicit handling rather than a silent default.
| Failure mode | What goes wrong | Correct handling |
|---|---|---|
| Whole-day recompute | Unchanged intervals get new rows; cause attribution is lost | Scope to changed keys only via input fingerprints |
| In-place overwrite | Original invoiced figure is destroyed; disputes unprovable | Append signed adjustment under a new run_id |
| Non-idempotent replay | A retried restatement double-counts a delta | Upsert on (run_id, point, interval); delta always vs prior run |
| Estimate not flagged | True-up cannot tell which intervals were substituted | Carry a substitution flag on every preliminary row |
| Out-of-window correction | A closed period is silently reopened | Route to exception review; do not auto-apply |
| Float drift on delta | prior − new leaves sub-cent residue that never nets to zero | Quantize every amount to cents with Decimal before diffing |
| DST boundary interval | Spring-forward gap or fall-back overlap misaligns keys | Key on timezone-aware interval_start; handle the duplicated hour explicitly |
Two of these deserve emphasis. Zero-delta intervals — where an input changed but the recomputed amount is identical to the cent (a price correction below the rounding boundary, say) — should still be scoped as affected during detection but must not emit an adjustment row, or the ledger fills with meaningless zero deltas that obscure the real movements. Stale-telemetry substitution interacts directly with true-up: the preliminary run’s estimated intervals are exactly the ones most likely to change at final, so the substitution flag is not cosmetic — it is the predictor of where the delta will land, and it drives the corrected loss and price lookups documented in Loss Factor Mapping Strategies and Pricing Logic Implementation.
Schema drift on the corrected snapshot is a hard-stop rather than a warning: if a late meter file changes a column type or unit, the fingerprint comparison becomes meaningless because the canonical form has shifted under it. The restatement validates the incoming snapshot against the same contract the preliminary run enforced before any comparison runs, rejecting a drifted file to a dead-letter path so a unit change never masquerades as a genuine data correction.
Thresholds & Alerting
Not every delta is equal. A one-cent true-up on a single interval is noise; a five-figure swing on a portfolio the day before an invoice locks is an event. The component classifies each restatement by the magnitude and breadth of its deltas and routes accordingly, using the same tiering discipline as Threshold Tuning & Alerts.
The alert decision keys off three configurable parameters: the absolute per-interval delta, the aggregate signed delta across the run, and the fraction of a portfolio’s intervals that moved. A large aggregate built from many tiny in-tolerance deltas is treated differently from the same aggregate concentrated in one interval — the latter signals a data error, the former a broad price revision.
from decimal import Decimal
def classify_restatement(
deltas: list[Decimal],
per_interval_review: Decimal = Decimal("500.00"),
aggregate_block: Decimal = Decimal("25000.00"),
) -> str:
"""Tier a restatement: informational, review-required, or block-invoice."""
aggregate = sum(deltas, Decimal("0.00"))
max_abs = max((abs(d) for d in deltas), default=Decimal("0.00"))
if abs(aggregate) >= aggregate_block:
return "block_invoice" # needs sign-off before the invoice locks
if max_abs >= per_interval_review:
return "review_required" # a single interval moved materially
return "informational" # within routine true-up noise
The tiers map to escalation routing: informational deltas are logged and rolled into the next statement without intervention; review-required restatements notify the analyst who owns the affected portfolio; block-invoice restatements require explicit operator acknowledgment, logged with the operator identity, before the adjustment is allowed to affect a settled figure. Because the classification runs on the signed deltas the diff already produced, it adds no recomputation — it is a pure function of the restatement output.
Testing & Reconciliation
A restatement engine earns trust by proving two properties on every run: that the deltas compose (preliminary plus every subsequent delta equals the current settled position) and that an unchanged input produces no delta at all. Both are cheap assertions and both catch the errors that matter.
The shadow-calculation approach runs the restatement against a frozen fixture: a known preliminary ledger, a corrected snapshot with a hand-computed expected delta, and an assertion that the emitted adjustment matches to the cent. The idempotency test runs the same restatement twice and asserts the second run emits identical rows and changes no prior-run row.
from decimal import Decimal
def test_delta_composition():
"""Preliminary + final delta must equal the recomputed final amount, to the cent."""
prelim_amount = Decimal("1240.55")
final_inputs = IntervalInputs(
settlement_point="PJM.WESTHUB",
interval_start="2026-03-08T02:00:00-05:00",
metered_mwh=Decimal("52.400"),
price_per_mwh=Decimal("24.15"),
loss_factor=Decimal("0.9820"),
price_version="rt-final-v3",
)
final_amount = recompute_line_item(final_inputs)
adjustment = emit_adjustment(
("PJM.WESTHUB", final_inputs.interval_start),
prelim_amount, final_amount,
prior_run_id="prelim-2026-03-08",
run_id="final-2026-03-08",
cause="vee_meter_replaced_estimate",
)
# The composed position must reconstruct the recomputed amount exactly.
composed = prelim_amount + Decimal(adjustment["delta_amount"])
assert composed == final_amount
def test_unchanged_input_emits_no_delta():
"""Re-running with identical inputs must scope zero affected keys."""
key = ("PJM.WESTHUB", "2026-03-08T03:00:00-05:00")
inputs = IntervalInputs(key[0], key[1], Decimal("50.0"),
Decimal("30.00"), Decimal("0.99"), "rt-final-v3")
prior = {key: input_fingerprint(inputs)}
affected = scope_affected_keys(prior, {key: inputs})
assert affected == set()
Reconciliation against the ISO closes the loop. After a true-up, the engine joins its adjustment ledger against the ISO’s resettlement statement on (settlement_point, interval_start) and asserts the signed deltas agree within tolerance. A residual outside the band is treated as a topology or version mismatch — usually the restatement pinned a different price publication than the ISO’s recalculation — rather than a rounding artifact, and it routes to the same exception path a preliminary reconciliation break would. The storage format for the frozen prior ledgers that make replay cheap is weighed in CSV vs Parquet for settlement ledger storage.
Frequently Asked Questions
What is the difference between a true-up and a resettlement?
A true-up is the scheduled progression from a preliminary statement to a final one as estimated meter data is replaced by validated (VEE) actuals — it happens on a fixed calendar offset for every trading day. A resettlement is an event-driven reopening of an already-final period inside the tariff’s dispute window, triggered by a corrected meter reading, a corrected price publication, or a granted dispute. Mechanically they run the same code — detect changed inputs, recompute only those intervals, diff against the prior run — but a resettlement additionally checks that the correction falls inside the permitted window before it is allowed to apply.
Why recompute only the affected intervals instead of the whole day?
Two reasons: attribution and auditability. Recomputing the whole day and overwriting the ledger produces the same final numbers but destroys the ability to say which input changed and why a charge moved, because every interval gets a fresh row whether or not anything about it changed. Scoping to the intervals whose input fingerprint actually differs means each adjustment row bridges a specific cause to a specific delta, the true-up cost scales with the size of the correction rather than the size of the day, and the audit trail stays legible to a counterparty months later.
How does idempotent restatement prevent double-counting?
Every restatement is keyed on (run_id, settlement_point, interval_start) and every delta is computed against the immediately prior run, not against a running accumulator. Persistence upserts on that key, so if a restatement job crashes midway and is retried, the second execution replaces exactly the rows for the current run and recomputes the same delta against the same frozen prior ledger — the result is bit-identical, and no prior-run row is ever touched. Because the delta is always relative to a pinned baseline rather than added to a mutable total, replaying a run can never inflate the settled position.
What happens when a correction arrives after the dispute window closes?
The engine does not silently absorb it. A correction whose interval falls outside the tariff’s resettlement window is scoped as affected but routed to a governed exception review rather than auto-applied, because reopening a closed accounting period changes its treatment and may require a manual filing or a booking to a current-period adjustment account instead. The dispute-window boundary is versioned configuration mapped per market, so the gate reflects the rule that was in force for the trading day being corrected.