Corporate KPI Prediciton Market Correlation with Share Price
Do prediction markets for corporate KPIs, in this case Boeing's
commercial deliveries, correlate with the underlying stock price?
Uses the prediction market dataset available @
Snowflake.
import numpy as np
import pandas as pd
import plotly.graph_objects as go
from scipy.optimize import curve_fit
from scipy.special import erf
from prediction_markets.database.snowflake_writer import connector
TICKER = "BA"
ENTITY = "Boeing"
PROPERTY = "commercial deliveries"
MARKET_TITLE = "%commercial airplane deliveries in 2026?%"
PRICE_START_DATE = "2026-07-11"
FIT_SCALE_GUESS = 670 # initial guess for the log-normal median
MAX_GAP_DAYS = 5 # how long a stale market quote is carried onto a trading day
SMOOTH_WINDOW = 7 # median filter width, in trading days
CORR_WINDOW = 30 # rolling-correlation width, in trading days
QUOTES_STMT = """
SELECT
price_time::DATE AS date,
price AS p,
std_property_value AS v
FROM PREDICTION_MARKET_FACTORS.PUBLIC.STANDARDIZED_MARKETS
WHERE price IS NOT NULL
AND std_resolved_entity ILIKE %(entity)s
AND std_property = %(property)s
AND market_title ILIKE %(market_title)s
ORDER BY date, price_time -- so "last" per (date, threshold) is that day's final quote
"""
quotes = query(QUOTES_STMT, entity=ENTITY, property=PROPERTY, market_title=MARKET_TITLE)
print(f"{len(quotes)} quotes across {quotes['V'].nunique()} thresholds")
quotes.head()
51 quotes across 7 thresholds
DATE
P
V
0
2026-07-11
0.91
580
1
2026-07-11
0.91
600
2
2026-07-11
0.18
680
3
2026-07-12
0.44
660
4
2026-07-12
0.70
640
Log-normal fit
def lognormal_cdf(x: np.ndarray, mu: float, sigma: float) -> np.ndarray:
return 0.5 * (1 + erf((np.log(x) - mu) / (sigma * np.sqrt(2))))
def fit_lognormal_mean(thresholds: np.ndarray, survival: np.ndarray) -> float:
"""Mean of the log-normal whose CDF best fits a quoted survival curve.
`survival[i]` is the market price for "value exceeds thresholds[i]" — i.e. P(X > x),
so the CDF is fitted against 1 - survival.
"""
(mu, sigma), _ = curve_fit(
lognormal_cdf,
thresholds,
1.0 - survival,
p0=[np.log(FIT_SCALE_GUESS), 1.0],
)
return float(np.exp(mu + sigma**2 / 2))
def daily_forecast(quotes: pd.DataFrame) -> pd.Series:
"""One market-implied point forecast per date, for the dates whose fit converges."""
# One column per threshold, one row per date. Carry each threshold's last quote
# forward so a date only needs *some* market activity to be fitted.
grid = (
quotes.astype({"V": float, "P": float})
.assign(DATE=lambda d: pd.to_datetime(d["DATE"]))
.pivot_table(index="DATE", columns="V", values="P", aggfunc="last")
.sort_index()
.ffill()
)
fits = {}
for date, row in grid.iterrows():
row = row.dropna()
if len(row) < 2: # curve_fit needs at least one point per parameter
continue
try:
fits[date] = fit_lognormal_mean(row.index.values, row.values)
except (RuntimeError, ValueError): # no convergence on this day's curve
continue
# A near-zero sigma sends the mean to +inf, which would flatten every plot below.
fitted = pd.Series(fits, dtype=float, name="FORECAST").rename_axis("DATE")
return fitted[np.isfinite(fitted)]
forecast = daily_forecast(quotes)
print(
f"{len(forecast)} fitted dates "
f"({forecast.index.min().date()} -> {forecast.index.max().date()})"
)
forecast.describe()
25 fitted dates (2026-07-11 -> 2026-08-25)
count 25.000000
mean 653.088986
std 7.994472
min 645.447043
25% 650.426805
50% 651.633211
75% 652.488092
max 690.215739
Name: FORECAST, dtype: float64
PRICES_STMT = """
SELECT
date AS price_date,
value AS closing_price
FROM SNOWFLAKE_PUBLIC_DATA_PAID.PUBLIC_DATA.STOCK_PRICE_TIMESERIES
WHERE ticker = %(ticker)s
AND variable_name = 'Post-Market Close'
AND date > %(start_date)s
ORDER BY date
"""
prices = (
query(PRICES_STMT, ticker=TICKER, start_date=PRICE_START_DATE)
.assign(PRICE_DATE=lambda d: pd.to_datetime(d["PRICE_DATE"]))
.set_index("PRICE_DATE")["CLOSING_PRICE"]
.astype(float)
.sort_index()
)
prices.tail()
# The forecast is on calendar dates, the stock on trading days. Carry the forecast onto
# the union of the two calendars first, so a Saturday quote lands on the following Monday
# instead of being dropped by the join.
forecast_on_trading_days = (
forecast.reindex(forecast.index.union(prices.index))
.ffill(limit=MAX_GAP_DAYS)
.reindex(prices.index)
)
combined = pd.concat([prices, forecast_on_trading_days], axis=1).dropna()
# Carried quotes leave flat runs, and occasionally a stale price inverts the survival
# curve. Keep a median-filtered copy to see how much of the result rides on those days.
combined["FORECAST_SMOOTH"] = (
combined["FORECAST"].rolling(SMOOTH_WINDOW, center=True, min_periods=3).median()
)
print(
f"overlap with {TICKER}: {len(combined)} trading days "
f"({combined.index.min().date()} -> {combined.index.max().date()})"
)
combined.describe()
overlap with BA: 32 trading days (2026-07-13 -> 2026-08-25)
CLOSING_PRICE
FORECAST
FORECAST_SMOOTH
count
32.000000
32.000000
32.000000
mean
220.765000
651.335372
651.101519
std
9.976393
1.587714
1.094425
min
205.480000
648.965059
648.965059
25%
213.585000
650.149852
650.676755
50%
217.930000
651.286654
651.159169
75%
231.437500
652.120710
652.107567
max
239.910000
654.989962
652.210310
combined.plot()
Correlate closing prices with the KPI forecast
# Correlate CHANGES, not levels. The stock and the fitted forecast both trend, so a
# rolling correlation of their levels largely reports whether two trends happen to point
# the same way inside each window -- it swings between +1 and -1 on almost no information.
chg = pd.DataFrame(
{
f"{TICKER}_RET": np.log(combined["CLOSING_PRICE"]).diff(),
"D_FORECAST": np.log(combined["FORECAST"]).diff(),
"D_FORECAST_SMOOTH": np.log(combined["FORECAST_SMOOTH"]).diff(),
}
).dropna()
pairs = {
"changes (raw forecast)": (f"{TICKER}_RET", "D_FORECAST"),
f"changes ({SMOOTH_WINDOW}d median filtered)": (
f"{TICKER}_RET",
"D_FORECAST_SMOOTH",
),
}
full = pd.DataFrame(
[
{
"basis": label,
"pearson": chg[a].corr(chg[b]),
"spearman": chg[a].corr(chg[b], method="spearman"),
"n": len(chg),
}
for label, (a, b) in pairs.items()
]
+ [
{
"basis": "levels (spurious, for reference)",
"pearson": combined["CLOSING_PRICE"].corr(combined["FORECAST"]),
"spearman": combined["CLOSING_PRICE"].corr(
combined["FORECAST"], method="spearman"
),
"n": len(combined),
}
]
).set_index("basis")
print(f"=== Full-sample correlation ({TICKER} close vs. fitted delivery forecast) ===")
print(full.to_string(float_format=fmt4))
# ~2 standard errors under the null, as a yardstick for the numbers above
print(f"\n+/-2 SE at n={len(chg)}: {2 / np.sqrt(len(chg)):.3f}")
=== Full-sample correlation (BA close vs. fitted delivery forecast) ===
pearson spearman n
basis
changes (raw forecast) -0.1566 -0.2866 31
changes (7d median filtered) 0.1463 0.1094 31
levels (spurious, for reference) 0.1924 0.2544 32
+/-2 SE at n=31: 0.359
rolling_corrs = pd.DataFrame(
{label: chg[a].rolling(CORR_WINDOW).corr(chg[b]) for label, (a, b) in pairs.items()}
).dropna(how="all")
summary = pd.DataFrame(
{
"Mean Correlation": rolling_corrs.mean(),
"Median Correlation": rolling_corrs.median(),
"Max Abs Correlation": rolling_corrs.abs().max(),
"Std Dev": rolling_corrs.std(),
}
).dropna()
print(
f"=== Rolling {CORR_WINDOW}-day correlation of changes "
f"({len(rolling_corrs)} windows) ==="
)
print(summary.to_string(float_format=fmt4))
rolling_corrs.plot(
figsize=(11, 4),
title=f"Rolling {CORR_WINDOW}d correlation: {TICKER} return vs. change in fitted forecast",
ylim=(-1, 1),
)
=== Rolling 30-day correlation of changes (2 windows) ===
Mean Correlation Median Correlation Max Abs Correlation Std Dev
changes (raw forecast) -0.1481 -0.1481 0.1572 0.0129
changes (7d median filtered) 0.1445 0.1445 0.1464 0.0027