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