Lean  $LEAN_TAG$
SecurityExchangeHours.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 using System;
17 using System.Collections.Generic;
18 using System.Linq;
19 using System.Runtime.CompilerServices;
20 using NodaTime;
21 using QuantConnect.Util;
22 
24 {
25  /// <summary>
26  /// Represents the schedule of a security exchange. This includes daily regular and extended market hours
27  /// as well as holidays, early closes and late opens.
28  /// </summary>
29  /// <remarks>
30  /// This type assumes that IsOpen will be called with increasingly future times, that is, the calls should never back
31  /// track in time. This assumption is required to prevent time zone conversions on every call.
32  /// </remarks>
33  public class SecurityExchangeHours
34  {
35  private HashSet<long> _holidays;
36  private IReadOnlyDictionary<DateTime, TimeSpan> _earlyCloses;
37  private IReadOnlyDictionary<DateTime, TimeSpan> _lateOpens;
38 
39  // these are listed individually for speed
40  private LocalMarketHours _sunday;
41  private LocalMarketHours _monday;
42  private LocalMarketHours _tuesday;
43  private LocalMarketHours _wednesday;
44  private LocalMarketHours _thursday;
45  private LocalMarketHours _friday;
46  private LocalMarketHours _saturday;
47  private Dictionary<DayOfWeek, LocalMarketHours> _openHoursByDay;
48  private static List<DayOfWeek> daysOfWeek = new List<DayOfWeek>() {
49  DayOfWeek.Sunday,
50  DayOfWeek.Monday,
51  DayOfWeek.Tuesday,
52  DayOfWeek.Wednesday,
53  DayOfWeek.Thursday,
54  DayOfWeek.Friday,
55  DayOfWeek.Saturday
56  };
57 
58  /// <summary>
59  /// Gets the time zone this exchange resides in
60  /// </summary>
61  public DateTimeZone TimeZone { get; private set; }
62 
63  /// <summary>
64  /// Gets the holidays for the exchange
65  /// </summary>
66  public HashSet<DateTime> Holidays
67  {
68  get { return _holidays.ToHashSet(x => new DateTime(x)); }
69  }
70 
71  /// <summary>
72  /// Gets the market hours for this exchange
73  /// </summary>
74  /// <remarks>
75  /// This returns the regular schedule for each day, without taking into account special cases
76  /// such as holidays, early closes, or late opens.
77  /// In order to get the actual market hours for a specific date, use <see cref="GetMarketHours(DateTime)"/>
78  /// </remarks>
79  public IReadOnlyDictionary<DayOfWeek, LocalMarketHours> MarketHours => _openHoursByDay;
80 
81  /// <summary>
82  /// Gets the early closes for this exchange
83  /// </summary>
84  public IReadOnlyDictionary<DateTime, TimeSpan> EarlyCloses => _earlyCloses;
85 
86  /// <summary>
87  /// Gets the late opens for this exchange
88  /// </summary>
89  public IReadOnlyDictionary<DateTime, TimeSpan> LateOpens => _lateOpens;
90 
91  /// <summary>
92  /// Gets the most common tradable time during the market week.
93  /// For a normal US equity trading day this is 6.5 hours.
94  /// This does NOT account for extended market hours and only
95  /// considers <see cref="MarketHoursState.Market"/>
96  /// </summary>
97  public TimeSpan RegularMarketDuration { get; private set; }
98 
99  /// <summary>
100  /// Checks whether the market is always open or not
101  /// </summary>
102  public bool IsMarketAlwaysOpen { private set; get; }
103 
104  /// <summary>
105  /// Gets a <see cref="SecurityExchangeHours"/> instance that is always open
106  /// </summary>
107  public static SecurityExchangeHours AlwaysOpen(DateTimeZone timeZone)
108  {
109  var dayOfWeeks = Enum.GetValues(typeof (DayOfWeek)).OfType<DayOfWeek>();
110  return new SecurityExchangeHours(timeZone,
111  Enumerable.Empty<DateTime>(),
112  dayOfWeeks.Select(LocalMarketHours.OpenAllDay).ToDictionary(x => x.DayOfWeek),
113  new Dictionary<DateTime, TimeSpan>(),
114  new Dictionary<DateTime, TimeSpan>()
115  );
116  }
117 
118  /// <summary>
119  /// Initializes a new instance of the <see cref="SecurityExchangeHours"/> class
120  /// </summary>
121  /// <param name="timeZone">The time zone the dates and hours are represented in</param>
122  /// <param name="holidayDates">The dates this exchange is closed for holiday</param>
123  /// <param name="marketHoursForEachDayOfWeek">The exchange's schedule for each day of the week</param>
124  /// <param name="earlyCloses">The dates this exchange has an early close</param>
125  /// <param name="lateOpens">The dates this exchange has a late open</param>
127  DateTimeZone timeZone,
128  IEnumerable<DateTime> holidayDates,
129  Dictionary<DayOfWeek, LocalMarketHours> marketHoursForEachDayOfWeek,
130  IReadOnlyDictionary<DateTime, TimeSpan> earlyCloses,
131  IReadOnlyDictionary<DateTime, TimeSpan> lateOpens)
132  {
133  TimeZone = timeZone;
134  _holidays = holidayDates.Select(x => x.Date.Ticks).ToHashSet();
135  _earlyCloses = earlyCloses;
136  _lateOpens = lateOpens;
137  _openHoursByDay = marketHoursForEachDayOfWeek;
138 
139  SetMarketHoursForDay(DayOfWeek.Sunday, out _sunday);
140  SetMarketHoursForDay(DayOfWeek.Monday, out _monday);
141  SetMarketHoursForDay(DayOfWeek.Tuesday, out _tuesday);
142  SetMarketHoursForDay(DayOfWeek.Wednesday, out _wednesday);
143  SetMarketHoursForDay(DayOfWeek.Thursday, out _thursday);
144  SetMarketHoursForDay(DayOfWeek.Friday, out _friday);
145  SetMarketHoursForDay(DayOfWeek.Saturday, out _saturday);
146 
147  // pick the most common market hours duration, if there's a tie, pick the larger duration
148  RegularMarketDuration = _openHoursByDay.Values.GroupBy(lmh => lmh.MarketDuration)
149  .OrderByDescending(grp => grp.Count())
150  .ThenByDescending(grp => grp.Key)
151  .First().Key;
152 
153  IsMarketAlwaysOpen = CheckIsMarketAlwaysOpen();
154  }
155 
156  /// <summary>
157  /// Determines if the exchange is open at the specified local date time.
158  /// </summary>
159  /// <param name="localDateTime">The time to check represented as a local time</param>
160  /// <param name="extendedMarketHours">True to use the extended market hours, false for just regular market hours</param>
161  /// <returns>True if the exchange is considered open at the specified time, false otherwise</returns>
162  public bool IsOpen(DateTime localDateTime, bool extendedMarketHours)
163  {
164  if (_holidays.Contains(localDateTime.Date.Ticks))
165  {
166  return false;
167  }
168 
169  return GetMarketHours(localDateTime).IsOpen(localDateTime.TimeOfDay, extendedMarketHours);
170  }
171 
172  /// <summary>
173  /// Determines if the exchange is open at any point in time over the specified interval.
174  /// </summary>
175  /// <param name="startLocalDateTime">The start of the interval in local time</param>
176  /// <param name="endLocalDateTime">The end of the interval in local time</param>
177  /// <param name="extendedMarketHours">True to use the extended market hours, false for just regular market hours</param>
178  /// <returns>True if the exchange is considered open at the specified time, false otherwise</returns>
179  [MethodImpl(MethodImplOptions.AggressiveInlining)]
180  public bool IsOpen(DateTime startLocalDateTime, DateTime endLocalDateTime, bool extendedMarketHours)
181  {
182  if (startLocalDateTime == endLocalDateTime)
183  {
184  // if we're testing an instantaneous moment, use the other function
185  return IsOpen(startLocalDateTime, extendedMarketHours);
186  }
187 
188  // we must make intra-day requests to LocalMarketHours, so check for a day gap
189  var start = startLocalDateTime;
190  var end = new DateTime(Math.Min(endLocalDateTime.Ticks, start.Date.Ticks + Time.OneDay.Ticks - 1));
191  do
192  {
193  if (!_holidays.Contains(start.Date.Ticks))
194  {
195  // check to see if the market is open
196  var marketHours = GetMarketHours(start);
197  if (marketHours.IsOpen(start.TimeOfDay, end.TimeOfDay, extendedMarketHours))
198  {
199  return true;
200  }
201  }
202 
203  start = start.Date.AddDays(1);
204  end = new DateTime(Math.Min(endLocalDateTime.Ticks, end.Ticks + Time.OneDay.Ticks));
205  }
206  while (end > start);
207 
208  return false;
209  }
210 
211  /// <summary>
212  /// Determines if the exchange will be open on the date specified by the local date time
213  /// </summary>
214  /// <param name="localDateTime">The date time to check if the day is open</param>
215  /// <returns>True if the exchange will be open on the specified date, false otherwise</returns>
216  public bool IsDateOpen(DateTime localDateTime)
217  {
218  var marketHours = GetMarketHours(localDateTime);
219  if (marketHours.IsClosedAllDay)
220  {
221  // if we don't have hours for this day then we're not open
222  return false;
223  }
224 
225  // if we don't have a holiday then we're open
226  return !_holidays.Contains(localDateTime.Date.Ticks);
227  }
228 
229  /// <summary>
230  /// Gets the local date time corresponding to the previous market open to the specified time
231  /// </summary>
232  /// <param name="localDateTime">The time to begin searching for the last market open (non-inclusive)</param>
233  /// <param name="extendedMarketHours">True to include extended market hours in the search</param>
234  /// <returns>The previous market opening date time to the specified local date time</returns>
235  public DateTime GetPreviousMarketOpen(DateTime localDateTime, bool extendedMarketHours)
236  {
237  var time = localDateTime;
238  var marketHours = GetMarketHours(time);
239  var nextMarketOpen = GetNextMarketOpen(time, extendedMarketHours);
240 
241  if (localDateTime == nextMarketOpen)
242  {
243  return localDateTime;
244  }
245 
246  // let's loop for a week
247  for (int i = 0; i < 7; i++)
248  {
249  foreach(var segment in marketHours.Segments.Reverse())
250  {
251  if ((time.Date + segment.Start <= localDateTime) &&
252  (segment.State == MarketHoursState.Market || extendedMarketHours))
253  {
254  // Check the current segment is not part of another segment before
255  var timeOfDay = time.Date + segment.Start;
256  if (GetNextMarketOpen(timeOfDay.AddTicks(-1), extendedMarketHours) == timeOfDay)
257  {
258  return timeOfDay;
259  }
260  }
261  }
262 
263  time = time.AddDays(-1);
264  marketHours = GetMarketHours(time);
265  }
266 
267  throw new InvalidOperationException(Messages.SecurityExchangeHours.LastMarketOpenNotFound(localDateTime, IsMarketAlwaysOpen));
268  }
269 
270  /// <summary>
271  /// Gets the local date time corresponding to the next market open following the specified time
272  /// </summary>
273  /// <param name="localDateTime">The time to begin searching for market open (non-inclusive)</param>
274  /// <param name="extendedMarketHours">True to include extended market hours in the search</param>
275  /// <returns>The next market opening date time following the specified local date time</returns>
276  public DateTime GetNextMarketOpen(DateTime localDateTime, bool extendedMarketHours)
277  {
278  var time = localDateTime;
279  var oneWeekLater = localDateTime.Date.AddDays(15);
280 
281  var lastDay = time.Date.AddDays(-1);
282  var lastDayMarketHours = GetMarketHours(lastDay);
283  var lastDaySegment = lastDayMarketHours.Segments.LastOrDefault();
284  do
285  {
286  var marketHours = GetMarketHours(time);
287  if (!marketHours.IsClosedAllDay && !_holidays.Contains(time.Date.Ticks))
288  {
289  var marketOpenTimeOfDay = marketHours.GetMarketOpen(time.TimeOfDay, extendedMarketHours, lastDaySegment?.End);
290  if (marketOpenTimeOfDay.HasValue)
291  {
292  var marketOpen = time.Date + marketOpenTimeOfDay.Value;
293  if (localDateTime < marketOpen)
294  {
295  return marketOpen;
296  }
297  }
298 
299  // If there was an early close the market opens until next day first segment,
300  // so we don't take into account continuous segments between days, then
301  // lastDaySegment should be null
302  if (_earlyCloses.ContainsKey(time.Date))
303  {
304  lastDaySegment = null;
305  }
306  else
307  {
308  lastDaySegment = marketHours.Segments.LastOrDefault();
309  }
310  }
311  else
312  {
313  lastDaySegment = null;
314  }
315 
316  time = time.Date + Time.OneDay;
317  }
318  while (time < oneWeekLater);
319 
320  throw new ArgumentException(Messages.SecurityExchangeHours.UnableToLocateNextMarketOpenInTwoWeeks);
321  }
322 
323  /// <summary>
324  /// Gets the local date time corresponding to the next market close following the specified time
325  /// </summary>
326  /// <param name="localDateTime">The time to begin searching for market close (non-inclusive)</param>
327  /// <param name="extendedMarketHours">True to include extended market hours in the search</param>
328  /// <returns>The next market closing date time following the specified local date time</returns>
329  public DateTime GetNextMarketClose(DateTime localDateTime, bool extendedMarketHours)
330  {
331  var time = localDateTime;
332  var oneWeekLater = localDateTime.Date.AddDays(15);
333  do
334  {
335  var marketHours = GetMarketHours(time);
336  if (!marketHours.IsClosedAllDay && !_holidays.Contains(time.Date.Ticks))
337  {
338  // Get next day first segment. This is made because we need to check the segment returned
339  // by GetMarketClose() ends at segment.End and not continues in the next segment. We get
340  // the next day first segment for the case in which the next market close is the last segment
341  // of the current day
342  var nextSegment = GetNextOrPreviousSegment(time, isNextDay: true);
343  var marketCloseTimeOfDay = marketHours.GetMarketClose(time.TimeOfDay, extendedMarketHours, nextSegment?.Start);
344  if (marketCloseTimeOfDay.HasValue)
345  {
346  var marketClose = time.Date + marketCloseTimeOfDay.Value;
347  if (localDateTime < marketClose)
348  {
349  return marketClose;
350  }
351  }
352  }
353 
354  time = time.Date + Time.OneDay;
355  }
356  while (time < oneWeekLater);
357 
358  throw new ArgumentException(Messages.SecurityExchangeHours.UnableToLocateNextMarketCloseInTwoWeeks);
359  }
360 
361  /// <summary>
362  /// Returns next day first segment or previous day last segment
363  /// </summary>
364  /// <param name="time">Time of reference</param>
365  /// <param name="isNextDay">True to get next day first segment. False to get previous day last segment</param>
366  /// <returns>Next day first segment or previous day last segment</returns>
367  private MarketHoursSegment GetNextOrPreviousSegment(DateTime time, bool isNextDay)
368  {
369  var nextOrPrevious = isNextDay ? 1 : -1;
370  var nextOrPreviousDay = time.Date.AddDays(nextOrPrevious);
371  if (_earlyCloses.ContainsKey(nextOrPreviousDay.Date))
372  {
373  return null;
374  }
375 
376  var segments = GetMarketHours(nextOrPreviousDay).Segments;
377  return isNextDay ? segments.FirstOrDefault() : segments.LastOrDefault();
378  }
379 
380  /// <summary>
381  /// Check whether the market is always open or not
382  /// </summary>
383  /// <returns>True if the market is always open, false otherwise</returns>
384  private bool CheckIsMarketAlwaysOpen()
385  {
386  LocalMarketHours marketHours = null;
387  for (var i = 0; i < daysOfWeek.Count; i++)
388  {
389  var day = daysOfWeek[i];
390  switch (day)
391  {
392  case DayOfWeek.Sunday:
393  marketHours = _sunday;
394  break;
395  case DayOfWeek.Monday:
396  marketHours = _monday;
397  break;
398  case DayOfWeek.Tuesday:
399  marketHours = _tuesday;
400  break;
401  case DayOfWeek.Wednesday:
402  marketHours = _wednesday;
403  break;
404  case DayOfWeek.Thursday:
405  marketHours = _thursday;
406  break;
407  case DayOfWeek.Friday:
408  marketHours = _friday;
409  break;
410  case DayOfWeek.Saturday:
411  marketHours = _saturday;
412  break;
413  }
414 
415  if (!marketHours.IsOpenAllDay)
416  {
417  return false;
418  }
419  }
420 
421  return true;
422  }
423 
424  /// <summary>
425  /// Helper to extract market hours from the <see cref="_openHoursByDay"/> dictionary, filling
426  /// in Closed instantes when not present
427  /// </summary>
428  private void SetMarketHoursForDay(DayOfWeek dayOfWeek, out LocalMarketHours localMarketHoursForDay)
429  {
430  if (!_openHoursByDay.TryGetValue(dayOfWeek, out localMarketHoursForDay))
431  {
432  // assign to our dictionary that we're closed this day, as well as our local field
433  _openHoursByDay[dayOfWeek] = localMarketHoursForDay = LocalMarketHours.ClosedAllDay(dayOfWeek);
434  }
435  }
436 
437  /// <summary>
438  /// Helper to access the market hours field based on the day of week
439  /// </summary>
440  /// <param name="localDateTime">The local date time to retrieve market hours for</param>
441  /// <remarks>
442  /// This method will return an adjusted instance of <see cref="LocalMarketHours"/> for the specified date,
443  /// that is, it will account for holidays, early closes, and late opens (e.g. if the security trades regularly on Mondays,
444  /// but a specific Monday is a holiday, this method will return a <see cref="LocalMarketHours"/> that is closed all day).
445  /// In order to get the regular schedule, use the <see cref="MarketHours"/> property.
446  /// </remarks>
447  public LocalMarketHours GetMarketHours(DateTime localDateTime)
448  {
449  if (_holidays.Contains(localDateTime.Ticks))
450  {
451  return new LocalMarketHours(localDateTime.DayOfWeek);
452  }
453 
454  LocalMarketHours marketHours;
455  switch (localDateTime.DayOfWeek)
456  {
457  case DayOfWeek.Sunday:
458  marketHours = _sunday;
459  break;
460  case DayOfWeek.Monday:
461  marketHours = _monday;
462  break;
463  case DayOfWeek.Tuesday:
464  marketHours = _tuesday;
465  break;
466  case DayOfWeek.Wednesday:
467  marketHours = _wednesday;
468  break;
469  case DayOfWeek.Thursday:
470  marketHours = _thursday;
471  break;
472  case DayOfWeek.Friday:
473  marketHours = _friday;
474  break;
475  case DayOfWeek.Saturday:
476  marketHours = _saturday;
477  break;
478  default:
479  throw new ArgumentOutOfRangeException(nameof(localDateTime), localDateTime, null);
480  }
481 
482  var hasEarlyClose = _earlyCloses.TryGetValue(localDateTime.Date, out var earlyCloseTime);
483  var hasLateOpen = _lateOpens.TryGetValue(localDateTime.Date, out var lateOpenTime);
484  if (!hasEarlyClose && !hasLateOpen)
485  {
486  return marketHours;
487  }
488 
489  IReadOnlyList<MarketHoursSegment> marketHoursSegments = marketHours.Segments;
490 
491  // If the earlyCloseTime is between a segment, change the close time with it
492  // and add it after the segments prior to the earlyCloseTime
493  // Otherwise, just take the segments prior to the earlyCloseTime
494  List<MarketHoursSegment> segmentsEarlyClose = null;
495  if (hasEarlyClose)
496  {
497  var index = marketHoursSegments.Count;
498  MarketHoursSegment newSegment = null;
499  for (var i = 0; i < marketHoursSegments.Count; i++)
500  {
501  var segment = marketHoursSegments[i];
502  if (segment.Start <= earlyCloseTime && earlyCloseTime <= segment.End)
503  {
504  newSegment = new MarketHoursSegment(segment.State, segment.Start, earlyCloseTime);
505  index = i;
506  break;
507  }
508  else if (earlyCloseTime < segment.Start)
509  {
510  // we will drop any remaining segment starting by this one
511  index = i - 1;
512  break;
513  }
514  }
515 
516  segmentsEarlyClose = new List<MarketHoursSegment>(marketHoursSegments.Take(index));
517  if (newSegment != null)
518  {
519  segmentsEarlyClose.Add(newSegment);
520  }
521  }
522 
523  // It could be the case we have a late open after an early close (the market resumes after the early close), in that case, we should take
524  // the segments before the early close and the the segments after the late opens and append them. Therefore, if that's not the case, this is,
525  // if there was an early close but there is not a late open or it's before the early close, we need to update the variable marketHours with
526  // the value of newMarketHours, so that it contains the segments before the early close
527  if (segmentsEarlyClose != null && (!hasLateOpen || earlyCloseTime >= lateOpenTime))
528  {
529  marketHoursSegments = segmentsEarlyClose;
530  }
531 
532  // If the lateOpenTime is between a segment, change the start time with it
533  // and add it before the segments previous to the lateOpenTime
534  // Otherwise, just take the segments previous to the lateOpenTime
535  List<MarketHoursSegment> segmentsLateOpen = null;
536  if (hasLateOpen)
537  {
538  var index = 0;
539  segmentsLateOpen = new List<MarketHoursSegment>();
540  for(var i = 0; i < marketHoursSegments.Count; i++)
541  {
542  var segment = marketHoursSegments[i];
543  if (segment.Start <= lateOpenTime && lateOpenTime <= segment.End)
544  {
545  segmentsLateOpen.Add(new (segment.State, lateOpenTime, segment.End));
546  index = i + 1;
547  break;
548  }
549  else if (lateOpenTime < segment.Start)
550  {
551  index = i;
552  break;
553  }
554  }
555 
556  segmentsLateOpen.AddRange(marketHoursSegments.TakeLast(marketHoursSegments.Count - index));
557  marketHoursSegments = segmentsLateOpen;
558  }
559 
560  // Since it could be the case we have a late open after an early close (the market resumes after the early close), we need to take
561  // the segments before the early close and the segments after the late open and append them to obtain the expected market hours
562  if (segmentsEarlyClose != null && hasLateOpen && earlyCloseTime <= lateOpenTime)
563  {
564  segmentsEarlyClose.AddRange(segmentsLateOpen);
565  marketHoursSegments = segmentsEarlyClose;
566  }
567 
568  return new LocalMarketHours(localDateTime.DayOfWeek, marketHoursSegments);
569  }
570 
571  /// <summary>
572  /// Gets the previous trading day
573  /// </summary>
574  /// <param name="localDate">The date to start searching at in this exchange's time zones</param>
575  /// <returns>The previous trading day</returns>
576  public DateTime GetPreviousTradingDay(DateTime localDate)
577  {
578  localDate = localDate.AddDays(-1);
579  while (!IsDateOpen(localDate))
580  {
581  localDate = localDate.AddDays(-1);
582  }
583 
584  return localDate;
585  }
586 
587  /// <summary>
588  /// Gets the next trading day
589  /// </summary>
590  /// <param name="date">The date to start searching at</param>
591  /// <returns>The next trading day</returns>
592  public DateTime GetNextTradingDay(DateTime date)
593  {
594  date = date.AddDays(1);
595  while (!IsDateOpen(date))
596  {
597  date = date.AddDays(1);
598  }
599 
600  return date;
601  }
602 
603  /// <summary>
604  /// Sets the exchange hours to be the same as the given exchange hours without changing the reference
605  /// </summary>
606  /// <param name="other">The hours to set</param>
607  internal void Update(SecurityExchangeHours other)
608  {
609  if (other == null)
610  {
611  return;
612  }
613 
614  _holidays = other._holidays;
615  _earlyCloses = other._earlyCloses;
616  _lateOpens = other._lateOpens;
617  _sunday = other._sunday;
618  _monday = other._monday;
619  _tuesday = other._tuesday;
620  _wednesday = other._wednesday;
621  _thursday = other._thursday;
622  _friday = other._friday;
623  _saturday = other._saturday;
624  _openHoursByDay = other._openHoursByDay;
625  TimeZone = other.TimeZone;
628  }
629  }
630 }