From 671b0ec081b7fd28cbed94068b9057b6d0b590e3 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Wed, 27 May 2026 12:11:21 -0500 Subject: [PATCH 1/3] Fix PM-settled index options settlement time on 3rd Friday --- .../IndexOption/IndexOptionSymbol.cs | 20 ++++++++++++------ Common/Securities/Option/OptionSymbol.cs | 10 ++++++--- Tests/Indicators/DeltaTests.cs | 21 +++++++++++++++++++ 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/Common/Securities/IndexOption/IndexOptionSymbol.cs b/Common/Securities/IndexOption/IndexOptionSymbol.cs index 49420285910c..2ddf0e41b87d 100644 --- a/Common/Securities/IndexOption/IndexOptionSymbol.cs +++ b/Common/Securities/IndexOption/IndexOptionSymbol.cs @@ -73,17 +73,25 @@ public static bool IsStandard(Symbol symbol) case "NQX": case "SPXW": case "RUTW": - // they have weeklies and monthly contracts - // NQX https://www.nasdaq.com/docs/NQXFactSheet.pdf - // SPXW https://www.cboe.com/tradable_products/sp_500/spx_weekly_options/specifications/ - // RUTW expires every day - return FuturesExpiryUtilityFunctions.ThirdFriday(symbol.ID.Date) == symbol.ID.Date; + // they have weeklies and monthly contracts + // NQX https://www.nasdaq.com/docs/NQXFactSheet.pdf + // SPXW https://www.cboe.com/tradable_products/sp_500/spx_weekly_options/specifications/ + // RUTW expires every day + return FuturesExpiryUtilityFunctions.ThirdFriday(symbol.ID.Date) == symbol.ID.Date; default: // NDX/SPX/NQX/VIX/VIXW/NDXP/RUT are all normal contracts return true; } } + /// + /// Returns true if the index option is AM settled + /// + public static bool IsAMSettled(Symbol symbol) + { + return !_nonStandardIndexOptionTickers.Contains(symbol.ID.Symbol.LazyToUpper()); + } + /// /// Checks if the ticker provided is a supported Index Option /// @@ -107,7 +115,7 @@ public static bool IsIndexOption(string ticker) /// Index ticker public static string MapToUnderlying(string indexOption) { - if(_nonStandardOptionToIndex.TryGetValue(indexOption.LazyToUpper(), out var index)) + if (_nonStandardOptionToIndex.TryGetValue(indexOption.LazyToUpper(), out var index)) { return index; } diff --git a/Common/Securities/Option/OptionSymbol.cs b/Common/Securities/Option/OptionSymbol.cs index b82146594c38..906784dc0c83 100644 --- a/Common/Securities/Option/OptionSymbol.cs +++ b/Common/Securities/Option/OptionSymbol.cs @@ -76,7 +76,7 @@ public static bool IsWeekly(Symbol symbol) /// The underlying ticker public static string MapToUnderlying(string optionTicker, SecurityType securityType) { - if(securityType == SecurityType.FutureOption || securityType == SecurityType.Future) + if (securityType == SecurityType.FutureOption || securityType == SecurityType.Future) { return FuturesOptionsSymbolMappings.MapFromOption(optionTicker); } @@ -133,8 +133,12 @@ public static DateTime GetSettlementDateTime(Symbol symbol) throw new ArgumentException($"The symbol {symbol} is not an option type"); } - // Standard index options are AM-settled, which means they settle on market open of the expiration date - if (expiryTime.Date == symbol.ID.Date.Date && symbol.SecurityType == SecurityType.IndexOption && IsStandard(symbol)) + // Standard index options are AM-settled, which means they settle on market open of the expiration date. + // Non-standard tickers (e.g. SPXW, RUTW) are always PM-settled, even when expiring on the 3rd Friday. + if (expiryTime.Date == symbol.ID.Date.Date + && symbol.SecurityType == SecurityType.IndexOption + && IsStandard(symbol) + && IndexOptionSymbol.IsAMSettled(symbol)) { expiryTime = exchangeHours.GetNextMarketOpen(expiryTime.Date, false); } diff --git a/Tests/Indicators/DeltaTests.cs b/Tests/Indicators/DeltaTests.cs index f68f31e433cb..e5c81d627c1f 100644 --- a/Tests/Indicators/DeltaTests.cs +++ b/Tests/Indicators/DeltaTests.cs @@ -131,6 +131,27 @@ public void ComparesAgainstExternalData2(decimal price, decimal spotPrice, Optio Assert.AreEqual(refDelta, (double)indicator.Current.Value, 0.0017d); } + [Test] + public void IVAndDeltaAreNonZeroForPMSettledIndexOptionOnExpirationDay() + { + // SPXW expiring on the 3rd Friday is PM-settled (15:15 CT), + // so at 10 AM the contract still has time value — IV and Delta must be non-zero. + var thirdFriday = new DateTime(2016, 02, 19); + var spxwSymbol = Symbol.CreateOption(Symbols.SPX, "SPXW", Market.USA, OptionStyle.European, + OptionRight.Call, 1900m, thirdFriday); + + var delta = new Delta(spxwSymbol, 0.005m, 0.02m, optionModel: OptionPricingModelType.BlackScholes, + ivModel: OptionPricingModelType.BlackScholes); + + var currentTime = new DateTime(2016, 02, 19, 10, 0, 0); + delta.Update(new IndicatorDataPoint(spxwSymbol, currentTime, 20m)); + delta.Update(new IndicatorDataPoint(spxwSymbol.Underlying, currentTime, 1900m)); + + Assert.IsTrue(delta.IsReady); + Assert.IsTrue(delta.ImpliedVolatility.Current.Value > 0); + Assert.IsTrue(delta.Current.Value > 0); + } + [TestCase(0.5, 470.0, OptionRight.Put, 0)] [TestCase(0.5, 470.0, OptionRight.Put, 5)] [TestCase(0.5, 470.0, OptionRight.Put, 10)] From 6c2bc9ab13943259ec1076a2a3fb750ef4f74328 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Wed, 27 May 2026 14:46:38 -0500 Subject: [PATCH 2/3] Add unit tests --- .../Securities/Options/OptionSymbolTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Tests/Common/Securities/Options/OptionSymbolTests.cs b/Tests/Common/Securities/Options/OptionSymbolTests.cs index f87317d42b2d..10548e8c4547 100644 --- a/Tests/Common/Securities/Options/OptionSymbolTests.cs +++ b/Tests/Common/Securities/Options/OptionSymbolTests.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using NUnit.Framework; +using QuantConnect.Securities.IndexOption; using QuantConnect.Securities.Option; namespace QuantConnect.Tests.Common.Securities.Options @@ -87,6 +88,15 @@ private static IEnumerable ExpirationDateTimeTestCases() var amSettledIndexOption = Symbol.CreateOption(Symbols.SPX, "SPX", Market.USA, OptionStyle.European, OptionRight.Call, 200m, new DateTime(2016, 02, 18)); yield return new TestCaseData(amSettledIndexOption, new DateTime(2016, 02, 18, 8, 30, 0)); + + // 3rd Friday cases: SPX is AM-settled, SPXW is PM-settled even on the same date + var spxThirdFriday = Symbol.CreateOption(Symbols.SPX, "SPX", Market.USA, OptionStyle.European, + OptionRight.Call, 200m, new DateTime(2016, 02, 19)); + yield return new TestCaseData(spxThirdFriday, new DateTime(2016, 02, 19, 8, 30, 0)); + + var spxwThirdFriday = Symbol.CreateOption(Symbols.SPX, "SPXW", Market.USA, OptionStyle.European, + OptionRight.Call, 200m, new DateTime(2016, 02, 19)); + yield return new TestCaseData(spxwThirdFriday, new DateTime(2016, 02, 19, 15, 15, 0)); } [TestCaseSource(nameof(ExpirationDateTimeTestCases))] @@ -95,5 +105,22 @@ public void CalculatesSettlementDateTime(Symbol symbol, DateTime expectedSettlem var settlementDateTime = OptionSymbol.GetSettlementDateTime(symbol); Assert.AreEqual(expectedSettlementDateTime, settlementDateTime); } + + // AM-settled: SPX, NDX, RUT, VIX -> settle at market open on expiry day + // PM-settled: SPXW, RUTW, VIXW, NDXP, NQX -> always settle at market close + [TestCase("SPX", true)] + [TestCase("NDX", true)] + [TestCase("RUT", true)] + [TestCase("VIX", true)] + [TestCase("SPXW", false)] + [TestCase("RUTW", false)] + [TestCase("VIXW", false)] + [TestCase("NDXP", false)] + [TestCase("NQX", false)] + public void IsAMSettledClassifiesAllIndexOptionTickers(string ticker, bool expectedAMSettled) + { + var option = Symbol.CreateOption(Symbols.SPX, ticker, Market.USA, OptionStyle.European, OptionRight.Call, 200m, new DateTime(2016, 02, 19)); + Assert.AreEqual(expectedAMSettled, IndexOptionSymbol.IsAMSettled(option)); + } } } From b59eb799dc4cd0c6692187c9596eacfa7b3efddd Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Thu, 28 May 2026 11:38:22 -0500 Subject: [PATCH 3/3] Fix 0DTE PM-settled index options expiry at 4:00 PM ET --- Common/Securities/Option/OptionSymbol.cs | 7 +++++++ .../Securities/Options/OptionSymbolTests.cs | 20 +++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Common/Securities/Option/OptionSymbol.cs b/Common/Securities/Option/OptionSymbol.cs index 906784dc0c83..cb37fbb84405 100644 --- a/Common/Securities/Option/OptionSymbol.cs +++ b/Common/Securities/Option/OptionSymbol.cs @@ -142,6 +142,13 @@ public static DateTime GetSettlementDateTime(Symbol symbol) { expiryTime = exchangeHours.GetNextMarketOpen(expiryTime.Date, false); } + // 0DTE PM-settled index options expire at 4:00 PM ET, not the regular 4:15 PM ET (CBOE spec) + else if (expiryTime.Date == symbol.ID.Date.Date + && symbol.SecurityType == SecurityType.IndexOption + && !IndexOptionSymbol.IsAMSettled(symbol)) + { + expiryTime = expiryTime.Date.Add(TimeSpan.FromHours(15)); + } return expiryTime; } diff --git a/Tests/Common/Securities/Options/OptionSymbolTests.cs b/Tests/Common/Securities/Options/OptionSymbolTests.cs index 10548e8c4547..d5d15e1ed575 100644 --- a/Tests/Common/Securities/Options/OptionSymbolTests.cs +++ b/Tests/Common/Securities/Options/OptionSymbolTests.cs @@ -83,7 +83,7 @@ private static IEnumerable ExpirationDateTimeTestCases() var pmSettledIndexOption = Symbol.CreateOption(Symbols.SPX, "SPXW", Market.USA, OptionStyle.European, OptionRight.Call, 200m, new DateTime(2016, 02, 12)); - yield return new TestCaseData(pmSettledIndexOption, new DateTime(2016, 02, 12, 15, 15, 0)); + yield return new TestCaseData(pmSettledIndexOption, new DateTime(2016, 02, 12, 15, 0, 0)); var amSettledIndexOption = Symbol.CreateOption(Symbols.SPX, "SPX", Market.USA, OptionStyle.European, OptionRight.Call, 200m, new DateTime(2016, 02, 18)); @@ -96,7 +96,7 @@ private static IEnumerable ExpirationDateTimeTestCases() var spxwThirdFriday = Symbol.CreateOption(Symbols.SPX, "SPXW", Market.USA, OptionStyle.European, OptionRight.Call, 200m, new DateTime(2016, 02, 19)); - yield return new TestCaseData(spxwThirdFriday, new DateTime(2016, 02, 19, 15, 15, 0)); + yield return new TestCaseData(spxwThirdFriday, new DateTime(2016, 02, 19, 15, 0, 0)); } [TestCaseSource(nameof(ExpirationDateTimeTestCases))] @@ -106,6 +106,22 @@ public void CalculatesSettlementDateTime(Symbol symbol, DateTime expectedSettlem Assert.AreEqual(expectedSettlementDateTime, settlementDateTime); } + [TestCase("SPXW")] + [TestCase("RUTW")] + [TestCase("VIXW")] + [TestCase("NDXP")] + [TestCase("NQX")] + public void ZeroDTEPMSettledIndexOptionsExpireAt4PM(string ticker) + { + var expiry = new DateTime(2024, 1, 5); // regular Friday + var underlying = Symbol.Create(IndexOptionSymbol.MapToUnderlying(ticker), SecurityType.Index, Market.USA); + var option = Symbol.CreateOption(underlying, ticker, Market.USA, OptionStyle.European, OptionRight.Call, 200m, expiry); + + var settlement = OptionSymbol.GetSettlementDateTime(option); + + Assert.AreEqual(expiry.Date.AddHours(15), settlement); + } + // AM-settled: SPX, NDX, RUT, VIX -> settle at market open on expiry day // PM-settled: SPXW, RUTW, VIXW, NDXP, NQX -> always settle at market close [TestCase("SPX", true)]