Requesting Data

Individual Contracts

Introduction

The AddOptionContractadd_option_contract method enables you to add an individual Option contract to your algorithm. To check which contracts are currently available to add to your algorithm, use the OptionChainoption_chain method. If you want to subscribe to a set of contracts instead of individual contracts one-by-one, see Universes.

Create Subscriptions

Before you can subscribe to an Option contract, you must configure the underlying Equity and get the contract Symbol.

public class BasicOptionAlgorithm : QCAlgorithm
{
    private Symbol _underlying, _contractSymbol;
    public override void Initialize()
    {
        SetStartDate(2024, 1, 1);
        _underlying = AddEquity("SPY", dataNormalizationMode: DataNormalizationMode.Raw).Symbol;
    }

    public override void OnData(Slice data)
    {
        if (_contractSymbol != null)
        {
            return;
        }
        var chain = OptionChain(_underlying);
        var expiry = chain.Select(contract => contract.Expiry).Min();
        _contractSymbol = chain
            .Where(contract => 
                contract.Expiry == expiry && 
                contract.Right == OptionRight.Call &&
                contract.Greeks.Delta > 0.3m && 
                contract.Greeks.Delta < 0.7m
            )
            .OrderByDescending(contract => contract.OpenInterest)
            .First()
            .Symbol;
        AddOptionContract(_contractSymbol);
    }
}
class BasicOptionAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2024, 1, 1)
        self._underlying = self.add_equity('SPY', data_normalization_mode=DataNormalizationMode.RAW).symbol
        self._contract_symbol = None

    def on_data(self, data):
        if self._contract_symbol:
            return
        chain = self.option_chain(self._underlying).data_frame
        expiry = chain.expiry.min()
        self._contract_symbol = chain[
            (chain.expiry == expiry) &
            (chain.right == OptionRight.CALL) &
            (chain.delta < 0.7) &
            (chain.delta > 0.3)
        ].sort_values('openinterest').index[-1]
        self.add_option_contract(self._contract_symbol)

Configure the Underlying Equity

If you want to subscribe to the underlying Equity in the Initializeinitialize method, set the Equity data normalization to DataNormalizationMode.RawDataNormalizationMode.RAW.

_underlying = AddEquity("SPY", dataNormalizationMode: DataNormalizationMode.Raw).Symbol;
self._underlying = self.add_equity("SPY", data_normalization_mode=DataNormalizationMode.RAW).symbol

If your algorithm has a dynamic universe of Equities, before you add the Equity universe in the Initializeinitialize method, set the universe data normalization mode to DataNormalizationMode.RawDataNormalizationMode.RAW.

UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw;
self.universe_settings.data_normalization_mode = DataNormalizationMode.RAW

Get Contract Symbols

To subscribe to an Option contract, you need the contract Symbol. The preferred method to getting Option contract Symbol objects is to use the OptionChainoption_chain method. This method returns an OptionChain object, which represent an entire chain of Option contracts for a single underlying security. You can even format the chain data into a DataFrame where each row in the DataFrame represents a single contract. With the chain, sort and filter the data to find the specific contract(s) you want to trade.

// Get the contracts available to trade.
var chain = OptionChain(_underlying);

// Select a contract.
var expiry = chain.Select(contract => contract.Expiry).Min();
_contractSymbol = chain
    .Where(contract => 
        // Select call contracts with the closest expiry.
        contract.Expiry == expiry && 
        contract.Right == OptionRight.Call &&
        // Select contracts with a 0.3-0.7 delta.
        contract.Greeks.Delta > 0.3m && 
        contract.Greeks.Delta < 0.7m
    )
    // Select the contract with the largest open interest.
    .OrderByDescending(contract => contract.OpenInterest)
    .First()
    // Get the Symbol of the target contract.
    .Symbol;
# Get the contracts available to trade (in DataFrame format).
chain = self.option_chain(self._underlying).data_frame

# Select a contract.
expiry = chain.expiry.min()
self._contract_symbol = chain[
    # Select call contracts with the closest expiry.
    (chain.expiry == expiry) & 
    (chain.right == OptionRight.CALL) &
    # Select contracts with a 0.3-0.7 delta.
    (chain.delta > 0.3) &
    (chain.delta < 0.7)
    # Select the contract with the largest open interest.
].sort_values('openinterest').index[-1]

Subscribe to Contracts

To create an Equity Option contract subscription, pass the contract Symbol to the AddOptionContractadd_option_contract method. Save a reference to the contract Symbolsymbol so you can easily access the Option contract in the OptionChain that LEAN passes to the OnDataon_data method. This method returns an Option object. To override the default pricing model of the Option, set a pricing model.

var option = AddOptionContract(_contractSymbol);
option.PriceModel = OptionPriceModels.BinomialCoxRossRubinstein();
option = self.add_option_contract(self._contract_symbol)
option.price_model = OptionPriceModels.binomial_cox_ross_rubinstein()

The AddOptionContractadd_option_contract method creates a subscription for a single Option contract and adds it to your user-defined universe. To create a dynamic universe of Option contracts, add an Equity Options universe or an Options Universe Selection model.

Warm Up Contract Prices

If you subscribe to an Option contract with AddOptionContractadd_option_contract, you'll need to wait until the next Slice to receive data and trade the contract. To trade the contract in the same time step you subscribe to the contract, set the current price of the contract in a security initializer.

var seeder = new FuncSecuritySeeder(GetLastKnownPrices);
SetSecurityInitializer(new BrokerageModelSecurityInitializer(BrokerageModel, seeder));
seeder = FuncSecuritySeeder(self.get_last_known_prices)
self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, seeder))

Supported Assets

To view the supported assets in the US Equities dataset, see the Data Explorer.

Resolutions

The following table shows the available resolutions and data formats for Equity Option contract subscriptions:

ResolutionTradeBarQuoteBarTrade TickQuote Tick
TickTICK

SecondSECOND

MinuteMINUTEgreen checkgreen check
HourHOURgreen checkgreen check
DailyDAILYgreen checkgreen check

The default resolution for Option contract subscriptions is Resolution.MinuteResolution.MINUTE. To change the resolution, pass a resolution argument to the AddOptionContractadd_option_contract method.

AddOptionContract(_contractSymbol, Resolution.Minute);
self.add_option_contract(self._contract_symbol, Resolution.MINUTE)

To create custom resolution periods, see Consolidating Data.

Supported Markets

LEAN groups all of the US Equity exchanges under Market.USA. You don't need to pass a Marketmarket argument to the AddOptionContractadd_option_contract method because the contract Symbolsymbol already contains the market.

Fill Forward

Fill forward means if there is no data point for the current slice, LEAN uses the previous data point. Fill forward is the default data setting. If you disable fill forward, you may get stale fills or you may see trade volume as zero.

To disable fill forward for a security, set the fillForwardfill_forward argument to false when you create the security subscription.

AddOptionContract(_contractSymbol, fillForward: false);
self.add_option_contract(self._contract_symbol, fill_forward=False)

Margin and Leverage

LEAN models buying power and margin calls to ensure your algorithm stays within the margin requirements. Options are already leveraged products, so you can't change their leverage.

Extended Market Hours

By default, your security subscriptions only cover regular trading hours. To subscribe to pre and post-market trading hours for a specific asset, enable the extendedMarketHoursextended_market_hours argument when you create the security subscription.

AddOptionContract(_contractSymbol, extendedMarketHours: true);
self.add_option_contract(self._contract_symbol, extended_market_hours=True)

You only receive extended market hours data if you create the subscription with minute, second, or tick resolution. If you create the subscription with daily or hourly resolution, the bars only reflect the regular trading hours.

To view the schedule of regular and extended market hours, see Market Hours.

Data Normalization

The data normalization mode doesn't affect the data that LEAN passes to OnDataon_data or the data from history request. By default, LEAN doesn't adjust Equity Options data for splits and dividends of their underlying. If you change the data normalization mode, it won't change the outcome.

If you hold an Option contract when a corporate action occurs for the underlying Equity, LEAN automatically closes your position.

Remove Subscriptions

To remove a contract subscription that you created with AddOptionContractadd_option_contract, call the RemoveOptionContractremove_option_contract method. This method is an alias for RemoveSecurityremove_security.

RemoveOptionContract(_contractSymbol);
self.remove_option_contract(self._contract_symbol)

The RemoveOptionContractremove_option_contract method cancels your open orders for the contract and liquidates your holdings.

Properties

The AddOptionContractadd_option_contract method returns an Option object, which have the following properties:

Helper Methods

The Option object provides methods you can use for basic calculations. These methods require the underlying price. To get the Option object and the Security object for its underlying in any function, use the Option Symbolsymbol to access the value in the Securitiessecurities object.

var option = Securities[_contractSymbol];
var underlying = Securities[_contractSymbol.Underlying];
var underlyingPrice = underlying.Price;
option = self.securities[self._contract_symbol]
underlying = self.securities[self._contract_symbol.underlying]
underlying_price = underlying.price

To get the Option payoff, call the GetPayOffget_pay_off method.

var payOff = option.GetPayOff(underlyingPrice);
pay_off = option.get_pay_off(underlying_price)

To get the Option intrinsic value, call the GetIntrinsicValueget_intrinsic_value method.

var intrinsicValue = option.GetIntrinsicValue(underlyingPrice);
intrinsic_value = option.get_intrinsic_value(underlying_price)

To get the Option out-of-the-money amount, call the OutOfTheMoneyAmountout_of_the_money_amount method.

var otmAmount = option.OutOfTheMoneyAmount(underlyingPrice);
otm_amount = option.out_of_the_money_amount(underlying_price)

To check whether the Option can be automatic exercised, call the IsAutoExercisedis_auto_exercised method.

var isAutoExercised = option.IsAutoExercised(underlyingPrice);
is_auto_exercised = option.is_auto_exercised(underlying_price)

Exceptions and Edge Cases

The following sections explain exceptions and edge cases with subscribing to individual Option contracts.

Default Underlying Subscription Settings

If you subscribe to an Equity Option contract but don't have a subscription to the underlying Equity, LEAN automatically subscribes to the underlying Equity with the following settings:

SettingValue
Fill forwardSame as the Option contract
Leverage0
Extended Market HoursSame as the Option contract
Data NormalizationDataNormalizationMode.RawDataNormalizationMode.RAW

In this case, you still need the Equity Symbol to subscribe to Equity Option contracts. If you don't have access to it, create it.

