Trade Bot Dictionary · Sports

injury_anchor

injury_anchor reads the market's title, works out which sports team it names, and lets your bot trade only if that team currently has enough players on the injury report. Say you want to fade a side that is missing bodies: this is the switch that finds them. It reads ESPN's public injury pages for baseball and basketball, free and without a key, and it is one of the more genuinely appealing ideas in the library — availability really does move sports prices, and almost nothing else we poll is that close to the actual game.

Then you look at what it is reading. The table it queries is never pruned. When a player comes off ESPN's list — recovered, activated, released — our row for him survives, with his old status frozen in place, indefinitely. Right now 637 of the 993 rows are players ESPN has stopped listing, some of them since May, and the gate does not look at the timestamp at all. On top of that, the dictionary that turns "Miami Heat" into an ESPN team number is wrong for 10 of the 30 NBA teams and 4 of the 30 in baseball, so even the rows it does find often belong to a different city.

The clearest way to say it: at the setting the builder produces, every single baseball market this gate admits today is admitted on the strength of a player who is no longer injured. Not most. All of them — 68 of 160 open games pass, and 0 of those 68 would pass if the query were restricted to rows collected in the last six hours. No bot has ever used this signal, which is the only reason none of it has cost anything.

Bots using it
0 none live, none out-of-sample, none archived, none ever
Data source
ESPN public injuries API — free, keyless, MLB + NBA
Refresh
Every 6 h 13 */6 * * *
Rows stored
993 — 637 of them stale
Backtest-replayable
No — see catch #7
Anchor module
quants/injury_anchor.py

What it actually does

Every other signal in this dictionary is either a whole-bot master switch — one number for the whole world, on or off — or a filter tied to a specific market's own price. injury_anchor is the rare third thing: a per-market filter driven by the words in the market's title. That makes it more interesting than most of the shelf, and also gives it a failure mode none of the others have.

The mechanics are three steps. First it scans the title for team names, matching abbreviation, nickname or city — BOS, Celtics and Boston all resolve to the same team. Second, for each team it found, it counts the rows in our injury_reports table whose status is one you asked for. Third, if any matched team's count reaches your threshold, the gate opens and the bot may trade that market; otherwise it moves on. A title naming no team it recognises always fails, which is the right default.

The configuration is three fields:

Where the data actually comes from

ESPN. Specifically site.web.api.espn.com/apis/site/v2/sports/baseball/mlb/injuries and its basketball twin. No key, no registration, no quota we have ever hit. Checked live from the production box while writing this: HTTP 200 in 0.73 s for baseball, 0.57 s for basketball, returning 279 players across all 30 MLB teams and 77 across 27 NBA teams. Each entry carries the player, the team, a status string, a date and a short comment.

Worth flagging up front, because the registry gets this wrong: the signal registry reports this signal's source as "MLB feeds". It is not MLB's feed, it is ESPN's, and it is not only baseball — the module ships a full alias table for all 30 NBA teams and the builder defaults to basketball. That artefact is explained at the bottom of this page.

The fetch is done by predictive/injury_poller.py on cron 13 */6 * * * — four times a day, at 00:13, 06:13, 12:13 and 18:13 UTC. It writes into injury_reports in predictive.db, keyed on (league, team_id, athlete_name), using INSERT OR REPLACE. This morning's 06:13 run finished in 6.95 seconds and wrote 356 rows.

Two facts about that poller are worth putting side by side. Over its life it has run 301 times: 240 completed, and 61 died on database is locked — our own SQLite contention, the same affliction documented on the funding_anchor and onchain_fees_anchor pages. ESPN, meanwhile, has failed us exactly zero times. Not one HTTP error, not one timeout, in 301 attempts. The vendor is the most reliable thing in this entire pipeline.

And then there is what the poller does not do. It never deletes. ESPN's endpoint is a snapshot — here is who is hurt right now — but we treat it as an append. Every row that has ever appeared is still there. That single missing line is the subject of most of this page.

