| name | equity-options |
| description | Use when an algorithm trades options on a SINGLE named underlying Equity — subscribing (py`add_option`cs`AddOption` vs py`add_option_contract`cs`AddOptionContract`), reading the chain (the slice's py`option_chains`cs`OptionChains` vs the py`option_chain()`cs`OptionChain()` method), greeks/IV data freshness, strike/expiry filters, price seeding, and warm-up. Triggers — code calling any of those four APIs; questions like "why are my deltas/IV stale", "why can't I trade the contract I just added", "why is the chain empty", "which API gives decision-time greeks". Skip when — options on a DYNAMIC equity universe (see chained-universes-options), historical/backfilled option data for past dates (see historical-data-equity-options), or multi-leg order placement and assignment/exercise handling (see option-strategies). |
Equity Options: subscriptions and chain data — QuantConnect / LEAN
Two decisions shape every equity-options algorithm, and each has two answers that look interchangeable but carry different data: how you subscribe (pyadd_optioncsAddOption vs pyadd_option_contractcsAddOptionContract) and how you read the chain (the slice's pyoption_chainscsOptionChains vs the pyoption_chain()csOptionChain() method). Picking the wrong member of either pair produces algorithms that trade on day-old numbers without any error.
Greeks/IV freshness — the core rule
There are two different option datasets behind the APIs (doc: "There is a daily, pre-calculated dataset based on the end of the previous trading day. There is also a stream of values that are calculated on-the-fly as your algorithm runs."):
| Access path | Data you get |
|---|
pyoption.set_filter(...)csoption.SetFilter(...) filter function (incl. py.delta()/.iv()cs.Delta()/.IV() filters) | "daily, pre-calculated values based on the end of the previous trading day" — not customizable |
pyself.option_chain(symbol)csOptionChain(symbol) method | same prior-day EOD pre-calculated values; returns all tradable contracts, not just your filter's picks |
pyself.history[OptionUniverse](...)csHistory<OptionUniverse>(...) | same prior-day EOD values, one full chain per trading day |
the slice: pydata.option_chainscsdata.OptionChains (or pyself.current_slice.option_chainscsCurrentSlice.OptionChains) | current values from the Option price model, computed on request against live data — customizable via pyoption.price_modelcsoption.PriceModel |
The operating rule that follows: any intraday decision — entry sizing, delta hedging, IV-threshold checks, strike selection at a decision time — should read the slice's chain. The pyoption_chain()csOptionChain() method is for daily contract discovery (find which contracts exist, filter by prior-day delta/OI, then subscribe); using it for intraday sizing or greeks means every number is one day stale, and nothing errors to tell you. Greeks on slice contracts are computed lazily — accessing pygreekscsGreeks costs nothing; accessing pygreeks.deltacsGreeks.Delta triggers the calculation — so only read the greeks you need.
Pick ONE route — and subscribe only what the strategy can trade
Decide the route once, in pyinitializecsInitialize. Mixing them is usually a smell:
- Universe route (py
add_optioncsAddOption + filter): a universe's purpose is its slice chain. If all selection, pricing, and sizing happen through pyoption_chain()csOptionChain() anyway, the minute-resolution subscriptions are pure cost — either read the slice for the decisions that need live data, or drop the universe and subscribe the picks directly.
- Discovery route (py
option_chain()csOptionChain() + pyadd_option_contractcsAddOptionContract): the daily chain picks the contracts; subscribe each pick. Match the data to the decision: the pyoption_chain()csOptionChain() rows are previous-close values — fine when daily-granularity data suits the strategy (screening, ranking, a daily-cadence rule that tolerates day-old marks), but when the strategy calls for decision-time values (intraday sizing, hedging, entry marks), read the subscribed picks' live quotes from the slice. Whichever you use, know which one you are using.
- A subscription earns its cost by being read or being held: a contract you own needs its subscription (position pricing, fills, expiry processing) even if you never read a chain. What doesn't earn its cost is breadth — a wide py
add_optioncsAddOption universe where the algorithm neither reads the slice chain nor holds more than its few picks.
Size the subscription to the trade, not to the chain:
- Prefer a strategy-shaped filter when one matches the structure — py
u.iron_condor(30, 5, 10)csu.IronCondor(30, 5, 10), pyu.straddle(30)csu.Straddle(30), pyu.call_spread(30, 5)csu.CallSpread(30, 5), and friends subscribe only the legs the strategy needs.
- Otherwise derive the strike span and DTE window from the structure's own numbers (target deltas, wing widths, expiry rules), adding a delta-band filter (py
u.delta(0.05, 0.30)csu.Delta(0.05m, 0.30m)) where the structure is delta-targeted. The span should be the tightest one that always contains the tradable candidates — not a safety blanket.
- py
strikes(-100, 100)csStrikes(-100, 100), or an unfiltered pyexpiration(0, 500)csExpiration(0, 500) window, subscribes thousands of minute-resolution contracts — a cost bug even when the algorithm is otherwise correct.
Subscription route 1: pyadd_optioncsAddOption — a filtered basket (universe)
For strategies that pick contracts from a chain at decision time (spreads, condors, straddles, rolling structures), subscribe once in pyinitializecsInitialize:
self.universe_settings.asynchronous = True
option = self.add_option("SPY")
option.set_filter(lambda u: u.include_weeklys().strikes(-3, 3).expiration(15, 60))
self._symbol = option.symbol
UniverseSettings.Asynchronous = true;
var option = AddOption("SPY");
option.SetFilter(u => u.IncludeWeeklys().Strikes(-3, 3).Expiration(15, 60));
_symbol = option.Symbol;
- The canonical py
option.symbolcsoption.Symbol is not tradable — it is the key into pydata.option_chainscsdata.OptionChains and the first argument of every OptionStrategies factory.
- Filter timing: selection "usually runs at the first bar of every day"; a newly selected contract's data arrives "in the next
Slice". So the chain can be absent on any given event — always guard: pychain = data.option_chains.get(self._symbol); if not chain: returncsif (!data.OptionChains.TryGetValue(_symbol, out var chain)) return;.
- Default filter if you never call py
set_filtercsSetFilter: standards + weeklys, within 1 strike of the underlying price, expiring within 35 days — almost never what a spec wants; set the filter explicitly.
- Filter building blocks: py
strikes(min, max)csStrikes(min, max) (relative strike counts around the money, not dollars), pyexpiration(min_days, max_days)csExpiration(minDays, maxDays), pycalls_only()/puts_only()csCallsOnly()/PutsOnly(), pyweeklys_only()/standards_only()/include_weeklys()csWeeklysOnly()/StandardsOnly()/IncludeWeeklys(), pyfront_month()/back_month()csFrontMonth()/BackMonth(), greek/OI filters pydelta(min, max)csDelta(min, max), pyiv(min, max)csIV(min, max), pyopen_interest(min, max)csOpenInterest(min, max) (these compare prior-day EOD values — use them for coarse pre-selection, then pick precisely from live slice greeks), and strategy-shaped helpers (pyu.straddle(30)csu.Straddle(30), pyu.iron_condor(30, 5, 10)csu.IronCondor(30, 5, 10)) that narrow the universe to exactly the legs a named strategy needs.
- Resolution rule: the option resolution must be ≥ the underlying Equity's resolution (minute equity → minute/hour/daily options). Contract subscriptions support minute, hour, and daily; minute is the default.
Reading the chain at a decision time:
def on_data(self, data: Slice) -> None:
chain = data.option_chains.get(self._symbol)
if not chain:
return
spot = chain.underlying.price
atm_call = min((c for c in chain if c.right == OptionRight.CALL),
key=lambda c: abs(c.strike - spot))
delta = atm_call.greeks.delta
mid = (atm_call.bid_price + atm_call.ask_price) / 2
public override void OnData(Slice data)
{
if (!data.OptionChains.TryGetValue(_symbol, out var chain)) return;
var spot = chain.Underlying.Price;
var atmCall = chain.Where(c => c.Right == OptionRight.Call)
.OrderBy(c => Math.Abs(c.Strike - spot)).First();
var delta = atmCall.Greeks.Delta;
var mid = (atmCall.BidPrice + atmCall.AskPrice) / 2;
}
In a Scheduled Event there is no slice parameter — read the same object through pyself.current_slice.option_chainscsCurrentSlice.OptionChains.
Subscription route 2: pyadd_option_contractcsAddOptionContract — individual known contracts
For strategies that hold a few specific contracts found by daily screening, discover with the pyoption_chain()csOptionChain() method, then subscribe each pick:
chain = self.option_chain(self._underlying, flatten=True).data_frame
expiry = chain.expiry.min()
symbol = chain[(chain.expiry == expiry) & (chain.right == OptionRight.CALL) &
(chain.delta > 0.3)].sort_values("openinterest").index[-1]
self.add_option_contract(symbol)
var chain = OptionChain(_underlying);
var expiry = chain.Min(c => c.Expiry);
var symbol = chain
.Where(c => c.Expiry == expiry && c.Right == OptionRight.Call && c.Greeks.Delta > 0.3m)
.OrderBy(c => c.OpenInterest).Last().Symbol;
AddOptionContract(symbol);
The three idioms that must accompany this route:
- Seed prices — a contract subscribed this time step has no data until the next slice ("you'll need to wait until the next
Slice to receive data and trade the contract"). Set pyself.settings.seed_initial_prices = TruecsSettings.SeedInitialPrices = true; in pyinitializecsInitialize to trade it immediately.
- RAW underlying — subscribe the underlying with py
self.add_equity("SPY", data_normalization_mode=DataNormalizationMode.RAW)csAddEquity("SPY", dataNormalizationMode: DataNormalizationMode.Raw) so strike-vs-price comparisons share a scale. If you skip this, LEAN auto-subscribes (or force-switches an existing subscription) to RAW anyway — see the history caveat below.
- Volatility warm-up — py
set_warm_upcsSetWarmUp in pyinitializecsInitialize cannot anticipate contracts added later; warm the underlying's volatility model in a security initializer (feed ~30 daily bars through pysecurity.volatility_model.update(...)cssecurity.VolatilityModel.Update(...)) so slice-chain IV/greeks are sane from the first read. On the pyadd_optioncsAddOption route the simple pyself.set_warm_up(31, Resolution.DAILY)csSetWarmUp(31, Resolution.Daily) suffices (default volatility model needs 30+1 daily bars).
To read a subscribed contract from the slice, index the chain with the canonical: pydata.option_chains.get(self._contract_symbol.canonical)csdata.OptionChains.TryGetValue(_contractSymbol.Canonical, out var chain). pyremove_option_contract(symbol)csRemoveOptionContract(symbol) cancels its open orders and liquidates the position.
The RAW-underlying history caveat
Because any option subscription forces the underlying Equity to RAW normalization, a pyhistory()csHistory() call on that underlying now returns raw closes — dividend gaps included. Any realized-volatility, daily-return, or overnight-gap calculation on an option underlying must request adjusted data explicitly: pyself.history(self._underlying, 22, Resolution.DAILY, data_normalization_mode=DataNormalizationMode.ADJUSTED)csHistory(_underlying, 22, Resolution.Daily, dataNormalizationMode: DataNormalizationMode.Adjusted). Do not "fix" this by subscribing the underlying ADJUSTED — LEAN silently switches it back to RAW.
Common mistakes
- Sizing or hedging from py
option_chain()csOptionChain() intraday. Its marks and greeks are the previous close; position sizes computed from them are provably off versus decision-time quotes. Decision-time numbers come from the slice's chain.
- Trading the canonical symbol, or passing a contract symbol where the canonical is required (chain lookup,
OptionStrategies factories).
- No py
if not chain:csTryGetValue guard — the chain is legitimately absent before the first daily selection and whenever the filter turns over.
- Forgetting py
include_weeklys()csIncludeWeeklys() when a spec's DTE window (e.g. 21–45 days, closest to 30) needs weekly expiries to hit its target.
- Carrying a wide universe you never read — subscribing a broad chain while doing all selection, pricing, and sizing through py
option_chain()csOptionChain(): every subscription beyond the contracts actually held is pure cost. Read the universe's slice chain for the decisions that need live data, or drop the universe and use pyadd_option_contractcsAddOptionContract on the picks.
- Subscribing a huge chain as a tradability crutch (±100 strikes, unbounded expiries) so any discovered contract can be ordered directly — subscribe the picks with py
add_option_contractcsAddOptionContract instead, and let a strategy-shaped or structure-derived filter keep the universe minimal.
- Computing RV/returns on the force-RAW underlying without an explicit adjusted-normalization history request.
- Only American-style US equity options are supported; strikes in the filter API are relative counts, not dollar offsets — py
strikes(-1, 1)csStrikes(-1, 1) means one strike either side of the money.