Beta — Data under validation. Values may contain errors.

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).

Capture price

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

NameTypeDefaultDescription
techstring"all"Tech filter. Default "all" (= every classified real tech). Same 18-value enum as ED.OMIE.BIDS / ED.OFFER.STRATEGY.
startdateYYYY-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.
enddateYYYY-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).
agg1..76**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. espais IN ('ES','MI') (default), ptpais IN ('PT','MI'), iberianpais IN ('ES','PT','MI') (includes the MIBEL pais='MI' bucket).
headers0 | 10Set 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=1 semantic (ADR-011 — breaking change)**: agg=1 was 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). Use ED.OFFER.HEATMAP if 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 start and end are omitted AND agg ∈ {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). When agg ∈ {2..5} (sub-annual) the legacy end − 12 months default is kept. Any explicit start/end overrides the default unchanged.
  • **Pre-aggregation routing**: when agg ∈ {1, 6, 7} AND start = YYYY-01-01 AND end = (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)) for agg ∈ {6, 7} and omie_offer_curve_year_tech_hora (PK (year, zone, tech, hora_madrid)) for agg=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 for agg=1, but computed on-the-fly).
  • **Pre-agg drift on n_slots for aliases**: when tech ∈ {all, vre, thermal} is served from the pre-agg, n_slots is SUM(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 returns COUNT(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.STRATEGY is the function that exposes pct_neg/pct_safety. ED.OFFER.CURVE returns 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=15 for the 15-min product). The qty_mw * granularity_min / 60.0 normalisation 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-01 will 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/thermal cause 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-MM for monthly, YYYY-MM-DD for daily, YYYY-Qn for quarterly, YYYY-Hn for semiannual, YYYY for annual, literal 'TOTAL' for agg=7. **DST notes** for agg=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.

Related functions