Introduction to Electricity Markets#
Information |
Details |
---|---|
Learning Objectives |
• Understand why electricity markets replaced regulated monopolies |
Prerequisites |
Linear programming with PuLP, basic economics, power systems fundamentals |
Estimated Time |
120 minutes |
Topics |
Market clearing, merit order, LMP, two-settlement systems, market power |
Introduction#
Electricity markets coordinate billions of dollars in daily transactions while maintaining the delicate physics of a balanced grid. Unlike other commodities, electricity cannot be economically stored at scale and must be produced the instant it’s consumed. This fundamental constraint, combined with the critical nature of electricity supply, creates unique market design challenges that don’t exist in other industries.
For most of the 20th century, electricity was provided by vertically integrated utilities that owned generation, transmission, and distribution assets. Regulators set rates to cover costs plus a fair return on investment. While this model ensured universal service and reliability, it provided little incentive for efficiency or innovation. Generation decisions were made through central planning rather than competition, often leading to overcapacity and higher costs for consumers.
The restructuring movement that began in the 1990s introduced competition into electricity generation while maintaining regulated monopolies for transmission and distribution. This hybrid model aims to harness market forces where competition is viable while preserving regulation where natural monopolies exist. Today’s electricity markets must balance multiple objectives: minimizing costs, ensuring reliability, providing fair compensation to generators, and creating appropriate investment signals for new capacity.
This lesson introduces the fundamental concepts that enable competitive electricity markets to function efficiently. You’ll learn how prices are discovered through the interaction of supply and demand, why prices vary by location due to transmission constraints, and how financial settlements work across multiple time horizons. These concepts form the foundation for understanding modern grid operations and the integration of renewable energy resources.
Setting Up#
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pulp import *
import warnings
warnings.filterwarnings('ignore')
# Set display precision for cleaner output
np.set_printoptions(precision=2, suppress=True)
pd.options.display.float_format = '{:.2f}'.format
# Plotting style
plt.style.use('seaborn-v0_8-darkgrid')
plt.rcParams['figure.figsize'] = (10, 6)
plt.rcParams['font.size'] = 11
Economic Efficiency and Market Clearing#
Markets exist to allocate scarce resources efficiently. In electricity, this means dispatching the lowest-cost generators to meet demand, a principle known as economic dispatch. The market clearing process determines both which generators produce power and the price all generators receive. This single clearing price, despite generators having different costs, creates powerful incentives for efficiency and innovation.
To understand market clearing, consider how supply and demand interact. Generators submit offers indicating how much they’re willing to produce at different prices, while consumers (through their retailers or utilities) express demand. The market operator stacks generators from lowest to highest cost, creating the supply curve. The intersection with demand determines both the clearing price and which generators are dispatched.