How injury_anchor moves data from ESPN's public injury pages into a trade decision, why two thirds of the rows it reads are players who are no longer injured, how the team-number map is rotated by one slot so a third of the NBA resolves to the wrong franchise, and how the only status the builder can ask for compares with the statuses ESPN actually sends The first band traces the data path from left to right in five boxes: ESPN's public injuries API, free and keyless; the poller injury_poller.py running four times a day on cron minute 13; the injury_reports table holding 993 rows and never pruned; the gate itself, which reads the market title, resolves a team and counts rows; and finally the bot, which trades or skips. Notes under the boxes record that ESPN has failed zero times in 301 poller runs while 61 of those runs died on our own database lock, that 637 of the 993 rows are stale, that the gate reads no timestamp at all, and that no bot is downstream. Beneath the path, three boxes explain the mechanism that creates the stale rows: ESPN drops a player once he recovers, the poller only ever inserts or replaces and never deletes, so the old row survives with its old status for ever. The second band is a single horizontal bar representing all 993 rows, divided by how long ago each row was last refreshed. Three hundred and sixty rows were refreshed by this morning's poll; 117 are one to seven days stale; 153 are eight to thirty days stale; and 363 rows, the largest single block, have not been refreshed for 31 days or more. Below the bar, two panels. On the left, the twelve baseball rows still marked with the status Out, listed with the date each was last seen, the oldest from 2 June — and the note that ESPN lists none of these twelve players anywhere on its injury report today. On the right, the consequence: at the default setting, 68 of the 160 open baseball game markets pass the gate as the table stands, and zero of them pass if only rows collected in the last six hours are counted. The third band shows the team-number defect. Ten rows on the left give an ESPN team id and the team the module believes it refers to; ten rows on the right give the same ids and the team ESPN actually means. Faint diagonal lines connect each left-hand row to the right-hand row one slot above it, and one long line wraps from the top of the left column to the bottom of the right, showing that the whole block is rotated by exactly one position. Read down the left column the cities are in perfect alphabetical order, from Memphis to Utah; ESPN's numbering is not alphabetical, because it was assigned by franchise history. To the right, three bars measure the damage on today's markets: the gate as shipped admits 22 markets, with corrected ids it would admit 48, and the two versions agree on only 12 — so 10 of the 22 markets it currently admits are admitted on another team's injury list. Beneath, the four baseball ids that are wrong, which form a closed four-way cycle between Toronto, Atlanta, Washington and Arizona. The fourth band compares vocabularies. On the left, what you can ask for: the builder offers only the status Out, and the module's documentation additionally promises Questionable, Doubtful, Out For Season, Probable and Game Time Decision — each marked with zero, because none of those five strings has ever appeared in the table. On the right, what ESPN actually sends for baseball, drawn as bars: 60-Day-IL 166 players, 15-Day-IL 50, 10-Day-IL 48, Day-To-Day 14, suspension 1, and Out zero. A strip along the bottom shows that even with the correct statuses selected, the threshold barely discriminates: requiring one player admits 152 of 160 games, five players admits 146, and it takes ten before the gate excludes about half the board, because every one of the 30 baseball teams has between 2 and 14 players on the injured list at any moment. 1 · THE PATH ESPN /sports/…/injuries free · keyless · 0.6 s injury_poller 13 */6 * * * 4 × a day injury_reports 993 rows never pruned the gate title → team → count ≥ min_out_count ? your bot trade this market · or skip 0 bots, ever 0 vendor failures in 301 runs 61 runs died on our own DB lock 637 of 993 rows are stale reads no timestamp at all a player recovers ESPN drops him from the snapshot the poller writes what it sees INSERT OR REPLACE · no DELETE his old row survives, unchanged and still counts, for ever 2 · THE GRAVEYARD — ALL 993 ROWS, BY HOW LONG AGO EACH WAS LAST REFRESHED 360 refreshed this morning 117 1–7 days 153 8–30 days 363 not seen for 31 days or more ▏current oldest row last refreshed 29 May — the poller's first ever run ▏ 637 of 993 rows (64.2%) are players ESPN no longer lists. The gate counts every one of them. The 12 baseball rows still marked "Out" the status the builder defaults to · date = last seen by our poller Yu DarvishSD29 Jul Brendan RodgersBOS2 Jul Kendall GeorgeLAD2 Jul Carson SpiersCIN2 Jul Declan CroninTEX2 Jul Bryan AbreuHOU25 Jun Robert StockNYM2 Jul Jesus TinocoMIA2 Jul Ryan LaskoATH2 Jul Wei-En LinATH2 Jul Nabil CrismattTEX2 Jul C. MlodzinskiPIT2 Jun ESPN lists none of these twelve anywhere today. What passes, of the 160 open baseball games the builder's default setting: 1+ player, status "Out" as the table stands 68 markets counting only rows from the last 6 hours 0 markets Every market it admits is admitted on a healed player. 3 · THE WRONG TEAM — THE ID MAP IS ROTATED BY EXACTLY ONE SLOT what the module believes read the cities down: perfectly alphabetical what ESPN actually means numbered by franchise history, not by alphabet id 14 → Grizzlies · Memphis id 15 → Heat · Miami id 16 → Bucks · Milwaukee id 18 → Timberwolves · Minnesota id 19 → Knicks · New York id 20 → Magic · Orlando id 21 → 76ers · Philadelphia id 22 → Suns · Phoenix id 26 → Blazers · Portland id 29 → Jazz · Utah id 14 = Miami Heat id 15 = Milwaukee Bucks id 16 = Minnesota T'wolves id 18 = New York Knicks id 19 = Orlando Magic id 20 = Philadelphia 76ers id 21 = Phoenix Suns id 22 = Portland Blazers id 26 = Utah Jazz id 29 = Memphis Grizzlies Every name sits one slot too low. Someone sorted the league alphabetically; ESPN had numbered it by franchise history. Today's markets basketball gate, default as shipped 22 with correct ids 48 the two agree on 12 so 10 of the 22 it lets through are the wrong team's injuries. Baseball has the same disease in a smaller, closed four-way cycle: id 14 → module: Braves · Atlanta = ESPN: Toronto Blue Jays id 20 → module: Blue Jays · Toronto = ESPN: Washington id 15 → module: D-backs · Arizona = ESPN: Atlanta Braves id 29 → module: Nationals · Wash. = ESPN: Arizona 4 · WHAT YOU CAN ASK FOR vs WHAT ESPN ACTUALLY SENDS What you can ask for the builder offers exactly one status, and no way to change it Out 0 baseball players have this status and five more the module's own documentation promises: Questionable Doubtful Probable Out For Season Game Time Decision none of the five exists What ESPN actually sends for baseball, right now 279 players across all 30 teams 60-Day-IL 15-Day-IL 10-Day-IL Day-To-Day suspension Out 166 50 48 14 1 0 ← the one the builder asks for And even with the right statuses selected, the threshold barely filters anything open baseball games admitted, out of 160 — because every one of the 30 teams has 2 to 14 players on the IL at any moment (median 9) min 1 152 min 5 146 min 10 82 Sources: ESPN public injuries API (live, 12 Aug 2026) · predictive.db injury_reports, 993 rows · kalshi_markets, 7,414 open markets · quants/injury_anchor.py · quants/engine.py
Every number is measured, none estimated. Band 1 is the data path and the one missing line — DELETE — that produces band 2. Band 2 is the whole table sorted by age: the blue block on the left is what ESPN says today, everything to the right of it is memory. Band 3 shows the team-number map rotated by one slot, which is what happens when you alphabetise a list somebody else numbered by history. Band 4 compares the single status the builder can ask for against the six ESPN actually sends.

