Skip to content

Commit 4cc7999

Browse files
AlexCatarinoclaude
andcommitted
Default to all market hours when no symbol is provided
Add no-symbol overloads of `AfterMarketOpen`, `BeforeMarketOpen`, `AfterMarketClose` and `BeforeMarketClose` so scheduled events can be defined without referencing a specific security. Per-date, the helpers pick the earliest open / latest close across the algorithm's non-always-open exchanges, falling back to US equities (SPY) when no eligible security is subscribed. US equities are skipped during the walk since SPY's exchange hours already represent them. Closes #9461. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6c2c4d5 commit 4cc7999

3 files changed

Lines changed: 419 additions & 0 deletions

File tree

Common/Scheduling/BaseScheduleRules.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using NodaTime;
1818
using QuantConnect.Interfaces;
1919
using QuantConnect.Securities;
20+
using System.Collections.Generic;
2021

2122
namespace QuantConnect.Scheduling
2223
{
@@ -70,6 +71,34 @@ protected SecurityExchangeHours GetSecurityExchangeHours(Symbol symbol)
7071
return security.Exchange.Hours;
7172
}
7273

74+
/// <summary>
75+
/// Helper method to fetch the exchange hours of the securities currently in <see cref="Securities"/>
76+
/// whose markets are not always open. If no such securities are present, falls back to US equities (SPY).
77+
/// </summary>
78+
protected IEnumerable<SecurityExchangeHours> GetMarketOpenCloseExchangeHours()
79+
{
80+
// Pre-seed with SPY's exchange hours: this guarantees a fallback when no eligible
81+
// security is subscribed and implicitly covers every US equity, which shares the
82+
// same exchange hours — so we can skip US equities below to save the lookup.
83+
var hours = new HashSet<SecurityExchangeHours>
84+
{
85+
MarketHoursDatabase.GetEntry(Market.USA, "SPY", SecurityType.Equity).ExchangeHours
86+
};
87+
foreach (var (symbol, security) in Securities)
88+
{
89+
if (security.Type == SecurityType.Equity && symbol.ID.Market == Market.USA)
90+
{
91+
continue;
92+
}
93+
var exchangeHours = security.Exchange.Hours;
94+
if (!exchangeHours.IsMarketAlwaysOpen)
95+
{
96+
hours.Add(exchangeHours);
97+
}
98+
}
99+
return hours;
100+
}
101+
73102
protected Symbol GetSymbol(string ticker)
74103
{
75104
if (SymbolCache.TryGetSymbol(ticker, out var symbolCache))

Common/Scheduling/TimeRules.cs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,19 @@ public ITimeRule Every(TimeSpan interval)
155155
return new FuncTimeRule(name, applicator);
156156
}
157157

158+
/// <summary>
159+
/// Specifies an event should fire at market open +- <paramref name="minutesBeforeOpen"/>.
160+
/// Picks, per date, the earliest market open across the algorithm's securities, ignoring always-open
161+
/// exchanges. Defaults to US equities (SPY) when no eligible security is subscribed.
162+
/// </summary>
163+
/// <param name="minutesBeforeOpen">The minutes before market open that the event should fire</param>
164+
/// <param name="extendedMarketOpen">True to use extended market open, false to use regular market open</param>
165+
/// <returns>A time rule that fires the specified number of minutes before the earliest market open</returns>
166+
public ITimeRule BeforeMarketOpen(double minutesBeforeOpen = 0, bool extendedMarketOpen = false)
167+
{
168+
return AfterMarketOpen(minutesBeforeOpen * (-1), extendedMarketOpen);
169+
}
170+
158171
/// <summary>
159172
/// Specifies an event should fire at market open +- <paramref name="minutesBeforeOpen"/>
160173
/// </summary>
@@ -176,6 +189,56 @@ public ITimeRule BeforeMarketOpen(Symbol symbol, double minutesBeforeOpen = 0, b
176189
return AfterMarketOpen(symbol, minutesBeforeOpen * (-1), extendedMarketOpen);
177190
}
178191

