Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions Common/Securities/IndexOption/IndexOptionSymbol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

/// <summary>
/// Returns true if the index option is AM settled
/// </summary>
public static bool IsAMSettled(Symbol symbol)
{
return !_nonStandardIndexOptionTickers.Contains(symbol.ID.Symbol.LazyToUpper());
}

/// <summary>
/// Checks if the ticker provided is a supported Index Option
/// </summary>
Expand All @@ -107,7 +115,7 @@ public static bool IsIndexOption(string ticker)
/// <returns>Index ticker</returns>
public static string MapToUnderlying(string indexOption)
{
if(_nonStandardOptionToIndex.TryGetValue(indexOption.LazyToUpper(), out var index))
if (_nonStandardOptionToIndex.TryGetValue(indexOption.LazyToUpper(), out var index))
{
return index;
}
Expand Down
17 changes: 14 additions & 3 deletions Common/Securities/Option/OptionSymbol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public static bool IsWeekly(Symbol symbol)
/// <returns>The underlying ticker</returns>
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);
}
Expand Down Expand Up @@ -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;
}
Expand Down
45 changes: 44 additions & 1 deletion Tests/Common/Securities/Options/OptionSymbolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -82,11 +83,20 @@ private static IEnumerable<TestCaseData> 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))]
Expand All @@ -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));
}
}
}
21 changes: 21 additions & 0 deletions Tests/Indicators/DeltaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Loading