Files
2026-07-13 13:02:50 +08:00

412 lines
23 KiB
C#

/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
using System;
using NodaTime;
using System.Linq;
using QuantConnect.Interfaces;
using QuantConnect.Securities;
using System.Collections.Generic;
using static QuantConnect.StringExtensions;
namespace QuantConnect.Scheduling
{
/// <summary>
/// Helper class used to provide better syntax when defining time rules
/// </summary>
public class TimeRules : BaseScheduleRules
{
/// <summary>
/// Initializes a new instance of the <see cref="TimeRules"/> helper class
/// </summary>
/// <param name="algorithm">The algorithm instance</param>
/// <param name="securities">The security manager</param>
/// <param name="timeZone">The algorithm's default time zone</param>
/// <param name="marketHoursDatabase">The market hours database instance to use</param>
public TimeRules(IAlgorithm algorithm, SecurityManager securities, DateTimeZone timeZone, MarketHoursDatabase marketHoursDatabase)
: base(algorithm, securities, timeZone, marketHoursDatabase)
{
}
/// <summary>
/// Sets the default time zone
/// </summary>
/// <param name="timeZone">The time zone to use for helper methods that can't resolve a time zone</param>
public void SetDefaultTimeZone(DateTimeZone timeZone)
{
TimeZone = timeZone;
}
/// <summary>
/// Specifies an event should fire at the current time
/// </summary>
public ITimeRule Now => new FuncTimeRule("Now", dates => {
return dates.Select(date =>
{
// we ignore the given date and just use the current time, why? if the algorithm used 'DateRules.Today'
// we get the algorithms first 'Date', which during warmup might not be a complete date, depending on the warmup period
// and since Today returns dates we might get a time in the past which get's ignored. See 'WarmupTrainRegressionAlgorithm'
// which reproduces GH issue #6410
return Securities.UtcTime;
});
});
/// <summary>
/// Convenience property for running a scheduled event at midnight in the algorithm time zone
/// </summary>
public ITimeRule Midnight => new FuncTimeRule("Midnight", dates => dates.Select(date => date.ConvertToUtc(TimeZone)));
/// <summary>
/// Convenience property for running a scheduled event at noon in the algorithm time zone
/// </summary>
public ITimeRule Noon => new FuncTimeRule("Noon", dates => dates.Select(date => date.ConvertToUtc(TimeZone).AddHours(12)));
/// <summary>
/// Specifies an event should fire at the specified time of day in the algorithm's time zone
/// </summary>
/// <param name="timeOfDay">The time of day in the algorithm's time zone the event should fire</param>
/// <returns>A time rule that fires at the specified time in the algorithm's time zone</returns>
public ITimeRule At(TimeSpan timeOfDay)
{
return At(timeOfDay, TimeZone);
}
/// <summary>
/// Specifies an event should fire at the specified time of day in the algorithm's time zone
/// </summary>
/// <param name="hour">The hour</param>
/// <param name="minute">The minute</param>
/// <param name="second">The second</param>
/// <returns>A time rule that fires at the specified time in the algorithm's time zone</returns>
public ITimeRule At(int hour, int minute, int second = 0)
{
return At(new TimeSpan(hour, minute, second), TimeZone);
}
/// <summary>
/// Specifies an event should fire at the specified time of day in the specified time zone
/// </summary>
/// <param name="hour">The hour</param>
/// <param name="minute">The minute</param>
/// <param name="timeZone">The time zone the event time is represented in</param>
/// <returns>A time rule that fires at the specified time in the algorithm's time zone</returns>
public ITimeRule At(int hour, int minute, DateTimeZone timeZone)
{
return At(new TimeSpan(hour, minute, 0), timeZone);
}
/// <summary>
/// Specifies an event should fire at the specified time of day in the specified time zone
/// </summary>
/// <param name="hour">The hour</param>
/// <param name="minute">The minute</param>
/// <param name="second">The second</param>
/// <param name="timeZone">The time zone the event time is represented in</param>
/// <returns>A time rule that fires at the specified time in the algorithm's time zone</returns>
public ITimeRule At(int hour, int minute, int second, DateTimeZone timeZone)
{
return At(new TimeSpan(hour, minute, second), timeZone);
}
/// <summary>
/// Specifies an event should fire at the specified time of day in the specified time zone
/// </summary>
/// <param name="timeOfDay">The time of day in the algorithm's time zone the event should fire</param>
/// <param name="timeZone">The time zone the date time is expressed in</param>
/// <returns>A time rule that fires at the specified time in the algorithm's time zone</returns>
public ITimeRule At(TimeSpan timeOfDay, DateTimeZone timeZone)
{
var name = string.Join(",", timeOfDay.TotalHours.ToStringInvariant("0.##"));
Func<IEnumerable<DateTime>, IEnumerable<DateTime>> applicator = dates =>
from date in dates
let localEventTime = date + timeOfDay
let utcEventTime = localEventTime.ConvertToUtc(timeZone)
select utcEventTime;
return new FuncTimeRule(name, applicator);
}
/// <summary>
/// Specifies an event should fire periodically on the requested interval
/// </summary>
/// <param name="interval">The frequency with which the event should fire, can not be zero or less</param>
/// <returns>A time rule that fires after each interval passes</returns>
public ITimeRule Every(TimeSpan interval)
{
if (interval <= TimeSpan.Zero)
{
throw new ArgumentException("TimeRules.Every(): time span interval can not be zero or less");
}
var name = Invariant($"Every {interval.TotalMinutes:0.##} min");
Func<IEnumerable<DateTime>, IEnumerable<DateTime>> applicator = dates => EveryIntervalIterator(dates, interval, TimeZone);
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>
/// <param name="symbol">The symbol whose market open we want an event for</param>
/// <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 symbol's market open</returns>
public ITimeRule BeforeMarketOpen(string symbol, double minutesBeforeOpen = 0, bool extendedMarketOpen = false) => BeforeMarketOpen(GetSymbol(symbol), minutesBeforeOpen, extendedMarketOpen);
/// <summary>
/// Specifies an event should fire at market open +- <paramref name="minutesBeforeOpen"/>
/// </summary>
/// <param name="symbol">The symbol whose market open we want an event for</param>
/// <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 symbol's market open</returns>
public ITimeRule BeforeMarketOpen(Symbol symbol, double minutesBeforeOpen = 0, bool extendedMarketOpen = false)
{
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>
/// <param name="symbol">The symbol whose market open we want an event for</param>
/// <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 symbol's market open</returns>
public ITimeRule AfterMarketOpen(string symbol, double minutesAfterOpen = 0, bool extendedMarketOpen = false) => AfterMarketOpen(GetSymbol(symbol), minutesAfterOpen, extendedMarketOpen);
/// <summary>
/// Specifies an event should fire at market open +- <paramref name="minutesAfterOpen"/>
/// </summary>
/// <param name="symbol">The symbol whose market open we want an event for</param>
/// <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 symbol's market open</returns>
public ITimeRule AfterMarketOpen(Symbol symbol, double minutesAfterOpen = 0, bool extendedMarketOpen = false)
{
var type = extendedMarketOpen ? "ExtendedMarketOpen" : "MarketOpen";
var afterOrBefore = minutesAfterOpen > 0 ? "after" : "before";
var name = Invariant($"{symbol}: {Math.Abs(minutesAfterOpen):0.##} min {afterOrBefore} {type}");
var exchangeHours = GetSecurityExchangeHours(symbol);
var timeAfterOpen = TimeSpan.FromMinutes(minutesAfterOpen);
Func<IEnumerable<DateTime>, IEnumerable<DateTime>> applicator = dates =>
from date in dates
let marketOpen = exchangeHours.GetFirstDailyMarketOpen((date + Time.OneDay).AddTicks(-1), extendedMarketOpen)
// make sure the market open is of this date
where exchangeHours.IsDateOpen(date, extendedMarketOpen) && marketOpen.Date == date.Date
let localEventTime = marketOpen + timeAfterOpen
let utcEventTime = localEventTime.ConvertToUtc(exchangeHours.TimeZone)
select utcEventTime;
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>
/// <param name="symbol">The symbol whose market close we want an event for</param>
/// <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 symbol's market close</returns>
public ITimeRule AfterMarketClose(string symbol, double minutesAfterClose = 0, bool extendedMarketClose = false) => AfterMarketClose(GetSymbol(symbol), minutesAfterClose, extendedMarketClose);
/// <summary>
/// Specifies an event should fire at the market close +- <paramref name="minutesAfterClose"/>
/// </summary>
/// <param name="symbol">The symbol whose market close we want an event for</param>
/// <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 symbol's market close</returns>
public ITimeRule AfterMarketClose(Symbol symbol, double minutesAfterClose = 0, bool extendedMarketClose = false)
{
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>
/// <param name="symbol">The symbol whose market close we want an event for</param>
/// <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 symbol's market close</returns>
public ITimeRule BeforeMarketClose(string symbol, double minutesBeforeClose = 0, bool extendedMarketClose = false) => BeforeMarketClose(GetSymbol(symbol), minutesBeforeClose, extendedMarketClose);
/// <summary>
/// Specifies an event should fire at the market close +- <paramref name="minutesBeforeClose"/>
/// </summary>
/// <param name="symbol">The symbol whose market close we want an event for</param>
/// <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 symbol's market close</returns>
public ITimeRule BeforeMarketClose(Symbol symbol, double minutesBeforeClose = 0, bool extendedMarketClose = false)
{
var type = extendedMarketClose ? "ExtendedMarketClose" : "MarketClose";
var afterOrBefore = minutesBeforeClose > 0 ? "before" : "after";
var name = Invariant($"{symbol}: {Math.Abs(minutesBeforeClose):0.##} min {afterOrBefore} {type}");
var exchangeHours = GetSecurityExchangeHours(symbol);
var timeBeforeClose = TimeSpan.FromMinutes(minutesBeforeClose);
Func<IEnumerable<DateTime>, IEnumerable<DateTime>> applicator = dates =>
from date in dates
let marketClose = exchangeHours.GetLastDailyMarketClose(date, extendedMarketClose)
// make sure the market open is of this date
where exchangeHours.IsDateOpen(date, extendedMarketClose)
let localEventTime = marketClose - timeBeforeClose
let utcEventTime = localEventTime.ConvertToUtc(exchangeHours.TimeZone)
select utcEventTime;
return new FuncTimeRule(name, applicator);
}
/// <summary>
/// For each provided date will yield all the time intervals based on the supplied time span
/// </summary>
/// <param name="dates">The dates for which we want to create the different intervals</param>
/// <param name="interval">The interval value to use, can not be zero or less</param>
/// <param name="timeZone">The time zone the date time is expressed in</param>
private static IEnumerable<DateTime> EveryIntervalIterator(IEnumerable<DateTime> dates, TimeSpan interval, DateTimeZone timeZone)
{
if (interval <= TimeSpan.Zero)
{
throw new ArgumentException("TimeRules.EveryIntervalIterator(): time span interval can not be zero or less");
}
foreach (var date in dates)
{
for (var time = TimeSpan.Zero; time < Time.OneDay; time += interval)
{
yield return (date + time).ConvertToUtc(timeZone);
}
}
}
}
}