The shaded areas in the figure represent economic surplus; the total benefit created by the market. Consumer surplus (blue) captures the value consumers receive above what they pay, while producer surplus (red) represents generator profits above their costs. Competitive markets maximize this total surplus, ensuring resources are allocated to their highest-value uses. In contrast, regulated monopolies may set prices that create deadweight loss, reducing overall economic efficiency.
Merit Order Dispatch#
The foundation of electricity market operations is the merit order principle: generators are dispatched in order of increasing marginal cost until demand is met. This ensures the lowest-cost resources are always used first, minimizing total production costs. The last generator needed to meet demand, called the marginal unit, sets the market clearing price that all generators receive.
This uniform pricing might seem counterintuitive—why should a low-cost generator receive the same price as an expensive one? The answer lies in incentive compatibility. Uniform pricing encourages generators to bid their true costs, as bidding higher risks not being dispatched while bidding lower means selling at a loss. This truthful bidding is essential for efficient market outcomes.
# Create generator data
generators = pd.DataFrame({
'name': ['Nuclear', 'Coal_1', 'Coal_2', 'Gas_CC', 'Gas_CT'],
'capacity': [400, 300, 250, 200, 150],
'marginal_cost': [10, 25, 28, 35, 55]
})
# Sort by marginal cost to create merit order
generators = generators.sort_values('marginal_cost')
generators['cumulative_capacity'] = generators['capacity'].cumsum()
print("Merit Order Stack:")
print(generators[['name', 'capacity', 'marginal_cost']])
Merit Order Stack:
name capacity marginal_cost
0 Nuclear 400 10
1 Coal_1 300 25
2 Coal_2 250 28
3 Gas_CC 200 35
4 Gas_CT 150 55
Now let’s implement the market clearing algorithm. The market operator collects all supply offers, sorts them by price, and dispatches generators until demand is satisfied. This simple mechanism, despite its elegance, efficiently coordinates thousands of generators in real power systems.
def clear_market(generators, demand):
"""Simple market clearing algorithm."""
generators = generators.sort_values('marginal_cost')
cumulative = 0
dispatch = []
clearing_price = 0
for _, gen in generators.iterrows():
if cumulative < demand:
dispatched = min(gen['capacity'], demand - cumulative)
dispatch.append({
'name': gen['name'],
'dispatch': dispatched,
'marginal_cost': gen['marginal_cost']
})
cumulative += dispatched
if cumulative >= demand:
clearing_price = gen['marginal_cost']
break
return pd.DataFrame(dispatch), clearing_price
# Run market clearing
demand = 850 # MW
dispatch, price = clear_market(generators, demand)
print(f"\nDemand: {demand} MW")
print(f"Clearing Price: ${price}/MWh\n")
print("Dispatch Results:")
print(dispatch)
Demand: 850 MW
Clearing Price: $28/MWh
Dispatch Results:
name dispatch marginal_cost
0 Nuclear 400 10
1 Coal_1 300 25
2 Coal_2 150 28

Producer Surplus and Market Incentives#
Producer surplus, which is the difference between the market price and a generator’s marginal cost, provides crucial economic signals. Efficient generators with costs below the clearing price earn substantial profits, incentivizing investment in low-cost technologies. Conversely, expensive generators that rarely run earn little profit, signaling they may need to retire or improve efficiency.
This profit mechanism might appear to reward generators unfairly, especially when low-cost units earn high profits during scarcity. However, these profits serve an essential function: they signal where new investment is needed and compensate generators for their fixed costs. Without adequate producer surplus, generators cannot recover their capital investments, leading to underinvestment and eventual reliability problems.
# Calculate producer surplus
dispatch['producer_surplus'] = (price - dispatch['marginal_cost']) * dispatch['dispatch']
total_surplus = dispatch['producer_surplus'].sum()
print("Producer Surplus by Generator:")
for _, row in dispatch.iterrows():
print(f" {row['name']:8} ${row['producer_surplus']:,.0f}")
print(f"\nTotal Producer Surplus: ${total_surplus:,.0f}")
Producer Surplus by Generator:
Nuclear $7,200
Coal_1 $900
Coal_2 $0
Total Producer Surplus: $8,100
Locational Marginal Pricing#
So far, we’ve assumed electricity can flow freely to any location, but real transmission networks have physical limits. When transmission lines reach their capacity, power cannot flow from cheap generators to expensive load centers, causing prices to diverge across locations. This spatial price variation, captured through Locational Marginal Pricing (LMP), sends crucial economic signals about where generation and transmission investments are most valuable.
LMP represents the marginal cost of serving one additional MW of load at a specific location, accounting for generation costs, transmission constraints, and losses. When no transmission constraints bind, LMPs across the system converge to a single energy price. However, when constraints bind, LMPs separate, with higher prices in import-constrained areas and lower prices where generation exceeds local demand.
To understand how transmission constraints create price separation, we will solve a simple 3-bus optimal power flow problem. This example shows how LMPs emerge naturally from the optimization’s dual variables.
Our network has three buses in a triangle configuration:
North: Cheap generation available (300 MW @ $20/MWh)
Central: Expensive backup generation (100 MW @ $50/MWh) and 150 MW load
South: 150 MW load only
The optimization minimizes total generation cost while respecting power balance at each bus and transmission line limits.
Scenario 1: No Congestion#
First, let’s solve the system with high transmission limits that don’t constrain the optimal flow pattern. In this case, all power can flow freely from the cheapest generator to all loads.
Generation:
North: 300.0 MW @ $20/MWh
Central: 0.0 MW @ $50/MWh
Locational Marginal Prices (from dual variables):
North: $20.00/MWh
Central: $20.00/MWh
South: $20.00/MWh
Total Cost: $6000.00

