From 2996695fe879d9b16f2f6d5c1be03bfa29d64bc6 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Wed, 29 Apr 2026 11:40:37 -0500 Subject: [PATCH 1/5] Align OnWarmupFinished time to StartDate when ScheduledUniverse skips midnight --- ...hedScheduledUniverseRegressionAlgorithm.cs | 110 ++++++++++++++++++ Engine/AlgorithmManager.cs | 12 ++ 2 files changed, 122 insertions(+) create mode 100644 Algorithm.CSharp/OnWarmupFinishedScheduledUniverseRegressionAlgorithm.cs diff --git a/Algorithm.CSharp/OnWarmupFinishedScheduledUniverseRegressionAlgorithm.cs b/Algorithm.CSharp/OnWarmupFinishedScheduledUniverseRegressionAlgorithm.cs new file mode 100644 index 000000000000..02b28c45faa9 --- /dev/null +++ b/Algorithm.CSharp/OnWarmupFinishedScheduledUniverseRegressionAlgorithm.cs @@ -0,0 +1,110 @@ +/* + * 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; +using System.Collections.Generic; +using QuantConnect.Algorithm.Framework.Selection; +using QuantConnect.Interfaces; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm asserting OnWarmupFinished fires at StartDate (midnight) + /// when using a ScheduledUniverseSelectionModel that triggers at 8 AM, skipping midnight entirely. + /// + public class OnWarmupFinishedScheduledUniverseRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private bool _onWarmupFinishedCalled; + + public override void Initialize() + { + SetStartDate(2013, 10, 08); + SetEndDate(2013, 10, 11); + SetCash(100000); + + UniverseSettings.Resolution = Resolution.Minute; + SetWarmup(TimeSpan.FromDays(1)); + + // Universe triggers at 8 AM + SetUniverseSelection(new ScheduledUniverseSelectionModel( + DateRules.EveryDay(), + TimeRules.At(8, 0), + _ => new[] { QuantConnect.Symbol.Create("SPY", SecurityType.Equity, Market.USA) } + )); + } + + public override void OnWarmupFinished() + { + _onWarmupFinishedCalled = true; + + if (Time != StartDate) + { + throw new RegressionTestException( + $"Expected OnWarmupFinished to fire at StartDate ({StartDate:yyyy-MM-dd HH:mm:ss}), " + + $"but fired at {Time:yyyy-MM-dd HH:mm:ss}"); + } + } + + public override void OnEndOfAlgorithm() + { + if (!_onWarmupFinishedCalled) + { + throw new RegressionTestException("OnWarmupFinished was never called"); + } + } + + public bool CanRunLocally { get; } = true; + + public List Languages { get; } = new() { Language.CSharp }; + + public long DataPoints => 3948; + + public int AlgorithmHistoryDataPoints => 0; + + public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed; + + 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", "-31.448"}, + {"Tracking Error", "0.164"}, + {"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/AlgorithmManager.cs b/Engine/AlgorithmManager.cs index 588e4b87a4bb..8006f631b6b2 100644 --- a/Engine/AlgorithmManager.cs +++ b/Engine/AlgorithmManager.cs @@ -727,6 +727,18 @@ private IEnumerable Stream(IAlgorithm algorithm, ISynchronizer synchr { // warmup finished, send an update warmingUp = false; + + // Align time to StartDate so OnWarmupFinished always fires at midnight, + // even when the first post warmup slice arrives later (e.g. a ScheduledUniverse at 8 AM). + if (!algorithm.LiveMode) + { + var warmupEndUtc = algorithm.StartDate.ConvertToUtc(algorithm.TimeZone); + if (algorithm.UtcTime > warmupEndUtc) + { + algorithm.SetDateTime(warmupEndUtc); + } + } + // we trigger this callback here and not internally in the algorithm so that we can go through python if required algorithm.OnWarmupFinished(); algorithm.Debug("Algorithm finished warming up."); From 0ea3ff934bee1403875d523366a0b244f7ab74d1 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Mon, 4 May 2026 20:54:07 -0500 Subject: [PATCH 2/5] Align algorithm time to StartDate before OnWarmupFinished fires --- Engine/AlgorithmManager.cs | 12 ------------ Engine/DataFeeds/Synchronizer.cs | 9 +++++++++ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Engine/AlgorithmManager.cs b/Engine/AlgorithmManager.cs index 8006f631b6b2..588e4b87a4bb 100644 --- a/Engine/AlgorithmManager.cs +++ b/Engine/AlgorithmManager.cs @@ -727,18 +727,6 @@ private IEnumerable Stream(IAlgorithm algorithm, ISynchronizer synchr { // warmup finished, send an update warmingUp = false; - - // Align time to StartDate so OnWarmupFinished always fires at midnight, - // even when the first post warmup slice arrives later (e.g. a ScheduledUniverse at 8 AM). - if (!algorithm.LiveMode) - { - var warmupEndUtc = algorithm.StartDate.ConvertToUtc(algorithm.TimeZone); - if (algorithm.UtcTime > warmupEndUtc) - { - algorithm.SetDateTime(warmupEndUtc); - } - } - // we trigger this callback here and not internally in the algorithm so that we can go through python if required algorithm.OnWarmupFinished(); algorithm.Debug("Algorithm finished warming up."); diff --git a/Engine/DataFeeds/Synchronizer.cs b/Engine/DataFeeds/Synchronizer.cs index 2404d53e9f82..e3f47e7118e4 100644 --- a/Engine/DataFeeds/Synchronizer.cs +++ b/Engine/DataFeeds/Synchronizer.cs @@ -30,6 +30,7 @@ namespace QuantConnect.Lean.Engine.DataFeeds public class Synchronizer : ISynchronizer, IDataFeedTimeProvider, IDisposable { private DateTimeZone _dateTimeZone; + private DateTime _warmupEndUtc; /// /// The algorithm instance @@ -116,6 +117,13 @@ public virtual IEnumerable StreamData(CancellationToken cancellationT // check for cancellation if (timeSlice == null || cancellationToken.IsCancellationRequested) break; + // If the first post warmup slice skips past StartDate, emit a time pulse at StartDate + // so the algorithm time is aligned before OnWarmupFinished fires + if (!Algorithm.LiveMode && Algorithm.IsWarmingUp && timeSlice.Time > _warmupEndUtc) + { + yield return TimeSliceFactory.CreateTimePulse(_warmupEndUtc); + } + if (timeSlice.IsTimePulse && Algorithm.UtcTime == timeSlice.Time) { previousWasTimePulse = timeSlice.IsTimePulse; @@ -167,6 +175,7 @@ protected virtual void PostInitialize() // this is set after the algorithm initializes _dateTimeZone = Algorithm.TimeZone; + _warmupEndUtc = Algorithm.StartDate.ConvertToUtc(_dateTimeZone); TimeSliceFactory = new TimeSliceFactory(_dateTimeZone); SubscriptionSynchronizer.SetTimeSliceFactory(TimeSliceFactory); } From e3a9e5269d064a823339ba620050e8ed1cc041d4 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Fri, 8 May 2026 12:10:06 -0500 Subject: [PATCH 3/5] Apply warmup time alignment fix to LiveSynchronizer --- Engine/DataFeeds/LiveSynchronizer.cs | 5 +++++ Engine/DataFeeds/Synchronizer.cs | 12 ++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Engine/DataFeeds/LiveSynchronizer.cs b/Engine/DataFeeds/LiveSynchronizer.cs index d6b864038315..ac78a87e5f05 100644 --- a/Engine/DataFeeds/LiveSynchronizer.cs +++ b/Engine/DataFeeds/LiveSynchronizer.cs @@ -139,6 +139,11 @@ public override IEnumerable StreamData(CancellationToken cancellation // check for cancellation if (timeSlice == null || cancellationToken.IsCancellationRequested) break; + if (Algorithm.IsWarmingUp && timeSlice.Time > WarmupEndUtc) + { + yield return TimeSliceFactory.CreateTimePulse(WarmupEndUtc); + } + var frontierUtc = FrontierTimeProvider.GetUtcNow(); // emit on data or if we've elapsed a full second since last emit or there are security changes if (timeSlice.SecurityChanges != SecurityChanges.None diff --git a/Engine/DataFeeds/Synchronizer.cs b/Engine/DataFeeds/Synchronizer.cs index e3f47e7118e4..40f82a4a6d86 100644 --- a/Engine/DataFeeds/Synchronizer.cs +++ b/Engine/DataFeeds/Synchronizer.cs @@ -30,7 +30,11 @@ namespace QuantConnect.Lean.Engine.DataFeeds public class Synchronizer : ISynchronizer, IDataFeedTimeProvider, IDisposable { private DateTimeZone _dateTimeZone; - private DateTime _warmupEndUtc; + + /// + /// UTC time at which the warm up period ends + /// + protected DateTime WarmupEndUtc { get; private set; } /// /// The algorithm instance @@ -119,9 +123,9 @@ public virtual IEnumerable StreamData(CancellationToken cancellationT // If the first post warmup slice skips past StartDate, emit a time pulse at StartDate // so the algorithm time is aligned before OnWarmupFinished fires - if (!Algorithm.LiveMode && Algorithm.IsWarmingUp && timeSlice.Time > _warmupEndUtc) + if (Algorithm.IsWarmingUp && timeSlice.Time > WarmupEndUtc) { - yield return TimeSliceFactory.CreateTimePulse(_warmupEndUtc); + yield return TimeSliceFactory.CreateTimePulse(WarmupEndUtc); } if (timeSlice.IsTimePulse && Algorithm.UtcTime == timeSlice.Time) @@ -175,7 +179,7 @@ protected virtual void PostInitialize() // this is set after the algorithm initializes _dateTimeZone = Algorithm.TimeZone; - _warmupEndUtc = Algorithm.StartDate.ConvertToUtc(_dateTimeZone); + WarmupEndUtc = Algorithm.StartDate.ConvertToUtc(_dateTimeZone); TimeSliceFactory = new TimeSliceFactory(_dateTimeZone); SubscriptionSynchronizer.SetTimeSliceFactory(TimeSliceFactory); } From b54821a473a81f2d02fa647c7dc58e0cfecac12f Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Fri, 8 May 2026 12:18:59 -0500 Subject: [PATCH 4/5] Minor fix --- Engine/DataFeeds/LiveSynchronizer.cs | 2 +- Engine/DataFeeds/Synchronizer.cs | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Engine/DataFeeds/LiveSynchronizer.cs b/Engine/DataFeeds/LiveSynchronizer.cs index ac78a87e5f05..f84eaadd109b 100644 --- a/Engine/DataFeeds/LiveSynchronizer.cs +++ b/Engine/DataFeeds/LiveSynchronizer.cs @@ -139,7 +139,7 @@ public override IEnumerable StreamData(CancellationToken cancellation // check for cancellation if (timeSlice == null || cancellationToken.IsCancellationRequested) break; - if (Algorithm.IsWarmingUp && timeSlice.Time > WarmupEndUtc) + if (ShouldEmitWarmupEndPulse(timeSlice)) { yield return TimeSliceFactory.CreateTimePulse(WarmupEndUtc); } diff --git a/Engine/DataFeeds/Synchronizer.cs b/Engine/DataFeeds/Synchronizer.cs index 40f82a4a6d86..0e3195bcb385 100644 --- a/Engine/DataFeeds/Synchronizer.cs +++ b/Engine/DataFeeds/Synchronizer.cs @@ -121,9 +121,7 @@ public virtual IEnumerable StreamData(CancellationToken cancellationT // check for cancellation if (timeSlice == null || cancellationToken.IsCancellationRequested) break; - // If the first post warmup slice skips past StartDate, emit a time pulse at StartDate - // so the algorithm time is aligned before OnWarmupFinished fires - if (Algorithm.IsWarmingUp && timeSlice.Time > WarmupEndUtc) + if (ShouldEmitWarmupEndPulse(timeSlice)) { yield return TimeSliceFactory.CreateTimePulse(WarmupEndUtc); } @@ -162,6 +160,12 @@ public virtual IEnumerable StreamData(CancellationToken cancellationT Log.Trace("Synchronizer.GetEnumerator(): Exited thread."); } + /// + /// Returns true when the first post warmup slice skips past StartDate + /// so a time pulse can be emitted to align algorithm time before OnWarmupFinished fires + /// + protected bool ShouldEmitWarmupEndPulse(TimeSlice timeSlice) => Algorithm.IsWarmingUp && timeSlice.Time > WarmupEndUtc; + /// /// Performs additional initialization steps after algorithm initialization /// From 5c0ecf17f4123612ec2edda84b3f5980a9c8a4f5 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Fri, 8 May 2026 16:46:38 -0500 Subject: [PATCH 5/5] Skip warmup pulse if algorithm not locked --- Engine/DataFeeds/Synchronizer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Engine/DataFeeds/Synchronizer.cs b/Engine/DataFeeds/Synchronizer.cs index 0e3195bcb385..86f85e1764cf 100644 --- a/Engine/DataFeeds/Synchronizer.cs +++ b/Engine/DataFeeds/Synchronizer.cs @@ -164,7 +164,10 @@ public virtual IEnumerable StreamData(CancellationToken cancellationT /// Returns true when the first post warmup slice skips past StartDate /// so a time pulse can be emitted to align algorithm time before OnWarmupFinished fires /// - protected bool ShouldEmitWarmupEndPulse(TimeSlice timeSlice) => Algorithm.IsWarmingUp && timeSlice.Time > WarmupEndUtc; + protected bool ShouldEmitWarmupEndPulse(TimeSlice timeSlice) + { + return Algorithm.GetLocked() && Algorithm.IsWarmingUp && timeSlice.Time > WarmupEndUtc; + } /// /// Performs additional initialization steps after algorithm initialization