The Wheel Screener Methodology

A defensible ranking for cash-secured puts and covered calls. Adjusts naive yield for historical assignment probability, conditional loss given assignment, and the risk-free hurdle. Go to the screener

Educational. Scores do not constitute trading recommendations. See Terms §17.

TL;DR

Every public wheel screener ranks cash-secured puts by some form of premium / strike × 365 / DTE. That number is misleading because it ignores:

  • How often the put actually breaches — measured against the actual historical price tape, not just the option's delta.
  • How deep the breach is when it happens — the conditional drawdown given assignment, in percent of strike.
  • The risk-free rate — every dollar tied up in CSP collateral is a dollar not earning T-bill yield. A CSP that doesn't clear the T-bill is a worse T-bill with tail risk.
  • Event risk — earnings inside the DTE window inflate IV but also realized moves. Lumping these in with calm names corrupts the ranking.
  • Bag-hold risk — a 50%-IV strike on a stock that fell 60% over 5 years isn't paying you premium, it's paying you to catch a falling knife.

The screener that follows replaces naive yield with excess annualized expected return, computed against an empirical breach table built from 5 years of daily closes on the ~2,500-symbol wheel-eligible universe. Every component is exposed so you can re-rank by any signal you trust.

The wrong way Why naive yield misleads

The default screener formula in every Reddit thread:

naive_yield_annualized = (mid_premium / strike) × (365 / DTE)

This treats every premium dollar as pure return. It tells you nothing about the probability the position ends in assignment, nothing about how bad the assignment is when it happens, and nothing about the opportunity cost of locking up collateral.

The trap. Below is the kind of position naive yield rewards: a 7-day 50-delta put on a meme-rotation stock. Naive yield prints 80% annualized. You feel paid. In reality, the historical probability of being assigned is ~40%, the average conditional loss given assignment is 10% of strike, and the symbol's 5-year return is negative. You're not earning premium; you're being paid to receive a falling stock that the market correctly thinks is going lower.

The fix is not to throw out yield. It's to keep yield and subtract the things you'd subtract if you were honest.

Fix 1 — Assignment probability Historical + implied

