Lean  $LEAN_TAG$
MultiSymbolIndicator.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
18 using NodaTime;
19 using QuantConnect.Data;
20 using System.Collections.Generic;
21 using System.Linq;
22 
24 {
25  /// <summary>
26  /// Base class for indicators that work with multiple different symbols.
27  /// </summary>
28  /// <typeparam name="TInput">Indicator input data type</typeparam>
29  public abstract class MultiSymbolIndicator<TInput> : IndicatorBase<TInput>, IIndicatorWarmUpPeriodProvider
30  where TInput : IBaseData
31  {
32  /// <summary>
33  /// Relevant data for each symbol the indicator works on, including all inputs
34  /// and actual data points used for calculation.
35  /// </summary>
36  protected Dictionary<Symbol, SymbolData> DataBySymbol { get; }
37 
38  /// <summary>
39  /// The most recently computed value of the indicator.
40  /// </summary>
41  protected decimal IndicatorValue { get; set; }
42 
43  /// <summary>
44  /// Required period, in data points, for the indicator to be ready and fully initialized.
45  /// </summary>
46  public int WarmUpPeriod { get; set; }
47 
48  /// <summary>
49  /// Gets a flag indicating when this indicator is ready and fully initialized
50  /// </summary>
51  public override bool IsReady => DataBySymbol.Values.All(data => data.DataPoints.IsReady);
52 
53  /// <summary>
54  /// Initializes the dual symbol indicator.
55  /// <para>
56  /// The constructor accepts a target symbol and a reference symbol. It also initializes
57  /// the time zones for both symbols and checks if they are different.
58  /// </para>
59  /// </summary>
60  /// <param name="name">The name of the indicator.</param>
61  /// <param name="symbols">The symbols the indicator works on .</param>
62  /// <param name="period">The period (number of data points) over which to calculate the indicator.</param>
63  protected MultiSymbolIndicator(string name, IEnumerable<Symbol> symbols, int period)
64  : base(name)
65  {
66  DataBySymbol = symbols.ToDictionary(symbol => symbol, symbol => new SymbolData(symbol, period));
67  var isTimezoneDifferent = DataBySymbol.Values.Select(data => data.ExchangeTimeZone).Distinct().Count() > 1;
68  WarmUpPeriod = period + (isTimezoneDifferent ? 1 : 0);
69  }
70 
71  /// <summary>
72  /// Checks and computes the indicator if the input data matches.
73  /// This method ensures the input data points are from matching time periods and different symbols.
74  /// </summary>
75  /// <param name="input">The input data point (e.g., TradeBar for a symbol).</param>
76  /// <returns>The most recently computed value of the indicator.</returns>
77  protected override decimal ComputeNextValue(TInput input)
78  {
79  if (!DataBySymbol.TryGetValue(input.Symbol, out var symbolData))
80  {
81  throw new ArgumentException($"Input symbol {input.Symbol} does not correspond to any " +
82  $"of the symbols this indicator works on ({string.Join(", ", DataBySymbol.Keys)})");
83  }
84 
85  if (Samples == 1)
86  {
87  SetResolution(input);
88  return decimal.Zero;
89  }
90 
91  symbolData.CurrentInput = input;
92 
93  // Ready to calculate when all symbols get data for the same time
94  if (DataBySymbol.Values.Select(data => data.CurrentInputEndTimeUtc).Distinct().Count() == 1)
95  {
96  // Add the actual inputs that should be used to the rolling windows
97  foreach (var data in DataBySymbol.Values)
98  {
99  data.DataPoints.Add(data.CurrentInput);
100  }
102  }
103 
104  return IndicatorValue;
105  }
106 
107  /// <summary>
108  /// Computes the next value of this indicator from the given state.
109  /// This will be called only when the indicator is ready, that is,
110  /// when data for all symbols at a given time is available.
111  /// </summary>
112  protected abstract decimal ComputeIndicator();
113 
114  /// <summary>
115  /// Resets this indicator to its initial state
116  /// </summary>
117  public override void Reset()
118  {
119  IndicatorValue = 0;
120  foreach (var data in DataBySymbol.Values)
121  {
122  data.Reset();
123  }
124  base.Reset();
125  }
126 
127  /// <summary>
128  /// Determines the resolution of the input data based on the time difference between its start and end times.
129  /// Resolution will <see cref="Resolution.Daily"/> if the difference exceeds 1 hour; otherwise, calculates a higher equivalent resolution.
130  /// Then it sets the resolution to the symbols data so that the time alignment is performed correctly.
131  /// </summary>
132  private void SetResolution(TInput input)
133  {
134  var timeDifference = input.EndTime - input.Time;
135  var resolution = timeDifference.TotalHours > 1 ? Resolution.Daily : timeDifference.ToHigherResolutionEquivalent(false);
136  foreach (var (symbol, data) in DataBySymbol)
137  {
138  data.SetResolution(resolution);
139  if (symbol == input.Symbol)
140  {
141  data.CurrentInput = input;
142  }
143  }
144  }
145 
146  /// <summary>
147  /// Contains the data points, the current input and other relevant indicator data for a symbol.
148  /// </summary>
149  protected class SymbolData
150  {
151  private static MarketHoursDatabase _marketHoursDatabase = MarketHoursDatabase.FromDataFolder();
152 
153  private TInput _currentInput;
154  private Resolution _resolution;
155 
156  /// <summary>
157  /// The exchange time zone for the security represented by this symbol.
158  /// </summary>
159  public DateTimeZone ExchangeTimeZone { get; }
160 
161  /// <summary>
162  /// Data points for the symbol.
163  /// This only hold the data points that have been used to calculate the indicator,
164  /// which are those that had matching end times for every symbol.
165  /// </summary>
167 
168  /// <summary>
169  /// The last input data point for the symbol.
170  /// </summary>
171  public TInput CurrentInput
172  {
173  get => _currentInput;
174  set
175  {
176  _currentInput = value;
177  if (_currentInput != null)
178  {
179  CurrentInputEndTimeUtc = AdjustDateToResolution(_currentInput.EndTime.ConvertToUtc(ExchangeTimeZone));
180  NewInput?.Invoke(this, _currentInput);
181  }
182  else
183  {
184  CurrentInputEndTimeUtc = default;
185  }
186  }
187  }
188 
189  /// <summary>
190  /// Event that fires when a new input data point is set for the symbol.
191  /// </summary>
192  public event EventHandler<TInput> NewInput;
193 
194  /// <summary>
195  /// The end time of the last input data point for the symbol in UTC.
196  /// </summary>
197  public DateTime CurrentInputEndTimeUtc { get; private set; }
198 
199  /// <summary>
200  /// Initializes a new instance of the <see cref="SymbolData"/> class.
201  /// </summary>
202  public SymbolData(Symbol symbol, int period)
203  {
204  ExchangeTimeZone = _marketHoursDatabase.GetExchangeHours(symbol.ID.Market, symbol, symbol.ID.SecurityType).TimeZone;
205  DataPoints = new(period);
206  }
207 
208  /// <summary>
209  /// Resets this symbol data to its initial state
210  /// </summary>
211  public void Reset()
212  {
213  DataPoints.Reset();
214  CurrentInput = default;
215  }
216 
217  /// <summary>
218  /// Truncates the given DateTime based on the specified resolution (Daily, Hourly, Minute, or Second).
219  /// </summary>
220  /// <param name="date">The DateTime to truncate.</param>
221  /// <returns>A DateTime truncated to the specified resolution.</returns>
222  private DateTime AdjustDateToResolution(DateTime date)
223  {
224  switch (_resolution)
225  {
226  case Resolution.Daily:
227  return date.Date;
228  case Resolution.Hour:
229  return date.Date.AddHours(date.Hour);
230  case Resolution.Minute:
231  return date.Date.AddHours(date.Hour).AddMinutes(date.Minute);
232  default:
233  return date;
234  }
235  }
236 
237  /// <summary>
238  /// Sets the resolution for this symbol data, to be used for time alignment.
239  /// </summary>
240  public void SetResolution(Resolution resolution)
241  {
242  _resolution = resolution;
243  }
244  }
245  }
246 }