You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Once tomorrow's real day-ahead prices enter the optimization horizon, the DP can hold battery charge back from tonight's known, better sell price and export it instead at tomorrow's known, worse sell price. Confirmed with synthetic testing built from a real user's (Frank, #126) exact config, today's real prices, real Solcast tomorrow-solar forecast, and his real consumption pattern — varying only tomorrow's price array.
Belpex/ENTSO-e day-ahead prices are published values once available, not forecasts. Choosing a lower known price over a higher known price is a quantifiable revenue loss, not a legitimate risk/uncertainty tradeoff.
Background
This traces from the "74% SOC at midnight" thread in #126 (see this comment onward). Two earlier theories were raised there and both were checked against real debug bundles and ruled out:
Buy/sell price asymmetry ("DP holds charge because avoiding tomorrow's import beats tonight's export") — ruled out: Frank's actual 07:00 plan on 11 Jul already showed correct drain-to-floor behavior for tonight's peak, using the same asymmetric prices that would apply at any time of day. The asymmetry alone doesn't explain the later drift.
Terminal-value placeholder overestimating future value — already fixed by fix: cap DP terminal-value estimate at best in-horizon export price (replaces #245) #251 (merged 2026-07-09, present in Frank's v9.9.0b11) for the no-tomorrow-data branch of _calculate_terminal_value(). That fix caps the placeholder at the best in-horizon sell price. It does not touch the branch used once tomorrow's real prices are present, where terminal value is forced to 0.0 and the DP directly optimizes the real 2-day horizon (core/bess/battery_system_manager.py:1716-1788).
This issue documents synthetic testing that isolates a real defect in that second, previously-untested branch.
Reproduction
Built via mock-run.sh against a scenario generated from Frank's real debug bundle (attached to #126, exported 2026-07-12 06:07):
.venv/bin/python scripts/mock_ha/scenarios/from_debug_log.py bess-debug-2026-07-12-060741.md
# → scripts/mock_ha/scenarios/bess-debug-060741.json# (verbatim entity snapshot: real config, real today prices, real Solcast# tomorrow-solar forecast, real historical consumption)
Two edits to the generated scenario, applied per test:
sensors["sensor.belpex_h_average_electricity_price"].attributes.prices_tomorrow — populated with a synthetic 24-hour array (varied per test below; empty in the original export since Belpex hadn't published yet at 06:07).
bess_config.home.consumption_strategy → "fixed" with default_hourly set to Frank's real historical average (0.517 kWh/h, computed from the bundle's own historical_periods). Required because mock-HA doesn't implement the recorder/list_statistic_ids WS command Frank's real ha_statistics strategy depends on — without this the schedule build fails outright (Failed to update battery schedule: WS command recorder/list_statistic_ids failed).
mock_time set to @2026-07-12 06:10:00 (Europe/Brussels) so the very first optimization cycle already sees the injected tomorrow array. Then:
Exported at tomorrow's worse peak (periods 180–191, drains to 7.1 there)
Flat — tomorrow flat 0.02 EUR/kWh all day, no peak at all
no opportunity at all
9.6 kWh
Consumed locally overnight via LOAD_SUPPORT (avoids ~0.32 EUR/kWh grid import)
Analysis
The Worse scenario is the smoking gun. With tomorrow's price known (not forecast) to be ~21% below tonight's, a revenue-maximizing optimizer with full information should front-load the export onto tonight's better price — there's no uncertainty being hedged, both numbers are already fixed day-ahead auction results. Instead the DP holds ~5.3 kWh back from tonight (12.4 kWh vs. the 7.1 kWh floor-baseline) and sells it at tomorrow's confirmed-worse peak. That's a direct, quantifiable loss: roughly 5.3 kWh × (0.147 − 0.114) ≈ €0.17 in this dulled-by-21% test alone, and the gap scales with how much worse tomorrow's peak is — Belpex/day-ahead prices routinely vary by far more than 21% day to day, and Frank's original real-world observation (07:00 plan correctly draining to floor → midnight actual still ~74% SOC) is consistent with a much larger version of this same pattern.
The Flat scenario is not the same bug. There, the ~2.5 kWh held above the floor is spent overnight via LOAD_SUPPORT against Frank's real (tiny) consumption load, avoiding grid import at ~0.32 EUR/kWh — that's a legitimate self-consumption call (avoided-import cost beats a near-zero sell price) and plausibly reflects the extended 2-day horizon correctly provisioning for a second night's early-morning low-solar consumption that a 1-day horizon can't see at all. This part is not obviously wrong and is scoped separately from the export-to-worse-future-price defect above.
What's not yet known
The exact code-level cause inside the DP's per-period reward/value computation (core/bess/dp_battery_algorithm.py, _compute_reward at line 233, battery_wear_cost = energy_stored * cycle_cost_per_kwh at lines 309/349/434) for why it doesn't correctly rank a higher known near price above a lower known far price once the horizon spans two peaks. cycle_cost_per_kwh (0.035) is in the right order of magnitude to be interacting with the price differential in the Worse scenario, but this hasn't been traced through the actual DP state transitions yet.
Whether this is a discretization/state-space artifact (e.g. period granularity or battery-power-rate modeling) or a genuine bug in how marginal value is compared across non-adjacent periods.
Whether the fix belongs in the DP's reward function, in how the extended real-forecast horizon is constructed, or in a discount applied to farther-horizon periods so nearer certain revenue is preferred when peaks are comparable.
Next steps
Trace _compute_reward / the backward-induction value comparison in dp_battery_algorithm.py for the Worse-scenario inputs to find why a period at 21:00 today (price 0.145) doesn't dominate a period at 21:00 tomorrow (price 0.114) net of cycle_cost_per_kwh, then propose a targeted fix.
The three synthetic scenario JSONs used here (frank-126-synthetic-tomorrow{,-worse,-flat}.json, all derived from Frank's original export-debug-data bundle) are available to attach/commit as regression fixtures if useful for a follow-up PR.
Summary
Once tomorrow's real day-ahead prices enter the optimization horizon, the DP can hold battery charge back from tonight's known, better sell price and export it instead at tomorrow's known, worse sell price. Confirmed with synthetic testing built from a real user's (Frank, #126) exact config, today's real prices, real Solcast tomorrow-solar forecast, and his real consumption pattern — varying only tomorrow's price array.
Belpex/ENTSO-e day-ahead prices are published values once available, not forecasts. Choosing a lower known price over a higher known price is a quantifiable revenue loss, not a legitimate risk/uncertainty tradeoff.
Background
This traces from the "74% SOC at midnight" thread in #126 (see this comment onward). Two earlier theories were raised there and both were checked against real debug bundles and ruled out:
_calculate_terminal_value(). That fix caps the placeholder at the best in-horizon sell price. It does not touch the branch used once tomorrow's real prices are present, where terminal value is forced to0.0and the DP directly optimizes the real 2-day horizon (core/bess/battery_system_manager.py:1716-1788).This issue documents synthetic testing that isolates a real defect in that second, previously-untested branch.
Reproduction
Built via
mock-run.shagainst a scenario generated from Frank's real debug bundle (attached to #126, exported 2026-07-12 06:07):Two edits to the generated scenario, applied per test:
sensors["sensor.belpex_h_average_electricity_price"].attributes.prices_tomorrow— populated with a synthetic 24-hour array (varied per test below; empty in the original export since Belpex hadn't published yet at 06:07).bess_config.home.consumption_strategy→"fixed"withdefault_hourlyset to Frank's real historical average (0.517 kWh/h, computed from the bundle's ownhistorical_periods). Required because mock-HA doesn't implement therecorder/list_statistic_idsWS command Frank's realha_statisticsstrategy depends on — without this the schedule build fails outright (Failed to update battery schedule: WS command recorder/list_statistic_ids failed).mock_timeset to@2026-07-12 06:10:00(Europe/Brussels) so the very first optimization cycle already sees the injected tomorrow array. Then:...and read the
### Period Decisionstable for periods 84–95 (21:00–23:45 tonight) and 180–191 (21:00–23:45 tomorrow).Battery: 15 kWh capacity, 5 kW charge/discharge,
min_soc=47%→min_soe_kwh=7.05. Today's real evening peak: sell ≈0.36–0.37 (buy-price column shown;export_spot_multiplier=1.018, no export markup) around 21:00–23:00.Results
#251's fix)LOAD_SUPPORT(avoids ~0.32 EUR/kWh grid import)Analysis
The Worse scenario is the smoking gun. With tomorrow's price known (not forecast) to be ~21% below tonight's, a revenue-maximizing optimizer with full information should front-load the export onto tonight's better price — there's no uncertainty being hedged, both numbers are already fixed day-ahead auction results. Instead the DP holds ~5.3 kWh back from tonight (12.4 kWh vs. the 7.1 kWh floor-baseline) and sells it at tomorrow's confirmed-worse peak. That's a direct, quantifiable loss: roughly 5.3 kWh × (0.147 − 0.114) ≈ €0.17 in this dulled-by-21% test alone, and the gap scales with how much worse tomorrow's peak is — Belpex/day-ahead prices routinely vary by far more than 21% day to day, and Frank's original real-world observation (07:00 plan correctly draining to floor → midnight actual still ~74% SOC) is consistent with a much larger version of this same pattern.
The Flat scenario is not the same bug. There, the ~2.5 kWh held above the floor is spent overnight via
LOAD_SUPPORTagainst Frank's real (tiny) consumption load, avoiding grid import at ~0.32 EUR/kWh — that's a legitimate self-consumption call (avoided-import cost beats a near-zero sell price) and plausibly reflects the extended 2-day horizon correctly provisioning for a second night's early-morning low-solar consumption that a 1-day horizon can't see at all. This part is not obviously wrong and is scoped separately from the export-to-worse-future-price defect above.What's not yet known
core/bess/dp_battery_algorithm.py,_compute_rewardat line 233,battery_wear_cost = energy_stored * cycle_cost_per_kwhat lines 309/349/434) for why it doesn't correctly rank a higher known near price above a lower known far price once the horizon spans two peaks.cycle_cost_per_kwh(0.035) is in the right order of magnitude to be interacting with the price differential in the Worse scenario, but this hasn't been traced through the actual DP state transitions yet.Next steps
Trace
_compute_reward/ the backward-induction value comparison indp_battery_algorithm.pyfor the Worse-scenario inputs to find why a period at 21:00 today (price 0.145) doesn't dominate a period at 21:00 tomorrow (price 0.114) net ofcycle_cost_per_kwh, then propose a targeted fix.The three synthetic scenario JSONs used here (
frank-126-synthetic-tomorrow{,-worse,-flat}.json, all derived from Frank's original export-debug-data bundle) are available to attach/commit as regression fixtures if useful for a follow-up PR.