_underlying = QuantConnect.Symbol.Create("SPY", SecurityType.Equity, Market.USA);
self._underlying = Symbol.create("SPY", SecurityType.EQUITY, Market.USA)

Manually Creating Option Symbol Objects

To subscribe to an Option contract, you need the contract Symbol. You can get the contract Symbol from the CreateOptioncreate_option or OptionChainoption_chain methods. If you use the CreateOptioncreate_option method, you need to provide the details of an existing contract.

_contractSymbol = QuantConnect.Symbol.CreateOption(_underlying, Market.USA,
    OptionStyle.American, OptionRight.Call, 365, new DateTime(2022, 6, 17));
self._contract_symbol = Symbol.create_option(self._underlying, Market.USA,
    OptionStyle.AMERICAN, OptionRight.CALL, 365, datetime(2022, 6, 17))

Overriding the Initial Implied Volatility Guess

To override the initial guess of implied volatility, set and warm up the underlying volatility model.

Examples

Example 1: Covered Call

A cover call consists of a short call and with a lot of the underlying equity. Although it capped the maximum profit if the underlying skyrocketted, it also provide extra credit received while speculating the underlying will rise.

public class EquityOptionExampleAlgorithm : QCAlgorithm
{
    private Symbol _aapl;
    private List<Symbol> _options = new();
    
    public override void Initialize()
    {
        // Seed the security price to ensure the retrieval of the ATM calls at the initial filtering.
        SetSecurityInitializer(new BrokerageModelSecurityInitializer(BrokerageModel, new FuncSecuritySeeder(GetLastKnownPrices)));
        // Set the data normalization mode as raw for option strike-price comparability.
        _aapl = AddEquity("AAPL", dataNormalizationMode: DataNormalizationMode.Raw).Symbol;
    
        // Filter an updated option list at market open everyday by a scheduled event.
        Schedule.On(
            DateRules.EveryDay(_aapl),
            TimeRules.AfterMarketOpen(_aapl, 0),
            RefreshOptionList
        );
    }
    
    private void RefreshOptionList()
    {
        // Get all tradable option contracts for AAPL at the current time for filtering.
        var contractSymbols = OptionChain(_aapl);
        // Select the calls expires within 30 days and within $5 strike from ATM as leg of the covered call.
        // $5 buffer is given on selecting the ATM call due to price movement.
        _options = contractSymbols
            .Where(symbol => 
                symbol.ID.Date < Time.AddDays(30) && 
                symbol.ID.OptionRight == OptionRight.Call && 
                Math.Abs(symbol.ID.StrikePrice - Securities[_aapl].Price) <= 5
            )
            .Select(symbol => AddOptionContract(symbol).Symbol)
            .ToList();
    }
    
    public override void OnData(Slice slice)
    {
        if (!Portfolio.Invested && slice.Bars.ContainsKey(_aapl) && _options.Count > 0)
        {
            // To form a covered call, get the contract closest to ATM and expiry.
            var contract = _options.OrderBy(x => x.ID.Date)
                .ThenBy(x => Math.Abs(x.ID.StrikePrice - slice.Bars[_aapl].Price))
                .First();
    
            // Covered call involves shorting 1 ATM call and ordering 1 lot of underlying.
            MarketOrder(contract, -1);
            MarketOrder(_aapl, Securities[contract].SymbolProperties.ContractMultiplier);
        }
    }
    
    public override void OnOrderEvent(OrderEvent orderEvent)
    {
        // Close AAPL position if the short call is not exercised (OTM).
        // If it is exercised, the underlying will be used to settle the contract automatically.
        if (orderEvent.Ticket.OrderType == OrderType.OptionExercise &&
            !orderEvent.IsInTheMoney)
        {
            MarketOrder(_aapl, -Securities[orderEvent.Symbol].SymbolProperties.ContractMultiplier);
        }
    }
}
class EquityOptionExampleAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self.options = []
        # Seed the security price to ensure the retrieval of the ATM calls at the initial filtering.
        self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))
        # Set the data normalization mode as raw for option strike-price comparability.
        self.aapl = self.add_equity("AAPL", data_normalization_mode=DataNormalizationMode.RAW).symbol
        
        # Filter an updated option list at market open everyday by a scheduled event.
        self.schedule.on(
            self.date_rules.every_day(self.aapl),
            self.time_rules.at(9, 0),
            self.refresh_option_list
        )
    
    def refresh_option_list(self) -> None:
        # Get all tradable option contracts for AAPL at the current time for filtering.
        contract_symbols = self.option_chain(self.aapl)
        # Select the calls expires within 30 days and within $5 strike from ATM as leg of the covered call.
        # $5 buffer is given on selecting the ATM call due to price movement.
        self.options = [self.add_option_contract(symbol).symbol for symbol in contract_symbols
            if symbol.id.date < self.time + timedelta(30) \
            and symbol.id.option_right == OptionRight.CALL \
            and abs(symbol.id.strike_price - self.securities[self.aapl].price) <= 5]
    
    def on_data(self, slice: Slice) -> None:
        if not self.portfolio.invested and self.aapl in slice.bars and self.options:
            # To form a covered call, get the contract closest to ATM and expiry.
            expiry = min(x.id.date for x in self.options)
            contract = sorted([x for x in self.options if x.id.date == expiry],
                key=lambda x: abs(x.id.strike_price - self.securities[self.aapl].price))[0]
                
            # Covered call involves shorting 1 ATM call and ordering 1 lot of underlying.
            self.market_order(contract, -1)
            self.market_order(self.aapl, self.securities[contract].symbol_properties.contract_multiplier)
    
    def on_order_event(self, order_event: OrderEvent) -> None:
        # Close AAPL position if the short call is not exercised (OTM).
        # If it is exercised, the underlying will be used to settle the contract automatically.
        if order_event.ticket.order_type == OrderType.OPTION_EXERCISE \
        and not order_event.is_in_the_money:
            self.market_order(self.aapl, -self.securities[order_event.symbol].symbol_properties.contract_multiplier)

