Hybrid dispatcher: greedy self-consumption + per-day grid arbitrage
Single algorithm with two passes:
Pass 1 — greedy self-consumption (real-firmware default):
For each hour, charge any surplus, discharge into any demand.
This nails sunny days: battery fills from morning surplus, exports
only after capacity is reached, drains during evening peak.
Pass 2 — daily price-spread arbitrage on the residual capacity:
For each UTC day, repeatedly find the most profitable
cheap-charge → expensive-discharge pair (positive after round-trip
efficiency), execute it, recompute SoC trajectory, repeat until no
profitable cycle remains. Discharge must fit within hourly demand
for plug-in batteries (no grid push). Sees only that day's prices,
matching what a Tibber/Frank-style smart-charging controller does
with day-ahead price visibility.
Effect:
- Sunny weeks (June 17-19): unchanged — greedy fill + evening discharge.
- No-sun weeks (Feb 7+): now generates arbitrage savings instead of €0.
- Mixed weeks: greedy handles surplus, arbitrage handles the rest.
Updated: test_grid_arbitrage_kicks_in_on_no_sun_days replaces the prior
"greedy doesn't arbitrage" test; now verifies the dispatcher charges
cheap hours and discharges expensive ones when no PV is available.
This commit is contained in:
parent
6f8238376b
commit
9e77b35ff4
2 changed files with 121 additions and 33 deletions
|
|
@ -47,10 +47,10 @@ def test_plugin_never_exports():
|
|||
assert (out["grid_kwh_with_battery"] >= -1e-9).all()
|
||||
|
||||
|
||||
def test_greedy_does_not_grid_arbitrage_without_pv():
|
||||
"""Greedy dispatch is self-consumption only — it won't import cheap and
|
||||
discharge expensive without a surplus source. (LP would; greedy by
|
||||
design doesn't, matching real plug-in firmware.)"""
|
||||
def test_grid_arbitrage_kicks_in_on_no_sun_days():
|
||||
"""Without PV but with a daily price spread, the dispatcher should
|
||||
charge during the cheapest hours and discharge during the most
|
||||
expensive — same dynamic-tariff behaviour Tibber-style controllers do."""
|
||||
prices = [0.05] * 12 + [0.50] * 12
|
||||
demands = [1.0] * 24
|
||||
df = make_df(prices, demands)
|
||||
|
|
@ -58,8 +58,12 @@ def test_greedy_does_not_grid_arbitrage_without_pv():
|
|||
round_trip_eff=0.9, allows_export=False)
|
||||
schedule = oracle_daily_schedule(df, bat)
|
||||
out = simulate(df, bat, schedule)
|
||||
assert out["charge_kwh"].sum() == 0
|
||||
assert out["discharge_kwh"].sum() == 0
|
||||
# Charging happened during the cheap morning hours (0..11)
|
||||
assert out["charge_kwh"].iloc[:12].sum() > 0
|
||||
# Discharging happened during the expensive afternoon (12..23)
|
||||
assert out["discharge_kwh"].iloc[12:].sum() > 0
|
||||
# Arbitrage produced some saving
|
||||
assert out["savings"].sum() > 0
|
||||
|
||||
|
||||
def test_greedy_fills_from_surplus_then_overflows():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue