Iron Condor Screener from the SQL API
How to build an iron condor screener in SQL from daily options greeks, ranked by IV percentile, with a check on how many candidates still clear a week later.
An iron condor screener answers one narrow question with data: on a single option chain, which four leg combinations pay enough credit to justify the width of their wings. This post builds that screener in SQL, one visible stage at a time, from daily per contract deltas and closing prices. It then adds something a live snapshot cannot produce: the same filters rerun a week later on the same four contracts, with a count of how many still clear.
The structure itself is covered in iron condor vs iron butterfly, and it is an input here. The screener takes the shape as given, then asks the chain which instances of it exist on a given date.
What an iron condor screener filters for
Four rules turn a chain of several thousand contracts into a short list:
- a delta band for the two short legs, 0.12 to 0.20 below, which fixes how far out of the money they sit
- a wing width, 5 points, which fixes the maximum loss
- a floor on credit as a share of maximum loss, 20 percent, which is what the ranking runs on
- a tradability test on all four legs, so a structure priced off a contract nobody traded never reaches the list
Delta, the option's sensitivity to a one dollar move in the underlying, does double duty. Puts carry negative delta and calls carry positive delta, so the band alone picks the correct side of the chain with no separate contract type filter. The covered call screener in this series runs the same stages against a simpler join.
Stage one: pin one underlying and one expiration
Start narrow. One underlying, SPY. One snapshot date, the latest session present in the data. One expiration, the most heavily traded of those with 25 to 45 days left. Strikes are restricted to the 5 point grid, which is where the symmetric wings come from.
| strike | put_delta | call_delta | expiry | as_of |
|---|---|---|---|---|
| 720 | -0.123 | 0.918 | Oct 30, 2026 | Sep 24, 2026 |
| 725 | -0.137 | 0.874 | Oct 30, 2026 | Sep 24, 2026 |
| 730 | -0.162 | 0.859 | Oct 30, 2026 | Sep 24, 2026 |
| 735 | -0.183 | 0.822 | Oct 30, 2026 | Sep 24, 2026 |
| 740 | -0.212 | 0.759 | Oct 30, 2026 | Sep 24, 2026 |
| 745 | -0.249 | 0.73 | Oct 30, 2026 | Sep 24, 2026 |
| 750 | -0.288 | 0.692 | Oct 30, 2026 | Sep 24, 2026 |
| 755 | -0.332 | 0.651 | Oct 30, 2026 | Sep 24, 2026 |
| 760 | -0.384 | 0.61 | Oct 30, 2026 | Sep 24, 2026 |
| 765 | -0.441 | 0.557 | Oct 30, 2026 | Sep 24, 2026 |
| 770 | -0.504 | 0.497 | Oct 30, 2026 | Sep 24, 2026 |
| 775 | -0.576 | 0.434 | Oct 30, 2026 | Sep 24, 2026 |
| 780 | -0.639 | 0.368 | Oct 30, 2026 | Sep 24, 2026 |
| 785 | -0.66 | 0.309 | Oct 30, 2026 | Sep 24, 2026 |
| 790 | -0.767 | 0.246 | Oct 30, 2026 | Sep 24, 2026 |
| 800 | -0.821 | 0.143 | Oct 30, 2026 | Sep 24, 2026 |
The exact SQL behind every number
WITH
(
SELECT max(date)
FROM global_markets.options_greeks
WHERE underlying_symbol = 'SPY'
) AS snapshot,
(
SELECT expiration_date
FROM global_markets.options_greeks
WHERE underlying_symbol = 'SPY'
AND date = snapshot
AND iv_converged = 1
AND volume > 0
AND days_to_expiry BETWEEN 25 AND 45
GROUP BY expiration_date
ORDER BY sum(volume) DESC
LIMIT 1
) AS target_expiry
SELECT
toString(toUInt32(toFloat64(strike_price))) AS strike,
round(avgIf(toFloat64(delta), toFloat64(delta) < 0), 3) AS put_delta,
round(avgIf(toFloat64(delta), toFloat64(delta) > 0), 3) AS call_delta,
formatDateTime(target_expiry, '%b %e, %Y') AS expiry,
formatDateTime(snapshot, '%b %e, %Y') AS as_of
FROM global_markets.options_greeks
WHERE underlying_symbol = 'SPY'
AND date = snapshot
AND expiration_date = target_expiry
AND iv_converged = 1
AND volume > 0
AND toFloat64(option_close) > 0
AND toUInt32(round(toFloat64(strike_price) * 100)) % 500 = 0
AND abs(toFloat64(strike_price) / toFloat64(underlying_close) - 1) < 0.06
GROUP BY strike_price
HAVING countIf(toFloat64(delta) < 0) > 0
AND countIf(toFloat64(delta) > 0) > 0
ORDER BY strike_priceAs of Sep 24, 2026, the Oct 30, 2026 expiration carried 16 strikes on that grid within 6 percent of the underlying price, each with a converged volatility fit and at least one contract traded. At the bottom of the grid, strike 720, the put shows a delta of -0.123 and the call at the same strike shows 0.918. At the top, strike 800, the readings have swapped: -0.821 on the put, 0.143 on the call. The two short legs of a condor live in the shallow tails of that curve, one on each side.
Stage two: assemble the four legs
A condor is two vertical spreads sharing one expiration. The query builds each side by joining the chain to itself: a short leg inside the delta band, and a long leg exactly 5 points further out. Cross joining the put side to the call side produces every four leg combination on the chain, and the credit test does the culling.
| structure | short_deltas | net_credit | max_loss | credit_pct | expiry | as_of |
|---|---|---|---|---|---|---|
| 730/725 put + 795/800 call | -0.16 / 0.2 | 1.69 | 3.31 | 51.1 | Oct 30, 2026 | Sep 24, 2026 |
| 735/730 put + 795/800 call | -0.18 / 0.2 | 1.47 | 3.53 | 41.6 | Oct 30, 2026 | Sep 24, 2026 |
| 720/715 put + 795/800 call | -0.12 / 0.2 | 1.46 | 3.54 | 41.2 | Oct 30, 2026 | Sep 24, 2026 |
| 725/720 put + 795/800 call | -0.14 / 0.2 | 1.33 | 3.67 | 36.2 | Oct 30, 2026 | Sep 24, 2026 |
| 730/725 put + 800/805 call | -0.16 / 0.14 | 1.21 | 3.79 | 31.9 | Oct 30, 2026 | Sep 24, 2026 |
| 735/730 put + 800/805 call | -0.18 / 0.14 | 0.99 | 4.01 | 24.7 | Oct 30, 2026 | Sep 24, 2026 |
| 720/715 put + 800/805 call | -0.12 / 0.14 | 0.98 | 4.02 | 24.4 | Oct 30, 2026 | Sep 24, 2026 |
| 725/720 put + 800/805 call | -0.14 / 0.14 | 0.85 | 4.15 | 20.5 | Oct 30, 2026 | Sep 24, 2026 |
The exact SQL behind every number
WITH
(
SELECT max(date)
FROM global_markets.options_greeks
WHERE underlying_symbol = 'SPY'
) AS snapshot,
(
SELECT expiration_date
FROM global_markets.options_greeks
WHERE underlying_symbol = 'SPY'
AND date = snapshot
AND iv_converged = 1
AND volume > 0
AND days_to_expiry BETWEEN 25 AND 45
GROUP BY expiration_date
ORDER BY sum(volume) DESC
LIMIT 1
) AS target_expiry,
chain AS
(
SELECT
toFloat64(strike_price) AS k,
if(toFloat64(delta) < 0, 'put', 'call') AS side,
avg(toFloat64(option_close)) AS px,
avg(toFloat64(delta)) AS d
FROM global_markets.options_greeks
WHERE underlying_symbol = 'SPY'
AND date = snapshot
AND expiration_date = target_expiry
AND iv_converged = 1
AND volume > 0
AND toFloat64(option_close) > 0
AND toUInt32(round(toFloat64(strike_price) * 100)) % 500 = 0
GROUP BY k, side
),
put_spreads AS
(
SELECT s.k AS short_k, l.k AS long_k, s.d AS short_delta, s.px - l.px AS credit
FROM chain AS s
CROSS JOIN chain AS l
WHERE s.side = 'put' AND l.side = 'put'
AND s.d BETWEEN -0.20 AND -0.12
AND abs(l.k - (s.k - 5)) < 0.01
),
call_spreads AS
(
SELECT s.k AS short_k, l.k AS long_k, s.d AS short_delta, s.px - l.px AS credit
FROM chain AS s
CROSS JOIN chain AS l
WHERE s.side = 'call' AND l.side = 'call'
AND s.d BETWEEN 0.12 AND 0.20
AND abs(l.k - (s.k + 5)) < 0.01
)
SELECT
concat(toString(toUInt32(p.short_k)), '/', toString(toUInt32(p.long_k)), ' put + ',
toString(toUInt32(c.short_k)), '/', toString(toUInt32(c.long_k)), ' call') AS structure,
concat(toString(round(p.short_delta, 2)), ' / ',
toString(round(c.short_delta, 2))) AS short_deltas,
round(p.credit + c.credit, 2) AS net_credit,
round(5 - (p.credit + c.credit), 2) AS max_loss,
round(100 * (p.credit + c.credit) / (5 - (p.credit + c.credit)), 1) AS credit_pct,
formatDateTime(target_expiry, '%b %e, %Y') AS expiry,
formatDateTime(snapshot, '%b %e, %Y') AS as_of
FROM put_spreads AS p
CROSS JOIN call_spreads AS c
WHERE p.credit + c.credit BETWEEN 0.05 AND 4.0
AND 100 * (p.credit + c.credit) / (5 - (p.credit + c.credit)) >= 20
ORDER BY credit_pct DESC
LIMIT 12As of Sep 24, 2026, 8 structures on the Oct 30, 2026 chain reached the panel, which caps the display at twelve. The richest by credit ratio is the 730/725 put + 795/800 call, with short deltas of -0.16 / 0.2, collecting $1.69 per share against a maximum loss of $3.31. That is a ratio of 51.1 percent. The thinnest structure still on the list pays 20.5 percent.
Which knob loosens first
Every screen has one binding constraint. The way to find it is a sweep: hold everything fixed, move one filter, count the survivors. This panel runs the credit floor from 10 to 50 percent twice, once with the tight 0.12 to 0.20 delta band and once with a wider 0.10 to 0.25 band.
| credit_floor | tight_band | wide_band | as_of |
|---|---|---|---|
| 10% | 8 | 28 | Sep 24, 2026 |
| 20% | 8 | 23 | Sep 24, 2026 |
| 30% | 5 | 18 | Sep 24, 2026 |
| 40% | 3 | 11 | Sep 24, 2026 |
| 50% | 1 | 6 | Sep 24, 2026 |
The exact SQL behind every number
WITH
(
SELECT max(date)
FROM global_markets.options_greeks
WHERE underlying_symbol = 'SPY'
) AS snapshot,
(
SELECT expiration_date
FROM global_markets.options_greeks
WHERE underlying_symbol = 'SPY'
AND date = snapshot
AND iv_converged = 1
AND volume > 0
AND days_to_expiry BETWEEN 25 AND 45
GROUP BY expiration_date
ORDER BY sum(volume) DESC
LIMIT 1
) AS target_expiry,
chain AS
(
SELECT
toFloat64(strike_price) AS k,
if(toFloat64(delta) < 0, 'put', 'call') AS side,
avg(toFloat64(option_close)) AS px,
avg(toFloat64(delta)) AS d
FROM global_markets.options_greeks
WHERE underlying_symbol = 'SPY'
AND date = snapshot
AND expiration_date = target_expiry
AND iv_converged = 1
AND volume > 0
AND toFloat64(option_close) > 0
AND toUInt32(round(toFloat64(strike_price) * 100)) % 500 = 0
GROUP BY k, side
),
put_spreads AS
(
SELECT s.d AS short_delta, s.px - l.px AS credit
FROM chain AS s
CROSS JOIN chain AS l
WHERE s.side = 'put' AND l.side = 'put'
AND s.d BETWEEN -0.25 AND -0.10
AND abs(l.k - (s.k - 5)) < 0.01
),
call_spreads AS
(
SELECT s.d AS short_delta, s.px - l.px AS credit
FROM chain AS s
CROSS JOIN chain AS l
WHERE s.side = 'call' AND l.side = 'call'
AND s.d BETWEEN 0.10 AND 0.25
AND abs(l.k - (s.k + 5)) < 0.01
)
SELECT
concat(toString(f.min_ratio), '%') AS credit_floor,
countIf(cand.credit_pct >= f.min_ratio AND cand.tight = 1) AS tight_band,
countIf(cand.credit_pct >= f.min_ratio) AS wide_band,
formatDateTime(snapshot, '%b %e, %Y') AS as_of
FROM
(
SELECT
round(100 * (p.credit + c.credit) / (5 - (p.credit + c.credit)), 1) AS credit_pct,
if((abs(p.short_delta) BETWEEN 0.12 AND 0.20)
AND (c.short_delta BETWEEN 0.12 AND 0.20), 1, 0) AS tight
FROM put_spreads AS p
CROSS JOIN call_spreads AS c
WHERE p.credit + c.credit BETWEEN 0.05 AND 4.0
) AS cand
CROSS JOIN
(
SELECT arrayJoin([10, 20, 30, 40, 50]) AS min_ratio
) AS f
GROUP BY f.min_ratio
ORDER BY f.min_ratioAt the 20% floor, the tight band leaves 8 structures standing and the wider band leaves 23. Raise the floor to 50% and the tight band is down to 1. The credit floor is the knob that loosens first in practice: it is a ratio the query already computes for every candidate, and moving it leaves the set of eligible contracts untouched. Widening the delta band pulls the short strikes closer to the money, which changes the trade rather than the acceptance threshold.
Rank the survivors by IV percentile
Credit ranks structures inside one chain. Comparing across underlyings needs a second axis, and the common one is where an underlying's implied volatility sits inside its own trailing year. IV percentile is the share of the last year's sessions whose at the money implied volatility was at or below the latest reading. IV rank vs IV percentile covers why those are two different statistics. This panel computes the percentile version from the same daily rows the screener already reads.
| symbol | current_iv_pct | iv_percentile | as_of |
|---|---|---|---|
| KO | 20.2 | 70 | Sep 24, 2026 |
| MSFT | 28.2 | 46 | Sep 24, 2026 |
| AAPL | 24.5 | 39 | Sep 24, 2026 |
| QQQ | 19.4 | 36 | Sep 24, 2026 |
| SPY | 13.8 | 29 | Sep 24, 2026 |
| NVDA | 31.5 | 1 | Sep 24, 2026 |
The exact SQL behind every number
WITH
daily AS
(
SELECT
underlying_symbol AS symbol,
date,
round(100 * avg(toFloat64(implied_volatility)), 2) AS iv_pct
FROM global_markets.options_greeks
WHERE underlying_symbol IN ('SPY', 'QQQ', 'AAPL', 'MSFT', 'NVDA', 'KO')
AND date >= today() - 400
AND iv_converged = 1
AND volume > 0
AND days_to_expiry BETWEEN 20 AND 45
AND abs(toFloat64(strike_price) / toFloat64(underlying_close) - 1) < 0.05
GROUP BY symbol, date
),
latest AS
(
SELECT
symbol,
argMax(iv_pct, date) AS current_iv,
max(date) AS snapshot
FROM daily
GROUP BY symbol
)
SELECT
l.symbol AS symbol,
round(l.current_iv, 1) AS current_iv_pct,
round(100 * countIf(d.iv_pct <= l.current_iv) / count(), 0) AS iv_percentile,
formatDateTime(l.snapshot, '%b %e, %Y') AS as_of
FROM latest AS l
INNER JOIN daily AS d ON d.symbol = l.symbol
GROUP BY l.symbol, l.current_iv, l.snapshot
ORDER BY iv_percentile DESCAs of Sep 24, 2026, the highest reading in this set is KO, at the 70th percentile of its own trailing year with at the money implied volatility of 20.2 percent. The lowest is NVDA, at 1 with implied volatility of 31.5 percent. Two underlyings can print nearly the same absolute volatility and sit at opposite ends of their own histories, which is why the ranking is done against a name's own past. Highest IV rank stocks runs the same calculation across a much longer list.
Do last week's candidates still clear a week later?
A snapshot screen has no memory of itself. History in the same daily rows gives it one. This panel takes the last session of each week over roughly three months, runs the full screen on it, then looks up the identical four contracts on the last session of the following week and reapplies the delta band, the traded test and the credit floor. Days to expiry is deliberately not reapplied: a structure with 30 days left at selection has 23 a week later, and retesting that would fail every candidate by construction.
| week | week_of | candidates | still_clearing | survival_pct |
|---|---|---|---|---|
| 2026-07-06 | Jul 6 | 15 | 0 | 0 |
| 2026-07-13 | Jul 13 | 32 | 0 | 0 |
| 2026-07-20 | Jul 20 | 22 | 2 | 9 |
| 2026-07-27 | Jul 27 | 24 | 0 | 0 |
| 2026-08-03 | Aug 3 | 14 | 2 | 14 |
| 2026-08-10 | Aug 10 | 14 | 0 | 0 |
| 2026-08-17 | Aug 17 | 22 | 6 | 27 |
| 2026-08-24 | Aug 24 | 19 | 4 | 21 |
| 2026-08-31 | Aug 31 | 16 | 0 | 0 |
| 2026-09-07 | Sep 7 | 22 | 3 | 14 |
| 2026-09-14 | Sep 14 | 17 | 9 | 53 |
The exact SQL behind every number
WITH
weeks AS
(
SELECT toMonday(date) AS wk, max(date) AS session
FROM global_markets.options_greeks
WHERE underlying_symbol = 'SPY'
AND date >= today() - 84
GROUP BY wk
),
legs AS
(
SELECT
toMonday(g.date) AS wk,
g.expiration_date AS expiration_date,
toFloat64(g.strike_price) AS k,
if(toFloat64(g.delta) < 0, 'put', 'call') AS side,
max(g.days_to_expiry) AS dte,
avg(toFloat64(g.option_close)) AS px,
avg(toFloat64(g.delta)) AS d
FROM global_markets.options_greeks AS g
INNER JOIN weeks AS w ON g.date = w.session
WHERE g.underlying_symbol = 'SPY'
AND g.date >= today() - 84
AND g.iv_converged = 1
AND g.volume > 0
AND toFloat64(g.option_close) > 0
AND toUInt32(round(toFloat64(g.strike_price) * 100)) % 500 = 0
GROUP BY wk, expiration_date, k, side
),
verticals AS
(
SELECT
s.wk AS wk,
s.wk + 7 AS next_wk,
s.expiration_date AS expiration_date,
s.side AS side,
s.k AS short_k,
s.dte AS dte,
s.px - l.px AS credit
FROM legs AS s
INNER JOIN legs AS l
ON s.wk = l.wk AND s.expiration_date = l.expiration_date AND s.side = l.side
WHERE ((s.side = 'put' AND s.d BETWEEN -0.20 AND -0.12 AND abs(l.k - (s.k - 5)) < 0.01)
OR (s.side = 'call' AND s.d BETWEEN 0.12 AND 0.20 AND abs(l.k - (s.k + 5)) < 0.01))
),
condors AS
(
SELECT
p.wk AS wk,
p.next_wk AS next_wk,
p.expiration_date AS expiration_date,
p.short_k AS short_put,
c.short_k AS short_call,
p.dte AS dte,
round(100 * (p.credit + c.credit) / (5 - (p.credit + c.credit)), 1) AS credit_pct
FROM verticals AS p
INNER JOIN verticals AS c
ON p.wk = c.wk AND p.expiration_date = c.expiration_date
WHERE p.side = 'put'
AND c.side = 'call'
AND p.credit + c.credit BETWEEN 0.05 AND 4.0
)
SELECT
toString(a.wk) AS week,
formatDateTime(a.wk, '%b %e') AS week_of,
count() AS candidates,
countIf(b.credit_pct >= 20) AS still_clearing,
round(100 * countIf(b.credit_pct >= 20) / count(), 0) AS survival_pct
FROM condors AS a
LEFT JOIN condors AS b
ON a.next_wk = b.wk
AND a.expiration_date = b.expiration_date
AND a.short_put = b.short_put
AND a.short_call = b.short_call
WHERE a.dte BETWEEN 25 AND 45
AND a.credit_pct >= 20
AND a.next_wk <= toMonday((
SELECT max(date)
FROM global_markets.options_greeks
WHERE underlying_symbol = 'SPY'
))
GROUP BY a.wk
ORDER BY a.wkFor the most recent complete pair of weeks, the week of Sep 14, the screen produced 17 candidates and 9 of them still cleared seven days on, a survival rate of 53 percent. The first cohort in the panel, the week of Jul 6, came in at 0 percent on 15 candidates. A candidate leaves the list when its short deltas drift outside the band or when the credit ratio slips under the floor. Both of those move with the passage of time on the same contracts, which is what a snapshot alone never shows.
When the screen returns nothing
Some snapshots produce an empty list, and the query returns zero rows rather than an error. Two conditions account for most of them: an expiration whose 5 point grid has no strike sitting inside the delta band, and a chain where no four leg combination clears the credit floor. The sweep panel separates the two. If the count at the lowest floor is already near zero, the delta band emptied the list, and the floor is not the thing to move.
FAQ
What delta do iron condor screeners use for the short legs?
Most screens band the short legs somewhere between 0.10 and 0.20 delta, and the choice is a tradeoff the data can price. Widening from 0.12 through 0.20 to 0.10 through 0.25 moved the survivor count at the 20% floor from 8 to 23 on Sep 24, 2026.
How is credit to maximum loss calculated on an iron condor?
With equal wings, maximum loss is the wing width minus the net credit. On Sep 24, 2026 the top ranked structure took in $1.69 against 5 point wings, leaving $3.31 of maximum loss and a ratio of 51.1 percent. Both figures assume a fill at the prices the screen read.
Can you screen for iron condors in SQL?
Yes. Each panel here is a single statement: self join the chain to build the two verticals, cross join the sides, then filter on the delta band and the credit ratio. Open the SQL under any panel to see the whole thing.
Why rank by IV percentile instead of raw implied volatility?
A 14 percent reading can be high for one underlying and low for another. Percentile puts every name on the same 0 to 100 scale against its own trailing year, which makes the ranking comparable across a watchlist.
What data does an iron condor screener need?
Per contract delta, a price for every leg, the strike and expiration of each contract, and enough history to ask the persistence question. Bid and ask are what a live screen fills against; the daily rows here carry a close and a volume instead, so options trade data is where the execution side lives.
Data notes and caveats
- Every panel anchors to the latest session present in the daily greeks rows, printed in the
as_ofcolumn of each result. The front edge of that data runs a day or two behind the live tape. - A leg is kept only when its volatility fit converged and the contract traded that day. That is the proxy for a quotable leg, since these rows carry a daily close and a volume with no bid or ask.
- Credit is computed from closing prices on both legs, so every ratio on this page is a mark rather than a fill.
- The persistence panel reapplies every filter except the days to expiry window, which shrinks by seven days on the same contracts.
- Strikes are limited to multiples of 5, which is what makes symmetric 5 point wings available on both sides of the chain.
Every panel above opens to the exact SQL that produced it, and the same tables are reachable through the free SQL API. Point the screen at another underlying by changing the symbol and the wing width, then run it on the Strasmore terminal.