With no transmission constraints binding, all buses have the same LMP of $20/MWh. This is exactly equal to the marginal cost of the North generator. The expensive Central generator doesn’t run because cheap power can flow freely throughout the network.
Scenario 2: Transmission Congestion#
Now let’s restrict the North-Central line capacity. This forces the system to use expensive local generation at Central because insufficient cheap power can flow from North.
Generation:
North: 230.0 MW @ $20/MWh
Central: 70.0 MW @ $50/MWh
Power Flows:
North → Central: 80.0 MW (limit: 80 MW)
North → South: 150.0 MW
Central → South: -0.0 MW
Locational Marginal Prices (from dual variables):
North: $20.00/MWh
Central: $50.00/MWh
South: $50.00/MWh
Total Cost: $8100.00

Key Insights#
The congested North-Central line creates distinct LMPs:
North: $20/MWh (the marginal cost of its generator)
Central: $50/MWh (must use expensive local generation)
South: $50/MWh (affected by the system-wide congestion)
These prices come directly from the optimization’s dual variables (shadow prices). The LMP at each bus represents the true marginal cost of serving one additional MW at that location, accounting for both generation costs and transmission constraints.
The $30/MWh price difference between North and Central signals the economic value of either:
Expanding the North-Central transmission capacity
Building cheaper generation at Central
Reducing demand at Central through efficiency or demand response
This price signal guides efficient long-term infrastructure investment decisions in competitive electricity markets.
Two-Settlement Markets#
Modern electricity markets operate through multiple settlement periods to manage uncertainty and ensure reliability. The day-ahead market closes approximately 24 hours before the operating day, allowing generators to commit units and schedule fuel deliveries. During the operating day, the real-time market runs every five minutes to handle deviations from day-ahead schedules caused by forecast errors, forced outages, or unexpected events.
This two-settlement structure balances competing objectives. The day-ahead market provides financial certainty and operational planning time, essential for thermal generators with long startup times. The real-time market ensures continuous balance between supply and demand, dispatching flexible resources to handle uncertainty. Together, they create a robust system that maintains reliability while providing appropriate price signals.