Example 2: 0-DTE Bull Put Spread

0DTE options often trades with high volume and volatility, providing arbitration opportunities and higher profit margin on spread type trading. In this example, we make use of 0-DTE SPY options to trade bull put spread option strategy.

public class EquityOptionExampleAlgorithm : QCAlgorithm
{
    private Symbol _spy;
    private List<Symbol> _options = new();
    
    public override void Initialize()
    {
        // Seed the underlying security price to ensure accurate filtering for puts of $5 above/below current market price.
        SetSecurityInitializer(new BrokerageModelSecurityInitializer(BrokerageModel, new FuncSecuritySeeder(GetLastKnownPrices)));
        // Set the data normalization mode as raw for option strike-price comparability.
        _spy = AddEquity("SPY", dataNormalizationMode: DataNormalizationMode.Raw).Symbol;
    
        // Filter an updated option list at market open everyday by a scheduled event.
        Schedule.On(
            DateRules.EveryDay(_spy),
            TimeRules.At(9, 0),
            RefreshOptionList
        );
    
        // Use a scheduled event to close all positions before market close.
        Schedule.On(
            DateRules.EveryDay(_spy),
            TimeRules.BeforeMarketClose(_spy, 1),
            ExitPositions
        );
    }
    
    private void RefreshOptionList()
    {
        // Get all tradable option contracts for SPY at the current time for filtering.
        var contractSymbols = OptionChain(_spy);
        // Select the 0-DTE puts by setting expiry within 1 day.
        var filteredSymbols = contractSymbols
            .Where(symbol => symbol.ID.Date < Time.AddDays(1) && symbol.ID.OptionRight == OptionRight.Put)
            .ToList();
        // Ensure at least 2 contracts available to form a put spread.
        if (filteredSymbols.Count < 2)
        {
            _options = new();
            return;
        }
        // To form a put spread, select and subscribe to put contracts $5 above and below from the current underlying price.
        var itmPut = filteredSymbols.MinBy(symbol => Math.Abs(symbol.ID.StrikePrice - Securities[_spy].Price - 5));
        var otmPut = filteredSymbols.MinBy(symbol => Math.Abs(symbol.ID.StrikePrice - Securities[_spy].Price + 5));
        _options = new List<Symbol> { AddOptionContract(itmPut).Symbol, AddOptionContract(otmPut).Symbol };
    }
    
    public override void OnData(Slice slice)
    {
        // To avoid over-trading, limit the position opening to before 3pm.
        // To ensure the put spread formed correctly, make sure at least 2 contracts selected.
        if (!Portfolio.Invested && Time.Hour < 15 &&  _options.Count == 2)
        {
            // A bull put spread involves buying a lower-strike put and selling a high-strike put
            MarketOrder(_options.MaxBy(x => x.ID.StrikePrice), -1);
            MarketOrder(_options.MinBy(x => x.ID.StrikePrice), 1);
        }
    }
    
    private void ExitPositions()
    {
        // Exit all positions before market close to avoid option assignment and overnight risk.
        Liquidate();
    }
}
class EquityOptionExampleAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self.options = []
        # Seed the underlying security price to ensure accurate filtering for puts of $5 above/below current market price.
        self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))
        # Set the data normalization mode as raw for option strike-price comparability.
        self.spy = self.add_equity("SPY", data_normalization_mode=DataNormalizationMode.RAW).symbol
        
        # Filter an updated option list at market open everyday by a scheduled event.
        self.schedule.on(
            self.date_rules.every_day(self.spy),
            self.time_rules.at(9, 0),
            self.refresh_option_list
        )
    
        # Use a scheduled event to close all positions before market close.
        self.schedule.on(
            self.date_rules.every_day(self.spy),
            self.time_rules.before_market_close(self.spy, 1),
            self.exit_position
        )
    
    def refresh_option_list(self) -> None:
        # Get all tradable option contracts for SPY at the current time for filtering.
        contract_symbols = self.option_chain(self.spy)
        # Select the 0-DTE puts by setting expiry within 1 day.
        filtered_symbols = [symbol for symbol in contract_symbols
            if symbol.id.date < self.time + timedelta(1) and symbol.id.option_right == OptionRight.PUT]
        # Ensure at least 2 contracts available to form a put spread.
        if len(filtered_symbols) < 2:
            self.options = []
            return
            
        # To form a put spread, select and subscribe to put contracts $5 above and below from the current underlying price.
        itm_put = sorted(filtered_symbols, key=lambda symbol: abs(symbol.id.strike_price - self.securities[self.spy].price - 5))[0]
        otm_put = sorted(filtered_symbols, key=lambda symbol: abs(symbol.id.strike_price - self.securities[self.spy].price + 5))[0]
        self.options = [self.add_option_contract(itm_put).symbol, self.add_option_contract(otm_put).symbol]
    
    def on_data(self, slice: Slice) -> None:
        # To avoid over-trading, limit the position opening to before 3pm.
        # To ensure the put spread formed correctly, make sure at least 2 contracts selected.
        if not self.portfolio.invested and self.time.hour < 15 and len(self.options) == 2:
            # A bull put spread involves buying a lower-strike put and selling a high-strike put
            sorted_by_strike = sorted(self.options, key=lambda x: x.id.strike_price)
            self.market_order(sorted_by_strike[-1], -1)
            self.market_order(sorted_by_strike[0], 1)
    
    def exit_position(self) -> None:
        # Exit all positions before market close to avoid option assignment and overnight risk.
        self.liquidate()

