Lean  $LEAN_TAG$
LocalMarketHours.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.Linq;
18 using System.Collections.Generic;
19 using System.Collections.ObjectModel;
20 
22 {
23  /// <summary>
24  /// Represents the market hours under normal conditions for an exchange and a specific day of the week in terms of local time
25  /// </summary>
26  public class LocalMarketHours
27  {
28  private static readonly LocalMarketHours _closedMonday = new(DayOfWeek.Monday);
29  private static readonly LocalMarketHours _closedTuesday = new(DayOfWeek.Tuesday);
30  private static readonly LocalMarketHours _closedWednesday = new(DayOfWeek.Wednesday);
31  private static readonly LocalMarketHours _closedThursday = new(DayOfWeek.Thursday);
32  private static readonly LocalMarketHours _closedFriday = new(DayOfWeek.Friday);
33  private static readonly LocalMarketHours _closedSaturday = new(DayOfWeek.Saturday);
34  private static readonly LocalMarketHours _closedSunday = new(DayOfWeek.Sunday);
35 
36  private static readonly LocalMarketHours _openMonday = new(DayOfWeek.Monday, new MarketHoursSegment(MarketHoursState.Market, TimeSpan.Zero, Time.OneDay));
37  private static readonly LocalMarketHours _openTuesday = new(DayOfWeek.Tuesday, new MarketHoursSegment(MarketHoursState.Market, TimeSpan.Zero, Time.OneDay));
38  private static readonly LocalMarketHours _openWednesday = new(DayOfWeek.Wednesday, new MarketHoursSegment(MarketHoursState.Market, TimeSpan.Zero, Time.OneDay));
39  private static readonly LocalMarketHours _openThursday = new(DayOfWeek.Thursday, new MarketHoursSegment(MarketHoursState.Market, TimeSpan.Zero, Time.OneDay));
40  private static readonly LocalMarketHours _openFriday = new(DayOfWeek.Friday, new MarketHoursSegment(MarketHoursState.Market, TimeSpan.Zero, Time.OneDay));
41  private static readonly LocalMarketHours _openSaturday = new(DayOfWeek.Saturday, new MarketHoursSegment(MarketHoursState.Market, TimeSpan.Zero, Time.OneDay));
42  private static readonly LocalMarketHours _openSunday = new(DayOfWeek.Sunday, new MarketHoursSegment(MarketHoursState.Market, TimeSpan.Zero, Time.OneDay));
43 
44  /// <summary>
45  /// Gets whether or not this exchange is closed all day
46  /// </summary>
47  public bool IsClosedAllDay { get; }
48 
49  /// <summary>
50  /// Gets whether or not this exchange is closed all day
51  /// </summary>
52  public bool IsOpenAllDay { get; }
53 
54  /// <summary>
55  /// Gets the day of week these hours apply to
56  /// </summary>
57  public DayOfWeek DayOfWeek { get; }
58 
59  /// <summary>
60  /// Gets the tradable time during the market day.
61  /// For a normal US equity trading day this is 6.5 hours.
62  /// This does NOT account for extended market hours and only
63  /// considers <see cref="MarketHoursState.Market"/>
64  /// </summary>
65  public TimeSpan MarketDuration { get; }
66 
67  /// <summary>
68  /// Gets the individual market hours segments that define the hours of operation for this day
69  /// </summary>
70  public ReadOnlyCollection<MarketHoursSegment> Segments { get; }
71 
72  /// <summary>
73  /// Initializes a new instance of the <see cref="LocalMarketHours"/> class
74  /// </summary>
75  /// <param name="day">The day of the week these hours are applicable</param>
76  /// <param name="segments">The open/close segments defining the market hours for one day</param>
77  public LocalMarketHours(DayOfWeek day, params MarketHoursSegment[] segments)
78  : this(day, (IEnumerable<MarketHoursSegment>) segments)
79  {
80  }
81 
82  /// <summary>
83  /// Initializes a new instance of the <see cref="LocalMarketHours"/> class
84  /// </summary>
85  /// <param name="day">The day of the week these hours are applicable</param>
86  /// <param name="segments">The open/close segments defining the market hours for one day</param>
87  public LocalMarketHours(DayOfWeek day, IEnumerable<MarketHoursSegment> segments)
88  {
89  DayOfWeek = day;
90  // filter out the closed states, we'll assume closed if no segment exists
91  Segments = new ReadOnlyCollection<MarketHoursSegment>((segments ?? Enumerable.Empty<MarketHoursSegment>()).Where(x => x.State != MarketHoursState.Closed).ToList());
92  IsClosedAllDay = Segments.Count == 0;
93  IsOpenAllDay = Segments.Count == 1
94  && Segments[0].Start == TimeSpan.Zero
95  && Segments[0].End == Time.OneDay
96  && Segments[0].State == MarketHoursState.Market;
97 
98  for (var i = 0; i < Segments.Count; i++)
99  {
100  var segment = Segments[i];
101  if (segment.State == MarketHoursState.Market)
102  {
103  MarketDuration += segment.End - segment.Start;
104  }
105  }
106  }
107 
108  /// <summary>
109  /// Initializes a new instance of the <see cref="LocalMarketHours"/> class from the specified open/close times
110  /// </summary>
111  /// <param name="day">The day of week these hours apply to</param>
112  /// <param name="extendedMarketOpen">The extended market open time</param>
113  /// <param name="marketOpen">The regular market open time, must be greater than or equal to the extended market open time</param>
114  /// <param name="marketClose">The regular market close time, must be greater than the regular market open time</param>
115  /// <param name="extendedMarketClose">The extended market close time, must be greater than or equal to the regular market close time</param>
116  public LocalMarketHours(DayOfWeek day, TimeSpan extendedMarketOpen, TimeSpan marketOpen, TimeSpan marketClose, TimeSpan extendedMarketClose)
117  : this(day, MarketHoursSegment.GetMarketHoursSegments(extendedMarketOpen, marketOpen, marketClose, extendedMarketClose))
118  {
119  }
120 
121  /// <summary>
122  /// Initializes a new instance of the <see cref="LocalMarketHours"/> class from the specified open/close times
123  /// using the market open as the extended market open and the market close as the extended market close, effectively
124  /// removing any 'extended' session from these exchange hours
125  /// </summary>
126  /// <param name="day">The day of week these hours apply to</param>
127  /// <param name="marketOpen">The regular market open time</param>
128  /// <param name="marketClose">The regular market close time, must be greater than the regular market open time</param>
129  public LocalMarketHours(DayOfWeek day, TimeSpan marketOpen, TimeSpan marketClose)
130  : this(day, marketOpen, marketOpen, marketClose, marketClose)
131  {
132  }
133 
134  /// <summary>
135  /// Gets the market opening time of day
136  /// </summary>
137  /// <param name="time">The reference time, the open returned will be the first open after the specified time if there are multiple market open segments</param>
138  /// <param name="extendedMarketHours">True to include extended market hours, false for regular market hours</param>
139  /// <param name="previousDayLastSegment">The previous days last segment. This is used when the potential next market open is the first segment of the day
140  /// so we need to check that segment is not part of previous day last segment. If null, it means there were no segments on the last day</param>
141  /// <returns>The market's opening time of day</returns>
142  public TimeSpan? GetMarketOpen(TimeSpan time, bool extendedMarketHours, TimeSpan? previousDayLastSegment = null)
143  {
144  var previousSegment = previousDayLastSegment;
145  bool prevSegmentIsFromPrevDay = true;
146  for (var i = 0; i < Segments.Count; i++)
147  {
148  var segment = Segments[i];
149  if (segment.State == MarketHoursState.Closed || segment.End <= time)
150  {
151  // update prev segment end time only if the current segment could have been taken into account
152  // (regular hours or, when enabled, extended hours segment)
153  if (segment.State == MarketHoursState.Market || extendedMarketHours)
154  {
155  previousSegment = segment.End;
156  prevSegmentIsFromPrevDay = false;
157  }
158 
159  continue;
160  }
161 
162  // let's try this segment if it's regular market hours or if it is extended market hours and extended market is allowed
163  if (segment.State == MarketHoursState.Market || extendedMarketHours)
164  {
165  if (!IsContinuousMarketOpen(previousSegment, segment.Start, prevSegmentIsFromPrevDay))
166  {
167  return segment.Start;
168  }
169 
170  previousSegment = segment.End;
171  prevSegmentIsFromPrevDay = false;
172  }
173  }
174 
175  // we couldn't locate an open segment after the specified time
176  return null;
177  }
178 
179  /// <summary>
180  /// Gets the market closing time of day
181  /// </summary>
182  /// <param name="time">The reference time, the close returned will be the first close after the specified time if there are multiple market open segments</param>
183  /// <param name="extendedMarketHours">True to include extended market hours, false for regular market hours</param>
184  /// <param name="nextDaySegmentStart">Next day first segment start. This is used when the potential next market close is
185  /// the last segment of the day so we need to check that segment is not continued on next day first segment.
186  /// If null, it means there are no segments on the next day</param>
187  /// <returns>The market's closing time of day</returns>
188  public TimeSpan? GetMarketClose(TimeSpan time, bool extendedMarketHours, TimeSpan? nextDaySegmentStart = null)
189  {
190  return GetMarketClose(time, extendedMarketHours, lastClose: false, nextDaySegmentStart);
191  }
192 
193  /// <summary>
194  /// Gets the market closing time of day
195  /// </summary>
196  /// <param name="time">The reference time, the close returned will be the first close after the specified time if there are multiple market open segments</param>
197  /// <param name="extendedMarketHours">True to include extended market hours, false for regular market hours</param>
198  /// <param name="lastClose">True if the last available close of the date should be returned, else the first will be used</param>
199  /// <param name="nextDaySegmentStart">Next day first segment start. This is used when the potential next market close is
200  /// the last segment of the day so we need to check that segment is not continued on next day first segment.
201  /// If null, it means there are no segments on the next day</param>
202  /// <returns>The market's closing time of day</returns>
203  public TimeSpan? GetMarketClose(TimeSpan time, bool extendedMarketHours, bool lastClose, TimeSpan? nextDaySegmentStart = null)
204  {
205  TimeSpan? potentialResult = null;
206  TimeSpan? nextSegment;
207  bool nextSegmentIsFromNextDay = false;
208  for (var i = 0; i < Segments.Count; i++)
209  {
210  var segment = Segments[i];
211  if (segment.State == MarketHoursState.Closed || segment.End <= time)
212  {
213  continue;
214  }
215 
216  if (i != Segments.Count - 1)
217  {
218  var potentialNextSegment = Segments[i+1];
219 
220  // Check whether we can consider PostMarket or not
221  if (potentialNextSegment.State != MarketHoursState.Market && !extendedMarketHours)
222  {
223  nextSegment = null;
224  }
225  else
226  {
227  nextSegment = Segments[i+1].Start;
228  }
229  }
230  else
231  {
232  nextSegment = nextDaySegmentStart;
233  nextSegmentIsFromNextDay = true;
234  }
235 
236  if ((segment.State == MarketHoursState.Market || extendedMarketHours))
237  {
238  if (lastClose)
239  {
240  // we continue, there might be another close next
241  potentialResult = segment.End;
242  }
243  else if (!IsContinuousMarketOpen(segment.End, nextSegment, nextSegmentIsFromNextDay))
244  {
245  return segment.End;
246  }
247  }
248  }
249  return potentialResult;
250  }
251 
252  /// <summary>
253  /// Determines if the exchange is open at the specified time
254  /// </summary>
255  /// <param name="time">The time of day to check</param>
256  /// <param name="extendedMarketHours">True to check exended market hours, false to check regular market hours</param>
257  /// <returns>True if the exchange is considered open, false otherwise</returns>
258  public bool IsOpen(TimeSpan time, bool extendedMarketHours)
259  {
260  for (var i = 0; i < Segments.Count; i++)
261  {
262  var segment = Segments[i];
263  if (segment.State == MarketHoursState.Closed)
264  {
265  continue;
266  }
267 
268  if (segment.Contains(time))
269  {
270  return extendedMarketHours || segment.State == MarketHoursState.Market;
271  }
272  }
273 
274  // if we didn't find a segment then we're closed
275  return false;
276  }
277 
278  /// <summary>
279  /// Determines if the exchange is open during the specified interval
280  /// </summary>
281  /// <param name="start">The start time of the interval</param>
282  /// <param name="end">The end time of the interval</param>
283  /// <param name="extendedMarketHours">True to check exended market hours, false to check regular market hours</param>
284  /// <returns>True if the exchange is considered open, false otherwise</returns>
285  public bool IsOpen(TimeSpan start, TimeSpan end, bool extendedMarketHours)
286  {
287  if (start == end)
288  {
289  return IsOpen(start, extendedMarketHours);
290  }
291 
292  for (var i = 0; i < Segments.Count; i++)
293  {
294  var segment = Segments[i];
295  if (segment.State == MarketHoursState.Closed)
296  {
297  continue;
298  }
299 
300  if (extendedMarketHours || segment.State == MarketHoursState.Market)
301  {
302  if (segment.Overlaps(start, end))
303  {
304  return true;
305  }
306  }
307  }
308 
309  // if we didn't find a segment then we're closed
310  return false;
311  }
312 
313  /// <summary>
314  /// Gets a <see cref="LocalMarketHours"/> instance that is always closed
315  /// </summary>
316  /// <param name="dayOfWeek">The day of week</param>
317  /// <returns>A <see cref="LocalMarketHours"/> instance that is always closed</returns>
318  public static LocalMarketHours ClosedAllDay(DayOfWeek dayOfWeek)
319  {
320  switch (dayOfWeek)
321  {
322  case DayOfWeek.Sunday:
323  return _closedSunday;
324  case DayOfWeek.Monday:
325  return _closedMonday;
326  case DayOfWeek.Tuesday:
327  return _closedTuesday;
328  case DayOfWeek.Wednesday:
329  return _closedWednesday;
330  case DayOfWeek.Thursday:
331  return _closedThursday;
332  case DayOfWeek.Friday:
333  return _closedFriday;
334  case DayOfWeek.Saturday:
335  return _closedSaturday;
336  default:
337  throw new ArgumentOutOfRangeException(nameof(dayOfWeek));
338  }
339  }
340 
341  /// <summary>
342  /// Gets a <see cref="LocalMarketHours"/> instance that is always open
343  /// </summary>
344  /// <param name="dayOfWeek">The day of week</param>
345  /// <returns>A <see cref="LocalMarketHours"/> instance that is always open</returns>
346  public static LocalMarketHours OpenAllDay(DayOfWeek dayOfWeek)
347  {
348  switch (dayOfWeek)
349  {
350  case DayOfWeek.Sunday:
351  return _openSunday;
352  case DayOfWeek.Monday:
353  return _openMonday;
354  case DayOfWeek.Tuesday:
355  return _openTuesday;
356  case DayOfWeek.Wednesday:
357  return _openWednesday;
358  case DayOfWeek.Thursday:
359  return _openThursday;
360  case DayOfWeek.Friday:
361  return _openFriday;
362  case DayOfWeek.Saturday:
363  return _openSaturday;
364  default:
365  throw new ArgumentOutOfRangeException(nameof(dayOfWeek));
366  }
367  }
368 
369  /// <summary>
370  /// Check the given segment is not part of the current previous segment
371  /// </summary>
372  /// <param name="previousSegmentEnd">Previous segment end time before the current segment</param>
373  /// <param name="nextSegmentStart">The next segment start time</param>
374  /// <param name="prevSegmentIsFromPrevDay">Indicated whether the previous segment is from the previous day or not
375  /// (then it is from the same day as the next segment). Defaults to true</param>
376  /// <returns>True if indeed the given segment is part of the last segment. False otherwise</returns>
377  public static bool IsContinuousMarketOpen(TimeSpan? previousSegmentEnd, TimeSpan? nextSegmentStart, bool prevSegmentIsFromPrevDay = true)
378  {
379  if (previousSegmentEnd != null && nextSegmentStart != null)
380  {
381  if (prevSegmentIsFromPrevDay)
382  {
383  // midnight passing to the next day
384  return previousSegmentEnd.Value == Time.OneDay && nextSegmentStart.Value == TimeSpan.Zero;
385  }
386 
387  // passing from one segment to another in the same day
388  return previousSegmentEnd.Value == nextSegmentStart.Value;
389  }
390  return false;
391  }
392 
393  /// <summary>
394  /// Returns a string that represents the current object.
395  /// </summary>
396  /// <returns>
397  /// A string that represents the current object.
398  /// </returns>
399  /// <filterpriority>2</filterpriority>
400  public override string ToString()
401  {
402  return Messages.LocalMarketHours.ToString(this);
403  }
404  }
405 }