Specificatie van het rekenmodel
De volledige rekenspecificatie: welke formules de terugverdientijd bepalen, welke scenario's we doorrekenen en welke drempels tot kopen, wachten of niet kopen leiden.
Dit is het onderzoeksdocument zoals het is vastgelegd, ongewijzigd overgenomen uit docs/batterijscout-calculator-spec-v2.md. Het is in het Engels geschreven.
Authoritative for the terugverdientijd-engine. Supersedes v1. v2 changes: Regime-A saldering annual-balance model (v1 treated all export as fully salderable — wrong for overproducers) · intra-month capture factor (v1 monthly sums overstated small-battery utilization) · scenarios renamed conservative/base/favourable with per-variable direction table (fixes monotonicity ambiguity) · trading uplift defined net (gross − revenue share − cycling cost − subscription), cap clarified · year-by-year degradation simulation replaces flat 0.90 multiplier · economic sizing replaces median-export matching (guardrail retained) · payback-vs-warranty honesty flag · headline = first-year simple payback + separate 10-yr cumulative net benefit (inflation removed from headline) · roof-orientation input · EV/heat-pump behavioural qualifiers (fixes double counting) · golden tests hardened + G11–G15.
0. Principles (unchanged from v1)
Calculate, never presume · every constant has source + date + VERIFY flag; CI blocks production recommendations while any required constant is unverified · versioned assumptionSet + engine version on every result and PDF · ranges with named drivers, not false precision.
1. Inputs (v1 table plus/amended)
- roofOrientation: zuid / oost-west / noord / weet_niet (default oost-west). Adjusts pvFactor profile shape and effective yield (zuid ×1.0, oost-west ×0.85 yield but flatter daily curve → selfUse +0.03, noord ×0.6 with warning; VERIFY factors vs PVGIS).
- hasEV + evChargingPattern: overdag_thuis / avond / gemengd / weet_niet. Self-use bonus (+0.05) ONLY when overdag_thuis.
- hasHeatPump + type hybride/all_electric: increases winter consFactor weighting; NO summer self-use bonus (v1's flat +0.03 removed — heat pumps barely run when PV peaks).
- hasElectricBoiler + boilerSolarControlled ja/nee/weet_niet: self-use bonus only if ja.
- Consumption defaults, contract, export compensation, feed-in structure, phase, budget, goal, ownership: as v1. "Weet ik niet" first-class everywhere; every default disclosed with its effect.
2. Scenarios (replaces v1 L/B/H)
Three named scenarios; each variable set in the battery-favourability direction, guaranteeing payback(conservative) ≥ payback(base) ≥ payback(favourable):
| variable | conservative | base | favourable | VERIFY source |
|---|---|---|---|---|
| importPriceCt | 25 | 30 | 38 | CBS/ANWB |
| exportCompCtPost2027 | 8 | 4 | 2 | supplier sheets |
| feedInChargeCtPerKwh avoided | 0 | 2 | 4 | supplier terugleverkosten |
| captureFactor (see §3.4) | 0.80 | 0.88 | 0.95 | profile study |
| degradationCurve | to 75%@yr10 | to 80%@yr10 | to 85%@yr10 | warranties |
| installatieKosten (when band) | max | mid | min | offers |
| Headline payback = base, range = [conservative, favourable]. No inflation in headline (simple first-year payback). 10-yr cumulative net benefit (§3.7) uses year-by-year simulation, still undiscounted, methodology page says so. |
3. Energy & value model
3.1 Monthly profiles
pvFactor[12] per orientation (zuid peaked, oost-west flatter; VERIFY PVGIS NL), consFactor[12] per household config (heat-pump variant winter-weighted). Σ=1 each.
3.2 Direct self-use & evening demand
s0 by profile (thuis_overdag .40 / gemengd .30 / werkend_buitenshuis .22) + qualified bonuses per §1 (max cumulative bonus +0.10). eveningShare e as v1.
3.3 Monthly flows (per battery b, per year y with capacity(y) from degradation curve)
pvM = pvProduction × orientationYield × pvFactor[m]
consM = annualConsumption × consFactor[m]
exportM = max(0, pvM × (1 − s0))
deficitM = max(0, consM × e − pvM × 0.05)
theoreticalChargeM = min(exportM, usable(y) × daysM, maxChargeKw × window × daysM)
chargeM = theoreticalChargeM × captureFactor # §3.4
dischargeM= min(chargeM × rte, deficitM)
shiftedM = dischargeM ; chargeActualM = dischargeM / rte
3.4 Capture factor
Accounts for intra-month solar volatility (monthly sums assume surplus arrives conveniently; real surplus clusters on sunny days a small battery can't fully absorb). Scenario-set scalar in v1 of the engine (see §2); flagged for calibration against representative-day profiles in engine v2. Methodology page discloses this explicitly.
3.5 Regime A — saldering annual balance (until 31-12-2026) [v2 correction]
importBefore = max(0, annualConsumption − pvProduction × s0_effective) # grid imports pre-battery
exportBefore = Σ exportM
salderable = min(exportBefore, importBefore)
nonSalderable= exportBefore − salderable
Battery charging is allocated first from nonSalderable export (marginal value of shifting = importPrice − exportComp, genuinely positive), then from salderable export (marginal value ≈ efficiency loss only, negative). Overproducing households (export > import) can therefore see positive pre-2027 value; balanced households see ~none. The engine computes this allocation; copy explains both cases honestly.
3.6 Regime B — post-2027 (per year y)
valueYr(y) = Σm [ shiftedM×importPriceCt − chargeActualM×exportCompCt + chargeActualM×feedInChargeCt ] − emsSubscriptionCtYr − netTradingUplift excluded (shown separately)
3.7 Payback & cumulative benefit
- Headline: paybackSimple = netCost / valueYr(y=1, base scenario)
- Cumulative: simulate y=1..10 with capacity(y); tenYearNetBenefit = Σ valueYr(y) − netCost; cumulative payback year = first y where Σ ≥ netCost
- netCost per offer prijsType; quote_required → [min,max] band widens the range
- Warranty flag [v2]: effectiveWarrantyYears = min(garantieJaren, garantieThroughputKwh / avgAnnualThroughput, garantieCycli / avgAnnualCycles). If paybackBase > effectiveWarrantyYears → mandatory result banner: "De geschatte terugverdientijd overschrijdt de garantieperiode." Ratio payback/effectiveWarranty ≤0.7 strong / 0.7–1.0 borderline / >1.0 weak feeds recommendation copy.
4. Dynamic trading uplift [v2 definition]
netTradingUpliftYr = grossTradingEurPerUsableKwhYr × usableKwh × (1 − aggregatorRevenueSharePct)
− incrementalCycleDegradationCt − emsDeltaCt
Defaults (VERIFY, aggregator publications are interested sources — label confidence low): gross 0/20/35 €/kWh-usable/yr (conservative/base/favourable), revenue share 30%, cap on NET uplift = €40 × usableKwh per year total. Displayed always as separate line, never in payback. R4 (below) uses net, favourable-capped-at-base uplift and requires battery×aggregator×contract compatibility all verified.
5. Recommendation decision tree (v2)
Primary scenario = Regime B base. Regime A shown transparently (incl. §3.5 overproducer case).
R0 huur/no-fixed-install → plug-in track
R1 pv==0 & goal!=backup:
wants smart-charging/trading path & compatible setup → DYNAMISCH_CONTRACT_EERST(trading-motivatie, low confidence)
else → NIET_KOPEN
R2 goal==backup → backup-capable shortlist, payback labeled secondary → KOOP_NU(backup)
R3 paybackBase ≤ 7 AND payback ≤ 0.7×effectiveWarranty → KOOP_NU / KOOP_PLUG_IN per track
R4 7 < paybackBase ≤ 10:
not on dynamisch AND netTradingUplift (§4) would bring ≤7 AND compatibility verified → DYNAMISCH_CONTRACT_EERST
else → WACHT_randgeval: both paths shown, email CTA "Mail mij een seintje op 1 januari 2027 met herberekende tarieven"
R5 10 < paybackBase ≤ 12 → WACHT (same email CTA)
R6 paybackBase > 12 OR payback > effectiveWarranty×1.3 OR valueYr ≤ 0 (G13) → NIET_KOPEN(in jouw situatie) — alternatives explained, no battery CTA
6. Battery shortlist — economic ranking [v2]
For every compatible published+verified battery (hard filters: phase, track, budget): run the full engine → rank by tenYearNetBenefit (tiebreak: paybackBase, then warranty throughput). Guardrails: exclude usable > 2× median Apr–Sep daily surplus ("te groot gekocht" flag) and G12 diminishing-utilization check. Shortlist 2–4. Offer fallback chain per PRD §6 so a missing affiliate link never breaks the flow.
7. Outputs, confidence, reproducibility
As v1 §6–7 plus: scenario table uses conservative/base/favourable columns; warranty flag; trading line separate; Regime A/B both shown; result stores full intermediate vector (charged, shifted, residual export, salderable split, valueYr per year) — required for golden tests and PDFs.
8. Golden tests
Frozen per case after assumption verification: annual charged/shifted/residual export, salderable split, valueYr(1), tenYearNetBenefit, paybackBase, range, recommendation, confidence, estimated-input list, shortlist slugs (or empty), top sensitivity drivers. G1–G10 as v1 (re-derived under v2 model), plus:
- G11 saldering cap: import 2000, export 4000 → assert nonSalderable=2000 and positive Regime-A value from that tranche only
- G12 oversize: same household, 5 vs 15 kWh → assert per-kWh utilization declines and 15 kWh not shortlisted when tenYearNetBenefit lower
- G13 negative value: favourable-to-export world (exportComp high, import low, subscription) → valueYr ≤ 0 → "geen terugverdientijd" state, R6, no nonsense numbers
- G14 quote band: installed min/max only → payback shown as band, widened range asserted
- G15 stale/unverified battery: best-ranked but unverified → excluded from shortlist, admin warning logged
9. Engineering constraints
As v1 §9, plus: engine simulates years 1–10 (still trivially fast); intermediate vector exposed for tests; captureFactor and all §2 scenario values live in the single assumption config; dedup key for click logs is a daily-rotating HMAC (no raw IP), retention 30 days.