From 5b00341f170f2740b320c53a713bc5c3b40686fa Mon Sep 17 00:00:00 2001
From: Jonathan Wu <61775916+jonathanwu906@users.noreply.github.com>
Date: Wed, 1 Jul 2026 11:08:43 +0800
Subject: [PATCH] Fix duplicate OnData when dynamically adding an at-expiry
contract
Adding a delistable contract (e.g. via AddOptionContract) at or after its delisting date from inside a scheduled event caused DelistingEventProvider to emit a stale delisting warning immediately, producing a spurious extra slice and a second OnData call at the same time (GH #9279). CorporateEventEnumeratorFactory now only attaches a DelistingEventProvider when the subscription starts before the delisting date. Adds a regression algorithm that fails without the fix and passes with it.
---
...ractInScheduledEventRegressionAlgorithm.cs | 138 ++++++++++++++++++
.../CorporateEventEnumeratorFactory.cs | 6 +-
2 files changed, 143 insertions(+), 1 deletion(-)
create mode 100644 Algorithm.CSharp/AddOptionContractInScheduledEventRegressionAlgorithm.cs
diff --git a/Algorithm.CSharp/AddOptionContractInScheduledEventRegressionAlgorithm.cs b/Algorithm.CSharp/AddOptionContractInScheduledEventRegressionAlgorithm.cs
new file mode 100644
index 000000000000..656f9829332d
--- /dev/null
+++ b/Algorithm.CSharp/AddOptionContractInScheduledEventRegressionAlgorithm.cs
@@ -0,0 +1,138 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using System.Linq;
+using System.Collections.Generic;
+using QuantConnect.Data;
+using QuantConnect.Interfaces;
+
+namespace QuantConnect.Algorithm.CSharp
+{
+ ///
+ /// Regression algorithm for GH issue #9279: adding a contract at/after its delisting date from a
+ /// scheduled event used to emit a stale delisting warning right away, producing a spurious extra
+ /// slice (and extra OnData call) at the same time. After the fix no delisting event is emitted for
+ /// the late-added contract.
+ ///
+ public class AddOptionContractInScheduledEventRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
+ {
+ private Symbol _contract;
+ private bool _contractAdded;
+
+ public override void Initialize()
+ {
+ SetStartDate(2014, 6, 6);
+ SetEndDate(2014, 6, 6);
+ SetCash(100000);
+
+ UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw;
+
+ var aapl = AddEquity("AAPL", Resolution.Minute).Symbol;
+
+ // Nearest-expiry contract (expires the same day): the case that used to trigger the stale warning
+ _contract = OptionChain(aapl)
+ .OrderBy(symbol => symbol.ID.Date)
+ .ThenBy(symbol => symbol.ID.StrikePrice)
+ .FirstOrDefault();
+
+ Schedule.On(DateRules.On(2014, 6, 6), TimeRules.At(10, 0), () =>
+ {
+ if (_contract != null)
+ {
+ AddOptionContract(_contract);
+ _contractAdded = true;
+ }
+ });
+ }
+
+ public override void OnData(Slice slice)
+ {
+ if (slice.Delistings.Count > 0)
+ {
+ throw new RegressionTestException(
+ $"Unexpected delisting event(s) at {Time} for a contract added at/after its delisting date " +
+ "inside a scheduled event (GH #9279).");
+ }
+ }
+
+ public override void OnEndOfAlgorithm()
+ {
+ if (!_contractAdded)
+ {
+ throw new RegressionTestException("Expected the option contract to be added during the scheduled event");
+ }
+ }
+
+ ///
+ /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
+ ///
+ public bool CanRunLocally { get; } = true;
+
+ ///
+ /// This is used by the regression test system to indicate which languages this algorithm is written in.
+ ///
+ public List Languages { get; } = new() { Language.CSharp };
+
+ ///
+ /// Data Points count of all timeslices of algorithm
+ ///
+ public long DataPoints => -1;
+
+ ///
+ /// Data Points count of the algorithm history
+ ///
+ public int AlgorithmHistoryDataPoints => -1;
+
+ ///
+ /// Final status of the algorithm
+ ///
+ public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
+
+ ///
+ /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
+ ///
+ public Dictionary ExpectedStatistics => new Dictionary
+ {
+ {"Total Orders", "0"},
+ {"Average Win", "0%"},
+ {"Average Loss", "0%"},
+ {"Compounding Annual Return", "0%"},
+ {"Drawdown", "0%"},
+ {"Expectancy", "0"},
+ {"Start Equity", "100000"},
+ {"End Equity", "100000"},
+ {"Net Profit", "0%"},
+ {"Sharpe Ratio", "0"},
+ {"Sortino Ratio", "0"},
+ {"Probabilistic Sharpe Ratio", "0%"},
+ {"Loss Rate", "0%"},
+ {"Win Rate", "0%"},
+ {"Profit-Loss Ratio", "0"},
+ {"Alpha", "0"},
+ {"Beta", "0"},
+ {"Annual Standard Deviation", "0"},
+ {"Annual Variance", "0"},
+ {"Information Ratio", "0"},
+ {"Tracking Error", "0"},
+ {"Treynor Ratio", "0"},
+ {"Total Fees", "$0.00"},
+ {"Estimated Strategy Capacity", "$0"},
+ {"Lowest Capacity Asset", ""},
+ {"Portfolio Turnover", "0%"},
+ {"Drawdown Recovery", "0"},
+ {"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"}
+ };
+ }
+}
diff --git a/Engine/DataFeeds/Enumerators/Factories/CorporateEventEnumeratorFactory.cs b/Engine/DataFeeds/Enumerators/Factories/CorporateEventEnumeratorFactory.cs
index 0f1aea1cc03f..c28377c56da6 100644
--- a/Engine/DataFeeds/Enumerators/Factories/CorporateEventEnumeratorFactory.cs
+++ b/Engine/DataFeeds/Enumerators/Factories/CorporateEventEnumeratorFactory.cs
@@ -72,7 +72,11 @@ public static IEnumerator CreateEnumerators(
if (config.CanBeDelisted())
{
- tradableEventProviders.Add(new DelistingEventProvider());
+ var delistingDate = config.Symbol.GetDelistingDate(mapFileProvider.ResolveMapFile(config));
+ if (startTime.Date < delistingDate.Date)
+ {
+ tradableEventProviders.Add(new DelistingEventProvider());
+ }
}
var enumerator = new AuxiliaryDataEnumerator(