Ancillary Services: FCR, aFRR, mFRR

← Learning Hub Lesson 6 · Battery Optimizer Course · Estimated time: 25 minutes

Batteries don't just arbitrage energy prices. They can also provide grid balancing services — and the revenues are often higher and more stable than spot arbitrage alone. This lesson covers the three main frequency response products and how to include them in your optimizer.

What are ancillary services?

The grid frequency must stay at 50.000 Hz (in Europe). When generation ≠ demand, frequency deviates. TSOs (Transmission System Operators) procure reserve capacity from assets like batteries to restore balance. You get paid for keeping capacity available — even if you never actually deliver energy.

The three products

FCR — Frequency Containment Reserve

aFRR — Automatic Frequency Restoration Reserve

mFRR — Manual Frequency Restoration Reserve

Battery sweet spot: FCR. Batteries excel at FCR because they respond in milliseconds (far faster than the 30-second requirement), the energy volumes are small (symmetric around zero), and the capacity payments are high relative to the power reserved. Many batteries earn 60–80% of revenue from FCR alone.

FCR and the battery: the SoC challenge

To provide symmetric FCR (±X MW), your battery must be able to both increase and decrease output at any moment. This means maintaining SoC near 50% — if you're empty, you can't discharge; if full, you can't charge.

# FCR reservation: ±5 MW for a 4-hour block
# Battery must keep enough headroom in both directions

fcr_power = 5.0  # MW reserved for FCR
fcr_duration = 0.25  # hours (15 min sustain requirement)
fcr_energy = fcr_power * fcr_duration  # 1.25 MWh headroom each direction

# SoC must stay within these bounds during FCR provision:
# SoC ≥ SoC_min + fcr_energy  (can still discharge if called)
# SoC ≤ SoC_max - fcr_energy  (can still charge if called)

Co-optimization: arbitrage + FCR

The key insight: you can provide FCR and do arbitrage simultaneously, as long as you respect the capacity reservation. The optimizer decides how to split the battery's power between markets:

"""
cooptimization.py — Joint day-ahead arbitrage + FCR bidding
"""
import pyomo.environ as pyo
import numpy as np

T = 24
prices = [28, 25, 22, 20, 19, 21, 35, 55,
          62, 58, 52, 48, 45, 44, 47, 53,
          68, 85, 92, 78, 60, 45, 35, 30]

# FCR auction results: capacity price per 4-hour block (€/MW/h)
# Blocks: 0-3, 4-7, 8-11, 12-15, 16-19, 20-23
fcr_capacity_prices = [3.5, 4.0, 5.5, 6.0, 12.0, 4.5]  # €/MW/h

P_max = 10.0
E_max = 20.0
eta = 0.90
eta_sqrt = np.sqrt(eta)
soc_init = 10.0
SoC_min = 0.1 * E_max
SoC_max = 0.9 * E_max
deg_cost = 5.0
fcr_duration = 0.25  # 15-min sustain

model = pyo.ConcreteModel()
model.T = pyo.RangeSet(0, T - 1)
model.B = pyo.RangeSet(0, 5)  # 6 FCR blocks

# Decision variables
model.p_charge = pyo.Var(model.T, bounds=(0, None))
model.p_discharge = pyo.Var(model.T, bounds=(0, None))
model.soc = pyo.Var(model.T, bounds=(SoC_min, SoC_max))
model.y = pyo.Var(model.T, within=pyo.Binary)
model.fcr = pyo.Var(model.B, bounds=(0, P_max))  # MW reserved for FCR per block

# Map hours to FCR blocks
def block_of(t):
    return t // 4

# Power limits: arbitrage power + FCR reservation ≤ P_max
def power_limit_charge(m, t):
    b = block_of(t)
    return m.p_charge[t] + m.fcr[b] <= P_max
def power_limit_discharge(m, t):
    b = block_of(t)
    return m.p_discharge[t] + m.fcr[b] <= P_max
model.pl_c = pyo.Constraint(model.T, rule=power_limit_charge)
model.pl_d = pyo.Constraint(model.T, rule=power_limit_discharge)

