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); }