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
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
- Idempotency. The
seendict is in-memory — fine for a single instance. For HA, swap for Redis or alast_alerted_blockcolumn in your DB. - Retries. On 5xx from our API, back off exponentially — don't tight-loop. A 60-second cap is reasonable.
- 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.
- Key hygiene. This is a server-side cron — pin an IP allow-list on the key per the Authentication guide.
