Skip to content

fix(mpc): self-consumption terminal SoC price = mean import, not spread - #299

Merged
frahlg merged 1 commit into
masterfrom
fix/mpc-self-consumption-terminal-price
May 25, 2026
Merged

fix(mpc): self-consumption terminal SoC price = mean import, not spread#299
frahlg merged 1 commit into
masterfrom
fix/mpc-self-consumption-terminal-price

Conversation

@frahlg

@frahlg frahlg commented May 25, 2026

Copy link
Copy Markdown
Member

Live regression — root cause

Active right now on homelab-rpi: 4 kWh of PV exported today while batteries sit at 7.5 % SoC. The planner picked "idle — export PV surplus" for every slot.

`selfConsumptionTerminalPrice` returned mean(import) − mean(export) (the arbitrage spread). On SE retail tariffs that's ~110 öre/kWh — half the value mpc/CLAUDE.md documents ("mean import price over horizon so the planner is SoC-neutral").

DP cost comparison per kWh of PV in slot 0

spread terminal (~110) mean-import terminal (~210)
charge (η × terminal) 0.95 × 110 = 104 öre 0.95 × 210 = 200 öre
export (spot) 98 öre 98 öre
margin to charge +6 öre +102 öre

Spread's 6 öre margin sits inside the DP's action-quantization noise — it picks export. Mean-import gives a solid +100 öre/kWh preference for charging from PV surplus.

Why this doesn't open the door to grid-charging

`mpc.go` line 583 already triples the cost of HOUSE-driven grid import for self-consumption mode. That includes implicit imports required for grid-charging:

  • PV-fed charge: cost 0, terminal credit +200 öre → strong win
  • Grid-fed charge (cheap night, spot 30, import 80): cost 60 öre (×3 penalty), terminal credit +50 öre → net −10 öre → DP declines

PV charging stays free and dominates; import charging stays uneconomic.

Tests

  • `TestSelfConsumptionTerminalPriceIsMeanImport` (replaces `IsImportMinusExport`) — assert mean(import) regardless of spot/bonus/fee
  • `TestSelfConsumptionTerminalPriceIgnoresExportRate` (replaces `ClampsToZero`) — even when spot > retail, value is mean import
  • `TestSelfConsumptionTerminalPriceEmpty` — unchanged
  • Full suite green: `go test ./...`

🤖 Generated with Claude Code

Live regression (2026-05-25): planner picked "idle — export PV
surplus" for every slot, leaving the fleet at 7.5 % SoC while
exporting 4+ kWh of free PV mid-day. selfConsumptionTerminalPrice
was returning mean(import) − mean(export) — the arbitrage spread.
On realistic SE tariffs (mean import 210 öre/kWh, mean export 100
öre/kWh) that's ~110 öre/kWh, half the documented "mean import" value.

DP cost comparison per kWh of PV in slot 0 with spread terminal:
  charge:  +η × terminal = 0.95 × 110 = 104 öre  (value of stored SoC)
  export:  +spot        = 98 öre                  (immediate revenue)

Win for charge is ~6 öre — well inside the DP's action-quantization
noise, so it picks export. With mean-import terminal (~210 öre):

  charge:  +η × terminal = 0.95 × 210 = 200 öre
  export:  +spot         = 98 öre

Solid +100 öre win for charge per kWh. Operator's "fill the
battery when surplus exists" intuition restored.

Grid-charging is not encouraged by the bigger terminal credit
because mpc.go's strict self-consumption bias (line 583) triples
the cost of HOUSE-driven grid import, including the implicit
import required to grid-charge. PV-fed charging stays free and
dominates; import-fed charging stays uneconomic.

mpc/CLAUDE.md already documented the intended formula as "mean
import price over horizon so the planner is SoC-neutral" — this
fix aligns the implementation with the docs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@frahlg
frahlg merged commit 65a8f43 into master May 25, 2026
1 check passed
@frahlg
frahlg deleted the fix/mpc-self-consumption-terminal-price branch May 25, 2026 06:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant