Strasmore Research
Learn Matt ConnorBy Matt Connor · data as of September 28, 2026 · refreshed weekly

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.

QueryOne chain, one expiration: delta at every 5 point strike
strikeput_deltacall_deltaexpiryas_of
720-0.1230.918Oct 30, 2026Sep 24, 2026
725-0.1370.874Oct 30, 2026Sep 24, 2026
730-0.1620.859Oct 30, 2026Sep 24, 2026
735-0.1830.822Oct 30, 2026Sep 24, 2026
740-0.2120.759Oct 30, 2026Sep 24, 2026
745-0.2490.73Oct 30, 2026Sep 24, 2026
750-0.2880.692Oct 30, 2026Sep 24, 2026
755-0.3320.651Oct 30, 2026Sep 24, 2026
760-0.3840.61Oct 30, 2026Sep 24, 2026
765-0.4410.557Oct 30, 2026Sep 24, 2026
770-0.5040.497Oct 30, 2026Sep 24, 2026
775-0.5760.434Oct 30, 2026Sep 24, 2026
780-0.6390.368Oct 30, 2026Sep 24, 2026
785-0.660.309Oct 30, 2026Sep 24, 2026
790-0.7670.246Oct 30, 2026Sep 24, 2026
800-0.8210.143Oct 30, 2026Sep 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_price
Run this yourself

As 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.

QueryIron condor candidates that cleared every filter
structureshort_deltasnet_creditmax_losscredit_pctexpiryas_of
730/725 put + 795/800 call-0.16 / 0.21.693.3151.1Oct 30, 2026Sep 24, 2026
735/730 put + 795/800 call-0.18 / 0.21.473.5341.6Oct 30, 2026Sep 24, 2026
720/715 put + 795/800 call-0.12 / 0.21.463.5441.2Oct 30, 2026Sep 24, 2026
725/720 put + 795/800 call-0.14 / 0.21.333.6736.2Oct 30, 2026Sep 24, 2026
730/725 put + 800/805 call-0.16 / 0.141.213.7931.9Oct 30, 2026Sep 24, 2026
735/730 put + 800/805 call-0.18 / 0.140.994.0124.7Oct 30, 2026Sep 24, 2026
720/715 put + 800/805 call-0.12 / 0.140.984.0224.4Oct 30, 2026Sep 24, 2026
725/720 put + 800/805 call-0.14 / 0.140.854.1520.5Oct 30, 2026Sep 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 12
Run this yourself

As 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.

QuerySurvivors as the credit floor and the delta band move
credit_floortight_bandwide_bandas_of
10%828Sep 24, 2026
20%823Sep 24, 2026
30%518Sep 24, 2026
40%311Sep 24, 2026
50%16Sep 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_ratio
Run this yourself

At 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.

QueryWhere each underlying's implied volatility sits in its own year
symbolcurrent_iv_pctiv_percentileas_of
KO20.270Sep 24, 2026
MSFT28.246Sep 24, 2026
AAPL24.539Sep 24, 2026
QQQ19.436Sep 24, 2026
SPY13.829Sep 24, 2026
NVDA31.51Sep 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 DESC
Run this yourself

As 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.

QueryHow many of each week's candidates still cleared a week later
weekweek_ofcandidatesstill_clearingsurvival_pct
2026-07-06Jul 61500
2026-07-13Jul 133200
2026-07-20Jul 202229
2026-07-27Jul 272400
2026-08-03Aug 314214
2026-08-10Aug 101400
2026-08-17Aug 1722627
2026-08-24Aug 2419421
2026-08-31Aug 311600
2026-09-07Sep 722314
2026-09-14Sep 1417953
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.wk
Run this yourself

For 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_of column 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.

#iron condor#options screener#sql api#implied volatility#options data