Brazil runs one of the world's largest hydro-dominated power systems, and its grid operator ONS publishes the half-hourly CMO (Custo Marginal de Operação, marginal operating cost) for each submarket as open data under CC BY 4.0. The CMO is the basis of the PLD spot price. Here's how to use it.
The raw data
Dataset page: https://dados.ons.org.br/dataset/cmo-semi-horario
One CSV per year, semicolon-separated:
id_subsistema;nom_subsistema;din_instante;val_cmo
SE;SUDESTE;2026-11-08 23:30:00;27.75
N;NORTE;2026-11-08 23:30:00;103.95
-
id_subsistema:SE(Southeast/Center-West),S(South),NE(Northeast),N(North) -
din_instante: start of the half-hour, Brasília time (UTC-03:00), no offset in the string -
val_cmo: BRL/MWh
Hourly averages with only the standard library
import csv, io, urllib.request
from collections import defaultdict
from datetime import datetime, timedelta, timezone
URL = "https://ons-aws-prod-opendata.s3.amazonaws.com/dataset/cmo_tm/CMO_SEMIHORARIO_2026.csv"
BRT = timezone(timedelta(hours=-3))
text = urllib.request.urlopen(URL, timeout=90).read().decode("utf-8-sig")
hourly = defaultdict(list)
for r in csv.DictReader(io.StringIO(text), delimiter=";"):
if not r["val_cmo"]:
continue
t = datetime.strptime(r["din_instante"][:19], "%Y-%m-%d %H:%M:%S").replace(tzinfo=BRT)
hourly[(r["id_subsistema"], t.replace(minute=0))].append(float(r["val_cmo"]))
latest = max(k[1] for k in hourly)
for zone in ("SE", "S", "NE", "N"):
v = hourly[(zone, latest)]
print(zone, latest.isoformat(), round(sum(v) / len(v), 2), "BRL/MWh")
Gotchas:
- The file for the current year is ~2 MB and grows daily; a window that crosses New Year needs two files.
- The latest data is usually through the previous day.
- CMO ≠ PLD: the official PLD settled by CCEE applies regulatory floor/ceiling values, so they differ at the extremes.
Or call a ready-made API
If you'd rather not host this, I published it as a free Apify Actor: latam-electricity-prices. English zone names, half-hourly/hourly/daily aggregation with min/max, ISO timestamps in local time and UTC, and licence/attribution on every row. There's no per-result price; you need a free Apify account for a token.
curl -s -X POST \
"https://api.apify.com/v2/acts/grinning_naiad_h0w~latam-electricity-prices/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"subsystems":"SE,N","granularity":"daily","sinceDays":30}'
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("grinning_naiad_h0w/latam-electricity-prices").call(
run_input={"granularity": "hourly", "sinceDays": 2}
)
rows = list(client.dataset(run["defaultDatasetId"]).iterate_items())
print(rows[0])
A real daily row:
{"bidding_zone":"SE","bidding_zone_name":"Southeast/Center-West","period_start":"2026-11-08T00:00:00-03:00","granularity":"daily","price":14.73,"price_min":0,"price_max":32.11,"samples":48,"currency":"BRL","unit":"BRL/MWh","license":"Creative Commons Attribution 4.0 (CC BY 4.0)"}
Other Latin American markets?
I'd like to add Colombia, Mexico and Chile, but I only add a source when its official terms clearly allow reuse, and I couldn't confirm that yet. If you know where those licences are published, please comment.
Data: Operador Nacional do Sistema Elétrico (ONS), dados.ons.org.br, CC BY 4.0.
Top comments (0)