MISO Data Guide
===============
Overview
--------
The Midcontinent Independent System Operator (MISO) operates one of the world's largest energy markets, serving 45
million people across 15 U.S. states and Manitoba, Canada. ISO-DART provides comprehensive access to MISO's Data
Exchange REST API for pricing, load, generation, and market operations data.
Quick Reference
---------------
.. list-table::
:header-rows: 1
:widths: 25 20 25 30
* - Data Category
- Update Frequency
- Historical Availability
- API Requirement
* - LMP (DA/RT)
- 5-min/Hourly
- 2009-present
- Pricing API Key
* - MCP
- 5-min/Hourly
- 2009-present
- Pricing API Key
* - Load/Demand
- 5-min/Hourly
- 2010-present
- LGI API Key
* - Fuel Mix
- 5-min intervals
- 2014-present
- LGI API Key
* - Generation
- Hourly
- 2010-present
- LGI API Key
MISO Markets
------------
MISO operates an integrated day-ahead and real-time market system:
Day-Ahead Market
~~~~~~~~~~~~~~~~
* **Purpose**: Schedule generation and manage congestion for next operating day
* **Timeline**: Closes at 11 AM CT, results posted by 4 PM CT
* **Resolution**: Hourly intervals
* **Products**: Energy, Operating Reserves, Ramp Capability
Real-Time Market
~~~~~~~~~~~~~~~~
* **Purpose**: Balance supply and demand in real-time
* **Timeline**: Continuous 5-minute dispatch
* **Resolution**: 5-minute intervals
* **Products**: Energy, Regulation, Spinning/Supplemental Reserves
API Access & Authentication
----------------------------
MISO uses a REST API with two separate products requiring different API keys:
Pricing API
~~~~~~~~~~~
Covers:
* LMP data (ExAnte/ExPost)
* Market Clearing Prices (MCP)
* Ancillary Services pricing
Get your key at: https://data-exchange.misoenergy.org/
Load, Generation & Interchange (LGI) API
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Covers:
* Load and demand data
* Generation by fuel type
* Fuel on the margin
* Interchange flows
* Outages and constraints
Get your key at: https://data-exchange.misoenergy.org/
Configuration Setup
~~~~~~~~~~~~~~~~~~~
Create ``user_config.ini`` in your project root:
.. code-block:: ini
[miso]
pricing_api_key = your-pricing-api-key-here
lgi_api_key = your-lgi-api-key-here
data_dir = data/MISO
max_retries = 3
timeout = 30
Or use the template generator:
.. code-block:: python
from lib.iso.miso import MISOConfig
# Creates template config file
MISOConfig.create_template_ini()
# Edit user_config.ini with your keys
Available Data Types
--------------------
1. Pricing Data
~~~~~~~~~~~~~~~
Locational Marginal Prices (LMP)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
MISO LMP data is available in four variants:
* **Day-Ahead ExAnte**: Forward-looking DA market prices
* **Day-Ahead ExPost**: Final settled DA market prices
* **Real-Time ExAnte**: 5-minute ahead RT prices
* **Real-Time ExPost**: Final settled RT prices
**Example - Download DA ExAnte LMP**:
.. code-block:: python
from datetime import date
from lib.iso.miso import MISOClient, MISOConfig
# Load configuration with API keys
config = MISOConfig.from_ini_file()
client = MISOClient(config)
# Download LMP data
data = client.get_lmp(
lmp_type='da_exante',
start_date=date(2024, 1, 1),
duration=30,
node='ALTW.WELLS1' # Optional: filter by node
)
# Save to CSV
if data:
client.save_to_csv(data, 'miso_da_exante_lmp.csv')
**Output**: ``data/MISO/miso_da_exante_lmp_2024-01-01.csv``
**Key Columns**:
* ``node``: Pricing node identifier
* ``value``: LMP in $/MWh
* ``lmp``: Total LMP
* ``mcc``: Marginal Congestion Component
* ``mlc``: Marginal Loss Component
* ``interval``: Date/time of interval
Market Clearing Prices (MCP)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Ancillary service market clearing prices by zone and product.
.. code-block:: python
# ASM Day-Ahead ExAnte MCP
data = client.get_mcp(
mcp_type='asm_da_exante',
start_date=date(2024, 1, 1),
duration=30,
zone='MISO', # Optional: filter by zone
product='RegUp' # Optional: filter by product
)
**MCP Types**:
* ``asm_da_exante``: ASM Day-Ahead ExAnte
* ``asm_da_expost``: ASM Day-Ahead ExPost
* ``asm_rt_exante``: ASM Real-Time ExAnte
* ``asm_rt_expost``: ASM Real-Time ExPost
* ``asm_rt_summary``: ASM Real-Time Summary
**Ancillary Service Products**:
* **RegUp**: Regulation Up
* **RegDown**: Regulation Down
* **Spin**: Spinning Reserve
* **Supp**: Supplemental Reserve
2. Load & Demand Data
~~~~~~~~~~~~~~~~~~~~~~
System Demand
^^^^^^^^^^^^^
Multiple demand data types available:
.. code-block:: python
# Real-Time Actual Load
data = client.get_demand(
demand_type='rt_actual',
start_date=date(2024, 1, 1),
duration=30,
region='MISO', # Optional: filter by region
time_resolution='daily' # or 'hourly', '5min'
)
**Demand Types**:
* ``da_demand``: Day-Ahead scheduled demand
* ``rt_forecast``: Real-Time demand forecast
* ``rt_actual``: Real-Time actual load
* ``rt_state_estimator``: State estimator load
Load Forecasts
^^^^^^^^^^^^^^
Medium-term load forecasts:
.. code-block:: python
data = client.get_load_forecast(
start_date=date(2024, 1, 1),
duration=30,
region='MISO',
time_resolution='daily'
)
**Use Cases**:
* Planning and operations
* Load shape analysis
* Forecast accuracy studies
3. Generation Data
~~~~~~~~~~~~~~~~~~
Fuel Mix
^^^^^^^^
Real-time fuel on the margin (5-minute resolution):
.. code-block:: python
data = client.get_fuel_mix(
start_date=date(2024, 1, 1),
duration=30,
region='MISO',
fuel_type='Coal' # Optional filter
)
**Fuel Types**: Coal, Gas, Nuclear, Wind, Solar, Hydro, Other
Generation by Type
^^^^^^^^^^^^^^^^^^
Various generation data products:
.. code-block:: python
# Real-Time fuel type generation
data = client.get_generation(
gen_type='rt_fuel_type',
start_date=date(2024, 1, 1),
duration=30
)
**Generation Types**:
* ``da_cleared_physical``: DA cleared physical generation
* ``da_cleared_virtual``: DA cleared virtual generation
* ``da_fuel_type``: DA generation by fuel type
* ``da_offered_ecomax``: DA offered economic max
* ``rt_cleared``: RT cleared generation
* ``rt_fuel_type``: RT generation by fuel type
* ``rt_fuel_margin``: RT fuel on margin
4. Interchange Data
~~~~~~~~~~~~~~~~~~~~
Net interchange flows between MISO and neighboring systems:
.. code-block:: python
data = client.get_interchange(
interchange_type='rt_net_actual',
start_date=date(2024, 1, 1),
duration=30,
region='MISO',
adjacent_ba='PJM' # Optional filter
)
**Interchange Types**:
* ``da_net_scheduled``: DA scheduled interchange
* ``rt_net_actual``: RT actual interchange
* ``rt_net_scheduled``: RT scheduled interchange
* ``historical``: Historical net scheduled
5. Outages & Constraints
~~~~~~~~~~~~~~~~~~~~~~~~~
Binding Constraints
^^^^^^^^^^^^^^^^^^^
Real-time binding transmission constraints:
.. code-block:: python
data = client.get_binding_constraints(
start_date=date(2024, 1, 1),
duration=30,
interval='all' # or specific interval
)
**Use Cases**:
* Congestion analysis
* Price driver identification
* Transmission planning
Outage Data
^^^^^^^^^^^
Generation outage forecasts and actual outages:
.. code-block:: python
# Outage forecast
data = client.get_outages(
outage_type='forecast',
start_date=date(2024, 1, 1),
duration=30
)
# Real-time outages
data = client.get_outages(
outage_type='rt_outage',
start_date=date(2024, 1, 1),
duration=30
)
Common Workflows
----------------
1. Daily Price Update
~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from datetime import date, timedelta
from lib.iso.miso import MISOClient, MISOConfig
def daily_miso_update():
"""Download yesterday's MISO data."""
yesterday = date.today() - timedelta(days=1)
config = MISOConfig.from_ini_file()
client = MISOClient(config)
# Download LMP
lmp_data = client.get_lmp('da_exante', yesterday, 1)
if lmp_data:
client.save_to_csv(lmp_data, f'miso_lmp_{yesterday}.csv')
print(f"✓ Downloaded LMP for {yesterday}")
# Download fuel mix
fuel_data = client.get_fuel_mix(yesterday, 1)
if fuel_data:
client.save_to_csv(fuel_data, f'miso_fuel_{yesterday}.csv')
print(f"✓ Downloaded fuel mix for {yesterday}")
if __name__ == '__main__':
daily_miso_update()
2. Fuel Mix Analysis
~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
import pandas as pd
import matplotlib.pyplot as plt
from datetime import date
from lib.iso.miso import MISOClient, MISOConfig
# Download data
config = MISOConfig.from_ini_file()
client = MISOClient(config)
data = client.get_fuel_mix(
start_date=date(2024, 1, 1),
duration=31
)
client.save_to_csv(data, 'miso_fuel_jan2024.csv')
# Load and analyze
df = pd.read_csv('data/MISO/miso_fuel_jan2024.csv')
# Convert to datetime
df['datetime'] = pd.to_datetime(df['interval'])
# Group by fuel type and date
daily_by_fuel = df.groupby([
df['datetime'].dt.date,
'fuelType'
])['value'].sum().unstack()
# Plot stacked area chart
fig, ax = plt.subplots(figsize=(14, 8))
daily_by_fuel.plot.area(ax=ax, alpha=0.8)
ax.set_xlabel('Date')
ax.set_ylabel('Generation (MWh)')
ax.set_title('MISO Fuel Mix - January 2024')
ax.legend(title='Fuel Type', bbox_to_anchor=(1.05, 1))
plt.tight_layout()
plt.savefig('miso_fuel_mix_jan2024.png', dpi=300)
3. Load Correlation Study
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
import pandas as pd
import numpy as np
from scipy.stats import pearsonr
# Load LMP and demand data
lmp_df = pd.read_csv('data/MISO/miso_lmp_jan2024.csv')
load_df = pd.read_csv('data/MISO/miso_rt_actual_load_jan2024.csv')
# Merge on timestamp
lmp_df['datetime'] = pd.to_datetime(lmp_df['interval'])
load_df['datetime'] = pd.to_datetime(load_df['interval'])
# Get hourly averages
lmp_hourly = lmp_df.groupby(
lmp_df['datetime'].dt.floor('H')
)['lmp'].mean()
load_hourly = load_df.groupby(
load_df['datetime'].dt.floor('H')
)['value'].mean()
# Calculate correlation
correlation, p_value = pearsonr(
lmp_hourly.values,
load_hourly.values
)
print(f"Price-Load Correlation: {correlation:.3f}")
print(f"P-value: {p_value:.6f}")
# Scatter plot
plt.figure(figsize=(10, 8))
plt.scatter(load_hourly.values, lmp_hourly.values, alpha=0.5)
plt.xlabel('System Load (MW)')
plt.ylabel('LMP ($/MWh)')
plt.title(f'MISO Load vs. Price\nCorrelation: {correlation:.3f}')
plt.grid(True, alpha=0.3)
# Add trend line
z = np.polyfit(load_hourly.values, lmp_hourly.values, 1)
p = np.poly1d(z)
plt.plot(load_hourly.values, p(load_hourly.values), "r--", linewidth=2)
plt.tight_layout()
plt.savefig('miso_price_load_correlation.png', dpi=300)
Data Quality & Limitations
---------------------------
Update Schedule
~~~~~~~~~~~~~~~
* **DA ExAnte LMP**: Available ~4 PM CT day before operating day
* **DA ExPost LMP**: Available ~4 PM CT day after operating day
* **RT ExAnte LMP**: Available within 5 minutes of interval
* **RT ExPost LMP**: Available ~1 hour after interval
* **Fuel Mix**: Updated every 5 minutes
Historical Data
~~~~~~~~~~~~~~~
.. list-table::
:header-rows: 1
:widths: 40 20 40
* - Data Type
- Start Date
- Notes
* - LMP (All types)
- January 2009
- Complete history
* - MCP
- January 2009
- Complete history
* - Load/Demand
- January 2010
- Complete history
* - Fuel Mix
- March 2014
- When tracking began
* - Generation Data
- January 2010
- Varies by product
Known Issues
~~~~~~~~~~~~
1. **API Rate Limits**: 100 requests/minute (adjustable in config)
2. **Large Date Ranges**: Use pagination for requests over 90 days
3. **Node Names**: May change over time; check MISO node list
4. **Time Zones**: All times in Central Time (CT)
Rate Limiting
~~~~~~~~~~~~~
.. code-block:: python
from lib.iso.miso import MISOConfig
# Adjust rate limit delay (seconds between requests)
config = MISOConfig(
rate_limit_delay=0.8 # Default is 0.6
)
client = MISOClient(config)
Performance Tips
----------------
1. Use Filters
~~~~~~~~~~~~~~
Filter data at the API level to reduce response size:
.. code-block:: python
# Better: filter by node
data = client.get_lmp(
lmp_type='da_exante',
start_date=date(2024, 1, 1),
duration=30,
node='SPECIFIC.NODE' # Only this node
)
# Avoid: downloading all nodes then filtering
data = client.get_lmp('da_exante', date(2024, 1, 1), 30)
filtered = {k: v for k, v in data.items() if 'SPECIFIC.NODE' in str(v)}
2. Adjust Time Resolution
~~~~~~~~~~~~~~~~~~~~~~~~~~
Use coarser resolution when possible:
.. code-block:: python
# For daily analysis, use daily resolution
data = client.get_demand(
demand_type='rt_actual',
start_date=date(2024, 1, 1),
duration=30,
time_resolution='daily' # Much faster than '5min'
)
3. Batch Processing
~~~~~~~~~~~~~~~~~~~
Process multiple months in a loop:
.. code-block:: python
from datetime import date
from dateutil.relativedelta import relativedelta
start = date(2024, 1, 1)
for month in range(12):
month_start = start + relativedelta(months=month)
month_end = month_start + relativedelta(months=1)
days = (month_end - month_start).days
print(f"Processing {month_start.strftime('%B %Y')}")
data = client.get_lmp(
lmp_type='da_exante',
start_date=month_start,
duration=days
)
if data:
filename = f"miso_lmp_{month_start.strftime('%Y%m')}.csv"
client.save_to_csv(data, filename)
Troubleshooting
---------------
Issue: "Authentication failed - check API key"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Solutions**:
1. Verify API key is correct in ``user_config.ini``
2. Check you're using the right key for the right product:
* Pricing API key → LMP, MCP data
* LGI API key → Load, generation, interchange data
3. Ensure API subscription is active at https://data-exchange.misoenergy.org/
Issue: "No data returned"
~~~~~~~~~~~~~~~~~~~~~~~~~~
**Solutions**:
* Check date range is valid (not in future)
* Verify data exists for that period
* Try a specific node/zone filter
* Check MISO API status: https://data-exchange.misoenergy.org/
Issue: Rate limit errors (429)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Solutions**:
.. code-block:: python
# Increase delay between requests
config = MISOConfig(
rate_limit_delay=1.0 # Increase from default 0.6
)
# Or reduce requests per batch
# Download in smaller chunks
Issue: Timeout errors
~~~~~~~~~~~~~~~~~~~~~
**Solutions**:
.. code-block:: python
# Increase timeout
config = MISOConfig(
timeout=60 # Default is 30 seconds
)
# Or break into smaller date ranges
Additional Resources
--------------------
* `MISO Data Exchange `_ - API portal
* `MISO Markets `_ - Market information
* `MISO Real-Time Dashboard `_
* `MISO Market Reports `_
* `API Documentation `_
Next Steps
----------
* :doc:`api-keys` - Detailed API key setup guide
* :doc:`pricing` - MISO Pricing Data Deep Dive
* :doc:`load-gen` - Load and Generation Analysis
* :doc:`../index` - Return to ISO Index