Two independent estimates of P(assignment), shown side-by-side:

  • Implied (the market's view). The risk-neutral probability that the short put closes in the money, approximated by |short_delta|. Pulled directly from the chain.
  • Historical (the tape's view). Walk five years of daily closes for this ticker. For each historical observation date t, ask: did close[t + DTE] drop more than moneyness% below close[t]? Aggregate to a breach rate per (DTE, moneyness%) cell.

The interesting signal is the gap between the two:

edge ≈ |delta| − historical_breach_rate

When |delta| > historical_breach_rate by a meaningful margin, the market is pricing assignment risk above what actually happens at that moneyness — the wheel edge. When the inequality flips, the market is right and you're underpaid. The screener exposes this as p_breach_gap and you can sort by it directly.

For ranking, the conservative blend is max(implied, historical) — when the two disagree, take the more pessimistic side. This biases the score toward positions that are cheap by both measures.

Fix 2 — Conditional loss given assignment The honest tail

P(assignment) is half the story. The other half: when this thing does get assigned, how much worse off are you than you would have been if it had expired worthless?

For a CSP that breaches, the conditional loss is the additional drop below the strike, as a fraction of strike:

cond_loss_pct = E[ (strike − close[t+DTE]) / strike | breach ]

For a covered call that gets called away, the equivalent is forgone upside above the strike:

cond_loss_pct = E[ (close[t+DTE] − strike) / strike | breach ]

Both come from the same historical walk that produced the breach rate. The screener stores conditional mean, p95, and max per (DTE, moneyness%) cell so you can also see the worst case, not just the average.

The expected cycle return becomes:

expected_return_per_share = (1 − p_breach) × mid_premium + p_breach × (mid_premium − cond_loss_pct × strike)
Fix 3 — Risk-free hurdle Your CSP vs. a T-bill

A cash-secured put locks up strike × 100 for the life of the trade. With short-tenor Treasuries at 4–5%, that capital has a real opportunity cost. The honest comparison is not "did I make money" — it's "did I make more money than the T-bill I'm displacing."

excess_annualized_return = (expected_return_per_share / strike) × (365 / DTE) − risk_free_rate_at_DTE_tenor

The screener reads the live risk-free curve from the Federal Reserve cron, picks the tenor closest to your DTE (4-week, 3-month, 6-month, 1-year), and linearly interpolates. Sub-T-bill positions don't get filtered — they just print negative excess_annualized_return so you can see what's actually cheap.

Filters — earnings & bag-hold What math can't fix

Earnings inside DTE. Earnings releases drive the largest single-day idiosyncratic moves in the dataset. IV is rich, but realized moves are also rich. Some people specifically want this — earnings premium is a legitimate sub-strategy. Lumping it with non-event setups corrupts the ranking, so the screener flags it (has_earnings_in_dte) and the UI lets you include, exclude, or filter to earnings-only.

Bag-hold score (0–3). Composite flag for "this stock will be a problem to own at strike if you get assigned":

ComponentTriggerWhy it matters
5-year return < 0Persistent downtrendThe wheel doesn't work on stocks where the price floor keeps falling.
Realized vol > 60%Annualized stdev of daily log returnsHigh vol = wider tail, deeper conditional losses.
1-year max drawdown < −50%Recent crash inside lookbackIf half the value can disappear in 12 months, the assignment math is incomplete.

Sum of triggers = 0–3. The screener doesn't drop these — they're flagged. Filter by max_bag_hold if you only want clean names; leave it at 3 if you specifically want the spicy yield.

Leveraged & inverse ETFs Default excluded

3x and 2x leveraged ETFs (SOXL, TQQQ, UPRO, NUGT, MVLL, TSLL, etc.) rank near the top of any wheel screener that sorts by yield. The premium is real — IV is enormous because these vehicles move 2–3× the underlying every day. The problem is what happens after assignment.

Daily-rebal NAV decay (volatility drag). A 3x ETF achieves 3× the underlying's daily return through nightly rebalancing. The compounded multi-day return is not 3× the underlying's multi-day return:

Underlying: +10%, −10%, +10%, −10% → final = −2.0% 3x daily: +30%, −30%, +30%, −30% → final = −17.4%

In choppy markets the leveraged ETF erodes even when the underlying ends flat. The issuer (Direxion, ProShares, GraniteShares, etc.) prints this in the prospectus: "intended for daily exposure, not for periods longer than one day." Wheeling them — where assignment means owning the vehicle for the duration of the next CC cycle, possibly weeks — fights that design directly.

The implication for assignment math. Our historical breach table walks 5 years of daily closes, so the conditional_loss_pct on a leveraged ETF already partially reflects decay-driven drawdowns. But the table can't price the path-dependent rebal effect during your specific holding period. Two assignments at the same strike with the same DTE will have systematically different recovery profiles than the unleveraged equivalent — and worse on average in any non-trending tape.

How the screener handles it.

  • ~131 known leveraged / inverse ETFs are tagged in the cache with is_leveraged=true and a signed leverage_factor (±2, ±3, ±1.5 for single-stock products).
  • The default API filter is leveraged=exclude. The UI flips this via a dropdown.
  • Leveraged rows that pass the filter get a LEV3x / INV3x badge next to the symbol and a +1 bump to bag_hold_score (capped at 3) so the bag-hold filter also catches them.

If you specifically want to wheel leveraged premium for the IV, switch the dropdown to "Include" or "Only" — the data is there. The default just keeps the top of the ranking honest.

Fix 4 — Multi-window breach & regime drift Stationarity, addressed

A single five-year breach rate is a lossy summary. If the underlying spent 2021–2022 in a volatile bear regime and 2024–2026 in a calm rotation, averaging them produces a number that fits neither. The breach table now computes the same cell three ways and exposes the disagreement:

breach_rate_5y = breaches over full ~1,260-trading-day window (the anchor) breach_rate_3y = breaches over the trailing 756 trading days breach_rate_1y = breaches over the trailing 252 trading days regime_drift_pct = (breach_rate_1y / breach_rate_5y − 1) × 100

The drift number is the headline signal:

  • Drift > +25%: recent regime is materially riskier than the 5y baseline. Your historical anchor is optimistic; expect breaches more often than the 5y number suggests.
  • Drift between −15% and +15%: regime stable. Trust the 5y number.
  • Drift < −25%: recent regime is calmer. Premium may be priced off a fearful long-run memory the tape no longer supports — a potential edge for sellers, but watch for regime reversion.

What feeds the score. wheel_score uses breach_rate_5y as the historical anchor (most observations, least noise). Drift is surfaced as a separate column so you can see the disagreement and choose whether to trust the 5y number or down-weight it.

Fix 5 — Empirical post-assignment recovery The other side of cond_loss

Conditional loss says "if you get assigned, here's the average drop below strike." It does not say what happens next. For a wheeler who rolls into CCs after assignment, the relevant question is: how often did the stock recover back to strike before the next CC cycle ended?

For each historical breach in the table, we walk forward and ask:

recovery_30d = P(max close in next 21 trading days ≥ original short strike | breach) recovery_60d = P(max close in next 42 trading days ≥ original short strike | breach) recovery_90d = P(max close in next 63 trading days ≥ original short strike | breach)

A high recovery rate means most assignments self-resolve within one or two CC cycles — the cond_loss is real but rarely permanent. A low recovery rate means assignments tend to stick. Concrete examples from current data:

Symbol5y breachCond lossRec 30dRec 60dRec 90dRead
AAPL 30d 5% OTM22%3.6%73%82%88%Clean: even when assigned, ~3 of 4 recover inside one CC cycle.
TSLA 30d 5% OTM36%10.4%47%67%72%Spicy: slow to recover, but most do within 90 days.
MARA 30d 5% OTM45%20.0%41%53%60%Trap: 40% of assignments stick past 90 days — these are the bag-holds.

Recovery is computed for puts only. For called-away CCs the position is closed, so the "recovery" framing doesn't apply — the wheel resets to CSPs.

Composite score Everything together
wheel_score = excess_annualized_return × liquidity_score liquidity_score = oi_factor × spread_factor oi_factor ∈ {0.65, 0.85, 1.0} (50 / 200 / 1000 OI thresholds) spread_factor ∈ [0.4, 1.0] (bid-ask / mid; rejected above 50%)

Plain multiplication. No black-box weights. The headline is the expected economic return above T-bills, discounted for execution friction.

Every component the score is built from is exposed in the screener — sort by any of them. The defaults are: side=put, min_dte=7, max_dte=45, earnings=include, max_bag_hold=3, sort=wheel_score.

Worked examples

Three CSP candidates from real chain data on June 11, 2026, ranked in the order the methodology would order them. Numbers come from the production cache; see the live screener for the latest.

SymbolStrikeDTEMny%MidΔp_implp_histCond lossRaw yieldExcessBagVerdict
SOXL LEV3x $22071.97$24.10−0.4141.4%39.9%10.15%571%+348%2 Real premium edge (implied ≈ historical, both ~40%), but SOXL is a 3x leveraged semis ETF — daily-rebal NAV decay means the assignment math understates the cost of being stuck holding. Bag-hold bumped to 2 by the leverage tag. Hidden by default in the screener; opt in only if you specifically want to farm 3x-ETF premium.
AAPL $29570.20$3.75−0.4646.0%24.4%2.5%66%+2.3%0 Honest yield: market is pricing 46% breach, history says 24%. Conditional loss tiny (low vol). Clean name. Raw yield 66% is a mirage — once you adjust, it clears T-bills by 2pp.
MARA $13714.8$1.78−0.3736.9%45.7%29.1%70%−1.7%3 The trap. Raw yield 70% looks juicy. Historical breach (45.7%) is well above implied (36.9%) — the market is underpricing assignment. Conditional loss 29% of strike, bag-hold maxed. Negative excess. Avoid.

Reading the table. Sorting by raw yield puts MARA above AAPL. Sorting by excess_annualized_return — the methodology's headline column — puts SOXL ≫ AAPL ≫ MARA. Same chains, completely different ranking. That's the whole point.

Known limitations

Three of the previous limitations got promoted to features (see Fix 4 — regime drift and Fix 5 — recovery rate above, and the data-sanity bullet below for the illiquid-quote hardening). The remaining honest caveats:

  • Implied volatility skew is not separately decomposed. The wheel screener bakes skew into p_breach_implied via |delta| at the actual strike, which is enough for ranking but does not surface RR25 / BF25 as standalone columns the way the Iron Condor screener does. When p_breach_impliedp_breach_historical on a low-vol name, sticky put skew is the usual explanation — but you have to infer it. v2 will surface the decomposition.
  • Dividend timing is ignored for CCs. Deep-ITM calls with ex-div inside DTE risk early assignment as the call holder captures the dividend instead of you. v1 does not flag this; v2 will pull from DividendTracker. For now, prefer CCs that don't span an ex-div.
  • Illiquid-name stale quotes. The data-sanity layer rejects: bid ≤ 0, mid > 20% of strike (puts) / 20% of underlying (calls), mid/strike inconsistent with |delta|, no trades today AND OI < 200, and last-trade vs mid divergence > 50%. These catch the worst stale-snapshot cases but on thin names treat liquidity_score as a hard floor, not a tiebreaker — a "great" row backed by 50 OI and a $0.10/$0.30 quote is not actually fillable at mid.
  • Regime drift is descriptive, not predictive. Fix 4 surfaces how much the 1y breach rate diverges from the 5y, but the score still uses the 5y anchor. If you trust the recent regime more, sort by p_breach_gap or filter for drift < 0 — but the methodology won't tell you whether to trust the 1y or the 5y. That's a judgment call the data can't make for you.
Use the screener

The full screener is live at /wheel-screener with sortable columns for every component above.

Open the screener