Lean  $LEAN_TAG$
DateRules.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 
17 using System;
18 using NodaTime;
19 using System.Linq;
20 using System.Globalization;
22 using System.Collections.Generic;
23 
25 {
26  /// <summary>
27  /// Helper class used to provide better syntax when defining date rules
28  /// </summary>
30  {
31  /// <summary>
32  /// Initializes a new instance of the <see cref="DateRules"/> helper class
33  /// </summary>
34  /// <param name="securities">The security manager</param>
35  /// <param name="timeZone">The algorithm's default time zone</param>
36  /// <param name="marketHoursDatabase">The market hours database instance to use</param>
37  public DateRules(SecurityManager securities, DateTimeZone timeZone, MarketHoursDatabase marketHoursDatabase)
38  : base(securities, timeZone, marketHoursDatabase)
39  {
40  }
41 
42  /// <summary>
43  /// Sets the default time zone
44  /// </summary>
45  /// <param name="timeZone">The time zone to use for helper methods that can't resolve a time zone</param>
46  public void SetDefaultTimeZone(DateTimeZone timeZone)
47  {
48  TimeZone = timeZone;
49  }
50 
51  /// <summary>
52  /// Specifies an event should fire only on the specified day
53  /// </summary>
54  /// <param name="year">The year</param>
55  /// <param name="month">The month</param>
56  /// <param name="day">The day</param>
57  /// <returns></returns>
58  public IDateRule On(int year, int month, int day)
59  {
60  // make sure they're date objects
61  var dates = new[] {new DateTime(year, month, day)};
62  return new FuncDateRule(string.Join(",", dates.Select(x => x.ToShortDateString())), (start, end) => dates.Where(x => x >= start && x <= end));
63  }
64 
65  /// <summary>
66  /// Specifies an event should fire only on the specified days
67  /// </summary>
68  /// <param name="dates">The dates the event should fire</param>
69  public IDateRule On(params DateTime[] dates)
70  {
71  // make sure they're date objects
72  dates = dates.Select(x => x.Date).ToArray();
73  return new FuncDateRule(string.Join(",", dates.Select(x => x.ToShortDateString())), (start, end) => dates.Where(x => x >= start && x <= end));
74  }
75 
76  /// <summary>
77  /// Specifies an event should only fire today in the algorithm's time zone
78  /// using _securities.UtcTime instead of 'start' since ScheduleManager backs it up a day
79  /// </summary>
80  public IDateRule Today => new FuncDateRule("TodayOnly",
81  (start, e) => {
82  return new[] { Securities.UtcTime.ConvertFromUtc(TimeZone).Date };
83  }
84  );
85 
86  /// <summary>
87  /// Specifies an event should only fire tomorrow in the algorithm's time zone
88  /// using _securities.UtcTime instead of 'start' since ScheduleManager backs it up a day
89  /// </summary>
90  public IDateRule Tomorrow => new FuncDateRule("TomorrowOnly",
91  (start, e) => new[] {Securities.UtcTime.ConvertFromUtc(TimeZone).Date.AddDays(1)}
92  );
93 
94  /// <summary>
95  /// Specifies an event should fire on each of the specified days of week
96  /// </summary>
97  /// <param name="day">The day the event should fire</param>
98  /// <returns>A date rule that fires on every specified day of week</returns>
99  public IDateRule Every(DayOfWeek day) => Every(new[] { day });
100 
101  /// <summary>
102  /// Specifies an event should fire on each of the specified days of week
103  /// </summary>
104  /// <param name="days">The days the event should fire</param>
105  /// <returns>A date rule that fires on every specified day of week</returns>
106  public IDateRule Every(params DayOfWeek[] days)
107  {
108  var hash = days.ToHashSet();
109  return new FuncDateRule(string.Join(",", days), (start, end) => Time.EachDay(start, end).Where(date => hash.Contains(date.DayOfWeek)));
110  }
111 
112  /// <summary>
113  /// Specifies an event should fire every day
114  /// </summary>
115  /// <returns>A date rule that fires every day</returns>
117  {
118  return new FuncDateRule("EveryDay", Time.EachDay);
119  }
120 
121  /// <summary>
122  /// Specifies an event should fire every day the symbol is trading
123  /// </summary>
124  /// <param name="symbol">The symbol whose exchange is used to determine tradable dates</param>
125  /// <param name="extendedMarketHours">True to include days with extended market hours only, like sunday for futures</param>
126  /// <returns>A date rule that fires every day the specified symbol trades</returns>
127  public IDateRule EveryDay(Symbol symbol, bool extendedMarketHours = false)
128  {
129  var securitySchedule = GetSecurityExchangeHours(symbol);
130  return new FuncDateRule($"{symbol.Value}: EveryDay", (start, end) => Time.EachTradeableDay(securitySchedule, start, end, extendedMarketHours));
131  }
132 
133  /// <summary>
134  /// Specifies an event should fire on the first of each year + offset
135  /// </summary>
136  /// <param name="daysOffset"> The amount of days to offset the schedule by; must be between 0 and 365.</param>
137  /// <returns>A date rule that fires on the first of each year + offset</returns>
138  public IDateRule YearStart(int daysOffset = 0)
139  {
140  return YearStart(null, daysOffset);
141  }
142 
143  /// <summary>
144  /// Specifies an event should fire on the first tradable date + offset for the specified symbol of each year
145  /// </summary>
146  /// <param name="symbol">The symbol whose exchange is used to determine the first tradable date of the year</param>
147  /// <param name="daysOffset"> The amount of tradable days to offset the schedule by; must be between 0 and 365</param>
148  /// <returns>A date rule that fires on the first tradable date + offset for the
149  /// specified security each year</returns>
150  public IDateRule YearStart(Symbol symbol, int daysOffset = 0)
151  {
152  // Check that our offset is allowed
153  if (daysOffset < 0 || 365 < daysOffset)
154  {
155  throw new ArgumentOutOfRangeException(nameof(daysOffset), "DateRules.YearStart() : Offset must be between 0 and 365");
156  }
157 
158  SecurityExchangeHours securityExchangeHours = null;
159  if (symbol != null)
160  {
161  securityExchangeHours = GetSecurityExchangeHours(symbol);
162  }
163 
164  // Create the new DateRule and return it
165  return new FuncDateRule(GetName(symbol, "YearStart", daysOffset), (start, end) => YearIterator(securityExchangeHours, start, end, daysOffset, true));
166  }
167 
168  /// <summary>
169  /// Specifies an event should fire on the last of each year
170  /// </summary>
171  /// <param name="daysOffset"> The amount of days to offset the schedule by; must be between 0 and 365</param>
172  /// <returns>A date rule that fires on the last of each year - offset</returns>
173  public IDateRule YearEnd(int daysOffset = 0)
174  {
175  return YearEnd(null, daysOffset);
176  }
177 
178  /// <summary>
179  /// Specifies an event should fire on the last tradable date - offset for the specified symbol of each year
180  /// </summary>
181  /// <param name="symbol">The symbol whose exchange is used to determine the last tradable date of the year</param>
182  /// <param name="daysOffset">The amount of tradable days to offset the schedule by; must be between 0 and 365.</param>
183  /// <returns>A date rule that fires on the last tradable date - offset for the specified security each year</returns>
184  public IDateRule YearEnd(Symbol symbol, int daysOffset = 0)
185  {
186  // Check that our offset is allowed
187  if (daysOffset < 0 || 365 < daysOffset)
188  {
189  throw new ArgumentOutOfRangeException(nameof(daysOffset), "DateRules.YearEnd() : Offset must be between 0 and 365");
190  }
191 
192  SecurityExchangeHours securityExchangeHours = null;
193  if (symbol != null)
194  {
195  securityExchangeHours = GetSecurityExchangeHours(symbol);
196  }
197 
198  // Create the new DateRule and return it
199  return new FuncDateRule(GetName(symbol, "YearEnd", -daysOffset), (start, end) => YearIterator(securityExchangeHours, start, end, daysOffset, false));
200  }
201 
202  /// <summary>
203  /// Specifies an event should fire on the first of each month + offset
204  /// </summary>
205  /// <param name="daysOffset"> The amount of days to offset the schedule by; must be between 0 and 30.</param>
206  /// <returns>A date rule that fires on the first of each month + offset</returns>
207  public IDateRule MonthStart(int daysOffset = 0)
208  {
209  return new FuncDateRule(GetName(null, "MonthStart", daysOffset), (start, end) => MonthIterator(null, start, end, daysOffset, true));
210  }
211 
212  /// <summary>
213  /// Specifies an event should fire on the first tradable date + offset for the specified symbol of each month
214  /// </summary>
215  /// <param name="symbol">The symbol whose exchange is used to determine the first tradable date of the month</param>
216  /// <param name="daysOffset"> The amount of tradable days to offset the schedule by; must be between 0 and 30</param>
217  /// <returns>A date rule that fires on the first tradable date + offset for the
218  /// specified security each month</returns>
219  public IDateRule MonthStart(Symbol symbol, int daysOffset = 0)
220  {
221  // Check that our offset is allowed
222  if (daysOffset < 0 || 30 < daysOffset)
223  {
224  throw new ArgumentOutOfRangeException(nameof(daysOffset), "DateRules.MonthStart() : Offset must be between 0 and 30");
225  }
226 
227  // Create the new DateRule and return it
228  return new FuncDateRule(GetName(symbol, "MonthStart", daysOffset), (start, end) => MonthIterator(GetSecurityExchangeHours(symbol), start, end, daysOffset, true));
229  }
230 
231  /// <summary>
232  /// Specifies an event should fire on the last of each month
233  /// </summary>
234  /// <param name="daysOffset"> The amount of days to offset the schedule by; must be between 0 and 30</param>
235  /// <returns>A date rule that fires on the last of each month - offset</returns>
236  public IDateRule MonthEnd(int daysOffset = 0)
237  {
238  return new FuncDateRule(GetName(null, "MonthEnd", -daysOffset), (start, end) => MonthIterator(null, start, end, daysOffset, false));
239  }
240 
241  /// <summary>
242  /// Specifies an event should fire on the last tradable date - offset for the specified symbol of each month
243  /// </summary>
244  /// <param name="symbol">The symbol whose exchange is used to determine the last tradable date of the month</param>
245  /// <param name="daysOffset">The amount of tradable days to offset the schedule by; must be between 0 and 30.</param>
246  /// <returns>A date rule that fires on the last tradable date - offset for the specified security each month</returns>
247  public IDateRule MonthEnd(Symbol symbol, int daysOffset = 0)
248  {
249  // Check that our offset is allowed
250  if (daysOffset < 0 || 30 < daysOffset)
251  {
252  throw new ArgumentOutOfRangeException(nameof(daysOffset), "DateRules.MonthEnd() : Offset must be between 0 and 30");
253  }
254 
255  // Create the new DateRule and return it
256  return new FuncDateRule(GetName(symbol, "MonthEnd", -daysOffset), (start, end) => MonthIterator(GetSecurityExchangeHours(symbol), start, end, daysOffset, false));
257  }
258 
259  /// <summary>
260  /// Specifies an event should fire on Monday + offset each week
261  /// </summary>
262  /// <param name="daysOffset">The amount of days to offset monday by; must be between 0 and 6</param>
263  /// <returns>A date rule that fires on Monday + offset each week</returns>
264  public IDateRule WeekStart(int daysOffset = 0)
265  {
266  // Check that our offset is allowed
267  if (daysOffset < 0 || 6 < daysOffset)
268  {
269  throw new ArgumentOutOfRangeException(nameof(daysOffset), "DateRules.WeekStart() : Offset must be between 0 and 6");
270  }
271 
272  return new FuncDateRule(GetName(null, "WeekStart", daysOffset), (start, end) => WeekIterator(null, start, end, daysOffset, true));
273  }
274 
275  /// <summary>
276  /// Specifies an event should fire on the first tradable date + offset for the specified
277  /// symbol each week
278  /// </summary>
279  /// <param name="symbol">The symbol whose exchange is used to determine the first
280  /// tradeable date of the week</param>
281  /// <param name="daysOffset">The amount of tradable days to offset the first tradable day by</param>
282  /// <returns>A date rule that fires on the first + offset tradable date for the specified
283  /// security each week</returns>
284  public IDateRule WeekStart(Symbol symbol, int daysOffset = 0)
285  {
286  var securitySchedule = GetSecurityExchangeHours(symbol);
287  var tradingDays = securitySchedule.MarketHours.Values
288  .Where(x => x.IsClosedAllDay == false).OrderBy(x => x.DayOfWeek).ToList();
289 
290  // Limit offsets to securities weekly schedule
291  if (daysOffset > tradingDays.Count - 1)
292  {
293  throw new ArgumentOutOfRangeException(nameof(daysOffset),
294  $"DateRules.WeekStart() : {tradingDays.First().DayOfWeek}+{daysOffset} is out of range for {symbol}'s schedule," +
295  $" please use an offset between 0 - {tradingDays.Count - 1}; Schedule : {string.Join(", ", tradingDays.Select(x => x.DayOfWeek))}");
296  }
297 
298  // Create the new DateRule and return it
299  return new FuncDateRule(GetName(symbol, "WeekStart", daysOffset), (start, end) => WeekIterator(securitySchedule, start, end, daysOffset, true));
300  }
301 
302  /// <summary>
303  /// Specifies an event should fire on Friday - offset
304  /// </summary>
305  /// <param name="daysOffset"> The amount of days to offset Friday by; must be between 0 and 6 </param>
306  /// <returns>A date rule that fires on Friday each week</returns>
307  public IDateRule WeekEnd(int daysOffset = 0)
308  {
309  // Check that our offset is allowed
310  if (daysOffset < 0 || 6 < daysOffset)
311  {
312  throw new ArgumentOutOfRangeException(nameof(daysOffset), "DateRules.WeekEnd() : Offset must be between 0 and 6");
313  }
314 
315  return new FuncDateRule(GetName(null, "WeekEnd", -daysOffset), (start, end) => WeekIterator(null, start, end, daysOffset, false));
316  }
317 
318  /// <summary>
319  /// Specifies an event should fire on the last - offset tradable date for the specified
320  /// symbol of each week
321  /// </summary>
322  /// <param name="symbol">The symbol whose exchange is used to determine the last
323  /// tradable date of the week</param>
324  /// <param name="daysOffset"> The amount of tradable days to offset the last tradable day by each week</param>
325  /// <returns>A date rule that fires on the last - offset tradable date for the specified security each week</returns>
326  public IDateRule WeekEnd(Symbol symbol, int daysOffset = 0)
327  {
328  var securitySchedule = GetSecurityExchangeHours(symbol);
329  var tradingDays = securitySchedule.MarketHours.Values
330  .Where(x => x.IsClosedAllDay == false).OrderBy(x => x.DayOfWeek).ToList();
331 
332  // Limit offsets to securities weekly schedule
333  if (daysOffset > tradingDays.Count - 1)
334  {
335  throw new ArgumentOutOfRangeException(nameof(daysOffset),
336  $"DateRules.WeekEnd() : {tradingDays.Last().DayOfWeek}-{daysOffset} is out of range for {symbol}'s schedule," +
337  $" please use an offset between 0 - {tradingDays.Count - 1}; Schedule : {string.Join(", ", tradingDays.Select(x => x.DayOfWeek))}");
338  }
339 
340  // Create the new DateRule and return it
341  return new FuncDateRule(GetName(symbol, "WeekEnd", -daysOffset), (start, end) => WeekIterator(securitySchedule, start, end, daysOffset, false));
342  }
343 
344  /// <summary>
345  /// Determine the string representation for a given rule
346  /// </summary>
347  /// <param name="symbol">Symbol for the rule</param>
348  /// <param name="ruleType">Rule type in string form</param>
349  /// <param name="offset">The amount of offset on this rule</param>
350  /// <returns></returns>
351  private static string GetName(Symbol symbol, string ruleType, int offset)
352  {
353  // Convert our offset to +#, -#, or empty string if 0
354  var offsetString = offset.ToString("+#;-#;''", CultureInfo.InvariantCulture);
355  var name = symbol == null ? $"{ruleType}{offsetString}" : $"{symbol.Value}: {ruleType}{offsetString}";
356 
357  return name;
358  }
359 
360 
361  /// <summary>
362  /// Get the closest trading day to a given DateTime for a given <see cref="SecurityExchangeHours"/>.
363  /// </summary>
364  /// <param name="securityExchangeHours"><see cref="SecurityExchangeHours"/> object with schedule for this Security</param>
365  /// <param name="baseDay">The day to base our search from</param>
366  /// <param name="offset">Amount to offset the schedule by tradable days</param>
367  /// <param name="searchForward">Search into the future for the closest day if true; into the past if false</param>
368  /// <param name="boundary">The boundary DateTime on the resulting day</param>
369  /// <returns></returns>
370  private static DateTime GetScheduledDay(SecurityExchangeHours securityExchangeHours, DateTime baseDay, int offset, bool searchForward, DateTime? boundary = null)
371  {
372  // By default the scheduled date is the given day
373  var scheduledDate = baseDay;
374 
375  // If its not open on this day find the next trading day by searching in the given direction
376  if (!securityExchangeHours.IsDateOpen(scheduledDate, extendedMarketHours: true))
377  {
378  scheduledDate = searchForward
379  ? securityExchangeHours.GetNextTradingDay(scheduledDate)
380  : securityExchangeHours.GetPreviousTradingDay(scheduledDate);
381  }
382 
383  // Offset the scheduled day accordingly
384  for (var i = 0; i < offset; i++)
385  {
386  scheduledDate = searchForward
387  ? securityExchangeHours.GetNextTradingDay(scheduledDate)
388  : securityExchangeHours.GetPreviousTradingDay(scheduledDate);
389  }
390 
391  // If there is a boundary ensure we enforce it
392  if (boundary.HasValue)
393  {
394  // If we are searching forward and the resulting date is after this boundary we
395  // revert to the last tradable day equal to or less than boundary
396  if (searchForward && scheduledDate > boundary)
397  {
398  scheduledDate = GetScheduledDay(securityExchangeHours, (DateTime)boundary, 0, false);
399  }
400 
401  // If we are searching backward and the resulting date is after this boundary we
402  // revert to the last tradable day equal to or greater than boundary
403  if (!searchForward && scheduledDate < boundary)
404  {
405  scheduledDate = GetScheduledDay(securityExchangeHours, (DateTime)boundary, 0, true);
406  }
407  }
408 
409  return scheduledDate;
410  }
411 
412  private static IEnumerable<DateTime> BaseIterator(
413  SecurityExchangeHours securitySchedule,
414  DateTime start,
415  DateTime end,
416  int offset,
417  bool searchForward,
418  DateTime periodBegin,
419  DateTime periodEnd,
420  Func<DateTime, DateTime> baseDateFunc,
421  Func<DateTime, DateTime> boundaryDateFunc)
422  {
423  // No schedule means no security, set to open everyday
424  if (securitySchedule == null)
425  {
426  securitySchedule = SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork);
427  }
428 
429  foreach (var date in Time.EachDay(periodBegin, periodEnd))
430  {
431  var baseDate = baseDateFunc(date);
432  var boundaryDate = boundaryDateFunc(date);
433 
434  // Determine the scheduled day for this period
435  if (date == baseDate)
436  {
437  var scheduledDay = GetScheduledDay(securitySchedule, baseDate, offset, searchForward, boundaryDate);
438 
439  // Ensure the date is within our schedules range
440  if (scheduledDay >= start && scheduledDay <= end)
441  {
442  yield return scheduledDay;
443  }
444  }
445  }
446  }
447 
448  private static IEnumerable<DateTime> MonthIterator(SecurityExchangeHours securitySchedule, DateTime start, DateTime end, int offset, bool searchForward)
449  {
450  // Iterate all days between the beginning of "start" month, through end of "end" month.
451  // Necessary to ensure we schedule events in the month we start and end.
452  var beginningOfStartMonth = new DateTime(start.Year, start.Month, 1);
453  var endOfEndMonth = new DateTime(end.Year, end.Month, DateTime.DaysInMonth(end.Year, end.Month));
454 
455  // Searching forward the first of the month is baseDay, with boundary being the last
456  // Searching backward the last of the month is baseDay, with boundary being the first
457  Func<DateTime, DateTime> baseDateFunc = date => searchForward ? new DateTime(date.Year, date.Month, 1) : new DateTime(date.Year, date.Month, DateTime.DaysInMonth(date.Year, date.Month));
458  Func<DateTime, DateTime> boundaryDateFunc = date => searchForward ? new DateTime(date.Year, date.Month, DateTime.DaysInMonth(date.Year, date.Month)) : new DateTime(date.Year, date.Month, 1);
459 
460  return BaseIterator(securitySchedule, start, end, offset, searchForward, beginningOfStartMonth, endOfEndMonth, baseDateFunc, boundaryDateFunc);
461  }
462 
463  private static IEnumerable<DateTime> YearIterator(SecurityExchangeHours securitySchedule, DateTime start, DateTime end, int offset, bool searchForward)
464  {
465  // Iterate all days between the beginning of "start" year, through end of "end" year
466  // Necessary to ensure we schedule events in the year we start and end.
467  var beginningOfStartOfYear = new DateTime(start.Year, start.Month, 1);
468  var endOfEndYear = new DateTime(end.Year, end.Month, DateTime.DaysInMonth(end.Year, end.Month));
469 
470  // Searching forward the first of the year is baseDay, with boundary being the last
471  // Searching backward the last of the year is baseDay, with boundary being the first
472  Func<DateTime, DateTime> baseDateFunc = date => searchForward ? new DateTime(date.Year, 1, 1) : new DateTime(date.Year, 12, 31);
473  Func<DateTime, DateTime> boundaryDateFunc = date => searchForward ? new DateTime(date.Year, 12, 31) : new DateTime(date.Year, 1, 1);
474 
475  return BaseIterator(securitySchedule, start, end, offset, searchForward, beginningOfStartOfYear, endOfEndYear, baseDateFunc, boundaryDateFunc);
476  }
477 
478  private static IEnumerable<DateTime> WeekIterator(SecurityExchangeHours securitySchedule, DateTime start, DateTime end, int offset, bool searchForward)
479  {
480  // Determine the weekly base day and boundary to schedule off of
481  DayOfWeek weeklyBaseDay;
482  DayOfWeek weeklyBoundaryDay;
483  if (securitySchedule == null)
484  {
485  // No schedule means no security, set to open everyday
486  securitySchedule = SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork);
487 
488  // Searching forward Monday is baseDay, with boundary being the following Sunday
489  // Searching backward Friday is baseDay, with boundary being the previous Saturday
490  weeklyBaseDay = searchForward ? DayOfWeek.Monday : DayOfWeek.Friday;
491  weeklyBoundaryDay = searchForward ? DayOfWeek.Saturday + 1 : DayOfWeek.Sunday - 1;
492  }
493  else
494  {
495  // Fetch the securities schedule
496  var weeklySchedule = securitySchedule.MarketHours.Values
497  .Where(x => x.IsClosedAllDay == false).OrderBy(x => x.DayOfWeek).ToList();
498 
499  // Determine our weekly base day and boundary for this security
500  weeklyBaseDay = searchForward ? weeklySchedule.First().DayOfWeek : weeklySchedule.Last().DayOfWeek;
501  weeklyBoundaryDay = searchForward ? weeklySchedule.Last().DayOfWeek : weeklySchedule.First().DayOfWeek;
502  }
503 
504  // Iterate all days between the beginning of "start" week, through end of "end" week.
505  // Necessary to ensure we schedule events in the week we start and end.
506  // Also if we have a sunday for start/end we need to adjust for it being the front of the week when we want it as the end of the week.
507  var startAdjustment = start.DayOfWeek == DayOfWeek.Sunday ? -7 : 0;
508  var beginningOfStartWeek = start.AddDays(-(int)start.DayOfWeek + 1 + startAdjustment); // Date - DayOfWeek + 1
509 
510  var endAdjustment = end.DayOfWeek == DayOfWeek.Sunday ? -7 : 0;
511  var endOfEndWeek = end.AddDays(-(int)end.DayOfWeek + 7 + endAdjustment); // Date - DayOfWeek + 7
512 
513  // Determine the schedule for each week in this range
514  foreach (var date in Time.EachDay(beginningOfStartWeek, endOfEndWeek).Where(x => x.DayOfWeek == weeklyBaseDay))
515  {
516  var boundary = date.AddDays(weeklyBoundaryDay - weeklyBaseDay);
517  var scheduledDay = GetScheduledDay(securitySchedule, date, offset, searchForward, boundary);
518 
519  // Ensure the date is within our schedules range
520  if (scheduledDay >= start && scheduledDay <= end)
521  {
522  yield return scheduledDay;
523  }
524  }
525  }
526  }
527 }