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..cb37fbb84405 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,11 +133,22 @@ 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); } + // 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 f87317d42b2d..d5d15e1ed575 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 @@ -82,11 +83,20 @@ 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)); 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, 0, 0)); } [TestCaseSource(nameof(ExpirationDateTimeTestCases))] @@ -95,5 +105,38 @@ public void CalculatesSettlementDateTime(Symbol symbol, DateTime expectedSettlem var settlementDateTime = OptionSymbol.GetSettlementDateTime(symbol); 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)] + [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)); + } } } 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)]