Accuracy of Prediction Markets for Corporate Earnings

2026-09-15

Are Polymarket markets for public company earning beats/misses accurate?

Using the Brier score to guage how well the last-traded price on prediction markets predicts whether a company will beat the consensus earnings forcast.

Visualize the data @ https://prediction-market-data.com/standardized/?event_type=Corporate+Kpi&property=Earnings

Code
import os

import numpy as np
import pandas as pd
import plotly.graph_objects as go
import plotly.io as pio
import snowflake.connector
from scipy import stats
from sklearn.metrics import brier_score_loss

pio.renderers.default = "plotly_mimetype+notebook"

connector = snowflake.connector.connect(
    account=os.environ["SNOWFLAKE_ACCOUNT"],
    user=os.environ["SNOWFLAKE_USER"],
    token=os.environ["SNOWFLAKE_TOKEN"],
    authenticator="programmatic_access_token",
    warehouse=os.environ["SNOWFLAKE_WAREHOUSE"],
    database=os.environ["SNOWFLAKE_DATABASE"],
    schema=os.environ["SNOWFLAKE_SCHEMA"],
    role=os.environ["SNOWFLAKE_ROLE"],
)

Get the Data

prediction-market-data dataset on Snowflake, https://app.snowflake.com/marketplace/providers/GZT1Z3TUXVI/Prediction%20Market%20Data

Code
estimates_query = """
select
    price_time::DATE as date,
    price AS p,
    std_resolved_ticker as ticker,
    DATE(market_close)::VARCHAR as MARKET_CLOSE,
    resolution
from
    PREDICTION_MARKET_FACTORS.PUBLIC.STANDARDIZED_MARKETS
where
    std_event_type = 'corporate KPI'
    and std_property_value = 'estimate'
order by date, price_time
"""

cursor = connector.cursor()
cursor.execute(estimates_query)
estimates = cursor.fetch_pandas_all()

wide = (
    estimates.assign(DATE=pd.to_datetime(estimates["DATE"]))
    .pivot_table(
        index="DATE", columns=["TICKER", "MARKET_CLOSE"], values="P", aggfunc="last"
    )
    .sort_index()
)

resolution = (
    estimates.drop_duplicates(["TICKER", "MARKET_CLOSE"])
    .set_index(["TICKER", "MARKET_CLOSE"])["RESOLUTION"]
    .eq("Yes")  # 1 = beat the estimate, 0 = missed
    .astype(int)
    .rename("resolution")
)
resolution.index.names = ["TICKER", "MARKET_CLOSE"]

print(
    f"{len(resolution)} resolved markets, {resolution.mean():.1%} beat their estimate"
)
1234 resolved markets, 74.9% beat their estimate

Last price before the market closed for each resolved market

Code
last_probability = wide.ffill().iloc[-1].rename("last_probability")
last_probability.index.names = ["TICKER", "MARKET_CLOSE"]

predictors = pd.concat([resolution, last_probability], axis=1).dropna()
predictors.columns = ["resolution", "market_prob"]
print(f"{len(predictors)} markets with a market price and a known resolution")
1234 markets with a market price and a known resolution

Compute the Brier score and p-value

Code
brier = brier_score_loss(predictors["resolution"], predictors["market_prob"])
print(f"prediction market Brier score: {brier:.4f}  (n={len(predictors)})")
prediction market Brier score: 0.1154  (n=1234)

The raw sample skews heavily toward “beat” (74.8%)

Resample to a balanced 50/50 mix of beat/missed markets, undersample the majority class down to the minority class’s count.

Code
RANDOM_SEED = 0

minority_size = predictors["resolution"].value_counts().min()
balanced = pd.concat(
    group.sample(minority_size, random_state=RANDOM_SEED)
    for _, group in predictors.groupby("resolution")
)

print(balanced["resolution"].value_counts())

brier_balanced = brier_score_loss(balanced["resolution"], balanced["market_prob"])
print(
    f"\nprediction market Brier score (balanced 50/50): {brier_balanced:.4f}  (n={len(balanced)})"
)
resolution
0    310
1    310
Name: count, dtype: int64

prediction market Brier score (balanced 50/50): 0.1718  (n=620)

Is Brier score correlated with company market cap?

Approximated as market cap as shares outstanding (SEC XBRL, as of the most recent filing before market_close) x last share price before market_close.

Getting market cap from XBRL is fraught, but probably good enough for our purposes.

Code
tickers = sorted({t for t, _ in predictors.index})
placeholders = ", ".join(["%s"] * len(tickers))
start_date = pd.to_datetime(estimates["DATE"]).min().date().isoformat()

# share prices, to price the shares outstanding into a market cap
prices_stmt = f"""
SELECT date AS price_date, ticker, value AS closing_price
FROM SNOWFLAKE_PUBLIC_DATA_PAID.PUBLIC_DATA.STOCK_PRICE_TIMESERIES
WHERE ticker IN ({placeholders}) AND variable_name = 'Post-Market Close' AND date >= %s
ORDER BY ticker, date
"""
cursor = connector.cursor()
cursor.execute(prices_stmt, (*tickers, start_date))
prices = cursor.fetch_pandas_all()
prices["PRICE_DATE"] = pd.to_datetime(prices["PRICE_DATE"])

