DEV Community

Ayrat Murtazin
Ayrat Murtazin

Posted on

Black-Scholes on Polymarket: Finding Mispriced Binary Events with Python

Polymarket is a decentralized prediction market where participants bet on binary outcomes — "Will MSFT close above $420 by Friday?" trades as a contract paying $1 if true, $0 if false. What most participants miss is that this structure is mathematically identical to a cash-or-nothing binary option — an instrument that the Black-Scholes framework has priced analytically for over 40 years. When two markets price the same event using fundamentally different methodologies, mispricings emerge.

This article walks through a complete Python implementation that pulls a live implied volatility surface from Yahoo Finance, applies the Black-Scholes N(d2) formula to derive a risk-neutral probability, and compares that probability against the corresponding Polymarket YES price. A divergence of 5% or greater is treated as a trade signal. We will cover the mathematical intuition, build the scanner from scratch using yfinance and scipy, and discuss realistic expectations around edge, liquidity, and model limitations.


Most algo trading content gives you theory.
This gives you the code.

3 Python strategies. Fully backtested. Colab notebook included.
Plus a free ebook with 5 more strategies the moment you subscribe.

5,000 quant traders already run these:

Subscribe | AlgoEdge Insights

Black-Scholes on Polymarket: Finding Mispriced Binary Events with Python

This article covers:

  • Section 1 — The Core Insight:** Why Polymarket stock markets are structurally identical to binary options and how Black-Scholes derives an objective probability
  • Section 2 — Python Implementation:** Setup and parameters (2.1), fetching implied volatility from a live options chain (2.2), computing Black-Scholes N(d2) and scanning for mispricings (2.3), and visualizing the probability gap (2.4)
  • Section 3 — Results and Strategy Performance:** What the scanner finds in practice, historical edge estimates, and realistic ROI framing
  • Section 4 — Use Cases:** Where this approach applies beyond a single ticker
  • Section 5 — Limitations and Edge Cases:** Where the model breaks down and what risks to monitor

1. The Core Insight: Two Markets, One Event, One Price

A Polymarket contract on "Will MSFT close above $420 on expiry?" pays exactly $1 if the condition is met and $0 otherwise. In traditional derivatives, this is called a cash-or-nothing binary call option. Black-Scholes prices this instrument analytically — and the pricing formula reduces to a single, interpretable quantity: N(d2), the cumulative normal distribution evaluated at d2.

N(d2) is the risk-neutral probability that the underlying asset closes above the strike at expiration, given current price, time to expiry, implied volatility, and the risk-free rate. It does not rely on subjective sentiment or gut feel. It is derived from the actual options market, where institutional participants with significant capital have already revealed their volatility expectations through traded prices.

Polymarket participants, by contrast, are predominantly crypto-native traders. They apply intuition, news sentiment, and momentum — but no systematic link exists between their prices and the tradfi options surface. There is no automated arbitrage bot continuously enforcing parity between CBOE implied volatility and Polymarket YES prices. That structural gap is the edge.

The intuition is straightforward: if the options market implies a 41% probability that MSFT closes above $420, and Polymarket is selling YES contracts at 34 cents, you are buying a bet at a 7-percentage-point discount to its mathematically derived fair value. Over many such trades, that systematic underpricing should compound into positive expected value — assuming your edge does not erode through transaction costs and liquidity constraints.

2. Python Implementation

2.1 Setup and Parameters

The scanner requires four configurable parameters per trade candidate: the ticker symbol, the target strike price, the Polymarket YES price (read manually from the market), and the risk-free rate. Time to expiry is derived from the options chain expiration date.

# --- Dependencies ---
# pip install yfinance scipy numpy pandas matplotlib

import numpy as np
import pandas as pd
import yfinance as yf
from scipy.stats import norm
from scipy.optimize import brentq
import matplotlib
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")

# --- Parameters ---
TICKER = "MSFT"           # Underlying equity ticker
TARGET_STRIKE = 420.0     # Polymarket strike price ($)
POLYMARKET_YES = 0.34     # Current Polymarket YES price (0–1 scale)
RISK_FREE_RATE = 0.053    # Annualized risk-free rate (approx 3-month T-bill)
MIN_EDGE_THRESHOLD = 0.05 # Minimum N(d2) – Polymarket gap to flag a trade
DAYS_LOOKAHEAD = 30       # Max days to expiry to consider for IV extraction
Enter fullscreen mode Exit fullscreen mode