Example 3: Filter Option Contracts By Option Greeks indicator

In this example, we demonstrate filtering option contracts by Delta, an Option Greeks indicator. We filter all the call contracts with Delta greater than 0.99 that expires just more than 7 days later on Monday morning. This filter is useful when creating a hedge replicate portfolio for arbitration.

public class EquityOptionExampleAlgorithm : QCAlgorithm
{
    private Symbol _spy;
    private DividendYieldProvider _dividendYieldModel;
    private List<Symbol> _universe = new();
    
    public override void Initialize()
    {
        // Set the data normalization mode as raw for option strike-price comparability.
        _spy = AddEquity("SPY", dataNormalizationMode: DataNormalizationMode.Raw).Symbol;
        // Instantiate a dividend yield provider to calculate accurate greeks later.
        _dividendYieldModel = new DividendYieldProvider(_spy);
    
        // Scheduled week start filtering to obtain the contracts to be traded for the week.
        Schedule.On(
            DateRules.Every(DayOfWeek.Monday),
            TimeRules.At(7, 30),
            Filter
        );
    }
    
    private void Filter()
    {
        // Get all tradable option contracts for SPY at the current time for filtering.
        var contractSymbols = OptionChain(_spy);
        // Filter the contracts tradable the whole week but expire soon to ensure liquidity.
        var expiry = contractSymbols.Where(x => x.ID.Date >= Time.AddDays(7))
            .Min(x => x.ID.Date);
        var filtered = contractSymbols.Where(x => x.ID.Date == expiry).ToList();
    
        // Group by strike price to create Delta that take both call and put input.
        var symbolDelta = new Dictionary<Symbol, decimal>();
        foreach (var strike in filtered.Select(x => x.ID.StrikePrice).Distinct())
        {
            // There should only be 1 call and put per strike, make sure both are present to feed into the Delta indicator.
            var call = filtered.SingleOrDefault(x => 
                x.ID.StrikePrice == strike && x.ID.OptionRight == OptionRight.Call);
            var put = filtered.SingleOrDefault(x => 
                x.ID.StrikePrice == strike && x.ID.OptionRight == OptionRight.Put);
            if (call == null || put == null) continue;
    
            // Since positive delta must be a call, save the call symbol and delta in the dictionary for later sorting.
            var delta = GetDelta(call, put);
            symbolDelta.Add(call, delta);
        }
    
        // Select the calls using the dictionary with delta filtering, request for the individual contract data for trading.
        _universe = symbolDelta.Where(kvp => kvp.Value >= 0.99m)
            .Select(x => AddOptionContract(x.Key).Symbol)
            .ToList();
    }
    
    private decimal GetDelta(Symbol call, Symbol put)
    {
        // Use both call and put with interest rate and dividend yield provider to obtain accurate OTM contract IV as IV for both call and put.
        // For American option, Forward Tree option pricing model is more accurate.
        var delta = new Delta(call, RiskFreeInterestRateModel, _dividendYieldModel, put, OptionPricingModelType.ForwardTree);
        // Warm up the indicator using both mirror contract pairs and the underlying to calculate the delta.
        IndicatorHistory(delta, new[] { call, put, _spy }, 1);
        // Return the latest delta value.
        return delta.Current.Value;
    }
}
class EquityOptionExampleAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self._universe = []
        # Set the data normalization mode as raw for option strike-price comparability.
        self.spy = self.add_equity("SPY", data_normalization_mode=DataNormalizationMode.RAW).symbol
        # Instantiate a dividend yield provider to calculate accurate greeks later.
        self.dividend_yield_provider = DividendYieldProvider(self.spy)
    
        # Scheduled week start filtering to obtain the contracts to be traded for the week.
        self.schedule.on(
            self.date_rules.every(DayOfWeek.MONDAY),
            self.time_rules.at(7, 30),
            self.filter
        )
    
    def filter(self) -> None:
        # Get all tradable option contracts for SPY at the current time for filtering.
        contract_symbols = self.option_chain(self.spy)
        # Filter the contracts tradable the whole week but expire soon to ensure liquidity.
        expiry = min(x.id.date for x in contract_symbols if x.id.date >= self.time + timedelta(7))
        filtered = [x for x in contract_symbols if x.id.date == expiry]
    
        # Group by strike price to create Delta that take both call and put input.
        symbol_delta = {}
        for strike in set(x.id.strike_price for x in filtered):
            # There should only be 1 call and put per strike, make sure both are present to feed into the Delta indicator.
            call = next(filter(lambda x: x.id.option_right == OptionRight.CALL and x.id.strike_price == strike, filtered), None)
            put = next(filter(lambda x: x.id.option_right == OptionRight.PUT and x.id.strike_price == strike, filtered), None)
            if not call or not put:
                continue
            
            # Since positive delta must be a call, save the call symbol and delta in the dictionary for later sorting.
            delta = self.get_delta(call, put)
            symbol_delta[call] = delta
        
        # Select the calls using the dictionary with delta filtering, request for the individual contract data for trading.
        self._universe = [self.add_option_contract(call).symbol 
            for call, delta in symbol_delta.items() if delta >= 0.99]
        
    def get_delta(self, call: Symbol, put: Symbol) -> float:
        # Use both call and put with interest rate and dividend yield provider to obtain accurate OTM contract IV as IV for both call and put.
        # For American option, Forward Tree option pricing model is more accurate.
        delta = Delta(call, self.risk_free_interest_rate_model, self.dividend_yield_provider, put, OptionPricingModelType.FORWARD_TREE)
        # Warm up the indicator using both mirror contract pairs and the underlying to calculate the delta.
        self.indicator_history(delta, [call, put, self.spy], 1)
        # Return the latest delta value.
        return delta.current.value

