Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions Common/Scheduling/DateRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,96 @@ public IDateRule YearEnd(Symbol symbol, int daysOffset = 0, bool extendedMarketH
return new FuncDateRule(GetName(symbol, "YearEnd", -daysOffset), (start, end) => YearIterator(securityExchangeHours, start, end, daysOffset, false, extendedMarketHours));
}

/// <summary>
/// Specifies an event should fire on the first of each quarter + offset
/// </summary>
/// <param name="daysOffset"> The amount of days to offset the schedule by; must be between 0 and 92.</param>
/// <returns>A date rule that fires on the first of each quarter + offset</returns>
public IDateRule QuarterStart(int daysOffset = 0)
{
return QuarterStart((Symbol)null, daysOffset, false);
}

/// <summary>
/// Specifies an event should fire on the first tradable date + offset for the specified symbol of each quarter
/// </summary>
/// <param name="symbol">The symbol whose exchange is used to determine the first tradable date of the quarter</param>
/// <param name="daysOffset"> The amount of tradable days to offset the schedule by; must be between 0 and 92</param>
/// <param name="extendedMarketHours">True to include days with extended market hours only, like sunday for futures</param>
/// <returns>A date rule that fires on the first tradable date + offset for the
/// specified security each quarter</returns>
public IDateRule QuarterStart(string symbol, int daysOffset = 0, bool extendedMarketHours = true) => QuarterStart(GetSymbol(symbol), daysOffset, extendedMarketHours);

/// <summary>
/// Specifies an event should fire on the first tradable date + offset for the specified symbol of each quarter
/// </summary>
/// <param name="symbol">The symbol whose exchange is used to determine the first tradable date of the quarter</param>
/// <param name="daysOffset"> The amount of tradable days to offset the schedule by; must be between 0 and 92</param>
/// <param name="extendedMarketHours">True to include days with extended market hours only, like sunday for futures</param>
/// <returns>A date rule that fires on the first tradable date + offset for the
/// specified security each quarter</returns>
public IDateRule QuarterStart(Symbol symbol, int daysOffset = 0, bool extendedMarketHours = true)
{
// Check that our offset is allowed
if (daysOffset < 0 || 92 < daysOffset)
{
throw new ArgumentOutOfRangeException(nameof(daysOffset), "DateRules.QuarterStart() : Offset must be between 0 and 92");
}

SecurityExchangeHours securityExchangeHours = null;
if (symbol != null)
{
securityExchangeHours = GetSecurityExchangeHours(symbol);
}

// Create the new DateRule and return it
return new FuncDateRule(GetName(symbol, "QuarterStart", daysOffset), (start, end) => QuarterIterator(securityExchangeHours, start, end, daysOffset, true, extendedMarketHours));
}

/// <summary>
/// Specifies an event should fire on the last of each quarter
/// </summary>
/// <param name="daysOffset"> The amount of days to offset the schedule by; must be between 0 and 92</param>
/// <returns>A date rule that fires on the last of each quarter - offset</returns>
public IDateRule QuarterEnd(int daysOffset = 0)
{
return QuarterEnd((Symbol)null, daysOffset, false);
}

/// <summary>
/// Specifies an event should fire on the last tradable date - offset for the specified symbol of each quarter
/// </summary>
/// <param name="symbol">The symbol whose exchange is used to determine the last tradable date of the quarter</param>
/// <param name="daysOffset">The amount of tradable days to offset the schedule by; must be between 0 and 92.</param>
/// <param name="extendedMarketHours">True to include days with extended market hours only, like sunday for futures</param>
/// <returns>A date rule that fires on the last tradable date - offset for the specified security each quarter</returns>
public IDateRule QuarterEnd(string symbol, int daysOffset = 0, bool extendedMarketHours = true) => QuarterEnd(GetSymbol(symbol), daysOffset, extendedMarketHours);