Settlement Mathematics#
The financial settlement in two-settlement markets follows specific rules that create appropriate incentives. Generators receive the day-ahead price for their day-ahead schedule and the real-time price for any deviations. This structure encourages accurate scheduling while allowing profitable responses to real-time conditions.
Consider a generator scheduled for 100 MW in the day-ahead market at $30/MWh. If it actually produces 110 MW in real-time when prices are $35/MWh, its revenue combines both settlements: $30 × 100 MW for the day-ahead position plus $35 × 10 MW for the additional real-time production. This mechanism ensures generators have incentives to follow dispatch instructions while responding to real-time price signals.
Price Formation and Volatility#
Electricity prices exhibit unique volatility patterns driven by the inability to store power economically. Unlike other commodities where inventory buffers supply and demand imbalances, electricity markets must clear instantaneously. This creates price spikes during scarcity and occasional negative prices during oversupply, particularly with high renewable generation.
Forecast errors are the primary driver of price differences between day-ahead and real-time markets. When actual demand exceeds forecasts, the system must quickly dispatch expensive peaking units, driving real-time prices above day-ahead levels. Conversely, overforecasting leads to excess committed generation that must be backed down, potentially causing real-time prices to fall below day-ahead prices. These price patterns create opportunities for flexible resources and financial traders while signaling the value of improved forecasting.
# Generate sample price data showing volatility
np.random.seed(42)
hours = np.arange(24)
# DA prices with daily pattern
da_prices = 30 + 15 * np.sin((hours - 6) * np.pi / 12) + np.random.normal(0, 2, 24)
# RT prices with forecast error impact
forecast_error = np.random.normal(0, 50, 24) # MW
rt_prices = da_prices + 0.1 * forecast_error + np.random.normal(0, 3, 24)
# Calculate spread
price_spread = rt_prices - da_prices
print("Price Statistics:")
print(f" Mean DA Price: ${da_prices.mean():.2f}/MWh")
print(f" Mean RT Price: ${rt_prices.mean():.2f}/MWh")
print(f" Max Spread: ${price_spread.max():.2f}/MWh")
print(f" Min Spread: ${price_spread.min():.2f}/MWh")
Price Statistics:
Mean DA Price: $29.70/MWh
Mean RT Price: $28.68/MWh
Max Spread: $12.06/MWh
Min Spread: $-10.36/MWh