Example 4: Wheel Strategy

The Wheel strategy is a popular trading strategy for Options that enables traders to build a steady flow of income from Equity assets they want to hold for the long term.

public class EquityOptionExampleAlgorithm : QCAlgorithm
{
    private Symbol _spy;
    // Set OTM threshold for wheel strategy profit margin.
    private decimal _otmThreshold = 0.05m;
    
    public override void Initialize()
    {
        // Set the data normalization mode as raw for option strike-price comparability.
        _spy = AddEquity("SPY", dataNormalizationMode: DataNormalizationMode.Raw).Symbol;
    }
    
    public override void OnData(Slice slice)
    {
        // To use the latest underlying price to filter the option contract, ensure the SPY in the bar data.
        // Open short put contract position only when the last wheel is completed.
        if (!Portfolio.Invested && slice.Bars.ContainsKey(_spy))
        {
            // Initiate the wheel by shorting a least-OTM put contract that the strike is below the threshold.
            var symbol = GetTargetContract(OptionRight.Put, slice.Bars[_spy].Price * (1 - _otmThreshold));
            SetHoldings(symbol, -0.2m);
        }
        // Open short call contract position only when the put is assigned (portfolio with the underlying) to close the wheel and underlying position by the call assignment.
        else if (Portfolio[_spy].Invested && slice.Bars.ContainsKey(_spy))
        {
            // Short the corresponding number of a least-OTM call contract that the strike is above the threshold.
            var symbol = GetTargetContract(OptionRight.Call, slice.Bars[_spy].Price * (1 + _otmThreshold));
            MarketOrder(symbol, -Portfolio[_spy].Quantity / Securities[_spy].SymbolProperties.ContractMultiplier);
        }
    }
    
    private Symbol GetTargetContract(OptionRight right, decimal targetPrice)
    {
        // Get all tradable option contracts for SPY at the current time for filtering.
        var contractSymbols = OptionChain(_spy).ToList();
        // Filter for the least-OTM contract that is off by the threshold to form the wheel.
        // Expiry is set to be at least 30 days to earn the time decay, which is highest in the last month.
        var expiry = contractSymbols.Where(x => x.ID.Date > Time.AddDays(30))
            .Min(x => x.ID.Date);
        var filtered = contractSymbols.Where(x =>
                x.ID.Date == expiry &&
                x.ID.OptionRight == right &&
                (right == OptionRight.Call ? x.ID.StrikePrice >= targetPrice : x.ID.StrikePrice <= targetPrice)
            )
            .OrderBy(x => x.ID.StrikePrice)
            .ToList();
        var selected = right == OptionRight.Call ? filtered.First() : filtered.Last();
        // Request the selected contract data for trading.
        return AddOptionContract(selected).Symbol;
    }
}
class EquityOptionExampleAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        # Set the data normalization mode as raw for option strike-price comparability.
        self.spy = self.add_equity("SPY", data_normalization_mode=DataNormalizationMode.RAW).symbol
        # Set OTM threshold for wheel strategy profit margin.
        self.otm_threshold = 0.05
    
    def on_data(self, slice: Slice) -> None:
        # To use the latest underlying price to filter the option contract, ensure the SPY in the bar data.
        # Open short put contract position only when the last wheel is completed.
        if not self.portfolio.invested and self.spy in slice.bars:
            # Initiate the wheel by shorting a least-OTM put contract that the strike is below the threshold.
            symbol = self.get_target_contract(OptionRight.PUT, slice.bars[self.spy].price * (1 - self.otm_threshold))
            self.set_holdings(symbol, -0.2)
        # Open short call contract position only when the put is assigned (portfolio with the underlying) to close the wheel and underlying position by the call assignment.
        elif self.portfolio[self.spy].invested and self.spy in slice.bars:
            # Short the corresponding number of a least-OTM call contract that the strike is above the threshold.
            symbol = self.get_target_contract(OptionRight.CALL, slice.bars[self.spy].price * (1 + self.otm_threshold))
            self.market_order(symbol, self.portfolio[self.spy].quantity / self.securities[self.spy].symbol_properties.contract_multipliers)
        
    def get_target_contract(self, right: OptionRight, target_price: float) -> Symbol:
        # Get all tradable option contracts for SPY at the current time for filtering.
        contract_symbols = self.option_chain(self.spy)
        # Filter for the least-OTM contract that is off by the threshold to form the wheel.
        # Expiry is set to be at least 30 days to earn the time decay, which is highest in the last month.
        expiry = min(x.id.date for x in contract_symbols)
        filtered = [x for x in contract_symbols \
            if x.id.date == expiry \
            and x.id.option_right == right \
            and (x.id.strike_price >= target_price if right == OptionRight.CALL else x.id.strike_price <= target_price)]
        sorted_by_strikes = sorted(filtered, key=lambda x: x.id.strike_price)
        selected = sorted_by_strikes[0] if right == OptionRight.CALL else sorted_by_strikes[-1]
        # Request the selected contract data for trading.
        return self.add_option_contract(selected).symbol

