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