Market Power and Competition#
Market power—the ability to profitably raise prices above competitive levels—poses a constant concern in electricity markets. The combination of inelastic demand, transmission constraints, and limited storage creates opportunities for generators to exercise market power, especially during high-demand periods. Understanding and mitigating market power is essential for maintaining competitive markets.
Several metrics help identify market power potential. Market concentration, measured by the Herfindahl-Hirschman Index (HHI), indicates whether a few large suppliers dominate the market. The pivotal supplier test identifies generators whose capacity is essential to meet demand—these suppliers know the system needs them and can bid strategically. When multiple indicators suggest market power concerns, regulators implement mitigation measures such as bid caps or must-offer requirements.
# Calculate market concentration metrics
market_shares = np.array([0.35, 0.25, 0.20, 0.15, 0.05]) # Five suppliers
supplier_names = ['MegaCorp', 'PowerGen', 'GreenEnergy', 'LocalUtil', 'SmallGen']
# Herfindahl-Hirschman Index
hhi = sum((share * 100) ** 2 for share in market_shares)
print("Market Concentration Analysis:")
print(f"\nHHI: {hhi:.0f}")
print(" < 1500: Unconcentrated")
print(" 1500-2500: Moderately concentrated")
print(" > 2500: Highly concentrated")
print(f"\nStatus: {'Highly concentrated' if hhi > 2500 else 'Moderately concentrated' if hhi > 1500 else 'Unconcentrated'}")
# Pivotal supplier analysis
total_capacity = 1000 # MW
peak_demand = 850 # MW
supplier_capacities = market_shares * total_capacity
print("\nPivotal Supplier Test (Peak Demand = 850 MW):")
for name, capacity in zip(supplier_names, supplier_capacities):
others_capacity = total_capacity - capacity
is_pivotal = others_capacity < peak_demand
print(f" {name}: {'PIVOTAL' if is_pivotal else 'Not pivotal'}")
Market Concentration Analysis:
HHI: 2500
< 1500: Unconcentrated
1500-2500: Moderately concentrated
> 2500: Highly concentrated
Status: Moderately concentrated
Pivotal Supplier Test (Peak Demand = 850 MW):
MegaCorp: PIVOTAL
PowerGen: PIVOTAL
GreenEnergy: PIVOTAL
LocalUtil: Not pivotal
SmallGen: Not pivotal
The pivotal supplier test reveals which generators have significant market power. When a supplier is pivotal, the system cannot meet demand without them, giving them the ability to raise prices knowing they must be dispatched. This structural market power exists regardless of the supplier’s behavior and requires regulatory oversight to prevent abuse.
Exercises#
Exercise 1: Market Clearing Verification#
A fundamental principle of competitive electricity markets states that the market-clearing price equals the marginal cost of the most expensive generator that is dispatched (the marginal unit). This exercise explores this relationship and what happens when demand changes slightly.
Using the generator data provided, solve the market-clearing problem for a demand of 950 MW using linear programming. Extract both the dispatch solution and the LMP from the dual variable. Then identify which generator is marginal—the highest-cost unit that is producing power—and compare its marginal cost to the LMP. They should be equal in an unconstrained market. Finally, calculate the total cost of serving this demand and the producer surplus for each generator.
Hint: The marginal generator is the one with the highest marginal cost among those with non-zero dispatch. Use the .pi
attribute of the power balance constraint to get the LMP directly. This is the shadow price that represents the marginal cost of serving one additional MW of demand.
# Exercise 1: Your code here
exercise_gens = pd.DataFrame({
'name': ['Coal_A', 'Coal_B', 'Gas_A', 'Gas_B', 'Peaker'],
'capacity': [350, 300, 250, 200, 100],
'marginal_cost': [22, 26, 32, 38, 65]
})
demand = 950 # MW
# Your solution here
Exercise 2: Price Spread Arbitrage#
Price differences between day-ahead and real-time markets create arbitrage opportunities for participants who can accurately predict these spreads. This exercise examines the economic value of perfect foresight and the risks involved in spread trading.
You’re given hourly day-ahead and real-time prices for a 24-hour period. Calculate the profit from perfect arbitrage with a 10 MW position: buy in the cheaper market and sell in the more expensive one for each hour. Sum your profits across all hours to find the total arbitrage value. Then analyze the risk by calculating what would happen if you always bought day-ahead and sold real-time (a directional bet) versus the perfect arbitrage strategy.
Hint: For each hour, the arbitrage profit is position_size × |RT_price - DA_price|
. You buy in the cheaper market and sell in the expensive one. For the directional strategy, profit is position_size × (RT_price - DA_price)
, which can be negative if RT < DA.
# Exercise 2: Your code here
np.random.seed(100)
da_prices = 30 + 5 * np.random.randn(24)
rt_prices = da_prices + 3 * np.random.randn(24)
position_size = 10 # MW
# Your solution here
Exercise 3: Settlement Calculation#
Understanding two-settlement market mathematics is crucial for generators optimizing their revenues. This exercise walks through a complete settlement calculation for a generator participating in both day-ahead and real-time markets.
A generator has the following schedule and actual output over 4 hours. For each hour, calculate the day-ahead revenue (DA schedule × DA price), the real-time deviation revenue ((RT output - DA schedule) × RT price), and the total revenue. Note that deviation revenue can be negative if the generator underproduces when real-time prices are high. Sum across all hours to find total revenues and identify which hours contributed most to profitability.
Hint: Create a new column for DA revenue, RT deviation revenue, and total revenue. The deviation can be positive (overgeneration) or negative (undergeneration). When RT price > DA price, overgeneration is profitable.
# Exercise 3: Your code here
schedule_data = pd.DataFrame({
'hour': [1, 2, 3, 4],
'da_schedule': [100, 100, 150, 120],
'da_price': [25, 28, 35, 30],
'rt_output': [95, 110, 150, 125],
'rt_price': [22, 32, 40, 28]
})
# Your solution here
Summary#
This lesson introduced the fundamental concepts that enable competitive electricity markets to function efficiently. You learned how merit order dispatch minimizes costs, why uniform pricing creates appropriate incentives, and how transmission constraints lead to locational price differences. The two-settlement system balances planning certainty with operational flexibility, while market power metrics help identify potential competition problems.
These concepts form the foundation for understanding modern grid operations. As you progress to studying network-constrained optimization and unit commitment, remember that the economic principles: marginal cost pricing, producer surplus, and settlement mathematics. Despite some imperfections, those principles are seen throughout electricity market designs.