Implementation chart

Every value here is observable in real time. POLYMARKET_YES is the only manual input — read it directly from the Polymarket contract page for the matching expiry and strike. All other parameters flow from live market data fetched in the next step.

2.2 Fetching Implied Volatility from the Options Chain

Rather than assuming a constant volatility, we extract the at-the-money implied volatility from the nearest expiry options chain. We invert Black-Scholes numerically using Brent's method to derive IV from the market price of a call option closest to our target strike.

def black_scholes_call(S, K, T, r, sigma):
    """Standard Black-Scholes call price."""
    if T <= 0 or sigma <= 0:
        return max(S - K, 0.0)
    d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    return S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)

def implied_volatility(market_price, S, K, T, r):
    """Numerically invert Black-Scholes to find IV using Brent's method."""
    if market_price <= max(S - K * np.exp(-r * T), 0):
        return np.nan
    try:
        iv = brentq(
            lambda sigma: black_scholes_call(S, K, T, r, sigma) - market_price,
            1e-6, 10.0, xtol=1e-6
        )
        return iv
    except ValueError:
        return np.nan

def fetch_iv_from_chain(ticker, target_strike, rfr, days_lookahead):
    """
    Pull the nearest expiry options chain and return IV for the
    option strike closest to target_strike.
    """
    stock = yf.Ticker(ticker)
    spot = stock.history(period="1d")["Close"].iloc[-1]

    expirations = stock.options
    today = pd.Timestamp.today()

    # Filter to expirations within lookahead window
    valid_expiries = [
        exp for exp in expirations
        if 0 < (pd.Timestamp(exp) - today).days <= days_lookahead
    ]
    if not valid_expiries:
        raise ValueError(f"No expirations found within {days_lookahead} days.")

    # Use the nearest valid expiry
    nearest_expiry = valid_expiries[0]
    T = (pd.Timestamp(nearest_expiry) - today).days / 365.0

    chain = stock.option_chain(nearest_expiry)
    calls = chain.calls.copy()
    calls = calls[calls["lastPrice"] > 0.05]  # Filter illiquid strikes

    # Find the strike closest to our target
    calls["strike_diff"] = (calls["strike"] - target_strike).abs()
    best_row = calls.loc[calls["strike_diff"].idxmin()]

    market_price = best_row["lastPrice"]
    actual_strike = best_row["strike"]

    iv = implied_volatility(market_price, spot, actual_strike, T, rfr)

    return {
        "spot": round(spot, 2),
        "strike": actual_strike,
        "expiry": nearest_expiry,
        "T": round(T, 4),
        "market_call_price": round(market_price, 2),
        "implied_volatility": round(iv, 4) if iv else None
    }

iv_data = fetch_iv_from_chain(TICKER, TARGET_STRIKE, RISK_FREE_RATE, DAYS_LOOKAHEAD)
print(pd.Series(iv_data).to_string())
Enter fullscreen mode Exit fullscreen mode

2.3 Computing N(d2) and Scanning for the Mispricing

With spot price, strike, time to expiry, and implied volatility in hand, computing N(d2) — the Black-Scholes risk-neutral probability — is a single closed-form calculation. We then compare it against the Polymarket YES price and flag any gap exceeding the threshold.

def nd2_probability(S, K, T, r, sigma):
    """
    Compute N(d2): the risk-neutral probability that S > K at expiry.
    This is the fair value of a cash-or-nothing binary call.
    """
    d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    return norm.cdf(d2)

def run_scanner(iv_data, polymarket_yes, min_edge):
    """Compare Black-Scholes probability against Polymarket price."""
    S = iv_data["spot"]
    K = iv_data["strike"]
    T = iv_data["T"]
    sigma = iv_data["implied_volatility"]

    if sigma is None:
        print("IV extraction failed — insufficient options data.")
        return None

    bs_prob = nd2_probability(S, K, T, RISK_FREE_RATE, sigma)
    edge = bs_prob - polymarket_yes
    signal = "BUY YES" if edge >= min_edge else (
              "BUY NO"  if edge <= -min_edge else "NO TRADE")

    results = {
        "Ticker":             TICKER,
        "Spot ($)":           S,
        "Strike ($)":         K,
        "Expiry":             iv_data["expiry"],
        "Days to Expiry":     round(T * 365),
        "Implied Vol":        f"{sigma:.1%}",
        "BS Probability":     f"{bs_prob:.1%}",
        "Polymarket YES":     f"{polymarket_yes:.1%}",
        "Edge":               f"{edge:+.1%}",
        "Signal":             signal,
        "Est. ROI (if YES)":  f"{(1 - polymarket_yes) / polymarket_yes:.1%}"
    }
    return results

