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  /// <returns>A date rule that fires every day the specified symbol trades</returns>
126  public IDateRule EveryDay(Symbol symbol)
127  {
128  var securitySchedule = GetSecurityExchangeHours(symbol);
129  return new FuncDateRule($"{symbol.Value}: EveryDay", (start, end) => Time.EachTradeableDay(securitySchedule, start, end));
130  }
131 
132  /// <summary>
133  /// Specifies an event should fire on the first of each month + offset
134  /// </summary>
135  /// <param name="daysOffset"> The amount of days to offset the schedule by; must be between 0 and 30.</param>
136  /// <returns>A date rule that fires on the first of each month + offset</returns>
137  public IDateRule MonthStart(int daysOffset = 0)
138  {
139  return new FuncDateRule(GetName(null, "MonthStart", daysOffset), (start, end) => MonthIterator(null, start, end, daysOffset, true));
140  }
141 
142  /// <summary>
143  /// Specifies an event should fire on the first tradable date + offset for the specified symbol of each month
144  /// </summary>
145  /// <param name="symbol">The symbol whose exchange is used to determine the first tradable date of the month</param>
146  /// <param name="daysOffset"> The amount of tradable days to offset the schedule by; must be between 0 and 30</param>
147  /// <returns>A date rule that fires on the first tradable date + offset for the
148  /// specified security each month</returns>
149  public IDateRule MonthStart(Symbol symbol, int daysOffset = 0)
150  {
151  // Check that our offset is allowed
152  if (daysOffset < 0 || 30 < daysOffset)
153  {
154  throw new ArgumentOutOfRangeException(nameof(daysOffset), "DateRules.MonthStart() : Offset must be between 0 and 30");
155  }
156 
157  // Create the new DateRule and return it
158  return new FuncDateRule(GetName(symbol, "MonthStart", daysOffset), (start, end) => MonthIterator(GetSecurityExchangeHours(symbol), start, end, daysOffset, true));
159  }
160 
161  /// <summary>
162  /// Specifies an event should fire on the last of each month
163  /// </summary>
164  /// <param name="daysOffset"> The amount of days to offset the schedule by; must be between 0 and 30</param>
165  /// <returns>A date rule that fires on the last of each month - offset</returns>
166  public IDateRule MonthEnd(int daysOffset = 0)
167  {
168  return new FuncDateRule(GetName(null, "MonthEnd", -daysOffset), (start, end) => MonthIterator(null, start, end, daysOffset, false));
169  }
170 
171  /// <summary>
172  /// Specifies an event should fire on the last tradable date - offset for the specified symbol of each month
173  /// </summary>
174  /// <param name="symbol">The symbol whose exchange is used to determine the last tradable date of the month</param>
175  /// <param name="daysOffset">The amount of tradable days to offset the schedule by; must be between 0 and 30.</param>
176  /// <returns>A date rule that fires on the last tradable date - offset for the specified security each month</returns>
177  public IDateRule MonthEnd(Symbol symbol, int daysOffset = 0)
178  {
179  // Check that our offset is allowed
180  if (daysOffset < 0 || 30 < daysOffset)
181  {
182  throw new ArgumentOutOfRangeException(nameof(daysOffset), "DateRules.MonthEnd() : Offset must be between 0 and 30");
183  }
184 
185  // Create the new DateRule and return it
186  return new FuncDateRule(GetName(symbol, "MonthEnd", -daysOffset), (start, end) => MonthIterator(GetSecurityExchangeHours(symbol), start, end, daysOffset, false));
187  }
188 
189  /// <summary>
190  /// Specifies an event should fire on Monday + offset each week
191  /// </summary>
192  /// <param name="daysOffset">The amount of days to offset monday by; must be between 0 and 6</param>
193  /// <returns>A date rule that fires on Monday + offset each week</returns>
194  public IDateRule WeekStart(int daysOffset = 0)
195  {
196  // Check that our offset is allowed
197  if (daysOffset < 0 || 6 < daysOffset)
198  {
199  throw new ArgumentOutOfRangeException(nameof(daysOffset), "DateRules.WeekStart() : Offset must be between 0 and 6");
200  }
201 
202  return new FuncDateRule(GetName(null, "WeekStart", daysOffset), (start, end) => WeekIterator(null, start, end, daysOffset, true));
203  }
204 
205  /// <summary>
206  /// Specifies an event should fire on the first tradable date + offset for the specified
207  /// symbol each week
208  /// </summary>
209  /// <param name="symbol">The symbol whose exchange is used to determine the first
210  /// tradeable date of the week</param>
211  /// <param name="daysOffset">The amount of tradable days to offset the first tradable day by</param>
212  /// <returns>A date rule that fires on the first + offset tradable date for the specified
213  /// security each week</returns>
214  public IDateRule WeekStart(Symbol symbol, int daysOffset = 0)
215  {
216  var securitySchedule = GetSecurityExchangeHours(symbol);
217  var tradingDays = securitySchedule.MarketHours.Values
218  .Where(x => x.IsClosedAllDay == false).OrderBy(x => x.DayOfWeek).ToList();
219 
220  // Limit offsets to securities weekly schedule
221  if (daysOffset > tradingDays.Count - 1)
222  {
223  throw new ArgumentOutOfRangeException(nameof(daysOffset),
224  $"DateRules.WeekStart() : {tradingDays.First().DayOfWeek}+{daysOffset} is out of range for {symbol}'s schedule," +
225  $" please use an offset between 0 - {tradingDays.Count - 1}; Schedule : {string.Join(", ", tradingDays.Select(x => x.DayOfWeek))}");
226  }
227 
228  // Create the new DateRule and return it
229  return new FuncDateRule(GetName(symbol, "WeekStart", daysOffset), (start, end) => WeekIterator(securitySchedule, start, end, daysOffset, true));
230  }
231 
232  /// <summary>
233  /// Specifies an event should fire on Friday - offset
234  /// </summary>
235  /// <param name="daysOffset"> The amount of days to offset Friday by; must be between 0 and 6 </param>
236  /// <returns>A date rule that fires on Friday each week</returns>
237  public IDateRule WeekEnd(int daysOffset = 0)
238  {
239  // Check that our offset is allowed
240  if (daysOffset < 0 || 6 < daysOffset)
241  {
242  throw new ArgumentOutOfRangeException(nameof(daysOffset), "DateRules.WeekEnd() : Offset must be between 0 and 6");
243  }
244 
245  return new FuncDateRule(GetName(null, "WeekEnd", -daysOffset), (start, end) => WeekIterator(null, start, end, daysOffset, false));
246  }
247 
248  /// <summary>
249  /// Specifies an event should fire on the last - offset tradable date for the specified
250  /// symbol of each week
251  /// </summary>
252  /// <param name="symbol">The symbol whose exchange is used to determine the last
253  /// tradable date of the week</param>
254  /// <param name="daysOffset"> The amount of tradable days to offset the last tradable day by each week</param>
255  /// <returns>A date rule that fires on the last - offset tradable date for the specified security each week</returns>
256  public IDateRule WeekEnd(Symbol symbol, int daysOffset = 0)
257  {
258  var securitySchedule = GetSecurityExchangeHours(symbol);
259  var tradingDays = securitySchedule.MarketHours.Values
260  .Where(x => x.IsClosedAllDay == false).OrderBy(x => x.DayOfWeek).ToList();
261 
262  // Limit offsets to securities weekly schedule
263  if (daysOffset > tradingDays.Count - 1)
264  {
265  throw new ArgumentOutOfRangeException(nameof(daysOffset),
266  $"DateRules.WeekEnd() : {tradingDays.Last().DayOfWeek}-{daysOffset} is out of range for {symbol}'s schedule," +
267  $" please use an offset between 0 - {tradingDays.Count - 1}; Schedule : {string.Join(", ", tradingDays.Select(x => x.DayOfWeek))}");
268  }
269 
270  // Create the new DateRule and return it
271  return new FuncDateRule(GetName(symbol, "WeekEnd", -daysOffset), (start, end) => WeekIterator(securitySchedule, start, end, daysOffset, false));
272  }
273 
274  /// <summary>
275  /// Determine the string representation for a given rule
276  /// </summary>
277  /// <param name="symbol">Symbol for the rule</param>
278  /// <param name="ruleType">Rule type in string form</param>
279  /// <param name="offset">The amount of offset on this rule</param>
280  /// <returns></returns>
281  private static string GetName(Symbol symbol, string ruleType, int offset)
282  {
283  // Convert our offset to +#, -#, or empty string if 0
284  var offsetString = offset.ToString("+#;-#;''", CultureInfo.InvariantCulture);
285  var name = symbol == null ? $"{ruleType}{offsetString}" : $"{symbol.Value}: {ruleType}{offsetString}";
286 
287  return name;
288  }
289 
290 
291  /// <summary>
292  /// Get the closest trading day to a given DateTime for a given <see cref="SecurityExchangeHours"/>.
293  /// </summary>
294  /// <param name="securityExchangeHours"><see cref="SecurityExchangeHours"/> object with schedule for this Security</param>
295  /// <param name="baseDay">The day to base our search from</param>
296  /// <param name="offset">Amount to offset the schedule by tradable days</param>
297  /// <param name="searchForward">Search into the future for the closest day if true; into the past if false</param>
298  /// <param name="boundary">The boundary DateTime on the resulting day</param>
299  /// <returns></returns>
300  private static DateTime GetScheduledDay(SecurityExchangeHours securityExchangeHours, DateTime baseDay, int offset, bool searchForward, DateTime? boundary = null)
301  {
302  // By default the scheduled date is the given day
303  var scheduledDate = baseDay;
304 
305  // If its not open on this day find the next trading day by searching in the given direction
306  if (!securityExchangeHours.IsDateOpen(scheduledDate))
307  {
308  scheduledDate = searchForward
309  ? securityExchangeHours.GetNextTradingDay(scheduledDate)
310  : securityExchangeHours.GetPreviousTradingDay(scheduledDate);
311  }
312 
313  // Offset the scheduled day accordingly
314  for (var i = 0; i < offset; i++)
315  {
316  scheduledDate = searchForward
317  ? securityExchangeHours.GetNextTradingDay(scheduledDate)
318  : securityExchangeHours.GetPreviousTradingDay(scheduledDate);
319  }
320 
321  // If there is a boundary ensure we enforce it
322  if (boundary.HasValue)
323  {
324  // If we are searching forward and the resulting date is after this boundary we
325  // revert to the last tradable day equal to or less than boundary
326  if (searchForward && scheduledDate > boundary)
327  {
328  scheduledDate = GetScheduledDay(securityExchangeHours, (DateTime)boundary, 0, false);
329  }
330 
331  // If we are searching backward and the resulting date is after this boundary we
332  // revert to the last tradable day equal to or greater than boundary
333  if (!searchForward && scheduledDate < boundary)
334  {
335  scheduledDate = GetScheduledDay(securityExchangeHours, (DateTime)boundary, 0, true);
336  }
337  }
338 
339  return scheduledDate;
340  }
341 
342  private static IEnumerable<DateTime> MonthIterator(SecurityExchangeHours securitySchedule, DateTime start, DateTime end, int offset, bool searchForward)
343  {
344  // No schedule means no security, set to open everyday
345  if (securitySchedule == null)
346  {
347  securitySchedule = SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork);
348  }
349 
350  // Iterate all days between the beginning of "start" month, through end of "end" month.
351  // Necessary to ensure we schedule events in the month we start and end.
352  var beginningOfStartMonth = new DateTime(start.Year, start.Month, 1);
353  var endOfEndMonth = new DateTime(end.Year, end.Month, DateTime.DaysInMonth(end.Year, end.Month));
354 
355  foreach (var date in Time.EachDay(beginningOfStartMonth, endOfEndMonth))
356  {
357  var daysInMonth = DateTime.DaysInMonth(date.Year, date.Month);
358 
359  // Searching forward the first of the month is baseDay, with boundary being the last
360  // Searching backward the last of the month is baseDay, with boundary being the first
361  var baseDate = searchForward? new DateTime(date.Year, date.Month, 1) : new DateTime(date.Year, date.Month, daysInMonth);
362  var boundaryDate = searchForward ? new DateTime(date.Year, date.Month, daysInMonth) : new DateTime(date.Year, date.Month, 1);
363 
364  // Determine the scheduled day for this month
365  if (date == baseDate)
366  {
367  var scheduledDay = GetScheduledDay(securitySchedule, baseDate, offset, searchForward, boundaryDate);
368 
369  // Ensure the date is within our schedules range
370  if (scheduledDay >= start && scheduledDay <= end)
371  {
372  yield return scheduledDay;
373  }
374  }
375  }
376  }
377 
378  private static IEnumerable<DateTime> WeekIterator(SecurityExchangeHours securitySchedule, DateTime start, DateTime end, int offset, bool searchForward)
379  {
380  // Determine the weekly base day and boundary to schedule off of
381  DayOfWeek weeklyBaseDay;
382  DayOfWeek weeklyBoundaryDay;
383  if (securitySchedule == null)
384  {
385  // No schedule means no security, set to open everyday
386  securitySchedule = SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork);
387 
388  // Searching forward Monday is baseDay, with boundary being the following Sunday
389  // Searching backward Friday is baseDay, with boundary being the previous Saturday
390  weeklyBaseDay = searchForward ? DayOfWeek.Monday : DayOfWeek.Friday;
391  weeklyBoundaryDay = searchForward ? DayOfWeek.Saturday + 1 : DayOfWeek.Sunday - 1;
392  }
393  else
394  {
395  // Fetch the securities schedule
396  var weeklySchedule = securitySchedule.MarketHours.Values
397  .Where(x => x.IsClosedAllDay == false).OrderBy(x => x.DayOfWeek).ToList();
398 
399  // Determine our weekly base day and boundary for this security
400  weeklyBaseDay = searchForward ? weeklySchedule.First().DayOfWeek : weeklySchedule.Last().DayOfWeek;
401  weeklyBoundaryDay = searchForward ? weeklySchedule.Last().DayOfWeek : weeklySchedule.First().DayOfWeek;
402  }
403 
404  // Iterate all days between the beginning of "start" week, through end of "end" week.
405  // Necessary to ensure we schedule events in the week we start and end.
406  // 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.
407  var startAdjustment = start.DayOfWeek == DayOfWeek.Sunday ? -7 : 0;
408  var beginningOfStartWeek = start.AddDays(-(int)start.DayOfWeek + 1 + startAdjustment); // Date - DayOfWeek + 1
409 
410  var endAdjustment = end.DayOfWeek == DayOfWeek.Sunday ? -7 : 0;
411  var endOfEndWeek = end.AddDays(-(int)end.DayOfWeek + 7 + endAdjustment); // Date - DayOfWeek + 7
412 
413  // Determine the schedule for each week in this range
414  foreach (var date in Time.EachDay(beginningOfStartWeek, endOfEndWeek).Where(x => x.DayOfWeek == weeklyBaseDay))
415  {
416  var boundary = date.AddDays(weeklyBoundaryDay - weeklyBaseDay);
417  var scheduledDay = GetScheduledDay(securitySchedule, date, offset, searchForward, boundary);
418 
419  // Ensure the date is within our schedules range
420  if (scheduledDay >= start && scheduledDay <= end)
421  {
422  yield return scheduledDay;
423  }
424  }
425  }
426  }
427 }