@@ -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