Lean  $LEAN_TAG$
SecurityCache.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;
17 using System.Linq;
18 using QuantConnect.Data;
20 using System.Collections.Generic;
21 using System.Runtime.CompilerServices;
24 using QuantConnect.Util;
25 
27 {
28  /// <summary>
29  /// Base class caching spot for security data and any other temporary properties.
30  /// </summary>
31  public class SecurityCache
32  {
33  // let's share the empty readonly version, so we don't need null checks
34  private static readonly IReadOnlyList<BaseData> _empty = new List<BaseData>();
35 
36  // this is used to prefer quote bar data over the tradebar data
37  private DateTime _lastQuoteBarUpdate;
38  private DateTime _lastOHLCUpdate;
39  private BaseData _lastData;
40 
41  private readonly object _locker = new();
42  private IReadOnlyList<BaseData> _lastTickQuotes = _empty;
43  private IReadOnlyList<BaseData> _lastTickTrades = _empty;
44  private Dictionary<Type, IReadOnlyList<BaseData>> _dataByType;
45 
46  private Dictionary<string, object> _properties;
47 
48  /// <summary>
49  /// Gets the most recent price submitted to this cache
50  /// </summary>
51  public decimal Price { get; private set; }
52 
53  /// <summary>
54  /// Gets the most recent open submitted to this cache
55  /// </summary>
56  public decimal Open { get; private set; }
57 
58  /// <summary>
59  /// Gets the most recent high submitted to this cache
60  /// </summary>
61  public decimal High { get; private set; }
62 
63  /// <summary>
64  /// Gets the most recent low submitted to this cache
65  /// </summary>
66  public decimal Low { get; private set; }
67 
68  /// <summary>
69  /// Gets the most recent close submitted to this cache
70  /// </summary>
71  public decimal Close { get; private set; }
72 
73  /// <summary>
74  /// Gets the most recent bid submitted to this cache
75  /// </summary>
76  public decimal BidPrice { get; private set; }
77 
78  /// <summary>
79  /// Gets the most recent ask submitted to this cache
80  /// </summary>
81  public decimal AskPrice { get; private set; }
82 
83  /// <summary>
84  /// Gets the most recent bid size submitted to this cache
85  /// </summary>
86  public decimal BidSize { get; private set; }
87 
88  /// <summary>
89  /// Gets the most recent ask size submitted to this cache
90  /// </summary>
91  public decimal AskSize { get; private set; }
92 
93  /// <summary>
94  /// Gets the most recent volume submitted to this cache
95  /// </summary>
96  public decimal Volume { get; private set; }
97 
98  /// <summary>
99  /// Gets the most recent open interest submitted to this cache
100  /// </summary>
101  public long OpenInterest { get; private set; }
102 
103  /// <summary>
104  /// Collection of keyed custom properties
105  /// </summary>
106  public Dictionary<string, object> Properties
107  {
108  get
109  {
110  if (_properties == null)
111  {
112  _properties = new Dictionary<string, object>();
113  }
114  return _properties;
115  }
116  }
117 
118  /// <summary>
119  /// Add a list of market data points to the local security cache for the current market price.
120  /// </summary>
121  /// <remarks>Internally uses <see cref="AddData"/> using the last data point of the provided list
122  /// and it stores by type the non fill forward points using <see cref="StoreData"/></remarks>
123  public void AddDataList(IReadOnlyList<BaseData> data, Type dataType, bool? containsFillForwardData = null)
124  {
125  var nonFillForwardData = data;
126  // maintaining regression requires us to NOT cache FF data
127  if (containsFillForwardData != false)
128  {
129  var dataFiltered = new List<BaseData>(data.Count);
130  for (var i = 0; i < data.Count; i++)
131  {
132  var dataPoint = data[i];
133  if (!dataPoint.IsFillForward)
134  {
135  dataFiltered.Add(dataPoint);
136  }
137  }
138  nonFillForwardData = dataFiltered;
139  }
140  if (nonFillForwardData.Count != 0)
141  {
142  StoreData(nonFillForwardData, dataType);
143  }
144  else if (dataType == typeof(OpenInterest))
145  {
146  StoreData(data, typeof(OpenInterest));
147  }
148 
149  var last = data[data.Count - 1];
150 
151  ProcessDataPoint(last, cacheByType: false);
152  }
153 
154  /// <summary>
155  /// Add a new market data point to the local security cache for the current market price.
156  /// Rules:
157  /// Don't cache fill forward data.
158  /// Always return the last observation.
159  /// If two consecutive data has the same time stamp and one is Quotebars and the other Tradebar, prioritize the Quotebar.
160  /// </summary>
161  public void AddData(BaseData data)
162  {
163  ProcessDataPoint(data, cacheByType: true);
164  }
165 
166  /// <summary>
167  /// Will consume the given data point updating the cache state and it's properties
168  /// </summary>
169  /// <param name="data">The data point to process</param>
170  /// <param name="cacheByType">True if this data point should be cached by type</param>
171  protected virtual void ProcessDataPoint(BaseData data, bool cacheByType)
172  {
173  var tick = data as Tick;
174  if (tick?.TickType == TickType.OpenInterest)
175  {
176  if (cacheByType)
177  {
178  StoreDataPoint(data);
179  }
180  OpenInterest = (long)tick.Value;
181  return;
182  }
183 
184  // Only cache non fill-forward data and non auxiliary
185  if (data.IsFillForward) return;
186 
187  if (cacheByType)
188  {
189  StoreDataPoint(data);
190  }
191 
192  // we store auxiliary data by type but we don't use it to set 'lastData' nor price information
193  if (data.DataType == MarketDataType.Auxiliary) return;
194 
195  var isDefaultDataType = SubscriptionManager.IsDefaultDataType(data);
196 
197  // don't set _lastData if receive quotebar then tradebar w/ same end time. this
198  // was implemented to grant preference towards using quote data in the fill
199  // models and provide a level of determinism on the values exposed via the cache.
200  if ((_lastData == null
201  || _lastQuoteBarUpdate != data.EndTime
202  || data.DataType != MarketDataType.TradeBar)
203  // we will only set the default data type to preserve determinism and backwards compatibility
204  && isDefaultDataType)
205  {
206  _lastData = data;
207  }
208 
209  if (tick != null)
210  {
211  if (tick.Value != 0) Price = tick.Value;
212 
213  switch (tick.TickType)
214  {
215  case TickType.Trade:
216  if (tick.Quantity != 0) Volume = tick.Quantity;
217  break;
218 
219  case TickType.Quote:
220  if (tick.BidPrice != 0) BidPrice = tick.BidPrice;
221  if (tick.BidSize != 0) BidSize = tick.BidSize;
222 
223  if (tick.AskPrice != 0) AskPrice = tick.AskPrice;
224  if (tick.AskSize != 0) AskSize = tick.AskSize;
225  break;
226  }
227  return;
228  }
229 
230  var bar = data as IBar;
231  if (bar != null)
232  {
233  // we will only set OHLC values using the default data type to preserve determinism and backwards compatibility.
234  // Gives priority to QuoteBar over TradeBar, to be removed when default data type completely addressed GH issue 4196
235  if ((_lastQuoteBarUpdate != data.EndTime || _lastOHLCUpdate != data.EndTime) && isDefaultDataType)
236  {
237  _lastOHLCUpdate = data.EndTime;
238  if (bar.Open != 0) Open = bar.Open;
239  if (bar.High != 0) High = bar.High;
240  if (bar.Low != 0) Low = bar.Low;
241  if (bar.Close != 0)
242  {
243  Price = bar.Close;
244  Close = bar.Close;
245  }
246  }
247 
248  var tradeBar = bar as TradeBar;
249  if (tradeBar != null)
250  {
251  if (tradeBar.Volume != 0) Volume = tradeBar.Volume;
252  }
253 
254  var quoteBar = bar as QuoteBar;
255  if (quoteBar != null)
256  {
257  _lastQuoteBarUpdate = quoteBar.EndTime;
258  if (quoteBar.Ask != null && quoteBar.Ask.Close != 0) AskPrice = quoteBar.Ask.Close;
259  if (quoteBar.Bid != null && quoteBar.Bid.Close != 0) BidPrice = quoteBar.Bid.Close;
260  if (quoteBar.LastBidSize != 0) BidSize = quoteBar.LastBidSize;
261  if (quoteBar.LastAskSize != 0) AskSize = quoteBar.LastAskSize;
262  }
263  }
264  else if (data.DataType != MarketDataType.Auxiliary)
265  {
266  if (data.DataType != MarketDataType.Base || data.Price != 0)
267  {
268  Price = data.Price;
269  }
270  }
271  }
272 
273  /// <summary>
274  /// Stores the specified data list in the cache WITHOUT updating any of the cache properties, such as Price
275  /// </summary>
276  /// <param name="data">The collection of data to store in this cache</param>
277  /// <param name="dataType">The data type</param>
278  public void StoreData(IReadOnlyList<BaseData> data, Type dataType)
279  {
280  if (dataType == typeof(Tick))
281  {
282  var tick = data[data.Count - 1] as Tick;
283  switch (tick?.TickType)
284  {
285  case TickType.Trade:
286  _lastTickTrades = data;
287  return;
288  case TickType.Quote:
289  _lastTickQuotes = data;
290  return;
291  }
292  }
293 
294  lock (_locker)
295  {
296  _dataByType ??= new();
297  _dataByType[dataType] = data;
298  }
299  }
300 
301  /// <summary>
302  /// Get last data packet received for this security if any else null
303  /// </summary>
304  /// <returns>BaseData type of the security</returns>
305  public BaseData GetData()
306  {
307  return _lastData;
308  }
309 
310  /// <summary>
311  /// Get last data packet received for this security of the specified type
312  /// </summary>
313  /// <typeparam name="T">The data type</typeparam>
314  /// <returns>The last data packet, null if none received of type</returns>
315  public T GetData<T>()
316  where T : BaseData
317  {
318  IReadOnlyList<BaseData> list;
319  if (!TryGetValue(typeof(T), out list) || list.Count == 0)
320  {
321  return default(T);
322  }
323  return list[list.Count - 1] as T;
324  }
325 
326  /// <summary>
327  /// Gets all data points of the specified type from the most recent time step
328  /// that produced data for that type
329  /// </summary>
330  public IEnumerable<T> GetAll<T>()
331  {
332  if (typeof(T) == typeof(Tick))
333  {
334  return _lastTickTrades.Concat(_lastTickQuotes).Cast<T>();
335  }
336 
337  lock (_locker)
338  {
339  if (_dataByType == null || !_dataByType.TryGetValue(typeof(T), out var list))
340  {
341  return new List<T>();
342  }
343 
344  return list.Cast<T>();
345  }
346  }
347 
348  /// <summary>
349  /// Reset cache storage and free memory
350  /// </summary>
351  public void Reset()
352  {
353  Price = 0;
354 
355  Open = 0;
356  High = 0;
357  Low = 0;
358  Close = 0;
359 
360  BidPrice = 0;
361  BidSize = 0;
362  AskPrice = 0;
363  AskSize = 0;
364 
365  Volume = 0;
366  OpenInterest = 0;
367 
368  _dataByType = null;
369  _lastTickQuotes = _empty;
370  _lastTickTrades = _empty;
371  }
372 
373  /// <summary>
374  /// Gets whether or not this dynamic data instance has data stored for the specified type
375  /// </summary>
376  public bool HasData(Type type)
377  {
378  return TryGetValue(type, out _);
379  }
380 
381  /// <summary>
382  /// Gets whether or not this dynamic data instance has data stored for the specified type
383  /// </summary>
384  public bool TryGetValue(Type type, out IReadOnlyList<BaseData> data)
385  {
386  if (type == typeof(Fundamentals))
387  {
388  // for backwards compatibility
389  type = typeof(FundamentalUniverse);
390  }
391  else if (type == typeof(ETFConstituentData))
392  {
393  // for backwards compatibility
394  type = typeof(ETFConstituentUniverse);
395  }
396  else if (type == typeof(Tick))
397  {
398  var quote = _lastTickQuotes.LastOrDefault();
399  var trade = _lastTickTrades.LastOrDefault();
400  var isQuoteDefaultDataType = quote != null && SubscriptionManager.IsDefaultDataType(quote);
401  var isTradeDefaultDataType = trade != null && SubscriptionManager.IsDefaultDataType(trade);
402 
403  // Currently, IsDefaultDataType returns true for both cases,
404  // So we will return the list with the tick with the most recent timestamp
405  if (isQuoteDefaultDataType && isTradeDefaultDataType)
406  {
407  data = quote.EndTime > trade.EndTime ? _lastTickQuotes : _lastTickTrades;
408  return true;
409  }
410 
411  data = isQuoteDefaultDataType ? _lastTickQuotes : _lastTickTrades;
412  return data?.Count > 0;
413  }
414 
415  data = default;
416  return _dataByType != null && _dataByType.TryGetValue(type, out data);
417  }
418 
419  [MethodImpl(MethodImplOptions.AggressiveInlining)]
420  private void StoreDataPoint(BaseData data)
421  {
422  if (data.GetType() == typeof(Tick))
423  {
424  var tick = data as Tick;
425  switch (tick?.TickType)
426  {
427  case TickType.Trade:
428  _lastTickTrades = new List<BaseData> { tick };
429  break;
430  case TickType.Quote:
431  _lastTickQuotes = new List<BaseData> { tick };
432  break;
433  }
434  }
435  else
436  {
437  lock (_locker)
438  {
439  _dataByType ??= new();
440  // Always keep track of the last observation
441  IReadOnlyList<BaseData> list;
442  if (!_dataByType.TryGetValue(data.GetType(), out list))
443  {
444  list = new List<BaseData> { data };
445  _dataByType[data.GetType()] = list;
446  }
447  else
448  {
449  // we KNOW this one is actually a list, so this is safe
450  // we overwrite the zero entry so we're not constantly newing up lists
451  ((List<BaseData>)list)[0] = data;
452  }
453  }
454  }
455  }
456 
457  /// <summary>
458  /// Helper method that modifies the target security cache instance to use the
459  /// type cache of the source
460  /// </summary>
461  /// <remarks>Will set in the source cache any data already present in the target cache</remarks>
462  /// <remarks>This is useful for custom data securities which also have an underlying security,
463  /// will allow both securities to access the same data by type</remarks>
464  /// <param name="sourceToShare">The source cache to use</param>
465  /// <param name="targetToModify">The target security cache that will be modified</param>
466  public static void ShareTypeCacheInstance(SecurityCache sourceToShare, SecurityCache targetToModify)
467  {
468  sourceToShare._dataByType ??= new();
469  if (targetToModify._dataByType != null)
470  {
471  lock (targetToModify._locker)
472  {
473  lock (sourceToShare._locker)
474  {
475  foreach (var kvp in targetToModify._dataByType)
476  {
477  sourceToShare._dataByType.TryAdd(kvp.Key, kvp.Value);
478  }
479  }
480  }
481  }
482  targetToModify._dataByType = sourceToShare._dataByType;
483  targetToModify._lastTickTrades = sourceToShare._lastTickTrades;
484  targetToModify._lastTickQuotes = sourceToShare._lastTickQuotes;
485  }
486 
487  /// <summary>
488  /// Applies the split to the security cache values
489  /// </summary>
490  internal void ApplySplit(Split split)
491  {
492  Price *= split.SplitFactor;
493  Open *= split.SplitFactor;
494  High *= split.SplitFactor;
495  Low *= split.SplitFactor;
496  Close *= split.SplitFactor;
497  Volume /= split.SplitFactor;
498  BidPrice *= split.SplitFactor;
499  AskPrice *= split.SplitFactor;
500  AskSize /= split.SplitFactor;
501  BidSize /= split.SplitFactor;
502 
503  // Adjust values for the last data we have cached
504  Action<BaseData> scale = data => data.Scale((target, factor, _) => target * factor, 1 / split.SplitFactor, split.SplitFactor, decimal.Zero);
505  _dataByType?.Values.DoForEach(x => x.DoForEach(scale));
506  _lastTickQuotes.DoForEach(scale);
507  _lastTickTrades.DoForEach(scale);
508  }
509  }
510 }