From f9f4c3f2d134d9bd1d18d594845084b813415357 Mon Sep 17 00:00:00 2001 From: Martin Molinero Date: Fri, 21 Aug 2026 11:42:26 -0300 Subject: [PATCH 1/2] Skip stale chain universe open interest when a newer tick was received Track the end time of the last open interest update in the security cache and only take the chain universe open interest when it's not older. --- Common/Securities/SecurityCache.cs | 9 +- Tests/Common/Securities/SecurityCacheTests.cs | 119 ++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/Common/Securities/SecurityCache.cs b/Common/Securities/SecurityCache.cs index e9ca80fa1668..5776e51842c1 100644 --- a/Common/Securities/SecurityCache.cs +++ b/Common/Securities/SecurityCache.cs @@ -38,6 +38,7 @@ public class SecurityCache // this is used to prefer quote bar data over the tradebar data private DateTime _lastQuoteBarUpdate; private DateTime _lastOHLCUpdate; + private DateTime _lastOpenInterestUpdate; private BaseData _lastData; private readonly object _locker = new(); @@ -198,6 +199,7 @@ protected virtual void ProcessDataPoint(BaseData data, bool cacheByType) StoreDataPoint(data); } OpenInterest = (long)tick.Value; + _lastOpenInterestUpdate = tick.EndTime; // Update the session with the latest open interest Session?.Update(data); @@ -325,12 +327,16 @@ public virtual void StoreData(IReadOnlyList data, Type dataType) /// Helper method to update the open interest cache property from a chain universe data point, /// which carries the contracts daily open interest /// + /// The chain universe data is skipped if a more recent open interest value was already received, + /// for example in live trading, where the open interest ticks of the day arrive before the previous + /// tradable date's chain universe file is fed into the algorithm /// The data point being stored protected void UpdateOpenInterest(BaseData data) { - if (data is BaseChainUniverseData chainUniverseData) + if (data is BaseChainUniverseData chainUniverseData && chainUniverseData.EndTime >= _lastOpenInterestUpdate) { OpenInterest = (long)chainUniverseData.OpenInterest; + _lastOpenInterestUpdate = chainUniverseData.EndTime; } } @@ -455,6 +461,7 @@ public void Reset() _lastOHLCUpdate = default; _lastQuoteBarUpdate = default; + _lastOpenInterestUpdate = default; Session?.Reset(); UnsubscribeToTimeUpdatedEvent(); } diff --git a/Tests/Common/Securities/SecurityCacheTests.cs b/Tests/Common/Securities/SecurityCacheTests.cs index f0e74a829aaa..e13736656bf4 100644 --- a/Tests/Common/Securities/SecurityCacheTests.cs +++ b/Tests/Common/Securities/SecurityCacheTests.cs @@ -26,6 +26,10 @@ using QuantConnect.Data.UniverseSelection; using QuantConnect.Python; using QuantConnect.Securities; +using QuantConnect.Securities.Future; +using QuantConnect.Securities.FutureOption; +using QuantConnect.Securities.IndexOption; +using QuantConnect.Securities.Option; using QuantConnect.Tests.Common.Data.Fundamental; using QuantConnect.Util; @@ -424,6 +428,121 @@ public void TickTypeDependencyTests() Assert.AreEqual(securityCache.Volume, volume); } + [TestCaseSource(nameof(ChainUniverseOpenInterestCacheTypes))] + public void StoreData_ChainUniverseData_SetsOpenInterest(Type cacheType) + { + var cache = (SecurityCache)Activator.CreateInstance(cacheType); + var universeData = CreateChainUniverseData(cache, new DateTime(2016, 02, 18), 1234); + + cache.StoreData(new[] { universeData }, universeData.GetType()); + + Assert.AreEqual(1234, cache.OpenInterest); + Assert.IsTrue(cache.HasData(universeData.GetType())); + } + + [Test] + public void StoreData_ChainUniverseData_DoesNotSetOpenInterestOnBaseCache() + { + var cache = new SecurityCache(); + var universeData = CreateChainUniverseData(cache, new DateTime(2016, 02, 18), 1234); + + cache.StoreData(new[] { universeData }, universeData.GetType()); + + Assert.AreEqual(0, cache.OpenInterest); + Assert.IsTrue(cache.HasData(universeData.GetType())); + } + + [TestCaseSource(nameof(ChainUniverseOpenInterestCacheTypes))] + public void StoreData_ChainUniverseData_DoesNotOverrideNewerOpenInterestTick(Type cacheType) + { + var cache = (SecurityCache)Activator.CreateInstance(cacheType); + var universeData = CreateChainUniverseData(cache, new DateTime(2016, 02, 18), 1234); + + // In live trading the chain universe file of the previous tradable date is fed into the algorithm + // after the open interest tick of the day was already received + var openInterestTick = new OpenInterest(universeData.EndTime.AddHours(6), universeData.Symbol, 5000); + cache.AddDataList(new[] { openInterestTick }, typeof(OpenInterest)); + Assert.AreEqual(5000, cache.OpenInterest); + + cache.StoreData(new[] { universeData }, universeData.GetType()); + + Assert.AreEqual(5000, cache.OpenInterest); + // The universe data point is still stored in the cache + Assert.IsTrue(cache.HasData(universeData.GetType())); + } + + [TestCaseSource(nameof(ChainUniverseOpenInterestCacheTypes))] + public void StoreData_ChainUniverseData_OverridesOlderOpenInterestTick(Type cacheType) + { + var cache = (SecurityCache)Activator.CreateInstance(cacheType); + var universeData = CreateChainUniverseData(cache, new DateTime(2016, 02, 18), 1234); + + // In backtesting the chain universe data of the day is emitted at the end of the day, + // after the open interest ticks of the day + var openInterestTick = new OpenInterest(universeData.EndTime.AddHours(-6), universeData.Symbol, 5000); + cache.AddData(openInterestTick); + Assert.AreEqual(5000, cache.OpenInterest); + + cache.StoreData(new[] { universeData }, universeData.GetType()); + + Assert.AreEqual(1234, cache.OpenInterest); + } + + [TestCaseSource(nameof(ChainUniverseOpenInterestCacheTypes))] + public void OpenInterestTick_OverridesChainUniverseDataOpenInterest(Type cacheType) + { + var cache = (SecurityCache)Activator.CreateInstance(cacheType); + var date = new DateTime(2016, 02, 18); + var universeData = CreateChainUniverseData(cache, date, 1234); + cache.StoreData(new[] { universeData }, universeData.GetType()); + Assert.AreEqual(1234, cache.OpenInterest); + + // A newer open interest tick always overrides the universe open interest + var openInterestTick = new OpenInterest(universeData.EndTime.AddHours(6), universeData.Symbol, 5000); + cache.AddData(openInterestTick); + Assert.AreEqual(5000, cache.OpenInterest); + + // And the next day's universe data is newer than the tick so it overrides it + var nextUniverseData = CreateChainUniverseData(cache, date.AddDays(1), 4321); + cache.StoreData(new[] { nextUniverseData }, nextUniverseData.GetType()); + Assert.AreEqual(4321, cache.OpenInterest); + } + + [TestCaseSource(nameof(ChainUniverseOpenInterestCacheTypes))] + public void Reset_ClearsLastOpenInterestUpdateTime(Type cacheType) + { + var cache = (SecurityCache)Activator.CreateInstance(cacheType); + var universeData = CreateChainUniverseData(cache, new DateTime(2016, 02, 18), 1234); + var openInterestTick = new OpenInterest(universeData.EndTime.AddHours(6), universeData.Symbol, 5000); + cache.AddData(openInterestTick); + + cache.Reset(); + Assert.AreEqual(0, cache.OpenInterest); + + cache.StoreData(new[] { universeData }, universeData.GetType()); + + Assert.AreEqual(1234, cache.OpenInterest); + } + + private static readonly Type[] ChainUniverseOpenInterestCacheTypes = + { + typeof(OptionCache), + typeof(IndexOptionCache), + typeof(FutureOptionCache), + typeof(FutureCache) + }; + + private static BaseChainUniverseData CreateChainUniverseData(SecurityCache cache, DateTime date, decimal openInterest) + { + // open,high,low,close,volume,open_interest + var csv = $"100,101,99,100,5000,{openInterest.ToStringInvariant()}"; + if (cache is FutureCache) + { + return new FutureUniverse(date, Symbols.Future_ESZ18_Dec2018, csv); + } + return new OptionUniverse(date, Symbols.SPY_C_192_Feb19_2016, csv); + } + [Test] public void GetAllData_ReturnsListOfData() { From 598db051679387ede238724b227a0e03de7a235c Mon Sep 17 00:00:00 2001 From: Martin Molinero Date: Fri, 21 Aug 2026 12:39:27 -0300 Subject: [PATCH 2/2] Let the open interest tick win ties with the chain universe data end time Daily open interest data is stamped at midnight, matching the end time of the previous date's chain universe data, so the tick is the newer value. --- ...ionUniverseOpenInterestRegressionAlgorithm.cs | 4 ++-- Common/Securities/SecurityCache.cs | 2 +- Tests/Common/Securities/SecurityCacheTests.cs | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Algorithm.CSharp/OptionUniverseOpenInterestRegressionAlgorithm.cs b/Algorithm.CSharp/OptionUniverseOpenInterestRegressionAlgorithm.cs index 6dfce705568b..2f1d4880e076 100644 --- a/Algorithm.CSharp/OptionUniverseOpenInterestRegressionAlgorithm.cs +++ b/Algorithm.CSharp/OptionUniverseOpenInterestRegressionAlgorithm.cs @@ -86,11 +86,11 @@ private void AssertOpenInterest(Security security, bool checkOpenInterestTick) return; } - // If a more recent open interest tick was received from the data feed, the cache will reflect it instead + // If an open interest tick as recent as the chain universe data was received from the data feed, the cache will reflect it instead if (checkOpenInterestTick) { var lastOpenInterestTick = security.Cache.GetData(); - if (lastOpenInterestTick != null && lastOpenInterestTick.EndTime > chainUniverseData.EndTime) + if (lastOpenInterestTick != null && lastOpenInterestTick.EndTime >= chainUniverseData.EndTime) { return; } diff --git a/Common/Securities/SecurityCache.cs b/Common/Securities/SecurityCache.cs index 5776e51842c1..1b5145254f6e 100644 --- a/Common/Securities/SecurityCache.cs +++ b/Common/Securities/SecurityCache.cs @@ -333,7 +333,7 @@ public virtual void StoreData(IReadOnlyList data, Type dataType) /// The data point being stored protected void UpdateOpenInterest(BaseData data) { - if (data is BaseChainUniverseData chainUniverseData && chainUniverseData.EndTime >= _lastOpenInterestUpdate) + if (data is BaseChainUniverseData chainUniverseData && chainUniverseData.EndTime > _lastOpenInterestUpdate) { OpenInterest = (long)chainUniverseData.OpenInterest; _lastOpenInterestUpdate = chainUniverseData.EndTime; diff --git a/Tests/Common/Securities/SecurityCacheTests.cs b/Tests/Common/Securities/SecurityCacheTests.cs index e13736656bf4..f4d8a24a0048 100644 --- a/Tests/Common/Securities/SecurityCacheTests.cs +++ b/Tests/Common/Securities/SecurityCacheTests.cs @@ -471,6 +471,22 @@ public void StoreData_ChainUniverseData_DoesNotOverrideNewerOpenInterestTick(Typ Assert.IsTrue(cache.HasData(universeData.GetType())); } + [TestCaseSource(nameof(ChainUniverseOpenInterestCacheTypes))] + public void StoreData_ChainUniverseData_DoesNotOverrideOpenInterestTickWithSameEndTime(Type cacheType) + { + var cache = (SecurityCache)Activator.CreateInstance(cacheType); + var universeData = CreateChainUniverseData(cache, new DateTime(2016, 02, 18), 1234); + + // Daily open interest data is stamped at midnight, matching the end time of the previous date's chain universe data + var openInterestTick = new OpenInterest(universeData.EndTime, universeData.Symbol, 5000); + cache.AddData(openInterestTick); + Assert.AreEqual(5000, cache.OpenInterest); + + cache.StoreData(new[] { universeData }, universeData.GetType()); + + Assert.AreEqual(5000, cache.OpenInterest); + } + [TestCaseSource(nameof(ChainUniverseOpenInterestCacheTypes))] public void StoreData_ChainUniverseData_OverridesOlderOpenInterestTick(Type cacheType) {