Lean  $LEAN_TAG$
DateChangeTimeKeeper.cs
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 
17 using System;
18 using QuantConnect.Util;
19 using QuantConnect.Data;
20 using System.Collections.Generic;
22 using System.Runtime.CompilerServices;
23 
25 {
26  /// <summary>
27  /// Time keeper specialization to keep time for a subscription both in data and exchange time zones.
28  /// It also emits events when the exchange date changes, which is useful to emit date change events
29  /// required for some daily actions like mapping symbols, delistings, splits, etc.
30  /// </summary>
31  internal class DateChangeTimeKeeper : TimeKeeper, IDisposable
32  {
33  private IEnumerator<DateTime> _tradableDatesInDataTimeZone;
34  private SubscriptionDataConfig _config;
35  private SecurityExchangeHours _exchangeHours;
36  private DateTime _delistingDate;
37 
38  private DateTime _previousNewExchangeDate;
39 
40  private bool _needsMoveNext;
41  private bool _initialized;
42 
43  private DateTime _exchangeTime;
44  private DateTime _dataTime;
45  private bool _exchangeTimeNeedsUpdate;
46  private bool _dataTimeNeedsUpdate;
47 
48  /// <summary>
49  /// The current time in the data time zone
50  /// </summary>
51  public DateTime DataTime
52  {
53  get
54  {
55  if (_dataTimeNeedsUpdate)
56  {
57  _dataTime = GetTimeIn(_config.DataTimeZone);
58  _dataTimeNeedsUpdate = false;
59  }
60  return _dataTime;
61  }
62  }
63 
64  /// <summary>
65  /// The current time in the exchange time zone
66  /// </summary>
67  public DateTime ExchangeTime
68  {
69  get
70  {
71  if (_exchangeTimeNeedsUpdate)
72  {
73  _exchangeTime = GetTimeIn(_exchangeHours.TimeZone);
74  _exchangeTimeNeedsUpdate = false;
75  }
76  return _exchangeTime;
77  }
78  }
79 
80  /// <summary>
81  /// Event that fires every time the exchange date changes
82  /// </summary>
83  public event EventHandler<DateTime> NewExchangeDate;
84 
85  /// <summary>
86  /// Initializes a new instance of the <see cref="DateChangeTimeKeeper"/> class
87  /// </summary>
88  /// <param name="tradableDatesInDataTimeZone">The tradable dates in data time zone</param>
89  /// <param name="config">The subscription data configuration this instance will keep track of time for</param>
90  /// <param name="exchangeHours">The exchange hours</param>
91  /// <param name="delistingDate">The symbol's delisting date</param>
92  public DateChangeTimeKeeper(IEnumerable<DateTime> tradableDatesInDataTimeZone, SubscriptionDataConfig config,
93  SecurityExchangeHours exchangeHours, DateTime delistingDate)
94  : base(Time.BeginningOfTime, new[] { config.DataTimeZone, config.ExchangeTimeZone })
95  {
96  _tradableDatesInDataTimeZone = tradableDatesInDataTimeZone.GetEnumerator();
97  _config = config;
98  _exchangeHours = exchangeHours;
99  _delistingDate = delistingDate;
100  _exchangeTimeNeedsUpdate = true;
101  _dataTimeNeedsUpdate = true;
102  _needsMoveNext = true;
103  }
104 
105  /// <summary>
106  /// Disposes the resources
107  /// </summary>
108  public void Dispose()
109  {
110  _tradableDatesInDataTimeZone.DisposeSafely();
111  }
112 
113  /// <summary>
114  /// Sets the current UTC time for this time keeper
115  /// </summary>
116  /// <param name="utcDateTime">The current time in UTC</param>
117  public override void SetUtcDateTime(DateTime utcDateTime)
118  {
119  base.SetUtcDateTime(utcDateTime);
120  _exchangeTimeNeedsUpdate = true;
121  _dataTimeNeedsUpdate = true;
122  }
123 
124  /// <summary>
125  /// Advances the time keeper towards the target exchange time.
126  /// If an exchange date is found before the target time, it is emitted and the time keeper is set to that date.
127  /// The caller must check whether the target time was reached or if the time keeper was set to a new exchange date before the target time.
128  /// </summary>
129  public void AdvanceTowardsExchangeTime(DateTime targetExchangeTime)
130  {
131  if (!_initialized)
132  {
133  throw new InvalidOperationException($"The time keeper has not been initialized. " +
134  $"{nameof(TryAdvanceUntilNextDataDate)} needs to be called at least once to flush the first date before advancing.");
135  }
136 
137  var currentExchangeTime = ExchangeTime;
138  // Advancing within the same exchange date, just update the time, no new exchange date will be emitted
139  if (targetExchangeTime.Date == currentExchangeTime.Date)
140  {
141  SetExchangeTime(targetExchangeTime);
142  return;
143  }
144 
145  while (currentExchangeTime < targetExchangeTime)
146  {
147  var newExchangeTime = currentExchangeTime + Time.OneDay;
148  if (newExchangeTime > targetExchangeTime)
149  {
150  newExchangeTime = targetExchangeTime;
151  }
152 
153  var newExchangeDate = newExchangeTime.Date;
154 
155  // We found a new exchange date before the target time, emit it first
156  if (newExchangeDate != currentExchangeTime.Date &&
157  _exchangeHours.IsDateOpen(newExchangeDate, _config.ExtendedMarketHours))
158  {
159  // Stop here, set the new exchange date
160  SetExchangeTime(newExchangeDate);
161  EmitNewExchangeDate(newExchangeDate);
162  return;
163  }
164 
165  currentExchangeTime = newExchangeTime;
166  }
167 
168  // We reached the target time, set it
169  SetExchangeTime(targetExchangeTime);
170  }
171 
172  /// <summary>
173  /// Advances the time keeper until the next data date, emitting the new exchange date if this happens before the new data date
174  /// </summary>
175  public bool TryAdvanceUntilNextDataDate()
176  {
177  if (!_initialized)
178  {
179  return EmitFirstExchangeDate();
180  }
181 
182  // Before moving forward, check whether we need to emit a new exchange date
183  if (TryEmitPassedExchangeDate())
184  {
185  return true;
186  }
187 
188  if (!_needsMoveNext || _tradableDatesInDataTimeZone.MoveNext())
189  {
190  var nextDataDate = _tradableDatesInDataTimeZone.Current;
191  var nextExchangeTime = nextDataDate.ConvertTo(_config.DataTimeZone, _exchangeHours.TimeZone);
192  var nextExchangeDate = nextExchangeTime.Date;
193 
194  if (nextExchangeDate > _delistingDate)
195  {
196  // We are done, but an exchange date might still need to be emitted
197  TryEmitPassedExchangeDate();
198  _needsMoveNext = false;
199  return false;
200  }
201 
202  // If the exchange is not behind the data, the data might have not been enough to emit the exchange date,
203  // which already passed if we are moving on to the next data date. So we need to check if we need to emit it here.
204  // e.g. moving data date from tuesday to wednesday, but the exchange date is already past the end of tuesday
205  // (by N hours, depending on the time zones offset). If data didn't trigger the exchange date change, we need to do it here.
206  if (!IsExchangeBehindData(nextExchangeTime, nextDataDate) && nextExchangeDate > _previousNewExchangeDate)
207  {
208  EmitNewExchangeDate(nextExchangeDate);
209  SetExchangeTime(nextExchangeDate);
210  // nextExchangeDate == DataTime means time zones are synchronized, need to move next only when exchange is actually ahead
211  _needsMoveNext = nextExchangeDate == DataTime;
212  return true;
213  }
214 
215  _needsMoveNext = true;
216  SetDataTime(nextDataDate);
217  return true;
218  }
219 
220  _needsMoveNext = false;
221  return false;
222  }
223 
224  /// <summary>
225  /// Emits the first exchange date for the algorithm so that the first daily events are triggered (mappings, delistings, etc.)
226  /// </summary>
227  /// <returns>True if the new exchange date is emitted. False if already done or the tradable dates enumerable is empty</returns>
228  private bool EmitFirstExchangeDate()
229  {
230  if (_initialized)
231  {
232  return false;
233  }
234 
235  if (!_tradableDatesInDataTimeZone.MoveNext())
236  {
237  _initialized = true;
238  return false;
239  }
240 
241  var firstDataDate = _tradableDatesInDataTimeZone.Current;
242  var firstExchangeTime = firstDataDate.ConvertTo(_config.DataTimeZone, _exchangeHours.TimeZone);
243  var firstExchangeDate = firstExchangeTime.Date;
244 
245  DateTime exchangeDateToEmit;
246  // The exchange is ahead of the data, so we need to emit the current exchange date, which already passed
247  if (firstExchangeTime < firstDataDate && _exchangeHours.IsDateOpen(firstExchangeDate, _config.ExtendedMarketHours))
248  {
249  exchangeDateToEmit = firstExchangeDate;
250  SetExchangeTime(exchangeDateToEmit);
251  // Don't move, the current data date still needs to be consumed
252  _needsMoveNext = false;
253  }
254  // The exchange is behind of (or in sync with) data: exchange has not passed to this new date, but with emit it here
255  // so that first daily things are done (mappings, delistings, etc.)
256  else
257  {
258  exchangeDateToEmit = firstDataDate;
259  SetDataTime(firstDataDate);
260  _needsMoveNext = true;
261  }
262 
263  EmitNewExchangeDate(exchangeDateToEmit);
264  _initialized = true;
265  return true;
266  }
267 
268  /// <summary>
269  /// Determines whether the exchange time zone is behind the data time zone
270  /// </summary>
271  /// <returns></returns>
272  public bool IsExchangeBehindData()
273  {
274  return IsExchangeBehindData(ExchangeTime, DataTime);
275  }
276 
277  /// <summary>
278  /// Determines whether the exchange time zone is behind the data time zone
279  /// </summary>
280  private static bool IsExchangeBehindData(DateTime exchangeTime, DateTime dataTime)
281  {
282  return dataTime > exchangeTime;
283  }
284 
285  [MethodImpl(MethodImplOptions.AggressiveInlining)]
286  private void SetExchangeTime(DateTime exchangeTime)
287  {
288  SetUtcDateTime(exchangeTime.ConvertToUtc(_exchangeHours.TimeZone));
289  }
290 
291  [MethodImpl(MethodImplOptions.AggressiveInlining)]
292  private void SetDataTime(DateTime dataTime)
293  {
294  SetUtcDateTime(dataTime.ConvertToUtc(_config.DataTimeZone));
295  }
296 
297  /// <summary>
298  /// Checks that before moving to the next data date, if the exchange date has already passed and has been emitted, else it emits it.
299  /// This can happen when the exchange is behind of the data. e.g We advance data date from Monday to Tuesday, then the data itself
300  /// will drive the exchange data change (N hours later, depending on the time zones offset).
301  /// But if there is no enough data or the file is not found, the new exchange date will not be emitted, so we need to do it here.
302  /// </summary>
303  private bool TryEmitPassedExchangeDate()
304  {
305  if (_needsMoveNext && _tradableDatesInDataTimeZone.Current != default)
306  {
307  // This data date passed, and it should have emitted as an exchange tradable date when detected
308  // as a date change in the data itself, if not, emit it now before moving to the next data date
309  var currentDataDate = _tradableDatesInDataTimeZone.Current;
310  if (_previousNewExchangeDate < currentDataDate &&
311  _exchangeHours.IsDateOpen(currentDataDate, _config.ExtendedMarketHours))
312  {
313  var nextExchangeDate = currentDataDate;
314  SetExchangeTime(nextExchangeDate);
315  EmitNewExchangeDate(nextExchangeDate);
316  return true;
317  }
318  }
319 
320  return false;
321  }
322 
323  /// <summary>
324  /// Emits a new exchange date event
325  /// </summary>
326  private void EmitNewExchangeDate(DateTime newExchangeDate)
327  {
328  NewExchangeDate?.Invoke(this, newExchangeDate);
329  _previousNewExchangeDate = newExchangeDate;
330  }
331  }
332 }