01 · THE PROBLEMRenewables made power cheap — and prices wild
The UK has a legally binding net-zero target for 2050, and the generation mix shows it: clean power has displaced coal and is now overtaking gas. But the transition rewired how electricity is priced. Unlike a gas plant, a wind farm can't choose when to generate — output follows the weather, on every timescale at once. In a liberalised market where each trading period clears at the marginal cost of meeting demand, that intermittency has consequences: an abundance of wind can push prices toward zero or even negative, while scarcity events send them spiking.
For a wind producer this is a double bind. Your asset earns the least exactly when it produces the most (everyone's turbines are spinning), and you can't capture the spikes (the wind doesn't take orders). Worse, the UK's imbalance-settlement regime penalises forecast errors in either direction — so being wrong about your own output costs money twice. Volatility, for a passive generator, is pure downside.
A co-located battery changes the game. Store energy when prices are low, sell when they're high, and the volatility that punished you becomes the spread you trade. But the battery is only as smart as its view of the next few hours — which makes this, at its core, a forecasting problem welded to an optimisation problem. That's the system this dissertation builds, end to end, on UK data.
02 · THE SYSTEMTwo forecasts, one optimiser, one decision per hour
Every hour, the producer faces four options: sell the generation now, store it, discharge what's stored, or sit idle. The framework feeds two six-hour-ahead forecasts — wind power and electricity price — into a mixed-integer optimiser that makes that call, subject to the physics and economics of a real battery.
03 · THE PHYSICSFrom wind speed to watts: Betz's law in the feature set
The wind data is ERA5 reanalysis from the ECMWF — hourly atmospheric variables on a ~31 km grid: wind components at 10 m and 100 m, air temperature, surface friction velocity (u*, which captures how terrain roughness drags on the wind). Raw ERA5 doesn't give you wind speed directly; it gives the zonal (east–west) and meridional (north–south) components, u and v, which combine by simple vector magnitude:
Speed alone isn't power. The target variable — theoretical turbine output — comes from the fundamental wind-power equation rooted in Betz's analysis of how much kinetic energy a rotor can extract from moving air (Betz, 1926):
where ρ is air density, A the swept rotor area, v the wind speed, and Cp the power coefficient — fixed at 0.5 here, typical of modern turbines (Betz proved no rotor can ever exceed ~0.593). Two things follow from that little equation. First, power scales with the cube of wind speed: a 10% change in wind is a ~33% change in output, which is exactly why wind revenue is so volatile. Second, embedding the equation grounds the deep-learning models in physics — they learn to predict an output anchored to real turbine behaviour rather than a free-floating curve.
04 · EXPLOREWhat correlates with power — and what got dropped
Before modelling, the candidate features faced a correlation audit against power output. The verdict was clean:
| u10 | v10 | u100 | v100 | gust | temp | radiation | u* (friction) | wind speed | power | |
|---|---|---|---|---|---|---|---|---|---|---|
| u10 | 1.00 | 0.44 | 0.99 | 0.38 | 0.43 | 0.03 | -0.10 | 0.43 | 0.47 | 0.47 |
| v10 | 0.44 | 1.00 | 0.47 | 0.99 | 0.36 | 0.20 | -0.04 | 0.35 | 0.39 | 0.47 |
| u100 | 0.99 | 0.47 | 1.00 | 0.41 | 0.41 | 0.02 | -0.12 | 0.42 | 0.47 | 0.46 |
| v100 | 0.38 | 0.99 | 0.41 | 1.00 | 0.35 | 0.22 | -0.03 | 0.34 | 0.38 | 0.45 |
| gust | 0.43 | 0.36 | 0.41 | 0.35 | 1.00 | 0.15 | 0.24 | 0.98 | 0.89 | 0.84 |
| temp | 0.03 | 0.20 | 0.02 | 0.22 | 0.15 | 1.00 | 0.51 | 0.12 | -0.07 | -0.03 |
| radiation | -0.10 | -0.04 | -0.12 | -0.03 | 0.24 | 0.51 | 1.00 | 0.21 | -0.14 | -0.09 |
| u* (friction) | 0.43 | 0.35 | 0.42 | 0.34 | 0.98 | 0.12 | 0.21 | 1.00 | 0.91 | 0.83 |
| wind speed | 0.47 | 0.39 | 0.47 | 0.38 | 0.89 | -0.07 | -0.14 | 0.91 | 1.00 | 0.89 |
| power | 0.47 | 0.47 | 0.46 | 0.45 | 0.84 | -0.03 | -0.09 | 0.83 | 0.89 | 1.00 |
- Wind speed dominates (r = 0.89 with power) — as the cubic law demands. Gust speed and friction velocity follow at 0.83–0.84, adding texture about turbulence and terrain.
- Temperature was dropped, with the physics as the argument: in P = ½ρACpv³, temperature only enters through air density (ρ ≈ p/RT), and that effect is tiny next to the variability of v³. The data agreed: r = −0.03. Same verdict for solar radiation (−0.09), which doesn't appear in the power equation at all.
- Near-duplicates exposed: the 10 m and 100 m wind components correlate at 0.99 — the audit prevented the model from being fed the same information twice dressed as two features.
The exploratory pass also surfaced a diurnal rhythm — average output peaks in the early afternoon around 13:00 — and the raw series shows what the models are up against: spikes past 14,000 kW next to long becalmed stretches. Intermittency isn't a footnote in this data; it is the data.
05 · SETUPHow the models were trained
Four deep-learning architectures were built for each forecasting task — a feed-forward MLP, a 1D-CNN, an LSTM, and a CNN-LSTM hybrid — chosen to span the design space: pure function approximation, local pattern extraction, long-range memory, and the combination. The discipline was identical across all eight model instances:
| Component | Choice | Rationale |
|---|---|---|
| Input window | 18-hour lookback | Captures strong hourly autocorrelation and the daily demand cycle |
| Output | Direct 6-step prediction | One shot for all six hours — beat recursive one-step forecasting on stability |
| Split | Chronological 80/20 + validation | No future data leaks backwards; scaling statistics fitted on train only |
| Loss | Huber | Robust to the price spikes that would dominate a pure MSE objective |
| Tuning | Optuna, up to 100 trials/model | Bayesian search (TPE) over layers, units, learning rate, dropout, batch size |
| Regularisation | Dropout + early stopping + weight decay | Training and validation losses converge — verified per model, not assumed |
| Price features | Lagged prices only | Calendar features were engineered, then dropped: the recurrent models learned the daily and weekly cycles from the sequence itself |
06 · FORECASTING PRICESThe hybrid wins the market
Price signals mix sharp local spikes with longer daily and weekly rhythms, and the CNN-LSTM's structure mirrors that split: convolutional layers catch the spikes, the recurrent layers carry the rhythm. The benchmark agreed — the hybrid leads every headline metric.
| Model | RMSE (€) | MAE (€) | R² | Pearson R |
|---|---|---|---|---|
| CNN-LSTM | 21.75 | 16.17 | 0.7255 | 0.852 |
| MLP | 22.00 | 16.37 | 0.7191 | 0.8612 |
| LSTM | 23.03 | 17.02 | 0.6921 | 0.8414 |
| 1D-CNN | 23.50 | 17.33 | 0.6795 | 0.8408 |
07 · FORECASTING WINDThe plot twist: the simplest model wins
Same four architectures, same protocol, different winner. On wind power, the humble MLP beat every sequence model — lowest RMSE and MAE at all six horizons, highest R² at five of six. With a rich, physics-grounded ERA5 feature set, wind prediction behaves less like a sequence problem and more like nonlinear regression, and a well-tuned MLP is exactly that. Meanwhile the LSTM scored the highest Pearson correlation (0.862): it tracks the shape of the series best even where its magnitudes miss — a genuinely different skill, and which one matters depends on what the downstream decision consumes.
No single architecture dominated both tasks — and that's the finding, not a failure. The price task rewarded the hybrid; the wind task rewarded a flexible function approximator; the "best" model even changes with the metric (point accuracy vs shape tracking). A production system should run a portfolio of specialised models per data stream, not crown one architecture and force it everywhere.
08 · THE OPTIMISERProfit, with physics attached
Forecasts feed a mixed-integer linear programme (PuLP, CBC solver) that maximises revenue over each six-hour window. Each hour it sets three non-negative decision variables — energy sold directly, charging power, discharging power — with a binary variable forbidding simultaneous charge and discharge. The objective is total revenue: direct sales plus discharged energy at market price, minus charged energy and minus degradation cost. The constraints are how batteries actually work:
| Constraint | Value | Why it matters |
|---|---|---|
| Usable capacity | 60 MWh | You can't arbitrage what you can't store |
| Charge / discharge power | 10 MW | Caps how fast the battery can act on a price spike |
| Round-trip efficiency | 86% | Every stored MWh comes back smaller — spreads must beat the loss |
| State of charge | 5–95% | Protects battery life at the extremes |
| Degradation cost | €0.01 / kWh | Cycling isn't free; the optimiser won't trade thin spreads |
| Energy balance | SoCt+1 = SoCt + ηcPc − Pd/ηd | Charge and discharge losses applied where they physically occur |
The degradation term is the quiet hero: it makes the optimiser conservative in flat markets and aggressive in volatile ones — exactly the behaviour a real asset manager wants, emerging from the economics rather than a hand-coded rule.
09 · RESULTSWhat the arbitrage earned
Against a baseline that sells all generation immediately, the forecast-driven optimiser averaged £858.85 per six-hour window vs £801.72 — a +7.13% uplift across the dataset. The average hides the texture, which three windows illustrate:
- Steady spread (+38.5%): prices climb gradually; the optimiser charges early using available wind, sells late, and beats the baseline comfortably.
- Price spike (+109.1%): the forecast sees a spike coming; the battery pre-charges over four hours and discharges aggressively into the peak. The baseline, selling as it generates, misses most of it.
- Spike, but no fuel (+2.4%): a late spike with little wind available — the optimiser anticipates correctly but has almost nothing stored to sell. Forecast quality can't conjure energy.
The single best window in the dataset shows the ceiling. Prices started near zero and climbed to roughly £150/MWh across six hours; the optimiser charged through the cheap hours at up to 10 MW and emptied the battery into the final two:
10 · HONESTYThe limitations, stated plainly
The framework treats point forecasts as if they were certain, so the reported uplift is an upper bound under that assumption — realised gains would be reduced by forecast error. The six-hour horizon can't hold charge for tomorrow's bigger spike. Degradation is modelled as a flat linear cost when real degradation is nonlinear in temperature, depth of discharge and C-rate. Round-trip efficiency is held constant when it varies with operating conditions. And one year of training data meets a market that policy and the generation mix keep reshaping — the models need rolling retraining to stay calibrated.
I reported the 7.13% explicitly as an idealised ceiling rather than a promised return. An analysis that hides its assumptions gets one good meeting; an analysis that prices them in gets trusted with the next decision. The fix list is concrete: probabilistic forecasts feeding risk-aware (stochastic or robust) optimisation, longer or nested horizons, richer degradation models, rolling retraining on updated market and weather data.
11 · NEXTWhere this goes
The most interesting extension is economically-driven forecast training: stop optimising RMSE and start optimising the profit the downstream decision actually realises. The forecast exists only to serve the decision, so train it on the decision's objective. Beyond that: multi-market participation — day-ahead, continuous intraday, the balancing mechanism and ancillary services, where prior studies show material gains from joint bidding — and reinforcement-learning dispatch policies that learn the market directly instead of through a forecasting intermediary.
Stack: TensorFlow / Keras · MLP · 1D-CNN · LSTM · CNN-LSTM · Optuna (TPE) · PuLP (MILP, CBC) · ERA5 (ECMWF) · ENTSO-E · pandas · University of Surrey, supervised by Dr. Xun Zhou