diff --git a/03 Writing Algorithms/03 Securities/99 Asset Classes/02 Equity Options/01 Requesting Data/02 Individual Contracts/99 Examples.html b/03 Writing Algorithms/03 Securities/99 Asset Classes/02 Equity Options/01 Requesting Data/02 Individual Contracts/99 Examples.html index 0dc5e695e4..0c482f77ab 100644 --- a/03 Writing Algorithms/03 Securities/99 Asset Classes/02 Equity Options/01 Requesting Data/02 Individual Contracts/99 Examples.html +++ b/03 Writing Algorithms/03 Securities/99 Asset Classes/02 Equity Options/01 Requesting Data/02 Individual Contracts/99 Examples.html @@ -1,420 +1,435 @@
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.
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((security) => new FuncSecuritySeeder(GetLastKnownPrices).SeedSecurity(security)); - // 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() +public class EquityOptionExampleAlgorithm : QCAlgorithm { - // Get all tradable option contracts for AAPL at the current time for filtering - var contractSymbols = OptionChainProvider.GetOptionContractList(_aapl, Time); - // 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) + private Symbol _aapl; + private List<Symbol> _options = new(); + + public override void Initialize() { - // 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); + // Seed the security price to ensure the retrieval of the ATM calls at the initial filtering. + SetSecurityInitializer((security) => new FuncSecuritySeeder(GetLastKnownPrices).SeedSecurity(security)); + // 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 + ); } -} - -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) + + private void RefreshOptionList() { - MarketOrder(_aapl, -Securities[orderEvent.Symbol].SymbolProperties.ContractMultiplier); + // Get all tradable option contracts for AAPL at the current time for filtering. + var contractSymbols = OptionChainProvider.GetOptionContractList(_aapl, Time); + // 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); + } } }-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(lambda security: FuncSecuritySeeder(self.get_last_known_prices).seed_security(security)) - # 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_provider.get_option_contract_list(self.aapl, self.time) - # 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)+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(lambda security: FuncSecuritySeeder(self.get_last_known_prices).seed_security(security)) + # 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_provider.get_option_contract_list(self.aapl, self.time) + # 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)
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.
private Symbol _spy; -private List<Symbol> _options = new(); - -public override void Initialize() +public class EquityOptionExampleAlgorithm : QCAlgorithm { - // Seed the underlying security price to ensure accurate filtering for puts of $5 above/below current market price - SetSecurityInitializer((security) => new FuncSecuritySeeder(GetLastKnownPrices).SeedSecurity(security)); - // 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 = OptionChainProvider.GetOptionContractList(_spy, Time); - // 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) + private Symbol _spy; + private List<Symbol> _options = new(); + + public override void Initialize() { - _options = new(); - return; + // Seed the underlying security price to ensure accurate filtering for puts of $5 above/below current market price. + SetSecurityInitializer((security) => new FuncSecuritySeeder(GetLastKnownPrices).SeedSecurity(security)); + // 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 + ); } - // 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) + + private void RefreshOptionList() { - // 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); + // Get all tradable option contracts for SPY at the current time for filtering. + var contractSymbols = OptionChainProvider.GetOptionContractList(_spy, Time); + // 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(); } -} - -private void ExitPositions() -{ - // Exit all positions before market close to avoid option assignment and overnight risk - Liquidate(); }-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(lambda security: FuncSecuritySeeder(self.get_last_known_prices).seed_security(security)) - # 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_provider.get_option_contract_list(self.spy, self.time) - # 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: +class EquityOptionExampleAlgorithm(QCAlgorithm): + def initialize(self) -> None: self.options = [] - return + # Seed the underlying security price to ensure accurate filtering for puts of $5 above/below current market price. + self.set_security_initializer(lambda security: FuncSecuritySeeder(self.get_last_known_prices).seed_security(security)) + # Set the data normalization mode as raw for option strike-price comparability. + self.spy = self.add_equity("SPY", data_normalization_mode=DataNormalizationMode.RAW).symbol - # 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()+ # 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_provider.get_option_contract_list(self.spy, self.time) + # 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()
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.
private Symbol _spy; -private DividendYieldProvider _dividendYieldModel; -private List<Symbol> _universe = new(); - -public override void Initialize() +public class EquityOptionExampleAlgorithm : QCAlgorithm { - // 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 = OptionChainProvider.GetOptionContractList(_spy, Time); - // 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()) + private Symbol _spy; + private DividendYieldProvider _dividendYieldModel; + private List<Symbol> _universe = new(); + + public override void Initialize() { - // 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); + // 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 = OptionChainProvider.GetOptionContractList(_spy, Time); + // 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; } - - // 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; }-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_provider.get_option_contract_list(self.spy, self.time) - # 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 +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_provider.get_option_contract_list(self.spy, self.time) + # 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 - # 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+ # 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
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.
private Symbol _spy; -// Set OTM threshold for wheel strategy profit margin -private decimal _otmThreshold = 0.05m; - -public override void Initialize() +public class EquityOptionExampleAlgorithm : QCAlgorithm { - // 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)) + private Symbol _spy; + // Set OTM threshold for wheel strategy profit margin. + private decimal _otmThreshold = 0.05m; + + public override void Initialize() { - // 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); + // Set the data normalization mode as raw for option strike-price comparability. + _spy = AddEquity("SPY", dataNormalizationMode: DataNormalizationMode.Raw).Symbol; } - // 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)) + + public override void OnData(Slice slice) { - // 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); + // 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 = OptionChainProvider.GetOptionContractList(_spy, Time).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; } -} - -private Symbol GetTargetContract(OptionRight right, decimal targetPrice) -{ - // Get all tradable option contracts for SPY at the current time for filtering - var contractSymbols = OptionChainProvider.GetOptionContractList(_spy, Time).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; }-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: - # Get the target put contract - symbol = self.get_target_contract(OptionRight.PUT, slice.bars[self.spy].price * (1 - self.otm_threshold)) - # Short the put contract - 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_provider.get_option_contract_list(self.spy, self.time) - # 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+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_provider.get_option_contract_list(self.spy, self.time) + # 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.