This repository implements a deterministic (perfect-foresight) day-ahead co-optimization of a Battery Energy Storage System (BESS) between:
- Day-Ahead (DA) energy arbitrage,
- FCR (primary frequency reserve, symmetric),
- aFRR (secondary automatic reserve, UP / DOWN products).
The goal is to compute, day by day, an optimal schedule (SoC, charge/discharge, reserve bids) on a 15-minute grid while respecting battery and reserve feasibility constraints.
This work is largely inspired by a reference thesis used throughout the project : "Optimizing Residential Battery Energy Storage Systems Across Frequency Regulation Markets and Energy Arbitrage" by Elias Schuhmacher and Eric Rosen wrote in 2025.
An example of a day-ahead optimized schedule (January 15, 2021):

Details of three strategies PnL (January 15, 2021):

Comparison of three strategies PnL over January:

A BESS can earn:
- Energy arbitrage: charge low, discharge high.
- Reserve capacity payments: get paid for being available to provide frequency control (even if not activated).
Because energy and reserve share the same power (MW) and energy (MWh) limits, the trade-off is non-trivial: allocating headroom to reserves may reduce arbitrage, and aggressive arbitrage may violate reserve deliverability.
Two public datasets are used:
- French day-ahead prices (from Ember) originally hourly, in €/MWh.
- French balancing capacity remuneration (from RTE Services) 15-min, in €/MW/15min.
Please find the links to the data below:
- https://ember-energy.org/data/european-wholesale-electricity-price-data
- https://www.services-rte.com/fr/telechargez-les-donnees-publiees-par-rte.html?category=market&type=balancing_capacity&subType=procured_reserves
We operate everything on a UTC 15-minute grid (96 steps/day). DA hourly prices are expanded to 15-min as piecewise-constant; reserve products are kept for FCR, aFRR_UP, aFRR_DOWN; small gaps are forward-filled. Processed datasets are stored as compact Parquet files for fast reproducibility in the repo (data directory).
We follow a standard battery model (SoC dynamics + power/energy constraints), grounded in the reference thesis and adapted to an industrial/utility scale.
Typical parameters used in experiments:
- Power: 10 MW
- Energy: 20 MWh
- SoC bounds: 10% – 90%
- Efficiencies: 90% per conversion step
- Degradation proxy: linear throughput penalty, ~15 €/MWh
We consider three products settled on the same 15-min grid:
-
Energy (DA): scheduled charging/discharging at price
$\pi_t$ (€/MWh) - FCR capacity: symmetric reserve, assumed net SoC impact ~ 0 on average (but consumes power headroom)
- aFRR capacity: UP/DOWN products, modeled with a simplified activation mechanism
We solve one optimization per day with:
-
$T = 96$ time steps,$\Delta t = 0.25$ hours - initial SoC
$S_0$ coming from the previous day (optional multi-day linking)
-
$P_t^{ch} \ge 0$ ,$P_t^{dis} \ge 0$ : charge/discharge power (MW) -
$R_t^{fcr} \ge 0$ : FCR capacity (MW) -
$R_t^{up} \ge 0$ ,$R_t^{down} \ge 0$ : aFRR UP/DOWN capacity (MW) -
$A_t^{up} \ge 0$ ,$A_t^{down} \ge 0$ : aFRR activation power (MW) -
$S_t$ : state-of-charge (MWh)
Directional aFRR consumes headroom in the corresponding direction:
For FCR, we enforce symmetric deliverability (headroom in both directions):
Activation cannot exceed capacity:
We enforce a uniform activation in time with 25% activation ratios (
Upward reserve requires enough energy above
with
- Python + CVXPY formulation
- Solved as a linear program, using ECOS solver
- Modular, object-oriented structure:
Batterystores technical parametersDaySolversolves one dayDaySolutionstores schedule + metrics + statusOrchestratorruns day-by-day and aggregates results
This repo is
uv-friendly !
# 1) Create venv
uv venv .venv
.venv\Scripts\activate
# 2) Install dependencies
uv sync
# 3) Run
uv run python main.py