/* * 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.Securities; using NodaTime; using QuantConnect.Data; using System.Collections.Generic; using System.Linq; namespace QuantConnect.Indicators { /// /// Base class for indicators that work with multiple different symbols. /// /// Indicator input data type public abstract class MultiSymbolIndicator : IndicatorBase, IIndicatorWarmUpPeriodProvider where TInput : IBaseData { /// /// Relevant data for each symbol the indicator works on, including all inputs /// and actual data points used for calculation. /// protected Dictionary DataBySymbol { get; } /// /// The most recently computed value of the indicator. /// protected decimal IndicatorValue { get; set; } /// /// Required period, in data points, for the indicator to be ready and fully initialized. /// public int WarmUpPeriod { get; set; } /// /// Gets a flag indicating when this indicator is ready and fully initialized /// public override bool IsReady => DataBySymbol.Values.All(data => data.DataPoints.IsReady); /// /// Initializes the dual symbol indicator. /// /// The constructor accepts a target symbol and a reference symbol. It also initializes /// the time zones for both symbols and checks if they are different. /// /// /// The name of the indicator. /// The symbols the indicator works on . /// The period (number of data points) over which to calculate the indicator. protected MultiSymbolIndicator(string name, IEnumerable symbols, int period) : base(name) { DataBySymbol = symbols.ToDictionary(symbol => symbol, symbol => new SymbolData(symbol, period)); var isTimezoneDifferent = DataBySymbol.Values.Select(data => data.ExchangeTimeZone).Distinct().Count() > 1; WarmUpPeriod = period + (isTimezoneDifferent ? 1 : 0); } /// /// Checks and computes the indicator if the input data matches. /// This method ensures the input data points are from matching time periods and different symbols. /// /// The input data point (e.g., TradeBar for a symbol). /// The most recently computed value of the indicator. protected override decimal ComputeNextValue(TInput input) { if (!DataBySymbol.TryGetValue(input.Symbol, out var symbolData)) { throw new ArgumentException($"Input symbol {input.Symbol} does not correspond to any " + $"of the symbols this indicator works on ({string.Join(", ", DataBySymbol.Keys)})"); } if (Samples == 1) { SetResolution(input); return decimal.Zero; } symbolData.CurrentInput = input; // Ready to calculate when all symbols get data for the same time if (DataBySymbol.Values.Select(data => data.CurrentInputEndTimeUtc).Distinct().Count() == 1) { // Add the actual inputs that should be used to the rolling windows foreach (var data in DataBySymbol.Values) { data.DataPoints.Add(data.CurrentInput); } IndicatorValue = ComputeIndicator(); } return IndicatorValue; } /// /// Computes the next value of this indicator from the given state. /// This will be called only when the indicator is ready, that is, /// when data for all symbols at a given time is available. /// protected abstract decimal ComputeIndicator(); /// /// Resets this indicator to its initial state /// public override void Reset() { IndicatorValue = 0; foreach (var data in DataBySymbol.Values) { data.Reset(); } base.Reset(); } /// /// Determines the resolution of the input data based on the time difference between its start and end times. /// Resolution will if the difference exceeds 1 hour; otherwise, calculates a higher equivalent resolution. /// Then it sets the resolution to the symbols data so that the time alignment is performed correctly. /// private void SetResolution(TInput input) { var timeDifference = input.EndTime - input.Time; var resolution = timeDifference.TotalHours > 1 ? Resolution.Daily : timeDifference.ToHigherResolutionEquivalent(false); foreach (var (symbol, data) in DataBySymbol) { data.SetResolution(resolution); if (symbol == input.Symbol) { data.CurrentInput = input; } } } /// /// Contains the data points, the current input and other relevant indicator data for a symbol. /// protected class SymbolData { private static MarketHoursDatabase _marketHoursDatabase = MarketHoursDatabase.FromDataFolder(); private TInput _currentInput; private Resolution _resolution; /// /// The exchange time zone for the security represented by this symbol. /// public DateTimeZone ExchangeTimeZone { get; } /// /// Data points for the symbol. /// This only hold the data points that have been used to calculate the indicator, /// which are those that had matching end times for every symbol. /// public RollingWindow DataPoints { get; } /// /// The last input data point for the symbol. /// public TInput CurrentInput { get => _currentInput; set { _currentInput = value; if (_currentInput != null) { CurrentInputEndTimeUtc = AdjustDateToResolution(_currentInput.EndTime.ConvertToUtc(ExchangeTimeZone)); NewInput?.Invoke(this, _currentInput); } else { CurrentInputEndTimeUtc = default; } } } /// /// Event that fires when a new input data point is set for the symbol. /// public event EventHandler NewInput; /// /// The end time of the last input data point for the symbol in UTC. /// public DateTime CurrentInputEndTimeUtc { get; private set; } /// /// Initializes a new instance of the class. /// public SymbolData(Symbol symbol, int period) { ExchangeTimeZone = _marketHoursDatabase.GetExchangeHours(symbol.ID.Market, symbol, symbol.ID.SecurityType).TimeZone; DataPoints = new(period); } /// /// Resets this symbol data to its initial state /// public void Reset() { DataPoints.Reset(); CurrentInput = default; } /// /// Truncates the given DateTime based on the specified resolution (Daily, Hourly, Minute, or Second). /// /// The DateTime to truncate. /// A DateTime truncated to the specified resolution. private DateTime AdjustDateToResolution(DateTime date) { switch (_resolution) { case Resolution.Daily: return date.Date; case Resolution.Hour: return date.Date.AddHours(date.Hour); case Resolution.Minute: return date.Date.AddHours(date.Hour).AddMinutes(date.Minute); default: return date; } } /// /// Sets the resolution for this symbol data, to be used for time alignment. /// public void SetResolution(Resolution resolution) { _resolution = resolution; } } } }