Trade Bot Dictionary · Stocks & macro

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
How finance_anchor gets from a price feed to a trade decision, and what its freshness check really allows The top half traces the data path. Three sources feed one poller. Stooq, the only source named in our signal registry and in the anchor module's docstring, is drawn crossed out: the exact URL the poller builds has returned HTTP 404 since 7 June 2026, and the log records 18,220 consecutive failures. Yahoo Finance, written as a fail-fast fallback that was not supposed to be needed, now supplies all fifteen index, volatility, commodity, ETF and equity quotes every cycle. FRED, the St Louis Fed's data service, supplies ten Treasury and macro series and is not mentioned in the registry at all. All three run through predictive/finance_poller.py on a cron of every five minutes, which inserts twenty-five rows per cycle into the finance_quotes table in predictive.db; the table holds 466,150 rows going back to 2 June 2026 and is pruned at 120 days. The gate, finance_anchor_satisfied, reads the single newest row for the symbol you named, checks it against a thirty-minute freshness window, and compares its price or change_pct against your threshold. Pass means the bot scans markets as normal; fail means the bot skips the entire scan, because this gate does not depend on any individual market. The bottom half is a ruler measuring staleness in three bands. The first band shows the thirty-minute freshness window the code intends. The second shows what the comparison actually admits: because the stored timestamp is ISO-8601 with a T separator and SQLite's cutoff is written with a space, and the letter T sorts after a space, every row bearing today's UTC date passes — a window that stretches from thirty minutes just after midnight UTC to nearly twenty-four hours just before it. The third band shows how old the underlying numbers themselves are right now, which the gate never checks at all: the VIX and the equity quotes are from today, the Treasury and dollar series are three days old, initial jobless claims nine days, the unemployment rate forty days, CPI seventy days, and GDP growth one hundred and thirty-one days — all of them passing as fresh. SOURCE POLLER TABLE GATE DECISION stooq.com/q/l/ HTTP 404 — 64 days the only source our registry names query1.finance.yahoo.com 15 quotes / cycle S&P · Nasdaq · Dow · VIX · gold · WTI SPY · QQQ · 7 mega-caps api.stlouisfed.org (FRED) 10 series / cycle 10Y · 2Y · fed funds · 2s10s · CPI unemployment · GDP · claims · breakeven · USD finance_poller.py */5 * * * * 25 rows per cycle appends, never updates takes 191 s of its 300 s finance_quotes in predictive.db 466,150 rows from 2 Jun 2026 pruned at 120 days finance_anchor_ satisfied() newest row for YOUR symbol is it < 30 min old? price above / below your level or change_pct above / below one symbol decides everything PASS scan markets normally FAIL skip the whole scan — no trades this cycle HOW OLD IS "FRESH"? 30 min 1 day 3 d 9 d 40 d 70 d 131 d What the code says FRESH_SECS = 30 * 60 a 30-minute window. Older than that, refuse to trade. What it actually admits 'T' sorts after ' ' anything stamped today (UTC) — up to ~24 h. Verified: a row from 00:00:02 UTC passes even a 60-second freshness test. How old the number is quote_date — never checked VIX 0 d 10Y · 2s10s · USD 3 d claims 9 d unemployment 40 d CPI YoY 70 d GDP growth 131 d Every marker in the bottom band passes the gate's freshness check. The check asks when we fetched the number, not what day the number is from.
The data path, and the gap between the freshness the code intends and the freshness it enforces. Readings in the bottom band are the 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:01Z64 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:

SourceSuppliesPer cycleStatus
Yahoo Financequery1.finance.yahoo.com
S&P 500, Nasdaq, Dow, VIX, gold, WTI, SPY, QQQ, and Apple, Tesla, Nvidia, Microsoft, Amazon, Alphabet, Meta
15 quotes15 callscarrying everything
FREDapi.stlouisfed.org, the St Louis Fed
10Y and 2Y Treasury yields, fed funds, 2s10s spread, CPI YoY, unemployment, GDP growth, initial jobless claims, 10Y breakeven inflation, broad dollar index
10 series10 callsworking
Stooqstooq.com/q/l/
was the batched primary for all 15 of Yahoo's instruments in one call
nothing1 call, 404dead 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:

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 dayVIX lowVIX highGate open
22–30 July (7 sessions)16.6420.840%
31 July15.8818.6682%
3 August15.5616.3082%
4 August15.5616.5931%
5 August15.4917.4863%
6 August15.1115.85100%
7 August14.8015.35100%
10 August15.1015.48100%

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 tradesWinsWin rateAvg entryP&LPer trade
VIX-Calm Stocks Reversion out-of-sample 5 of 1631.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:

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.