results = run_scanner(iv_data, POLYMARKET_YES, MIN_EDGE_THRESHOLD)
if results:
    print("\n--- MISPRICING SCANNER RESULTS ---")
    for k, v in results.items():
        print(f"  {k:<25} {v}")
Enter fullscreen mode Exit fullscreen mode

2.4 Visualization

The chart below renders three probability scenarios across a range of implied volatilities — conservative (IV − 5%), base (extracted IV), and stressed (IV + 5%) — alongside the Polymarket YES price. This allows you to see how robust the edge is to volatility estimation error.

plt.style.use("dark_background")
fig, ax = plt.subplots(figsize=(10, 5))

S = iv_data["spot"]
K = iv_data["strike"]
T = iv_data["T"]
base_iv = iv_data["implied_volatility"]

iv_range = np.linspace(0.05, 0.80, 300)
probs = [nd2_probability(S, K, T, RISK_FREE_RATE, iv) for iv in iv_range]

ax.plot(iv_range, probs, color="#00BFFF", linewidth=2, label="N(d2) — BS Probability")
ax.axhline(POLYMARKET_YES, color="#FF6347", linewidth=1.5,
           linestyle="--", label=f"Polymarket YES = {POLYMARKET_YES:.0%}")
ax.axvline(base_iv, color="#FFD700", linewidth=1.2,
           linestyle=":", label=f"Extracted IV = {base_iv:.1%}")

# Shade the edge region
ax.fill_between(iv_range, POLYMARKET_YES, probs,
                where=[p > POLYMARKET_YES for p in probs],
                alpha=0.15, color="#00FF7F", label="Positive Edge Zone")

ax.set_xlabel("Implied Volatility", fontsize=12)
ax.set_ylabel("Risk-Neutral Probability", fontsize=12)
ax.set_title(f"Black-Scholes N(d2) vs Polymarket YES Price\n"
             f"{TICKER}  |  Strike ${K}  |  Expiry {iv_data['expiry']}",
             fontsize=13, pad=12)
ax.yaxis.set_major_formatter(matplotlib.ticker.PercentFormatter(1.0))
ax.xaxis.set_major_formatter(matplotlib.ticker.PercentFormatter(1.0))
ax.legend(loc="upper right", fontsize=10)
ax.grid(alpha=0.2)
plt.tight_layout()
plt.savefig("bs_polymarket_edge.png", dpi=150, bbox_inches="tight")
plt.show()
Enter fullscreen mode Exit fullscreen mode

Figure 1. N(d2) probability curve as a function of implied volatility (blue), overlaid with the Polymarket YES price (red dashed). The green shaded region represents implied volatility levels where Black-Scholes assigns a higher probability than the Polymarket market — the actionable edge zone. The vertical gold line marks the IV extracted from the live options chain.


Enjoying this strategy so far? This is only a taste of what's possible.

Go deeper with my newsletter: longer, more detailed articles + full Google Colab implementations for every approach.

Or get everything in one powerful package with AlgoEdge Insights: 30+ Python-Powered Trading Strategies — The Complete 2026 Playbook — it comes with detailed write-ups + dedicated Google Colab code/links for each of the 30+ strategies, so you can code, test, and trade them yourself immediately.

Exclusive for readers: 20% off the book with code MEDIUM20.

Join newsletter for free or Claim Your Discounted Book and take your trading to the next level!

3. Results and Strategy Performance

In the worked example — MSFT at $415, strike $420, 21 days to expiry, implied volatility at 25% — Black-Scholes returns N(d2) = 0.41, while the Polymarket YES contract trades at 34 cents. The 7-percentage-point gap exceeds the 5% threshold, generating a BUY YES signal. A $100 position in a contract priced at $0.34 that resolves at $1.00 yields a gross return of approximately 194% on capital at risk.

The structural reason this edge persists is the absence of automated cross-market arbitrage. Connecting CBOE options data to Polymarket smart contracts requires bridging tradfi data feeds, blockchain execution, and position management across regulatory boundaries — a non-trivial engineering challenge that most market participants have not solved. The edge exists precisely because it is inconvenient to harvest mechanically.

