/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2024 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 QuantConnect.Util; using QuantConnect.Securities; using QuantConnect.Data.Market; namespace QuantConnect.Data.Consolidators { /// /// Consolidator for open markets bar only, extended hours bar are not consolidated. /// public class MarketHourAwareConsolidator : ConsolidatorBase { private readonly bool _dailyStrictEndTimeEnabled; private readonly bool _extendedMarketHours; private bool _useStrictEndTime; /// /// The consolidation period requested /// protected TimeSpan Period { get; } /// /// The consolidator instance /// protected IDataConsolidator Consolidator { get; } /// /// The associated security exchange hours instance /// protected SecurityExchangeHours ExchangeHours { get; set; } /// /// The associated data time zone /// protected DateTimeZone DataTimeZone { get; set; } /// /// Gets the type consumed by this consolidator /// public override Type InputType => Consolidator.InputType; /// /// Gets a clone of the data being currently consolidated /// public override IBaseData WorkingData => Consolidator.WorkingData; /// /// Gets the type produced by this consolidator /// public override Type OutputType => Consolidator.OutputType; /// /// Initializes a new instance of the class. /// /// The resolution. /// The target data type /// The target tick type /// True if extended market hours should be consolidated public MarketHourAwareConsolidator(bool dailyStrictEndTimeEnabled, Resolution resolution, Type dataType, TickType tickType, bool extendedMarketHours) { _dailyStrictEndTimeEnabled = dailyStrictEndTimeEnabled; Period = resolution.ToTimeSpan(); _extendedMarketHours = extendedMarketHours; Consolidator = CreateConsolidator(resolution, dataType, tickType); Consolidator.DataConsolidated += ForwardConsolidatedBar; } /// /// Initializes a new instance of the class for an arbitrary period. /// Intraday periods are anchored to the market open without extending past the close. /// /// True if daily strict end times should be enabled /// The consolidation period /// The target data type /// The target tick type /// True if extended market hours should be consolidated public MarketHourAwareConsolidator(bool dailyStrictEndTimeEnabled, TimeSpan period, Type dataType, TickType tickType, bool extendedMarketHours) { _dailyStrictEndTimeEnabled = dailyStrictEndTimeEnabled; Period = period; _extendedMarketHours = extendedMarketHours; // when the period exactly matches a standard resolution, reuse the resolution based consolidation so its // well-tested behavior is preserved; only arbitrary periods need the market-open anchored intraday calendar var resolution = period.ToHigherResolutionEquivalent(false); if (resolution.ToTimeSpan() == period) { Consolidator = CreateConsolidator(resolution, dataType, tickType); } else { Func calendar = period < Time.OneDay ? IntradayCalendar : DailyStrictEndTime; Consolidator = CreateConsolidator(calendar, dataType, tickType); } Consolidator.DataConsolidated += ForwardConsolidatedBar; } /// /// Creates the inner consolidator that produces the requested output. /// protected virtual IDataConsolidator CreateConsolidator(Resolution resolution, Type dataType, TickType tickType) { if (dataType == typeof(Tick)) { if (tickType == TickType.Trade) { return resolution == Resolution.Daily ? new TickConsolidator(DailyStrictEndTime) : new TickConsolidator(Period); } return resolution == Resolution.Daily ? new TickQuoteBarConsolidator(DailyStrictEndTime) : new TickQuoteBarConsolidator(Period); } if (dataType == typeof(TradeBar)) { return resolution == Resolution.Daily ? new TradeBarConsolidator(DailyStrictEndTime) : new TradeBarConsolidator(Period); } if (dataType == typeof(QuoteBar)) { return resolution == Resolution.Daily ? new QuoteBarConsolidator(DailyStrictEndTime) : new QuoteBarConsolidator(Period); } throw new ArgumentNullException(nameof(dataType), $"{dataType.Name} not supported"); } /// /// Creates the underlying calendar based consolidator for the given data type, used for arbitrary periods /// protected virtual IDataConsolidator CreateConsolidator(Func calendar, Type dataType, TickType tickType) { if (dataType == typeof(Tick)) { return tickType == TickType.Trade ? new TickConsolidator(calendar) : new TickQuoteBarConsolidator(calendar); } if (dataType == typeof(TradeBar)) { return new TradeBarConsolidator(calendar); } if (dataType == typeof(QuoteBar)) { return new QuoteBarConsolidator(calendar); } throw new ArgumentNullException(nameof(dataType), $"{dataType.Name} not supported"); } /// /// Updates this consolidator with the specified data /// /// The new data for the consolidator public override void Update(IBaseData data) { Initialize(data); // US equity hour data from the database starts at 9am but the exchange opens at 9:30am. Thus, we need to handle // this case specifically to avoid skipping the first hourly bar. To avoid this, we assert the period is daily, // the data resolution is hour and the exchange opens at any point in time over the data.Time to data.EndTime interval if (_extendedMarketHours || ExchangeHours.IsOpen(data.Time, false) || (Period == Time.OneDay && (data.EndTime - data.Time >= Time.OneHour) && ExchangeHours.IsOpen(data.Time, data.EndTime, false))) { Consolidator.Update(data); } } /// /// Scans this consolidator to see if it should emit a bar due to time passing /// /// The current time in the local time zone (same as ) public override void Scan(DateTime currentLocalTime) { Consolidator.Scan(currentLocalTime); } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public override void Dispose() { Consolidator.DataConsolidated -= ForwardConsolidatedBar; Consolidator.Dispose(); base.Dispose(); } /// /// Resets the consolidator /// public override void Reset() { _useStrictEndTime = false; ExchangeHours = null; DataTimeZone = null; Consolidator.Reset(); base.Reset(); } /// /// Perform late initialization based on the datas symbol /// protected void Initialize(IBaseData data) { if (ExchangeHours == null) { var symbol = data.Symbol; var marketHoursDatabase = MarketHoursDatabase.FromDataFolder(); ExchangeHours = marketHoursDatabase.GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType); DataTimeZone = marketHoursDatabase.GetDataTimeZone(symbol.ID.Market, symbol, symbol.SecurityType); _useStrictEndTime = UseStrictEndTime(data.Symbol); } } /// /// Determines a bar start time and period /// protected virtual CalendarInfo DailyStrictEndTime(DateTime dateTime) { // strict end times describe a single daily bar, so periods larger than a day fall back to standard period consolidation if (!_useStrictEndTime || Period > Time.OneDay) { return new(Period > Time.OneDay ? dateTime : dateTime.RoundDown(Period), Period); } return LeanData.GetDailyCalendar(dateTime, ExchangeHours, _extendedMarketHours); } /// /// Determines a bar start time and period for intraday consolidation, anchored to the market open /// without extending past the market close so a bar never spans across closed market hours /// protected virtual CalendarInfo IntradayCalendar(DateTime dateTime) { if (ExchangeHours == null || ExchangeHours.IsMarketAlwaysOpen) { return new(dateTime.RoundDown(Period), Period); } return LeanData.GetIntradayCalendar(dateTime, Period, ExchangeHours, _extendedMarketHours); } /// /// Useful for testing /// protected virtual bool UseStrictEndTime(Symbol symbol) { return LeanData.UseStrictEndTime(_dailyStrictEndTimeEnabled, symbol, Period, ExchangeHours); } /// /// Will forward the underlying consolidated bar to consumers on this object. /// This wrapper keeps its own rolling window in addition to the inner consolidator's window. /// protected virtual void ForwardConsolidatedBar(object sender, IBaseData consolidated) { OnDataConsolidated(consolidated); } } }