THE DOOM INDEX
← back to the index

Methodology

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.

1 · The model

#ComponentWhat it measuresSourceWeight
1MomentumL10 record vs. season win%MLB Stats API0.25
2Offense Trend15-game rolling runs scored vs. season avgMLB Stats API0.10
3Run Prevention15-game rolling runs allowed (INVERTED)MLB Stats API0.10
4Playoff Pulse7-day change in playoff oddsFanGraphs0.25
5Regression WatchPythagorean gap (INVERTED, contrarian)MLB Stats API0.10
6Hope PremiumWS odds: market price vs model (log-odds)Polymarket + FanGraphs0.20
index = Σ (component_score × weight)
Zones: 0–20 Extreme Fear · 20–40 Fear · 40–60 Neutral · 60–80 Greed · 80–100 Extreme Greed
Display labels: DOOMED · BRACING · WATCHFUL · BELIEVING · REFUSE TO LOSE

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
Worked example: z = −1.08 → 50 + (−1.08 / 2.0) × 50 = 50 − 27 = 23 (Fear). z = 0 always means "performing exactly at baseline" → 50 (Neutral).

2 · Component formulas

All examples below use plausible numbers for the 2026 team (45–44, .506 as of July 4).

2.1 Momentum weight 0.25

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?"

For a .506 team, SE = sqrt(.506 × .494 / 10) ≈ 0.158. So even 7-3 is only z ≈ +1.23 → score ≈ 81. This is correct behavior: 10 games is noisy, and the SE denominator encodes that honestly. 5-5 for a .506 team → z ≈ −0.04 → score ≈ 49. Dead neutral, as it should be.

2.2 Offense Trend weight 0.10

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)
Worked example: season 4.40 R/G, last 15 = 4.10, sigma = 0.45. z = (4.10 − 4.40)/0.45 = −0.67 → score ≈ 33 (Bracing). Note this measures deviation from the team's OWN baseline, not league quality: "unusually cold," not "bad offense."

2.3 Run Prevention Trend weight 0.10

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)
Worked example: season 4.30 RA/G, last 15 = 3.90, sigma = 0.50. z = −(3.90 − 4.30)/0.50 = +0.80 → score ≈ 70 (Believing).

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.)

2.4 Playoff Pulse weight 0.25

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.

Worked example: odds went 41% → 48% this week. delta = +7, z = +1.4 → score = 85. The 5.0 scaler is an empirical prior (typical weekly odds moves for a bubble team are ±3–6 pts); tune it after a month of live data by measuring the actual std dev.

2.5 Regression Watch weight 0.10 · the contrarian component

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.

Worked example: 45–44 (.506) with RS=390, RA=380 → pythag ≈ .512. gap = −.006 → z = +0.2 → score ≈ 55. Slightly underperforming run quality = slight hidden upside.

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.

2.6 Hope Premium weight 0.20

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:

ConditionSummary
|premium| < ln 1.15market and model agree
premium >= ln 3pure hope money
premium <= -ln 3left for dead by the crowd
else, positivecrowd pays {ratio}× the model
else, negativetrading {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.

3 · Full worked example: one team, one day, end to end

An example team: the 2026 Mariners, 45–44 on July 4, hold WC3, just beat the Angels 1–0 behind Bryce Miller.

ComponentRaw signalzScore× Weight
Momentum6–4 L10 vs .506 season+0.596516.3
Offense Trend4.10 R/G L15 vs 4.40 season−0.67333.3
Run Prevention3.90 RA/G L15 vs 4.30 (inverted)+0.80707.0
Playoff PulseOdds 44% → 47% (7d)+0.606516.3
Regression WatchPythag gap −0.006 (inverted)+0.20555.5
Hope PremiumWS mkt 5.45% vs model 5.0%+0.135310.6
INDEX59 · 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: {...} }

4 · Honest methodology

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.

The Doom Index is rebuilt daily from public data. See the live index for today's read.
Built by Isochronous Labs
Not affiliated with, endorsed by, or sponsored by Major League Baseball or any of its clubs. Team names used descriptively. Data: MLB Stats API, FanGraphs, Polymarket. Anonymous usage analytics.