Skip to content

Commit 9046162

Browse files
authored
Fix OnWarmupFinished algorithm time alignment (#9445)
* Align OnWarmupFinished time to StartDate when ScheduledUniverse skips midnight * Align algorithm time to StartDate before OnWarmupFinished fires * Apply warmup time alignment fix to LiveSynchronizer * Minor fix * Skip warmup pulse if algorithm not locked * Fix OnWarmupFinished timing in live trading
1 parent 0c874da commit 9046162

3 files changed

Lines changed: 136 additions & 0 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using QuantConnect.Algorithm.Framework.Selection;
19+
using QuantConnect.Interfaces;
20+
21+
namespace QuantConnect.Algorithm.CSharp
22+
{
23+
/// <summary>
24+
/// Regression algorithm asserting OnWarmupFinished fires at StartDate (midnight)
25+
/// when using a ScheduledUniverseSelectionModel that triggers at 8 AM, skipping midnight entirely.
26+
/// </summary>
27+
public class OnWarmupFinishedScheduledUniverseRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
28+
{
29+
private bool _onWarmupFinishedCalled;
30+
31+
public override void Initialize()
32+
{
33+
SetStartDate(2013, 10, 08);
34+
SetEndDate(2013, 10, 11);
35+
SetCash(100000);
36+
37+
UniverseSettings.Resolution = Resolution.Minute;
38+
SetWarmup(TimeSpan.FromDays(1));
39+
40+
// Universe triggers at 8 AM
41+
SetUniverseSelection(new ScheduledUniverseSelectionModel(
42+
DateRules.EveryDay(),
43+
TimeRules.At(8, 0),
44+
_ => new[] { QuantConnect.Symbol.Create("SPY", SecurityType.Equity, Market.USA) }
45+
));
46+
}
47+
48+
public override void OnWarmupFinished()
49+
{
50+
_onWarmupFinishedCalled = true;
51+
52+
if (Time != StartDate)
53+
{
54+
throw new RegressionTestException(
55+
$"Expected OnWarmupFinished to fire at StartDate ({StartDate:yyyy-MM-dd HH:mm:ss}), " +
56+
$"but fired at {Time:yyyy-MM-dd HH:mm:ss}");
57+
}
58+
}
59+
60+
public override void OnEndOfAlgorithm()
61+
{
62+
if (!_onWarmupFinishedCalled)
63+
{
64+
throw new RegressionTestException("OnWarmupFinished was never called");
65+
}
66+
}
67+
68+
public bool CanRunLocally { get; } = true;
69+
70+
public List<Language> Languages { get; } = new() { Language.CSharp };
71+
72+
public long DataPoints => 3948;
73+
74+
public int AlgorithmHistoryDataPoints => 0;
75+
76+
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
77+
78+
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
79+
{
80+
{"Total Orders", "0"},
81+
{"Average Win", "0%"},
82+
{"Average Loss", "0%"},
83+
{"Compounding Annual Return", "0%"},
84+
{"Drawdown", "0%"},
85+
{"Expectancy", "0"},
86+
{"Start Equity", "100000"},
87+
{"End Equity", "100000"},
88+
{"Net Profit", "0%"},
89+
{"Sharpe Ratio", "0"},
90+
{"Sortino Ratio", "0"},
91+
{"Probabilistic Sharpe Ratio", "0%"},
92+
{"Loss Rate", "0%"},
93+
{"Win Rate", "0%"},
94+
{"Profit-Loss Ratio", "0"},
95+
{"Alpha", "0"},
96+
{"Beta", "0"},
97+
{"Annual Standard Deviation", "0"},
98+
{"Annual Variance", "0"},
99+
{"Information Ratio", "-31.448"},
100+
{"Tracking Error", "0.164"},
101+
{"Treynor Ratio", "0"},
102+
{"Total Fees", "$0.00"},
103+
{"Estimated Strategy Capacity", "$0"},
104+
{"Lowest Capacity Asset", ""},
105+
{"Portfolio Turnover", "0%"},
106+
{"Drawdown Recovery", "0"},
107+
{"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"}
108+
};
109+
}
110+
}

Engine/DataFeeds/LiveSynchronizer.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ public override IEnumerable<TimeSlice> StreamData(CancellationToken cancellation
139139
// check for cancellation
140140
if (timeSlice == null || cancellationToken.IsCancellationRequested) break;
141141

142+
if (ShouldEmitWarmupEndPulse(timeSlice))
143+
{
144+
yield return TimeSliceFactory.CreateTimePulse(WarmupEndUtc);
145+
}
146+
142147
var frontierUtc = FrontierTimeProvider.GetUtcNow();
143148
// emit on data or if we've elapsed a full second since last emit or there are security changes
144149
if (timeSlice.SecurityChanges != SecurityChanges.None
@@ -207,6 +212,7 @@ protected override void PostInitialize()
207212
{
208213
base.PostInitialize();
209214
_frontierTimeProvider.Initialize(base.GetTimeProvider());
215+
WarmupEndUtc = TimeProvider.GetUtcNow();
210216
}
211217

212218
/// <summary>

Engine/DataFeeds/Synchronizer.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ public class Synchronizer : ISynchronizer, IDataFeedTimeProvider, IDisposable
3131
{
3232
private DateTimeZone _dateTimeZone;
3333

34+
/// <summary>
35+
/// UTC time at which the warm up period ends
36+
/// </summary>
37+
protected DateTime WarmupEndUtc { get; set; }
38+
3439
/// <summary>
3540
/// The algorithm instance
3641
/// </summary>
@@ -123,6 +128,11 @@ public virtual IEnumerable<TimeSlice> StreamData(CancellationToken cancellationT
123128
continue;
124129
}
125130

131+
if (ShouldEmitWarmupEndPulse(timeSlice))
132+
{
133+
yield return TimeSliceFactory.CreateTimePulse(WarmupEndUtc);
134+
}
135+
126136
// SubscriptionFrontierTimeProvider will return twice the same time if there are no more subscriptions or if Subscription.Current is null
127137
if (timeSlice.Time != previousEmitTime || previousWasTimePulse || timeSlice.UniverseData.Count != 0)
128138
{
@@ -150,6 +160,15 @@ public virtual IEnumerable<TimeSlice> StreamData(CancellationToken cancellationT
150160
Log.Trace("Synchronizer.GetEnumerator(): Exited thread.");
151161
}
152162

163+
/// <summary>
164+
/// Returns true when the first post warmup slice skips past StartDate
165+
/// so a time pulse can be emitted to align algorithm time before OnWarmupFinished fires
166+
/// </summary>
167+
protected bool ShouldEmitWarmupEndPulse(TimeSlice timeSlice)
168+
{
169+
return Algorithm.GetLocked() && Algorithm.IsWarmingUp && timeSlice.Time > WarmupEndUtc;
170+
}
171+
153172
/// <summary>
154173
/// Performs additional initialization steps after algorithm initialization
155174
/// </summary>
@@ -167,6 +186,7 @@ protected virtual void PostInitialize()
167186

168187
// this is set after the algorithm initializes
169188
_dateTimeZone = Algorithm.TimeZone;
189+
WarmupEndUtc = Algorithm.StartDate.ConvertToUtc(_dateTimeZone);
170190
TimeSliceFactory = new TimeSliceFactory(_dateTimeZone);
171191
SubscriptionSynchronizer.SetTimeSliceFactory(TimeSliceFactory);
172192
}

0 commit comments

Comments
 (0)