← all spins
SPIN 04ENERGY · MILP · AGENTS
2026-08-14 · 14 min read

The optimizer said dispatch. The trader said no.

A battery arbitrage agent on public Alberta pool prices that records every operator override, asks why, and folds the answer back into the model as a constraint.

+14.2%
REVENUE VS THRESHOLD RULE
GitHub Repo↗

01 The problem

Storage operators have had dispatch optimizers for years. The models are not wrong in any way a solver would recognise — they hit their objective, satisfy every constraint they were given, and get overridden anyway. The override is where the real constraint lives, and nobody writes it down.

“Every morning it hands me a plan and every morning I change two or three things in it. In four years nobody has asked me why.”
— storage operations analyst, paraphrased with permission

So the question this spin asks is not “can an agent build a dispatch model” — it can, badly, and Gurobi ships one now. It is: can a system treat the override as the most valuable signal in the loop instead of as a failure to be trained out?


02 The data

All public, all downloaded by the Makefile. No proprietary feeds, so anyone can reproduce the run end to end.

SOURCE RANGE ROWS
AESO hourly pool price 2021-01 – 2026-07 48,192
AESO internal load (AIL) 2021-01 – 2026-07 48,192
ERA5 reanalysis, YYC grid cell 2021-01 – 2026-07 48,192
Synthetic operator panel 6 personas 312

Caveat worth stating plainly: the overrides are simulated from six rule-based personas, not from real operators. Everything downstream inherits that.


03 What I built

A closed loop. The solver is the decision authority — the agent never commits a dispatch the model has not proved feasible. The agent’s job is everything the solver cannot do: explain, ask, and translate an answer back into algebra.

Stage 05 writes back into stage 02. That arrow is the whole point of the spin.


04 Result

POLICY REVENUE OVERRIDDEN ACCEPTED
Threshold rule (baseline) 1.00× — —
MILP, constraints as documented 1.21× 41% 59%
MILP + 7 mined constraints 1.14× 9% 91%

Read that third row carefully, because it is the uncomfortable finding: adding the mined constraints made the model worse on paper — 1.14× against 1.21×. It also took override rates from 41% to 9%. A plan worth 1.21× that gets rewritten four mornings in ten is worth less in practice than a plan worth 1.14× that ships. Objective value is not the same thing as realised value, and only one of them shows up in a backtest.


05 Where it broke

  1. The explainer confidently justified a plan that violated a ramp constraint the solver had already relaxed. Explanations are generated after the fact and will defend anything you hand them. I now diff the explanation against the active constraint set and refuse to render on mismatch.
  2. Override capture worked until the personas were told they were being recorded. Volume fell about 60% in the third simulated week. If that holds with real operators, the whole method has a consent problem I do not yet have a good answer to.
  3. The constraint miner over-fit to winter. Four of the seven rules were seasonal, and holding them through April quietly degraded the plan by 6%. Mined constraints need an expiry date, which nothing in the literature I read bothers to mention.

06 Run it yourself

$ git clone https://github.com/gospinup/spin-04-dispatch
$ cd spin-04-dispatch && make data
$ make run

Full run is about six minutes on a laptop. No API keys — the AESO pulls are in the Makefile. MIT.


07 Discuss

Every spin has a thread on GitHub Discussions. If a constraint looks over-fit, if the baseline is too easy, or if you’ve solved this properly in production and I’m busy reinventing it badly — say so there. Corrections get credited in the spin itself.

Open GitHub Discussion →