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
29 changes: 29 additions & 0 deletions Common/Scheduling/BaseScheduleRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using NodaTime;
using QuantConnect.Interfaces;
using QuantConnect.Securities;
using System.Collections.Generic;

namespace QuantConnect.Scheduling
{
Expand Down Expand Up @@ -70,6 +71,34 @@ protected SecurityExchangeHours GetSecurityExchangeHours(Symbol symbol)
return security.Exchange.Hours;
}

/// <summary>
/// Helper method to fetch the exchange hours of the securities currently in <see cref="Securities"/>
/// whose markets are not always open. If no such securities are present, falls back to US equities (SPY).
/// </summary>
protected IEnumerable<SecurityExchangeHours> GetMarketOpenCloseExchangeHours()
{
// Pre-seed with SPY's exchange hours: this guarantees a fallback when no eligible
// security is subscribed and implicitly covers every US equity, which shares the
// same exchange hours — so we can skip US equities below to save the lookup.
var hours = new HashSet<SecurityExchangeHours>
{
MarketHoursDatabase.GetEntry(Market.USA, "SPY", SecurityType.Equity).ExchangeHours
};
foreach (var (symbol, security) in Securities)
{
if (security.Type == SecurityType.Equity && symbol.ID.Market == Market.USA)
{
continue;
}
var exchangeHours = security.Exchange.Hours;
if (!exchangeHours.IsMarketAlwaysOpen)
{
hours.Add(exchangeHours);
}
}
return hours;
}

protected Symbol GetSymbol(string ticker)
{
if (SymbolCache.TryGetSymbol(ticker, out var symbolCache))
Expand Down
118 changes: 118 additions & 0 deletions Common/Scheduling/TimeRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,19 @@ public ITimeRule Every(TimeSpan interval)
return new FuncTimeRule(name, applicator);
}

/// <summary>
/// Specifies an event should fire at market open +- <paramref name="minutesBeforeOpen"/>.
/// Picks, per date, the earliest market open across the algorithm's securities, ignoring always-open
/// exchanges. Defaults to US equities (SPY) when no eligible security is subscribed.
/// </summary>
/// <param name="minutesBeforeOpen">The minutes before market open that the event should fire</param>
/// <param name="extendedMarketOpen">True to use extended market open, false to use regular market open</param>
/// <returns>A time rule that fires the specified number of minutes before the earliest market open</returns>
public ITimeRule BeforeMarketOpen(double minutesBeforeOpen = 0, bool extendedMarketOpen = false)
{
return AfterMarketOpen(minutesBeforeOpen * (-1), extendedMarketOpen);
}

/// <summary>
/// Specifies an event should fire at market open +- <paramref name="minutesBeforeOpen"/>
/// </summary>
Expand All @@ -176,6 +189,54 @@ public ITimeRule BeforeMarketOpen(Symbol symbol, double minutesBeforeOpen = 0, b
return AfterMarketOpen(symbol, minutesBeforeOpen * (-1), extendedMarketOpen);
}

/// <summary>
/// Specifies an event should fire at market open +- <paramref name="minutesAfterOpen"/>.
/// Picks, per date, the earliest market open across the algorithm's securities, ignoring always-open
/// exchanges. Defaults to US equities (SPY) when no eligible security is subscribed.
/// </summary>
/// <param name="minutesAfterOpen">The minutes after market open that the event should fire</param>
/// <param name="extendedMarketOpen">True to use extended market open, false to use regular market open</param>
/// <returns>A time rule that fires the specified number of minutes after the earliest market open</returns>
public ITimeRule AfterMarketOpen(double minutesAfterOpen = 0, bool extendedMarketOpen = false)
{
var type = extendedMarketOpen ? "ExtendedMarketOpen" : "MarketOpen";
var afterOrBefore = minutesAfterOpen > 0 ? "after" : "before";
var name = Invariant($"{Math.Abs(minutesAfterOpen):0.##} min {afterOrBefore} {type}");
var timeAfterOpen = TimeSpan.FromMinutes(minutesAfterOpen);

return new FuncTimeRule(name, dates => EarliestMarketOpenTimes(dates, extendedMarketOpen, timeAfterOpen));
}

