{
  "openapi": "3.0.3",
  "info": {
    "title": "DVhub Price API",
    "description": "Zentraler EPEX Day-Ahead Preisfeed für DVhub-Instanzen. Liefert historische und aktuelle Börsenpreise für 44 europäische Bidding Zones.\n\n**Einheit:** Alle Preise in **EUR/MWh** (Euro pro Megawattstunde). Umrechnung: 1 EUR/MWh = 0,1 ct/kWh.\n\n**Quellen:** ENTSO-E Transparency Platform (primär), Energy Charts / SMARD.de (Fallback)\n\n**Auflösung:** Stündlich (60min) vor 01.10.2024, Viertelstündlich (15min) danach (EPEX DE-LU Umstellung)\n\n**Währung:** EUR für alle 44 Zonen (EPEX handelt in Euro)\n\n**Nutzung:** Frei verfügbar für alle DVhub-Instanzen. Keine Authentifizierung erforderlich.",
    "version": "1.0.0",
    "contact": {
      "name": "DVhub",
      "url": "https://github.com/chloepriceless/dvhub"
    }
  },
  "servers": [
    { "url": "/", "description": "Same-Origin (aktueller Host)" }
  ],
  "paths": {
    "/api/zones": {
      "get": {
        "summary": "Alle verfügbaren Preiszonen",
        "description": "Listet alle 44 EPEX Day-Ahead Bidding Zones mit Abdeckungsstatistiken.",
        "tags": ["Zones"],
        "responses": {
          "200": {
            "description": "Liste aller Zonen",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total_zones": { "type": "integer", "example": 45 },
                    "zones_with_data": { "type": "integer", "example": 44 },
                    "zones": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "zone": { "type": "string", "example": "DE-LU" },
                          "first_record": { "type": "string", "format": "date-time" },
                          "last_record": { "type": "string", "format": "date-time" },
                          "total_records": { "type": "integer" },
                          "days_covered": { "type": "integer" }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/prices": {
      "get": {
        "summary": "Preise für Zeitraum und Zone",
        "description": "Liefert Day-Ahead-Preise für einen Zeitraum. Optional mit 15-Minuten-Interpolation für stündliche Daten.",
        "tags": ["Prices"],
        "parameters": [
          { "name": "start", "in": "query", "required": true, "schema": { "type": "string", "format": "date" }, "example": "2026-03-01", "description": "Startdatum (ISO 8601)" },
          { "name": "end", "in": "query", "required": true, "schema": { "type": "string", "format": "date" }, "example": "2026-03-02", "description": "Enddatum (exklusiv)" },
          { "name": "zone", "in": "query", "schema": { "type": "string", "default": "DE-LU" }, "description": "Bidding Zone" },
          { "name": "resolution", "in": "query", "schema": { "type": "string", "enum": ["15min"] }, "description": "15min interpoliert stündliche Daten" }
        ],
        "responses": {
          "200": {
            "description": "Preisdaten",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "zone": { "type": "string" },
                    "start": { "type": "string", "format": "date-time" },
                    "end": { "type": "string", "format": "date-time" },
                    "count": { "type": "integer" },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "ts": { "type": "string", "format": "date-time" },
                          "price": { "type": "number", "description": "Preis in EUR/MWh (Euro pro Megawattstunde). Umrechnung: Wert / 10 = ct/kWh" },
                          "resolution": { "type": "string" },
                          "source": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/prices/latest": {
      "get": {
        "summary": "Letzte 48 Stunden",
        "description": "Liefert die Preise der letzten 48 Stunden für eine Zone.",
        "tags": ["Prices"],
        "parameters": [
          { "name": "zone", "in": "query", "schema": { "type": "string", "default": "DE-LU" }, "description": "Bidding Zone" }
        ],
        "responses": {
          "200": { "description": "Aktuelle Preise" }
        }
      }
    },
    "/api/prices/stats": {
      "get": {
        "summary": "Abdeckungsstatistiken",
        "description": "Zeigt wie viele Daten für eine Zone vorhanden sind.",
        "tags": ["Statistics"],
        "parameters": [
          { "name": "zone", "in": "query", "schema": { "type": "string", "default": "DE-LU" }, "description": "Bidding Zone" }
        ],
        "responses": {
          "200": {
            "description": "Statistiken",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "zone": { "type": "string" },
                    "first_record": { "type": "string", "format": "date-time" },
                    "last_record": { "type": "string", "format": "date-time" },
                    "total_records": { "type": "integer" },
                    "hourly_records": { "type": "integer" },
                    "quarter_hourly_records": { "type": "integer" },
                    "days_covered": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/prices/gaps": {
      "get": {
        "summary": "Fehlende Daten finden",
        "description": "Listet Tage mit unvollständigen Preisdaten.",
        "tags": ["Statistics"],
        "parameters": [
          { "name": "zone", "in": "query", "schema": { "type": "string", "default": "DE-LU" }, "description": "Bidding Zone" }
        ],
        "responses": {
          "200": {
            "description": "Lueckenliste",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "zone": { "type": "string" },
                    "incomplete_days": { "type": "integer" },
                    "gaps": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": { "type": "string", "format": "date" },
                          "have": { "type": "integer" },
                          "expected": { "type": "integer" },
                          "missing": { "type": "integer" }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/prices/tomorrow-status": {
      "get": {
        "summary": "Status fuer morgige Actual-Daten",
        "description": "Zeigt den Status der morgigen Actual-Daten fuer eine Zone. Preview-Daten sind hier nicht enthalten.",
        "tags": ["Prices"],
        "parameters": [
          { "name": "zone", "in": "query", "schema": { "type": "string", "default": "DE-LU" }, "description": "Bidding Zone" }
        ],
        "responses": {
          "200": { "description": "Tomorrow-Status fuer Actual-Daten" }
        }
      }
    },
    "/api/preview/prices": {
      "get": {
        "summary": "Preview-Preise fuer Zeitraum und Zone",
        "description": "Liefert additive Preview-Preise aus aWATTar. Diese Daten sind getrennt von den bestehenden Actual-Endpunkten.",
        "tags": ["Preview"],
        "parameters": [
          { "name": "start", "in": "query", "required": true, "schema": { "type": "string", "format": "date" }, "example": "2026-03-01", "description": "Startdatum (ISO 8601)" },
          { "name": "end", "in": "query", "required": true, "schema": { "type": "string", "format": "date" }, "example": "2026-03-02", "description": "Enddatum (exklusiv)" },
          { "name": "zone", "in": "query", "schema": { "type": "string", "default": "DE-LU" }, "description": "Bidding Zone" }
        ],
        "responses": {
          "200": { "description": "Preview-Preisdaten" }
        }
      }
    },
    "/api/preview/tomorrow-status": {
      "get": {
        "summary": "Status fuer morgige Preview-Daten",
        "description": "Zeigt den Status der morgigen Preview-Daten aus der separaten Preview-Tabelle.",
        "tags": ["Preview"],
        "parameters": [
          { "name": "zone", "in": "query", "schema": { "type": "string", "default": "DE-LU" }, "description": "Bidding Zone" }
        ],
        "responses": {
          "200": { "description": "Tomorrow-Status fuer Preview-Daten" }
        }
      }
    },
    "/api/backfill": {
      "post": {
        "summary": "Backfill anstossen",
        "description": "Startet den Backfill fehlender Preisdaten für eine Zone im Hintergrund. Nur aus dem LAN erreichbar.",
        "tags": ["Admin"],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "zone": { "type": "string", "default": "DE-LU" },
                  "start": { "type": "string", "format": "date", "default": "2020-01-01" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Backfill gestartet" }
        }
      }
    },
    "/v1/pv/curtailment": {
      "get": {
        "summary": "Abregelungs-/Speicher-Rechner (Solarspitzengesetz)",
        "description": "Berechnet die nach Solarspitzengesetz (§ 51 EEG) in Negativpreis-Viertelstunden nullvergütet abgeregelte PV-Energie und den resultierenden Speicherbedarf. Ohne Orientierung: Flotten-Durchschnitt (cf = nationale Solar-Einspeisung / installierte Leistung). Mit azimuth/tilt/region: standortgenau per PVGIS-Klimaprofil umgewichtet (cf_anlage = cf_national · POA_orient/POA_flotte). Enthält den §51a-EEG-Verlängerungs-Zähler (Faktor 0,5 für Solar). UI: /pv/abregelung.",
        "tags": ["PV"],
        "parameters": [
          { "name": "kwp", "in": "query", "schema": { "type": "string", "default": "10,20,30" }, "description": "Eine oder mehrere Beispielgrößen in kWp (Komma-separiert)" },
          { "name": "from", "in": "query", "schema": { "type": "string" }, "example": "2025-01-01", "description": "Start (YYYY-MM-DD oder ISO 8601). Default: vor 365 Tagen" },
          { "name": "to", "in": "query", "schema": { "type": "string" }, "example": "2026-01-01", "description": "Ende (exklusiv). Default: jetzt" },
          { "name": "selfConsumptionKw", "in": "query", "schema": { "type": "number" }, "description": "Eigenverbrauch als Grundlast in kW (Default-Modell)" },
          { "name": "selfConsumptionKwhDay", "in": "query", "schema": { "type": "number" }, "description": "Alternativ: Eigenverbrauch in kWh/Tag (anteilig im Negativ-Fenster)" },
          { "name": "azimuth", "in": "query", "schema": { "type": "number" }, "example": 0, "description": "Optional Ausrichtung: 0=Süd, negativ=Ost, positiv=West (-90..90) → standortgenaue Umgewichtung" },
          { "name": "tilt", "in": "query", "schema": { "type": "number" }, "example": 30, "description": "Optional Dachneigung Grad (0..90)" },
          { "name": "region", "in": "query", "schema": { "type": "string" }, "example": "center", "description": "Optional north | center | south (alternativ lat)" },
          { "name": "detail", "in": "query", "schema": { "type": "string", "enum": ["false"] }, "description": "detail=false lässt die per-Tag days[]-Arrays weg" }
        ],
        "responses": {
          "200": {
            "description": "Abregelungs-/Speicherergebnis je Anlagengröße + §51a-Verlängerung + Coverage",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "systems": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "kwp": { "type": "number" },
                          "negDays": { "type": "integer" },
                          "totalLostKwh": { "type": "number" },
                          "perDay": { "type": "object", "properties": { "meanKwh": { "type": "number" }, "medianKwh": { "type": "number" }, "maxKwh": { "type": "number" } } },
                          "storageNeeded": { "type": "object", "properties": { "meanKwh": { "type": "number" }, "medianKwh": { "type": "number" }, "maxKwh": { "type": "number" } } }
                        }
                      }
                    },
                    "eegExtension": {
                      "type": "object",
                      "properties": {
                        "factor": { "type": "number", "example": 0.5 },
                        "totalVlvh": { "type": "integer" },
                        "totalExtraFullLoadHours": { "type": "number" },
                        "perYear": { "type": "array", "items": { "type": "object", "properties": { "year": { "type": "string" }, "negQuarterHours": { "type": "integer" }, "vollLastViertelstunden": { "type": "integer" }, "extraFullLoadHours": { "type": "number" } } } }
                      }
                    },
                    "coverage": { "type": "object", "properties": { "totalNegSlots": { "type": "integer" }, "slotsWithData": { "type": "integer" }, "coveragePct": { "type": "number" } } },
                    "warning": { "type": "string", "description": "gesetzt, wenn Datenabdeckung < 95% (Ergebnisse unterschätzt)" }
                  }
                }
              }
            }
          },
          "400": { "description": "Ungültiges kwp oder from/to" }
        }
      }
    },
    "/v1/pv/neg-price-slots": {
      "get": {
        "summary": "Rohe Negativpreis-Viertelstunden",
        "description": "Liefert die Day-Ahead-15min-Slots mit Preis < 0 (DE-LU) im Zeitraum — für Tagesansicht/Markierung.",
        "tags": ["PV"],
        "parameters": [
          { "name": "from", "in": "query", "schema": { "type": "string" }, "description": "Start (YYYY-MM-DD oder ISO 8601)" },
          { "name": "to", "in": "query", "schema": { "type": "string" }, "description": "Ende (exklusiv)" }
        ],
        "responses": { "200": { "description": "Negativpreis-Slots" } }
      }
    },
    "/v1/pv/generation": {
      "get": {
        "summary": "Nationale Solar-Einspeisung",
        "description": "Zeitreihe der nationalen Solar-Einspeisung (Energy-Charts public_power, MW) im Zeitraum.",
        "tags": ["PV"],
        "parameters": [
          { "name": "from", "in": "query", "schema": { "type": "string" }, "description": "Start (YYYY-MM-DD oder ISO 8601)" },
          { "name": "to", "in": "query", "schema": { "type": "string" }, "description": "Ende (exklusiv)" }
        ],
        "responses": { "200": { "description": "Solar-Einspeisung (solarMw je ts)" } }
      }
    },
    "/v1/pv/market-value": {
      "get": {
        "summary": "Marktwert Solar (monatlich + laufend)",
        "description": "Erzeugungsgewichteter Monats-Mittel des Day-Ahead-Preises (Σ Preis·Solar / Σ Solar) = Marktwert Solar. Eigenberechnung aus day_ahead_prices × pv_generation_national, credential-frei. ct/kWh = (€/MWh)/10.",
        "tags": ["PV"],
        "parameters": [
          { "name": "year", "in": "query", "schema": { "type": "string" }, "example": "2025", "description": "Kalenderjahr. Default: laufendes Jahr. Alternativ from/to." },
          { "name": "from", "in": "query", "schema": { "type": "string" }, "description": "Start (YYYY-MM-DD / ISO), überschreibt year" },
          { "name": "to", "in": "query", "schema": { "type": "string" }, "description": "Ende (exklusiv)" }
        ],
        "responses": {
          "200": {
            "description": "Monats-Marktwerte + laufender Schnitt",
            "content": { "application/json": { "schema": { "type": "object", "properties": {
              "period": { "type": "string" },
              "runningAvgCtKwh": { "type": "number" },
              "source": { "type": "string", "example": "computed" },
              "months": { "type": "array", "items": { "type": "object", "properties": {
                "year": { "type": "integer" }, "month": { "type": "integer" },
                "marketValueEurMwh": { "type": "number" }, "marketValueCtKwh": { "type": "number" },
                "runningAvgCtKwh": { "type": "number" } } } }
            } } } }
          },
          "400": { "description": "Ungültiges from/to" }
        }
      }
    },
    "/v1/pv/distribution": {
      "get": {
        "summary": "Monats-Verteilung der Solar-Erzeugung",
        "description": "Anteil der Solar-Jahreserzeugung je Kalendermonat, gemittelt über N volle Jahre, mit Median und P25/P75-Band (Saisonalität). Ersetzt hartcodiertes pvMonthlyDistributionPct.",
        "tags": ["PV"],
        "parameters": [
          { "name": "years", "in": "query", "schema": { "type": "string" }, "example": "2021-2025", "description": "Jahresbereich YYYY-YYYY. Default: letzte 5 volle Jahre" }
        ],
        "responses": {
          "200": {
            "description": "Monatsverteilung",
            "content": { "application/json": { "schema": { "type": "object", "properties": {
              "requestedYears": { "type": "string" },
              "years": { "type": "array", "items": { "type": "integer" } },
              "months": { "type": "array", "items": { "type": "object", "properties": {
                "month": { "type": "integer" }, "meanPct": { "type": "number" }, "medianPct": { "type": "number" },
                "p25Pct": { "type": "number" }, "p75Pct": { "type": "number" }, "nYears": { "type": "integer" } } } }
            } } } }
          },
          "400": { "description": "Ungültiges years-Format" }
        }
      }
    },
    "/v1/pv/neg-price-trend": {
      "get": {
        "summary": "Negativpreis-Wachstumstrend + Projektion",
        "description": "Negativpreis-Stunden/Tage je Jahr (price<0, resolutionskorrekt, Berlin-Kalender) aus day_ahead_prices; laufendes Jahr saisonal annualisiert; Projektions-Band (lineare Regression + CAGR, Regime-Cut ab 2023, da 2021/2022 anderes Preisregime).",
        "tags": ["PV"],
        "parameters": [
          { "name": "regimeFrom", "in": "query", "schema": { "type": "integer" }, "example": 2023, "description": "Erstes vollständiges Jahr für den Fit (Default 2023)" },
          { "name": "horizon", "in": "query", "schema": { "type": "integer" }, "example": 2, "description": "Projektionsjahre (1-5, Default 2)" }
        ],
        "responses": { "200": { "description": "perYear + current (annualisiert) + projection (Band)" } }
      }
    },
    "/v1/pv/storage-sizing": {
      "get": {
        "summary": "Speicher-Dimensionierung bei statischer Einspeisegrenze (Szenario)",
        "description": "Vorausschauendes, annahmebasiertes Szenario (keine Messdaten der konkreten Anlage): bei statischer Einspeise-/Wirkleistungsbegrenzung (% der kWp, z.B. 70%-Regel / § 9 EEG) — (a) abgeregelte Energie/Jahr, (b) nötige Speichergröße je Ziel-Deckung, (c) Deckungs-Kurve. PVGIS-Klimaprofil je Ausrichtung, kein DB-Zugriff.",
        "tags": ["PV"],
        "parameters": [
          { "name": "kwp", "in": "query", "schema": { "type": "number" }, "example": 10, "description": "Anlagengröße kWp (Default 10)" },
          { "name": "limitPercent", "in": "query", "schema": { "type": "number" }, "example": 70, "description": "Einspeisegrenze in % der kWp (30-100, Default 70)" },
          { "name": "azimuth", "in": "query", "schema": { "type": "number" }, "example": 0, "description": "0=Süd, negativ=Ost, positiv=West (-90..90)" },
          { "name": "tilt", "in": "query", "schema": { "type": "number" }, "example": 30, "description": "Dachneigung Grad (0..90)" },
          { "name": "region", "in": "query", "schema": { "type": "string" }, "example": "center", "description": "north | center | south (alternativ lat)" },
          { "name": "annualYieldKwhPerKwp", "in": "query", "schema": { "type": "number" }, "description": "Optional eigener Jahresertrag kWh/kWp (sonst PVGIS-Default je Ausrichtung)" }
        ],
        "responses": { "200": { "description": "curtailedKwh (a) + storageForCoverage (b) + coverageCurve (c)" }, "400": { "description": "Ungültige Parameter" } }
      }
    }
  }
}
