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.
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
- 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.
- 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.
- 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.