DEV Community

JP Open Data
JP Open Data

Posted on

Getting Brazil's wholesale electricity price (ONS CMO) in Python – hourly, in English, CC BY 4.0

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
Enter fullscreen mode Exit fullscreen mode
  • 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")
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode
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])
Enter fullscreen mode Exit fullscreen mode

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)"}
Enter fullscreen mode Exit fullscreen mode

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)

⌂ 换网址