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  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  /// <param name="extendedMarketHours">True to consider days with extended market hours only as open</param>
211  /// <returns>True if the exchange will be open on the specified date, false otherwise</returns>
212  public bool IsDateOpen(DateTime localDateTime, bool extendedMarketHours = false)
213  {
214  var marketHours = GetMarketHours(localDateTime);
215  if (marketHours.IsClosedAllDay)
216  {
217  // if we don't have hours for this day then we're not open
218  return false;
219  }
220 
221  if (marketHours.MarketDuration == TimeSpan.Zero)
222  {
223  // this date only has extended market hours, like sunday for futures, so we only return true if 'extendedMarketHours'
224  return extendedMarketHours;
225  }
226  return true;
227  }
228 
229  /// <summary>
230  /// Gets the local date time corresponding to the first market open to the specified previous date
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 GetFirstDailyMarketOpen(DateTime localDateTime, bool extendedMarketHours)
236  {
237  return GetPreviousMarketOpen(localDateTime, extendedMarketHours, firstOpen: true);
238  }
239 
240  /// <summary>
241  /// Gets the local date time corresponding to the previous market open to the specified time
242  /// </summary>
243  /// <param name="localDateTime">The time to begin searching for the last market open (non-inclusive)</param>
244  /// <param name="extendedMarketHours">True to include extended market hours in the search</param>
245  /// <returns>The previous market opening date time to the specified local date time</returns>
246  public DateTime GetPreviousMarketOpen(DateTime localDateTime, bool extendedMarketHours)
247  {
248  return GetPreviousMarketOpen(localDateTime, extendedMarketHours, firstOpen: false);
249  }
250 
251  /// <summary>
252  /// Gets the local date time corresponding to the previous market open to the specified time
253  /// </summary>
254  /// <param name="localDateTime">The time to begin searching for the last market open (non-inclusive)</param>
255  /// <param name="extendedMarketHours">True to include extended market hours in the search</param>
256  /// <returns>The previous market opening date time to the specified local date time</returns>
257  public DateTime GetPreviousMarketOpen(DateTime localDateTime, bool extendedMarketHours, bool firstOpen)
258  {
259  var time = localDateTime;
260  var marketHours = GetMarketHours(time);
261  var nextMarketOpen = GetNextMarketOpen(time, extendedMarketHours);
262 
263  if (localDateTime == nextMarketOpen)
264  {
265  return localDateTime;
266  }
267 
268  // let's loop for a week
269  for (int i = 0; i < 7; i++)
270  {
271  DateTime? potentialResult = null;
272  foreach(var segment in marketHours.Segments.Reverse())
273  {
274  if ((time.Date + segment.Start <= localDateTime) &&
275  (segment.State == MarketHoursState.Market || extendedMarketHours))
276  {
277  var timeOfDay = time.Date + segment.Start;
278  if (firstOpen)
279  {
280  potentialResult = timeOfDay;
281  }
282  // Check the current segment is not part of another segment before
283  else if (GetNextMarketOpen(timeOfDay.AddTicks(-1), extendedMarketHours) == timeOfDay)
284  {
285  return timeOfDay;
286  }
287  }
288  }
289 
290  if (potentialResult.HasValue)
291  {
292  return potentialResult.Value;
293  }
294 
295  time = time.AddDays(-1);
296  marketHours = GetMarketHours(time);
297  }
298 
299  throw new InvalidOperationException(Messages.SecurityExchangeHours.LastMarketOpenNotFound(localDateTime, IsMarketAlwaysOpen));
300  }
301 
302  /// <summary>
303  /// Gets the local date time corresponding to the next market open following the specified time
304  /// </summary>
305  /// <param name="localDateTime">The time to begin searching for market open (non-inclusive)</param>
306  /// <param name="extendedMarketHours">True to include extended market hours in the search</param>
307  /// <returns>The next market opening date time following the specified local date time</returns>
308  public DateTime GetNextMarketOpen(DateTime localDateTime, bool extendedMarketHours)
309  {
310  var time = localDateTime;
311  var oneWeekLater = localDateTime.Date.AddDays(15);
312 
313  var lastDay = time.Date.AddDays(-1);
314  var lastDayMarketHours = GetMarketHours(lastDay);
315  var lastDaySegment = lastDayMarketHours.Segments.LastOrDefault();
316  do
317  {
318  var marketHours = GetMarketHours(time);
319  if (!marketHours.IsClosedAllDay && !_holidays.Contains(time.Date.Ticks))
320  {
321  var marketOpenTimeOfDay = marketHours.GetMarketOpen(time.TimeOfDay, extendedMarketHours, lastDaySegment?.End);
322  if (marketOpenTimeOfDay.HasValue)
323  {
324  var marketOpen = time.Date + marketOpenTimeOfDay.Value;
325  if (localDateTime < marketOpen)
326  {
327  return marketOpen;
328  }
329  }
330 
331  // If there was an early close the market opens until next day first segment,
332  // so we don't take into account continuous segments between days, then
333  // lastDaySegment should be null
334  if (_earlyCloses.ContainsKey(time.Date))
335  {
336  lastDaySegment = null;
337  }
338  else
339  {
340  lastDaySegment = marketHours.Segments.LastOrDefault();
341  }
342  }
343  else
344  {
345  lastDaySegment = null;
346  }
347 
348  time = time.Date + Time.OneDay;
349  }
350  while (time < oneWeekLater);
351 
353  }
354 
355  /// <summary>
356  /// Gets the local date time corresponding to the last market close following the specified date
357  /// </summary>
358  /// <param name="localDateTime">The time to begin searching for market close (non-inclusive)</param>
359  /// <param name="extendedMarketHours">True to include extended market hours in the search</param>
360  /// <returns>The next market closing date time following the specified local date time</returns>
361  public DateTime GetLastDailyMarketClose(DateTime localDateTime, bool extendedMarketHours)
362  {
363  return GetNextMarketClose(localDateTime, extendedMarketHours, lastClose: true);
364  }
365 
366  /// <summary>
367  /// Gets the local date time corresponding to the next market close following the specified time
368  /// </summary>
369  /// <param name="localDateTime">The time to begin searching for market close (non-inclusive)</param>
370  /// <param name="extendedMarketHours">True to include extended market hours in the search</param>
371  /// <returns>The next market closing date time following the specified local date time</returns>
372  public DateTime GetNextMarketClose(DateTime localDateTime, bool extendedMarketHours)
373  {
374  return GetNextMarketClose(localDateTime, extendedMarketHours, lastClose: false);
375  }
376 
377  /// <summary>
378  /// Gets the local date time corresponding to the next market close following the specified time
379  /// </summary>
380  /// <param name="localDateTime">The time to begin searching for market close (non-inclusive)</param>
381  /// <param name="extendedMarketHours">True to include extended market hours in the search</param>
382  /// <param name="lastClose">True if the last available close of the date should be returned, else the first will be used</param>
383  /// <returns>The next market closing date time following the specified local date time</returns>
384  public DateTime GetNextMarketClose(DateTime localDateTime, bool extendedMarketHours, bool lastClose)
385  {
386  var time = localDateTime;
387  var oneWeekLater = localDateTime.Date.AddDays(15);
388  do
389  {
390  var marketHours = GetMarketHours(time);
391  if (!marketHours.IsClosedAllDay && !_holidays.Contains(time.Date.Ticks))
392  {
393  // Get next day first segment. This is made because we need to check the segment returned
394  // by GetMarketClose() ends at segment.End and not continues in the next segment. We get
395  // the next day first segment for the case in which the next market close is the last segment
396  // of the current day
397  var nextSegment = GetNextOrPreviousSegment(time, isNextDay: true);
398  var marketCloseTimeOfDay = marketHours.GetMarketClose(time.TimeOfDay, extendedMarketHours, lastClose, nextSegment?.Start);
399  if (marketCloseTimeOfDay.HasValue)
400  {
401  var marketClose = time.Date + marketCloseTimeOfDay.Value;
402  if (localDateTime < marketClose)
403  {
404  return marketClose;
405  }
406  }
407  }
408 
409  time = time.Date + Time.OneDay;
410  }
411  while (time < oneWeekLater);
412 
414  }
415 
416  /// <summary>
417  /// Returns next day first segment or previous day last segment
418  /// </summary>
419  /// <param name="time">Time of reference</param>
420  /// <param name="isNextDay">True to get next day first segment. False to get previous day last segment</param>
421  /// <returns>Next day first segment or previous day last segment</returns>
422  private MarketHoursSegment GetNextOrPreviousSegment(DateTime time, bool isNextDay)
423  {
424  var nextOrPrevious = isNextDay ? 1 : -1;
425  var nextOrPreviousDay = time.Date.AddDays(nextOrPrevious);
426  if (_earlyCloses.ContainsKey(nextOrPreviousDay.Date))
427  {
428  return null;
429  }
430 
431  var segments = GetMarketHours(nextOrPreviousDay).Segments;
432  return isNextDay ? segments.FirstOrDefault() : segments.LastOrDefault();
433  }
434 
435  /// <summary>
436  /// Check whether the market is always open or not
437  /// </summary>
438  /// <returns>True if the market is always open, false otherwise</returns>
439  private bool CheckIsMarketAlwaysOpen()
440  {
441  LocalMarketHours marketHours = null;
442  for (var i = 0; i < daysOfWeek.Count; i++)
443  {
444  var day = daysOfWeek[i];
445  switch (day)
446  {
447  case DayOfWeek.Sunday:
448  marketHours = _sunday;
449  break;
450  case DayOfWeek.Monday:
451  marketHours = _monday;
452  break;
453  case DayOfWeek.Tuesday:
454  marketHours = _tuesday;
455  break;
456  case DayOfWeek.Wednesday:
457  marketHours = _wednesday;
458  break;
459  case DayOfWeek.Thursday:
460  marketHours = _thursday;
461  break;
462  case DayOfWeek.Friday:
463  marketHours = _friday;
464  break;
465  case DayOfWeek.Saturday:
466  marketHours = _saturday;
467  break;
468  }
469 
470  if (!marketHours.IsOpenAllDay)
471  {
472  return false;
473  }
474  }
475 
476  return true;
477  }
478 
479  /// <summary>
480  /// Helper to extract market hours from the <see cref="_openHoursByDay"/> dictionary, filling
481  /// in Closed instantes when not present
482  /// </summary>
483  private void SetMarketHoursForDay(DayOfWeek dayOfWeek, out LocalMarketHours localMarketHoursForDay)
484  {
485  if (!_openHoursByDay.TryGetValue(dayOfWeek, out localMarketHoursForDay))
486  {
487  // assign to our dictionary that we're closed this day, as well as our local field
488  _openHoursByDay[dayOfWeek] = localMarketHoursForDay = LocalMarketHours.ClosedAllDay(dayOfWeek);
489  }
490  }
491 
492  /// <summary>
493  /// Helper to access the market hours field based on the day of week
494  /// </summary>
495  /// <param name="localDateTime">The local date time to retrieve market hours for</param>
496  /// <remarks>
497  /// This method will return an adjusted instance of <see cref="LocalMarketHours"/> for the specified date,
498  /// that is, it will account for holidays, early closes, and late opens (e.g. if the security trades regularly on Mondays,
499  /// but a specific Monday is a holiday, this method will return a <see cref="LocalMarketHours"/> that is closed all day).
500  /// In order to get the regular schedule, use the <see cref="MarketHours"/> property.
501  /// </remarks>
502  public LocalMarketHours GetMarketHours(DateTime localDateTime)
503  {
504  if (_holidays.Contains(localDateTime.Date.Ticks))
505  {
506  return LocalMarketHours.ClosedAllDay(localDateTime.DayOfWeek);
507  }
508 
509  LocalMarketHours marketHours;
510  switch (localDateTime.DayOfWeek)
511  {
512  case DayOfWeek.Sunday:
513  marketHours = _sunday;
514  break;
515  case DayOfWeek.Monday:
516  marketHours = _monday;
517  break;
518  case DayOfWeek.Tuesday:
519  marketHours = _tuesday;
520  break;
521  case DayOfWeek.Wednesday:
522  marketHours = _wednesday;
523  break;
524  case DayOfWeek.Thursday:
525  marketHours = _thursday;
526  break;
527  case DayOfWeek.Friday:
528  marketHours = _friday;
529  break;
530  case DayOfWeek.Saturday:
531  marketHours = _saturday;
532  break;
533  default:
534  throw new ArgumentOutOfRangeException(nameof(localDateTime), localDateTime, null);
535  }
536 
537  var hasEarlyClose = _earlyCloses.TryGetValue(localDateTime.Date, out var earlyCloseTime);
538  var hasLateOpen = _lateOpens.TryGetValue(localDateTime.Date, out var lateOpenTime);
539  if (!hasEarlyClose && !hasLateOpen)
540  {
541  return marketHours;
542  }
543 
544  IReadOnlyList<MarketHoursSegment> marketHoursSegments = marketHours.Segments;
545 
546  // If the earlyCloseTime is between a segment, change the close time with it
547  // and add it after the segments prior to the earlyCloseTime
548  // Otherwise, just take the segments prior to the earlyCloseTime
549  List<MarketHoursSegment> segmentsEarlyClose = null;
550  if (hasEarlyClose)
551  {
552  var index = marketHoursSegments.Count;
553  MarketHoursSegment newSegment = null;
554  for (var i = 0; i < marketHoursSegments.Count; i++)
555  {
556  var segment = marketHoursSegments[i];
557  if (segment.Start <= earlyCloseTime && earlyCloseTime <= segment.End)
558  {
559  newSegment = new MarketHoursSegment(segment.State, segment.Start, earlyCloseTime);
560  index = i;
561  break;
562  }
563  else if (earlyCloseTime < segment.Start)
564  {
565  // we will drop any remaining segment starting by this one
566  index = i - 1;
567  break;
568  }
569  }
570 
571  segmentsEarlyClose = new List<MarketHoursSegment>(marketHoursSegments.Take(index));
572  if (newSegment != null)
573  {
574  segmentsEarlyClose.Add(newSegment);
575  }
576  }
577 
578  // 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
579  // 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,
580  // 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
581  // the value of newMarketHours, so that it contains the segments before the early close
582  if (segmentsEarlyClose != null && (!hasLateOpen || earlyCloseTime >= lateOpenTime))
583  {
584  marketHoursSegments = segmentsEarlyClose;
585  }
586 
587  // If the lateOpenTime is between a segment, change the start time with it
588  // and add it before the segments previous to the lateOpenTime
589  // Otherwise, just take the segments previous to the lateOpenTime
590  List<MarketHoursSegment> segmentsLateOpen = null;
591  if (hasLateOpen)
592  {
593  var index = 0;
594  segmentsLateOpen = new List<MarketHoursSegment>();
595  for(var i = 0; i < marketHoursSegments.Count; i++)
596  {
597  var segment = marketHoursSegments[i];
598  if (segment.Start <= lateOpenTime && lateOpenTime <= segment.End)
599  {
600  segmentsLateOpen.Add(new (segment.State, lateOpenTime, segment.End));
601  index = i + 1;
602  break;
603  }
604  else if (lateOpenTime < segment.Start)
605  {
606  index = i;
607  break;
608  }
609  }
610 
611  segmentsLateOpen.AddRange(marketHoursSegments.TakeLast(marketHoursSegments.Count - index));
612  marketHoursSegments = segmentsLateOpen;
613  }
614 
615  // 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
616  // the segments before the early close and the segments after the late open and append them to obtain the expected market hours
617  if (segmentsEarlyClose != null && hasLateOpen && earlyCloseTime <= lateOpenTime)
618  {
619  segmentsEarlyClose.AddRange(segmentsLateOpen);
620  marketHoursSegments = segmentsEarlyClose;
621  }
622 
623  return new LocalMarketHours(localDateTime.DayOfWeek, marketHoursSegments);
624  }
625 
626  /// <summary>
627  /// Gets the previous trading day
628  /// </summary>
629  /// <param name="localDate">The date to start searching at in this exchange's time zones</param>
630  /// <returns>The previous trading day</returns>
631  public DateTime GetPreviousTradingDay(DateTime localDate)
632  {
633  localDate = localDate.AddDays(-1);
634  while (!IsDateOpen(localDate))
635  {
636  localDate = localDate.AddDays(-1);
637  }
638 
639  return localDate;
640  }
641 
642  /// <summary>
643  /// Gets the next trading day
644  /// </summary>
645  /// <param name="date">The date to start searching at</param>
646  /// <returns>The next trading day</returns>
647  public DateTime GetNextTradingDay(DateTime date)
648  {
649  date = date.AddDays(1);
650  while (!IsDateOpen(date))
651  {
652  date = date.AddDays(1);
653  }
654 
655  return date;
656  }
657 
658  /// <summary>
659  /// Sets the exchange hours to be the same as the given exchange hours without changing the reference
660  /// </summary>
661  /// <param name="other">The hours to set</param>
662  internal void Update(SecurityExchangeHours other)
663  {
664  if (other == null)
665  {
666  return;
667  }
668 
669  _holidays = other._holidays;
670  _earlyCloses = other._earlyCloses;
671  _lateOpens = other._lateOpens;
672  _sunday = other._sunday;
673  _monday = other._monday;
674  _tuesday = other._tuesday;
675  _wednesday = other._wednesday;
676  _thursday = other._thursday;
677  _friday = other._friday;
678  _saturday = other._saturday;
679  _openHoursByDay = other._openHoursByDay;
680  TimeZone = other.TimeZone;
683  }
684  }
685 }