energy_anchor
energy_anchor is a master switch wired to America's fuel tanks. Once a week the US government counts how much crude oil, gasoline, diesel and natural gas is sitting in storage, and publishes the change since the week before. Stocks fell — a draw. Stocks rose — a build. You pick a size ("only when crude drew at least 2 million barrels"), and every cycle the bot looks up the most recent published change. If it clears your bar the bot goes on to do whatever it was going to do. If it doesn't, the bot places no trades at all that cycle, in any market. The trading folklore behind it is old and simple: a draw means the country is burning fuel faster than it is stockpiling it, which is supposed to be bullish for the price. A build is the reverse.
- Bots using it
- 1 (out-of-sample; none live)
- Data source
- US EIA Open Data v2
- Refresh
- Daily 16:00 UTC (data is weekly)
- History stored
- 18 weeks 7 with a usable number
- Backtest-replayable
- No — see catch #4
- Anchor module
- quants/energy_anchor.py
Where the data comes from
One source, and for once it is exactly what the code says it is: the US Energy Information Administration, a statistical agency inside the Department of Energy. Every week the EIA surveys refiners, pipeline operators, terminals and storage sites and publishes what is in the tanks. Two reports matter here, and the EIA's own release schedules state their timing plainly:
- The Weekly Petroleum Status Report — crude oil, gasoline and distillate. EIA's schedule page says the standard release is 10:30 a.m. eastern time on Wednesdays, with a list of holiday exceptions that push it to Thursday. The data files come out at 10:30; the full PDF and HTML package follows after 1:00 p.m. eastern.
- The Weekly Natural Gas Storage Report — how much gas is in underground storage in the lower 48. Its schedule page says 10:30 a.m. eastern time on Thursdays, again with holiday exceptions.
Each report covers a week that ended the previous Friday. So the number published on a Wednesday morning describes a week that finished five days earlier. That is normal for this data and not a fault — but it is the first of the delays stacked into this signal, and the others are ours.
We do not scrape the reports. We pull them from EIA Open Data v2 at api.eia.gov/v2, which is free and needs only a registered key (ours is in prod's .env). Four series, named explicitly in the poller:
| Fuel | EIA series | What it counts | Unit |
|---|---|---|---|
| crude | WCESTUS1 | commercial crude, excluding the SPR | kbbl |
| gasoline | WGTSTUS1 | total motor gasoline | kbbl |
| distillate | WDISTUS1 | diesel + heating oil | kbbl |
| natgas | NW2_EPG0_SWO_R48_BCF | gas in storage, lower 48 | Bcf |
Two details in that table are worth pausing on. The crude series is the ex-SPR one — commercial barrels only, not the government's Strategic Petroleum Reserve. That is the series traders actually watch, and picking the wrong one is a classic own-goal, so it is good that the code picked right. And the unit kbbl means thousand barrels, which trips up almost everybody the first time (see catch #8).
The poller
predictive/eia_poller.py, on the production crontab as 0 16 * * * — once a day at 16:00 UTC. The data is weekly, so a daily poll is generous; it simply re-requests the newest eight weeks of each series and upserts them. The API returns levels only, so the poller derives the change itself by subtracting the next-older week's level.
Health, from /var/log/eia-poller.log: 68 successful runs, and 10 individual series fetches that timed out — 6 on distillate, 4 on natural gas. When a series times out the poller prints a line to stderr and silently skips that fuel for the day; because the data is weekly and the poll is daily, a one-day miss normally heals itself the next morning. Crude — the only fuel any bot has ever used — has never timed out. The most recent run, however, crashed outright (catch #7).
The table
energy_data in predictive.db: one row per fuel per week, holding the level, the derived change, the unit and a fetch timestamp. It currently holds 68 rows across the four fuels — 18 weeks of crude and natural gas, 16 of gasoline and distillate — spanning week-endings 3 April to 31 July 2026. That is not a deep archive, and the reason is not that the data is scarce. It is that the poller only ever asks for eight weeks at a time, so our history began the day the poller did and has grown one week per week since. What EIA will actually give you, on the same free endpoint we already call, is in catch #4.
What it means for your bot
Practically: this is a weekly on/off switch that stays in whatever position the last report put it in. It is not a live feed and it does not tick. Once the Wednesday number lands, the gate's answer is fixed for the next seven days — the same true or false, on every one of the roughly 288 cycles the engine runs each day, until the following report replaces it.
That has a consequence people miss. The signal is not really "trade when inventories are falling". It is "trade during the week following a report that showed inventories fell". Those are different claims, and only the second one is testable against what the bot actually does.
It is also a global gate. It knows nothing about the market in front of it. If the switch is on, the bot is free to trade anything else in its config clears — energy markets, or anything at all, since the anchor itself never checks. Our one bot happens to be pointed at energy and commodities by its category filter, so the pairing is sensible. Nothing in energy_anchor enforces that.
The one bot that has used it
EIA Crude Draw Rider (id 42), created 11 June 2026, status out-of-sample — it has never been promoted to live. Its config, verbatim in the parts that matter: buy YES on energy or commodities markets whose title contains "above", priced between 30¢ and 75¢, with at least 2,000 in 24-hour volume, only on Wednesdays between 14:00 and 20:00 UTC, and only when {"metric":"crude","draw_above":2000} — a crude draw of at least 2 million barrels. Flat $5.00 a trade, maximum 4 fills a day, hold to expiry.
Since 11 June the engine has evaluated it 17,354 times with zero errors. Across those runs it found 124 candidates and took 6 fills. Every fill was on Polymarket, and every one was a "WTI Crude Oil closes above $X" market:
| Trade (all Polymarket, all YES) | Entry | Settled | P&L |
|---|---|---|---|
| Wed 1 Jul 16:55 — WTI closes above $69 | 41¢ | 0 | −$5.00 |
| Wed 8 Jul 14:00 — WTI closes above $73 | 70¢ | 1 | +$2.14 |
| Wed 8 Jul 14:00 — WTI closes above $74 | 39¢ | 1 | +$7.82 |
| Wed 5 Aug 14:15 — WTI closes above $74 | 69¢ | 1 | +$2.25 |
| Wed 5 Aug 15:35 — WTI closes above $75 | 58¢ | 1 | +$3.62 |
| Wed 5 Aug 16:10 — WTI closes above $76 | 43¢ | 0 | −$5.00 |
| 6 trades · 4 won · avg entry 53.3¢ | +$5.83 |
Four wins from six, and up $5.83 on $30 staked. Do not read that as a track record — read catch #9 first. It is three days, not six trades.
The catch
This signal has a good source, an honest module and a gate that genuinely fails closed. It also has a timing bug sitting underneath its only bot, and that one is worth the whole page.
1. 🔴 The report reaches our table a day after the window the bot trades in
The bot trades Wednesdays, 14:00–20:00 UTC — deliberately, because that is release morning in New York. But the EIA figure is not in our database at that point. Our poller runs at 16:00 UTC Wednesday, which is 12:00 noon eastern, ninety minutes after the 10:30 release — and on every week we can check, the API still had not published that week's row. It appears on the next day's run.
Here is how that is provable rather than merely asserted. The poller requests the newest eight weeks each day, so a given week's row stops being rewritten the moment an eighth newer week exists — its final fetched_at pins the last day it was still in the window. Three consecutive weeks tell the same story: the row for week-ending 17 Jul was still absent at 16:00 UTC on Wed 22 Jul; week-ending 24 Jul was still absent at 16:00 UTC on Wed 29 Jul; week-ending 31 Jul was still absent at 16:00 UTC on Wed 5 Aug. Each landed the following day.
And a completely separate table agrees. Across the eight Wednesdays this bot has been armed, the engine found candidates on exactly three — 1 Jul, 8 Jul and 5 Aug. Reconstructing what the gate could have read on each of those days from the stored weekly changes predicts open, open, closed, closed, closed, open for the six Wednesdays we can reconstruct. That is a six-for-six match with the run log, and it only matches if the gate was reading the previous week's number.
The consequence, in plain terms: every trade this bot has ever placed was made on a number that had been public for seven days, describing a week that had ended twelve days earlier. On 5 August it bought three contracts on the strength of a 7,167 kbbl draw. The figure EIA actually released that morning was a build of 2,479 — the opposite sign — and by the gate's own rule it should have shut the bot down for the week. It arrived on the 6th.
Worth saying out loud, because it is our copy and not EIA's: the builder currently offers this signal with the line "the inventory numbers that move energy markets the moment they print". Our copy of those numbers does not arrive at the moment they print. It arrives the next day.
2. It gates on the raw draw, not on the surprise — which is what markets price
Inventory numbers move prices through the gap between the print and what the desks expected, not through the print itself. A 5-million-barrel draw that everyone forecast is a non-event; a 1-million-barrel draw when the street looked for a build is bullish. draw_above cannot tell those apart, because it only ever sees one number. So the gate will happily fire on a thoroughly expected draw and sit out a genuinely surprising build.
We have no consensus figure anywhere in the system to compare against, so this is not something you can configure your way out of today. It is a real limit on what the signal can be, and it is the single biggest reason to treat it as a coarse regime filter rather than as a reaction to news.
3. The trading window is fixed in UTC; the release is fixed in New York time
EIA publishes at 10:30 a.m. eastern, which observes daylight saving. That is 14:30 UTC for roughly March–November and 15:30 UTC the rest of the year. The bot's window opens at a flat 14:00 UTC year-round, so in winter its first ninety minutes sit before the release rather than after it. Any bot that keys off a US government release time and stores its window in UTC has this problem twice a year.
The related trap: EIA's schedule page lists holiday exceptions that move the petroleum report to Thursday. A bot configured for day_of_week_in: [2] never trades on those days at all.
4. The backtester cannot replay it — and here the usual excuse does not hold
energy_anchor is listed in _UNSUPPORTED_ENTRY in quants/backtest.py, under a comment dated 4 June 2026 explaining that these signal-library anchors "need a forward-captured series we don't have retroactively". For most of them that is true. For this one it is not.
We asked EIA's API what it has. For crude, series WCESTUS1: 2,288 weekly observations going back to 20 August 1982. For natural gas: 866 weeks back to 1 January 2010. Forty-four years of the exact series this gate reads, on the endpoint we already call, with the key we already have. The only thing standing between us and it is the poller's length=8.
Until someone changes that, a backtest containing energy_anchor runs without the gate and still returns a curve — so it is not testing this signal at all. The backtester does flag the unsupported key rather than hiding it, which is why you should read that flag.
5. Eleven of our eighteen crude weeks have no usable number in them
The poller derives each week's change by subtracting the next-older week in the same response. The oldest row in an eight-week response has nothing older to subtract, so its change is written as empty. On the next day's run the window has usually moved on, and that row is never revisited — so the empty value is permanent. Right now 11 of 18 crude rows carry no change at all, and the same is true of 9 of 16 gasoline and distillate rows.
It is harmless for live trading, because the gate only ever reads the newest week, which always has a change. It matters if anyone ever backfills history for replay: done naively, the hole comes with it.
6. "Fresh" is measured on the week, not on the poller
The staleness guard is FRESH_DAYS = 21, applied to the week-ending date — not to when we last fetched anything. Since a report is published five days after its week ends, that leaves a row admissible for about sixteen days after release. Under normal operation this is invisible, because a new report replaces it every seven days.
It bites when the poller stops. If the feed died tomorrow, the gate would keep serving the last figure for another fortnight, with no error and no log line, and a bot gated on a big draw would keep trading on it the whole time. It does eventually fail closed, which is more than several of our other anchors manage — it just fails closed late.
7. 🔴 The poller's most recent run died, and it is the third one doing this
At 16:00 UTC on 10 August 2026 eia_poller.py crashed with sqlite3.OperationalError: database is locked, part-way through its first insert. Nothing committed; the newest write in the table is still the previous day's. One crash in 69 runs is not a crisis on its own — but it is the same error, on the same database, that killed the nightly resolution_buckets rebuild and both of the day's sentiment_poller runs. Three unrelated jobs, one shared predictive.db, one write-contention problem. That is a plumbing issue, not three coincidences, and it has been reported rather than patched here: this loop does not edit pollers.
8. The unit is thousand barrels, and the English does not make that clear
EIA reports these stocks in MBBL, where the M is the Roman thousand — so kbbl, thousand barrels. A config of draw_above: 2000 therefore means 2 million barrels, which is the number a newspaper would print. Our builder renders that setting as "only when crude oil stocks drew ≥ 2,000 kbbl last week" — accurate, but nobody reads "kbbl" and thinks "two million". Expect people to be off by a factor of a thousand in both directions, and check your own config against a headline before you trust it.
9. Six trades is really three bets
The six fills happened on three days, and on each day the bot bought several rungs of the same WTI settlement ladder — "above $74", "above $75", "above $76", all settling on the same price at the same moment. Those are not independent outcomes; they are one opinion about where oil closes, expressed three times. By day the record is: 1 Jul −$5.00, 8 Jul +$9.96, 5 Aug +$0.87. Three data points, two of them positive. That is not evidence of anything yet, in either direction, and the page would be lying if it implied otherwise.
How to actually use it
Trade the day after the release, not the day of it. This is the one change that would make the signal do what its author intended. The crude figure reaches our table on Thursday's 16:00 UTC poll, so the earliest a bot can act on a genuinely fresh report is day_of_week_in: [3] with hour_of_day_min: 17 — Thursday evening UTC. For a natural-gas bot the report is Thursday morning eastern, so it lands Friday: that is day_of_week_in: [4], same hour floor. You give up any hope of catching the release pop, but you were never catching it anyway — you were trading a week-old number and calling it release day.
Pick a threshold you can justify. Our seven usable crude weeks ran from a 7,167 draw to a 2,998 build. At draw_above: 2000 three of those seven cleared the bar, so the gate is roughly a coin flip; at 5,000 it would have opened twice. Seven weeks is far too thin to calibrate on and we are not going to pretend otherwise — but it is enough to tell you that four-digit thresholds are the right order of magnitude and five-digit ones will switch your bot off permanently.
Match the fuel to the market. Nothing enforces it, so it is on you. Crude draws belong with WTI and Brent markets; natural-gas storage belongs with gas and, in winter, with power and heating markets; distillate is the diesel and heating-oil story. Gating a gas market on a crude number is a perfectly legal config and a meaningless one.
Treat it as a regime filter, not as news. Given catch #2, the honest framing is that this signal tells you which side of a slow inventory cycle you are on for the coming week. That can be a reasonable thing to condition on. It is not a way to trade a headline, and any strategy that needs it to be one is built on sand.
Where to go next
eco_anchor is the other calendar-shaped signal in the library — same idea of trading around a scheduled US release, built from a hardcoded list of dates instead of a feed, and with its own version of the "is this date even right?" problem. finance_anchor is the one to read next if catch #6 interested you: its freshness check is broken in a way this one's is not. sentiment_anchor is the other global mood switch, and the other victim of the same locked database.
There is a second EIA-fed signal in the library that no bot uses: degree_days_anchor, which turns national heating and cooling demand into a gate and refreshes every three hours. It has not been written up yet.
Written 10 August 2026. Every number on this page was read from production on that date: the registry output of quants/signal_registry.py, the source of quants/energy_anchor.py and predictive/eia_poller.py, the live crontab, read-only queries against energy_data in predictive.db and the bot's fill and run records in quants.db, and the EIA's own API and release-schedule pages fetched directly. Two things the registry reports about this signal are wrong and are corrected above: it resolves the poller as db with no schedule, because its regex matches the words "predictive.db" in the module docstring rather than a poller filename; and its "live bots" column counts bots with status oos or live together, so the 1 it reports here is one out-of-sample bot and no live ones. Nothing on this page is a projection.