# SoC must have headroom for FCR activation
def soc_fcr_lower(m, t):
    b = block_of(t)
    return m.soc[t] >= SoC_min + m.fcr[b] * fcr_duration
def soc_fcr_upper(m, t):
    b = block_of(t)
    return m.soc[t] <= SoC_max - m.fcr[b] * fcr_duration
model.soc_fcr_lo = pyo.Constraint(model.T, rule=soc_fcr_lower)
model.soc_fcr_hi = pyo.Constraint(model.T, rule=soc_fcr_upper)

# SoC balance (same as before)
def soc_rule(m, t):
    prev = soc_init if t == 0 else m.soc[t-1]
    return m.soc[t] == prev + eta_sqrt * m.p_charge[t] - m.p_discharge[t] / eta_sqrt
model.soc_bal = pyo.Constraint(model.T, rule=soc_rule)

# No simultaneous charge/discharge
def nc(m, t): return m.p_charge[t] <= P_max * (1 - m.y[t])
def nd(m, t): return m.p_discharge[t] <= P_max * m.y[t]
model.nc = pyo.Constraint(model.T, rule=nc)
model.nd = pyo.Constraint(model.T, rule=nd)

# Objective: arbitrage revenue + FCR capacity revenue - degradation
def obj_rule(m):
    arb_revenue = sum(prices[t] * (m.p_discharge[t] - m.p_charge[t]) for t in m.T)
    fcr_revenue = sum(m.fcr[b] * fcr_capacity_prices[b] * 4 for b in m.B)  # 4 hours per block
    degradation = sum(deg_cost * (m.p_charge[t] + m.p_discharge[t]) for t in m.T)
    return arb_revenue + fcr_revenue - degradation
model.obj = pyo.Objective(rule=obj_rule, sense=pyo.maximize)

# Solve
solver = pyo.SolverFactory('highs')
result = solver.solve(model, tee=False)

print(f"Total revenue: €{pyo.value(model.obj):.2f}")
print(f"  Arbitrage:   €{sum(prices[t] * (pyo.value(model.p_discharge[t]) - pyo.value(model.p_charge[t])) for t in range(T)):.2f}")
print(f"  FCR:         €{sum(pyo.value(model.fcr[b]) * fcr_capacity_prices[b] * 4 for b in range(6)):.2f}")
print(f"\nFCR reservations per block:")
for b in range(6):
    print(f"  Block {b} (hours {b*4}-{b*4+3}): {pyo.value(model.fcr[b]):.1f} MW")

The trade-off: FCR vs. arbitrage

Reserving power for FCR reduces the power available for arbitrage. The optimizer balances:

Real-world insight: In Germany (2023–2025), FCR prices averaged €5–15/MW/h. A 10 MW battery fully committed to FCR earns €50–150/h = €1,200–3,600/day from capacity alone. Compare to pure arbitrage revenues of €500–1,500/day. FCR often wins, especially on low-volatility days.

aFRR and mFRR: energy payments

Unlike FCR (mostly capacity), aFRR and mFRR also pay for delivered energy when activated. The optimizer must account for:

Modeling this properly requires stochastic optimization — which we'll cover in Lesson 7. For now, the deterministic approach: estimate expected activation hours and include expected energy revenue.

Check your understanding

A 10 MW battery reserves 6 MW for FCR. How much power is available for day-ahead arbitrage?

Why does the optimizer add SoC headroom constraints (SoC ≥ SoC_min + fcr_energy) during FCR provision?

On a day with very low day-ahead price spreads but high FCR capacity prices, what should the optimizer do?

📖 Recommended reading

ENTSO-E — Electricity Balancing — the European framework for balancing markets (FCR, aFRR, mFRR). Read the "Implementation" section for PICASSO (aFRR) and MARI (mFRR) platform rules.

💬 Questions? The interaction between FCR and arbitrage is subtle — ask about prequalification requirements, pooling rules, or how TSO-specific rules (e.g., Germany vs. Netherlands) affect the formulation.

Rechtliches: Impressum