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/DataFeeds/LiveSynchronizer.cs b/Engine/DataFeeds/LiveSynchronizer.cs
index d6b864038315..636165f0ecbf 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 (ShouldEmitWarmupEndPulse(timeSlice))
+ {
+ 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
@@ -207,6 +212,7 @@ protected override void PostInitialize()
{
base.PostInitialize();
_frontierTimeProvider.Initialize(base.GetTimeProvider());
+ WarmupEndUtc = TimeProvider.GetUtcNow();
}
///
diff --git a/Engine/DataFeeds/Synchronizer.cs b/Engine/DataFeeds/Synchronizer.cs
index 2404d53e9f82..5b15c32fd892 100644
--- a/Engine/DataFeeds/Synchronizer.cs
+++ b/Engine/DataFeeds/Synchronizer.cs
@@ -31,6 +31,11 @@ public class Synchronizer : ISynchronizer, IDataFeedTimeProvider, IDisposable
{
private DateTimeZone _dateTimeZone;
+ ///
+ /// UTC time at which the warm up period ends
+ ///
+ protected DateTime WarmupEndUtc { get; set; }
+
///
/// The algorithm instance
///
@@ -123,6 +128,11 @@ public virtual IEnumerable StreamData(CancellationToken cancellationT
continue;
}
+ if (ShouldEmitWarmupEndPulse(timeSlice))
+ {
+ yield return TimeSliceFactory.CreateTimePulse(WarmupEndUtc);
+ }
+
// SubscriptionFrontierTimeProvider will return twice the same time if there are no more subscriptions or if Subscription.Current is null
if (timeSlice.Time != previousEmitTime || previousWasTimePulse || timeSlice.UniverseData.Count != 0)
{
@@ -150,6 +160,15 @@ 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)
+ {
+ return Algorithm.GetLocked() && Algorithm.IsWarmingUp && timeSlice.Time > WarmupEndUtc;
+ }
+
///
/// Performs additional initialization steps after algorithm initialization
///
@@ -167,6 +186,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);
}