For more details, refer to the Wheel Strategy research post.

Example 5: Scan and Update Option Chain Every 5 Minutes

The following example shows how to update the Option chain every five minutes. The OptionChainManager class implements the selection logic and manages the contract subscriptions.

namespace QuantConnect.Algorithm.CSharp
{
    public class OptionChainFullExample : QCAlgorithm
    {
        private Dictionary<Symbol, OptionChainManager> _chainManager = new();
        public override void Initialize()
        {
            SetStartDate(2023, 1, 2);
            SetEndDate(2023, 1, 30);
            SetCash(100000);

            UniverseSettings.Asynchronous = true;
            UniverseSettings.MinimumTimeInUniverse = TimeSpan.Zero;
            // Seed the security price to ensure the underlying price data is ready at the initial filtering
            SetSecurityInitializer(new BrokerageModelSecurityInitializer(BrokerageModel, new FuncSecuritySeeder(GetLastKnownPrices)));
            // Set the data normalization mode as raw for option strike-price comparability
            var spy = AddEquity("SPY", dataNormalizationMode: DataNormalizationMode.Raw).Symbol;
            // Set up a OptionChainManager to filter the contracts based on latest data by request
            _chainManager[QuantConnect.Symbol.CreateCanonicalOption(spy)] = new(-10, 10, 0, 7);
            // Daily population of the available contracts to ensure the contracts are tradable
            PopulateOptionChain();
            Schedule.On(DateRules.EveryDay(spy), TimeRules.AfterMarketOpen(spy, 1), PopulateOptionChain);
            // Filter for contract in every 5 minutes interval through scheduled event
            Schedule.On(DateRules.EveryDay(spy), TimeRules.Every(TimeSpan.FromMinutes(5)), Filter);
        }
        
        private void PopulateOptionChain()
        {
            // The contract list is updated daily, so we can get it and apply
            // the expiration filter as soon as the market opens.
            foreach (var (symbol, manager) in _chainManager)
            {
                manager.SetChain(OptionChain(symbol), Time);
            }
            Filter();
        }
        
        private void Filter()
        {
            foreach (var (symbol, manager) in _chainManager)
            {
                manager.Select(this, symbol);
            }
        }
        
        public override void OnData(Slice slice)
        {
            // Iterate the saved symbol and chain manager to obtain only the contract wanted
            foreach (var (symbol, manager) in _chainManager)
            {
                if (!slice.OptionChains.TryGetValue(symbol, out var chain))
                    continue;
                // Handle option exercise and assignment on unwanted underlying position
                if (Portfolio[symbol.Underlying].Invested)
                {
                    Liquidate(symbol.Underlying);
                }

                // Buy the ATM call with nearest expiry to speculate-trade the underlying through low cost, leveraging position
                var expiry = chain.Min(x => x.Expiry);
                var atmCall = chain
                    .Where(x => x.Expiry == expiry && x.Right == OptionRight.Call && Securities[x.Symbol].IsTradable)
                    .OrderBy(x => Math.Abs(chain.Underlying.Price - x.Strike))
                    .FirstOrDefault();
                if (atmCall != null && !Portfolio[atmCall.Symbol].Invested)
                {
                    MarketOrder(atmCall.Symbol, 1);
                }
            }
        }
    }

    internal class OptionChainManager
    {
        private readonly int _minStrike;
        private readonly int _maxStrike;
        private readonly int _minExpiry;
        private readonly int _maxExpiry;
        private List<OptionContract> _chain = new();
        private readonly List<Symbol> _symbols = new();

        public OptionChainManager(int minStrike, int maxStrike, int minExpiry, int maxExpiry)
        {
            _minStrike = minStrike;
            _maxStrike = maxStrike;
            _minExpiry = minExpiry;
            _maxExpiry = maxExpiry;
        }

        public void SetChain(OptionChain chain, DateTime time)
        {
            // Expiry criteria will not affect intra-day universe filtering, so it is done in a daily basis in higher level
            _chain = chain.Where(x =>
            {
                var totalDays = (x.Expiry - time).TotalDays;
                return _minExpiry <= totalDays && totalDays <= _maxExpiry;
            }).ToList();
        }
        
