gdp_surprise_anchor
gdp_surprise_anchor takes two numbers, subtracts one from the other, and lets your bot trade only when the gap is wide enough. The first number is the Atlanta Fed's GDPNow nowcast — its running estimate of how fast the economy is growing this quarter. The second is the last official GDP growth figure the government has actually published, which in practice is always last quarter. Subtract the second from the first and you get what this signal calls the surprise: positive means the economy looks to be speeding up, negative means it looks to be slowing. Right now that sum is 5.833 − 1.500 = +4.333, so at the setting we ship, the switch is open.
Here is the thing worth knowing before anything else, and it is not the sort of catch you can see from the builder. Most of the movement in this signal comes from the number being subtracted, not from the nowcast. Strip out the pandemic and last quarter's figure accounts for 85.1% of the variance in the gap. Across the 2,792 days we could reconstruct, the days this gate was open had an average previous quarter of +1.70%; the days it was shut averaged +3.42%. Read that again, because it is the whole page: the switch labelled "growth accelerating" turns on most reliably when the previous quarter was weak. It is at least as much a bad-quarter-just-ended detector as a good-quarter-underway detector, and nothing in the product says so.
- Bots using it
- 0 none live, none out-of-sample, none archived, none ever
- Data sources
- Two. FRED GDPNOW + FRED A191RL1Q225SBEA
- Refresh
- Nowcast daily 05:30 UTC · actual every 5 min
- Rows stored
- 61 quarters + 19,497 quote rows
- Backtest-replayable
- No — see catch #3
- Anchor module
- quants/gdp_surprise_anchor.py
What the two numbers are
Official American GDP arrives late. The Bureau of Economic Analysis publishes its first estimate of a quarter — the "advance" estimate — about a month after that quarter has finished. So for roughly four months at a stretch, the most-quoted number in macroeconomics describes a period that is already over.
GDPNow is the Atlanta Fed's answer to that gap: a model that assembles a running estimate of the current quarter's growth out of the monthly data that has already landed, and re-runs itself every time a relevant release drops. That is the first leg. The gdpnow_anchor page covers where it comes from and how good it is in detail; everything there applies to the first leg here, and this page will not repeat it.
The second leg is the real thing: BEA's published growth rate for a completed quarter, which we take from FRED series A191RL1Q225SBEA — "Real Gross Domestic Product, percent change from preceding period, seasonally adjusted annual rate". It is the number that appears in headlines as "the economy grew 1.5% last quarter".
Both are quoted the same way — percent change at a seasonally adjusted annual rate, where 2.5 means "if this pace held for a year, the economy would grow 2.5%". One unit is a percentage point (pp). Because the units match, subtracting one from the other is at least arithmetically legitimate, which is more than can be said for some of the comparisons in this library. Whether the result means what the label says is the rest of this page.
Where the data actually comes from
This is the only signal in the dictionary that depends on two separate pollers running on two different schedules. If either one stops, the gate shuts and your bot stops trading entirely. The signal registry only names one of them.
Leg one, the nowcast. FRED series GDPNOW, fetched by predictive/gdpnow_poller.py on cron 30 5 * * * — once a day at 05:30 UTC. It rewrites all 61 rows of macro_nowcast in predictive.db, one row per quarter, 2011 Q3 through 2026 Q3. This morning's run completed normally and stored ('2026-07-01', 5.833). The Atlanta Fed's own page currently reads "5.8% — Third-Quarter GDPNow Estimate for 2026:Q3, Updated: August 06, 2026", so the last fetch matches the public figure. That is all it proves — it says nothing about the other 60 rows.
Leg two, the last actual. FRED series A191RL1Q225SBEA, fetched by predictive/finance_poller.py on cron */5 * * * * — every five minutes, alongside 24 other instruments — and written as a row with symbol='GDPGROWTH' into finance_quotes. That table holds 19,497 GDPGROWTH rows going back to 3 June 2026, which is 96.4% of the 20,222 five-minute slots in that window. The poller asks FRED for the eight most recent observations and keeps the newest one.
Polling a quarterly series every five minutes is not as silly as it sounds — it costs nothing extra, since the poller is already running for the stock and rate quotes — but it does mean the same number is written 288 times a day for three months at a time. The value has changed exactly twice since we started keeping it: 1.6 → 2.1 on 25 June 2026, and 2.1 → 1.5 on 30 July 2026, which is the morning BEA published its advance estimate for Q2.
The gate itself lives in quants/gdp_surprise_anchor.py. It reads the newest row of each table, checks each for staleness, subtracts, and compares the result to your threshold. If either feed is missing or stale it returns False — the bot places no trades at all that cycle, in any market. It is a whole-bot master switch, not a filter on macro markets.
What it does for your bot
One threshold, one switch, whole bot. Set surprise_above: 1 and your bot is allowed to trade on any day the nowcast is running more than a percentage point above the last published figure — and is frozen completely on every other day. Not "trades macro markets differently": frozen. If you also configured it for sports, it will not trade sports either.
At the shipped setting that works out to 37.03% of the days we could evaluate and 27.65% of the calendar over the last decade — about one day in four. That is a real duty cycle, and worth saying plainly given how this dictionary has gone: this is the second Tier-2 signal in a row whose recommended setting is a sensible number rather than one the world has never reached. Three of the six Tier-2 signals written before it ship a builder default that has never once been satisfied in the history we keep.
The more useful way to think about the switch is that it is a level gate that gets re-based a few times a year. The number being subtracted only changes when BEA publishes — 8.1 times a year on average, with a median run of 38 days where it does not move at all, and one run of 141 days. Inside any of those runs, "surprise above 1.0" is arithmetically identical to "nowcast above some fixed level". All the momentum content of this signal arrives in a handful of steps a year, when the baseline jumps.
That does not make it a repackaged gdpnow_anchor, and it is worth being fair about that. We tuned a plain level gate to open on exactly as many days (gdpnow_anchor {above: 3.271}) and compared them day by day: they pick the same days 75.0% of the time and differ on the other quarter. Against the level thresholds we actually ship, the agreement is lower still — 63.9% versus above 2.5, 54.8% versus above 2.0. So there is genuinely something here that a level gate does not capture. The question is whether it is the thing the label promises.
The catch
1. The number being subtracted is doing most of the work
This is the one that changes how you should read the switch. Excluding the pandemic, the last-quarter figure accounts for 85.1% of the variance of the gap; its correlation with the gap is −0.676, against +0.485 for the current-quarter nowcast. In plain terms: the gap widens more reliably because last quarter was bad than because this quarter is good.
The cleanest way to see it is to compare the two states. On the 957 days the gate was open, the average previous quarter had grown +1.70% and the nowcast read +4.41%. On the 1,552 days it was shut, the previous quarter had grown +3.42% and the nowcast read +2.36%. Both legs move, but the previous quarter moves downward by 1.72 pp into the open state while the nowcast moves upward by 2.05 pp. A weak quarter behind you is close to half the signal.
There is a mechanical reason as well as a statistical one. Quarterly growth is measured against the previous quarter's level, so a quarter dragged down by a one-off — a strike, a weather event, an inventory unwind — mechanically raises the next quarter's growth rate even if underlying demand is flat. The signal cannot tell that rebound apart from a genuine pick-up in activity. "Growth accelerating" and "growth was unusually bad three months ago" produce the same reading.
2. Two legs, and neither stored one is what anyone could have seen at the time
gdpnow_anchor has one leg with a lookahead problem: FRED dates each nowcast by the quarter's start and revises it in place, so our table holds the final answer filed under a date thirteen weeks earlier. This signal inherits that leg — and adds a second one with the same disease. finance_quotes keeps only the latest value of A191RL1Q225SBEA, and BEA revises that series repeatedly: advance, second, third, then annual updates that rewrite years at a time.
Measured against FRED's vintage archives, on 94.0% of the 2,792 evaluable days the published figure our table holds is not the one that was actually on the wire that day. The revision averages 0.83 pp and reaches 4.90 pp. Combined with the nowcast leg's own revision (mean 1.64 pp), a replay of our two tables lands a median 1.22 pp away from the live gap, p90 4.17 pp, worst case 21.88 pp.
As a switch, at the shipped threshold, the two disagree on 25.4% of days — and the error leans the opposite way from gdpnow_anchor's. There, the naive replay traded on days the real signal blocked. Here it is mostly the reverse: 19.9% false-shut against 5.5% false-open. That is the less dangerous direction — a replay that under-trades flatters you less than one that over-trades — but it still means a quarter of the days in any study you run on this signal are the wrong days. The second leg does not cancel the first; it makes the combined error larger than the nowcast leg's alone.
3. A backtest containing this signal is not testing it
The string gdp appears zero times in quants/backtest.py — not in the supported list, not even in the commented _UNSUPPORTED_ENTRY block that documents why other anchors are excluded. Only the catch-all k not in _SUPPORTED_ENTRY stops it, so the backtest runs without the gate and hands you an equity curve anyway. You can tune a strategy that was never once tested with the switch you are about to ship it with.
In fairness it is not silent about it: the run flags unsupported_rules and the UI prints "advanced rule(s) not yet replayed". So it is a lie by omission rather than a lie — but the curve still looks like a result. This is the same defect documented on gdpnow_anchor; both GDP signals are missing from that file together.
4. The threshold is smaller than the measurement error of its own inputs
The gate asks whether a difference exceeds 1.0 pp. Consider what 1.0 pp is worth against each leg.
- The nowcast leg: GDPNow's final estimate of a quarter — its most accurate — misses BEA's first published figure by a mean absolute 0.869 pp over the 60 quarters measured on the gdpnow_anchor page. Earlier it is far worse: the gap to the quarter's final nowcast averages 2.43 pp in the first fortnight of nowcasting against 0.50 pp in the last stretch.
- The published leg: BEA's own advance estimate moves a mean 0.492 pp by the third estimate and 0.883 pp by the latest vintage. It exceeds the entire 1.0 pp gate threshold in 28.6% of the 42 quarters since 2016 — 2025 Q4 alone went from +1.4% to +0.5%.
So a threshold of 1.0 pp is roughly one measurement error of either leg. It is not an absurd number — you have to put it somewhere, and the duty cycle it produces is reasonable — but it should not be read as separating economic regimes. A gap of 1.1 and a gap of 0.9 are not distinguishable states of the world.
5. The docstring promises both feeds fail closed. Only one of them does.
The module says, in capitals, "FAILS CLOSED if either feed is missing/stale". The nowcast leg genuinely does: it parses the timestamp into a real Python datetime and rejects a row whose quarter is over 100 days old or whose poller has not run in 3 days.
The published-figure leg does not. Its check is fetched_at >= datetime('now', '-1800 seconds'), which compares our Python ISO timestamps — 2026-08-12T08:25:01+00:00, with a T — to SQLite's space-separated 2026-08-12 08:00:58. SQLite compares them as text, and "T" sorts after " ", so any row bearing today's UTC date passes. Verified live on production at 08:30 UTC: a row stamped 00:00:02 passes the supposed 30-minute test, and 101 rows were inside the "30-minute window". The real window is "since midnight UTC" — 30 minutes at 00:30, nearly 24 hours at 23:30.
The window was earning its keep, too: the GDPGROWTH feed has five gaps over 30 minutes, the longest 85 minutes, and FRED has failed on this specific series 9 times. It does still fail closed after the UTC date rolls over, so a truly dead feed is caught within a day. This is the same defect already documented on finance_anchor, tropical_anchor and funding_anchor. Not fixed here — this loop does not edit anchors or pollers — but Dave has it.
6. It is called a surprise, and it is not one
In macroeconomics a "surprise" is a published number minus what forecasters expected — the news content of a release, which is what markets actually reprice on. Every surprise index you have heard of is built that way. This signal holds no forecast and no consensus of any kind. It computes a nowcast minus a different quarter's realised figure, which is an acceleration, not a surprise.
Credit where due: the module docstring says exactly that — it calls it "the change in momentum" — and the builder's own description spells out the arithmetic honestly ("the live Atlanta Fed GDPNow nowcast minus the last official GDP print"). The defect is the label sitting on top of an accurate description. A user who reads the field name and skips the paragraph will think they have switched on "trade when GDP beats expectations". They have switched on "trade when this quarter's model output exceeds last quarter's published number by a percentage point" — which can be true in an economy nobody is surprised by, and false in one that has just shocked everybody.
7. The clock in the quarter decides more than you would expect
The gate is open 48.0% of days 30–44 into a quarter and only 25.4% of days 90 and beyond — roughly twice as likely to let you trade early in a quarter as late in one. The average gap follows the same arc: +0.89 pp on days 30–44, drifting down to −0.68 pp on days 90 and later. In other words GDPNow's early readings tend to sit above where the quarter eventually lands, so the gate is at its most permissive when the model has the least data. Why the early readings run high is not something we can settle from our own records, and this page is not going to guess — but the pattern is there in all 2,509 non-pandemic days, and nothing in the product mentions it.
On top of that, the gate is completely dark on 25.35% of all calendar days — 948 of the 3,740 modelled — because the previous quarter's row ages past the 100-day limit around day 8 of a new quarter while the new quarter's first nowcast does not land until about day 30. That behaviour is correct: refusing to treat a 92-day-old final as "now" is exactly right. The defect is that it is silent — no field note, no warning when you save, no log line. Four whole months in our window (Feb 2019, Nov 2025, Feb 2026, Mar 2026) had not one single day on which this gate could be evaluated.
8. What the registry says, and what is actually true
quants/signal_registry --json resolves this signal to "poller": "gdpnow_poller" and three tables: finance_quotes, macro_nowcast, and gdp_surprise_anchor. Two corrections.
- There is no table called gdp_surprise_anchor. The registry harvests backticked words from the docstring, and the docstring backticks the signal's own name in the sentence "bots that explicitly set gdp_surprise_anchor". A phantom, not a fabrication — but it is on the list.
- Naming one poller understates the dependency. This gate needs gdpnow_poller and finance_poller, on completely different schedules. If finance_poller stops, this signal fails closed and every bot carrying it goes silent — and nothing in the registry, the health check or the builder would point you at the right feed.
This is a different failure mode from the predictive.db regex artefact that hit five other signals: here the registry simply has no concept of a signal with two upstreams. The rest of what it reports — cron, replayable false, live_bots 0 — is correct, and here 0 really does mean 0.
9. Configurations that validate but cannot mean what they say
- validate_config bounds thresholds to ±20 pp, so {surprise_below: -30} is rejected — although the gap we reconstructed ranged from −48.8 to +66.7 in 2020. A user trying to write a rule about the only period in the record where this signal moved dramatically is told the number is out of range. The bound was copied from the level gate, where ±20 is roomy; a difference of two growth rates has about twice the range.
- {surprise_above: -5} validates and to_english renders it "GDPNow is running > -5pp ABOVE the last actual GDP print (growth accelerating)". It is a double negative describing a decelerating economy, labelled accelerating, and it is open almost always.
- {surprise_above: 3, surprise_below: -1} validates, is unsatisfiable, and reads back as "accelerating and cooling". The builder at least prints both clauses, which is better than funding_anchor, where the second one is silently hidden. This same validator gap now appears on six pages of this dictionary.
The case for the defence
The bug I went looking for is not there
The obvious way for a two-legged signal like this to break is for both legs to end up describing the same quarter, at which point you are not measuring acceleration at all — you are measuring GDPNow's own forecast error, which is near zero by construction. There is a plausible window for it every quarter: BEA publishes the advance figure for a quarter about 30 days after it ends, and GDPNow does not always start the next quarter immediately.
I reconstructed all 3,740 days and checked. It never happens. On all 2,792 evaluable days the two legs are exactly one quarter apart. There are 135 days in the record where they would have collided — and every one of them is blocked, because BEA's advance figure lands around day 122 of the nowcast's quarter while the staleness rule cuts the nowcast off at day 100. About three weeks of margin, every quarter, structurally.
Worth saying that the code does not know this is why. The 100-day limit is justified in the comments as "the obs is within ~1 quarter" — nothing about alignment — and the anchor never compares the two quarters even though finance_quotes stores quote_date in the same row it reads. It is protected by accident, with a comfortable margin. Raising that constant would break it silently.
Four more things it gets right
- The shipped threshold is well chosen. surprise_above: 1 — the builder default, the "Growth accelerating" recipe and the "Growth-Acceleration Watch" starter all use it — opens on 37.03% of evaluable days, and 38.14% with the pandemic excluded, so the figure is not an artefact of 2020. Second Tier-2 signal running with a sane default.
- It is the steadiest gate in the library. It changes state 3.9 times a year. Compare onchain_fees_anchor at 20.9 times a day. There is no hysteresis anywhere in the anchor set and this one does not need any.
- The revision hazard is latent, not active. A fair worry about a difference gate is that a rewrite of last quarter's history could flip it with no new information about this quarter. Of the 30 state changes in the record, 0 were caused by the published leg alone — 29 by the nowcast, 1 by both. Real hazard, has not fired.
- Both feeds are healthy today. The nowcast poller has crashed twice in its life, both on our own database lock, and recovered after ticker_index was rewritten — this morning's 05:30 run wrote all 61 rows. The five-minute leg is at 96.4% coverage with 9 vendor failures in 19,497 rows.
Has it ever made any money?
No, and not because it lost — because nothing has ever used it. Zero of our 76 strategy configurations (21 live, 15 out-of-sample, 36 archived, 4 draft) reference gdp_surprise_anchor, and zero of 88 saved strategy versions. No trades, no fills, no evaluations, no profit and loss. There is no track record on this page because there is no track record to publish.
The site does ship it in three places, so it is one click from being used: the builder's macro field, a recipe called "Growth accelerating", and a starter bot called "Growth-Acceleration Watch" that buys YES on economics markets priced 30¢–60¢. All three use surprise_above: 1. None has ever been activated.
That absence is worth one honest sentence. This is a gate, not an edge. It decides when you are allowed to trade, never what to trade or which side to take. Even a perfectly specified acceleration filter can only concentrate a strategy's existing edge into a subset of days — and if the strategy has no edge, a filter cannot manufacture one. The most that could be claimed for it is that macro prediction markets behave differently in an accelerating economy than a decelerating one, and we have never run the experiment that would show it.
How to actually use it
- Read it as "rebound", not "boom". The gate opens most reliably when the previous quarter was weak. If your thesis needs the economy to be strong in absolute terms, use gdpnow_anchor's level gate — that is what it measures. If your thesis is about a turn, this one is closer.
- Do not stack it with the level gate expecting confirmation. At the settings we ship, the two agree on only 63.9% of days. Two gates that disagree a third of the time will produce a bot that trades far less than either of you expected.
- Do not trust a backtest of it. The gate is not applied at all, and both stored legs are revised values that nobody could have seen. Between them that is a quarter of the days wrong.
- Expect roughly one day in four, clustered. 27.65% of the calendar, arriving in runs rather than scattered — heavier in the first half of a quarter, absent for about a month around every quarter boundary.
- Read it in percentage points, not percent. 1 means one percentage point of annualised growth rate, not 1% more than last quarter.
How this page was checked. Every figure comes from one of four places, all read on 12 August 2026: the module source (quants/gdp_surprise_anchor.py, quants/backtest.py, predictive/finance_poller.py, predictive/gdpnow_poller.py); production predictive.db and quants.db; FRED's live API, including the vintage archives that let us rebuild what each leg said on any past day (realtime_start/realtime_end, 824 archived GDPNow revisions and 907 GDP revisions); and the Atlanta Fed's own GDPNow page. The ten-year reconstruction covers 17 May 2016 — the first archived GDPNow vintage — to today. Nothing on this page is estimated, and where a measurement could not be made it says so.