Covered Call Screener: Build One in SQL
Build a covered call screener in SQL: rank a whole option chain by annualised premium yield inside a delta band, then replay the screen on past dates.
A covered call screener ranks every call in an option chain by the premium it pays, annualised, inside a chosen delta band. One SQL query does that job, against global_markets.options_greeks, the per-contract daily table of implied volatility and greeks that starts in August 2021. The query comes first, with the filters that keep its output readable. The second half replays the same screen on past dates to show how the calls it picked finished.
What a covered call screener measures
A covered call is 100 shares you already own plus one call sold against them. The buyer pays premium today for the right to buy those shares at the strike price up to expiration. The position arithmetic, including what the share leg does to the outcome, is worked through in covered call return math, and the position itself in how a covered call works. This post is about the ranking step that comes before any of it.
The ranking number is annualised premium yield: premium divided by the share price, scaled to a year. Sell a $2.00 call on a $200 stock with 30 days left and the premium is 1.0% of the share price. Scaled by 365/30 it prints as 12.2% annualised. Annualising is what puts a 9-day contract and a 45-day contract on one axis. It is a comparison scale, not a forecast: nothing in the number says the shares stay under the strike.
The delta band is the first knob
Delta is the change in the option's price for a $1 move in the shares, and it doubles as a rough odds figure. A 0.30 delta call finishes in the money something like 30% of the time. Option delta covers the mechanics. Premium and delta rise together, which is the trade-off a screener has to put in front of you rather than bury.
The panel below takes every AAPL call with 20 to 45 days to expiration that traded during August 2026, keeps the strikes above the share price, and averages the annualised yield inside each delta band. Call deltas are positive and put deltas are negative, so the delta filter alone isolates the calls.
| delta_band | contracts | avg_otm_pct | avg_annual_yield_pct | avg_iv_pct |
|---|---|---|---|---|
| delta 0.05-0.10 | 170 | 12.53 | 2.7 | 25.2 |
| delta 0.10-0.20 | 177 | 8.81 | 6.1 | 24.5 |
| delta 0.20-0.30 | 122 | 5.69 | 11.8 | 24.3 |
| delta 0.30-0.40 | 101 | 3.5 | 18.7 | 24.3 |
| delta 0.40-0.50 | 95 | 1.53 | 27.1 | 24.4 |
| delta 0.50-0.65 | 23 | 0.28 | 34.2 | 25 |
The exact SQL behind every number
SELECT
multiIf(delta < 0.10, 'delta 0.05-0.10',
delta < 0.20, 'delta 0.10-0.20',
delta < 0.30, 'delta 0.20-0.30',
delta < 0.40, 'delta 0.30-0.40',
delta < 0.50, 'delta 0.40-0.50',
'delta 0.50-0.65') AS delta_band,
count() AS contracts,
round(avg(toFloat64(strike_price) / toFloat64(underlying_close) - 1) * 100, 2) AS avg_otm_pct,
round(avg(toFloat64(option_close) / toFloat64(underlying_close)
* 365.0 / days_to_expiry) * 100, 1) AS avg_annual_yield_pct,
round(avg(implied_volatility) * 100, 1) AS avg_iv_pct
FROM global_markets.options_greeks
WHERE underlying_symbol = 'AAPL'
AND date >= '2026-08-01'
AND date < '2026-09-01'
AND iv_converged = 1
AND volume > 0
AND days_to_expiry BETWEEN 20 AND 45
AND delta BETWEEN 0.05 AND 0.65
AND strike_price > underlying_close
GROUP BY delta_band
ORDER BY min(delta)Read the curve from left to right. The lowest band, delta 0.05-0.10, sat an average 12.53% above the share price and paid 2.7% annualised across 170 contract-sessions. The top band, delta 0.50-0.65, sat 0.28% above spot and paid 34.2%. The implied volatility column barely moves down the table while the yield column climbs: the yield is coming from strike distance and odds, not from a richer volatility quote.
Where the premium sits across underlyings
Same month, delta window narrowed to 0.25 through 0.35, eight liquid names instead of one.
Of the 8 names that cleared the 20-contract minimum, AMD paid the most at the same delta: 36.7% annualised, alongside an average implied volatility of 63%. SPY sat at the other end of the board at 7%. Set the yield column next to the volatility column and the pairing is plain: at a fixed delta, premium yield is implied volatility wearing different units.
Five filters that turn a chain dump into a screen
An unfiltered chain is thousands of rows, most of them untradeable. Five filters do the cleanup, and each removes a specific kind of row.
- An out of the money band, 2% to 12% above the last close. Without it, deep in the money calls top the board on premium that is mostly intrinsic value rather than time value.
- A delta window, 0.25 to 0.35. Without it, 0.6 delta calls dominate the ranking with yields that come attached to the shares being called away most of the time.
- A minimum premium, 30 cents. Penny contracts annualise to big numbers, and a single tick of slippage eats a large share of the premium.
- A volume floor, 100 contracts on the screen date. The greeks rows carry no open interest column, so same-day contract volume stands in for it.
- A spread guard, expressed as a share of the midpoint. A $1.00 bid against a $1.10 ask is a $1.05 mid with a spread near 9.5% of it. The daily greeks rows carry no bid or ask, so this filter runs against
cache_options_quoteson whatever survives the first four.
Here are the first four applied to the last August 2026 session, ranked by annualised yield.
| contract_label | screened_on | dte | delta | otm_pct | premium | annual_yield_pct | volume |
|---|---|---|---|---|---|---|---|
| AMD Oct 2 $515 | Aug 31 | 32 | 0.297 | 9.65 | 11.97 | 29.1 | 343 |
| AMD Oct 2 $520 | Aug 31 | 32 | 0.273 | 10.71 | 10.66 | 25.9 | 296 |
| NVDA Sep 25 $230 | Aug 31 | 25 | 0.326 | 4.42 | 3.69 | 24.5 | 4538 |
| XOM Sep 25 $167.5 | Aug 31 | 25 | 0.311 | 3.95 | 2.12 | 19.2 | 180 |
| MSFT Sep 25 $525 | Aug 31 | 25 | 0.322 | 3.39 | 6.4 | 18.4 | 220 |
| NVDA Oct 9 $235 | Aug 31 | 39 | 0.298 | 6.69 | 4.1 | 17.4 | 333 |
| NVDA Oct 2 $235 | Aug 31 | 32 | 0.267 | 6.69 | 3.15 | 16.3 | 1288 |
| AAPL Sep 25 $330 | Aug 31 | 25 | 0.286 | 4.1 | 3.45 | 15.9 | 1145 |
| AAPL Oct 2 $330 | Aug 31 | 32 | 0.311 | 4.1 | 4.27 | 15.4 | 346 |
| XOM Oct 2 $170 | Aug 31 | 32 | 0.274 | 5.5 | 2.05 | 14.5 | 127 |
| MSFT Sep 25 $530 | Aug 31 | 25 | 0.266 | 4.38 | 4.9 | 14.1 | 151 |
| MSFT Oct 2 $535 | Aug 31 | 32 | 0.256 | 5.36 | 5.34 | 12 | 938 |
The exact SQL behind every number
WITH (
SELECT max(date)
FROM global_markets.options_greeks
WHERE date >= '2026-08-01' AND date < '2026-09-01'
) AS screen_session
SELECT
concat(underlying_symbol, ' ',
formatDateTime(expiration_date, '%b %e'), ' $',
toString(round(toFloat64(strike_price), 2))) AS contract_label,
formatDateTime(screen_session, '%b %e') AS screened_on,
days_to_expiry AS dte,
round(delta, 3) AS delta,
round((toFloat64(strike_price) / toFloat64(underlying_close) - 1) * 100, 2) AS otm_pct,
round(toFloat64(option_close), 2) AS premium,
round(toFloat64(option_close) / toFloat64(underlying_close)
* 365.0 / days_to_expiry * 100, 1) AS annual_yield_pct,
volume
FROM global_markets.options_greeks
WHERE underlying_symbol IN ('AAPL', 'MSFT', 'NVDA', 'AMD', 'KO', 'JNJ', 'XOM', 'SPY')
AND date = screen_session
AND iv_converged = 1
AND volume >= 100
AND days_to_expiry BETWEEN 20 AND 45
AND delta BETWEEN 0.25 AND 0.35
AND toFloat64(strike_price) / toFloat64(underlying_close) - 1 BETWEEN 0.02 AND 0.12
AND option_close >= 0.30
ORDER BY annual_yield_pct DESC
LIMIT 12Run on Aug 31, the screen returned 12 rows, which is the point of a screen: a whole board to read. The top row, AMD Oct 2 $515, printed 29.1% annualised from a $11.97 premium at delta 0.297, 9.65% above the share price, with 32 days left and 343 contracts traded that session. The last row on the board still annualised 12%.
Replaying the screen on past dates
This data is end of day, and the front edge runs a session or two behind live, so the board above is a historical board. What end of day data offers instead of a live snapshot is history. The panel below runs the identical screen on the first session of each month from September 2025 through July 2026, with every selected call now expired, and joins each contract's expiration-day close from stocks_daily_aggs to settle whether it finished above the strike.
| month | calls_screened | avg_annual_yield_pct | itm_rate_all_pct | itm_rate_richest_pct |
|---|---|---|---|---|
| 2025-09 | 34 | 12.5 | 52.9 | 42.9 |
| 2025-10 | 28 | 17.9 | 42.9 | 83.3 |
| 2025-11 | 27 | 18.4 | 3.7 | 0 |
| 2025-12 | 30 | 15 | 10 | 16.7 |
| 2026-01 | 27 | 15.3 | 14.8 | 20 |
| 2026-02 | 18 | 20 | 0 | 0 |
| 2026-03 | 35 | 15.8 | 5.7 | 14.3 |
| 2026-04 | 52 | 15.8 | 92.3 | 100 |
| 2026-05 | 38 | 17 | 57.9 | 100 |
| 2026-06 | 29 | 17.8 | 3.4 | 0 |
| 2026-07 | 22 | 20.8 | 40.9 | 0 |
The exact SQL behind every number
WITH
selection_days AS
(
SELECT min(date) AS session_date
FROM global_markets.options_greeks
WHERE date >= '2025-09-01' AND date < '2026-08-01'
GROUP BY toStartOfMonth(date)
),
picks AS
(
SELECT
formatDateTime(date, '%Y-%m') AS month,
underlying_symbol AS symbol,
expiration_date AS expiry,
toFloat64(strike_price) AS strike,
toFloat64(option_close) / toFloat64(underlying_close)
* 365.0 / days_to_expiry * 100 AS annual_yield_pct
FROM global_markets.options_greeks
WHERE date >= '2025-09-01'
AND date < '2026-08-01'
AND date IN (SELECT session_date FROM selection_days)
AND underlying_symbol IN ('AAPL', 'MSFT', 'NVDA', 'AMD', 'KO', 'JNJ', 'XOM', 'SPY')
AND iv_converged = 1
AND volume >= 100
AND days_to_expiry BETWEEN 20 AND 45
AND delta BETWEEN 0.25 AND 0.35
AND toFloat64(strike_price) / toFloat64(underlying_close) - 1 BETWEEN 0.02 AND 0.12
AND option_close >= 0.30
AND expiration_date <= '2026-09-19'
),
rich_line AS
(
SELECT
month,
avg(annual_yield_pct) * 1.5 AS cutoff
FROM picks
GROUP BY month
)
SELECT
p.month AS month,
count() AS calls_screened,
round(avg(p.annual_yield_pct), 1) AS avg_annual_yield_pct,
round(countIf(toFloat64(u.close) > p.strike) / count() * 100, 1) AS itm_rate_all_pct,
round(countIf(toFloat64(u.close) > p.strike AND p.annual_yield_pct >= r.cutoff)
/ countIf(p.annual_yield_pct >= r.cutoff) * 100, 1) AS itm_rate_richest_pct
FROM picks AS p
INNER JOIN
(
SELECT ticker, date, close
FROM global_markets.stocks_daily_aggs
WHERE date >= '2025-09-15'
AND date <= '2026-09-19'
AND ticker IN ('AAPL', 'MSFT', 'NVDA', 'AMD', 'KO', 'JNJ', 'XOM', 'SPY')
) AS u ON u.ticker = p.symbol AND u.date = p.expiry
INNER JOIN rich_line AS r ON r.month = p.month
GROUP BY p.month
HAVING countIf(p.annual_yield_pct >= r.cutoff) > 0
ORDER BY p.monthAcross the 11 replay months, the screen selected 34 calls in 2025-09 at an average 12.5% annualised, and 22 calls in 2026-07 at 20.8%. The two in-the-money columns are the part worth sitting with. In 2025-09, 52.9% of the selected calls finished above their strike, against 42.9% for the richest contracts in that month's selection, defined as the ones paying at least 1.5 times the month's average annualised yield.
The mechanism is worth stating plainly, since a yield-sorted board keeps it out of sight. At a fixed delta band, a richer premium is a higher implied volatility quote, and a higher implied volatility quote is a wider distribution of finishing prices. The top of the board is the row the market prices for the most movement. A screen that sorts by yield and stops there is a screen that sorts by implied volatility and stops there.
That is also the argument for owning the query rather than renting a screen. Tightening the delta window to 0.20 through 0.28 pulls the whole yield column down and pulls the in-the-money rate down with it. Pushing the out of the money band from 2% to 5% leaves more room above the strike and less premium in the row. Shortening the expiry window concentrates the screen in the weeks where names with daily option expirations offer the most strikes to choose among. Each of those is one edit to a WHERE clause, with the whole board recomputed underneath it.
FAQ
How do you annualise a covered call premium?
Divide the premium by the current share price, then multiply by 365 divided by the days to expiration. A $1.50 premium on a $100 stock with 45 days left is 1.5% of the share price, or about 12.2% annualised. The scaling lets contracts with different expirations be ranked on one number.
Why do covered call screens center on 0.30 delta?
Delta approximates the odds a call finishes in the money, so a 0.30 band is a way of holding the odds roughly fixed while comparing premium across strikes, expirations, and underlyings. Move the band up and both the premium and the chance of the shares being called away move up with it.
Can you screen covered calls with SQL for free?
Yes. Per-contract daily implied volatility and greeks, plus daily underlying closes, are enough to rank a full chain by annualised premium yield, and the whole screen is a single SELECT. The endpoint and query limits are covered in the free SQL API guide.
Does end of day options data work for a covered call screen?
For ranking and for backtesting a ranking, yes. End of day rows will not give you a live bid to sell into, and the most recent session or two may still be loading, so the live quote check happens at execution time against the options quote data rather than in the screen itself.
Query windows, data notes, and the Python wrapper
Every panel here reads global_markets.options_greeks, one row per contract per session since August 2021, with greeks and implied volatility priced off that session's close. Windows: the delta ladder, the per-name comparison, and the screen board all cover August 2026, with the board itself running on the last session in that month. The replay selects on the first session of each month from September 2025 through July 2026 and joins expiration-day closes from stocks_daily_aggs over September 15, 2025 through September 19, 2026. All panels require iv_converged = 1 and same-day volume, which drops contracts whose greeks did not solve and contracts that did not trade.
Limits worth stating. The greeks rows carry no bid, ask, or open interest, which is why the spread guard reads the options quote data and why same-day contract volume stands in for open interest. The replay requires every selected contract to have already expired, so the last selection month is July 2026.
A thin wrapper is all the Python this needs, matching the one in the free SQL API guide:
import requests
API_URL = "..." # the SQL endpoint from the free SQL API guide
def run_sql(sql: str) -> list[dict]:
reply = requests.post(API_URL, json={"sql": sql}, timeout=180)
reply.raise_for_status()
return reply.json()["rows"]
board = run_sql(SCREEN_SQL)
for row in board[:5]:
print(row["contract_label"], row["annual_yield_pct"])
Install the one dependency with pip install requests. SCREEN_SQL is the screen above, pasted unchanged.
Every panel on this page ships with the exact SQL beneath it, so the knobs are all visible and all editable. Open one, change the delta band, and run the same question on the Strasmore terminal.