        public void Select(QCAlgorithm algorithm, Symbol underlyingSymbol)
        {
            if (_chain.IsNullOrEmpty())
                return;
            if (underlyingSymbol.IsCanonical())
                underlyingSymbol = underlyingSymbol.Underlying;

            var strikes = _chain.Select(x => x.Strike).OrderBy(x => x).Distinct().ToList();
            var spot = algorithm.Securities[underlyingSymbol].Price;
            var atm = strikes.OrderBy(x => Math.Abs(spot - x)).FirstOrDefault();
            var index = strikes.IndexOf(atm);
            var minStrike = strikes[Math.Max(0, index + _minStrike)];
            var maxStrike = strikes[Math.Min(strikes.Count - 1, index + _maxStrike)];
            var symbols = _chain
                .Where(x => minStrike <= x.Strike && x.Strike <= maxStrike)
                .Select(x => x.Symbol).ToList();

            // Also remove data subscription on the contracts being filtered out to release computation resources
                foreach (var symbol in _symbols.Except(symbols).ToList())
            {
                if (algorithm.RemoveOptionContract(symbol))
                    _symbols.Remove(symbol);
            }
            foreach (var symbol in symbols.Except(_symbols).ToList())
            {
                _symbols.Add(symbol);
                algorithm.AddOptionContract(symbol);
            }
        }
    }
}
class OptionChainFullExample(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2023, 1, 2)
        self.set_end_date(2023, 1, 30)
        self.set_cash(100000)

        self.universe_settings.asynchronous = True
        self.universe_settings.minimum_time_in_universe = timedelta(minutes=0)        
        # Seed the security price to ensure the underlying price data is ready at the initial filtering
        self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))
        # Set the data normalization mode as raw for option strike-price comparability
        spy = self.add_equity("SPY", data_normalization_mode=DataNormalizationMode.RAW).symbol
        # Set up a OptionChainManager to filter the contracts based on latest data by request
        self._chain_manager = {
            Symbol.create_canonical_option(spy): OptionChainManager(-10, 10, 0, 7)
        }
        # Daily population of the available contracts to ensure the contracts are tradable
        self._populate_option_chain()
        self.schedule.on(
            self.date_rules.every_day(spy), self.time_rules.after_market_open(spy, 1), self._populate_option_chain
        )
        # Filter for contract in every 5 minutes interval through scheduled event
        self.schedule.on(self.date_rules.every_day(spy), self.time_rules.every(timedelta(minutes=5)), self._filter)

    def _populate_option_chain(self):
        # The contract list is updated daily, so we can get it and apply
        # the expiration filter as soon as the market opens.
        for symbol, manager in self._chain_manager.items():
            manager.set_chain(self.option_chain(symbol), self.time)
        self._filter()

    def _filter(self):
        for symbol, manager in self._chain_manager.items():
            manager.select(self, symbol)

    def on_data(self, slice: Slice) -> None:
        # Iterate the saved symbol and chain manager to obtain only the contract wanted
        for symbol, _ in self._chain_manager.items():
            chain = slice.option_chains.get(symbol)
            if not chain: 
                continue
            expiry = min([x.expiry for x in chain])
            contracts = [
                x for x in chain 
                if x.expiry == expiry and x.right == OptionRight.CALL and self.securities[x.symbol].is_tradable
            ]
            if not contracts: 
                continue
            atm_call = sorted(contracts, key=lambda x: abs(chain.underlying.price-x.strike))[0]

            if not self.portfolio[atm_call.symbol].invested:
                self.market_order(atm_call.symbol, 1)


class OptionChainManager:
    _chain = []
    _symbols = set([])
    
    def __init__(self, min_strike, max_strike, min_expiry, max_expiry):
        self._min_strike = min_strike
        self._max_strike = max_strike
        self._min_expiry = min_expiry
        self._max_expiry = max_expiry
    
    def set_chain(self, chain: OptionChain, time: datetime) -> None:
        # Expiry criteria will not affect intra-day universe filtering, so it is done in a daily basis in higher level
        self._chain = [x for x in chain if self._min_expiry <= (x.expiry - time).days <= self._max_expiry]
    
    def select(self, algorithm: QCAlgorithm, symbol: Symbol) -> None:
        if not self._chain:
            return
        if symbol.is_canonical():
            symbol = symbol.underlying

        # Select by strike range from ATM
        strikes = sorted(set(x.strike for x in self._chain))
        spot = algorithm.securities[symbol].price
        atm = sorted(strikes, key=lambda x: abs(spot-x))[0]
        index = strikes.index(atm)
        min_strike = strikes[max(0, index + self._min_strike)]
        max_strike = strikes[min(len(strikes) - 1, index + self._max_strike)]
        symbols = set(x.symbol for x in self._chain if min_strike <= x.strike <= max_strike)
        
        # Also remove data subscription on the contracts being filtered out to release computation resources
        for symbol in self._symbols - symbols:
            if algorithm.remove_option_contract(symbol):
                self._symbols.remove(symbol)
        # Request data subscription on the newly selected contracts for trading
        for symbol in symbols - self._symbols:
            self._symbols.add(symbol)
            algorithm.add_option_contract(symbol)

You can also see our Videos. You can also get in touch with us via Discord.

Did you find this page helpful?

Contribute to the documentation: