Summary
The rolling-horizon DP re-optimizes only through the end of the current day (horizon == remaining_today), so _calculate_terminal_value()'s "today only" fallback (battery_system_manager.py:1704-1754) fires on every hourly re-optimization, not just an edge case. That fallback estimates the value of a kWh still in the battery at midnight using the median buy price:
median_price = statistics.median(buy_prices)
terminal_value = median_price * efficiency_discharge - cycle_cost_per_kwh
On contracts where buy and sell diverge a lot (e.g. Belgian Luminus Dynamic: buy ~€0.30-0.38/kWh, sell ~€0.10-0.16/kWh), this produces a terminal value (~€0.245/kWh in the reported case) well above any real, achievable evening export price. Backward induction then prefers holding the battery to "collect" this fictitious bonus at midnight over exporting at the real, known, better price available right now — every single re-optimization, all day.
Evidence (from #126 — Frank Leysen, 2026-07-07 14:30 debug export, v9.9.0b9)
- Optimization at period 58 (14:30),
horizon=38 = exactly remaining_today → fallback branch active.
- Remaining buy prices for the day ranged ~€0.21-0.38/kWh; median ≈ €0.30 →
terminal_value ≈ 0.30 × 0.95 - 0.04 ≈ €0.245/kWh.
- Actual achievable export price (Luminus formula) at 20:00-22:00 was only ~€0.13-0.16/kWh.
- Resulting predicted schedule: battery sits at 14.9-15.0 kWh (IDLE) through 20:00-20:45 despite rising evening sell price, then only trickles out -0.15 kWh/period afterward (matches home load, barely crosses into export) instead of discharging meaningfully into the evening peak.
This is distinct from #234 (cost-basis threading), #240 (load-first export miscrediting, already fixed in b9), and #236 (DP perf). It reproduces on every re-optimization for any user with a meaningfully asymmetric buy/sell contract, not just as a one-off.
Suggested direction
_calculate_terminal_value only has access to buy_prices today; the caller (battery_system_manager.py:1871-1873) already computes sell_prices in the same scope. Base the "today only" terminal estimate on the known, real value achievable via export (sell price) rather than an optimistic "you'll definitely avoid a purchase tomorrow" assumption — consistent with how _compute_reward already treats discharge value elsewhere in the DP (max(avoid_purchase_value, export_value), but grounded in what's actually knowable at the horizon boundary).
Filed per investigation in #126 (see comments there for full debug-log analysis).
Summary
The rolling-horizon DP re-optimizes only through the end of the current day (
horizon == remaining_today), so_calculate_terminal_value()'s "today only" fallback (battery_system_manager.py:1704-1754) fires on every hourly re-optimization, not just an edge case. That fallback estimates the value of a kWh still in the battery at midnight using the median buy price:On contracts where buy and sell diverge a lot (e.g. Belgian Luminus Dynamic: buy ~€0.30-0.38/kWh, sell ~€0.10-0.16/kWh), this produces a terminal value (~€0.245/kWh in the reported case) well above any real, achievable evening export price. Backward induction then prefers holding the battery to "collect" this fictitious bonus at midnight over exporting at the real, known, better price available right now — every single re-optimization, all day.
Evidence (from #126 — Frank Leysen, 2026-07-07 14:30 debug export, v9.9.0b9)
horizon=38= exactlyremaining_today→ fallback branch active.terminal_value ≈ 0.30 × 0.95 - 0.04 ≈ €0.245/kWh.This is distinct from #234 (cost-basis threading), #240 (load-first export miscrediting, already fixed in b9), and #236 (DP perf). It reproduces on every re-optimization for any user with a meaningfully asymmetric buy/sell contract, not just as a one-off.
Suggested direction
_calculate_terminal_valueonly has access tobuy_pricestoday; the caller (battery_system_manager.py:1871-1873) already computessell_pricesin the same scope. Base the "today only" terminal estimate on the known, real value achievable via export (sell price) rather than an optimistic "you'll definitely avoid a purchase tomorrow" assumption — consistent with how_compute_rewardalready treats discharge value elsewhere in the DP (max(avoid_purchase_value, export_value), but grounded in what's actually knowable at the horizon boundary).Filed per investigation in #126 (see comments there for full debug-log analysis).