Demand-Charge & Ratchet Calculation for Commercial and Industrial Accounts
For a residential account the bill is mostly energy — kilowatt-hours multiplied by a rate. For a commercial or industrial account the larger and more error-prone line item is usually the demand charge: a price per kilowatt levied on the customer’s highest sustained rate of consumption during the month, not the total energy consumed. This page sits inside Automated Rate Calculation & Rule Engines, and it addresses one specific class of production defect — a demand charge computed from the wrong window, the wrong aggregation, or a stale ratchet floor. Demand billing is where two customers with identical monthly kWh can owe wildly different amounts, and where a single mis-anchored interval can move hundreds of dollars. The stakes rise further with the ratchet: a contract term that sets billed demand to the greater of this month’s peak and a fixed percentage of the highest peak seen over a trailing window of months. A ratchet turns a one-time summer spike into a floor the customer pays against for the next year, so an off-by-one in the window or a lost interval is not a rounding nuisance — it is a defensible-charge problem an auditor will find. The four invariants this workflow enforces are: demand is derived from interval energy with Decimal, never float; the peak is taken over the contractually defined demand interval; the ratchet floor is computed from an explicit, versioned trailing window; and every billed-demand determination writes the inputs that produced it to an audit record.
Prerequisites
Demand and ratchet arithmetic is unforgiving of version drift because the same interval series, re-rated under a slightly different toolchain, must reproduce the same billed kW to the penny.
- Python 3.11+. Required for
zoneinfo,StrEnum, and modernX | Noneunions. Interval math crosses daylight-saving boundaries, so naivedatetimeis disqualified andpytzis never used. decimal.Decimalfor every metered and monetary quantity. Interval demand iskWh ÷ interval_hours; that division and the multiplication by a$/kWprice both accumulate binary drift underfloat, and demand charges are large enough that a fractional-kW error is visible on the invoice.zoneinfofor the utility’s civil time zone. The demand interval and the billing month are civil-time constructs; a spring-forward day has 23 hours and a fall-back day has 25, which changes how many intervals a day contains.- Validated interval reads. This workflow consumes clean, gap-flagged interval energy from meter data ingestion and validation pipelines; it does not itself repair meter data. Reads must arrive already typed, bounded, and de-duplicated.
- A resolved rate context per account. The demand price, the demand-interval length, the ratchet percentage, and the trailing-window length come from the account’s rate schedule, resolved by effective date the same way customer class and service-tier mapping resolves which schedule applies.
- Append-only audit storage. Billed demand is a derived, contestable figure; the inputs behind it must be retained, not just the result.
Architecture Overview
Demand billing is a pipeline, not a single max(). Interval energy is converted to interval demand, demand is aggregated to a monthly peak over the contractual interval, the peak is compared against a ratchet floor derived from a trailing window of prior peaks, and the greater of the two becomes billed demand — which is then priced and, separately, rolled forward to become part of next cycle’s window.
Figure: The current-month peak and the ratchet floor are computed on independent paths and reconciled by a single `max()`; the billed peak then feeds both pricing and next cycle's trailing window.
The decision structure is deliberately narrow. Interval energy is converted to demand exactly once, at a known interval length. The monthly peak is a max over intervals, but which intervals depends on whether the tariff bills non-coincident demand (the customer’s own highest interval) or coincident demand (the customer’s load at the moment of the system peak). The ratchet floor is not part of the current month’s data at all — it is history — and conflating the two is the most common source of a wrong demand charge.
Step-by-Step Implementation
Step 1 — Derive interval demand from interval energy with Decimal
Meters record energy (kWh) per interval; demand (kW) is the rate of that energy over the interval’s duration. The conversion is a division, so it must be done in Decimal at the exact interval length, and the interval length must come from the demand-interval definition on the rate schedule — not assumed to be 15 minutes because most tariffs happen to use that.
from dataclasses import dataclass
from datetime import datetime, timedelta
from decimal import Decimal, ROUND_HALF_UP
FOUR_PLACES = Decimal("0.0001") # kW carried at higher precision than dollars
@dataclass(frozen=True, slots=True)
class IntervalRead:
"""One settled interval of energy for a single meter, in the utility's civil time."""
start_local: datetime # timezone-aware, interval start
energy_kwh: Decimal # metered energy over this interval
span: timedelta # the interval's actual duration
estimated: bool = False # True if the interval was gap-filled upstream
def interval_demand_kw(read: IntervalRead) -> Decimal:
"""Convert interval energy to average demand: kW = kWh / hours-in-interval."""
hours = Decimal(read.span.total_seconds()) / Decimal(3600)
if hours <= 0:
raise ValueError(f"non-positive interval span at {read.start_local.isoformat()}")
return (read.energy_kwh / hours).quantize(FOUR_PLACES, rounding=ROUND_HALF_UP)
Carrying the span on the read rather than assuming a constant is what makes DST-affected days correct: the interval that contains the spring-forward transition may still be a nominal 15 minutes of wall clock, but a day rolled up from these reads will contain the right count of intervals because each one carries its own duration.
Step 2 — Determine the monthly peak, coincident or non-coincident
Non-coincident demand (NCD) is the single highest interval the customer produced during the billing month; it is the customer’s own worst moment regardless of what the rest of the system was doing. Coincident demand (CP) is the customer’s demand at the interval when the whole system peaked — a value supplied by the system operator, not discovered in the customer’s own series. Tariffs bill one, the other, or a blend, and the calculator must be explicit about which.
from collections.abc import Iterable
from enum import StrEnum
class DemandBasis(StrEnum):
NON_COINCIDENT = "ncd" # customer's own maximum interval
COINCIDENT = "cp" # customer's load at the system peak interval
@dataclass(frozen=True, slots=True)
class MonthlyPeak:
basis: DemandBasis
peak_kw: Decimal
occurred_local: datetime | None # None for a zero-demand month
intervals_seen: int
intervals_estimated: int
def monthly_peak(reads: Iterable[IntervalRead], basis: DemandBasis,
system_peak_start: datetime | None = None) -> MonthlyPeak:
"""Reduce a month of intervals to a single billed-basis peak in kW."""
best_kw = Decimal("0")
best_at: datetime | None = None
seen = estimated = 0
for read in reads:
seen += 1
estimated += int(read.estimated)
if basis is DemandBasis.COINCIDENT:
# Only the interval aligned to the system peak counts.
if system_peak_start is None:
raise ValueError("coincident basis requires a system_peak_start")
if read.start_local != system_peak_start:
continue
demand = interval_demand_kw(read)
if demand > best_kw:
best_kw, best_at = demand, read.start_local
return MonthlyPeak(basis, best_kw, best_at, seen, estimated)
For coincident billing the reduction narrows to a single aligned interval, so a missing read at that exact timestamp is not a smaller peak — it is an unresolvable one, which Step 5’s edge handling treats as an estimate rather than a silent zero.
Step 3 — Maintain a trailing-N-month peak history
The ratchet compares this month against the highest billed peak over a trailing window of prior months. That history must be an explicit, ordered structure keyed by billing month, so that “trailing 11 months” means exactly the eleven months before the current one and nothing drifts as cycles advance.
from collections import OrderedDict
class PeakHistory:
"""Ordered, immutable-by-convention record of prior monthly billed peaks."""
def __init__(self, window_months: int) -> None:
if window_months < 1:
raise ValueError("window_months must be >= 1")
self.window = window_months
self._peaks: "OrderedDict[str, Decimal]" = OrderedDict()
def record(self, billing_month: str, billed_kw: Decimal) -> None:
"""Append the billed peak for a closed month (YYYY-MM key)."""
self._peaks[billing_month] = billed_kw
self._peaks.move_to_end(billing_month)
def trailing_max(self, before_month: str) -> Decimal:
"""Highest billed peak in the window strictly before `before_month`."""
prior = [kw for m, kw in self._peaks.items() if m < before_month]
window = prior[-self.window:] # only the trailing N months
return max(window) if window else Decimal("0")
Recording the billed peak — not the raw metered peak — is deliberate: once a ratchet has lifted a month’s billed demand above its metered demand, that lifted value is what many tariffs carry forward, so the history must store what was billed. The focused rolling-window mechanics, including tie handling and partial history, are worked through in computing demand ratchets with rolling windows.
Step 4 — Apply the ratchet percentage to derive billed demand
Billed demand is the greater of the current month’s peak and the ratchet floor, where the floor is the ratchet percentage applied to the trailing maximum. Expressing the percentage as a Decimal fraction and quantizing the floor once keeps the comparison exact.
@dataclass(frozen=True, slots=True)
class BilledDemand:
billed_kw: Decimal
current_peak_kw: Decimal
ratchet_floor_kw: Decimal
floor_binding: bool # True when the ratchet, not this month, set the bill
def apply_ratchet(current: MonthlyPeak, history: PeakHistory,
billing_month: str, ratchet_pct: Decimal) -> BilledDemand:
"""Billed demand = max(current peak, ratchet_pct * trailing max)."""
trailing = history.trailing_max(billing_month)
floor = (ratchet_pct * trailing).quantize(FOUR_PLACES, rounding=ROUND_HALF_UP)
billed = max(current.peak_kw, floor)
return BilledDemand(
billed_kw=billed,
current_peak_kw=current.peak_kw,
ratchet_floor_kw=floor,
floor_binding=floor > current.peak_kw,
)
The floor_binding flag is small but important operationally: it is the difference between “the customer’s demand rose this month” and “the customer is paying against a peak set last summer,” and customer-service and audit both need to see which one drove the charge.
Step 5 — Compute the demand charge and hand off the billed peak
The charge itself is a single multiplication, quantized to cents at the money boundary. Billed demand is also returned so it can be rolled forward into the history (Step 3) once the month is closed and so downstream fee logic can act on it.
TWO_PLACES = Decimal("0.01")
def demand_charge(billed: BilledDemand, price_per_kw: Decimal) -> Decimal:
"""Price billed demand at the $/kW rate, quantized to cents."""
return (billed.billed_kw * price_per_kw).quantize(TWO_PLACES, rounding=ROUND_HALF_UP)
The resulting charge is one component of the invoice; per-kW riders and jurisdiction-specific assessments that ride on top of it are applied in surcharge and fee application logic, which expects billed demand as a settled Decimal rather than recomputing it.
Edge-Case Handling
The happy path is one max(); the cases that decide whether a demand charge is defensible are the awkward ones, and each has produced a wrong commercial bill in production.
- DST interval-length changes. On the spring-forward day the local timeline skips an hour and on the fall-back day it repeats one. Because each
IntervalReadcarries its ownspanand demand is derived from that span, the conversion in Step 1 stays correct, but any code that rolls intervals up by assuming “96 intervals per day” will over- or under-count on transition days. Count intervals, do not assume them, and resolve every boundary withzoneinfo. - Missing intervals within the peak window. A gap is not zero demand. For non-coincident billing a missing interval simply is not a candidate for the maximum, but if the actual peak fell in the gap the billed peak is understated; flag the month
ESTIMATEDwhen the gap coincides with the customer’s typical peak period rather than billing a confidently wrong lower number. For coincident billing a gap at the system-peak timestamp is unresolvable and must be estimated, never treated as a zero. Recovery of the underlying reads is the job of the ingestion and validation pipelines, not this calculator. - Ratchet reset and window roll-off. A ratchet floor is only as current as its window. When the month that set the trailing maximum ages past the window’s edge, the floor should drop — and if the code silently keeps a stale maximum, the customer keeps paying against a peak that has expired. The trailing window must be recomputed each cycle from the ordered history, never cached as a single running maximum.
- Partial-month peaks (move-in / move-out). A month that a commercial account only partly occupied still produces a peak, but billing full demand on a two-week occupancy can be inequitable and is often contractually prorated. Treat the partial month’s peak as real for the ratchet history only if the tariff says so; the proration of the charge itself belongs to proration and billing-period calculation.
- Zero-demand month. A vacant or seasonally idle account can produce a current peak of zero while the ratchet floor remains positive. This is correct behavior — the whole point of a ratchet is that the customer pays the floor even at zero usage — but the
floor_bindingflag and the audit record must make it explicit so it is not mistaken for a billing error.
Verification and Audit Trail
A demand charge that cannot show which interval, which basis, and which trailing window produced it is indefensible in front of a commercial customer disputing a five-figure line item. Every billed-demand determination should persist its inputs — the current peak and when it occurred, the trailing maximum and the months in the window, the ratchet percentage, and the floor_binding outcome — to append-only storage. This is the same discipline required by data governance and compliance, and it turns a contested charge into a reproducible one.
import hashlib
import json
from datetime import datetime, timezone
def demand_audit_record(billing_month: str, current: MonthlyPeak,
billed: BilledDemand, ratchet_pct: Decimal,
price_per_kw: Decimal) -> dict:
"""Persist every input behind a billed-demand determination."""
body = {
"billing_month": billing_month,
"basis": current.basis.value,
"current_peak_kw": str(billed.current_peak_kw),
"peak_occurred_local": current.occurred_local.isoformat()
if current.occurred_local else None,
"intervals_seen": current.intervals_seen,
"intervals_estimated": current.intervals_estimated,
"ratchet_floor_kw": str(billed.ratchet_floor_kw),
"ratchet_pct": str(ratchet_pct),
"billed_kw": str(billed.billed_kw),
"floor_binding": billed.floor_binding,
"demand_charge": str(demand_charge(billed, price_per_kw)),
"logged_utc": datetime.now(timezone.utc).isoformat(),
}
canonical = json.dumps(body, sort_keys=True)
body["hash"] = hashlib.sha3_256(canonical.encode("utf-8")).hexdigest()
return body
Correctness is confirmed two ways. First, replay determinism: re-rating the same interval series against the same rate context must reproduce the identical billed kW and charge, because any difference means a float leaked in or the window was resolved non-deterministically. Second, floor consistency: in any month where floor_binding is true, billed demand must equal the ratchet floor exactly, and the trailing maximum it was derived from must appear in the retained window — a check that catches a stale cache or an off-by-one window boundary before it reaches a ratepayer. Run both against historical commercial accounts before any change to demand or ratchet logic ships.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Demand charge far exceeds this month’s usage | Ratchet floor from a prior peak is binding, as designed | Confirm floor_binding and the trailing window; explain the ratchet to the customer rather than “correcting” a valid charge |
| Billed kW drifts on re-rate of the same month | float used for the kWh→kW division or the $/kW multiply |
Move all demand and price arithmetic to Decimal, quantize once at each boundary |
| Peak looks too low on a fall-back DST day | Day rolled up assuming a fixed interval count | Count intervals from the reads’ own span; resolve boundaries with zoneinfo, never a fixed 96/day |
| Coincident demand billed as zero | Missing read at the system-peak timestamp treated as zero | Flag the month ESTIMATED when the aligned interval is absent; never bill a zero coincident peak |
| Ratchet floor never decreases | Trailing maximum cached as a single running value | Recompute the trailing max each cycle from the ordered history so expired months roll off |
| Two identical-kWh customers billed very differently | Different peak shapes; demand, not energy, drives the charge | Expected behavior — verify each peak and basis; the flatter load correctly pays less demand |
| Partial-month account over-charged for demand | Full-month demand billed on a partial occupancy | Apply move-in/move-out proration to the charge and confirm the ratchet-history policy for partial months |
Frequently Asked Questions
What is the difference between a demand charge and an energy charge?
An energy charge prices total consumption — kilowatt-hours over the month. A demand charge prices the customer’s highest sustained rate of consumption — kilowatts over the demand interval — regardless of how many hours it lasted. Two customers can use the same energy while one draws it in a short, sharp spike and the other spreads it evenly; the spiky customer has a much higher demand and pays a larger demand charge, because the utility must size infrastructure for the peak, not the average.
What exactly is a demand ratchet?
A ratchet sets billed demand to the greater of the current month’s peak and a fixed percentage — often 75 to 90 percent — of the highest peak recorded over a trailing window of months, commonly eleven or twelve. Its purpose is to recover the cost of capacity the utility must keep available year-round for a customer whose peak is seasonal. A single summer spike therefore establishes a floor the customer pays against for the rest of the window even in low-usage months.
Should demand be computed with float or Decimal?
Always Decimal. Interval demand is metered energy divided by the interval’s duration, and demand charges are then a multiplication by a per-kilowatt price. Both operations accumulate IEEE-754 binary drift under float, and because demand charges are large, that drift becomes visible on the invoice and irreproducible on re-rate. Carry kW at four decimal places and quantize the final money figure to cents with ROUND_HALF_UP.
How do coincident and non-coincident demand differ in the calculation?
Non-coincident demand is the maximum over the customer’s own intervals — the customer’s single worst moment. Coincident demand is the customer’s load at the specific interval when the whole system peaked, a timestamp supplied by the system operator. The calculator must know which basis the tariff bills: non-coincident reduces over all intervals, while coincident selects the one aligned interval, which makes a missing read at that timestamp an estimate rather than a lower peak.
When does a missing interval change the billed demand?
For non-coincident billing a gap only matters if the true peak fell inside it; otherwise the maximum over the surviving intervals is still correct. For coincident billing a gap at the system-peak timestamp is decisive, because that one interval is the entire basis. In either case a gap that could suppress the real peak should flag the month as estimated rather than silently billing a lower, confidently wrong number.
Related Topics
- Automated Rate Calculation & Rule Engines — the parent guide this demand workflow plugs into after base energy is rated.
- Computing Demand Ratchets with Rolling Windows — the focused trailing-window function, with tie handling and partial-history behavior.
- Customer Class & Service-Tier Mapping — how the demand-billed rate schedule, interval length, and ratchet terms are resolved for an account.
- Surcharge & Fee Application Logic — the per-kW riders and assessments that ride on top of a settled demand charge.
- Meter Data Ingestion & Validation Pipelines — the source of the clean, gap-flagged interval reads this calculator consumes.
Up one level: Automated Rate Calculation & Rule Engines · Return to the utilitybilling.org home.