finance_anchor
finance_anchor is a master switch wired to one number from the outside world. You name a symbol — the VIX, the S&P 500, gold, WTI crude, one of seven mega-cap stocks, or one of ten Treasury and macro series — and a condition, like "below 16". Every cycle the bot looks up the latest quote for that one symbol. If the condition holds, the bot goes on to do whatever else it was going to do. If it doesn't, the bot places no trades at all that cycle, in any market. It is the "only trade when the weather outside is X" signal, where the weather is a financial market.
- Bots using it
- 1 (out-of-sample; none live)
- Data source
- Yahoo Finance + FRED (not Stooq — see below)
- Refresh
- Every 5 minutes
- Symbols you can gate on
- 23 of 25 we poll
- Backtest-replayable
- No — see catch #7
- Anchor module
- quants/finance_anchor.py
quote_date values on the newest row of each series as of 10 August 2026,
20:20 UTC. The ruler is not linear — it is spaced to fit the labels.
Where the data comes from
Our own signal registry answers this in one word:
Stooq. So does the docstring at the top of quants/finance_anchor.py, which reads
"Data source: predictive/finance_poller.py (Stooq batch, */5 cron)."
That has not been true since 7 June 2026. We checked before writing it down, which is the whole point
of these pages. The last row in our database that came from Stooq is stamped
2026-06-07T06:10:01Z — 64 days ago. Running the exact URL the poller builds, from the
production box, today, returns HTTP 404 and a Stooq page reading "The page you requested
does not exist or has been moved." The poller's own log has recorded
18,220 consecutive stooq fetch failed: HTTP Error 404 lines.
Nothing broke, because the poller was written with a fallback. So here is what actually feeds the gate:
| Source | Supplies | Per cycle | Status |
|---|---|---|---|
Yahoo Finance — query1.finance.yahoo.comS&P 500, Nasdaq, Dow, VIX, gold, WTI, SPY, QQQ, and Apple, Tesla, Nvidia, Microsoft, Amazon, Alphabet, Meta |
15 quotes | 15 calls | carrying everything |
FRED — api.stlouisfed.org, the St Louis Fed10Y and 2Y Treasury yields, fed funds, 2s10s spread, CPI YoY, unemployment, GDP growth, initial jobless claims, 10Y breakeven inflation, broad dollar index |
10 series | 10 calls | working |
Stooq — stooq.com/q/l/was the batched primary for all 15 of Yahoo's instruments in one call |
nothing | 1 call, 404 | dead 64 days |
Both live sources are free and need no paid plan; FRED wants a free API key, which sits in the prod
.env. The poller is predictive/finance_poller.py on cron
*/5 * * * *, and it writes 25 rows per cycle — one per instrument — into the
finance_quotes table in predictive.db. The table is append-only and now holds
466,150 rows back to 2 June 2026, pruned at 120 days.
Two consequences of the Stooq outage are worth stating plainly, because neither is visible from the outside:
- The poller now takes 191 seconds instead of half a second. Stooq was one batched CSV call for fifteen instruments. Yahoo is one call each, and the module's own comment notes that Yahoo "throttles datacenter IPs to ~45s/call", which is why it was given a 6-second fail-fast timeout and described as a fallback that "is not hit while every instrument below has a Stooq ticker". It is now hit fifteen times a cycle. Across the last 300 runs: mean 191.1 s, max 255.6 s — against a cron interval of 300 s. Seven runs out of 19,302 have gone over.
-
The meaning of
change_pctsilently changed that day. For a Stooq row the poller computed it as(close − open) / open— the move from this morning's open. For a Yahoo row it usespreviousClose— the move from last night's close. Those are different numbers, and on a gapping open they are very different. Any gate written as "S&P up more than 1% today" was measuring one thing before 7 June and another thing after, with nothing in the config changed.
What it means for your bot
You are not adding a forecast. You are adding a condition of entry. Two fields, one of each:
a symbol, and at least one of above, below,
change_pct_above or change_pct_below. In the builder it reads back to you in
English — {"symbol":"^VIX","below":16} becomes "only when VIX < 16".
The symbol names are forgiving. There is an alias table, so VIX, SPX,
S&P500, GOLD, OIL, DXY, CURVE,
CPI, CLAIMS and about sixty other spellings all resolve to the right canonical
ticker. Twenty-three symbols are gateable. Two that we poll — SPY and QQQ — are not: they were
added for the price-bracket matching layer, not for this gate, and asking for them returns
"unknown symbol 'SPY'".
Because the condition depends only on an outside number and never on the market in front of it, the engine treats it as a global short-circuit: when the condition fails, the bot skips the entire market scan rather than testing candidates one at a time. That makes it cheap, and blunt. Compare resolution_anchor, which judges each market on its own merits, or eco_anchor, which is the same shape of switch but wired to a clock instead of a price.
The one bot that uses it
Our registry reports "1 live bot". That count includes anything with status oos or
live, and for this signal it is one out-of-sample bot and none live — the same
bookkeeping quirk we flagged on the
eco_anchor page.
VIX-Calm Stocks Reversion, created 11 June 2026, is a mean-reversion bot on stock-market brackets:
buy YES between 25¢ and 60¢ after the ask has dropped 8¢ or more in 30 minutes, $5 flat, up to six fills a
day, hold to expiry. The finance_anchor gate is {"symbol":"^VIX","below":16}, and
the thesis written into its deploy note is explicit: with realised macro volatility low, an 8¢ drop in an
index market is book noise, not informed flow. In calm weather, fade the drop.
How often the light is green
Across all 19,310 VIX snapshots we have — 2 June to 10 August 2026, VIX ranging 14.80 to 23.34 — the
condition VIX < 16 held 27.3% of the time. But that average badly misdescribes what
it feels like, because volatility is sticky. The gate does not sprinkle a quarter of your trades evenly
across the calendar. It opens and shuts in blocks of days:
| Trading day | VIX low | VIX high | Gate open |
|---|---|---|---|
| 22–30 July (7 sessions) | 16.64 | 20.84 | 0% |
| 31 July | 15.88 | 18.66 | 82% |
| 3 August | 15.56 | 16.30 | 82% |
| 4 August | 15.56 | 16.59 | 31% |
| 5 August | 15.49 | 17.48 | 63% |
| 6 August | 15.11 | 15.85 | 100% |
| 7 August | 14.80 | 15.35 | 100% |
| 10 August | 15.10 | 15.48 | 100% |
For seven straight sessions in late July this bot could not trade at all. Then the VIX came down and it was switched fully on for days. That is what a level gate does: it turns your strategy into a regime-clustered strategy, and it means your results depend heavily on which few weeks you happened to be measuring.
What it has actually done
Nineteen trades between 16 June and 7 August 2026 — sixteen settled, three still open. All of them on Polymarket, despite the bot being configured for both books. Here is the whole record; there is no other:
| Settled trades | Wins | Win rate | Avg entry | P&L | Per trade |
|---|---|---|---|---|---|
| VIX-Calm Stocks Reversion out-of-sample | 5 of 16 | 31.2% | 46.6¢ | −$20.73 | −$1.30 |
Read that against the price, not against 50%. A binary bought at 46.6¢ needs to win about 46.6% of the time just to break even — that is the lesson from the resolution_anchor page. It won 31.2%. The gap is negative, and the losses are not a fee problem or a variance story dressed up: eleven of sixteen went to zero.
And sixteen trades is not a track record. We are publishing it because it is what exists, not because it settles anything. The bot's own deploy note set a falsifier — retire it if expectancy is still negative after 30 settles. It is at 16, and expectancy is −$1.30. On current evidence it is heading for the exit, but it has not got there yet.
One detail worth looking at, because it is not the gate's fault and is easy to miss. The three positions still open are "Will Apple be the third-largest company in the world by market cap on August 31?", "Will Meta be the #2 AI lab at the end of August 2026?" and "Will Meta have the #3 AI model at the end of August 2026?" Those are opinion markets on corporate league tables. They are not price brackets, nothing about them mean-reverts on a 30-minute ask drop, and the "book noise" thesis does not apply to them at all. They got in because they are filed under stocks. The VIX gate was working perfectly; the category filter is what defines the universe, and it is wider than the idea.
The catch
This is the most-used signal shape in the library and it has the longest list of them. Most are not about the idea, which is sound — they are about the plumbing underneath it.
1. The source of record is wrong, everywhere
The registry says Stooq. The module docstring says Stooq. Stooq has been 404 for 64 days, and the two sources actually feeding the gate — Yahoo Finance and FRED — appear in neither. FRED supplies ten of the twenty-five instruments, including every macro series, and is not mentioned in the registry at all. If you were auditing this signal from our own documentation you would audit the wrong vendor.
2. The freshness check does not check freshness
The gate is meant to refuse to trade on a stale quote: FRESH_SECS = 30 * 60, and the docstring
promises it "fails closed on stale/missing data". The SQL compares the stored timestamp against
datetime('now', '-1800 seconds'). But the stored value is ISO-8601 —
2026-08-10T20:20:01.653805+00:00 — and SQLite's datetime() returns
2026-08-10 19:55:39. It is a string comparison, and 'T' sorts after a
space, so every row bearing today's UTC date wins.
We ran it. A row stamped 00:00:02Z passes not only the 30-minute test but a
60-second one. Asked for rows inside its "30-minute window", the gate returned
5,919 rows spanning 237 polling cycles.
In normal operation this changes nothing, because the query still takes the newest row and the poller is healthy. It matters when the poller dies. Instead of standing down after 30 minutes, the bot keeps trading on the last quote it ever saw — for the rest of the UTC day, up to nearly 24 hours. It does start failing closed correctly after midnight UTC, when the date string finally rolls over. So the real freshness window is not 30 minutes; it is "since midnight", which is 30 minutes at 00:30 UTC and 23½ hours at 23:30. This is a live defect in a gate a bot depends on, not a documentation quibble. Fixing it means editing the anchor, which is outside what this dictionary loop is allowed to touch — so it is written up here and flagged for a maintenance pass rather than quietly patched.
3. "Fresh" means when we fetched it, not what day it is from
Every row carries a quote_date — the date the number actually belongs to. The gate never
reads it. So for the fast-moving symbols freshness roughly means what you would hope, and for the macro
series it means almost nothing. As of the moment this page was written, all of these were passing as fresh:
- GDP growth — 1.5%, from 1 April 2026. 131 days old.
- CPI YoY — 3.46%, from 1 June. 70 days old.
- Unemployment — 4.1%, from 1 July. 40 days old.
- Initial jobless claims — 199,000, from 1 August. 9 days old.
- 10Y, 2Y, fed funds, 2s10s, breakeven, dollar index — from 7 August. 3 days old, which is simply FRED's publication lag over a weekend.
None of that is wrong data — it is the latest published value, which is the best anyone has. The problem is
the label. A gate on GDPGROWTH is not reacting to anything; it is a constant that changes four
times a year, re-inserted 288 times a day. There is nothing wrong with gating on a constant, as long as you
know that is what you are doing.
A related quirk, harmless but worth knowing: markets close. On Saturday 8 and Sunday 9 August the poller
wrote 283 and 284 VIX rows, every one of them Friday's close of 14.90 with quote_date 7
August. Over a weekend the gate reads Friday and calls it now.
4. change_pct on a monthly or quarterly series is meaningless
The poller computes change_pct for a FRED series as the percent change between its
last two observations. For a daily series that is a day's move. For CPI it is a month. For GDP it
is a quarter. Our current GDP row carries change_pct = −28.571, and the builder will happily
render {"symbol":"GDP","change_pct_below":-5} back to you as
"only when GDP growth down > 5% today".
Nothing happened today. That −28.571 is the step between two quarterly prints, restated every five minutes for three months. A gate written that way is not a condition; it is permanently open or permanently shut until the next release, and it reads as if it were tracking something.
5. The units are whatever the publisher uses, and nothing checks them
validate_config checks that your symbol exists and your threshold is a number. It does not and
cannot know what scale you meant. Two real examples, tested against today's values:
-
{"symbol":"CLAIMS","below":300}validates, and reads back as "only when initial jobless claims < 300". Claims are stored as 199,000 — people, not thousands. The gate is permanently shut and the bot will never trade again. -
{"symbol":"2S10S","below":50}validates, and reads back as "only when 2s10s spread < 50". The spread is stored as 0.46 — percentage points, not basis points. The gate is permanently open and does nothing at all.
Both look completely reasonable in English, which is exactly what makes them dangerous. Before you trust a threshold, look up the number the way we store it. The same trap is waiting in CPI (3.46, percent) and the dollar index (119.06, an index where 100 is January 2006).
In the same spirit: above and below are combined with AND, so
{"symbol":"^VIX","above":20,"below":10} passes validation and reads back as "only when VIX
> 20 and VIX < 10". Nothing warns you that you have built a gate that can never open.
6. There is no hysteresis, so a threshold near the current level chatters
The condition is re-tested from scratch every cycle, with no band, no confirmation and no minimum hold. If the level you picked sits near where the market is trading, the bot flickers between fully on and fully off. On 4 August the VIX ran 15.56 to 16.59 and crossed the 16 line three times in 281 snapshots — which is why that day shows 31% open in the table above rather than 0% or 100%. Whether your bot traded depended on which side of a coin flip the tape was on when the cron happened to fire.
7. The backtester cannot replay it
finance_anchor is on the unsupported list in quants/backtest.py, added 4 June
2026 with the note that these anchors "live in the engine, but each needs a forward-captured series we
don't have retroactively". A backtest of a bot that uses this gate runs without the gate and still
returns a curve; the result is flagged in unsupported_rules, so the interface can be honest
about it, but the number you are looking at is not the strategy you wrote.
There is a real irony here. The poller was built for this: its docstring says the table "doubles as forward-price-history so backtests can replay finance gates from the day this ships", and it has now appended 466,150 rows over 69 days, exactly as promised. The history exists. The replay loop just never got wired to read it. Of the signals written up so far only eco_anchor is genuinely replayable.
8. It is a switch on the whole bot, not a filter on related markets
This one is design, not defect, but it is the assumption people get wrong. A low VIX does not make a
weather contract or a baseball game a better bet. If you attach {"symbol":"^VIX","below":16}
to a bot trading MLB, you have not filtered anything about baseball — you have just made your baseball bot
take the days off when equity volatility is high. Sometimes that is genuinely what you want. It should be
a decision, not a side effect.
How to actually use it
None of the above makes it a bad signal. It is the cleanest regime switch we have, and the fast symbols — VIX, the three indices, gold, WTI, the seven mega-caps — are genuinely current to within five minutes. A short list of things that follow from this page:
- Prefer the fast symbols. VIX, the indices, gold, oil and the mega-caps are same-day. The FRED macro series are levels that change monthly or quarterly; gate on them if you want a regime constant, not if you want a reaction.
- Look up the number before you pick the threshold. Catch #5 is the easiest way to build a bot that silently never trades. Check the scale we store, not the scale you read in a headline.
-
Use
aboveandbelow, and be wary ofchange_pct. Level gates are unambiguous. The percent-move fields changed meaning on 7 June and mean something different again on monthly and quarterly series. - Expect clustered results, and don't read a win rate from one regime as an edge. You only ever trade in the state the gate allows, so your sample is conditional by construction. Compare it against the entry price, not against 50%.
- Set the threshold away from where the market is sitting unless you want the chatter in catch #6.
- Don't ask a backtest whether the gate works. It cannot answer. Forward-test it.
Where to go next
eco_anchor is the same shape of global switch, wired to the macro calendar instead of a price — and it pairs naturally with this one. resolution_anchor is where the "compare it to the price, not to 50%" argument is made properly. arb_anchor and wx_anchor are the two signals with real trade volume behind them.
Written 10 August 2026. Every number on this page comes from the output of
quants/signal_registry.py, the source of quants/finance_anchor.py and
predictive/finance_poller.py, a read-only query against predictive.db or
quants.db, the poller's own log, or a live request to the vendor made from the production box on
the day of writing. Price and P&L figures cover 2 June – 10 August 2026 and are
simulated. Sixteen settled trades is not a track record and is not
offered as one. If you find something here that is wrong, it is a bug — tell us.