# shares outstanding, from SEC filings
shares_stmt = f"""
SELECT ci.primary_ticker AS ticker, ra.period_end_date, ra.value AS shares_outstanding
FROM SNOWFLAKE_PUBLIC_DATA_PAID.PUBLIC_DATA.COMPANY_INDEX ci
JOIN SNOWFLAKE_PUBLIC_DATA_PAID.PUBLIC_DATA.SEC_CORPORATE_REPORT_ATTRIBUTES ra
    ON ra.cik = ci.cik
WHERE ci.primary_ticker IN ({placeholders})
  AND ra.tag = 'EntityCommonStockSharesOutstanding'
ORDER BY ticker, ra.period_end_date
"""
cursor = connector.cursor()
cursor.execute(shares_stmt, tickers)
shares = cursor.fetch_pandas_all()
shares["PERIOD_END_DATE"] = pd.to_datetime(shares["PERIOD_END_DATE"])
shares["SHARES_OUTSTANDING"] = shares["SHARES_OUTSTANDING"].astype(float)

print(
    f"shares-outstanding coverage: {shares['TICKER'].nunique()} / {len(tickers)} tickers"
)
shares-outstanding coverage: 402 / 411 tickers
Code
predictors_flat = predictors.reset_index()
predictors_flat["close_date"] = pd.to_datetime(predictors_flat["MARKET_CLOSE"])
predictors_flat = predictors_flat.sort_values("close_date", kind="stable")

with_cap = pd.merge_asof(
    predictors_flat,
    prices.sort_values("PRICE_DATE", kind="stable")[
        ["PRICE_DATE", "TICKER", "CLOSING_PRICE"]
    ],
    left_on="close_date",
    right_on="PRICE_DATE",
    by="TICKER",
    direction="backward",
    allow_exact_matches=False,  # price strictly before market_close
)
with_cap = pd.merge_asof(
    with_cap,
    shares.sort_values("PERIOD_END_DATE", kind="stable")[
        ["PERIOD_END_DATE", "TICKER", "SHARES_OUTSTANDING"]
    ],
    left_on="close_date",
    right_on="PERIOD_END_DATE",
    by="TICKER",
    direction="backward",  # most recent filing as of market_close
)
with_cap["market_cap"] = with_cap["CLOSING_PRICE"] * with_cap["SHARES_OUTSTANDING"]

with_cap = with_cap.set_index(["TICKER", "MARKET_CLOSE"])[
    ["resolution", "market_prob", "market_cap"]
].dropna(subset=["market_cap"])

# a handful of SEC filings have obviously bad shares-outstanding values (e.g. off
# by a unit-scaling error) that produce an implausible sub-$100M cap for a
# large-cap earnings-reporting company -- drop those rather than let a data
# glitch masquerade as a "small company".
n_before = len(with_cap)
with_cap = with_cap[with_cap["market_cap"] >= 1e8]
print(
    f"{len(with_cap)} markets with a usable market cap (dropped {n_before - len(with_cap)} implausible values)"
)
1158 markets with a usable market cap (dropped 59 implausible values)
Code
with_cap["squared_error"] = (with_cap["market_prob"] - with_cap["resolution"]) ** 2
with_cap["log_market_cap"] = np.log10(with_cap["market_cap"])

rho, p_value = stats.spearmanr(with_cap["log_market_cap"], with_cap["squared_error"])
print(
    f"spearman corr(log market cap, squared error) = {rho:+.3f}  p={p_value:.3g}  (n={len(with_cap)})"
)

with_cap["cap_quartile"] = pd.qcut(
    with_cap["market_cap"], 4, labels=["Q1 (smallest)", "Q2", "Q3", "Q4 (largest)"]
)
by_cap = with_cap.groupby("cap_quartile", observed=True)["squared_error"].agg(
    brier_score="mean", n_markets="count"
)
spearman corr(log market cap, squared error) = -0.133  p=5.24e-06  (n=1158)
Code
fig = go.Figure()
fig.add_bar(
    x=by_cap.index,
    y=by_cap["brier_score"],
    marker_color="#3B7AB8",
)
fig.update_layout(
    title="Brier score by market-cap quartile",
    template="plotly_white",
    height=420,
)
fig.update_yaxes(title="Brier score (lower = better)")
fig.update_xaxes(title="market cap quartile")
fig.show()

Result

Yes, markets for larger companies are more accurate. Spearman correlation between log market cap and per-market squared error is ~-0.135 (p < 0.001). The Brier score drops from the smallest market-cap quartile to the largest – roughly 0.16 for Q1 to ~0.10 for Q3/Q4. Consistent with larger, more heavily-traded and more heavily-covered (analyst estimates, liquidity) companies being priced more efficiently on these earnings-beat markets than smaller, thinner ones.