One published study on applying Geometric Brownian Motion to Bitcoin directional forecasting reported 63.6% directional accuracy — notable because GBM (the stochastic process underlying Black-Scholes) fits crypto arguably better than equities, given the absence of dividend discontinuities and earnings gaps. For equity markets, results are more mixed; the model is a baseline, not a complete description of price dynamics.

Realistic expectations: this is not a high-frequency strategy. Polymarket stock markets carry thin liquidity, and maximum position sizes are often limited to a few hundred dollars before meaningful slippage. The edge is episodic — it appears when sentiment diverges sharply from the options surface — not persistent on every contract. Treat it as an opportunistic scanner, not a systematic production strategy.

Results visualization

4. Use Cases

  • Earnings event markets. Polymarket occasionally lists contracts around earnings dates ("Will MSFT beat EPS estimates?"). Implied volatility spikes before earnings, and the options surface encodes the market's consensus move expectation — a rich signal for binary pricing.

  • Macro binary events. Interest rate decision markets ("Will the Fed cut by 25bps?") can be cross-referenced with Fed Funds futures pricing, which provides an analogous risk-neutral probability without needing Black-Scholes.

  • Crypto prediction markets. Because GBM fits crypto price dynamics more cleanly than equities, N(d2) derived from Deribit or Binance options surfaces may produce tighter and more reliable edges against crypto Polymarket contracts.

  • Multi-strike scanning. The scanner can be extended to loop over multiple tickers and strikes simultaneously, building a ranked table of edge opportunities sorted by gap magnitude — a simple modification to the run_scanner function that wraps it in a for loop over a watchlist.

5. Limitations and Edge Cases

Constant volatility assumption. Black-Scholes assumes a flat volatility surface. In practice, implied volatility varies across strikes (the volatility smile) and time (term structure). Using ATM IV as a single representative figure is an approximation — for strikes far from spot, the smile can introduce meaningful pricing error. Consider using the IV from the specific strike nearest your target rather than ATM IV.

Thin Polymarket liquidity. Many stock-related markets on Polymarket have spreads of 3–5 cents and depth measured in hundreds, not thousands, of dollars. A 7% theoretical edge can be consumed entirely by adverse selection and spread costs before a single contract settles.

Fat-tail events. Black-Scholes assumes log-normal returns. During high-volatility regimes — macro shocks, earnings surprises, geopolitical events — actual return distributions have heavier tails than the model assumes. N(d2) will systematically underestimate tail probabilities in these environments.

Manual data input. The Polymarket YES price is entered manually. Stale prices introduce look-ahead error. The scanner should be run immediately before placing a trade, not hours after reading the contract page.

Model is a baseline, not an oracle. N(d2) is the risk-neutral probability under a specific set of assumptions, not the true physical probability of the event occurring. Risk-neutral and real-world measures diverge, especially for out-of-the-money strikes. The edge calculation assumes the options market is well-calibrated — which is reasonable on liquid names like MSFT, but less so for small-caps or illiquid expiries.

Concluding Thoughts

The central insight here is architectural: two markets pricing the same binary event with incompatible methodologies will periodically disagree. Black-Scholes N(d2) provides a mathematically grounded benchmark probability derived from the options surface, while Polymarket prices are crowd-aggregated sentiment. When those two estimates diverge by 5% or more, a risk-adjusted trade opportunity exists — one that requires no forecasting, only observation.

The implementation built here is a foundation, not a finished product. Meaningful extensions include: pulling IV from the full options surface rather than a single strike, automating Polymarket price ingestion via their public API, applying Kelly sizing to position management, and backtesting edge recurrence frequency across historical options data. Each of those extensions reduces model risk and increases deployment confidence.

If you found this approach useful, the same analytical framework — extracting mathematical structure from market microstructure — applies to a wide range of quantitative strategies. Future articles in this series cover Hidden Markov Model regime filters, pairs trading with cointegration, and volatility surface modeling. Follow along for the full implementation stack.


Most algo trading content gives you theory.
This gives you the code.

3 Python strategies. Fully backtested. Colab notebook included.
Plus a free ebook with 5 more strategies the moment you subscribe.

5,000 quant traders already run these:

Subscribe | AlgoEdge Insights

Top comments (0)