/// <summary>
/// Specifies an event should fire on the last tradable date - offset for the specified symbol of each quarter
/// </summary>
/// <param name="symbol">The symbol whose exchange is used to determine the last tradable date of the quarter</param>
/// <param name="daysOffset">The amount of tradable days to offset the schedule by; must be between 0 and 92.</param>
/// <param name="extendedMarketHours">True to include days with extended market hours only, like sunday for futures</param>
/// <returns>A date rule that fires on the last tradable date - offset for the specified security each quarter</returns>
public IDateRule QuarterEnd(Symbol symbol, int daysOffset = 0, bool extendedMarketHours = true)
{
// Check that our offset is allowed
if (daysOffset < 0 || 92 < daysOffset)
{
throw new ArgumentOutOfRangeException(nameof(daysOffset), "DateRules.QuarterEnd() : Offset must be between 0 and 92");
}

SecurityExchangeHours securityExchangeHours = null;
if (symbol != null)
{
securityExchangeHours = GetSecurityExchangeHours(symbol);
}

// Create the new DateRule and return it
return new FuncDateRule(GetName(symbol, "QuarterEnd", -daysOffset), (start, end) => QuarterIterator(securityExchangeHours, start, end, daysOffset, false, extendedMarketHours));
}

/// <summary>
/// Specifies an event should fire on the first of each month + offset
/// </summary>
Expand Down Expand Up @@ -538,6 +628,42 @@ private static IEnumerable<DateTime> MonthIterator(SecurityExchangeHours securit
return BaseIterator(securitySchedule, start, end, offset, searchForward, beginningOfStartMonth, endOfEndMonth, baseDateFunc, boundaryDateFunc, extendedMarketHours);
}

private static IEnumerable<DateTime> QuarterIterator(SecurityExchangeHours securitySchedule, DateTime start, DateTime end, int offset, bool searchForward, bool extendedMarketHours)
{
// Iterate all days between the beginning of "start" quarter, through end of "end" quarter.
// Necessary to ensure we schedule events in the quarter we start and end.
var startQuarterFirstMonth = ((start.Month - 1) / 3) * 3 + 1;
var beginningOfStartQuarter = new DateTime(start.Year, startQuarterFirstMonth, 1);

var endQuarterLastMonth = ((end.Month - 1) / 3) * 3 + 3;
var endOfEndQuarter = new DateTime(end.Year, endQuarterLastMonth, DateTime.DaysInMonth(end.Year, endQuarterLastMonth));

// Searching forward the first day of the quarter is baseDay, with boundary being the last day
// Searching backward the last day of the quarter is baseDay, with boundary being the first day
Func<DateTime, DateTime> baseDateFunc = date =>
{
var quarterFirstMonth = ((date.Month - 1) / 3) * 3 + 1;
if (searchForward)
{
return new DateTime(date.Year, quarterFirstMonth, 1);
}
var quarterLastMonth = quarterFirstMonth + 2;
return new DateTime(date.Year, quarterLastMonth, DateTime.DaysInMonth(date.Year, quarterLastMonth));
};
Func<DateTime, DateTime> boundaryDateFunc = date =>
{
var quarterFirstMonth = ((date.Month - 1) / 3) * 3 + 1;
if (searchForward)
{
var quarterLastMonth = quarterFirstMonth + 2;
return new DateTime(date.Year, quarterLastMonth, DateTime.DaysInMonth(date.Year, quarterLastMonth));
}
return new DateTime(date.Year, quarterFirstMonth, 1);
};

return BaseIterator(securitySchedule, start, end, offset, searchForward, beginningOfStartQuarter, endOfEndQuarter, baseDateFunc, boundaryDateFunc, extendedMarketHours);
}

private static IEnumerable<DateTime> YearIterator(SecurityExchangeHours securitySchedule, DateTime start, DateTime end, int offset, bool searchForward, bool extendedMarketHours)
{
// Iterate all days between the beginning of "start" year, through end of "end" year
Expand Down
145 changes: 145 additions & 0 deletions Tests/Common/Scheduling/DateRulesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,151 @@ public void EndOfMonthWithSymbolWithOffset(Symbols.SymbolsKey symbolKey, int[] e
}
}

[Test]
public void StartOfQuarterNoSymbol()
{
var rules = GetDateRules();
var rule = rules.QuarterStart();
var dates = rule.GetDates(new DateTime(2000, 01, 01), new DateTime(2000, 12, 31)).ToList();

Assert.AreEqual(4, dates.Count);
CollectionAssert.AreEqual(
new[]
{
new DateTime(2000, 1, 1),
new DateTime(2000, 4, 1),
new DateTime(2000, 7, 1),
new DateTime(2000, 10, 1)
},
dates);
}