private IEnumerable<DateTime> EarliestMarketOpenTimes(IEnumerable<DateTime> dates, bool extendedMarketOpen, TimeSpan timeAfterOpen)
{
var exchangeHoursList = GetMarketOpenCloseExchangeHours();
foreach (var date in dates)
{
var earliestUtc = default(DateTime);
foreach (var exchangeHours in exchangeHoursList)
{
if (!exchangeHours.IsDateOpen(date, extendedMarketOpen))
{
continue;
}
var marketOpen = exchangeHours.GetFirstDailyMarketOpen((date + Time.OneDay).AddTicks(-1), extendedMarketOpen);
if (marketOpen.Date != date.Date)
{
continue;
}
var utc = (marketOpen + timeAfterOpen).ConvertToUtc(exchangeHours.TimeZone);
if (earliestUtc == default || utc < earliestUtc)
{
earliestUtc = utc;
}
}
if (earliestUtc != default)
{
yield return earliestUtc;
}
}
}

/// <summary>
/// Specifies an event should fire at market open +- <paramref name="minutesAfterOpen"/>
/// </summary>
Expand Down Expand Up @@ -212,6 +273,19 @@ where exchangeHours.IsDateOpen(date, extendedMarketOpen) && marketOpen.Date == d
return new FuncTimeRule(name, applicator);
}

/// <summary>
/// Specifies an event should fire at the market close +- <paramref name="minutesAfterClose"/>.
/// Picks, per date, the latest market close across the algorithm's securities, ignoring always-open
/// exchanges. Defaults to US equities (SPY) when no eligible security is subscribed.
/// </summary>
/// <param name="minutesAfterClose">The time after market close that the event should fire</param>
/// <param name="extendedMarketClose">True to use extended market close, false to use regular market close</param>
/// <returns>A time rule that fires the specified number of minutes after the latest market close</returns>
public ITimeRule AfterMarketClose(double minutesAfterClose = 0, bool extendedMarketClose = false)
{
return BeforeMarketClose(minutesAfterClose * (-1), extendedMarketClose);
}

/// <summary>
/// Specifies an event should fire at the market close +- <paramref name="minutesAfterClose"/>
/// </summary>
Expand All @@ -233,6 +307,50 @@ public ITimeRule AfterMarketClose(Symbol symbol, double minutesAfterClose = 0, b
return BeforeMarketClose(symbol, minutesAfterClose * (-1), extendedMarketClose);
}

/// <summary>
/// Specifies an event should fire at the market close +- <paramref name="minutesBeforeClose"/>.
/// Picks, per date, the latest market close across the algorithm's securities, ignoring always-open
/// exchanges. Defaults to US equities (SPY) when no eligible security is subscribed.
/// </summary>
/// <param name="minutesBeforeClose">The time before market close that the event should fire</param>
/// <param name="extendedMarketClose">True to use extended market close, false to use regular market close</param>
/// <returns>A time rule that fires the specified number of minutes before the latest market close</returns>
public ITimeRule BeforeMarketClose(double minutesBeforeClose = 0, bool extendedMarketClose = false)
{
var type = extendedMarketClose ? "ExtendedMarketClose" : "MarketClose";
var afterOrBefore = minutesBeforeClose > 0 ? "before" : "after";
var name = Invariant($"{Math.Abs(minutesBeforeClose):0.##} min {afterOrBefore} {type}");
var timeBeforeClose = TimeSpan.FromMinutes(minutesBeforeClose);

return new FuncTimeRule(name, dates => LatestMarketCloseTimes(dates, extendedMarketClose, timeBeforeClose));
}

private IEnumerable<DateTime> LatestMarketCloseTimes(IEnumerable<DateTime> dates, bool extendedMarketClose, TimeSpan timeBeforeClose)
{
var exchangeHoursList = GetMarketOpenCloseExchangeHours();
foreach (var date in dates)
{
var latestUtc = default(DateTime);
foreach (var exchangeHours in exchangeHoursList)
{
if (!exchangeHours.IsDateOpen(date, extendedMarketClose))
{
continue;
}
var marketClose = exchangeHours.GetLastDailyMarketClose(date, extendedMarketClose);
var utc = (marketClose - timeBeforeClose).ConvertToUtc(exchangeHours.TimeZone);
if (utc > latestUtc)
{
latestUtc = utc;
}
}
if (latestUtc != default)
{
yield return latestUtc;
}
}
}

/// <summary>
/// Specifies an event should fire at the market close +- <paramref name="minutesBeforeClose"/>
/// </summary>
Expand Down
Loading
Loading