192+
/// <summary>
193+
/// Specifies an event should fire at market open +- <paramref name="minutesAfterOpen"/>.
194+
/// Picks, per date, the earliest market open across the algorithm's securities, ignoring always-open
195+
/// exchanges. Defaults to US equities (SPY) when no eligible security is subscribed.
196+
/// </summary>
197+
/// <param name="minutesAfterOpen">The minutes after market open that the event should fire</param>
198+
/// <param name="extendedMarketOpen">True to use extended market open, false to use regular market open</param>
199+
/// <returns>A time rule that fires the specified number of minutes after the earliest market open</returns>
200+
public ITimeRule AfterMarketOpen(double minutesAfterOpen = 0, bool extendedMarketOpen = false)
201+
{
202+
var type = extendedMarketOpen ? "ExtendedMarketOpen" : "MarketOpen";
203+
var afterOrBefore = minutesAfterOpen > 0 ? "after" : "before";
204+
var name = Invariant($"{Math.Abs(minutesAfterOpen):0.##} min {afterOrBefore} {type}");
205+
var timeAfterOpen = TimeSpan.FromMinutes(minutesAfterOpen);
206+
207+
return new FuncTimeRule(name, dates => EarliestMarketOpenTimes(dates, extendedMarketOpen, timeAfterOpen));
208+
}
209+
210+
private IEnumerable<DateTime> EarliestMarketOpenTimes(IEnumerable<DateTime> dates, bool extendedMarketOpen, TimeSpan timeAfterOpen)
211+
{
212+
var exchangeHoursList = GetMarketOpenCloseExchangeHours();
213+
foreach (var date in dates)
214+
{
215+
var hasValue = false;
216+
var earliestUtc = default(DateTime);
217+
foreach (var exchangeHours in exchangeHoursList)
218+
{
219+
if (!exchangeHours.IsDateOpen(date, extendedMarketOpen))
220+
{
221+
continue;
222+
}
223+
var marketOpen = exchangeHours.GetFirstDailyMarketOpen((date + Time.OneDay).AddTicks(-1), extendedMarketOpen);
224+
if (marketOpen.Date != date.Date)
225+
{
226+
continue;
227+
}
228+
var utc = (marketOpen + timeAfterOpen).ConvertToUtc(exchangeHours.TimeZone);
229+
if (!hasValue || utc < earliestUtc)
230+
{
231+
hasValue = true;
232+
earliestUtc = utc;
233+
}
234+
}
235+
if (hasValue)
236+
{
237+
yield return earliestUtc;
238+
}
239+
}
240+
}
241+
179242
/// <summary>
180243
/// Specifies an event should fire at market open +- <paramref name="minutesAfterOpen"/>
181244
/// </summary>
@@ -212,6 +275,19 @@ where exchangeHours.IsDateOpen(date, extendedMarketOpen) && marketOpen.Date == d
212275
return new FuncTimeRule(name, applicator);
213276
}
214277

278+
/// <summary>
279+
/// Specifies an event should fire at the market close +- <paramref name="minutesAfterClose"/>.
280+
/// Picks, per date, the latest market close across the algorithm's securities, ignoring always-open
281+
/// exchanges. Defaults to US equities (SPY) when no eligible security is subscribed.
282+
/// </summary>
283+
/// <param name="minutesAfterClose">The time after market close that the event should fire</param>
284+
/// <param name="extendedMarketClose">True to use extended market close, false to use regular market close</param>
285+
/// <returns>A time rule that fires the specified number of minutes after the latest market close</returns>
286+
public ITimeRule AfterMarketClose(double minutesAfterClose = 0, bool extendedMarketClose = false)
287+
{
288+
return BeforeMarketClose(minutesAfterClose * (-1), extendedMarketClose);
289+
}
290+
215291
/// <summary>
216292
/// Specifies an event should fire at the market close +- <paramref name="minutesAfterClose"/>
217293
/// </summary>
@@ -233,6 +309,52 @@ public ITimeRule AfterMarketClose(Symbol symbol, double minutesAfterClose = 0, b
233309
return BeforeMarketClose(symbol, minutesAfterClose * (-1), extendedMarketClose);
234310
}
235311

312+
/// <summary>
313+
/// Specifies an event should fire at the market close +- <paramref name="minutesBeforeClose"/>.
314+
/// Picks, per date, the latest market close across the algorithm's securities, ignoring always-open
315+
/// exchanges. Defaults to US equities (SPY) when no eligible security is subscribed.
316+
/// </summary>
317+
/// <param name="minutesBeforeClose">The time before market close that the event should fire</param>
318+
/// <param name="extendedMarketClose">True to use extended market close, false to use regular market close</param>
319+
/// <returns>A time rule that fires the specified number of minutes before the latest market close</returns>
320+
public ITimeRule BeforeMarketClose(double minutesBeforeClose = 0, bool extendedMarketClose = false)
321+
{
322+
var type = extendedMarketClose ? "ExtendedMarketClose" : "MarketClose";
323+
var afterOrBefore = minutesBeforeClose > 0 ? "before" : "after";
324+
var name = Invariant($"{Math.Abs(minutesBeforeClose):0.##} min {afterOrBefore} {type}");
325+
var timeBeforeClose = TimeSpan.FromMinutes(minutesBeforeClose);
326+
327+
return new FuncTimeRule(name, dates => LatestMarketCloseTimes(dates, extendedMarketClose, timeBeforeClose));
328+
}
329+
330+
private IEnumerable<DateTime> LatestMarketCloseTimes(IEnumerable<DateTime> dates, bool extendedMarketClose, TimeSpan timeBeforeClose)
331+
{
332+
var exchangeHoursList = GetMarketOpenCloseExchangeHours();
333+
foreach (var date in dates)
334+
{
335+
var hasValue = false;
336+
var latestUtc = default(DateTime);
337+
foreach (var exchangeHours in exchangeHoursList)
338+
{
339+
if (!exchangeHours.IsDateOpen(date, extendedMarketClose))
340+
{
341+
continue;
342+
}
343+
var marketClose = exchangeHours.GetLastDailyMarketClose(date, extendedMarketClose);
344+
var utc = (marketClose - timeBeforeClose).ConvertToUtc(exchangeHours.TimeZone);
345+
if (!hasValue || utc > latestUtc)
346+
{
347+
hasValue = true;
348+
latestUtc = utc;
349+
}
350+
}
351+
if (hasValue)
352+
{
353+
yield return latestUtc;
354+
}
355+
}
356+
}
357+
236358
/// <summary>
237359
/// Specifies an event should fire at the market close +- <paramref name="minutesBeforeClose"/>
238360
/// </summary>

0 commit comments

Comments
 (0)