Batterien machen nicht nur Preisarbitrage. Sie können auch Netzregelleistung bereitstellen — und die Erlöse sind oft höher und stabiler als reine Spot-Arbitrage. Diese Lektion behandelt die drei wichtigsten Frequenzregelungsprodukte und wie man sie in den Optimierer integriert.
Die Netzfrequenz muss bei 50,000 Hz bleiben (in Europa). Wenn Erzeugung ≠ Verbrauch, weicht die Frequenz ab. ÜNBs (Übertragungsnetzbetreiber) beschaffen Reservekapazität von Anlagen wie Batterien um das Gleichgewicht wiederherzustellen. Du wirst dafür bezahlt, Kapazität bereitzuhalten — selbst wenn du nie tatsächlich Energie lieferst.
Um symmetrische FCR (±X MW) bereitzustellen, muss deine Batterie jederzeit sowohl erhöhen als auch verringern können. Das bedeutet, den SoC nahe 50% zu halten — wenn sie leer ist, kann sie nicht entladen; wenn voll, nicht laden.
# FCR-Reservierung: ±5 MW für einen 4-Stunden-Block
# Batterie muss genug Spielraum in beide Richtungen halten
fcr_leistung = 5.0 # MW für FCR reserviert
fcr_dauer = 0.25 # Stunden (15 Min. Halteanforderung)
fcr_energie = fcr_leistung * fcr_dauer # 1,25 MWh Spielraum je Richtung
# SoC muss während FCR-Bereitstellung in diesen Grenzen bleiben:
# SoC ≥ SoC_min + fcr_energie (kann noch entladen wenn abgerufen)
# SoC ≤ SoC_max - fcr_energie (kann noch laden wenn abgerufen)
Die Schlüsselerkenntnis: Du kannst FCR bereitstellen und gleichzeitig Arbitrage betreiben, solange du die Kapazitätsreservierung respektierst. Der Optimierer entscheidet, wie er die Batterieleistung zwischen den Märkten aufteilt:
"""
co_optimierung.py — Gemeinsame Day-Ahead-Arbitrage + FCR-Gebot
"""
import pyomo.environ as pyo
import numpy as np
T = 24
preise = [28, 25, 22, 20, 19, 21, 35, 55,
62, 58, 52, 48, 45, 44, 47, 53,
68, 85, 92, 78, 60, 45, 35, 30]
# FCR-Auktionsergebnisse: Kapazitätspreis pro 4-Stunden-Block (€/MW/h)
# Blöcke: 0-3, 4-7, 8-11, 12-15, 16-19, 20-23
fcr_kapazitaetspreise = [3.5, 4.0, 5.5, 6.0, 12.0, 4.5] # €/MW/h
P_max = 10.0
E_max = 20.0
eta = 0.90
eta_sqrt = np.sqrt(eta)
soc_init = 10.0
SoC_min = 0.1 * E_max
SoC_max = 0.9 * E_max
deg_kosten = 5.0
fcr_dauer = 0.25 # 15-Min. Halten
model = pyo.ConcreteModel()
model.T = pyo.RangeSet(0, T - 1)
model.B = pyo.RangeSet(0, 5) # 6 FCR-Blöcke
# Entscheidungsvariablen
model.p_laden = pyo.Var(model.T, bounds=(0, None))
model.p_entladen = pyo.Var(model.T, bounds=(0, None))
model.soc = pyo.Var(model.T, bounds=(SoC_min, SoC_max))
model.y = pyo.Var(model.T, within=pyo.Binary)
model.fcr = pyo.Var(model.B, bounds=(0, P_max)) # MW für FCR pro Block
# Stunden zu FCR-Blöcken zuordnen
def block_von(t):
return t // 4
# Leistungsgrenzen: Arbitrage-Leistung + FCR-Reservierung ≤ P_max
def leistungsgrenze_laden(m, t):
b = block_von(t)
return m.p_laden[t] + m.fcr[b] <= P_max
def leistungsgrenze_entladen(m, t):
b = block_von(t)
return m.p_entladen[t] + m.fcr[b] <= P_max
model.lg_l = pyo.Constraint(model.T, rule=leistungsgrenze_laden)
model.lg_e = pyo.Constraint(model.T, rule=leistungsgrenze_entladen)
# SoC muss Spielraum für FCR-Aktivierung haben
def soc_fcr_unten(m, t):
b = block_von(t)
return m.soc[t] >= SoC_min + m.fcr[b] * fcr_dauer
def soc_fcr_oben(m, t):
b = block_von(t)
return m.soc[t] <= SoC_max - m.fcr[b] * fcr_dauer
model.soc_fcr_u = pyo.Constraint(model.T, rule=soc_fcr_unten)
model.soc_fcr_o = pyo.Constraint(model.T, rule=soc_fcr_oben)
# SoC-Bilanz
def soc_regel(m, t):
vorher = soc_init if t == 0 else m.soc[t-1]
return m.soc[t] == vorher + eta_sqrt * m.p_laden[t] - m.p_entladen[t] / eta_sqrt
model.soc_bil = pyo.Constraint(model.T, rule=soc_regel)
# Kein gleichzeitiges Laden/Entladen
def kl(m, t): return m.p_laden[t] <= P_max * (1 - m.y[t])
def ke(m, t): return m.p_entladen[t] <= P_max * m.y[t]
model.kl = pyo.Constraint(model.T, rule=kl)
model.ke = pyo.Constraint(model.T, rule=ke)
# Zielfunktion: Arbitrage-Erlös + FCR-Kapazitätserlös - Degradation
def ziel_regel(m):
arb_erloes = sum(preise[t] * (m.p_entladen[t] - m.p_laden[t]) for t in m.T)
fcr_erloes = sum(m.fcr[b] * fcr_kapazitaetspreise[b] * 4 for b in m.B)
degradation = sum(deg_kosten * (m.p_laden[t] + m.p_entladen[t]) for t in m.T)
return arb_erloes + fcr_erloes - degradation
model.ziel = pyo.Objective(rule=ziel_regel, sense=pyo.maximize)
# Lösen
solver = pyo.SolverFactory('highs')
ergebnis = solver.solve(model, tee=False)
print(f"Gesamterlös: €{pyo.value(model.ziel):.2f}")
print(f" Arbitrage: €{sum(preise[t] * (pyo.value(model.p_entladen[t]) - pyo.value(model.p_laden[t])) for t in range(T)):.2f}")
print(f" FCR: €{sum(pyo.value(model.fcr[b]) * fcr_kapazitaetspreise[b] * 4 for b in range(6)):.2f}")
print(f"\nFCR-Reservierungen pro Block:")
for b in range(6):
print(f" Block {b} (Stunden {b*4}-{b*4+3}): {pyo.value(model.fcr[b]):.1f} MW")
Leistung für FCR zu reservieren reduziert die für Arbitrage verfügbare Leistung. Der Optimierer balanciert:
Eine 10 MW Batterie reserviert 6 MW für FCR. Wie viel Leistung steht für Day-Ahead-Arbitrage zur Verfügung?
Warum fügt der Optimierer SoC-Spielraum-Nebenbedingungen hinzu (SoC ≥ SoC_min + fcr_energie) während der FCR-Bereitstellung?
An einem Tag mit sehr niedrigen Day-Ahead-Preisspreads aber hohen FCR-Kapazitätspreisen, was sollte der Optimierer tun?
ENTSO-E — Electricity Balancing — der europäische Rahmen für Regelleistungsmärkte (FCR, aFRR, mFRR). Lies den „Implementation"-Abschnitt für PICASSO (aFRR) und MARI (mFRR) Plattformregeln.
Rechtliches: Impressum