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.
estimates_query ="""select price_time::DATE as date, price AS p, std_resolved_ticker as ticker, DATE(market_close)::VARCHAR as MARKET_CLOSE, resolutionfrom PREDICTION_MARKET_FACTORS.PUBLIC.STANDARDIZED_MARKETSwhere 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
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 capprices_stmt =f"""SELECT date AS price_date, ticker, value AS closing_priceFROM SNOWFLAKE_PUBLIC_DATA_PAID.PUBLIC_DATA.STOCK_PRICE_TIMESERIESWHERE ticker IN ({placeholders}) AND variable_name = 'Post-Market Close' AND date >= %sORDER 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 filingsshares_stmt =f"""SELECT ci.primary_ticker AS ticker, ra.period_end_date, ra.value AS shares_outstandingFROM SNOWFLAKE_PUBLIC_DATA_PAID.PUBLIC_DATA.COMPANY_INDEX ciJOIN SNOWFLAKE_PUBLIC_DATA_PAID.PUBLIC_DATA.SEC_CORPORATE_REPORT_ATTRIBUTES ra ON ra.cik = ci.cikWHERE 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)
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.