What it does for your bot

In principle: it narrows your bot's universe to games where someone is missing, and lets you take a side on that. Fade the depleted team, or back the total to go under, or buy the healthy opponent. The shipped recipe in the builder is called "Injury contrarian" and it does exactly the first of those — side NO, basketball, one player out.

In practice, today, it does something else. Because the switch is per-market and title-driven, and because the table behind it is mostly memory, what it really produces is a fairly arbitrary subset of the board. Here is the concrete example, and it is the one I would show anybody who thinks a stale table is a small problem.

At the builder's default settings for baseball, the gate admits 86 open markets. Sixty-eight are baseball games. The other eighteen are Miami weather markets — "Will the high temp in Miami be 90-91° on Aug 11?" — because the title contains the word "Miami", which the alias table maps to the Marlins, and the Marlins have one row saying Out: the pitcher Jesus Tinoco, last seen on ESPN's list on 2 July, injured 8 May, and not on their report at all today.

So a single forty-day-old row about a relief pitcher is currently the thing standing between an injury bot and eighteen questions about the temperature in Florida. That is what "the gate has no clock" means in practice. (In fairness — and this matters — the weather half of that is latent rather than live; see catch #5.)

The catch

Nine, worst first. Numbers 1 and 2 are the ones that would actually cost money if a bot were switched on, and they compound: a stale row about the wrong team is two errors in the same lookup.

1. 🔴 The table is a graveyard, and the gate has no clock

ESPN's endpoint is a snapshot of who is hurt now. Our poller treats it as an append-only log: INSERT OR REPLACE on (league, team_id, athlete_name), and no DELETE anywhere in the file. When a player is activated, ESPN simply stops returning him — and our row for him stays, with his last-known status, permanently.

637 of the 993 rows (64.2%) were not refreshed by the most recent poll. 363 of them have not been touched for 31 days or more. The oldest was last written on 29 May — the poller's very first run — and the oldest injury date in the table is 9 February.

The gate does nothing about this. injury_anchor_satisfied() issues one query, filtering on league, team and status. There is no timestamp condition of any kind. The fetched_at column is written faithfully by the poller four times a day and never read by anything that decides a trade. Every other anchor in this library has a freshness check, some of them broken ones; this one does not have the concept.

The measured consequence, at the builder's own default settings for baseball: 10 of the 30 teams satisfy the gate, and all 10 qualify solely on ghost rows. Not one qualifies on a player ESPN currently lists. Of the 160 open baseball games, 68 pass as the table stands and 0 pass if the query is restricted to rows collected in the last six hours. I checked all twelve of the surviving Out rows against ESPN's live feed: none of the twelve appears anywhere on ESPN's injury report today.

Note the direction of the failure. Most gates in this dictionary fail closed — when the data rots, they stop trading. This one fails open: a rotting table accumulates more qualifying rows over time, so the longer it runs the more permissive it gets. That is the worse of the two directions.

2. 🔴 A third of the NBA resolves to the wrong franchise

The module carries a hand-written dictionary mapping ESPN's numeric team ids to the words that identify that team in a market title. Ten of the thirty NBA entries and four of the thirty baseball entries name the wrong club. Checked against the team ids in ESPN's own payload — the very same response the poller parses.

It is not random. The ten wrong basketball ids are 14, 15, 16, 18, 19, 20, 21, 22, 26 and 29, and across that run the module's list is rotated by exactly one position. Read the module's cities down in id order and they are in perfect alphabetical sequence — Memphis, Miami, Milwaukee, Minnesota, New York, Orlando, Philadelphia, Phoenix, Portland, Utah. Read ESPN's real teams for those same ids and you get the same ten clubs shifted one slot: Miami, Milwaukee, Minnesota, New York, Orlando, Philadelphia, Phoenix, Portland, Utah — and then Memphis, wrapping round to id 29.

The cause is legible from the data. ESPN's ids were assigned by franchise history, not by alphabet — Memphis holds 29 because the Grizzlies entered the league as Vancouver in 1995 and took the next number going; Arizona holds 29 in baseball as a 1998 expansion club. Whoever built our table sorted the modern names alphabetically, which slid Memphis into the middle of the M-block and pushed every team below it down one. Baseball's four errors are the same disease in a smaller, closed cycle: 14 → 15 → 29 → 20 → 14, i.e. Toronto, Atlanta, Arizona and Washington passing each other's ids around a ring.

What that does to a lookup: a market naming the Heat resolves to id 15, and id 15 is Milwaukee. So the gate decides a Miami question on the Bucks' injury list. Measured against today's open markets at the default basketball setting: the gate as shipped admits 22 markets; with corrected ids it would admit 48; and the two sets overlap on only 12. Ten of the twenty-two markets it currently lets through are admitted on another team's injuries, and thirty-six it should have caught are missed.

The bitter detail: the poller stores team_name in the very same row, straight from ESPN. The answer key has been sitting in the table the whole time, in a column the gate never reads.

3. 🔴 The only status the builder can ask for does not exist in baseball

include_statuses defaults to ["Out"], and the builder does not expose the field — its two controls are league and player count. So every injury bot built by clicking asks for exactly one status: Out.

ESPN does not use that word for baseball. Live from production today, its 279 MLB entries break down as 60-Day-IL 166, 15-Day-IL 50, 10-Day-IL 48, Day-To-Day 14 and one suspension. Out: zero. Baseball speaks in injured-list designations; the gate is asking a basketball question. (In basketball, where 7 of 77 players are Out, the default is at least meaningful.)

This is why catch #1 hides catch #3. A correctly-pruned table would leave the default baseball gate permanently shut, which is loud and obvious. Instead the ghosts supply twelve fossil Out rows and the gate opens on those, so the failure presents as a working filter rather than a dead one.

Two smaller vocabulary faults sit alongside it. Both docstrings list statuses that have never appearedQuestionable, Doubtful, Out For Season, Probable, Game Time Decision: zero rows each, across 993. And the comparison is case-sensitive, while ESPN is not internally consistent: the table holds both Paternity (5 rows) and paternity (6), so a config naming either spelling silently drops about half the matches. Same for suspension and bereavement, and 7-Day IL is spelled with a space where the other three use hyphens.

4. 🔴 The builder defaults to a league that has no markets

The builder's league dropdown defaults to nba, and the shipped "Injury contrarian" recipe is basketball too. Kalshi currently lists no NBA markets at all. All 160 open sports markets are KXMLBGAME — baseball. ESPN's own basketball feed reports season 2027, type 1: preseason.

So what do the 114 markets that the basketball aliases match turn out to be? Baseball games. Kalshi titles them by city — "Cincinnati vs Washington Winner?", "Atlanta vs New York Y Winner?" — and those cities are also basketball cities. The default recipe therefore fades a baseball team on the strength of a basketball injury list, and per catch #2 roughly half of those basketball lookups go to the wrong basketball team.

5. Title matching collides with city names — but this one is latent, not live

Team detection is a word-boundary substring search over abbreviation, nickname and city. City names are not sports words. Of the 720 open weather markets, 36 match a baseball team and 72 match a basketball one — Miami, Philadelphia, Chicago and Denver are the four that collide with Kalshi's weather roster. Nicknames are exposed too: "Heat" matches the word heat wherever it appears.

I want to be fair about how reachable this is. The engine only applies the gate to markets that already survived the bot's own category filter, and 73 of our 76 strategies declare one — the three that don't are all archived, and no strategy in the system combines sports with weather. So no bot has ever actually routed a weather market into this gate, and the 18 Miami markets in the example above are a property of the gate examined in isolation, not of anything that traded.

What remains true is narrower and still worth knowing: the gate provides no protection of its own. The engine's comment beside the call reads "sports markets only", and that is a guarantee the surrounding query makes, not one this function makes. Pick sports and weather in the builder, or leave the category empty, and the safety is gone with no warning.

6. It counts bodies, not importance — and the copy says otherwise

The gate counts rows. It has no concept of who the player is. A franchise starter and a fringe reliever on a 60-day list are one apiece, and ESPN's list is not purely injuries: our table holds 11 paternity rows, 8 bereavement, 6 suspensions and 6 on a "developmental list".

That would be a defensible simplification if we described it accurately. We don't. The plain-English renderer prints "only when a NBA team mentioned in the market has at least 1 key player sidelined by injury" — the gate cannot identify a key player and some of what it counts is not an injury. The builder's own description says "live ESPN report", and the sports signals page says the check happens "live, per game, at the moment a bot decides". The check does happen at that moment; what it reads is up to 75 days old.

Credit where it is due on that same card: it goes on to say "there's no standing board to print, so we don't fabricate one." That is the right instinct, stated plainly, and it is the instinct this whole dictionary is built on. The word doing the damage is "live".

7. The backtester cannot replay it — and the missing piece is the same one

injury_anchor is listed in the backtester's unsupported set, so a backtest containing this rule runs without the gate and still returns an equity curve. It does flag unsupported_rules and the UI prints "advanced rule(s) not yet replayed", so it is an omission rather than a lie — but if you don't read that line you will think you tested something you did not.

The comment explaining why is unusually honest, and it names its own fix: replay "needs historical injury snapshots (Phase 5 — write to injury_history table on each poll cycle so we can replay against past state)". Dated 28 May 2026. It was never built.

That is the joke at the centre of this page. The missing injury_history table and the ghost rows are the same problem seen from two ends. We keep a permanent, ever-growing record of every injury we have ever seen — which is exactly the archive the backtester wants — and because it is stored as if it were current state rather than history, it is simultaneously useless for replay and actively harmful to live decisions. One table would have fixed both.

8. The health monitor says this feed is fine

Our automatic feed check does watch this table. Its verdict this morning: Injuries · injury_reports · age=5.3 · max=24 · ok. Green, while 64.2% of the rows are stale.

The monitor is not being stupid, and I want to state its reasoning fairly, because it is better than mine was. The completeness check — the one that catches a feed losing half its samples — is deliberately disabled for this table, and the code says why: it counts distinct timestamps as a proxy for "polls that landed", and on an upsert table that proxy measures surviving keys instead of cycles. The comment even records that injury_reports scored 50% on the first dry run and that this was judged a false alarm. As a statement about that metric, entirely correct.

But the 50% was pointing at something real, and nothing replaced it. What is missing is not the cycle metric; it is a staleness metric — what share of the rows in this table were refreshed by the most recent poll. That number is trivial to compute, it is the headline of this page, and no monitor anywhere computes it. Recency cannot see it by construction: one refreshed row makes MAX(fetched_at) current no matter how many fossils sit behind it.

Separately, 20.3% of this poller's runs have died on database is locked — 61 of 301 — and the monitor doesn't read logs, so that is invisible too.

9. Even fixed, "at least one player out" is not information in baseball

Suppose all of the above were repaired tomorrow: rows expire, ids corrected, real IL statuses selectable. The default would still not filter anything, and this is a property of the sport rather than of our code.

Every one of the 30 baseball teams has players on the injured list at all times — between 2 and 14 of them right now, median 9. So with the correct statuses selected, min_out_count: 1 admits 152 of 160 open games, 5 admits 146, and you have to reach 10 before the gate excludes about half the board (82 of 160). The builder's default is 1. A gate open on 95% of the board is not a gate.

There is also a timing argument, which I'll state structurally rather than assert numbers for. ESPN's public injury page is an aggregator — it sits downstream of team and league reporting, and we poll it every six hours. That is a fine cadence for a coarse "who is depleted" filter and a poor one for reacting to news, and this signal is sold closer to the second than the first. How much of an injury is already in a Kalshi price by the time ESPN publishes is a real and interesting question that I have not measured and am not going to guess at; see the last section for why our data can't answer it.

The case for the defence

Five things that are genuinely right

  • The premise is sound and the ingredient is excellent. Availability really does move sports prices; this is not a spurious factor dressed up. And the feed is the best-behaved vendor in the library: free, keyless, no registration, ~0.6 s, complete for all 30 baseball teams — and zero failures in 301 attempts. Every single failure this pipeline has suffered was ours. Whatever is wrong here, it is not the data source.
  • It is not uniformly broken. Twenty of the thirty basketball aliases and twenty-six of the thirty baseball ones are correct. The status problem is a configuration fix, not evidence the signal lacks edge. Nothing on this page shows injury data can't work — it shows this wiring doesn't.
  • The ghosts are, accidentally, half of an archive. This is the inversion worth sitting with. Because the row survives, its final fetched_at records roughly when a player left the injury list, to within six hours — which is real, and is more than a correctly-pruned table would have kept. It is only half, because INSERT OR REPLACE overwrites status in place, so transitions and first-seen dates are destroyed. The fix and the feature point the same way: keep history deliberately, read current state deliberately, and stop conflating them.
  • Zero adoption is a state, not a verdict. No bot has used this, so none of the above has cost a cent — and the reasons for that non-adoption are plausibly the broken default and the out-of-season league rather than any judgement about predictive power. Nobody tried it and found it worthless; nobody tried it.
  • The parts that were designed rather than inherited are sensible. A title naming no recognised team fails closed. An unknown league fails closed. Six-hourly polling is proportionate for a roster-state feed. The alias approach — abbreviation, nickname and city — is the right shape for matching prediction-market titles; it is the contents of the table that are wrong, not the idea.

Has it ever made any money?

No, and it has never had the chance. Zero bots use injury_anchor and zero ever have: 0 of 76 strategy configurations (21 live, 15 out-of-sample, 36 archived, 4 draft) and 0 of 88 stored strategy versions mention it. No trades, no fills, no evaluations, no P&L. There is nothing to report and nothing has been invented to fill the gap.

For context, the signals that are in use cluster hard: weather accounts for 10 of the anchor slots across our strategies, our own settle-rate history 6, and everything else is in ones and twos. The sports shelf — this signal, starter quality, bullpen and park conditions — is almost entirely unused despite three pollers running for it every six hours.

How to actually use it

Straight answer: as it stands, don't — not because injury data is a bad idea but because this particular wiring will hand you a stale answer about the wrong team with no error. If you want to build on it anyway, or fix it, here is what the measurements above imply.

And the larger point, which is really about the design rather than the bugs. A gate that asks "does this team have N bodies on a list" is missing the four things that make injury information worth money: which player (a first-choice starter is not a fringe reliever), which role (losing a closer changes a total differently from losing a leadoff hitter), how long (a day-to-day knock is not a 60-day list), and above all whether it is already in the price. Injuries move markets when they are news. A count of everyone currently unavailable is not news; it is the standing state of the roster, and the market has had it as long as we have.

Which points at what a rebuilt version should key on: changes, not levels — a player appearing on the list, or moving between statuses, near a market whose price hasn't moved yet. That is a genuinely different signal, and it is exactly what injury_history from catch #7 would make possible. It is also why this page contains no study of whether injuries lead or lag Kalshi prices: our table has no events in it. Overwriting in place destroyed every transition, so what we hold is a current-state snapshot with a long stale tail, and no amount of care extracts a lead/lag measurement from that. Building the history table is the first move, and everything interesting comes after it.

Notes on the registry, and on this backlog

Two things the signal registry says about this signal are wrong, and both are worth naming because they are different failure modes.

It reports the source as "MLB feeds". The registry infers sources by scanning the module docstring against a hand-written list of vendor hints — and that list contains "MLB" but not "ESPN". The docstring names ESPN three times; what fires instead is the literal string "MLB" in the sentence "covers all 30 NBA and 30 MLB teams". So the registry credits a vendor we do not use, and silently drops the fact that this signal is basketball-capable and basketball-defaulted. That is worse than the "undocumented" false negative that hit onchain_fees_anchor: a blank tells you to go and look, a confident wrong answer does not.

It reports three tables and only one exists. injury_anchor and min_out_count are listed alongside injury_reports — the registry harvests backticked names from docstrings, and those two are a module name and a config field. Same artefact as the phantom tables on the eco_anchor and gdp_surprise_anchor pages. Everything else it resolves correctly — poller, cron, replayable, and live_bots: 0, where for once 0 really does mean 0.

For the record, our own backlog described this signal as "MLB injury reports". It is ESPN's, it covers two sports, and the builder defaults to the one it doesn't mention.

Every figure on this page was measured on the production box on 12 August 2026 and traces to one of: ESPN's live injuries API, the injury_reports table in predictive.db (993 rows), the kalshi_markets table (7,414 markets open at the time of writing), the poller log (301 runs), the strategies and strategy_versions tables in quants.db, or the source of quants/injury_anchor.py, quants/engine.py and predictive/injury_poller.py. Where a claim could not be checked against a primary source, this page says so rather than estimating. No performance figures appear here because none exist. All TinyCorp bots trade simulated money.