Physical Flows REST API

ENTSO-E Cross-Border Electricity Flows: Import/Export-Analyse, Congestion Management, Trading Opportunities.

🇪🇺 ENTSO-E ⏱️ 15-Min Resolution 🌍 70+ Borders ⏱️ Real-time

📡 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

  1. Fetch Day-Ahead Prices für 2 Länder (z.B. DE, AT)
  2. Fetch Physical Flows DE→AT (historisch 7 Tage)
  3. Korrelation: Flows ↑ wenn PriceDE < PriceAT?
  4. Wenn Ja: Market Coupling funktioniert → Arbitrage schwierig
  5. 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.