Building a ROIC-WACC Spread Screener

The ROIC-WACC spread is arguably the most important number in fundamental analysis. A positive spread means a company earns more on its invested capital than its cost of capital — it's creating economic value. This tutorial builds a screener that ranks the entire market by value creation.

~12 min read · Intermediate Python

The Theory

ROIC (Return on Invested Capital) measures how efficiently a company deploys capital. WACC (Weighted Average Cost of Capital) is the minimum return investors demand.

Economic Value Added = Invested Capital × (ROIC − WACC)

McKinsey's research (Valuation, 7th ed.) shows that long-run stock returns are driven almost entirely by two factors: revenue growth and ROIC relative to WACC. Companies that maintain ROIC > WACC for 10+ years (like Visa, Costco, MSCI) compound extraordinary shareholder value.

Spread > 10%
Value Creator
0-10%
Marginal
Negative
Value Destroyer

Step 1: Fetch Quant Health Data

import requests
import time

def get_quant_health(ticker):
    """Fetch quant health scores including ROIC-WACC spread."""
    url = f"https://securitiesdb.com/api/v1/stocks/{ticker}/quant-health"
    r = requests.get(url)
    if r.status_code != 200:
        return None
    return r.json()["data"]

# Test with a known value creator
data = get_quant_health("V")
vc = data["value_creation"]
print(f"Visa ROIC: {vc['roic']*100:.1f}%")
print(f"Visa WACC: {vc['wacc']*100:.1f}%")
print(f"Visa Spread: {vc['roic_wacc_spread']*100:.1f}%")
# Expected: massive positive spread (Visa has very high ROIC)

Step 2: Screen the Universe

# Screen a universe of blue chips
universe = [
    "AAPL", "MSFT", "GOOGL", "AMZN", "META", "NVDA",
    "V", "MA", "COST", "UNH", "JNJ", "PG", "KO", "PEP",
    "JPM", "BAC", "GS", "WMT", "HD", "LOW",
    "TSLA", "NFLX", "CRM", "ADBE", "INTC", "T", "VZ",
]

results = []
for ticker in universe:
    data = get_quant_health(ticker)
    if not data or not data.get("value_creation"):
        continue

    vc = data["value_creation"]
    scores = data["scores"]

    results.append({
        "ticker": ticker,
        "roic": vc.get("roic", 0),
        "wacc": vc.get("wacc", 0),
        "spread": vc.get("roic_wacc_spread", 0),
        "piotroski": scores.get("piotroski_f", 0),
        "altman_z": scores.get("altman_z", 0),
    })
    time.sleep(0.5)  # Be polite to the API

# Sort by spread descending
results.sort(key=lambda x: x["spread"], reverse=True)

Step 3: Analyze with DuPont Decomposition

The quant-health endpoint also returns a DuPont decomposition, telling you why ROIC is high: is it margin-driven (pricing power) or asset-turnover-driven (asset-light)?

# Deep-dive into the top 5 value creators
print("\n🏆 Top Value Creators — DuPont Decomposition:")
print(f"{'Ticker':8s} {'Spread':>8s} {'Margin':>8s} {'Turnover':>8s} {'Leverage':>8s} {'F-Score':>8s}")
print("-" * 48)

for r in results[:5]:
    data = get_quant_health(r["ticker"])
    dupont = data.get("dupont", {})
    print(f"{r['ticker']:8s} "
          f"{r['spread']*100:>7.1f}% "
          f"{dupont.get('net_margin', 0)*100:>7.1f}% "
          f"{dupont.get('asset_turnover', 0):>7.2f}x "
          f"{dupont.get('equity_multiplier', 0):>7.2f}x "
          f"{r['piotroski']:>7d}/9")
    time.sleep(0.5)

Step 4: Combine with DCF for a Buy Signal

Find value creators that are also undervalued by DCF:

# Cross-reference with DCF valuation
buy_candidates = []
for r in results:
    if r["spread"] <= 0.05:  # Only positive spread > 5%
        continue

    dcf = requests.get(
        f"https://securitiesdb.com/api/v1/stocks/{r['ticker']}/dcf"
    ).json().get("data", {})

    upside = dcf.get("upside_pct", 0)
    if upside > 15:  # At least 15% DCF upside
        buy_candidates.append({
            **r,
            "dcf_upside": upside,
            "fair_value": dcf.get("fair_value"),
        })
    time.sleep(0.5)

print("\n💎 Buy Candidates (Value Creator + DCF Undervalued):")
for c in buy_candidates:
    print(f"  {c['ticker']}: Spread={c['spread']*100:.1f}%, "
          f"DCF Upside={c['dcf_upside']:.0f}%, "
          f"Piotroski={c['piotroski']}/9")

What You Built

  • A ROIC-WACC spread screener for any stock universe
  • DuPont decomposition to understand why returns are high
  • Cross-referencing with DCF for a complete value + growth signal
  • Quality filter using Piotroski F-Score (>=7) and Altman Z-Score (>3)

This is the same analysis Bloomberg Terminal users pay $25,000/year for. The SecuritiesDB API makes it free and programmable.

Related API Endpoints