Lean  $LEAN_TAG$
FillForwardEnumerator.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 System.Collections;
19 using System.Collections.Generic;
20 using System.Linq;
21 using NodaTime;
22 using QuantConnect.Data;
25 using QuantConnect.Logging;
27 using QuantConnect.Util;
28 
30 {
31  /// <summary>
32  /// The FillForwardEnumerator wraps an existing base data enumerator and inserts extra 'base data' instances
33  /// on a specified fill forward resolution
34  /// </summary>
35  public class FillForwardEnumerator : IEnumerator<BaseData>
36  {
37  private DateTime? _delistedTime;
38  private BaseData _previous;
39  private bool _ended;
40  private bool _isFillingForward;
41 
42  private readonly bool _useStrictEndTime;
43  private readonly TimeSpan _dataResolution;
44  private readonly DateTimeZone _dataTimeZone;
45  private readonly bool _isExtendedMarketHours;
46  private readonly DateTime _subscriptionEndTime;
47  private readonly CalendarInfo _subscriptionEndDataCalendar;
48  private readonly IEnumerator<BaseData> _enumerator;
49  private readonly IReadOnlyRef<TimeSpan> _fillForwardResolution;
50  private readonly bool _strictEndTimeIntraDayFillForward;
51 
52  /// <summary>
53  /// The exchange used to determine when to insert fill forward data
54  /// </summary>
55  protected SecurityExchange Exchange { get; init; }
56 
57  /// <summary>
58  /// Initializes a new instance of the <see cref="FillForwardEnumerator"/> class that accepts
59  /// a reference to the fill forward resolution, useful if the fill forward resolution is dynamic
60  /// and changing as the enumeration progresses
61  /// </summary>
62  /// <param name="enumerator">The source enumerator to be filled forward</param>
63  /// <param name="exchange">The exchange used to determine when to insert fill forward data</param>
64  /// <param name="fillForwardResolution">The resolution we'd like to receive data on</param>
65  /// <param name="isExtendedMarketHours">True to use the exchange's extended market hours, false to use the regular market hours</param>
66  /// <param name="subscriptionEndTime">The end time of the subscription, once passing this date the enumerator will stop</param>
67  /// <param name="dataResolution">The source enumerator's data resolution</param>
68  /// <param name="dataTimeZone">The time zone of the underlying source data. This is used for rounding calculations and
69  /// is NOT the time zone on the BaseData instances (unless of course data time zone equals the exchange time zone)</param>
70  /// <param name="dailyStrictEndTimeEnabled">True if daily strict end times are enabled</param>
71  /// <param name="dataType">The configuration data type this enumerator is for</param>
72  public FillForwardEnumerator(IEnumerator<BaseData> enumerator,
73  SecurityExchange exchange,
74  IReadOnlyRef<TimeSpan> fillForwardResolution,
75  bool isExtendedMarketHours,
76  DateTime subscriptionEndTime,
77  TimeSpan dataResolution,
78  DateTimeZone dataTimeZone,
79  bool dailyStrictEndTimeEnabled,
80  Type dataType = null
81  )
82  {
83  _subscriptionEndTime = subscriptionEndTime;
84  Exchange = exchange;
85  _enumerator = enumerator;
86  _dataResolution = dataResolution;
87  _dataTimeZone = dataTimeZone;
88  _fillForwardResolution = fillForwardResolution;
89  _isExtendedMarketHours = isExtendedMarketHours;
90  _useStrictEndTime = dailyStrictEndTimeEnabled;
91  // OI data is fill-forwarded to the market close time when strict end times is enabled.
92  // Open interest data can arrive at any time and this would allow to synchronize it with trades and quotes when daily
93  // strict end times is enabled
94  _strictEndTimeIntraDayFillForward = dailyStrictEndTimeEnabled && dataType != null && dataType == typeof(OpenInterest);
95 
96  // '_dataResolution' and '_subscriptionEndTime' are readonly they won't change, so lets calculate this once here since it's expensive.
97  // if _useStrictEndTime and also _strictEndTimeIntraDayFillForward, this is a subscription with data that is not adjusted
98  // for the strict end time (like open interest) but require fill forward to synchronize with other data.
99  // Use the non strict end time calendar for the last day of data so that all data for that date is emitted.
100  if (_useStrictEndTime && !_strictEndTimeIntraDayFillForward)
101  {
102  var lastDayCalendar = GetDailyCalendar(_subscriptionEndTime);
103  while (lastDayCalendar.End > _subscriptionEndTime)
104  {
105  lastDayCalendar = GetDailyCalendar(lastDayCalendar.Start.AddDays(-1));
106  }
107  _subscriptionEndDataCalendar = lastDayCalendar;
108  }
109  else
110  {
111  _subscriptionEndDataCalendar = new (RoundDown(_subscriptionEndTime, _dataResolution), _dataResolution);
112  }
113  }
114 
115  /// <summary>
116  /// Gets the element in the collection at the current position of the enumerator.
117  /// </summary>
118  /// <returns>
119  /// The element in the collection at the current position of the enumerator.
120  /// </returns>
121  public BaseData Current
122  {
123  get;
124  private set;
125  }
126 
127  /// <summary>
128  /// Gets the current element in the collection.
129  /// </summary>
130  /// <returns>
131  /// The current element in the collection.
132  /// </returns>
133  /// <filterpriority>2</filterpriority>
134  object IEnumerator.Current => Current;
135 
136  /// <summary>
137  /// Advances the enumerator to the next element of the collection.
138  /// </summary>
139  /// <returns>
140  /// true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection.
141  /// </returns>
142  /// <exception cref="T:System.InvalidOperationException">The collection was modified after the enumerator was created. </exception><filterpriority>2</filterpriority>
143  public bool MoveNext()
144  {
145  if (_delistedTime.HasValue)
146  {
147  // don't fill forward after data after the delisted date
148  if (_previous == null || _previous.EndTime >= _delistedTime.Value)
149  {
150  return false;
151  }
152  }
153 
154  if (Current != null && Current.DataType != MarketDataType.Auxiliary)
155  {
156  // only set the _previous if the last item we emitted was NOT auxilliary data,
157  // since _previous is used for fill forward behavior
158  _previous = Current;
159  }
160 
161  BaseData fillForward;
162 
163  if (!_isFillingForward)
164  {
165  // if we're filling forward we don't need to move next since we haven't emitted _enumerator.Current yet
166  if (!_enumerator.MoveNext())
167  {
168  _ended = true;
169  if (_delistedTime.HasValue)
170  {
171  // don't fill forward delisted data
172  return false;
173  }
174 
175  // check to see if we ran out of data before the end of the subscription
176  if (_previous == null || _previous.EndTime >= _subscriptionEndTime)
177  {
178  // we passed the end of subscription, we're finished
179  return false;
180  }
181 
182  // we can fill forward the rest of this subscription if required
183  var endOfSubscription = (Current ?? _previous).Clone(true);
184  endOfSubscription.Time = _subscriptionEndDataCalendar.Start;
185  endOfSubscription.EndTime = endOfSubscription.Time + _subscriptionEndDataCalendar.Period;
186  if (RequiresFillForwardData(_fillForwardResolution.Value, _previous, endOfSubscription, out fillForward))
187  {
188  // don't mark as filling forward so we come back into this block, subscription is done
189  //_isFillingForward = true;
190  Current = fillForward;
191  return true;
192  }
193 
194  // don't emit the last bar if the market isn't considered open!
195  if (!Exchange.IsOpenDuringBar(endOfSubscription.Time, endOfSubscription.EndTime, _isExtendedMarketHours))
196  {
197  return false;
198  }
199 
200  if (Current != null && Current.EndTime == endOfSubscription.EndTime
201  // TODO this changes stats, why would the FF enumerator emit a data point beyoned the end time he was requested
202  //|| endOfSubscription.EndTime > _subscriptionEndTime
203  )
204  {
205  return false;
206  }
207  Current = endOfSubscription;
208  return true;
209  }
210  }
211  // If we are filling forward and the underlying is null, let's MoveNext() as long as it didn't end.
212  // This only applies for live trading, so that the LiveFillForwardEnumerator does not stall whenever
213  // we generate a fill-forward bar. The underlying enumerator is advanced so that we don't get stuck
214  // in a cycle of generating infinite fill-forward bars.
215  else if (_enumerator.Current == null && !_ended)
216  {
217  _ended = _enumerator.MoveNext();
218  }
219 
220  var underlyingCurrent = _enumerator.Current;
221  if (underlyingCurrent != null && underlyingCurrent.DataType == MarketDataType.Auxiliary)
222  {
223  var delisting = underlyingCurrent as Delisting;
224  if (delisting?.Type == DelistingType.Delisted)
225  {
226  _delistedTime = delisting.EndTime;
227  }
228  }
229 
230  if (_previous == null)
231  {
232  // first data point we dutifully emit without modification
233  Current = underlyingCurrent;
234  return true;
235  }
236 
237  if (RequiresFillForwardData(_fillForwardResolution.Value, _previous, underlyingCurrent, out fillForward))
238  {
239  if (_previous.EndTime >= _subscriptionEndTime)
240  {
241  // we passed the end of subscription, we're finished
242  return false;
243  }
244  // we require fill forward data because the _enumerator.Current is too far in future
245  _isFillingForward = true;
246  Current = fillForward;
247  return true;
248  }
249 
250  _isFillingForward = false;
251  Current = underlyingCurrent;
252  return true;
253  }
254 
255  /// <summary>
256  /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
257  /// </summary>
258  /// <filterpriority>2</filterpriority>
259  public void Dispose()
260  {
261  _enumerator.Dispose();
262  }
263 
264  /// <summary>
265  /// Sets the enumerator to its initial position, which is before the first element in the collection.
266  /// </summary>
267  /// <exception cref="T:System.InvalidOperationException">The collection was modified after the enumerator was created. </exception><filterpriority>2</filterpriority>
268  public void Reset()
269  {
270  _enumerator.Reset();
271  }
272 
273  /// <summary>
274  /// Determines whether or not fill forward is required, and if true, will produce the new fill forward data
275  /// </summary>
276  /// <param name="fillForwardResolution"></param>
277  /// <param name="previous">The last piece of data emitted by this enumerator</param>
278  /// <param name="next">The next piece of data on the source enumerator</param>
279  /// <param name="fillForward">When this function returns true, this will have a non-null value, null when the function returns false</param>
280  /// <returns>True when a new fill forward piece of data was produced and should be emitted by this enumerator</returns>
281  protected virtual bool RequiresFillForwardData(TimeSpan fillForwardResolution, BaseData previous, BaseData next, out BaseData fillForward)
282  {
283  // in live trading next can be null, in which case we create a potential FF bar and the live FF enumerator will decide what to do
284  var nextCalculatedEndTimeUtc = DateTime.MaxValue;
285  if (next != null)
286  {
287  // convert times to UTC for accurate comparisons and differences across DST changes
288  var previousTimeUtc = previous.Time.ConvertToUtc(Exchange.TimeZone);
289  var nextTimeUtc = next.Time.ConvertToUtc(Exchange.TimeZone);
290  var nextEndTimeUtc = next.EndTime.ConvertToUtc(Exchange.TimeZone);
291 
292  if (nextEndTimeUtc < previousTimeUtc)
293  {
294  Log.Error("FillForwardEnumerator received data out of order. Symbol: " + previous.Symbol.ID);
295  fillForward = null;
296  return false;
297  }
298 
299  // check to see if the gap between previous and next warrants fill forward behavior
300  var nextPreviousTimeUtcDelta = nextTimeUtc - previousTimeUtc;
301  if (nextPreviousTimeUtcDelta <= fillForwardResolution &&
302  nextPreviousTimeUtcDelta <= _dataResolution &&
303  // even if there is no gap between the two data points, we still fill forward to ensure a FF bar is emitted at strict end time
304  !_strictEndTimeIntraDayFillForward)
305  {
306  fillForward = null;
307  return false;
308  }
309 
310  var period = _dataResolution;
311  if (_useStrictEndTime)
312  {
313  // the period is not the data resolution (1 day) and can actually change dynamically, for example early close/late open
314  period = next.EndTime - next.Time;
315  }
316  else if (next.Time == next.EndTime)
317  {
318  // we merge corporate event data points (mapping, delisting, splits, dividend) which do not have
319  // a period or resolution
320  period = TimeSpan.Zero;
321  }
322  nextCalculatedEndTimeUtc = nextTimeUtc + period;
323  }
324 
325  // every bar emitted MUST be of the data resolution.
326 
327  // compute end times of the four potential fill forward scenarios
328  // 1. the next fill forward bar. 09:00-10:00 followed by 10:00-11:00 where 01:00 is the fill forward resolution
329  // 2. the next data resolution bar, same as above but with the data resolution instead
330  // 3. the next fill forward bar following the next market open, 15:00-16:00 followed by 09:00-10:00 the following open market day
331  // 4. the next data resolution bar following the next market open, same as above but with the data resolution instead
332 
333  // the precedence for validation is based on the order of the end times, obviously if a potential match
334  // is before a later match, the earliest match should win.
335 
336  foreach (var item in GetSortedReferenceDateIntervals(previous, fillForwardResolution, _dataResolution))
337  {
338  // issue GH 4925 , more description https://github.com/QuantConnect/Lean/pull/4941
339  // To build Time/EndTime we always use '+'/'-' dataResolution
340  // DataTime TZ = UTC -5; Exchange TZ = America/New York (-5/-4)
341  // Standard TimeZone 00:00:00 + 1 day = 1.00:00:00
342  // Daylight Time 01:00:00 + 1 day = 1.01:00:00
343 
344  // daylight saving time starts/end at 2 a.m. on Sunday
345  // Having this information we find that the specific bar of Sunday
346  // Starts in one TZ (Standard TZ), but finishes in another (Daylight TZ) (consider winter => summer)
347  // During simple arithmetic operations like +/- we shift the time, but not the time zone
348  // which is sensitive for specific dates (daylight movement) if we are in Exchange TimeZone, for example
349  // We have 00:00:00 + 1 day = 1.00:00:00, so both are in Standard TZ, but we expect endTime in Daylight, i.e. 1.01:00:00
350 
351  // futher down double Convert (Exchange TZ => data TZ => Exchange TZ)
352  // allows us to calculate Time using it's own TZ (aka reapply)
353  // and don't rely on TZ of bar start/end time
354  // i.e. 00:00:00 + 1 day = 1.01:00:00, both start and end are in their own TZ
355  // it's interesting that NodaTime consider next
356  // if time great or equal than 01:00 AM it's considered as "moved" (Standard, not Daylight)
357  // when time less than 01:00 AM it's considered as previous TZ (Standard, not Daylight)
358  // it's easy to fix this behavior by substract 1 tick before first convert, and then return it back.
359  // so we work with 0:59:59.. AM instead.
360  // but now follow native behavior
361 
362  // all above means, that all Time values, calculated using simple +/- operations
363  // sticks to original Time Zone, swallowing its own TZ and movement i.e.
364  // EndTime = Time + resolution, both Time and EndTime in the TZ of Time (Standard/Daylight)
365  // Time = EndTime - resolution, both Time and EndTime in the TZ of EndTime (Standard/Daylight)
366 
367  // next.EndTime sticks to Time TZ,
368  // potentialBarEndTime should be calculated in the same way as bar.EndTime, i.e. Time + resolution
369  // round down doesn't make sense for daily data using strict times
370  var startTime = (_useStrictEndTime && item.Period > Time.OneHour) ? item.Start : RoundDown(item.Start, item.Period);
371  var potentialBarEndTime = startTime.ConvertToUtc(Exchange.TimeZone) + item.Period;
372 
373  // to avoid duality it's necessary to compare potentialBarEndTime with
374  // next.EndTime calculated as Time + resolution,
375  // and both should be based on the same TZ (for example UTC)
376  if (potentialBarEndTime < nextCalculatedEndTimeUtc
377  // let's fill forward based on previous (which isn't auxiliary) if next is auxiliary and they share the end time
378  // we do allow emitting both an auxiliary data point and a Filled Forwared data for the same end time
379  || next != null && next.DataType == MarketDataType.Auxiliary && potentialBarEndTime == nextCalculatedEndTimeUtc)
380  {
381  // to check open hours we need to convert potential
382  // bar EndTime into exchange time zone
383  var potentialBarEndTimeInExchangeTZ =
384  potentialBarEndTime.ConvertFromUtc(Exchange.TimeZone);
385  var nextFillForwardBarStartTime = potentialBarEndTimeInExchangeTZ - item.Period;
386 
387  if (Exchange.IsOpenDuringBar(nextFillForwardBarStartTime, potentialBarEndTimeInExchangeTZ, _isExtendedMarketHours))
388  {
389  fillForward = previous.Clone(true);
390 
391  // bar are ALWAYS of the data resolution
392  var expectedPeriod = _dataResolution;
393  if (_useStrictEndTime)
394  {
395  // TODO: what about extended market hours
396  // NOTE: Not using Exchange.Hours.RegularMarketDuration so we can handle things like early closes.
397 
398  // The earliest start time would be endTime - regularMarketDuration,
399  // we use that as the potential time to get the exchange hours.
400  // We don't use directly nextFillForwardBarStartTime because there might be cases where there are
401  // adjacent extended and regular market hours segments that might cause the calendar start to be
402  // in the previous date, and if it's an extended hours-only date like a Sunday for futures,
403  // the market duration would be zero.
404  var marketHoursDateTime = potentialBarEndTimeInExchangeTZ - Exchange.Hours.RegularMarketDuration;
405  // That potential start is even before the calendar start, so we use the calendar start
406  if (marketHoursDateTime < item.Start)
407  {
408  marketHoursDateTime = item.Start;
409  }
410  var marketHours = Exchange.Hours.GetMarketHours(marketHoursDateTime);
411  expectedPeriod = marketHours.MarketDuration;
412  }
413  fillForward.Time = (potentialBarEndTime - expectedPeriod).ConvertFromUtc(Exchange.TimeZone);
414  fillForward.EndTime = potentialBarEndTimeInExchangeTZ;
415  return true;
416  }
417  }
418  else
419  {
420  break;
421  }
422  }
423 
424  // the next is before the next fill forward time, so do nothing
425  fillForward = null;
426  return false;
427  }
428 
429  private IEnumerable<CalendarInfo> GetSortedReferenceDateIntervals(BaseData previous, TimeSpan fillForwardResolution, TimeSpan dataResolution)
430  {
431  if (fillForwardResolution < dataResolution)
432  {
433  return GetReferenceDateIntervals(previous.EndTime, fillForwardResolution, dataResolution);
434  }
435 
436  if (fillForwardResolution > dataResolution)
437  {
438  return GetReferenceDateIntervals(previous.EndTime, dataResolution, fillForwardResolution);
439  }
440 
441  return GetReferenceDateIntervals(previous.EndTime, fillForwardResolution);
442  }
443 
444  /// <summary>
445  /// Get potential next fill forward bars.
446  /// </summary>
447  /// <remarks>Special case where fill forward resolution and data resolution are equal</remarks>
448  private IEnumerable<CalendarInfo> GetReferenceDateIntervals(DateTime previousEndTime, TimeSpan resolution)
449  {
450  // say daily bar goes from 9:30 to 16:00, if resolution is 1 day, IsOpenDuringBar can return true but it's not what we want
451  if (!_useStrictEndTime && Exchange.IsOpenDuringBar(previousEndTime, previousEndTime + resolution, _isExtendedMarketHours))
452  {
453  // if next in market us it
454  yield return new (previousEndTime, resolution);
455  }
456 
457  if (_useStrictEndTime)
458  {
459  // If we're using strict end times for open interest data, for instance, the actual data comes at any time
460  // but we want to emit a ff point at market close. If extended market hours are enabled, and previousEndTime
461  // is Thursday after last segment open time, the daily calendar will be for Monday, because a next market open
462  // won't be found for Friday. So we use the Date of the previousEndTime to get calendar starting that day (Thursday)
463  // and ending the next one (Friday).
464  if (_strictEndTimeIntraDayFillForward)
465  {
466  var firtMarketOpen = Exchange.Hours.GetNextMarketOpen(previousEndTime.Date, _isExtendedMarketHours);
467  var firstCalendar = LeanData.GetDailyCalendar(firtMarketOpen, Exchange.Hours, false);
468 
469  if (firstCalendar.End > previousEndTime)
470  {
471  yield return firstCalendar;
472  }
473  }
474 
475  // now we can try the bar after next market open
476  var marketOpen = Exchange.Hours.GetNextMarketOpen(previousEndTime, false);
477  yield return GetDailyCalendar(marketOpen);
478  }
479  else
480  {
481  // now we can try the bar after next market open
482  var marketOpen = Exchange.Hours.GetNextMarketOpen(previousEndTime, _isExtendedMarketHours);
483  yield return new(marketOpen, resolution);
484  }
485  }
486 
487  /// <summary>
488  /// Get potential next fill forward bars.
489  /// </summary>
490  private IEnumerable<CalendarInfo> GetReferenceDateIntervals(DateTime previousEndTime, TimeSpan smallerResolution, TimeSpan largerResolution)
491  {
492  List<CalendarInfo> result = null;
493  if (Exchange.IsOpenDuringBar(previousEndTime, previousEndTime + smallerResolution, _isExtendedMarketHours))
494  {
495  if (_useStrictEndTime)
496  {
497  // case A
498  result = new()
499  {
500  new(previousEndTime, smallerResolution)
501  };
502  }
503  else
504  {
505  // at the end of this method we perform an OrderBy which does not apply for this case because the consumer of this method
506  // will perform a round down that will end up using an unexpected FF bar. This behavior is covered by tests
507  yield return new (previousEndTime, smallerResolution);
508  }
509  }
510  result ??= new List<CalendarInfo>(4);
511 
512  // we need to round down because previous end time could be of the smaller resolution, in data TZ!
513  if (_useStrictEndTime)
514  {
515  // case B: say smaller resolution (FF res) is 1 hour, larget resolution (daily data resolution) is 1 day
516  // For example for SPX we need to emit the daily FF bar from 8:30->15:15, even before the 'A' case above which would be 15->16 bar
517  var dailyCalendar = GetDailyCalendar(previousEndTime);
518  if (previousEndTime < (dailyCalendar.Start + dailyCalendar.Period))
519  {
520  result.Add(new(dailyCalendar.Start, dailyCalendar.Period));
521  }
522  }
523  else
524  {
525  var start = RoundDown(previousEndTime, largerResolution);
526  if (Exchange.IsOpenDuringBar(start, start + largerResolution, _isExtendedMarketHours))
527  {
528  result.Add(new(start, largerResolution));
529  }
530  }
531 
532  // this is typically daily data being filled forward on a higher resolution
533  // since the previous bar was not in market hours then we can just fast forward
534  // to the next market open
535  var marketOpen = Exchange.Hours.GetNextMarketOpen(previousEndTime, _isExtendedMarketHours);
536  result.Add(new (marketOpen, smallerResolution));
537  if (_useStrictEndTime)
538  {
539  result.Add(GetDailyCalendar(Exchange.Hours.GetNextMarketOpen(previousEndTime, false)));
540  }
541 
542  // we need to order them because they might not be in an incremental order and consumer expects them to be
543  foreach (var referenceDateInterval in result.OrderBy(interval => interval.Start + interval.Period))
544  {
545  yield return referenceDateInterval;
546  }
547  }
548 
549  /// <summary>
550  /// We need to round down in data timezone.
551  /// For example GH issue 4392: Forex daily data, exchange tz time is 8PM, but time in data tz is 12AM
552  /// so rounding down on exchange tz will crop it, while rounding on data tz will return the same data point time.
553  /// Why are we even doing this? being able to determine the next valid data point for a resolution from a data point that might be in another resolution
554  /// </summary>
555  private DateTime RoundDown(DateTime value, TimeSpan interval)
556  {
557  return value.RoundDownInTimeZone(interval, Exchange.TimeZone, _dataTimeZone);
558  }
559 
560  private CalendarInfo GetDailyCalendar(DateTime localReferenceTime)
561  {
562  // daily data does not have extended market hours, even if requested
563  // and it's times are always market hours if using strict end times see 'SetStrictEndTimes'
564  return LeanData.GetDailyCalendar(localReferenceTime, Exchange.Hours, extendedMarketHours: false);
565  }
566  }
567 }