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)]