Jede bisherige Lektion nahm an, dass wir die Preise perfekt kennen. Tun wir nicht. Prognosen sind falsch. Wind fällt unerwartet ab. Ein Kraftwerk fällt aus. Diese Lektion zeigt drei Ansätze zum Umgang mit Preisunsicherheit — vom einfachsten zum anspruchsvollsten.
Dein Lektion-3-Optimierer nimmt einen einzelnen Preisvektor und findet den optimalen Fahrplan für genau diese Preise. Wenn die tatsächlichen Preise abweichen, kann dein „optimaler" Fahrplan suboptimal oder sogar verlustbringend sein.
# Beispiel: Prognose sagt Stunde 18 = €90/MWh → Optimierer entlädt
# Realität: Stunde 18 = €45/MWh (bewölkter Tag, Solar überproduiert)
# Ergebnis: Du hast zu €45 verkauft statt auf eine echte Spitze später zu warten
Führe deinen Optimierer mehrfach mit verschiedenen Preisszenarien aus und sieh, wie sich der Fahrplan ändert:
"""
sensitivitaet.py — Wie empfindlich ist der Fahrplan gegenüber Preisänderungen?
"""
import numpy as np
# Basisprognose
preise_basis = [28, 25, 22, 20, 19, 21, 35, 55,
62, 58, 52, 48, 45, 44, 47, 53,
68, 85, 92, 78, 60, 45, 35, 30]
# Szenarien generieren: Basis ± Rauschen
np.random.seed(42)
n_szenarien = 20
szenarien = []
for _ in range(n_szenarien):
rauschen = np.random.normal(0, 8, 24) # ±€8/MWh Standardabweichung
szenario = np.clip(np.array(preise_basis) + rauschen, -50, 200)
szenarien.append(szenario.tolist())
# Optimierer für jedes Szenario ausführen, Fahrpläne sammeln
# ergebnisse = [batterie_loesen(s) for s in szenarien]
# Analyse: Welche Stunden sind IMMER Laden/Entladen unabhängig vom Szenario?
# Das sind robuste Entscheidungen. Stunden die wechseln → riskant, konservativ bieten.
Statt für einen Preispfad zu optimieren, optimiere über mehrere Szenarien gleichzeitig. Die Zielfunktion wird zum erwarteten Erlös über alle Szenarien:
"""
stochastisch.py — Zweistufige stochastische Optimierung
"""
import pyomo.environ as pyo
import numpy as np
T = 24
P_max = 10.0
E_max = 20.0
eta = 0.90
eta_sqrt = np.sqrt(eta)
soc_init = 10.0
deg_kosten = 5.0
# Preisszenarien mit Wahrscheinlichkeiten
szenarien = {
'basis': {'prob': 0.5, '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]},
'hoch': {'prob': 0.25, 'preise': [35,30,28,25,24,28,45,70,80,75,68,62,58,55,60,68,88,110,120,95,75,55,42,35]},
'niedrig':{'prob': 0.25, 'preise': [22,20,18,16,15,17,28,42,48,45,40,38,35,34,36,40,52,65,70,58,45,35,28,22]},
}
model = pyo.ConcreteModel()
model.T = pyo.RangeSet(0, T - 1)
model.S = pyo.Set(initialize=szenarien.keys())
# ERSTE-STUFE-Entscheidungen: Gebotsmengen (festgelegt bevor Preise bekannt)
model.p_laden = pyo.Var(model.T, bounds=(0, P_max))
model.p_entladen = pyo.Var(model.T, bounds=(0, P_max))
model.soc = pyo.Var(model.T, bounds=(0.1*E_max, 0.9*E_max))
model.y = pyo.Var(model.T, within=pyo.Binary)
# Zielfunktion: erwarteter Erlös über Szenarien
def ziel_regel(m):
gesamt = 0
for s_name, s_daten in szenarien.items():
prob = s_daten['prob']
preise = s_daten['preise']
erloes = sum(preise[t] * (m.p_entladen[t] - m.p_laden[t]) for t in m.T)
deg = sum(deg_kosten * (m.p_laden[t] + m.p_entladen[t]) for t in m.T)
gesamt += prob * (erloes - deg)
return gesamt
model.ziel = pyo.Objective(rule=ziel_regel, sense=pyo.maximize)
# Nebenbedingungen (gleiche Struktur wie zuvor)
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)
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)
solver = pyo.SolverFactory('highs')
ergebnis = solver.solve(model, tee=False)
print(f"Erwarteter Erlös: €{pyo.value(model.ziel):.2f}")
Statt den Erwartungswert zu optimieren, garantiere eine Mindestperformance im schlimmsten Szenario:
"""
robust.py — Maximiere Worst-Case-Erlös über Szenarien
"""
import pyomo.environ as pyo
# ... gleiche Einrichtung wie oben ...
model = pyo.ConcreteModel()
model.T = pyo.RangeSet(0, T - 1)
model.p_laden = pyo.Var(model.T, bounds=(0, P_max))
model.p_entladen = pyo.Var(model.T, bounds=(0, P_max))
model.soc = pyo.Var(model.T, bounds=(0.1*E_max, 0.9*E_max))
model.y = pyo.Var(model.T, within=pyo.Binary)
# Hilfsvariable: Worst-Case-Erlös
model.worst_case = pyo.Var()
# Zielfunktion: Worst Case maximieren
model.ziel = pyo.Objective(expr=model.worst_case, sense=pyo.maximize)
# Worst Case muss ≤ Erlös in jedem Szenario sein
def szenario_schranke(m, s_name):
preise = szenarien[s_name]['preise']
erloes = sum(preise[t] * (m.p_entladen[t] - m.p_laden[t]) for t in m.T)
deg = sum(deg_kosten * (m.p_laden[t] + m.p_entladen[t]) for t in m.T)
return m.worst_case <= erloes - deg
model.S = pyo.Set(initialize=szenarien.keys())
model.szenarien_schranken = pyo.Constraint(model.S, rule=szenario_schranke)
Die robuste Lösung ist konservativer: sie jagt keinen Hochrisiko-Spitzen nach, die nur in einem Szenario auftauchen. Stattdessen findet sie Fahrpläne, die in allen Szenarien passabel abschneiden.
| Ansatz | Komplexität | Am besten wenn |
|---|---|---|
| Deterministisch + Marge | Niedrig | Gute Prognosen, liquider Intraday zur Fehlerkorrektur |
| Sensitivitätsanalyse | Niedrig | Robuste vs. riskante Stunden für Gebotsmengen identifizieren |
| Stochastisch (Erwartungswert) | Mittel | Probabilistische Prognosen verfügbar, risikoneutraler Betreiber |
| Robust (Worst-Case) | Mittel | Hohe Prognoseunsicherheit, risikoaverser Betreiber |
Ein deterministischer Optimierer erzeugt einen Fahrplan für prognostizierte Preise. Die tatsächlichen Preise liegen 20% niedriger über alle Stunden. Was passiert mit dem realisierten Erlös?
Im stochastischen Modell, was bewirkt das Multiplizieren des Erlöses jedes Szenarios mit seiner Wahrscheinlichkeit?
Im Vergleich zu stochastischer Optimierung erzeugt robuste Optimierung typischerweise Fahrpläne die:
Boyd & Vandenberghe, Kapitel 4.4 — „Robust Linear Programming" — formale Behandlung robuster Optimierung mit Unsicherheitsmengen. Erklärt die Dualität hinter der Max-Min-Formulierung.
Rechtliches: Impressum