/*
* 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;
}
}
}