[Test]
public void StartOfQuarterNoSymbolMidQuarterStart()
{
var rules = GetDateRules();
var rule = rules.QuarterStart();
var dates = rule.GetDates(new DateTime(2000, 02, 15), new DateTime(2000, 12, 31)).ToList();

Assert.AreEqual(3, dates.Count);
CollectionAssert.AreEqual(
new[]
{
new DateTime(2000, 4, 1),
new DateTime(2000, 7, 1),
new DateTime(2000, 10, 1)
},
dates);
}

[Test]
public void StartOfQuarterNoSymbolWithOffset()
{
var rules = GetDateRules();
var rule = rules.QuarterStart(5);
var dates = rule.GetDates(new DateTime(2000, 01, 01), new DateTime(2000, 12, 31)).ToList();

Assert.AreEqual(4, dates.Count);
CollectionAssert.AreEqual(
new[]
{
new DateTime(2000, 1, 6),
new DateTime(2000, 4, 6),
new DateTime(2000, 7, 6),
new DateTime(2000, 10, 6)
},
dates);
}

[Test]
public void StartOfQuarterWithSymbol()
{
var rules = GetDateRules();
var rule = rules.QuarterStart(Symbols.SPY);
var dates = rule.GetDates(new DateTime(2000, 01, 01), new DateTime(2000, 12, 31)).ToList();

Assert.AreEqual(4, dates.Count);
foreach (var date in dates)
{
Assert.AreNotEqual(DayOfWeek.Saturday, date.DayOfWeek);
Assert.AreNotEqual(DayOfWeek.Sunday, date.DayOfWeek);
Assert.Contains(date.Month, new[] { 1, 4, 7, 10 });
Assert.IsTrue(date.Day <= 3);
}
}

[Test]
public void EndOfQuarterNoSymbol()
{
var rules = GetDateRules();
var rule = rules.QuarterEnd();
var dates = rule.GetDates(new DateTime(2000, 01, 01), new DateTime(2000, 12, 31)).ToList();

Assert.AreEqual(4, dates.Count);
CollectionAssert.AreEqual(
new[]
{
new DateTime(2000, 3, 31),
new DateTime(2000, 6, 30),
new DateTime(2000, 9, 30),
new DateTime(2000, 12, 31)
},
dates);
}

[Test]
public void EndOfQuarterNoSymbolWithOffset()
{
var rules = GetDateRules();
var rule = rules.QuarterEnd(5);
var dates = rule.GetDates(new DateTime(2000, 01, 01), new DateTime(2000, 12, 31)).ToList();

Assert.AreEqual(4, dates.Count);
CollectionAssert.AreEqual(
new[]
{
new DateTime(2000, 3, 26),
new DateTime(2000, 6, 25),
new DateTime(2000, 9, 25),
new DateTime(2000, 12, 26)
},
dates);
}

[Test]
public void EndOfQuarterWithSymbol()
{
var rules = GetDateRules();
var rule = rules.QuarterEnd(Symbols.SPY);
var dates = rule.GetDates(new DateTime(2000, 01, 01), new DateTime(2000, 12, 31)).ToList();

Assert.AreEqual(4, dates.Count);
foreach (var date in dates)
{
Assert.AreNotEqual(DayOfWeek.Saturday, date.DayOfWeek);
Assert.AreNotEqual(DayOfWeek.Sunday, date.DayOfWeek);
Assert.Contains(date.Month, new[] { 3, 6, 9, 12 });
}
}

[TestCase(-1)]
[TestCase(93)]
public void QuarterStartOffsetOutOfRangeThrows(int offset)
{
var rules = GetDateRules();
Assert.Throws<ArgumentOutOfRangeException>(() => rules.QuarterStart(offset));
Assert.Throws<ArgumentOutOfRangeException>(() => rules.QuarterStart(Symbols.SPY, offset));
}

[TestCase(-1)]
[TestCase(93)]
public void QuarterEndOffsetOutOfRangeThrows(int offset)
{
var rules = GetDateRules();
Assert.Throws<ArgumentOutOfRangeException>(() => rules.QuarterEnd(offset));
Assert.Throws<ArgumentOutOfRangeException>(() => rules.QuarterEnd(Symbols.SPY, offset));
}

[Test]
public void StartOfYearNoSymbol()
{
Expand Down
Loading