Beta — Data under validation. Values may contain errors.

ED.PCT.AT.PRICE

Share of a volume metric whose hours fall on one side of a price threshold.

Generic + metrics

Signature

ED.PCT.AT.PRICE(volume, threshold, [op], [price], [start], [end], [zone], [agg], [unit], [headers])

Description

How much of a volume metric (generation, demand, cross-border flow, programmed) was produced/consumed in hours where a price series satisfied op threshold. Generalises questions like "how much solar PV happened at ≤ 0 EUR/MWh?" or "what fraction of demand was served below 10 EUR/MWh?".

The function joins the volume and the price on the same civil hour, evaluates price <op> threshold per hour, and reduces over the requested window. Result is a share of the total volume by default (unit="pct"), the absolute energy below the threshold (unit="mwh"), or the count of qualifying hours (unit="hours").

**Realized vs ex-ante**: pass volume="gen_*" for the realized (post-curtailment) view, or volume="program_pbf_*" for the ex-ante OMIE schedule view (matches ED.CAPTURE method=1). program_p48_* gives the post-RRTT view (after technical redispatch).

Parameters

NameTypeDefaultDescription
volume*string—Volume metric. Accepted families: gen_* (e.g. gen_solar_pv, gen_wind, gen_total), demand_*, xborder_*, program_pbf_*, program_p48_*, program_phfc_*. Use program_pbf_* for the ex-ante OMIE schedule view (matches ED.CAPTURE method=1).
threshold*number—Price threshold in EUR/MWh. Negative values are allowed (e.g. -10 for negative-price hours).
op"lte" | "lt" | "gte" | "gt""lte"Comparison: lte (default, ≤), lt (<), gte (≥), gt (>). Hours where the price exactly equals the threshold are included with lte/gte.
pricestring"dayahead"Price series to compare against. dayahead (default), pvpc, pvpc_final, intraday_s1_price / intraday_s2_price / intraday_s3_price, imbalance_price_up, imbalance_price_down.
startdate—Start. Defaults: omit end for today (Madrid, with a small overnight cutoff), omit start for 30 days before end.
enddate—End. Omit for today.
zone"ES" | "PT""ES"MIBEL zone: ES (default) or PT. Any other zone answers 400. The coverage rule below assumes a source that publishes every slot even when the value repeats the previous one — true of ESIOS/OMIE, and not of the ENTSO-E feed the non-Iberian zones come from, where a missing row is usually a flat value we do know.
agg2-7 | string7 (total)Aggregation is controlled by agg: 0=native, 5min/10min/15min, 1=hourly, 2=daily (default), 3=monthly, 4=quarterly, 5=semiannual, 6=annual, 7=total. Default is total (single number for the window); pass 3 for a monthly spill, etc.
unit"pct" | "mwh" | "hours""pct"pct (default) returns the share of total volume that satisfied the threshold (%). mwh returns the absolute energy below/above the threshold per bucket. hours returns the count of qualifying civil hours in each bucket.
headers0 | 10Set 1 for header row (only applies when agg returns more than one row).

* Required.

Returns

Number when agg="total" (default), otherwise a spill array — [period, value] rows. Units: % / MWh / hours depending on unit. With the default unit="pct" a bucket that holds no volume to divide by comes back as an empty cell — the share is undefined, not 0 % — and as #N/A when agg=7 (or "T") collapses the range to a single number. A window with no row at all has nothing to count either, so mwh and hours are #N/A too. Coverage: an hour counts only when both the volume series and the price series published every slot that hour has at their own cadence for that day (hourly, 15-min, 10-min or 5-min, read from the data, never from a date), and an hour with volume but no price is seen but not used. A bucket with rows and no covered hour is an empty cell, or #N/A when agg=7 collapses the range, in every unit. A measured 0 is untouched: over covered hours, a bucket where none met the threshold still returns 0 % / 0 MWh / 0 hours.

Examples

