Physical Flows REST API
ENTSO-E Cross-Border Electricity Flows: Import/Export-Analyse, Congestion Management, Trading Opportunities.
📡 Endpunkt
POST /api/entsoe/physical-flows
⚡ Borders
70+ Cross-Border
⏱️ Resolution
15 Minuten (PT15M)
📊 Response
600-1200ms
REST API Dokumentation
HTTP Endpunkt
POST https://api.cernion.de/api/entsoe/physical-flows
Request Parameters
| Parameter | Typ | Required | Beschreibung |
|---|---|---|---|
fromCountry |
string | ✅ Ja | Export-Land: "Germany", "France", etc. (Auto EIC-Mapping) |
toCountry |
string | ✅ Ja | Import-Land: "Austria", "Poland", etc. |
dateFrom |
string | ✅ Ja | Start-Datum: ISO 8601 (2026-02-01), YYYYMMDD, DD.MM.YYYY |
dateTo |
string | ✅ Ja | End-Datum (Max 31 Tage Range) |
includeStatistics |
boolean | ❌ Nein | Min/Max/Avg/Median + Congestion Stats - default: true |
Beispiel Request
{
"fromCountry": "Germany",
"toCountry": "Austria",
"dateFrom": "2026-02-01",
"dateTo": "2026-02-07",
"includeStatistics": true
}
Response Schema
{
"success": true,
"border": {
"from": "Germany",
"fromEIC": "10Y1001A1001A82H",
"to": "Austria",
"toEIC": "10YAT-APG------L"
},
"periodStart": "202602010000",
"periodEnd": "202602072300",
"resolution": "PT15M",
"unit": "MW",
"flows": [
{
"timestamp": "2026-02-01T00:00:00Z",
"flow": 1234.5,
"direction": "export",
"position": 1
},
{
"timestamp": "2026-02-01T00:15:00Z",
"flow": 1189.3,
"direction": "export",
"position": 2
}
],
"statistics": {
"totalDataPoints": 672,
"avgFlow": 1156.7,
"maxFlow": 2345.8,
"minFlow": -234.5,
"medianFlow": 1123.4,
"netExport": 778234.5,
"netExportMWh": 778.2,
"exportHours": 158,
"importHours": 10,
"congestionHours": 24,
"congestionRate": 14.3
},
"capacity": {
"ntc": 2400,
"utilizationAvg": 48.2,
"utilizationMax": 97.7,
"unit": "MW"
},
"metadata": {
"source": "ENTSO-E Transparency Platform",
"documentType": "A11",
"queryTime": "2026-02-08T10:45:22Z"
}
}
Flow Direction Convention
- Positive Flow (+MW): Export von fromCountry → toCountry
- Negative Flow (-MW): Import (Umkehr-Richtung)
- direction: "export" oder "import" (automatisch berechnet)
Code-Beispiele
curl -X POST https://api.cernion.de/api/entsoe/physical-flows \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"fromCountry": "Germany",
"toCountry": "Austria",
"dateFrom": "2026-02-01",
"dateTo": "2026-02-07",
"includeStatistics": true
}'
const API_TOKEN = 'YOUR_API_TOKEN';
async function analyzePhysicalFlows(from, to, dateFrom, dateTo) {
const response = await fetch(
'https://api.cernion.de/api/entsoe/physical-flows',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
fromCountry: from,
toCountry: to,
dateFrom,
dateTo,
includeStatistics: true
})
}
);
const data = await response.json();
console.log(`\n=== Physical Flows: ${data.border.from} → ${data.border.to} ===`);
console.log(`Period: ${dateFrom} to ${dateTo}\n`);
const stats = data.statistics;
console.log('Flow Statistics:');
console.log(` Average: ${stats.avgFlow.toFixed(0)} MW`);
console.log(` Peak: ${stats.maxFlow.toFixed(0)} MW`);
console.log(` Net Export: ${stats.netExportMWh.toFixed(0)} MWh`);
console.log(` Export Hours: ${stats.exportHours}h | Import Hours: ${stats.importHours}h\n`);
console.log('Congestion Analysis:');
console.log(` NTC Capacity: ${data.capacity.ntc} MW`);
console.log(` Avg Utilization: ${data.capacity.utilizationAvg.toFixed(1)}%`);
console.log(` Congestion Rate: ${stats.congestionRate.toFixed(1)}% (${stats.congestionHours}h)\n`);
// Congestion Alert
if (stats.congestionRate > 20) {
console.warn(`⚠️ HIGH CONGESTION: ${stats.congestionRate.toFixed(1)}% - Trading opportunities limited!`);
}
// Net Flow Assessment
const netDirection = stats.netExport > 0 ? 'Net Exporter' : 'Net Importer';
console.log(`${data.border.from} is ${netDirection} to ${data.border.to}`);
return data;
}
// Multi-Border Analysis for Germany
async function analyzeGermanyBorders() {
const borders = [
['Germany', 'Austria'],
['Germany', 'France'],
['Germany', 'Poland'],
['Germany', 'Netherlands'],
['Germany', 'Switzerland']
];
const dateFrom = '2026-02-01';
const dateTo = '2026-02-07';
console.log('\n=== Germany Cross-Border Analysis ===\n');
for (const [from, to] of borders) {
const data = await analyzePhysicalFlows(from, to, dateFrom, dateTo);
const net = data.statistics.netExportMWh;
const direction = net > 0 ? '→ Export' : '← Import';
console.log(`${to.padEnd(15)} ${direction.padStart(10)}: ${Math.abs(net).toFixed(0).padStart(6)} MWh`);
}
}
analyzePhysicalFlows('Germany', 'Austria', '2026-02-01', '2026-02-07');
import requests
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
API_TOKEN = 'YOUR_API_TOKEN'
class PhysicalFlowsAnalyzer:
def __init__(self, api_token):
self.api_token = api_token
self.base_url = 'https://api.cernion.de/api/entsoe'
def get_flows(self, from_country, to_country, date_from, date_to):
"""Fetch physical flows data"""
response = requests.post(
f'{self.base_url}/physical-flows',
headers={
'Authorization': f'Bearer {self.api_token}',
'Content-Type': 'application/json'
},
json={
'fromCountry': from_country,
'toCountry': to_country,
'dateFrom': date_from,
'dateTo': date_to,
'includeStatistics': True
}
)
return response.json()
def analyze_border(self, from_country, to_country, date_from, date_to):
"""Comprehensive border analysis"""
data = self.get_flows(from_country, to_country, date_from, date_to)
print(f'\n=== {data["border"]["from"]} → {data["border"]["to"]} ===')
print(f'Period: {date_from} to {date_to}')
stats = data['statistics']
capacity = data['capacity']
print(f'\n📊 Flow Statistics:')
print(f' Average Flow: {stats["avgFlow"]:.0f} MW')
print(f' Peak Flow: {stats["maxFlow"]:.0f} MW')
print(f' Min Flow: {stats["minFlow"]:.0f} MW')
print(f' Net Export: {stats["netExportMWh"]:.0f} MWh')
print(f'\n⚡ Directional Analysis:')
export_pct = (stats['exportHours'] / (stats['exportHours'] + stats['importHours'])) * 100
print(f' Export Hours: {stats["exportHours"]}h ({export_pct:.1f}%)')
print(f' Import Hours: {stats["importHours"]}h ({100-export_pct:.1f}%)')
print(f'\n🚧 Congestion:')
print(f' NTC Capacity: {capacity["ntc"]} MW')
print(f' Avg Utilization: {capacity["utilizationAvg"]:.1f}%')
print(f' Max Utilization: {capacity["utilizationMax"]:.1f}%')
print(f' Congestion Rate: {stats["congestionRate"]:.1f}% ({stats["congestionHours"]}h)')
if stats['congestionRate'] > 25:
print(f'\n⚠️ HIGH CONGESTION: Trading capacity limited!')
return data
def compare_german_borders(self, date_from='2026-02-01', date_to='2026-02-07'):
"""Compare all German cross-border flows"""
borders = [
('Germany', 'Austria'),
('Germany', 'France'),
('Germany', 'Poland'),
('Germany', 'Netherlands'),
('Germany', 'Switzerland'),
('Germany', 'Denmark'),
('Germany', 'Czech Republic')
]
results = []
for from_c, to_c in borders:
try:
data = self.get_flows(from_c, to_c, date_from, date_to)
stats = data['statistics']
capacity = data['capacity']
results.append({
'Border': f'{from_c[:2]} → {to_c[:2]}',
'To Country': to_c,
'Avg Flow MW': stats['avgFlow'],
'Net Export MWh': stats['netExportMWh'],
'Export Hours': stats['exportHours'],
'Import Hours': stats['importHours'],
'NTC MW': capacity['ntc'],
'Utilization %': capacity['utilizationAvg'],
'Congestion %': stats['congestionRate']
})
except Exception as e:
print(f'Error: {from_c} → {to_c}: {e}')
df = pd.DataFrame(results)
# Sort by Net Export (highest export first)
df = df.sort_values('Net Export MWh', ascending=False)
print('\n=== Germany Cross-Border Summary ===')
print(df.to_string(index=False))
# Net position
total_net = df['Net Export MWh'].sum()
print(f'\nGermany Net Position: {total_net:.0f} MWh')
print(f'Status: {"NET EXPORTER" if total_net > 0 else "NET IMPORTER"}')
# Congestion hotspots
congested = df[df['Congestion %'] > 20]
if len(congested) > 0:
print(f'\n⚠️ Congested Borders (>20%):')
print(congested[['Border', 'Congestion %', 'NTC MW']].to_string(index=False))
return df
def plot_flows(self, from_country, to_country, date_from, date_to):
"""Visualize flow patterns"""
data = self.get_flows(from_country, to_country, date_from, date_to)
df = pd.DataFrame(data['flows'])
df['timestamp'] = pd.to_datetime(df['timestamp'])
df['hour'] = df['timestamp'].dt.hour
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
# Time series
axes[0].plot(df['timestamp'], df['flow'], linewidth=0.8)
axes[0].axhline(y=0, color='r', linestyle='--', alpha=0.5)
axes[0].axhline(y=data['capacity']['ntc'], color='orange', linestyle='--', alpha=0.5, label='NTC Capacity')
axes[0].set_title(f'Physical Flows: {data["border"]["from"]} → {data["border"]["to"]}')
axes[0].set_ylabel('Flow (MW)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Hourly average
hourly_avg = df.groupby('hour')['flow'].mean()
axes[1].bar(hourly_avg.index, hourly_avg.values, color='steelblue')
axes[1].axhline(y=0, color='r', linestyle='--', alpha=0.5)
axes[1].set_title('Average Flow by Hour')
axes[1].set_xlabel('Hour of Day')
axes[1].set_ylabel('Avg Flow (MW)')
axes[1].grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.savefig(f'flows_{from_country}_{to_country}.png')
print(f'\nChart saved: flows_{from_country}_{to_country}.png')
if __name__ == '__main__':
analyzer = PhysicalFlowsAnalyzer(API_TOKEN)
# Single border analysis
analyzer.analyze_border('Germany', 'Austria', '2026-02-01', '2026-02-07')
# Multi-border comparison
df = analyzer.compare_german_borders()
# Export
df.to_csv('german_borders.csv', index=False)
# Visualization
analyzer.plot_flows('Germany', 'Austria', '2026-02-01', '2026-02-07')
Anwendungsfälle
💰 Trading Opportunities
Price Spread DE-AT: Flows →→→ AT bei DE-Preis < AT-Preis - 5€/MWh = Arbitrage
🚧 Congestion Management
Utilization >80%: Congestion Risk → Redispatch-Maßnahmen planen (§13 EnWG)
🌍 Import Dependency
Negative Net Export (Import): Gas-Verstromung in DE teurer als FR/PL Import
📊 Market Coupling
Flow Direction Correlation mit Price Difference → Market Coupling Efficiency Check
Wichtige Cross-Border Connections
🇩🇪 Germany Borders (9 Nachbarn)
| Border | NTC Capacity | Typical Flow |
|---|---|---|
| DE → AT | 2,400 MW | Export (Wind) |
| DE → FR | 3,000 MW | Bidirectional |
| DE → PL | 1,500 MW | Export |
| DE → NL | 3,500 MW | Export |
| DE → CH | 4,000 MW | Bidirectional (Pump Storage) |
⚡ Congestion Hotspots
- DE → AT: Loop Flows über PL/CZ → österreichische Netz-Überlastung
- FR → ES: Pyrenäen-Engpass (2,800 MW NTC, aber oft 95% Utilization)
- PL → DE: Kohle-Export bei Wind-Flaute in DE
- GB → FR/NL: Interconnectors oft >90% Utilization
🔄 Loop Flows
Definition: Strom "umgeht" direkte Border und fließt über Drittländer.
Beispiel: DE → AT direkte NTC 2,400 MW überlastet → Strom fließt über PL → CZ → AT
Problem: Belastet Nachbar-Netze → Redispatch-Kosten → §13a EnWG Umlage
API Use Case: Vergleiche DE→AT direkt vs. DE→PL + PL→CZ + CZ→AT = Loop Detection
💡 Trading Strategie
- Fetch Day-Ahead Prices für 2 Länder (z.B. DE, AT)
- Fetch Physical Flows DE→AT (historisch 7 Tage)
- Korrelation: Flows ↑ wenn PriceDE < PriceAT?
- Wenn Ja: Market Coupling funktioniert → Arbitrage schwierig
- Wenn Nein: Congestion verhindert Arbitrage → Intraday Trading!
ROI durch Cross-Border Trading & Congestion-Analyse
| Anwendung | Szenario | Profit/Jahr | Implementation |
|---|---|---|---|
| Cross-Border Arbitrage | 10 MW Portfolio, Price Spread DE-AT durchschnittlich 8€/MWh, 30% Execution Rate | 210.000€ (10 MW × 8€ × 8760h × 30%) | API + Trading Algo |
| Congestion Forecasting | Redispatch-Kosten 50% reduzieren durch proaktive §13 EnWG Maßnahmen | 420.000€ (für mittleren VNB mit 5 Mio€ Redispatch-Kosten/Jahr) | ML Model + API Feed |
| Import Optimization | Stadtwerk mit 500 GWh/Jahr Beschaffung, 2% Optimierung durch Flow-basiertes Timing | 600.000€ (500 GWh × 60€/MWh × 2%) | API Integration in Beschaffungs-Tool |
| Gesamt | 1.230.000€ Potential/Jahr | API: 299€/Monat | |
FAQ
Was bedeutet "Physical Flows" vs "Commercial Schedules"?
Physical Flows: Tatsächlich gemessene Stromflüsse an Border (A11). Commercial Schedules: Geplante/gehandelte Flows (nicht immer identisch wegen Loop Flows).
Warum ist NTC Utilization wichtig?
NTC (Net Transfer Capacity) = Max erlaubter Flow. >80% Utilization = Congestion Risk → Price Decoupling → Trading Opportunities (oder Redispatch-Bedarf).
Kann ich Flows für alle EU-Borders gleichzeitig abrufen?
API ist Border-spezifisch (1 Call = 1 Border). Für Multi-Border: Parallele Calls (Rate Limit: 60/min). Deutschland hat 9 Nachbarn = 9 Calls für komplette Analyse.
Wie erkenne ich Loop Flows?
Vergleichen Sie direkte Border (DE→AT) mit indirekten Routen (DE→PL, PL→CZ, CZ→AT). Wenn indirekte Flows steigen während direkte Border congested: Loop Flow Indikator!
Cross-Border Trading optimieren
1.2 Mio€ Potential durch Arbitrage, Congestion Management und Import-Optimierung
Durchschnittliche Setup-Zeit: 5 Minuten. Erste Flows nach wenigen Minuten.