/* * 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 QuantConnect.Util; using QuantConnect.Data; using System.Collections.Generic; using QuantConnect.Securities; using System.Runtime.CompilerServices; namespace QuantConnect.Lean.Engine.DataFeeds { /// /// Time keeper specialization to keep time for a subscription both in data and exchange time zones. /// It also emits events when the exchange date changes, which is useful to emit date change events /// required for some daily actions like mapping symbols, delistings, splits, etc. /// internal class DateChangeTimeKeeper : TimeKeeper, IDisposable { private IEnumerator _tradableDatesInDataTimeZone; private SubscriptionDataConfig _config; private SecurityExchangeHours _exchangeHours; private DateTime _delistingDate; private DateTime _previousNewExchangeDate; private bool _needsMoveNext; private bool _initialized; private DateTime _exchangeTime; private DateTime _dataTime; private bool _exchangeTimeNeedsUpdate; private bool _dataTimeNeedsUpdate; /// /// The current time in the data time zone /// public DateTime DataTime { get { if (_dataTimeNeedsUpdate) { _dataTime = GetTimeIn(_config.DataTimeZone); _dataTimeNeedsUpdate = false; } return _dataTime; } } /// /// The current time in the exchange time zone /// public DateTime ExchangeTime { get { if (_exchangeTimeNeedsUpdate) { _exchangeTime = GetTimeIn(_exchangeHours.TimeZone); _exchangeTimeNeedsUpdate = false; } return _exchangeTime; } } /// /// Event that fires every time the exchange date changes /// public event EventHandler NewExchangeDate; /// /// Initializes a new instance of the class /// /// The tradable dates in data time zone /// The subscription data configuration this instance will keep track of time for /// The exchange hours /// The symbol's delisting date public DateChangeTimeKeeper(IEnumerable tradableDatesInDataTimeZone, SubscriptionDataConfig config, SecurityExchangeHours exchangeHours, DateTime delistingDate) : base(Time.BeginningOfTime, new[] { config.DataTimeZone, config.ExchangeTimeZone }) { _tradableDatesInDataTimeZone = tradableDatesInDataTimeZone.GetEnumerator(); _config = config; _exchangeHours = exchangeHours; _delistingDate = delistingDate; _exchangeTimeNeedsUpdate = true; _dataTimeNeedsUpdate = true; _needsMoveNext = true; } /// /// Disposes the resources /// public void Dispose() { _tradableDatesInDataTimeZone.DisposeSafely(); } /// /// Sets the current UTC time for this time keeper /// /// The current time in UTC public override void SetUtcDateTime(DateTime utcDateTime) { base.SetUtcDateTime(utcDateTime); _exchangeTimeNeedsUpdate = true; _dataTimeNeedsUpdate = true; } /// /// Advances the time keeper towards the target exchange time. /// If an exchange date is found before the target time, it is emitted and the time keeper is set to that date. /// The caller must check whether the target time was reached or if the time keeper was set to a new exchange date before the target time. /// public void AdvanceTowardsExchangeTime(DateTime targetExchangeTime) { if (!_initialized) { throw new InvalidOperationException($"The time keeper has not been initialized. " + $"{nameof(TryAdvanceUntilNextDataDate)} needs to be called at least once to flush the first date before advancing."); } var currentExchangeTime = ExchangeTime; // Advancing within the same exchange date, just update the time, no new exchange date will be emitted if (targetExchangeTime.Date == currentExchangeTime.Date) { SetExchangeTime(targetExchangeTime); return; } while (currentExchangeTime < targetExchangeTime) { var newExchangeTime = currentExchangeTime + Time.OneDay; if (newExchangeTime > targetExchangeTime) { newExchangeTime = targetExchangeTime; } var newExchangeDate = newExchangeTime.Date; // We found a new exchange date before the target time, emit it first if (newExchangeDate != currentExchangeTime.Date && _exchangeHours.IsDateOpen(newExchangeDate, _config.ExtendedMarketHours)) { // Stop here, set the new exchange date SetExchangeTime(newExchangeDate); EmitNewExchangeDate(newExchangeDate); return; } currentExchangeTime = newExchangeTime; } // We reached the target time, set it SetExchangeTime(targetExchangeTime); } /// /// Advances the time keeper until the next data date, emitting the new exchange date if this happens before the new data date /// public bool TryAdvanceUntilNextDataDate() { if (!_initialized) { return EmitFirstExchangeDate(); } // Before moving forward, check whether we need to emit a new exchange date if (TryEmitPassedExchangeDate()) { return true; } if (!_needsMoveNext || _tradableDatesInDataTimeZone.MoveNext()) { var nextDataDate = _tradableDatesInDataTimeZone.Current; var nextExchangeTime = nextDataDate.ConvertTo(_config.DataTimeZone, _exchangeHours.TimeZone); var nextExchangeDate = nextExchangeTime.Date; if (nextExchangeDate > _delistingDate) { // We are done, but an exchange date might still need to be emitted TryEmitPassedExchangeDate(); _needsMoveNext = false; return false; } // If the exchange is not behind the data, the data might have not been enough to emit the exchange date, // which already passed if we are moving on to the next data date. So we need to check if we need to emit it here. // e.g. moving data date from tuesday to wednesday, but the exchange date is already past the end of tuesday // (by N hours, depending on the time zones offset). If data didn't trigger the exchange date change, we need to do it here. if (!IsExchangeBehindData(nextExchangeTime, nextDataDate) && nextExchangeDate > _previousNewExchangeDate) { EmitNewExchangeDate(nextExchangeDate); SetExchangeTime(nextExchangeDate); // nextExchangeDate == DataTime means time zones are synchronized, need to move next only when exchange is actually ahead _needsMoveNext = nextExchangeDate == DataTime; return true; } _needsMoveNext = true; SetDataTime(nextDataDate); return true; } _needsMoveNext = false; return false; } /// /// Emits the first exchange date for the algorithm so that the first daily events are triggered (mappings, delistings, etc.) /// /// True if the new exchange date is emitted. False if already done or the tradable dates enumerable is empty private bool EmitFirstExchangeDate() { if (_initialized) { return false; } if (!_tradableDatesInDataTimeZone.MoveNext()) { _initialized = true; return false; } var firstDataDate = _tradableDatesInDataTimeZone.Current; var firstExchangeTime = firstDataDate.ConvertTo(_config.DataTimeZone, _exchangeHours.TimeZone); var firstExchangeDate = firstExchangeTime.Date; DateTime exchangeDateToEmit; // The exchange is ahead of the data, so we need to emit the current exchange date, which already passed if (firstExchangeTime < firstDataDate && _exchangeHours.IsDateOpen(firstExchangeDate, _config.ExtendedMarketHours)) { exchangeDateToEmit = firstExchangeDate; SetExchangeTime(exchangeDateToEmit); // Don't move, the current data date still needs to be consumed _needsMoveNext = false; } // The exchange is behind of (or in sync with) data: exchange has not passed to this new date, but with emit it here // so that first daily things are done (mappings, delistings, etc.) else { exchangeDateToEmit = firstDataDate; SetDataTime(firstDataDate); _needsMoveNext = true; } EmitNewExchangeDate(exchangeDateToEmit); _initialized = true; return true; } /// /// Determines whether the exchange time zone is behind the data time zone /// /// public bool IsExchangeBehindData() { return IsExchangeBehindData(ExchangeTime, DataTime); } /// /// Determines whether the exchange time zone is behind the data time zone /// private static bool IsExchangeBehindData(DateTime exchangeTime, DateTime dataTime) { return dataTime > exchangeTime; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetExchangeTime(DateTime exchangeTime) { SetUtcDateTime(exchangeTime.ConvertToUtc(_exchangeHours.TimeZone)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetDataTime(DateTime dataTime) { SetUtcDateTime(dataTime.ConvertToUtc(_config.DataTimeZone)); } /// /// Checks that before moving to the next data date, if the exchange date has already passed and has been emitted, else it emits it. /// This can happen when the exchange is behind of the data. e.g We advance data date from Monday to Tuesday, then the data itself /// will drive the exchange data change (N hours later, depending on the time zones offset). /// But if there is no enough data or the file is not found, the new exchange date will not be emitted, so we need to do it here. /// private bool TryEmitPassedExchangeDate() { if (_needsMoveNext && _tradableDatesInDataTimeZone.Current != default) { // This data date passed, and it should have emitted as an exchange tradable date when detected // as a date change in the data itself, if not, emit it now before moving to the next data date var currentDataDate = _tradableDatesInDataTimeZone.Current; if (_previousNewExchangeDate < currentDataDate && _exchangeHours.IsDateOpen(currentDataDate, _config.ExtendedMarketHours)) { var nextExchangeDate = currentDataDate; SetExchangeTime(nextExchangeDate); EmitNewExchangeDate(nextExchangeDate); return true; } } return false; } /// /// Emits a new exchange date event /// private void EmitNewExchangeDate(DateTime newExchangeDate) { NewExchangeDate?.Invoke(this, newExchangeDate); _previousNewExchangeDate = newExchangeDate; } } }