HomeBlogEarnings Options Plays, Scientifically: Measuring Implied Move, IV Crush, and Execution Quality with CuteMarkets
Deep DiveApril 18, 2026·14 min read

Earnings Options Plays, Scientifically: Measuring Implied Move, IV Crush, and Execution Quality with CuteMarkets

Daniel Ratke

Daniel Ratke

Research & Engineering

Earnings Options Plays, Scientifically: Measuring Implied Move, IV Crush, and Execution Quality with CuteMarkets

Term map

Options-data vocabulary for this article

Read chains, contracts, quote freshness, trade tape context, Greeks, implied volatility, open interest, and entitlement gates as separate data objects. That vocabulary keeps an options-data workflow precise when it moves from docs to scanners, dashboards, and historical research.

Follow the linked definitions for Option chain snapshot, Contract snapshot, Volume/OI pressure, Options flow false positive, Scanner artifact, Historical REST window, Backfill, DTE bucket, Moneyness band, Quote condition, Trade condition, and IV skew.

Read this article with Options Data API, Options Chain API, Historical Options Data API, Options Volume and Open Interest, Options Flow False Positives, and Option Quote and Trade Conditions.

Abstract

Earnings announcements are discrete volatility events. The underlying reprices on new information, implied volatility typically collapses immediately after the release, and execution quality often deteriorates exactly when retail traders become most active. This combination makes earnings options trades attractive and dangerous at the same time.

This post presents a research-style framework for studying and trading options around earnings with the CuteMarkets API. The emphasis is on measurable quantities:

  • the market-implied move from the at-the-money straddle
  • the relationship between directional conviction and option structure selection
  • the role of term structure in calendars and diagonals
  • the importance of quote width and trade flow near entry
  • the difference between a mathematically valid options play and an executable one

The examples are intentionally practical. Every code snippet uses documented CuteMarkets endpoints, and every strategy is written so it can be screened, audited, or backtested.

1. Research Question

The central question is not "what is the best earnings trade?"

The better scientific question is:

Given an earnings date, a risk budget, and an options chain, which structure best expresses a view on direction, realized move, and volatility crush?

That question naturally decomposes into four sub-problems:

  1. Estimate the market-implied move.
  2. Measure chain quality and liquidity.
  3. Map a thesis to a structure.
  4. Evaluate post-event payoff with realistic pricing inputs.

2. Scope and Assumptions

This article is deliberately precise about scope.

  • CuteMarkets provides the options layer: chains, contract metadata, snapshots, trades, quotes, and aggregates.
  • The earnings calendar itself is assumed to come from your own event source.
  • All premiums are per share. Multiply by 100 for per-contract dollar values.
  • According to CuteMarkets' plan documentation, Free, Base, and Developer plans are delayed, while Expert is live and includes the quotes endpoint. That matters for any live earnings execution workflow.
  • The historical examples below use documented options endpoints only. If you want a fully self-contained event study, you still need an external source for earnings timestamps and, in many cases, underlying spot history.

3. Endpoint Map

The core endpoints used in this post are:

PurposeEndpoint
AuthenticationAuthorization: Bearer YOUR_API_KEY
Expirations for an underlyingGET /v1/tickers/expirations/{ticker}
Current chain snapshotGET /v1/options/chain/{ticker}
Single-contract snapshotGET /v1/options/snapshot/{underlying}/{option_contract}
Historical contract discoveryGET /v1/options/contracts with as_of
Historical tradesGET /v1/options/trades/{options_ticker}
Historical quotesGET /v1/options/quotes/{options_ticker}
Historical OHLC barsGET /v1/options/aggs/{ticker}/{multiplier}/{timespan}/{from_date}/{to_date}
Single-day open/closeGET /v1/options/open-close/{ticker}/{date}

Official references:

4. Experimental Setup

We start with a thin Python client. The goal is not abstraction for its own sake. The goal is reproducibility.

from __future__ import annotations

from dataclasses import dataclass
from datetime import date, timedelta
from typing import Any
from urllib.parse import quote

import requests

BASE_URL = "https://api.cutemarkets.com/v1"

class CuteMarketsError(RuntimeError):
    pass

