The Doom Index is a weighted average of six components, each normalized to a 0–100 score. Every component measures deviation from the team's own baseline, not absolute quality: a 95-win team playing .400 ball for two weeks should read FEAR. This is the core insight borrowed from CNN's Fear & Greed Index: sentiment is the derivative, not the level.
| # | Component | What it measures | Source | Weight |
|---|---|---|---|---|
| 1 | Momentum | L10 record vs. season win% | MLB Stats API | 0.25 |
| 2 | Offense Trend | 15-game rolling runs scored vs. season avg | MLB Stats API | 0.10 |
| 3 | Run Prevention | 15-game rolling runs allowed (INVERTED) | MLB Stats API | 0.10 |
| 4 | Playoff Pulse | 7-day change in playoff odds | FanGraphs | 0.25 |
| 5 | Regression Watch | Pythagorean gap (INVERTED, contrarian) | MLB Stats API | 0.10 |
| 6 | Hope Premium | WS odds: market price vs model (log-odds) | Polymarket + FanGraphs | 0.20 |
index = Σ (component_score × weight)
Normalization: the one function that matters. Each component produces a z-score, mapped linearly to 0–100 and clamped:
def z_to_score(z: float, z_cap: float = 2.0) -> float:
"""Map z ∈ [-z_cap, +z_cap] linearly onto [0, 100], clamped."""
z = max(-z_cap, min(z_cap, z))
return 50 + (z / z_cap) * 50
All examples below use plausible numbers for the 2026 team (45–44, .506 as of July 4).
p_L10 = wins in last 10 games / 10
p_season = season win pct
z = (p_L10 - p_season) / SE, where SE = sqrt(p_season × (1 - p_season) / 10)
score = z_to_score(z)
SE is the binomial standard error of a 10-game sample, i.e., "how much should a 10-game stretch naturally wobble for a team of this quality?"
rs_15 = mean runs scored per game, last 15 games
rs_season = mean runs scored per game, full season
sigma = std dev of the 15-game rolling RS series this season (floor 0.40)
z = (rs_15 - rs_season) / sigma
score = z_to_score(z)
ra_15 = mean runs ALLOWED per game, last 15 games
ra_season = mean runs allowed per game, full season
sigma = std dev of the 15-game rolling RA series (floor 0.40)
z = -(ra_15 - ra_season) / sigma # INVERTED: fewer runs allowed = greed
score = z_to_score(z)
Sigma floor rationale for both: per-game run totals have sd ≈ 3, so a 15-game rolling mean wobbles ≈ 3/√15 ≈ 0.77 under independence; 0.40 is a conservative floor for early-season stability. (The old combined run-diff component used 0.50 because the difference of two noisy series has higher variance than either side alone.)
delta = playoff_odds_today - playoff_odds_7_days_ago # percentage points
z = delta / 5.0 # 5 pts/week ≈ 1 sigma
score = z_to_score(z)
FanGraphs playoff odds already integrate schedule, roster, and opponents: the level is the best single summary of the season. The 7-day delta is the fear/greed.
pythag_wpct = RS^1.83 / (RS^1.83 + RA^1.83)
gap = actual_wpct - pythag_wpct
z = -(gap / 0.030) # NOTE THE MINUS SIGN
score = z_to_score(z)
Overperforming your Pythagorean expectation means you're winning more than your runs justify: luck, sequencing, one-run records. That's regression risk, i.e. latent fear, so the sign is inverted. 0.030 (~5 wins over a season) ≈ 1 sigma of typical Pythag gaps.
This is the "put/call ratio" of the index: it disagrees with the mood on purpose. Keep its weight low (0.10): it's a seasoning, not a base.
market_p = Polymarket World Series yes-price (fraction)
model_p = FanGraphs wsWin (fraction)
both clamped to [0.001, 0.99] # FG rounds dead teams to 0.0000; market tick-floors
premium = ln( odds(market_p) / odds(model_p) ) # odds(p) = p/(1-p)
if market_p <= 0.005 and model_p <= 0.005: premium = 0 # both at the floor: tick noise
z = premium / 0.7 # ln 2: the crowd paying 2× the model is a strong day
score = z_to_score(z) # +-2 cap, so a 4x ratio saturates the dial
The premium is already a spread (market vs model), so the deviation principle is
built in and the level is scored. The record stores ws_market and
ws_model (percents) plus a deterministic summary string:
| Condition | Summary |
|---|---|
| |premium| < ln 1.15 | market and model agree |
| premium >= ln 3 | pure hope money |
| premium <= -ln 3 | left for dead by the crowd |
| else, positive | crowd pays {ratio}× the model |
| else, negative | trading {discount}% under the model |
Fallback: if either source fails, reuse yesterday's value and mark
"hope_premium" stale. Replaced Fan Sentiment (Reddit + LLM) 2026-07:
Reddit now requires commercial licensing, and the market spread is the more honest
instrument anyway: real money wagered on feelings, net of fundamentals.
An example team: the 2026 Mariners, 45–44 on July 4, hold WC3, just beat the Angels 1–0 behind Bryce Miller.
| Component | Raw signal | z | Score | × Weight |
|---|---|---|---|---|
| Momentum | 6–4 L10 vs .506 season | +0.59 | 65 | 16.3 |
| Offense Trend | 4.10 R/G L15 vs 4.40 season | −0.67 | 33 | 3.3 |
| Run Prevention | 3.90 RA/G L15 vs 4.30 (inverted) | +0.80 | 70 | 7.0 |
| Playoff Pulse | Odds 44% → 47% (7d) | +0.60 | 65 | 16.3 |
| Regression Watch | Pythag gap −0.006 (inverted) | +0.20 | 55 | 5.5 |
| Hope Premium | WS mkt 5.45% vs model 5.0% | +0.13 | 53 | 10.6 |
| INDEX | 59 · WATCHFUL | |||
Notice what the split buys: the headline 59 hides that offense reads 33 while prevention reads 70: "pitching is carrying cold bats," legible at a glance.
{ "date": "2026-07-04", "index": 59, "zone": "WATCHFUL", "delta_1d": +3, components: {...} }
CNN's Fear & Greed index is famously mediocre as a predictive signal: it's a quotable summary of the present, not an oracle. Same here: this index will not tell you if your team makes the playoffs. It tells you, with numbers, exactly how worried everyone is. That is the product. One honesty note on Hope Premium: it prices the betting crowd's hope, not the fanbase's voice: sharps arbitrage away some hometown bias, and a structurally-overbet team reads as persistent lean rather than daily mood. History note: pulse and premium series were backfilled from FanGraphs and Polymarket in July 2026, regenerating historical index values that previously averaged in neutral stubs.