=ED.PCT.AT.PRICE("gen_solar_pv", 0, "lte", "dayahead", "2026-01-01", "2026-04-30")— % solar PV at price ≤ 0 EUR/MWh — realized (post-curtailment)
=ED.PCT.AT.PRICE("program_pbf_solar_pv", 0, "lte", "dayahead", "2026-01-01", "2026-04-30")— Same question in the OMIE PBF (ex-ante) view — equivalent to ED.CAPTURE method=1
=ED.PCT.AT.PRICE("gen_solar_pv", 5, "lte", "dayahead", "2026-01-01", "2026-04-30")— % solar PV at price ≤ 5 EUR/MWh (~65%)
=ED.PCT.AT.PRICE("gen_solar_pv", 0, "lte", "dayahead", "2026-01-01", "2026-04-30", "ES", 7, "mwh")— Total solar PV MWh at price ≤ 0 EUR/MWh
=ED.PCT.AT.PRICE("gen_solar_pv", 0, "lte", "dayahead", "2026-01-01", "2026-04-30", "ES", 3, "hours", 1)— Hours per month where dayahead ≤ 0 EUR/MWh, with headers
=ED.PCT.AT.PRICE("demand_real", 100, "gte", "dayahead", "2025-01-01", "2025-12-31")— % real demand served at price ≥ 100 EUR/MWh — peak-price exposure

Notes

  • **Realized (gen_*) vs ex-ante (program_pbf_*) vs post-RRTT (program_p48_*)**: each volume family gives a different denominator. gen_* reflects what actually flowed (already net of technical curtailment). program_pbf_* is the OMIE day-ahead schedule (matches ED.CAPTURE method=1). program_p48_* is post-RRTT (after Restricciones Técnicas).
  • **MW vs MWh aggregation**: gen_* / demand_* / xborder_* are stored as MW power, so the energy of a civil hour is the sum of the samples that hour holds divided by the samples that series publishes per hour on that civil day — the mean power, multiplied by the UTC hours the bucket spans. program_pbf_* / program_p48_* / program_phfc_* are stored as MWh per slot and are summed directly. Both code paths ultimately produce hourly MWh consistently — unit="mwh" numbers between gen_solar_pv and program_pbf_solar_pv are comparable in scale (~1-2% difference). Dividing by the day's slots rather than averaging is also what makes the repeated civil hour of the October DST change come out right: that bucket holds two UTC hours, and it used to report half the energy that flowed through it (2025-10-26 02:00, gen_wind: 7 401 MWh where 14 802 were delivered).
  • **Threshold operator**: lte/gte include exact equality. lt/gt are strict. For "hours at ≤ 0" use lte and threshold=0.
  • **hours unit**: counts civil hours (always 60-min buckets) where the price satisfied the operator, regardless of how much volume there was that hour — but only among the hours both series covered, so an hour whose volume or price series published just part of its slots is not counted here either (see **Gaps and coverage** below). Useful for documenting market stress separately from production.
  • **Gaps and coverage**: three cases come back empty (#N/A for the agg=7 scalar, an empty cell inside a spill) rather than as a number. **(1) A bucket with no row at all**, whatever the unit: "0 hours above the threshold" over a window the source has not published a single row for is a claim about nothing, not a measurement — in practice only the total (agg=7) reaches this, because a bucket of a spill exists precisely because a row created it. **(2) unit="pct" with a total volume of 0**: the share is the qualifying volume divided by the total volume of the bucket, and there is nothing to divide by. A bucket whose total volume is zero or negative — xborder_* are signed net balances, so a net exporter adds up below zero — has no defined share and returns empty. **(3) A bucket whose rows never add up to a covered hour**: an hour is counted only when the volume series *and* the price series each published every slot that hour has at their own cadence for that civil day, so an hour holding just some of its slots — or holding volume and no price at all — leaves the numerator and the denominator alike instead of being read as a smaller number, and a bucket left with no covered hour is empty in every unit. The cadence is read from the data and not from a date (ESIOS turned the programs back to hourly for the single day 2025-12-31, and the intraday_s* prices for 2025-06-16..19), and the repeated civil hour of the October DST change needs twice as many rows because it holds two UTC hours. All three replace the fabricated zero ED.RANGE stopped emitting in ADR-029, ED.CAPACITY.RANGE in 8.7.4.0 and ED.CURTAIL.RANGE in 8.7.5.0. A measured 0 is untouched: over covered hours, a bucket where none of them met the threshold returns 0 % / 0 MWh / 0 hours. The REST response carries the per-bucket detail in a coverage field, [bucket, hours_used, hours_seen] in the same order as the values — and with no header row of its own, so under headers=1 it is values[i+1] that pairs with coverage[i] — plus a top-level gap_note spelling out what an empty value means. With agg=7 (or "T") the reply is a single number and there is no coverage array at all: the same two counters arrive as top-level hours_used and hours_seen. Both are additive: values keeps its two columns, so the shape of the spill in Excel is unchanged.

Related functions