@dataclass
class CuteMarketsClient:
    api_key: str
    timeout: int = 30

    @property
    def headers(self) -> dict[str, str]:
        return {"Authorization": f"Bearer {self.api_key}"}

    def get(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
        url = f"{BASE_URL}{path}"
        response = requests.get(url, headers=self.headers, params=params, timeout=self.timeout)
        payload = response.json()

        if response.status_code >= 400 or payload.get("status") == "ERROR":
            error = payload.get("error", {})
            raise CuteMarketsError(
                f"{response.status_code} {error.get('code', 'unknown_error')}: "
                f"{error.get('message', 'Unknown error')}"
            )
        return payload

cm = CuteMarketsClient(api_key="YOUR_API_KEY")

Two convenience helpers are useful immediately.

def get_expirations(cm: CuteMarketsClient, ticker: str) -> list[date]:
    payload = cm.get(f"/tickers/expirations/{ticker}/")
    return [date.fromisoformat(x) for x in payload["results"]]

def get_chain(
    cm: CuteMarketsClient,
    ticker: str,
    *,
    expiration_date: str | None = None,
    contract_type: str | None = None,
    strike_gte: float | None = None,
    strike_lte: float | None = None,
    limit: int = 100,
) -> list[dict[str, Any]]:
    params: dict[str, Any] = {"limit": limit}
    if expiration_date is not None:
        params["expiration_date"] = expiration_date
    if contract_type is not None:
        params["contract_type"] = contract_type
    if strike_gte is not None:
        params["strike_price.gte"] = strike_gte
    if strike_lte is not None:
        params["strike_price.lte"] = strike_lte

    payload = cm.get(f"/options/chain/{ticker}/", params=params)
    return payload["results"]

5. Step One in Any Earnings Trade: Estimate the Implied Move

The cleanest first approximation is the at-the-money straddle:

expected_move ~= ATM_call_mid + ATM_put_mid

If spot is S, then the relative implied move is:

expected_move_pct = expected_move / S

That one number is the anchor for almost every earnings options play.

5.1 Choosing the correct expiry

If the company reports after the close, the relevant expiry must survive the overnight information shock. For a weekly setup, that usually means the first expiry on or after earnings_date + 1 day.

def nearest_post_earnings_expiry(
    cm: CuteMarketsClient,
    ticker: str,
    earnings_day: date,
    *,
    after_close: bool = True,
) -> date:
    expiries = get_expirations(cm, ticker)
    minimum = earnings_day + timedelta(days=1 if after_close else 0)
    candidates = [expiry for expiry in expiries if expiry >= minimum]
    if not candidates:
        raise ValueError(f"No valid expiry found for {ticker} after {minimum.isoformat()}")
    return min(candidates)

5.2 Computing the straddle implied move

The chain snapshot includes contract details, Greeks, implied volatility, and latest quote or trade context when your plan supports it. That is enough to build an ATM straddle estimator.

def option_mid(contract: dict[str, Any]) -> float:
    quote = contract.get("last_quote") or {}
    if "midpoint" in quote and quote["midpoint"] is not None:
        return float(quote["midpoint"])

    bid = quote.get("bid")
    ask = quote.get("ask")
    if bid is not None and ask is not None:
        return (float(bid) + float(ask)) / 2.0

    day = contract.get("day") or {}
    if day.get("close") is not None:
        return float(day["close"])

    raise ValueError("Contract has neither midpoint nor daily close")

def pair_by_strike(chain: list[dict[str, Any]]) -> dict[float, dict[str, dict[str, Any]]]:
    out: dict[float, dict[str, dict[str, Any]]] = {}
    for row in chain:
        details = row["details"]
        strike = float(details["strike_price"])
        kind = details["contract_type"]
        out.setdefault(strike, {})[kind] = row
    return out

def implied_move_from_chain(cm: CuteMarketsClient, ticker: str, earnings_day: date) -> dict[str, Any]:
    expiry = nearest_post_earnings_expiry(cm, ticker, earnings_day, after_close=True)
    chain = get_chain(cm, ticker, expiration_date=expiry.isoformat(), limit=100)

    if not chain:
        raise ValueError(f"Empty chain for {ticker} {expiry.isoformat()}")

    spot = float(chain[0]["underlying_asset"]["price"])
    by_strike = pair_by_strike(chain)

    paired = [
        (strike, legs["call"], legs["put"])
        for strike, legs in by_strike.items()
        if "call" in legs and "put" in legs
    ]
    if not paired:
        raise ValueError("No call/put pairs found")

    atm_strike, call_row, put_row = min(paired, key=lambda row: abs(row[0] - spot))
    call_mid = option_mid(call_row)
    put_mid = option_mid(put_row)
    implied_move = call_mid + put_mid

    return {
        "ticker": ticker,
        "spot": spot,
        "expiry": expiry.isoformat(),
        "atm_strike": atm_strike,
        "call_mid": call_mid,
        "put_mid": put_mid,
        "implied_move_abs": implied_move,
        "implied_move_pct": implied_move / spot,
        "call_iv": call_row.get("implied_volatility"),
        "put_iv": put_row.get("implied_volatility"),
        "call_delta": call_row.get("greeks", {}).get("delta"),
        "put_delta": put_row.get("greeks", {}).get("delta"),
    }

Example usage:

earnings_day = date(2026, 5, 7)  # example input from your own calendar
summary = implied_move_from_chain(cm, "AAPL", earnings_day)
print(summary)

Interpretation:

  • implied_move_abs is the market's approximate dollar move.
  • implied_move_pct is the same number scaled by spot.
  • if your independent forecast of realized move is smaller than the implied move, long premium is harder to justify.
  • if your independent forecast is larger, long gamma may be reasonable.

6. Example Play 1: The Long Straddle

The long straddle is the cleanest expression of the view:

"I do not know direction, but I believe realized move will exceed the market-implied move."

This is a pure long-vol expression. It is also the most vulnerable to pre-earnings overpricing.

6.1 Screening logic

A straddle is rarely attractive if:

  • the combined premium is already larger than your forecast move
  • the bid/ask spread is wide relative to midpoint
  • the event is too far away and theta bleed dominates
def score_long_straddle(
    cm: CuteMarketsClient,
    ticker: str,
    earnings_day: date,
    model_move_pct: float,
) -> dict[str, Any]:
    data = implied_move_from_chain(cm, ticker, earnings_day)
    edge_pct = model_move_pct - data["implied_move_pct"]

    upper_breakeven = data["atm_strike"] + data["implied_move_abs"]
    lower_breakeven = data["atm_strike"] - data["implied_move_abs"]

    return {
        **data,
        "model_move_pct": model_move_pct,
        "edge_pct": edge_pct,
        "upper_breakeven": upper_breakeven,
        "lower_breakeven": lower_breakeven,
        "verdict": "candidate" if edge_pct > 0 else "reject",
    }

Example:

candidate = score_long_straddle(
    cm,
    ticker="NFLX",
    earnings_day=date(2026, 7, 16),
    model_move_pct=0.085,  # 8.5% forecast from your own event model
)
print(candidate["verdict"], candidate["edge_pct"])

6.2 Scientific interpretation

A straddle is a hypothesis test:

  • null hypothesis: realized move will be less than or equal to implied move
  • alternative hypothesis: realized move will exceed implied move

The straddle only makes sense when you reject the null with real confidence, not when you merely "feel" the stock will move.

7. Example Play 2: A Directional Debit Spread

A debit spread is often a better earnings trade than a naked call or naked put because it offsets some pre-event volatility premium.

Bullish thesis:

  • buy a call with meaningful delta
  • sell a higher strike near or slightly beyond the implied move boundary

Bearish thesis:

  • buy a put with meaningful absolute delta
  • sell a lower strike near or slightly beyond the implied move boundary

7.1 Why debit spreads work well around earnings

Before earnings, the main problem with outright long options is more than direction risk. It is paying elevated implied volatility. A spread sells some of that volatility back to the market.

7.2 Building a bullish call spread

def select_call_spread(
    cm: CuteMarketsClient,
    ticker: str,
    earnings_day: date,
    *,
    long_delta_min: float = 0.25,
    long_delta_max: float = 0.50,
) -> dict[str, Any]:
    move = implied_move_from_chain(cm, ticker, earnings_day)
    expiry = move["expiry"]
    spot = move["spot"]
    target_short_strike = spot + move["implied_move_abs"]

    calls = get_chain(cm, ticker, expiration_date=expiry, contract_type="call", limit=100)

    liquid_calls = [
        c for c in calls
        if c.get("last_quote") or c.get("day")
    ]

    long_call = min(
        [
            c for c in liquid_calls
            if long_delta_min <= float(c.get("greeks", {}).get("delta") or 0.0) <= long_delta_max
        ],
        key=lambda c: abs(float(c["details"]["strike_price"]) - spot),
    )

    short_call = min(
        [
            c for c in liquid_calls
            if float(c["details"]["strike_price"]) > float(long_call["details"]["strike_price"])
        ],
        key=lambda c: abs(float(c["details"]["strike_price"]) - target_short_strike),
    )

    long_mid = option_mid(long_call)
    short_mid = option_mid(short_call)
    width = float(short_call["details"]["strike_price"]) - float(long_call["details"]["strike_price"])
    debit = long_mid - short_mid

    return {
        "ticker": ticker,
        "expiry": expiry,
        "spot": spot,
        "long_call": long_call["details"]["ticker"],
        "short_call": short_call["details"]["ticker"],
        "long_strike": float(long_call["details"]["strike_price"]),
        "short_strike": float(short_call["details"]["strike_price"]),
        "debit": debit,
        "max_profit": width - debit,
        "max_loss": debit,
        "breakeven": float(long_call["details"]["strike_price"]) + debit,
    }

Example:

spread = select_call_spread(cm, "MSFT", date(2026, 7, 29))
for k, v in spread.items():
    print(f"{k}: {v}")

7.3 When the call spread is superior to the straddle

Use the call spread when:

  • you have directional conviction
  • you do not need an outsized two-sided move
  • the implied move is expensive
  • the short strike can be placed near the upper implied-move boundary

The same logic applies symmetrically to bearish put spreads.

8. Example Play 3: A Short Iron Condor

The short iron condor expresses the view:

"The implied move is overpriced, and realized move will remain inside the market's expected range."

Scientifically, this is a short-volatility hypothesis with bounded tail risk.

8.1 Constructing the condor around the implied move

def nearest_strike(rows: list[dict[str, Any]], target: float) -> dict[str, Any]:
    return min(rows, key=lambda c: abs(float(c["details"]["strike_price"]) - target))

def build_iron_condor(
    cm: CuteMarketsClient,
    ticker: str,
    earnings_day: date,
    *,
    wing_width: float,
) -> dict[str, Any]:
    move = implied_move_from_chain(cm, ticker, earnings_day)
    expiry = move["expiry"]
    spot = move["spot"]
    implied_abs = move["implied_move_abs"]

    chain = get_chain(cm, ticker, expiration_date=expiry, limit=100)
    calls = [c for c in chain if c["details"]["contract_type"] == "call"]
    puts = [p for p in chain if p["details"]["contract_type"] == "put"]

    short_call = nearest_strike(calls, spot + implied_abs)
    short_put = nearest_strike(puts, spot - implied_abs)
    long_call = nearest_strike(calls, float(short_call["details"]["strike_price"]) + wing_width)
    long_put = nearest_strike(puts, float(short_put["details"]["strike_price"]) - wing_width)

    credit = (
        option_mid(short_call)
        + option_mid(short_put)
        - option_mid(long_call)
        - option_mid(long_put)
    )

    call_width = float(long_call["details"]["strike_price"]) - float(short_call["details"]["strike_price"])
    put_width = float(short_put["details"]["strike_price"]) - float(long_put["details"]["strike_price"])
    width = max(call_width, put_width)
    max_loss = width - credit

    return {
        "ticker": ticker,
        "expiry": expiry,
        "spot": spot,
        "short_call": short_call["details"]["ticker"],
        "short_put": short_put["details"]["ticker"],
        "long_call": long_call["details"]["ticker"],
        "long_put": long_put["details"]["ticker"],
        "short_call_strike": float(short_call["details"]["strike_price"]),
        "short_put_strike": float(short_put["details"]["strike_price"]),
        "long_call_strike": float(long_call["details"]["strike_price"]),
        "long_put_strike": float(long_put["details"]["strike_price"]),
        "credit": credit,
        "max_loss": max_loss,
        "upper_breakeven": float(short_call["details"]["strike_price"]) + credit,
        "lower_breakeven": float(short_put["details"]["strike_price"]) - credit,
    }

Example:

condor = build_iron_condor(
    cm,
    ticker="AMZN",
    earnings_day=date(2026, 8, 1),
    wing_width=5.0,
)
print(condor)

8.2 Guardrails

A condor should be rejected if:

  • entry credit is trivial relative to maximum loss
  • short strikes are inside the implied move rather than outside it
  • quote widths are too wide to trust midpoint pricing
  • volume and open interest are thin

That last point is critical. A "winning" backtest based on stale midpoint assumptions can be a losing live strategy.

9. Example Play 4: A Calendar Spread for Event IV

Calendars are structurally interesting around earnings because the front expiry often contains the event premium more directly than the back expiry.

Research hypothesis:

Short the most event-loaded expiry, buy the less event-loaded back-month expiry at the same strike.

This is not a pure direction trade. It is a view on relative term structure and post-event volatility normalization.

def build_call_calendar(
    cm: CuteMarketsClient,
    ticker: str,
    earnings_day: date,
) -> dict[str, Any]:
    front_expiry = nearest_post_earnings_expiry(cm, ticker, earnings_day, after_close=True)
    expiries = get_expirations(cm, ticker)
    later_expiries = [x for x in expiries if x > front_expiry]
    if not later_expiries:
        raise ValueError("No back expiry available")
    back_expiry = later_expiries[0]

    front_calls = get_chain(cm, ticker, expiration_date=front_expiry.isoformat(), contract_type="call", limit=100)
    back_calls = get_chain(cm, ticker, expiration_date=back_expiry.isoformat(), contract_type="call", limit=100)

    if not front_calls or not back_calls:
        raise ValueError("Missing chain on one side of the calendar")

    spot = float(front_calls[0]["underlying_asset"]["price"])
    front_atm = min(front_calls, key=lambda c: abs(float(c["details"]["strike_price"]) - spot))
    strike = float(front_atm["details"]["strike_price"])

    back_same_strike = min(
        back_calls,
        key=lambda c: abs(float(c["details"]["strike_price"]) - strike),
    )

    debit = option_mid(back_same_strike) - option_mid(front_atm)
    front_iv = float(front_atm.get("implied_volatility") or 0.0)
    back_iv = float(back_same_strike.get("implied_volatility") or 0.0)

    return {
        "ticker": ticker,
        "spot": spot,
        "strike": strike,
        "short_front_call": front_atm["details"]["ticker"],
        "long_back_call": back_same_strike["details"]["ticker"],
        "front_expiry": front_expiry.isoformat(),
        "back_expiry": back_expiry.isoformat(),
        "front_iv": front_iv,
        "back_iv": back_iv,
        "iv_slope": front_iv - back_iv,
        "net_debit": debit,
    }

Why this matters:

  • if front-month IV is dramatically richer than back-month IV, the event premium is concentrated
  • if the slope is flat, the calendar edge is weaker
  • if spot gaps violently through the strike, the calendar becomes a direction trade whether you intended it or not

10. Example Play 5: Quote Quality as an Entry Filter

CuteMarkets' chain endpoint can include latest quote and trade context, but the dedicated quotes endpoint is the cleaner tool for liquidity analysis. The official docs state that /v1/options/quotes/{options_ticker} requires the Expert plan.

For live earnings trading, this distinction is not minor. It is fundamental.

10.1 Measuring average spread before entry

def get_quotes(
    cm: CuteMarketsClient,
    option_ticker: str,
    *,
    ts_gte: str,
    ts_lte: str,
    limit: int = 500,
) -> list[dict[str, Any]]:
    encoded = quote(option_ticker, safe="")
    payload = cm.get(
        f"/options/quotes/{encoded}/",
        params={
            "timestamp.gte": ts_gte,
            "timestamp.lte": ts_lte,
            "limit": limit,
            "sort": "timestamp",
            "order": "asc",
        },
    )
    return payload["results"]

def quote_spread_stats(quotes: list[dict[str, Any]]) -> dict[str, float]:
    spreads = []
    rel_spreads = []
    for q in quotes:
        bid = float(q["bid_price"])
        ask = float(q["ask_price"])
        if ask <= 0 or ask < bid:
            continue
        mid = (bid + ask) / 2.0
        spread = ask - bid
        spreads.append(spread)
        rel_spreads.append(spread / mid if mid > 0 else 0.0)

    if not spreads:
        return {"mean_spread": float("nan"), "mean_rel_spread": float("nan")}

    return {
        "mean_spread": sum(spreads) / len(spreads),
        "mean_rel_spread": sum(rel_spreads) / len(rel_spreads),
    }

Example:

stats = quote_spread_stats(
    get_quotes(
        cm,
        "O:NFLX260402C00075000",
        ts_gte="2026-03-10",
        ts_lte="2026-03-10",
        limit=1000,
    )
)
print(stats)

A simple live-trading rule:

  • reject entries if mean relative spread exceeds 5% to 10% of midpoint
  • reject entries if spread instability spikes in the final minutes before the release

11. Example Play 6: Trade Tape Diagnostics

The historical trades endpoint is useful for a different question:

Are prints actually occurring near the quoted mid, or is the market becoming one-sided?

def get_trades(
    cm: CuteMarketsClient,
    option_ticker: str,
    *,
    ts_gte: str,
    ts_lte: str,
    limit: int = 500,
) -> list[dict[str, Any]]:
    encoded = quote(option_ticker, safe="")
    payload = cm.get(
        f"/options/trades/{encoded}/",
        params={
            "timestamp.gte": ts_gte,
            "timestamp.lte": ts_lte,
            "limit": limit,
            "sort": "timestamp",
            "order": "asc",
        },
    )
    return payload["results"]

def trade_vwap(trades: list[dict[str, Any]]) -> float:
    numerator = 0.0
    denominator = 0.0
    for trade in trades:
        price = float(trade["price"])
        size = float(trade["size"])
        numerator += price * size
        denominator += size
    return numerator / denominator if denominator else float("nan")

Example:

trades = get_trades(
    cm,
    "O:NFLX260402C00075000",
    ts_gte="2026-03-10",
    ts_lte="2026-03-10",
)
print("VWAP:", trade_vwap(trades))
print("Trade count:", len(trades))

This is useful for earnings because low quote width is not enough. You want:

  • narrow spreads
  • enough trade frequency
  • no evidence that fills are clustering only at the offer for longs or only at the bid for shorts

12. Example Play 7: Historical Event-Window Bars for One Contract

For post-trade evaluation, the aggregates endpoints are a clean way to reconstruct option behavior before and after earnings.

def get_option_bars(
    cm: CuteMarketsClient,
    option_ticker: str,
    *,
    start_day: str,
    end_day: str,
    multiplier: int = 1,
    timespan: str = "day",
    limit: int = 5000,
) -> list[dict[str, Any]]:
    encoded = quote(option_ticker, safe="")
    path = f"/options/aggs/{encoded}/{multiplier}/{timespan}/{start_day}/{end_day}/"
    payload = cm.get(path, params={"limit": limit, "sort": "asc"})
    return payload["results"]

Example:

bars = get_option_bars(
    cm,
    "O:NFLX260402C00075000",
    start_day="2026-03-01",
    end_day="2026-03-20",
)
print(bars[:3])

Two scientifically important notes from the CuteMarkets aggregates docs:

  • bars are built from qualifying trades
  • intervals with no qualifying trades are omitted

So missing bars mean "no qualifying trades," not "zero return."

13. Example Play 8: A Historical Debit-Spread Event Study

This is where event research becomes real.

CuteMarkets' contracts endpoint supports as_of, which is essential when you want the contract universe as it existed before a historical earnings event.

Below is a minimal event-study skeleton:

  • input: external table with ticker, earnings_day, underlying_close
  • use CuteMarkets to discover historical contracts as of earnings_day
  • pick strikes relative to the underlying close
  • use CuteMarkets daily open/close or aggregate bars to price the spread
def list_contracts_as_of(
    cm: CuteMarketsClient,
    ticker: str,
    *,
    as_of: str,
    expiration_date: str,
    contract_type: str,
) -> list[dict[str, Any]]:
    payload = cm.get(
        "/options/contracts/",
        params={
            "underlying_ticker": ticker,
            "as_of": as_of,
            "expiration_date": expiration_date,
            "contract_type": contract_type,
            "limit": 1000,
        },
    )
    return payload["results"]

def option_open_close(cm: CuteMarketsClient, option_ticker: str, day: str) -> dict[str, Any]:
    encoded = quote(option_ticker, safe="")
    return cm.get(f"/options/open-close/{encoded}/{day}/")

def choose_nearest_contract(contracts: list[dict[str, Any]], target_strike: float) -> dict[str, Any]:
    return min(contracts, key=lambda c: abs(float(c["strike_price"]) - target_strike))

Now the event-study loop:

def historical_bull_call_spread_return(
    cm: CuteMarketsClient,
    *,
    ticker: str,
    earnings_day: str,
    expiry_day: str,
    underlying_close: float,
    hold_to_day: str,
    long_offset_pct: float = 0.00,
    short_offset_pct: float = 0.05,
) -> dict[str, Any]:
    calls = list_contracts_as_of(
        cm,
        ticker,
        as_of=earnings_day,
        expiration_date=expiry_day,
        contract_type="call",
    )
    if not calls:
        raise ValueError("No calls found")

    long_target = underlying_close * (1.0 + long_offset_pct)
    short_target = underlying_close * (1.0 + short_offset_pct)

    long_call = choose_nearest_contract(calls, long_target)
    short_call = choose_nearest_contract(calls, short_target)

    long_entry = option_open_close(cm, long_call["ticker"], earnings_day)["close"]
    short_entry = option_open_close(cm, short_call["ticker"], earnings_day)["close"]
    long_exit = option_open_close(cm, long_call["ticker"], hold_to_day)["close"]
    short_exit = option_open_close(cm, short_call["ticker"], hold_to_day)["close"]

    entry_debit = float(long_entry) - float(short_entry)
    exit_value = float(long_exit) - float(short_exit)

    return {
        "ticker": ticker,
        "earnings_day": earnings_day,
        "expiry_day": expiry_day,
        "long_call": long_call["ticker"],
        "short_call": short_call["ticker"],
        "entry_debit": entry_debit,
        "exit_value": exit_value,
        "pnl_per_contract": (exit_value - entry_debit) * 100.0,
    }

This is not a full institutional backtester, but it is already scientifically useful:

  • contracts are historically correct via as_of
  • option prices come from documented historical endpoints
  • the earnings timestamp remains an explicit external input instead of a hidden assumption

14. Example Play 9: Batch Screening a Watchlist

A realistic workflow scans many names before deciding which earnings event deserves capital.

def screen_watchlist(
    cm: CuteMarketsClient,
    tickers: list[str],
    earnings_days: dict[str, date],
    model_moves: dict[str, float],
) -> list[dict[str, Any]]:
    rows = []
    for ticker in tickers:
        try:
            row = score_long_straddle(
                cm,
                ticker=ticker,
                earnings_day=earnings_days[ticker],
                model_move_pct=model_moves[ticker],
            )
            rows.append(row)
        except Exception as exc:
            rows.append(
                {
                    "ticker": ticker,
                    "error": str(exc),
                }
            )
    return rows

Example:

watchlist = ["AAPL", "AMZN", "GOOGL", "META", "MSFT", "NFLX", "NVDA"]
earnings_days = {
    "AAPL": date(2026, 7, 30),
    "AMZN": date(2026, 8, 1),
    "GOOGL": date(2026, 7, 23),
    "META": date(2026, 7, 31),
    "MSFT": date(2026, 7, 29),
    "NFLX": date(2026, 7, 16),
    "NVDA": date(2026, 8, 20),
}
model_moves = {
    "AAPL": 0.045,
    "AMZN": 0.072,
    "GOOGL": 0.065,
    "META": 0.083,
    "MSFT": 0.052,
    "NFLX": 0.095,
    "NVDA": 0.102,
}

rows = screen_watchlist(cm, watchlist, earnings_days, model_moves)
for row in rows:
    print(row)

This gives you a first-pass ranking:

  • names where implied move is already too rich
  • names where your forecast still exceeds implied
  • names where the data is tradable versus merely interesting

15. A Scientific Taxonomy of Earnings Plays

The strategies above can be organized by hypothesis.

StrategyPrimary hypothesisBest whenMain failure mode
Long straddleRealized move > implied moveVery uncertain direction, high event convexityIV overpayment
Debit spreadDirection is right, but IV is expensiveDirectional conviction with bounded riskMove is too small
Iron condorRealized move < implied movePremium inflated, move likely containedGap through short strikes
CalendarFront event IV is richer than back IVEvent premium concentrated in front expiryLarge spot gap
DiagonalTerm-structure view plus mild directionYou want longer gamma, shorter event premiumWrong direction plus IV normalization

This table is practical. It tells you what variable to test:

  • straddle: compare forecast realized move vs implied move
  • debit spread: compare directional probability vs premium paid
  • condor: compare realized move distribution vs short strikes
  • calendar: compare front-back term structure

16. Threats to Validity

Any serious earnings study should explicitly state its limitations.

16.1 Delayed versus live data

CuteMarkets documents that Free, Base, and Developer plans are delayed, and that Expert provides live data and quotes. If you are executing around the release itself, delayed data is not a small caveat. It changes the experiment.

16.2 Midpoint optimism

Midpoint fills are usually too optimistic in stressed earnings conditions. Use quotes and trades to estimate realistic slippage.

16.3 Missing earnings clock precision

"Earnings date" is not enough. You need to know whether the report is before open or after close. The chosen expiry and hold period depend on that.

16.4 Sparse option trading

CuteMarkets' aggregates documentation is clear that intervals without qualifying trades are omitted. Illiquid contracts can therefore look smoother than they really are if you do not account for missing prints.

16.5 External event data

CuteMarkets supplies the options market data. Your event timestamps, earnings calendar, and any fundamental forecast model must come from outside the options API.

17. Practical Playbook

If you wanted a disciplined production workflow, it would look like this:

  1. Ingest earnings dates from your event calendar.
  2. Use GET /v1/tickers/expirations/{ticker} to find valid post-event expiries.
  3. Use GET /v1/options/chain/{ticker} to estimate implied move and inspect Greeks.
  4. Reject names with poor liquidity or weak term structure.
  5. Map thesis to structure:
    • uncertain direction and large forecast move: straddle or strangle
    • directional thesis and rich IV: debit spread
    • overpriced implied move and liquid market: iron condor
    • concentrated front-month event premium: calendar
  6. Use quotes and trades to validate execution quality before entry.
  7. Use aggregates and open/close data for post-event evaluation and backtests.

18. Conclusion

The most important lesson in earnings options trading is that structure selection should follow measurement, not intuition.

With CuteMarkets, the measurement layer is strong:

  • expirations for event-aware expiry selection
  • chain snapshots for implied move, Greeks, and IV
  • contract metadata with as_of for historical reconstruction
  • trades and quotes for execution-quality analysis
  • aggregate bars for backtests and post-event review

That is enough to build a serious earnings research stack.

What it does not do automatically is give you a thesis. You still need an earnings calendar, a forecast model, and strict risk rules. But once those exist, the API surface is sufficient for screening, structuring, and evaluating the core earnings play families.

References

For the Earnings Options Plays, Scientifically: Measuring Implied Move, IV Crush, and Execution Quality with CuteMarkets workflow, continue through Options Data API, Options Chain API, Historical Options Data API, Options Volume and Open Interest, Options Flow False Positives, and Option Quote and Trade Conditions.

How the terminology applies

For Earnings Options Plays, Scientifically: Measuring Implied Move, IV Crush, and Execution Quality with CuteMarkets, the options data workflow should treat Option chain snapshot, Contract snapshot, Volume/OI pressure, Options flow false positive, Scanner artifact, and Historical REST window as operational state rather than glossary decoration. That framing keeps chain selection, contract snapshots, activity filters, quote state, and endpoint access tied to the exact contract the page is discussing.

A developer implementing this Deep Dive idea should persist Backfill, DTE bucket, Moneyness band, Quote condition, Trade condition, and IV skew beside the result, instead of leaving those words in a term card. It also makes false positives easier to diagnose because a high-activity contract can be separated from a tradable, timestamped, and entitled data object.

The review artifact for Earnings Options Plays, Scientifically: Measuring Implied Move, IV Crush, and Execution Quality with CuteMarkets becomes more useful when 0DTE contract, OCC root, Options data API, OPRA-originating data, OCC option symbol, and Bid/ask spread appear in the same body of evidence as the selected rows. When the article moves from concept to implementation, these fields should shape request order, cache boundaries, row labels, and review tables.

In production notes for this options data workflow, Midpoint, Quote/trade condition, Quote vs trade semantics, REST snapshot, WebSocket stream, and Entitlement gate define the checks that decide whether the workflow is reproducible. The result is a scanner or dashboard that explains why a contract was shown, skipped, refreshed, or passed into a downstream research step.

For Earnings Options Plays, Scientifically: Measuring Implied Move, IV Crush, and Execution Quality with CuteMarkets, the practical acceptance test is simple: another developer should be able to read the body, identify the exact inputs, reproduce the request sequence, and explain the accepted and rejected rows without relying on the bottom terminology grid. If a phrase appears in the page vocabulary, it should correspond to a stored field, a validation check, a replay step, or an implementation decision in the options data workflow.

This is also the reason the article should not measure success only by the final chart, table, or headline metric. The better standard is whether the data path, timing model, entitlement state, and evidence trail survive review. When those pieces are written directly into the body, the terminology becomes part of the workflow readers can implement.

Terminology

Market-data terms used in this article

These terms keep the article connected to the CuteMarkets knowledge base and to the exact API workflow behind the research.

Option chain snapshot

The current breadth view for an underlying across expirations, strikes, Greeks, IV, OI, quotes, and trades.

Contract snapshot

The focused one-leg view after a chain scanner or user selects an exact contract.

Volume/OI pressure

Same-day option volume divided by prior open interest, used as an attention filter rather than proof of new positioning.

Options flow false positive

A scanner row that looks meaningful but weakens after spread, quote age, event, trade, or structure checks.

Scanner artifact

The saved contract, score, volume, OI, premium, quote, trade, tag, and reject record behind an alert.

Historical REST window

A timestamp-bounded request for quotes, trades, contracts, or bars used to rebuild a past market state.

Backfill

A REST request used after a stream gap, retry, or missing cache hit to repair an interval explicitly.

DTE bucket

A days-to-expiration grouping such as 0DTE, weekly, monthly, LEAPS, or event-window contracts.

Moneyness band

The ITM, ATM, or OTM relationship between strike, contract side, underlying price, and delta.

Quote condition

A code attached to a bid/ask update that affects whether it belongs in scanners, backtests, or displayed state.

Trade condition

A code attached to a print that affects whether the last sale is regular, corrected, excluded, or only contextual.

IV skew

The shape of implied volatility across strikes or expirations, usually read with Greeks and term-structure context.

0DTE contract

An option that expires the same trading day and needs tighter spread, quote-age, and session-state controls.

OCC root

The symbol root inside the OCC option identifier, which can differ from casual ticker text in adjusted or special cases.

Options data API

The product surface for chains, contracts, quotes, trades, aggregates, Greeks, IV, open interest, and expirations.

OPRA-originating data

The U.S. listed-options source context behind quotes, trades, exchange participation, and consolidated option-market records.

OCC option symbol

The exact option contract identifier that preserves root, expiration, call or put side, and strike.

Bid/ask spread

The execution interval between bid and ask that determines whether a contract is realistically tradable.

Midpoint

The computed center between bid and ask, useful as a reference price but not proof that an order would fill.

Quote/trade condition

The condition-code, exchange, correction, sequence, and timestamp context that explains how a quote or trade row can be used.

Quote vs trade semantics

The distinction between executable bid/ask markets, printed transactions, and bar-level summaries.

REST snapshot

A reproducible request for current or historical market state, used for initialization, backfills, and audit logs.

WebSocket stream

A persistent live connection that needs subscription topics, reconnect tracking, freshness labels, and REST repair paths.

Entitlement gate

The product, plan, quote, live, delayed, historical, or commercial-use boundary checked before data is shown.

Daniel Ratke

Written by

Daniel Ratke

Research & Engineering

Daniel covers the deeper research notes: options backtesting, execution realism, robustness testing, data engineering, and strategy validation.