diff --git a/Algorithm/QCAlgorithm.cs b/Algorithm/QCAlgorithm.cs index 37883f9e704e..fc79bf006ed8 100644 --- a/Algorithm/QCAlgorithm.cs +++ b/Algorithm/QCAlgorithm.cs @@ -3513,7 +3513,8 @@ public OptionChains OptionChains(IEnumerable symbols, bool flatten = fal foreach (var (symbol, contracts) in optionChainsData) { var symbolProperties = SymbolPropertiesDatabase.GetSymbolProperties(symbol.ID.Market, symbol, symbol.SecurityType, AccountCurrency); - var optionChain = new OptionChain(symbol, GetTimeInExchangeTimeZone(symbol).Date, contracts, symbolProperties, flatten); + var exchangeHours = MarketHoursDatabase.GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType); + var optionChain = new OptionChain(symbol, UtcTime.ConvertFromUtc(exchangeHours.TimeZone).Date, contracts, symbolProperties, exchangeHours, flatten); chains.Add(symbol, optionChain); } diff --git a/Common/Data/Market/OptionChain.Filters.cs b/Common/Data/Market/OptionChain.Filters.cs index 70c006a0a0b0..1001e5c5c234 100644 --- a/Common/Data/Market/OptionChain.Filters.cs +++ b/Common/Data/Market/OptionChain.Filters.cs @@ -507,7 +507,7 @@ public OptionChain PutLadder(int minDaysTillExpiry, decimal higherStrikeFromAtm, /// protected override OptionChainFilterUniverse CreateFilterUniverse() { - return new OptionChainFilterUniverse(this); + return new OptionChainFilterUniverse(this, _symbolProperties, _exchangeHours); } /// diff --git a/Common/Data/Market/OptionChain.cs b/Common/Data/Market/OptionChain.cs index 15f5cf8887dc..5ff10441497d 100644 --- a/Common/Data/Market/OptionChain.cs +++ b/Common/Data/Market/OptionChain.cs @@ -29,6 +29,9 @@ namespace QuantConnect.Data.Market /// public partial class OptionChain : BaseChain, IOptionContractFilters { + private readonly SymbolProperties _symbolProperties; + private readonly SecurityExchangeHours _exchangeHours; + /// /// Initializes a new instance of the class /// @@ -40,6 +43,23 @@ public OptionChain(Symbol canonicalOptionSymbol, DateTime time, bool flatten = t { } + /// + /// Initializes a new instance of the class with the option symbol properties and exchange hours, + /// so the filters don't look them up + /// + /// The symbol for this chain. + /// The time of this chain + /// The option symbol properties + /// The option exchange hours + /// Whether to flatten the data frame + internal OptionChain(Symbol canonicalOptionSymbol, DateTime time, SymbolProperties symbolProperties, SecurityExchangeHours exchangeHours, + bool flatten = true) + : this(canonicalOptionSymbol, time, flatten) + { + _symbolProperties = symbolProperties; + _exchangeHours = exchangeHours; + } + /// /// Initializes a new option chain for a list of contracts as instances /// @@ -47,10 +67,11 @@ public OptionChain(Symbol canonicalOptionSymbol, DateTime time, bool flatten = t /// The time of this chain /// The list of contracts data /// The option symbol properties + /// The option exchange hours /// Whether to flatten the data frame public OptionChain(Symbol canonicalOptionSymbol, DateTime time, IEnumerable contracts, SymbolProperties symbolProperties, - bool flatten = true) - : this(canonicalOptionSymbol, time, flatten) + SecurityExchangeHours exchangeHours = null, bool flatten = true) + : this(canonicalOptionSymbol, time, symbolProperties, exchangeHours, flatten) { var underlyingSet = false; foreach (var contractData in contracts) @@ -62,7 +83,7 @@ public OptionChain(Symbol canonicalOptionSymbol, DateTime time, IEnumerable @@ -81,6 +104,8 @@ private OptionChain(OptionChain other) private OptionChain(OptionChain other, IEnumerable contracts) : base(other, contracts) { + _symbolProperties = other._symbolProperties; + _exchangeHours = other._exchangeHours; } /// diff --git a/Common/Data/Market/OptionContract.cs b/Common/Data/Market/OptionContract.cs index 4006c4c573aa..8ed99273a47f 100644 --- a/Common/Data/Market/OptionContract.cs +++ b/Common/Data/Market/OptionContract.cs @@ -15,6 +15,7 @@ using QuantConnect.Data.UniverseSelection; using QuantConnect.Interfaces; +using QuantConnect.Python; using QuantConnect.Securities; using QuantConnect.Securities.Option; using System; @@ -28,6 +29,8 @@ public class OptionContract : BaseContract { private IOptionData _optionData = OptionPriceModelResultData.Null; private readonly SymbolProperties _symbolProperties; + private readonly SecurityExchangeHours _exchangeHours; + private DateTime? _lastTradingDate; /// /// Gets the strike price @@ -105,9 +108,11 @@ public class OptionContract : BaseContract public decimal UnderlyingLastPrice => _optionData.UnderlyingLastPrice; /// - /// The option symbol properties + /// Calendar days from this contract's time until its last trading date: the previous open day for equity options expiring + /// on a Saturday or holiday, see , the expiration date otherwise /// - internal SymbolProperties SymbolProperties => _symbolProperties; + [PandasIgnore] + public override int DaysToExpiry => ((_lastTradingDate ??= GetLastTradingDate()) - Time.Date).Days; /// /// Initializes a new instance of the class @@ -117,6 +122,7 @@ public OptionContract(ISecurityPrice security) : base(security.Symbol) { _symbolProperties = security.SymbolProperties; + _exchangeHours = security.Exchange.Hours; } /// @@ -124,10 +130,12 @@ public OptionContract(ISecurityPrice security) /// /// The option universe contract data to use as source for this contract /// The contract symbol properties - public OptionContract(OptionUniverse contractData, SymbolProperties symbolProperties) + /// The contract exchange hours + public OptionContract(OptionUniverse contractData, SymbolProperties symbolProperties, SecurityExchangeHours exchangeHours = null) : base(contractData.Symbol) { _symbolProperties = symbolProperties; + _exchangeHours = exchangeHours; _optionData = new OptionUniverseData(contractData); } @@ -174,9 +182,10 @@ public static OptionContract Create(DateTime endTime, ISecurityPrice security, B /// /// The option universe contract data to use as source for this contract /// The contract symbol properties - public static OptionContract Create(OptionUniverse contractData, SymbolProperties symbolProperties) + /// The contract exchange hours + public static OptionContract Create(OptionUniverse contractData, SymbolProperties symbolProperties, SecurityExchangeHours exchangeHours = null) { - var contract = new OptionContract(contractData, symbolProperties) + var contract = new OptionContract(contractData, symbolProperties, exchangeHours) { Time = contractData.EndTime, }; @@ -210,6 +219,22 @@ internal override void Update(BaseData data) #region Option Contract Data Handlers + /// + /// The previous open day for equity options expiring on a Saturday or a holiday, the expiration date otherwise + /// + private DateTime GetLastTradingDate() + { + if (Symbol.SecurityType != SecurityType.Option) + { + return Symbol.ID.Date.Date; + } + + // equity options were dated on the Saturday after their last trading day until the OCC moved expirations to Friday in 2015 + return _exchangeHours == null + ? OptionSymbol.GetLastDayOfTrading(Symbol) + : OptionSymbol.GetLastDayOfTrading(Symbol, _exchangeHours); + } + private interface IOptionData { decimal LastPrice { get; } diff --git a/Common/Interfaces/ISecurityPrice.cs b/Common/Interfaces/ISecurityPrice.cs index 18ec075a9b07..ab01c0403c19 100644 --- a/Common/Interfaces/ISecurityPrice.cs +++ b/Common/Interfaces/ISecurityPrice.cs @@ -77,6 +77,11 @@ public interface ISecurityPrice /// SymbolProperties SymbolProperties { get; } + /// + /// of the symbol + /// + SecurityExchange Exchange { get; } + /// /// Update any security properties based on the latest market data and time /// diff --git a/Common/Securities/ContractSecurityFilterUniverse.cs b/Common/Securities/ContractSecurityFilterUniverse.cs index 7e0356c54832..3ab7c2420847 100644 --- a/Common/Securities/ContractSecurityFilterUniverse.cs +++ b/Common/Securities/ContractSecurityFilterUniverse.cs @@ -340,6 +340,16 @@ protected virtual DateTime AdjustExpirationReferenceDate(DateTime referenceDate) return referenceDate; } + /// + /// Gets the date the given contract stops trading, used by the expiration filters. Defaults to the contract expiration date + /// + /// The contract + /// The contract's last trading date + protected virtual DateTime GetLastTradingDate(TData contract) + { + return contract.Symbol.ID.Date.Date; + } + /// /// Applies filter selecting options contracts based on a range of expiration dates relative to the current day /// @@ -363,7 +373,11 @@ public virtual T Expiration(TimeSpan minExpiry, TimeSpan maxExpiry) var maxExpiryToDate = referenceDate + maxExpiry; Data = Data - .Where(data => data.Symbol.ID.Date.Date >= minExpiryToDate && data.Symbol.ID.Date.Date <= maxExpiryToDate) + .Where(contract => + { + var expiry = GetLastTradingDate(contract); + return expiry >= minExpiryToDate && expiry <= maxExpiryToDate; + }) .ToList(); return (T)this; diff --git a/Common/Securities/Option/OptionChainFilterUniverse.cs b/Common/Securities/Option/OptionChainFilterUniverse.cs index 79ee28623d74..e7994e389155 100644 --- a/Common/Securities/Option/OptionChainFilterUniverse.cs +++ b/Common/Securities/Option/OptionChainFilterUniverse.cs @@ -45,10 +45,13 @@ public class OptionChainFilterUniverse : BaseOptionFilterUniverse class over the contracts of the given chain /// /// The option chain to filter - internal OptionChainFilterUniverse(OptionChain chain) - : base(GetContracts(chain), GetUnderlying(chain), chain.ExchangeTime, GetStrikeMultiplier(chain)) + /// The option symbol properties, if known + /// The option exchange hours, looked up in the market hours database when null + internal OptionChainFilterUniverse(OptionChain chain, SymbolProperties symbolProperties, SecurityExchangeHours exchangeHours) + : base(GetContracts(chain), GetUnderlying(chain), chain.ExchangeTime, symbolProperties?.StrikeMultiplier ?? 1) { _symbol = chain.Symbol; + _exchangeHours = exchangeHours; } /// @@ -91,10 +94,5 @@ private static BaseData GetUnderlying(OptionChain chain) var underlying = chain.Underlying; return underlying != null && underlying.Price != 0 ? underlying : null; } - - private static decimal GetStrikeMultiplier(OptionChain chain) - { - return chain.Contracts.Values.FirstOrDefault()?.SymbolProperties?.StrikeMultiplier ?? 1; - } } } diff --git a/Common/Securities/Option/OptionFilterUniverse.cs b/Common/Securities/Option/OptionFilterUniverse.cs index 53dd5dc95d91..2f0b01a882ea 100644 --- a/Common/Securities/Option/OptionFilterUniverse.cs +++ b/Common/Securities/Option/OptionFilterUniverse.cs @@ -44,6 +44,9 @@ public abstract class BaseOptionFilterUniverse : ContractSecur private bool _refreshUniqueStrikes; private DateTime _lastExchangeDate; private readonly decimal _underlyingScaleFactor = 1; + // the contracts come grouped by expiration, so the last resolved date answers the next contract most of the time + private DateTime _lastExpiry; + private DateTime _lastTradingDate; /// /// The underlying price data @@ -157,6 +160,43 @@ protected override DateTime AdjustExpirationReferenceDate(DateTime referenceDate return referenceDate; } + /// + /// Gets the last trading date of the given contract: the previous open day for equity options expiring on a Saturday + /// or a holiday, see , the expiration date otherwise + /// + /// The contract + /// The date the contract stops trading + protected override DateTime GetLastTradingDate(TData contract) + { + return GetLastTradingDate(contract.Symbol); + } + + /// + /// Gets the last trading date of the given contract, see . Uses the universe + /// exchange hours and keeps the last resolved expiration, since every contract in the universe shares them + /// + /// The contract symbol + /// The date the contract stops trading + protected DateTime GetLastTradingDate(Symbol symbol) + { + var expiry = symbol.ID.Date.Date; + if (symbol.SecurityType != SecurityType.Option) + { + return expiry; + } + + if (expiry != _lastExpiry) + { + // equity options were dated on the Saturday after their last trading day until the OCC moved expirations to Friday in 2015 + _lastTradingDate = ExchangeHours == null + ? OptionSymbol.GetLastDayOfTrading(symbol) + : OptionSymbol.GetLastDayOfTrading(symbol, ExchangeHours); + _lastExpiry = expiry; + } + + return _lastTradingDate; + } + /// /// Applies filter selecting options contracts based on a range of strikes in relative terms /// @@ -1186,7 +1226,7 @@ private TUniverse Ladder(OptionRight right, int minDaysTillExpiry, decimal highe private IEnumerable GetContractsForExpiry(IEnumerable symbols, int minDaysTillExpiry) { var leastExpiryAccepted = _lastExchangeDate.AddDays(minDaysTillExpiry); - return symbols.Where(x => x.ID.Date >= leastExpiryAccepted) + return symbols.Where(x => GetLastTradingDate(x) >= leastExpiryAccepted) .GroupBy(x => x.ID.Date) .OrderBy(x => x.Key) .FirstOrDefault() diff --git a/Common/Securities/Option/OptionSymbol.cs b/Common/Securities/Option/OptionSymbol.cs index cb37fbb84405..02a6dc2dab41 100644 --- a/Common/Securities/Option/OptionSymbol.cs +++ b/Common/Securities/Option/OptionSymbol.cs @@ -94,6 +94,20 @@ public static string MapToUnderlying(string optionTicker, SecurityType securityT /// Option symbol /// public static DateTime GetLastDayOfTrading(Symbol symbol) + { + var exchangeHours = MarketHoursDatabase.FromDataFolder() + .GetEntry(symbol.ID.Market, symbol, symbol.SecurityType) + .ExchangeHours; + return GetLastDayOfTrading(symbol, exchangeHours); + } + + /// + /// Returns the last trading date for the option contract, using the given exchange hours instead of looking them up + /// + /// Option symbol + /// The exchange hours of the option + /// + public static DateTime GetLastDayOfTrading(Symbol symbol, SecurityExchangeHours exchangeHours) { // The OCC proposed rule change: starting from 1 Feb 2015 standard monthly contracts // expire on 3rd Friday, not Saturday following 3rd Friday as it was before. @@ -109,10 +123,6 @@ public static DateTime GetLastDayOfTrading(Symbol symbol) daysBefore--; } - var exchangeHours = MarketHoursDatabase.FromDataFolder() - .GetEntry(symbol.ID.Market, symbol, symbol.SecurityType) - .ExchangeHours; - while (!exchangeHours.IsDateOpen(symbolDateTime.AddDays(daysBefore))) { daysBefore--; diff --git a/Engine/DataFeeds/TimeSliceFactory.cs b/Engine/DataFeeds/TimeSliceFactory.cs index 153db02c58ba..6efc18c7af97 100644 --- a/Engine/DataFeeds/TimeSliceFactory.cs +++ b/Engine/DataFeeds/TimeSliceFactory.cs @@ -428,7 +428,10 @@ private bool HandleOptionData(DateTime algorithmTime, BaseData baseData, OptionC if (!optionChains.TryGetValue(canonical, out chain)) { // the data is already in the exchange time zone, unlike the algorithm time the chain is stamped with - chain = new OptionChain(canonical, algorithmTime) { ExchangeTime = baseData.EndTime }; + chain = new OptionChain(canonical, algorithmTime, security.SymbolProperties, security.Exchange.Hours) + { + ExchangeTime = baseData.EndTime + }; optionChains[canonical] = chain; } diff --git a/Tests/Common/Data/Market/OptionChainTests.cs b/Tests/Common/Data/Market/OptionChainTests.cs index a9242e17b0f0..9b9d5aaa311a 100644 --- a/Tests/Common/Data/Market/OptionChainTests.cs +++ b/Tests/Common/Data/Market/OptionChainTests.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -258,6 +259,141 @@ public void StrikesFilterIsSkippedWithoutUnderlyingPrice() Assert.AreEqual(chain.Count, chain.Strikes(0, 0).Count); } + // Before February 2015 equity options expired on Saturdays, the day after their last trading date. + // Days to expiration are counted on the last trading date, so 2012-02-18 is 0 days out on Friday 2012-02-17, + // and the Saturday after Good Friday 2012-04-06 is 0 days out on Thursday 2012-04-05 + [TestCase("2012-02-17", 0, 0, "2012-02-18")] + [TestCase("2012-02-17", 1, 40, "2012-03-17")] + [TestCase("2012-04-05", 0, 0, "2012-04-07")] + [TestCase("2012-04-05", 1, 60, "2012-05-19")] + public void ExpirationFilterCountsSaturdayExpiriesOnTheirLastTradingDate(string date, int minDays, int maxDays, string expectedExpiry) + { + var (data, underlying) = CreateSaturdayExpiriesData(date); + var expected = DateTime.ParseExact(expectedExpiry, "yyyy-MM-dd", CultureInfo.InvariantCulture); + + var universe = CreateUniverse(data, underlying, underlying.Time).Expiration(minDays, maxDays).ToList(); + var chain = new OptionChain(Canonical, underlying.Time, data, _symbolProperties).Expiration(minDays, maxDays).ToList(); + + Assert.AreEqual(2 * Strikes.Length, universe.Count); + Assert.IsTrue(universe.All(x => x.ID.Date == expected)); + CollectionAssert.AreEquivalent(universe.Select(x => x.Symbol.Value), chain.Select(x => x.Symbol.Value)); + } + + [TestCase("2012-02-17", 0, "2012-02-18")] + [TestCase("2012-02-17", 1, "2012-03-17")] + [TestCase("2012-04-05", 0, "2012-04-07")] + [TestCase("2012-04-05", 1, "2012-05-19")] + public void StrategyFiltersCountSaturdayExpiriesOnTheirLastTradingDate(string date, int minDaysTillExpiry, string expectedExpiry) + { + var (data, underlying) = CreateSaturdayExpiriesData(date); + var expected = DateTime.ParseExact(expectedExpiry, "yyyy-MM-dd", CultureInfo.InvariantCulture); + + var selected = CreateUniverse(data, underlying, underlying.Time).NakedCall(minDaysTillExpiry, 0).ToList(); + + Assert.AreEqual(1, selected.Count); + Assert.AreEqual(expected, selected[0].ID.Date); + Assert.AreEqual(100m, selected[0].ID.StrikePrice); + } + + // Contracts expiring on a holiday stop trading on the previous trading date, so they are zero DTE on that date: + // Good Friday, Thanksgiving, Independence Day and Christmas on their weekday, and Labor Day, a Monday, where the + // previous trading date is the Friday before the weekend + [TestCase("2012-04-05", "2012-04-06")] + [TestCase("2012-11-21", "2012-11-22")] + [TestCase("2014-07-03", "2014-07-04")] + [TestCase("2015-12-24", "2015-12-25")] + [TestCase("2012-08-31", "2012-09-03")] + public void ZeroDteCountsHolidayExpiriesOnThePreviousTradingDate(string lastTradingDate, string holidayExpiry) + { + var date = DateTime.ParseExact(lastTradingDate, "yyyy-MM-dd", CultureInfo.InvariantCulture); + var expiry = DateTime.ParseExact(holidayExpiry, "yyyy-MM-dd", CultureInfo.InvariantCulture); + var previous = date.AddDays(-1); + var expiries = new[] { expiry, expiry.AddDays(28) }; + var (data, underlying) = CreateUniverseData(date, 100m, expiries, Strikes); + var (previousData, previousUnderlying) = CreateUniverseData(previous, 100m, expiries, Strikes); + + // On the last trading date the holiday expiry is zero DTE, on the universe and on the chain + var zeroDte = CreateUniverse(data, underlying, date).ZeroDte().ToList(); + Assert.AreEqual(2 * Strikes.Length, zeroDte.Count); + Assert.IsTrue(zeroDte.All(x => x.ID.Date == expiry)); + var chain = new OptionChain(Canonical, date, data, _symbolProperties); + CollectionAssert.AreEquivalent(zeroDte.Select(x => x.Symbol.Value), chain.ZeroDte().Select(x => x.Symbol.Value)); + + // The day before it is one day out, and nothing expires + Assert.AreEqual(0, CreateUniverse(previousData, previousUnderlying, previous).ZeroDte().Count); + var oneDayOut = CreateUniverse(previousData, previousUnderlying, previous).Expiration(1, 1).ToList(); + Assert.AreEqual(2 * Strikes.Length, oneDayOut.Count); + Assert.IsTrue(oneDayOut.All(x => x.ID.Date == expiry)); + + // Universe rows are stamped at the end of their day, so the contracts built from the previous day's rows + // count their days from the last trading date + var contracts = new OptionChain(Canonical, date, previousData, _symbolProperties).Expiration([expiry]).ToList(); + Assert.AreEqual(2 * Strikes.Length, contracts.Count); + Assert.IsTrue(contracts.All(x => x.Time.Date == date && x.DaysToExpiry == 0)); + } + + [Test] + public void ZeroDteCountsIndexAndFutureOptionsOnTheirExpirationDate() + { + // Index options count on their expiration date, like ZeroDTEIndexOptionsRegressionAlgorithm expects for SPX on its Friday + var spx = Symbol.CreateCanonicalOption(Symbols.SPX, market: QuantConnect.Market.USA); + var spxw = Symbol.CreateCanonicalOption(Symbols.SPX, targetOption: "SPXW", market: QuantConnect.Market.USA); + AssertZeroDteOn(spx, new DateTime(2021, 1, 15), new DateTime(2021, 1, 15), 3800m); + AssertZeroDteOn(spxw, new DateTime(2021, 1, 13), new DateTime(2021, 1, 13), 3800m); + + // Future options trade until their expiration date + var future = Symbol.CreateFuture("ES", QuantConnect.Market.CME, new DateTime(2020, 3, 20)); + AssertZeroDteOn(Symbol.CreateCanonicalOption(future), future.ID.Date, future.ID.Date, 3200m); + } + + private static void AssertZeroDteOn(Symbol canonical, DateTime expiry, DateTime lastTradingDate, decimal strike) + { + var contracts = new[] { OptionRight.Call, OptionRight.Put } + .Select(right => Symbol.CreateOption(canonical.Underlying, canonical.ID.Symbol, canonical.ID.Market, canonical.ID.OptionStyle, right, strike, expiry)) + .Select(symbol => (symbol, 100m, 0.15m, new Greeks(0.5m, 0.01m, 5, -0.5m * 365m, 1, 0))) + .ToList(); + var previous = lastTradingDate.AddDays(-1); + var (data, underlying) = CreateUniverseData(canonical, lastTradingDate, strike, contracts); + var (previousData, previousUnderlying) = CreateUniverseData(canonical, previous, strike, contracts); + var symbolProperties = SymbolPropertiesDatabase.FromDataFolder().GetSymbolProperties(canonical.ID.Market, canonical, canonical.SecurityType, Currencies.USD); + OptionFilterUniverse Universe(List rows, BaseData spot, DateTime date) + { + var universe = new OptionFilterUniverse(CreateOption(canonical), rows, spot); + universe.Refresh(rows, spot, date); + return universe; + } + + // Zero DTE on the last trading date, on the universe and on the chain, one day out the day before + Assert.AreEqual(2, Universe(data, underlying, lastTradingDate).ZeroDte().Count, $"{canonical} zero DTE on {lastTradingDate:yyyy-MM-dd}"); + Assert.AreEqual(2, new OptionChain(canonical, lastTradingDate, data, symbolProperties).ZeroDte().Count); + Assert.AreEqual(0, Universe(previousData, previousUnderlying, previous).ZeroDte().Count); + Assert.AreEqual(2, Universe(previousData, previousUnderlying, previous).Expiration(1, 1).Count); + if (lastTradingDate != expiry) + { + Assert.AreEqual(0, Universe(data, underlying, expiry).ZeroDte().Count, $"{canonical} no longer trades on {expiry:yyyy-MM-dd}"); + } + + // Universe rows are stamped at the end of their day, so the contracts built from the previous day's rows count from the last trading date + var chain = new OptionChain(canonical, lastTradingDate, previousData, symbolProperties); + Assert.AreEqual(2, chain.Count); + Assert.IsTrue(chain.All(x => x.Time.Date == lastTradingDate && x.DaysToExpiry == 0)); + } + + [Test] + public void ChainBuiltWithExchangeHoursCountsSaturdayExpiriesOnTheFriday() + { + // 2012-02-17 is the Friday before a Saturday expiration: the walk-back runs on the hours the chain was built with + var date = new DateTime(2012, 2, 17); + var (data, _) = CreateSaturdayExpiriesData("2012-02-16"); + var chain = new OptionChain(Canonical, date, data, _symbolProperties, CreateOption().Exchange.Hours); + + var zeroDte = chain.ZeroDte(); + Assert.AreEqual(2 * Strikes.Length, zeroDte.Count); + Assert.IsTrue(zeroDte.All(x => x.Expiry == new DateTime(2012, 2, 18) && x.DaysToExpiry == 0)); + // the filtered chains carry the hours too + Assert.AreEqual(Strikes.Length, zeroDte.PutsOnly().ZeroDte().Count); + } + [Test] public void FiltersAreAvailableFromPython() { @@ -569,13 +705,19 @@ private OptionChain CreateChain() return new OptionChain(Canonical, Date, _data, _symbolProperties); } + private (List, BaseData) CreateSaturdayExpiriesData(string date) + { + var expiries = new[] { new DateTime(2012, 2, 18), new DateTime(2012, 3, 17), new DateTime(2012, 4, 7), new DateTime(2012, 5, 19) }; + return CreateUniverseData(DateTime.ParseExact(date, "yyyy-MM-dd", CultureInfo.InvariantCulture), 100m, expiries, Strikes); + } + private static Option CreateOption(Symbol canonical = null) { canonical ??= Canonical; var exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(canonical.ID.Market, canonical, canonical.SecurityType); return new Option( exchangeHours, - new SubscriptionDataConfig(typeof(TradeBar), canonical, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, false, false), + new SubscriptionDataConfig(typeof(TradeBar), canonical, Resolution.Minute, exchangeHours.TimeZone, exchangeHours.TimeZone, true, false, false), new Cash(Currencies.USD, 0, 1m), new OptionSymbolProperties(SymbolProperties.GetDefault(Currencies.USD)), ErrorCurrencyConverter.Instance, diff --git a/Tests/Common/Securities/OptionFilterTests.cs b/Tests/Common/Securities/OptionFilterTests.cs index 8e89f0b55fcd..d0b5549691e8 100644 --- a/Tests/Common/Securities/OptionFilterTests.cs +++ b/Tests/Common/Securities/OptionFilterTests.cs @@ -318,12 +318,15 @@ public void FiltersExpiryRange() var data = symbols.Select(x => new OptionUniverse() { Symbol = x }); var filterUniverse = new OptionFilterUniverse(option, data.ToList(), underlying); var filtered = filter.Filter(filterUniverse).ToList(); - Assert.AreEqual(5, filtered.Count); + // 2016-02-26 is a Friday: the weekend expiries 8 and 9 days out count on their last trading date, Friday 7 days out + Assert.AreEqual(7, filtered.Count); Assert.AreEqual(symbols[3], filtered[0].Symbol); Assert.AreEqual(symbols[4], filtered[1].Symbol); Assert.AreEqual(symbols[5], filtered[2].Symbol); Assert.AreEqual(symbols[6], filtered[3].Symbol); Assert.AreEqual(symbols[7], filtered[4].Symbol); + Assert.AreEqual(symbols[8], filtered[5].Symbol); + Assert.AreEqual(symbols[9], filtered[6].Symbol); } [Test]