From d947396b7c42483f05a0a8308ab6679d065f60cc Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 8 Sep 2026 16:46:58 -0400 Subject: [PATCH 01/12] Count days to the last trading date in the chain pickers at(), closest_expiry(), select() and days_to_expiry count Saturday and holiday expiries on the previous trading day, matching the shared filters. --- Common/Data/Market/OptionContract.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Common/Data/Market/OptionContract.cs b/Common/Data/Market/OptionContract.cs index 4006c4c573aa..3d5875932826 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,7 @@ public class OptionContract : BaseContract { private IOptionData _optionData = OptionPriceModelResultData.Null; private readonly SymbolProperties _symbolProperties; + private DateTime? _lastTradingDate; /// /// Gets the strike price @@ -104,6 +106,13 @@ public class OptionContract : BaseContract /// public decimal UnderlyingLastPrice => _optionData.UnderlyingLastPrice; + /// + /// Calendar days from this contract's time until its last trading date, the previous trading day + /// for expirations on a Saturday or holiday + /// + [PandasIgnore] + public override int DaysToExpiry => ((_lastTradingDate ??= OptionSymbol.GetLastDayOfTrading(Symbol)) - Time.Date).Days; + /// /// The option symbol properties /// From ca35b254d2b1be7902b16953ed70e66dd1809894 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 8 Sep 2026 16:46:59 -0400 Subject: [PATCH 02/12] Count expirations on their last trading date Equity options listed before February 2015 carry the OCC Saturday expiration date but stop trading on the Friday, and some weeklies are listed on holidays. ContractSecurityFilterUniverse.GetLastTradingDate feeds Expiration(), the option base moves non trading dates back to the previous open day, memoized per date, and the strategy pickers use the same date, so expiration(0, 0) selects a 0 DTE contract on its last trading day. --- .../ContractSecurityFilterUniverse.cs | 16 ++++++- .../Securities/Option/OptionFilterUniverse.cs | 42 +++++++++++++++++- Tests/Common/Data/Market/OptionChainTests.cs | 43 +++++++++++++++++++ Tests/Common/Securities/OptionFilterTests.cs | 5 ++- 4 files changed, 103 insertions(+), 3 deletions(-) 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/OptionFilterUniverse.cs b/Common/Securities/Option/OptionFilterUniverse.cs index 53dd5dc95d91..e7c73aeabaa2 100644 --- a/Common/Securities/Option/OptionFilterUniverse.cs +++ b/Common/Securities/Option/OptionFilterUniverse.cs @@ -44,6 +44,7 @@ public abstract class BaseOptionFilterUniverse : ContractSecur private bool _refreshUniqueStrikes; private DateTime _lastExchangeDate; private readonly decimal _underlyingScaleFactor = 1; + private readonly Dictionary _lastTradingDates = new(); /// /// The underlying price data @@ -157,6 +158,45 @@ protected override DateTime AdjustExpirationReferenceDate(DateTime referenceDate return referenceDate; } + /// + /// Gets the last trading date of the given contract. Expirations falling on a non trading day, like the + /// Saturday expirations of equity options before February 2015, are moved back to the previous trading day + /// + /// The contract + /// The date the contract stops trading + protected override DateTime GetLastTradingDate(TData contract) + { + return GetLastTradingDate(contract.Symbol.ID.Date); + } + + /// + /// Gets the last trading date for the given expiration date. Expirations falling on a non trading day, like the + /// Saturday expirations of equity options before February 2015, are moved back to the previous trading day + /// + /// The contract expiration date + /// The date the contract stops trading + protected DateTime GetLastTradingDate(DateTime expiry) + { + var date = expiry.Date; + if (ExchangeHours == null) + { + return date; + } + + if (!_lastTradingDates.TryGetValue(date, out var lastTradingDate)) + { + lastTradingDate = date; + // bounded so a closed exchange calendar can't make this loop forever + for (var i = 0; i < 7 && !ExchangeHours.IsDateOpen(lastTradingDate); i++) + { + lastTradingDate = lastTradingDate.AddDays(-1); + } + _lastTradingDates[date] = lastTradingDate; + } + + 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.ID.Date) >= leastExpiryAccepted) .GroupBy(x => x.ID.Date) .OrderBy(x => x.Key) .FirstOrDefault() diff --git a/Tests/Common/Data/Market/OptionChainTests.cs b/Tests/Common/Data/Market/OptionChainTests.cs index a9242e17b0f0..65c28bdeba21 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,42 @@ 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); + } + [Test] public void FiltersAreAvailableFromPython() { @@ -569,6 +606,12 @@ 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; 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] From 56a4e18d2f696cf0c595bad41b1b18893710aa16 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 14 Sep 2026 18:27:39 -0400 Subject: [PATCH 03/12] Use OptionSymbol.GetLastDayOfTrading for the last trading date and cover holiday expiries --- .../Securities/Option/OptionFilterUniverse.cs | 32 +++++----------- Tests/Common/Data/Market/OptionChainTests.cs | 37 +++++++++++++++++++ 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/Common/Securities/Option/OptionFilterUniverse.cs b/Common/Securities/Option/OptionFilterUniverse.cs index e7c73aeabaa2..edc511d1ce1f 100644 --- a/Common/Securities/Option/OptionFilterUniverse.cs +++ b/Common/Securities/Option/OptionFilterUniverse.cs @@ -159,39 +159,27 @@ protected override DateTime AdjustExpirationReferenceDate(DateTime referenceDate } /// - /// Gets the last trading date of the given contract. Expirations falling on a non trading day, like the - /// Saturday expirations of equity options before February 2015, are moved back to the previous trading day + /// Gets the last trading date of the given contract, see /// /// The contract /// The date the contract stops trading protected override DateTime GetLastTradingDate(TData contract) { - return GetLastTradingDate(contract.Symbol.ID.Date); + return GetLastTradingDate(contract.Symbol); } /// - /// Gets the last trading date for the given expiration date. Expirations falling on a non trading day, like the - /// Saturday expirations of equity options before February 2015, are moved back to the previous trading day + /// Gets the last trading date of the given contract, see . Cached per + /// expiration date, since every contract in the universe shares the exchange hours /// - /// The contract expiration date + /// The contract symbol /// The date the contract stops trading - protected DateTime GetLastTradingDate(DateTime expiry) + protected DateTime GetLastTradingDate(Symbol symbol) { - var date = expiry.Date; - if (ExchangeHours == null) + var expiry = symbol.ID.Date.Date; + if (!_lastTradingDates.TryGetValue(expiry, out var lastTradingDate)) { - return date; - } - - if (!_lastTradingDates.TryGetValue(date, out var lastTradingDate)) - { - lastTradingDate = date; - // bounded so a closed exchange calendar can't make this loop forever - for (var i = 0; i < 7 && !ExchangeHours.IsDateOpen(lastTradingDate); i++) - { - lastTradingDate = lastTradingDate.AddDays(-1); - } - _lastTradingDates[date] = lastTradingDate; + _lastTradingDates[expiry] = lastTradingDate = OptionSymbol.GetLastDayOfTrading(symbol); } return lastTradingDate; @@ -1226,7 +1214,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 => GetLastTradingDate(x.ID.Date) >= leastExpiryAccepted) + return symbols.Where(x => GetLastTradingDate(x) >= leastExpiryAccepted) .GroupBy(x => x.ID.Date) .OrderBy(x => x.Key) .FirstOrDefault() diff --git a/Tests/Common/Data/Market/OptionChainTests.cs b/Tests/Common/Data/Market/OptionChainTests.cs index 65c28bdeba21..7abb13a2afa5 100644 --- a/Tests/Common/Data/Market/OptionChainTests.cs +++ b/Tests/Common/Data/Market/OptionChainTests.cs @@ -295,6 +295,43 @@ public void StrategyFiltersCountSaturdayExpiriesOnTheirLastTradingDate(string da 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 FiltersAreAvailableFromPython() { From d5d7b0c1bf09659bd83a911df33611255bb8ce12 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 15 Sep 2026 08:49:40 -0400 Subject: [PATCH 04/12] Resolve the last trading date per option type, index options stop trading the day before their AM settlement --- Common/Data/Market/OptionContract.cs | 6 +-- Common/Extensions.cs | 23 +++++++++ .../Securities/Option/OptionFilterUniverse.cs | 8 +-- Tests/Common/Data/Market/OptionChainTests.cs | 49 ++++++++++++++++++- 4 files changed, 78 insertions(+), 8 deletions(-) diff --git a/Common/Data/Market/OptionContract.cs b/Common/Data/Market/OptionContract.cs index 3d5875932826..a5b01f14cb83 100644 --- a/Common/Data/Market/OptionContract.cs +++ b/Common/Data/Market/OptionContract.cs @@ -107,11 +107,11 @@ public class OptionContract : BaseContract public decimal UnderlyingLastPrice => _optionData.UnderlyingLastPrice; /// - /// Calendar days from this contract's time until its last trading date, the previous trading day - /// for expirations on a Saturday or holiday + /// Calendar days from this contract's time until its last trading date, see : + /// the previous open day for expirations on a Saturday or holiday, the day before for index options that settle in the morning /// [PandasIgnore] - public override int DaysToExpiry => ((_lastTradingDate ??= OptionSymbol.GetLastDayOfTrading(Symbol)) - Time.Date).Days; + public override int DaysToExpiry => ((_lastTradingDate ??= Symbol.GetLastTradingDate()) - Time.Date).Days; /// /// The option symbol properties diff --git a/Common/Extensions.cs b/Common/Extensions.cs index be982b6c8f16..83401fd44d35 100644 --- a/Common/Extensions.cs +++ b/Common/Extensions.cs @@ -56,6 +56,7 @@ using QuantConnect.Exceptions; using QuantConnect.Securities.Future; using QuantConnect.Securities.FutureOption; +using QuantConnect.Securities.IndexOption; using QuantConnect.Securities.Option; using QuantConnect.Statistics; using Newtonsoft.Json.Linq; @@ -3595,6 +3596,28 @@ public static DateTime GetDelistingDate(this Symbol symbol, MapFile mapFile = nu } } + /// + /// Gets the last trading date of the given option contract: the previous open day for equity options dated on a Saturday + /// or a holiday, the day before the expiration for the index options that settle in the morning, like SPX, and the + /// expiration date otherwise + /// + /// The option contract symbol + /// The date the contract stops trading + public static DateTime GetLastTradingDate(this Symbol symbol) + { + switch (symbol.ID.SecurityType) + { + case SecurityType.Option: + return OptionSymbol.GetLastDayOfTrading(symbol); + case SecurityType.IndexOption: + return IndexOptionSymbol.GetLastTradingDate(symbol.ID.Symbol, symbol.ID.Date.Date); + case SecurityType.FutureOption: + return FutureOptionSymbol.GetLastDayOfTrading(symbol); + default: + return symbol.ID.Date.Date; + } + } + /// /// Helper method to determine if a given symbol is of custom data /// diff --git a/Common/Securities/Option/OptionFilterUniverse.cs b/Common/Securities/Option/OptionFilterUniverse.cs index edc511d1ce1f..518a307273cc 100644 --- a/Common/Securities/Option/OptionFilterUniverse.cs +++ b/Common/Securities/Option/OptionFilterUniverse.cs @@ -159,7 +159,7 @@ protected override DateTime AdjustExpirationReferenceDate(DateTime referenceDate } /// - /// Gets the last trading date of the given contract, see + /// Gets the last trading date of the given contract, see /// /// The contract /// The date the contract stops trading @@ -169,8 +169,8 @@ protected override DateTime GetLastTradingDate(TData contract) } /// - /// Gets the last trading date of the given contract, see . Cached per - /// expiration date, since every contract in the universe shares the exchange hours + /// Gets the last trading date of the given contract, see . Cached per + /// expiration date, since every contract in the universe shares the option ticker and exchange hours /// /// The contract symbol /// The date the contract stops trading @@ -179,7 +179,7 @@ protected DateTime GetLastTradingDate(Symbol symbol) var expiry = symbol.ID.Date.Date; if (!_lastTradingDates.TryGetValue(expiry, out var lastTradingDate)) { - _lastTradingDates[expiry] = lastTradingDate = OptionSymbol.GetLastDayOfTrading(symbol); + _lastTradingDates[expiry] = lastTradingDate = symbol.GetLastTradingDate(); } return lastTradingDate; diff --git a/Tests/Common/Data/Market/OptionChainTests.cs b/Tests/Common/Data/Market/OptionChainTests.cs index 7abb13a2afa5..0a34794e2623 100644 --- a/Tests/Common/Data/Market/OptionChainTests.cs +++ b/Tests/Common/Data/Market/OptionChainTests.cs @@ -332,6 +332,53 @@ public void ZeroDteCountsHolidayExpiriesOnThePreviousTradingDate(string lastTrad Assert.IsTrue(contracts.All(x => x.Time.Date == date && x.DaysToExpiry == 0)); } + [Test] + public void ZeroDteCountsIndexAndFutureOptionsOnTheirLastTradingDate() + { + // SPX settles in the morning of its Friday expiration, so it stops trading on the Thursday; SPXW trades until its expiration + 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, 14), 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 FiltersAreAvailableFromPython() { @@ -655,7 +702,7 @@ private static Option CreateOption(Symbol canonical = null) 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, From 5655616f99329fa3afbf924c2f5e65459c848d1c Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 15 Sep 2026 09:07:08 -0400 Subject: [PATCH 05/12] Keep index options trading until their expiration date, as the 0DTE index option selection expects --- Common/Data/Market/OptionContract.cs | 2 +- Common/Extensions.cs | 9 +++------ Tests/Common/Data/Market/OptionChainTests.cs | 6 +++--- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Common/Data/Market/OptionContract.cs b/Common/Data/Market/OptionContract.cs index a5b01f14cb83..90337a19d379 100644 --- a/Common/Data/Market/OptionContract.cs +++ b/Common/Data/Market/OptionContract.cs @@ -108,7 +108,7 @@ public class OptionContract : BaseContract /// /// Calendar days from this contract's time until its last trading date, see : - /// the previous open day for expirations on a Saturday or holiday, the day before for index options that settle in the morning + /// the previous open day for expirations on a Saturday or holiday /// [PandasIgnore] public override int DaysToExpiry => ((_lastTradingDate ??= Symbol.GetLastTradingDate()) - Time.Date).Days; diff --git a/Common/Extensions.cs b/Common/Extensions.cs index 83401fd44d35..a7eafd5c862f 100644 --- a/Common/Extensions.cs +++ b/Common/Extensions.cs @@ -56,7 +56,6 @@ using QuantConnect.Exceptions; using QuantConnect.Securities.Future; using QuantConnect.Securities.FutureOption; -using QuantConnect.Securities.IndexOption; using QuantConnect.Securities.Option; using QuantConnect.Statistics; using Newtonsoft.Json.Linq; @@ -3597,9 +3596,8 @@ public static DateTime GetDelistingDate(this Symbol symbol, MapFile mapFile = nu } /// - /// Gets the last trading date of the given option contract: the previous open day for equity options dated on a Saturday - /// or a holiday, the day before the expiration for the index options that settle in the morning, like SPX, and the - /// expiration date otherwise + /// Gets the last trading date of the given option contract: the previous open day for equity and index options dated on a + /// Saturday or a holiday, see , and the expiration date for future options /// /// The option contract symbol /// The date the contract stops trading @@ -3608,9 +3606,8 @@ public static DateTime GetLastTradingDate(this Symbol symbol) switch (symbol.ID.SecurityType) { case SecurityType.Option: - return OptionSymbol.GetLastDayOfTrading(symbol); case SecurityType.IndexOption: - return IndexOptionSymbol.GetLastTradingDate(symbol.ID.Symbol, symbol.ID.Date.Date); + return OptionSymbol.GetLastDayOfTrading(symbol); case SecurityType.FutureOption: return FutureOptionSymbol.GetLastDayOfTrading(symbol); default: diff --git a/Tests/Common/Data/Market/OptionChainTests.cs b/Tests/Common/Data/Market/OptionChainTests.cs index 0a34794e2623..0a32691f5a9e 100644 --- a/Tests/Common/Data/Market/OptionChainTests.cs +++ b/Tests/Common/Data/Market/OptionChainTests.cs @@ -333,12 +333,12 @@ public void ZeroDteCountsHolidayExpiriesOnThePreviousTradingDate(string lastTrad } [Test] - public void ZeroDteCountsIndexAndFutureOptionsOnTheirLastTradingDate() + public void ZeroDteCountsIndexAndFutureOptionsOnTheirExpirationDate() { - // SPX settles in the morning of its Friday expiration, so it stops trading on the Thursday; SPXW trades until its expiration + // 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, 14), 3800m); + 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 From 722b2c761011822fe5dff8006266c9ea405a92c9 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 15 Sep 2026 09:27:03 -0400 Subject: [PATCH 06/12] Index options stop trading the business day before their AM settlement, as the exchange and the brokerage mapping do --- .../ZeroDTEIndexOptionsRegressionAlgorithm.cs | 9 +++++---- Common/Data/Market/OptionContract.cs | 3 ++- Common/Extensions.cs | 13 ++++++++++--- Tests/Common/Data/Market/OptionChainTests.cs | 8 +++++--- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Algorithm.CSharp/ZeroDTEIndexOptionsRegressionAlgorithm.cs b/Algorithm.CSharp/ZeroDTEIndexOptionsRegressionAlgorithm.cs index f2e2696c421d..69e7a4bb485c 100644 --- a/Algorithm.CSharp/ZeroDTEIndexOptionsRegressionAlgorithm.cs +++ b/Algorithm.CSharp/ZeroDTEIndexOptionsRegressionAlgorithm.cs @@ -27,8 +27,9 @@ public class ZeroDTEIndexOptionsRegressionAlgorithm : ZeroDTEOptionsRegressionAl { public override void Initialize() { - SetStartDate(2021, 01, 15); - SetEndDate(2021, 01, 15); + // The SPX contracts dated Friday the 15th stop trading on Thursday the 14th, their last 0DTE day + SetStartDate(2021, 01, 14); + SetEndDate(2021, 01, 14); SetCash(100000); var index = AddIndex("SPX"); @@ -42,14 +43,14 @@ public override void Initialize() _selectionDays = new List() { - new DateTime(2021, 01, 15), + new DateTime(2021, 01, 14), }; } /// /// Data Points count of all timeslices of algorithm /// - public override long DataPoints => 27; + public override long DataPoints => 6483; /// /// Data Points count of the algorithm history diff --git a/Common/Data/Market/OptionContract.cs b/Common/Data/Market/OptionContract.cs index 90337a19d379..cac530478ef4 100644 --- a/Common/Data/Market/OptionContract.cs +++ b/Common/Data/Market/OptionContract.cs @@ -108,7 +108,8 @@ public class OptionContract : BaseContract /// /// Calendar days from this contract's time until its last trading date, see : - /// the previous open day for expirations on a Saturday or holiday + /// the previous open day for expirations on a Saturday or holiday, the business day before for index options that settle + /// in the morning /// [PandasIgnore] public override int DaysToExpiry => ((_lastTradingDate ??= Symbol.GetLastTradingDate()) - Time.Date).Days; diff --git a/Common/Extensions.cs b/Common/Extensions.cs index a7eafd5c862f..625b15e5d76e 100644 --- a/Common/Extensions.cs +++ b/Common/Extensions.cs @@ -56,6 +56,7 @@ using QuantConnect.Exceptions; using QuantConnect.Securities.Future; using QuantConnect.Securities.FutureOption; +using QuantConnect.Securities.IndexOption; using QuantConnect.Securities.Option; using QuantConnect.Statistics; using Newtonsoft.Json.Linq; @@ -3596,8 +3597,10 @@ public static DateTime GetDelistingDate(this Symbol symbol, MapFile mapFile = nu } /// - /// Gets the last trading date of the given option contract: the previous open day for equity and index options dated on a - /// Saturday or a holiday, see , and the expiration date for future options + /// Gets the last trading date of the given option contract: the previous open day for equity options dated on a Saturday + /// or a holiday, see ; the business day before the expiration for the index + /// options that settle in the morning, like SPX, see ; and the + /// expiration date for future options /// /// The option contract symbol /// The date the contract stops trading @@ -3606,8 +3609,12 @@ public static DateTime GetLastTradingDate(this Symbol symbol) switch (symbol.ID.SecurityType) { case SecurityType.Option: - case SecurityType.IndexOption: return OptionSymbol.GetLastDayOfTrading(symbol); + case SecurityType.IndexOption: + var lastTradingDate = IndexOptionSymbol.GetLastTradingDate(symbol.ID.Symbol, symbol.ID.Date.Date); + // the exchange moves it to the preceding business day when it falls on a holiday + var exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType); + return exchangeHours.GetPreviousTradingDay(lastTradingDate.AddDays(1)); case SecurityType.FutureOption: return FutureOptionSymbol.GetLastDayOfTrading(symbol); default: diff --git a/Tests/Common/Data/Market/OptionChainTests.cs b/Tests/Common/Data/Market/OptionChainTests.cs index 0a32691f5a9e..5fb1436940d8 100644 --- a/Tests/Common/Data/Market/OptionChainTests.cs +++ b/Tests/Common/Data/Market/OptionChainTests.cs @@ -333,12 +333,14 @@ public void ZeroDteCountsHolidayExpiriesOnThePreviousTradingDate(string lastTrad } [Test] - public void ZeroDteCountsIndexAndFutureOptionsOnTheirExpirationDate() + public void ZeroDteCountsIndexAndFutureOptionsOnTheirLastTradingDate() { - // Index options count on their expiration date, like ZeroDTEIndexOptionsRegressionAlgorithm expects for SPX on its Friday + // SPX settles in the morning of its Friday expiration, so it stops trading on the Thursday, or the Wednesday when the + // Thursday is a holiday, like Thanksgiving; SPXW settles in the afternoon and trades until its expiration 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(spx, new DateTime(2021, 1, 15), new DateTime(2021, 1, 14), 3800m); + AssertZeroDteOn(spx, new DateTime(2020, 11, 27), new DateTime(2020, 11, 25), 3600m); AssertZeroDteOn(spxw, new DateTime(2021, 1, 13), new DateTime(2021, 1, 13), 3800m); // Future options trade until their expiration date From 67858a96b389f731a0cbaf56b4b848674f901b71 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 15 Sep 2026 15:26:59 -0400 Subject: [PATCH 07/12] Walk back to the last trading date for equity options only, reusing the universe exchange hours --- .../ZeroDTEIndexOptionsRegressionAlgorithm.cs | 9 +++---- Common/Data/Market/OptionContract.cs | 16 ++++++++--- Common/Extensions.cs | 27 ------------------- .../Securities/Option/OptionFilterUniverse.cs | 18 ++++++++++--- Common/Securities/Option/OptionSymbol.cs | 18 ++++++++++--- Tests/Common/Data/Market/OptionChainTests.cs | 8 +++--- 6 files changed, 47 insertions(+), 49 deletions(-) diff --git a/Algorithm.CSharp/ZeroDTEIndexOptionsRegressionAlgorithm.cs b/Algorithm.CSharp/ZeroDTEIndexOptionsRegressionAlgorithm.cs index 69e7a4bb485c..f2e2696c421d 100644 --- a/Algorithm.CSharp/ZeroDTEIndexOptionsRegressionAlgorithm.cs +++ b/Algorithm.CSharp/ZeroDTEIndexOptionsRegressionAlgorithm.cs @@ -27,9 +27,8 @@ public class ZeroDTEIndexOptionsRegressionAlgorithm : ZeroDTEOptionsRegressionAl { public override void Initialize() { - // The SPX contracts dated Friday the 15th stop trading on Thursday the 14th, their last 0DTE day - SetStartDate(2021, 01, 14); - SetEndDate(2021, 01, 14); + SetStartDate(2021, 01, 15); + SetEndDate(2021, 01, 15); SetCash(100000); var index = AddIndex("SPX"); @@ -43,14 +42,14 @@ public override void Initialize() _selectionDays = new List() { - new DateTime(2021, 01, 14), + new DateTime(2021, 01, 15), }; } /// /// Data Points count of all timeslices of algorithm /// - public override long DataPoints => 6483; + public override long DataPoints => 27; /// /// Data Points count of the algorithm history diff --git a/Common/Data/Market/OptionContract.cs b/Common/Data/Market/OptionContract.cs index cac530478ef4..cefb7e892ce6 100644 --- a/Common/Data/Market/OptionContract.cs +++ b/Common/Data/Market/OptionContract.cs @@ -107,12 +107,11 @@ public class OptionContract : BaseContract public decimal UnderlyingLastPrice => _optionData.UnderlyingLastPrice; /// - /// Calendar days from this contract's time until its last trading date, see : - /// the previous open day for expirations on a Saturday or holiday, the business day before for index options that settle - /// in the morning + /// 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 /// [PandasIgnore] - public override int DaysToExpiry => ((_lastTradingDate ??= Symbol.GetLastTradingDate()) - Time.Date).Days; + public override int DaysToExpiry => ((_lastTradingDate ??= GetLastTradingDate()) - Time.Date).Days; /// /// The option symbol properties @@ -220,6 +219,15 @@ 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() + { + // equity options were dated on the Saturday after their last trading day until the OCC moved expirations to Friday in 2015 + return Symbol.SecurityType == SecurityType.Option ? OptionSymbol.GetLastDayOfTrading(Symbol) : Symbol.ID.Date.Date; + } + private interface IOptionData { decimal LastPrice { get; } diff --git a/Common/Extensions.cs b/Common/Extensions.cs index 625b15e5d76e..be982b6c8f16 100644 --- a/Common/Extensions.cs +++ b/Common/Extensions.cs @@ -56,7 +56,6 @@ using QuantConnect.Exceptions; using QuantConnect.Securities.Future; using QuantConnect.Securities.FutureOption; -using QuantConnect.Securities.IndexOption; using QuantConnect.Securities.Option; using QuantConnect.Statistics; using Newtonsoft.Json.Linq; @@ -3596,32 +3595,6 @@ public static DateTime GetDelistingDate(this Symbol symbol, MapFile mapFile = nu } } - /// - /// Gets the last trading date of the given option contract: the previous open day for equity options dated on a Saturday - /// or a holiday, see ; the business day before the expiration for the index - /// options that settle in the morning, like SPX, see ; and the - /// expiration date for future options - /// - /// The option contract symbol - /// The date the contract stops trading - public static DateTime GetLastTradingDate(this Symbol symbol) - { - switch (symbol.ID.SecurityType) - { - case SecurityType.Option: - return OptionSymbol.GetLastDayOfTrading(symbol); - case SecurityType.IndexOption: - var lastTradingDate = IndexOptionSymbol.GetLastTradingDate(symbol.ID.Symbol, symbol.ID.Date.Date); - // the exchange moves it to the preceding business day when it falls on a holiday - var exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType); - return exchangeHours.GetPreviousTradingDay(lastTradingDate.AddDays(1)); - case SecurityType.FutureOption: - return FutureOptionSymbol.GetLastDayOfTrading(symbol); - default: - return symbol.ID.Date.Date; - } - } - /// /// Helper method to determine if a given symbol is of custom data /// diff --git a/Common/Securities/Option/OptionFilterUniverse.cs b/Common/Securities/Option/OptionFilterUniverse.cs index 518a307273cc..59c678f3c18b 100644 --- a/Common/Securities/Option/OptionFilterUniverse.cs +++ b/Common/Securities/Option/OptionFilterUniverse.cs @@ -159,7 +159,8 @@ protected override DateTime AdjustExpirationReferenceDate(DateTime referenceDate } /// - /// Gets the last trading date of the given contract, see + /// 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 @@ -169,17 +170,26 @@ protected override DateTime GetLastTradingDate(TData contract) } /// - /// Gets the last trading date of the given contract, see . Cached per - /// expiration date, since every contract in the universe shares the option ticker and exchange hours + /// Gets the last trading date of the given contract, see . Uses the universe + /// exchange hours and caches per expiration date, 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 (!_lastTradingDates.TryGetValue(expiry, out var lastTradingDate)) { - _lastTradingDates[expiry] = lastTradingDate = symbol.GetLastTradingDate(); + // 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); + _lastTradingDates[expiry] = lastTradingDate; } return lastTradingDate; diff --git a/Common/Securities/Option/OptionSymbol.cs b/Common/Securities/Option/OptionSymbol.cs index cb37fbb84405..b2014c4447ac 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/Tests/Common/Data/Market/OptionChainTests.cs b/Tests/Common/Data/Market/OptionChainTests.cs index 5fb1436940d8..0a32691f5a9e 100644 --- a/Tests/Common/Data/Market/OptionChainTests.cs +++ b/Tests/Common/Data/Market/OptionChainTests.cs @@ -333,14 +333,12 @@ public void ZeroDteCountsHolidayExpiriesOnThePreviousTradingDate(string lastTrad } [Test] - public void ZeroDteCountsIndexAndFutureOptionsOnTheirLastTradingDate() + public void ZeroDteCountsIndexAndFutureOptionsOnTheirExpirationDate() { - // SPX settles in the morning of its Friday expiration, so it stops trading on the Thursday, or the Wednesday when the - // Thursday is a holiday, like Thanksgiving; SPXW settles in the afternoon and trades until its expiration + // 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, 14), 3800m); - AssertZeroDteOn(spx, new DateTime(2020, 11, 27), new DateTime(2020, 11, 25), 3600m); + 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 From b2dabf253ce07cc9a1c4f845a7e92d09f751f83a Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 15 Sep 2026 15:28:48 -0400 Subject: [PATCH 08/12] Indentation --- Common/Securities/Option/OptionSymbol.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Common/Securities/Option/OptionSymbol.cs b/Common/Securities/Option/OptionSymbol.cs index b2014c4447ac..02a6dc2dab41 100644 --- a/Common/Securities/Option/OptionSymbol.cs +++ b/Common/Securities/Option/OptionSymbol.cs @@ -96,8 +96,8 @@ public static string MapToUnderlying(string optionTicker, SecurityType securityT public static DateTime GetLastDayOfTrading(Symbol symbol) { var exchangeHours = MarketHoursDatabase.FromDataFolder() - .GetEntry(symbol.ID.Market, symbol, symbol.SecurityType) - .ExchangeHours; + .GetEntry(symbol.ID.Market, symbol, symbol.SecurityType) + .ExchangeHours; return GetLastDayOfTrading(symbol, exchangeHours); } From 343d41b4d96ee86b4d23de74462da0694f2a276c Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 15 Sep 2026 18:15:54 -0400 Subject: [PATCH 09/12] Carry the exchange hours down to the chain filters and contracts, keep the last resolved expiration --- Algorithm/QCAlgorithm.cs | 3 ++- Common/Data/Market/BaseChain.cs | 7 +++++++ Common/Data/Market/OptionChain.cs | 6 ++++-- Common/Data/Market/OptionContract.cs | 20 +++++++++++++++---- .../Option/OptionChainFilterUniverse.cs | 1 + .../Securities/Option/OptionFilterUniverse.cs | 14 +++++++------ Engine/DataFeeds/TimeSliceFactory.cs | 6 +++++- Tests/Common/Data/Market/OptionChainTests.cs | 17 ++++++++++++++++ 8 files changed, 60 insertions(+), 14 deletions(-) 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/BaseChain.cs b/Common/Data/Market/BaseChain.cs index c199926efce1..11a990593b78 100644 --- a/Common/Data/Market/BaseChain.cs +++ b/Common/Data/Market/BaseChain.cs @@ -19,6 +19,7 @@ using System.Linq; using Python.Runtime; using QuantConnect.Python; +using QuantConnect.Securities; using QuantConnect.Securities.Option; using QuantConnect.Util; @@ -70,6 +71,11 @@ internal DateTime ExchangeTime set => _exchangeTime = value; } + /// + /// The exchange hours of the contracts when known, so the filters don't look them up + /// + internal SecurityExchangeHours ExchangeHours { get; set; } + /// /// Gets all ticks for every option contract in this chain, keyed by option symbol /// @@ -183,6 +189,7 @@ protected BaseChain(BaseChain other) Symbol = other.Symbol; Time = other.Time; _exchangeTime = other._exchangeTime; + ExchangeHours = other.ExchangeHours; Value = other.Value; Underlying = other.Underlying; Ticks = other.Ticks; diff --git a/Common/Data/Market/OptionChain.cs b/Common/Data/Market/OptionChain.cs index 15f5cf8887dc..14bcaa42ff9e 100644 --- a/Common/Data/Market/OptionChain.cs +++ b/Common/Data/Market/OptionChain.cs @@ -47,11 +47,13 @@ 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, so the filters and the contracts don't look them up /// Whether to flatten the data frame public OptionChain(Symbol canonicalOptionSymbol, DateTime time, IEnumerable contracts, SymbolProperties symbolProperties, - bool flatten = true) + SecurityExchangeHours exchangeHours = null, bool flatten = true) : this(canonicalOptionSymbol, time, flatten) { + ExchangeHours = exchangeHours; var underlyingSet = false; foreach (var contractData in contracts) { @@ -62,7 +64,7 @@ public OptionChain(Symbol canonicalOptionSymbol, DateTime time, IEnumerable @@ -126,6 +127,7 @@ public OptionContract(ISecurityPrice security) : base(security.Symbol) { _symbolProperties = security.SymbolProperties; + _exchangeHours = (security as Security)?.Exchange.Hours; } /// @@ -133,10 +135,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, so the days to expiry don't look them up + public OptionContract(OptionUniverse contractData, SymbolProperties symbolProperties, SecurityExchangeHours exchangeHours = null) : base(contractData.Symbol) { _symbolProperties = symbolProperties; + _exchangeHours = exchangeHours; _optionData = new OptionUniverseData(contractData); } @@ -183,9 +187,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, so the days to expiry don't look them up + 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, }; @@ -224,8 +229,15 @@ internal override void Update(BaseData data) /// 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 Symbol.SecurityType == SecurityType.Option ? OptionSymbol.GetLastDayOfTrading(Symbol) : Symbol.ID.Date.Date; + return _exchangeHours == null + ? OptionSymbol.GetLastDayOfTrading(Symbol) + : OptionSymbol.GetLastDayOfTrading(Symbol, _exchangeHours); } private interface IOptionData diff --git a/Common/Securities/Option/OptionChainFilterUniverse.cs b/Common/Securities/Option/OptionChainFilterUniverse.cs index 79ee28623d74..f0853ca04ec8 100644 --- a/Common/Securities/Option/OptionChainFilterUniverse.cs +++ b/Common/Securities/Option/OptionChainFilterUniverse.cs @@ -49,6 +49,7 @@ internal OptionChainFilterUniverse(OptionChain chain) : base(GetContracts(chain), GetUnderlying(chain), chain.ExchangeTime, GetStrikeMultiplier(chain)) { _symbol = chain.Symbol; + _exchangeHours = chain.ExchangeHours; } /// diff --git a/Common/Securities/Option/OptionFilterUniverse.cs b/Common/Securities/Option/OptionFilterUniverse.cs index 59c678f3c18b..2f0b01a882ea 100644 --- a/Common/Securities/Option/OptionFilterUniverse.cs +++ b/Common/Securities/Option/OptionFilterUniverse.cs @@ -44,7 +44,9 @@ public abstract class BaseOptionFilterUniverse : ContractSecur private bool _refreshUniqueStrikes; private DateTime _lastExchangeDate; private readonly decimal _underlyingScaleFactor = 1; - private readonly Dictionary _lastTradingDates = new(); + // 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 @@ -171,7 +173,7 @@ protected override DateTime GetLastTradingDate(TData contract) /// /// Gets the last trading date of the given contract, see . Uses the universe - /// exchange hours and caches per expiration date, since every contract in the universe shares them + /// 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 @@ -183,16 +185,16 @@ protected DateTime GetLastTradingDate(Symbol symbol) return expiry; } - if (!_lastTradingDates.TryGetValue(expiry, out var lastTradingDate)) + 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 + _lastTradingDate = ExchangeHours == null ? OptionSymbol.GetLastDayOfTrading(symbol) : OptionSymbol.GetLastDayOfTrading(symbol, ExchangeHours); - _lastTradingDates[expiry] = lastTradingDate; + _lastExpiry = expiry; } - return lastTradingDate; + return _lastTradingDate; } /// diff --git a/Engine/DataFeeds/TimeSliceFactory.cs b/Engine/DataFeeds/TimeSliceFactory.cs index 153db02c58ba..9224448e2e0e 100644 --- a/Engine/DataFeeds/TimeSliceFactory.cs +++ b/Engine/DataFeeds/TimeSliceFactory.cs @@ -428,7 +428,11 @@ 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) + { + ExchangeTime = baseData.EndTime, + ExchangeHours = (security as QuantConnect.Securities.Security)?.Exchange.Hours + }; optionChains[canonical] = chain; } diff --git a/Tests/Common/Data/Market/OptionChainTests.cs b/Tests/Common/Data/Market/OptionChainTests.cs index 0a32691f5a9e..7c48e866e127 100644 --- a/Tests/Common/Data/Market/OptionChainTests.cs +++ b/Tests/Common/Data/Market/OptionChainTests.cs @@ -379,6 +379,23 @@ OptionFilterUniverse Universe(List rows, BaseData spot, DateTime Assert.IsTrue(chain.All(x => x.Time.Date == lastTradingDate && x.DaysToExpiry == 0)); } + [Test] + public void ChainExchangeHoursReachTheFiltersAndTheContracts() + { + // 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 exchangeHours = CreateOption().Exchange.Hours; + var chain = new OptionChain(Canonical, date, data, _symbolProperties, exchangeHours); + Assert.AreSame(exchangeHours, chain.ExchangeHours); + + 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)); + Assert.AreSame(exchangeHours, zeroDte.ExchangeHours); + Assert.AreSame(exchangeHours, zeroDte.PutsOnly().ExchangeHours); + } + [Test] public void FiltersAreAvailableFromPython() { From ad98537fb9aff0a902f965a902a1be25b1e6ca18 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 16 Sep 2026 10:34:33 -0400 Subject: [PATCH 10/12] Keep the option symbol properties and exchange hours on the chain and hand them to its filter universe --- Common/Data/Market/BaseChain.cs | 7 ----- Common/Data/Market/OptionChain.Filters.cs | 2 +- Common/Data/Market/OptionChain.cs | 29 +++++++++++++++++-- Common/Data/Market/OptionContract.cs | 9 ++---- .../Option/OptionChainFilterUniverse.cs | 13 ++++----- Engine/DataFeeds/TimeSliceFactory.cs | 5 ++-- Tests/Common/Data/Market/OptionChainTests.cs | 10 +++---- 7 files changed, 40 insertions(+), 35 deletions(-) diff --git a/Common/Data/Market/BaseChain.cs b/Common/Data/Market/BaseChain.cs index 11a990593b78..c199926efce1 100644 --- a/Common/Data/Market/BaseChain.cs +++ b/Common/Data/Market/BaseChain.cs @@ -19,7 +19,6 @@ using System.Linq; using Python.Runtime; using QuantConnect.Python; -using QuantConnect.Securities; using QuantConnect.Securities.Option; using QuantConnect.Util; @@ -71,11 +70,6 @@ internal DateTime ExchangeTime set => _exchangeTime = value; } - /// - /// The exchange hours of the contracts when known, so the filters don't look them up - /// - internal SecurityExchangeHours ExchangeHours { get; set; } - /// /// Gets all ticks for every option contract in this chain, keyed by option symbol /// @@ -189,7 +183,6 @@ protected BaseChain(BaseChain other) Symbol = other.Symbol; Time = other.Time; _exchangeTime = other._exchangeTime; - ExchangeHours = other.ExchangeHours; Value = other.Value; Underlying = other.Underlying; Ticks = other.Ticks; 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 14bcaa42ff9e..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,13 +67,12 @@ 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, so the filters and the contracts don't look them up + /// The option exchange hours /// Whether to flatten the data frame public OptionChain(Symbol canonicalOptionSymbol, DateTime time, IEnumerable contracts, SymbolProperties symbolProperties, SecurityExchangeHours exchangeHours = null, bool flatten = true) - : this(canonicalOptionSymbol, time, flatten) + : this(canonicalOptionSymbol, time, symbolProperties, exchangeHours, flatten) { - ExchangeHours = exchangeHours; var underlyingSet = false; foreach (var contractData in contracts) { @@ -74,6 +93,8 @@ public OptionChain(Symbol canonicalOptionSymbol, DateTime time, IEnumerable @@ -83,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 b84aa02d2e64..8edba619e2e5 100644 --- a/Common/Data/Market/OptionContract.cs +++ b/Common/Data/Market/OptionContract.cs @@ -114,11 +114,6 @@ public class OptionContract : BaseContract [PandasIgnore] public override int DaysToExpiry => ((_lastTradingDate ??= GetLastTradingDate()) - Time.Date).Days; - /// - /// The option symbol properties - /// - internal SymbolProperties SymbolProperties => _symbolProperties; - /// /// Initializes a new instance of the class /// @@ -135,7 +130,7 @@ public OptionContract(ISecurityPrice security) /// /// The option universe contract data to use as source for this contract /// The contract symbol properties - /// The contract exchange hours, so the days to expiry don't look them up + /// The contract exchange hours public OptionContract(OptionUniverse contractData, SymbolProperties symbolProperties, SecurityExchangeHours exchangeHours = null) : base(contractData.Symbol) { @@ -187,7 +182,7 @@ 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 - /// The contract exchange hours, so the days to expiry don't look them up + /// The contract exchange hours public static OptionContract Create(OptionUniverse contractData, SymbolProperties symbolProperties, SecurityExchangeHours exchangeHours = null) { var contract = new OptionContract(contractData, symbolProperties, exchangeHours) diff --git a/Common/Securities/Option/OptionChainFilterUniverse.cs b/Common/Securities/Option/OptionChainFilterUniverse.cs index f0853ca04ec8..e7994e389155 100644 --- a/Common/Securities/Option/OptionChainFilterUniverse.cs +++ b/Common/Securities/Option/OptionChainFilterUniverse.cs @@ -45,11 +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 = chain.ExchangeHours; + _exchangeHours = exchangeHours; } /// @@ -92,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/Engine/DataFeeds/TimeSliceFactory.cs b/Engine/DataFeeds/TimeSliceFactory.cs index 9224448e2e0e..83317b593bba 100644 --- a/Engine/DataFeeds/TimeSliceFactory.cs +++ b/Engine/DataFeeds/TimeSliceFactory.cs @@ -428,10 +428,9 @@ 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) + chain = new OptionChain(canonical, algorithmTime, security.SymbolProperties, (security as QuantConnect.Securities.Security)?.Exchange.Hours) { - ExchangeTime = baseData.EndTime, - ExchangeHours = (security as QuantConnect.Securities.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 7c48e866e127..9b9d5aaa311a 100644 --- a/Tests/Common/Data/Market/OptionChainTests.cs +++ b/Tests/Common/Data/Market/OptionChainTests.cs @@ -380,20 +380,18 @@ OptionFilterUniverse Universe(List rows, BaseData spot, DateTime } [Test] - public void ChainExchangeHoursReachTheFiltersAndTheContracts() + 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 exchangeHours = CreateOption().Exchange.Hours; - var chain = new OptionChain(Canonical, date, data, _symbolProperties, exchangeHours); - Assert.AreSame(exchangeHours, chain.ExchangeHours); + 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)); - Assert.AreSame(exchangeHours, zeroDte.ExchangeHours); - Assert.AreSame(exchangeHours, zeroDte.PutsOnly().ExchangeHours); + // the filtered chains carry the hours too + Assert.AreEqual(Strikes.Length, zeroDte.PutsOnly().ZeroDte().Count); } [Test] From 8d94658ad7acddc477cffcd64de527e50c32078a Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 16 Sep 2026 11:42:12 -0400 Subject: [PATCH 11/12] Expose the exchange hours through ISecurityPrice so the option contracts and chains read them without a cast --- Common/Data/Market/OptionContract.cs | 2 +- Common/Interfaces/ISecurityPrice.cs | 5 +++++ Common/Securities/Security.cs | 5 +++++ Engine/DataFeeds/TimeSliceFactory.cs | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Common/Data/Market/OptionContract.cs b/Common/Data/Market/OptionContract.cs index 8edba619e2e5..0f4778b64e07 100644 --- a/Common/Data/Market/OptionContract.cs +++ b/Common/Data/Market/OptionContract.cs @@ -122,7 +122,7 @@ public OptionContract(ISecurityPrice security) : base(security.Symbol) { _symbolProperties = security.SymbolProperties; - _exchangeHours = (security as Security)?.Exchange.Hours; + _exchangeHours = security.ExchangeHours; } /// diff --git a/Common/Interfaces/ISecurityPrice.cs b/Common/Interfaces/ISecurityPrice.cs index 18ec075a9b07..c6347ccfeaa6 100644 --- a/Common/Interfaces/ISecurityPrice.cs +++ b/Common/Interfaces/ISecurityPrice.cs @@ -77,6 +77,11 @@ public interface ISecurityPrice /// SymbolProperties SymbolProperties { get; } + /// + /// of the symbol + /// + SecurityExchangeHours ExchangeHours { get; } + /// /// Update any security properties based on the latest market data and time /// diff --git a/Common/Securities/Security.cs b/Common/Securities/Security.cs index 99976451e14f..89da3a9157e1 100644 --- a/Common/Securities/Security.cs +++ b/Common/Securities/Security.cs @@ -109,6 +109,11 @@ public SymbolProperties SymbolProperties protected set; } + /// + /// Gets the exchange hours of this security, see + /// + public SecurityExchangeHours ExchangeHours => Exchange.Hours; + /// /// Type of the security. /// diff --git a/Engine/DataFeeds/TimeSliceFactory.cs b/Engine/DataFeeds/TimeSliceFactory.cs index 83317b593bba..d1ea1b02161a 100644 --- a/Engine/DataFeeds/TimeSliceFactory.cs +++ b/Engine/DataFeeds/TimeSliceFactory.cs @@ -428,7 +428,7 @@ 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, security.SymbolProperties, (security as QuantConnect.Securities.Security)?.Exchange.Hours) + chain = new OptionChain(canonical, algorithmTime, security.SymbolProperties, security.ExchangeHours) { ExchangeTime = baseData.EndTime }; From 182bf91c1977b3135a82b083d6e8e251611b022b Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 16 Sep 2026 12:22:41 -0400 Subject: [PATCH 12/12] Expose the exchange through ISecurityPrice instead of adding an exchange hours property --- Common/Data/Market/OptionContract.cs | 2 +- Common/Interfaces/ISecurityPrice.cs | 4 ++-- Common/Securities/Security.cs | 5 ----- Engine/DataFeeds/TimeSliceFactory.cs | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Common/Data/Market/OptionContract.cs b/Common/Data/Market/OptionContract.cs index 0f4778b64e07..8ed99273a47f 100644 --- a/Common/Data/Market/OptionContract.cs +++ b/Common/Data/Market/OptionContract.cs @@ -122,7 +122,7 @@ public OptionContract(ISecurityPrice security) : base(security.Symbol) { _symbolProperties = security.SymbolProperties; - _exchangeHours = security.ExchangeHours; + _exchangeHours = security.Exchange.Hours; } /// diff --git a/Common/Interfaces/ISecurityPrice.cs b/Common/Interfaces/ISecurityPrice.cs index c6347ccfeaa6..ab01c0403c19 100644 --- a/Common/Interfaces/ISecurityPrice.cs +++ b/Common/Interfaces/ISecurityPrice.cs @@ -78,9 +78,9 @@ public interface ISecurityPrice SymbolProperties SymbolProperties { get; } /// - /// of the symbol + /// of the symbol /// - SecurityExchangeHours ExchangeHours { get; } + SecurityExchange Exchange { get; } /// /// Update any security properties based on the latest market data and time diff --git a/Common/Securities/Security.cs b/Common/Securities/Security.cs index 89da3a9157e1..99976451e14f 100644 --- a/Common/Securities/Security.cs +++ b/Common/Securities/Security.cs @@ -109,11 +109,6 @@ public SymbolProperties SymbolProperties protected set; } - /// - /// Gets the exchange hours of this security, see - /// - public SecurityExchangeHours ExchangeHours => Exchange.Hours; - /// /// Type of the security. /// diff --git a/Engine/DataFeeds/TimeSliceFactory.cs b/Engine/DataFeeds/TimeSliceFactory.cs index d1ea1b02161a..6efc18c7af97 100644 --- a/Engine/DataFeeds/TimeSliceFactory.cs +++ b/Engine/DataFeeds/TimeSliceFactory.cs @@ -428,7 +428,7 @@ 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, security.SymbolProperties, security.ExchangeHours) + chain = new OptionChain(canonical, algorithmTime, security.SymbolProperties, security.Exchange.Hours) { ExchangeTime = baseData.EndTime };