India Energy Atlas

Guide

Market price monitoring

Build a lightweight IEX DAM/RTM price monitor that pages when the DAM–RTM spread exceeds a threshold. Under 50 lines of Python, uses /market/iex/spread.

Real-time IEX prices are a leading indicator for scheduling flexible load and charging BESS assets. This guide shows how to poll the spread, detect regime shifts, and hook into Slack / PagerDuty / your incident tool.

Use case

A positive DAM–RTM spread means the Real-Time Market cleared above the Day-Ahead — often a sign of unexpected demand, generation trips, or transmission congestion. Persistent spreads above ₹500/MWh over multiple blocks are a strong arbitrage signal for operators with flexible load or battery assets.

Polling cadence

IEX publishes at 15-minute block resolution. Polling /market/iex/spread every 2 minutes is safely within quota (Starter = 60/min) and catches each new block within 2 minutes of publication.

Dedupe by block

Always key your alert logic on the block integer, not on the poll wall-clock. Polls that land between block boundaries will return the same data — alerting on every poll produces noisy duplicates.

Example: spread alert

spread_monitor.pypython
import os, time
import requests

API = "https://api.energymap.in/developer/v1"
HEADERS = {"X-API-Key": os.environ["ATLAS_API_KEY"]}
SLACK = os.environ["SLACK_WEBHOOK_URL"]

THRESHOLD = 500  # INR/MWh
WINDOW = 3       # number of consecutive blocks above threshold

seen = {}  # date -> last alerted block


def notify(block, start, spread):
    msg = f":zap: IEX DAM-RTM spread {spread:+} INR/MWh at block {block} ({start} IST)"
    requests.post(SLACK, json={"text": msg}, timeout=5)


def poll():
    r = requests.get(f"{API}/market/iex/spread", headers=HEADERS, timeout=10)
    r.raise_for_status()
    payload = r.json()
    date, blocks = payload["date"], payload["blocks"]

    # Did the last `WINDOW` blocks all exceed the threshold?
    tail = blocks[-WINDOW:]
    if len(tail) < WINDOW:
        return
    if all(b["spread"] >= THRESHOLD for b in tail):
        last = tail[-1]
        if seen.get(date) != last["block"]:
            notify(last["block"], last["start"], last["spread"])
            seen[date] = last["block"]


if __name__ == "__main__":
    while True:
        try:
            poll()
        except Exception as e:
            print("poll failed:", e)
        time.sleep(120)

Production considerations

  1. Idempotency. The seen dict is in-memory — fine for a single instance. For HA, swap for Redis or a last_alerted_block column in your DB.
  2. Retries. On 5xx from our API, back off exponentially — don't tight-loop. A 60-second cap is reasonable.
  3. Quota budget. Polling every 2 minutes = 720 requests/day. Well within Starter's 10,000/day. If you monitor multiple metrics (spread + carbon + frequency) from one instance, share a single HTTP client and batch the work into one minute budget.
  4. Key hygiene. This is a server-side cron — pin an IP allow-list on the key per the Authentication guide.