ED.OFFER.CURVE
OMIE day-ahead offer curve histogram by tech — 8 price bins of offered volume. Finer-grained companion to ED.OFFER.STRATEGY (which collapses everything into 4 tiers).
Signature
ED.OFFER.CURVE([tech], [start], [end], [agg], [zone], [headers])Description
Returns the OMIE day-ahead **offer curve** (price histogram) per technology by splitting the offered volume (side='V', status='O') across **8 price bins** per (bucket, tech). Equivalent to the aggregate **merit-order intent** of each tech — finer-grained than ED.OFFER.STRATEGY (which collapses everything into 4 tiers).
**Bin boundaries (€/MWh)**:
- mwh_lt_neg10 — < -10 (deep negatives; must-run / hard hedge).
- mwh_neg10_to_0 — [-10, 0) (soft negatives; gaming the floor).
- mwh_0_to_5 — [0, 5) (near-zero price-takers).
- mwh_5_to_20 — [5, 20) (cheap VRE / hydro spill tier).
- mwh_20_to_50 — [20, 50) (mid-merit competitive band).
- mwh_50_to_100 — [50, 100) (gas baseload zone).
- mwh_100_to_180 — [100, 180) (scarcity ramp).
- mwh_gt_180 — ≥ 180 (safety-bid tier near the cap).
Plus n_slots (count of distinct hourly slots with any offered volume in the bucket × tech).
**Killer insight** (Dec 2025 ES, default range): **solar 99%** of volume concentrates in ≤20 €/MWh (price-taker), **thermal_gas 79%** in >100 €/MWh (scarcity/safety bidding), **wind spreads** across all bins (the only tech that consistently bids both deep negatives AND scarcity tiers, a structural footprint of its 24/7 production profile).
**Why this function exists**: ED.OFFER.STRATEGY collapses the offer curve into 4 headline tiers and pct's. ED.OFFER.CURVE is the raw 8-bin histogram — the dataset MIBEL traders manually request from OMIE today. Asset managers use it to detect strategy drift ("is my fleet bidding like its peers?") and to size the structural-supply curve per tech.
**Tech enum (18 accepted values — identical to ED.OMIE.BIDS / ED.OFFER.STRATEGY)**:
- *Renewables*: solar, wind, vre (= solar+wind+renewable_other+vre_unspecified), hydro, bombeo, bess, hybrid.
- *Thermal*: nuclear, coal, thermal_gas, thermal_fueloil, thermal (alias for thermal_gas + thermal_fueloil), biomass, cogen.
- *Other*: autoprod.
- *Aggregator*: all (default) — every classified real tech, ex interconnects + non-generation labels.
- *Special*: interconnect — INVERTED filter; returns only MIEU/MIP/MIE. Note: interconnects typically have **zero offered side** in OMIE data, so all 8 MWh bins may be 0.
Rejected with 400 (with explanatory hints): placeholder, unknown, representation, venta_generica, and any other unrecognised string.
**Expansion semantics**: vre/all/thermal cause the output to spill ONE row per sub-tech. Single-tech values keep the literal you asked for as the row label.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
| tech | string | "all" | Tech filter. Default "all" (= every classified real tech). Same 18-value enum as ED.OMIE.BIDS / ED.OFFER.STRATEGY. |
| start | date | — | YYYY-MM-DD. Omit for the **last completed natural year (Madrid)** start when agg ∈ {1, 6, 7} (e.g. 2025-01-01); otherwise end − 12 months. |
| end | date | — | YYYY-MM-DD. Omit for the **first day after the last completed natural year (Madrid)** when agg ∈ {1, 6, 7} (e.g. 2026-01-01); otherwise the first day after the last available month (OMIE publishes monthly with ~90-day lag). |
| agg | 1..7 | 6 | **1=hour-of-day Madrid (24 rows, bucket '00'..'23')** — per ADR-011, replaces the former hour-timestamp grain. 2=daily, 3=monthly, 4=quarterly, 5=semiannual, 6=annual (default), 7=total. String aliases H/D/M/Q/S/Y/T also accepted. |
| zone | "es" | "pt" | "iberian" | "es" | Geographic filter. es → pais IN ('ES','MI') (default), pt → pais IN ('PT','MI'), iberian → pais IN ('ES','PT','MI') (includes the MIBEL pais='MI' bucket). |
| headers | 0 | 1 | 0 | Set 1 for a header row. |
Returns
Spill matrix — 11 columns: [bucket, tech, mwh_lt_neg10, mwh_neg10_to_0, mwh_0_to_5, mwh_5_to_20, mwh_20_to_50, mwh_50_to_100, mwh_100_to_180, mwh_gt_180, n_slots]. One row per (bucket, tech) pair. For agg=7 (total) bucket is the literal string "TOTAL". Raw MWh per bin — no percentages (compute in Excel as needed).
Examples
=ED.OFFER.CURVE()— Last completed natural year offer curve all techs ES, yearly (all defaults — pre-agg path, sub-200ms)=ED.OFFER.CURVE("all", , , 1, "es", 1)— **24-row hour-of-day** Madrid curve all techs ES — last completed natural year, with header row (pre-agg path, sub-200ms). ADR-011.=ED.OFFER.CURVE("solar", "2025-01-01", "2026-01-01", 1, "es")— Hour-of-day solar 2025 ES — the merit-order shape by Madrid hour (pre-agg path). Pair with ED.OFFER.HEATMAP for the intraday×monthly view.=ED.OFFER.CURVE("solar", "2025-12-01", "2025-12-31", 3, "es")— Monthly solar Dec 2025 ES — 99% of volume in ≤20 €/MWh bins (live path)=ED.OFFER.CURVE("thermal_gas", "2025-01-01", "2025-12-31", 6, "es")— Annual thermal_gas 2025 ES — ~79% volume in >100 €/MWh tiers (live; boundaries off year-natural by 1d)=ED.OFFER.CURVE("thermal_gas", "2025-01-01", "2026-01-01", 6, "es")— Annual thermal_gas 2025 ES — same insight, but end=2026-01-01 hits the **pre-agg** path (sub-200ms)=ED.OFFER.CURVE("wind", "2025-12-01", "2025-12-31", 3, "es")— Monthly wind Dec 2025 ES — broadest distribution (negatives + scarcity)=ED.OFFER.CURVE("vre", "2025-01-01", "2025-12-31", 3, "iberian", 1)— Monthly VRE 2025 Iberian (4 sub-techs spilling separately) with header row=ED.OFFER.CURVE("nuclear", "2025-01-01", "2026-01-01", 6, "es")— Annual nuclear 2025 ES — paradox: large `mwh_lt_neg10` (avoiding shutdowns) + `mwh_gt_180` (reserve). Pre-agg path.Notes
- **
agg=1semantic (ADR-011 — breaking change)**:agg=1was redefined as **hour-of-day Madrid (0-23, 24 rows total)**, replacing the former hour-timestamp grain (8760 rows/year, impractical for Excel). The bucket is the 2-char string'00'..'23'(lpad'd hour). UseED.OFFER.HEATMAPif you need the intraday × monthly matrix instead of the collapsed-over-the-range hour-of-day curve. No JSDoc examples used the old hour-timestamp semantic, so no callers should silently break; manifest bumped 8.7.0.0 → 8.7.1.0 forces a re-pull in Excel clients. - **Default semantics (ADR-009 + ADR-011)**: when both
startandendare omitted ANDagg ∈ {1, 6, 7}, the range defaults to the **last completed natural year Madrid** (e.g. with OMIE data published through Apr 2026, default = 2025-01-01 → 2026-01-01). Whenagg ∈ {2..5}(sub-annual) the legacyend − 12 monthsdefault is kept. Any explicitstart/endoverrides the default unchanged. - **Pre-aggregation routing**: when
agg ∈ {1, 6, 7}ANDstart = YYYY-01-01ANDend = (YYYY+N)-01-01(any whole-natural-year range Madrid), data is served from a pre-agg table —omie_offer_curve_year_tech(PK(year, zone, tech)) foragg ∈ {6, 7}andomie_offer_curve_year_tech_hora(PK(year, zone, tech, hora_madrid)) foragg=1. Both refreshed monthly post-OMIE-ingest. Sub-200ms — ~40× faster than the live SQL path. Off-boundary ranges and sub-annual agg fall through to the live SQL fallback (which still returns the same 24-row hour-of-day shape foragg=1, but computed on-the-fly). - **Pre-agg drift on
n_slotsfor aliases**: whentech ∈ {all, vre, thermal}is served from the pre-agg,n_slotsisSUM(n_slots over sub-techs)and may double-count slots when multiple sub-techs offer in the same hourly slot. The 8 MWh bins are exact (additive). The live SQL path returnsCOUNT(DISTINCT ts_utc)instead. For exact distinct counts use a single sub-tech. - **Bin boundaries are fixed** at
-10, 0, 5, 20, 50, 100, 180 €/MWh. Chosen against the MIBEL price distribution (negative tail, free tier, mid-merit thermal, scarcity cap). Re-bucket in Excel post-fetch if you need different thresholds — bins are raw MWh, so any aggregation is a sum. - **No percentages by design**:
ED.OFFER.STRATEGYis the function that exposes pct_neg/pct_safety.ED.OFFER.CURVEreturns the 8 raw MWh columns so you can compute any ratio (SUMA(C2:F2)/SUMA(C2:J2)for the ≤20 share, etc.) without pre-baked opinions. - **Granularity asymmetry (Schema A/B vs C)**: pre-March-2025 OMIE files are 60-min granularity (
granularity_min=60); post-March-2025 are mixed 60-min + 15-min (granularity_min=15for the 15-min product). Theqty_mw * granularity_min / 60.0normalisation is applied uniformly — no double-counting — but the share of slots will appear higher post-March-2025 (4× more 15-min slots per hour). - **History cliff**: OMIE bid-curve dataset starts in **January 2018**. Queries with
start < 2018-01-01will return rows starting at the dataset boundary; bucket aggregates will be partial for the boundary bucket. - **Publication lag**: OMIE publishes monthly bid-curve data with ~90-day confidentiality lag. Queries for too-recent dates return 404 with the last-available month in the body (
last_available_mes). - **Special case
tech=interconnect**: filter is INVERTED — keeps only MIEU/MIP/MIE. Tech bucket is the literal string'interconnect'. Since interconnects typically have **zero offered side** in OMIE data, all 8 MWh bins may be 0. - **Expansion semantics**:
vre/all/thermalcause the output to spill ONE row per sub-tech. Single-tech literals keep that literal as the row label. - **Bucket format**:
'00'..'23'for hour-of-day (agg=1, 24 rows),YYYY-MMfor monthly,YYYY-MM-DDfor daily,YYYY-Qnfor quarterly,YYYY-Hnfor semiannual,YYYYfor annual, literal'TOTAL'foragg=7. **DST notes** foragg=1(live path): hour-02 collapses both fall-back instances into bucket'02'(correct — both are hour-2 Madrid); on spring-forward years hour-02 simply has fewer slots that day. Pre-agg path inherits the same behavior from the refresh script.