Lean  $LEAN_TAG$
BacktestingChainProvider.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 QuantConnect.Util;
18 using QuantConnect.Logging;
21 using System.Collections.Generic;
22 using System.Linq;
24 using QuantConnect.Data;
25 
27 {
28  /// <summary>
29  /// Base backtesting cache provider which will source symbols from local zip files
30  /// </summary>
31  public abstract class BacktestingChainProvider
32  {
33  // see https://github.com/QuantConnect/Lean/issues/6384
34  private static readonly TickType[] DataTypes = new[] { TickType.Quote, TickType.OpenInterest, TickType.Trade };
35  private static readonly Resolution[] Resolutions = new[] { Resolution.Minute, Resolution.Hour, Resolution.Daily };
36  private bool _loggedPreviousTradableDate;
37 
38  /// <summary>
39  /// The data cache instance to use
40  /// </summary>
42 
43  /// <summary>
44  /// Creates a new instance
45  /// </summary>
46  protected BacktestingChainProvider(IDataCacheProvider dataCacheProvider)
47  {
48  DataCacheProvider = dataCacheProvider;
49  }
50 
51  /// <summary>
52  /// Get the contract symbols associated with the given canonical symbol and date
53  /// </summary>
54  /// <param name="canonicalSymbol">The canonical symbol</param>
55  /// <param name="date">The date to search for</param>
56  protected IEnumerable<Symbol> GetSymbols(Symbol canonicalSymbol, DateTime date)
57  {
58  // TODO: This will be removed when all chains (including Futures and FOPs) are file-based instead of zip-entry based
59  if (canonicalSymbol.SecurityType == SecurityType.Option || canonicalSymbol.SecurityType == SecurityType.IndexOption)
60  {
61  return GetOptionSymbols(canonicalSymbol, date);
62  }
63 
64  IEnumerable<string> entries = null;
65  var usedResolution = Resolution.Minute;
66  foreach (var resolution in Resolutions)
67  {
68  usedResolution = resolution;
69  entries = GetZipEntries(canonicalSymbol, date, usedResolution);
70  if (entries != null)
71  {
72  break;
73  }
74  }
75 
76  if (entries == null)
77  {
79  if (mhdb.TryGetEntry(canonicalSymbol.ID.Market, canonicalSymbol, canonicalSymbol.SecurityType, out var entry) && !entry.ExchangeHours.IsDateOpen(date))
80  {
81  if (!_loggedPreviousTradableDate)
82  {
83  _loggedPreviousTradableDate = true;
84  Log.Trace($"BacktestingCacheProvider.GetSymbols(): {date} is not a tradable date for {canonicalSymbol}. When requesting contracts" +
85  $" for non tradable dates, will return contracts of previous tradable date.");
86  }
87 
88  // be user friendly, will return contracts from the previous tradable date
89  return GetSymbols(canonicalSymbol, Time.GetStartTimeForTradeBars(entry.ExchangeHours, date, Time.OneDay, 1, false, entry.DataTimeZone, dailyPreciseEndTime: false));
90  }
91 
93  {
94  Log.Debug($"BacktestingCacheProvider.GetSymbols(): found no source of contracts for {canonicalSymbol} for date {date.ToString(DateFormat.EightCharacter)} for any tick type");
95  }
96 
97  return Enumerable.Empty<Symbol>();
98  }
99 
100  // generate and return the contract symbol for each zip entry
101  return entries
102  .Select(zipEntryName => LeanData.ReadSymbolFromZipEntry(canonicalSymbol, usedResolution, zipEntryName))
103  .Where(symbol => !IsContractExpired(symbol, date));
104  }
105 
106  private IEnumerable<Symbol> GetOptionSymbols(Symbol canonicalSymbol, DateTime date)
107  {
108  var historyProvider = Composer.Instance.GetPart<IHistoryProvider>();
109  var marketHoursDataBase = MarketHoursDatabase.FromDataFolder();
110  var optionUniverseType = typeof(OptionUniverse);
111  // Use this GetEntry extension method since it's data type dependent, so we get the correct entry for the option universe
112  var marketHoursEntry = marketHoursDataBase.GetEntry(canonicalSymbol, new[] { optionUniverseType });
113 
114  var previousTradingDate = Time.GetStartTimeForTradeBars(marketHoursEntry.ExchangeHours, date, Time.OneDay, 1,
115  extendedMarketHours: false, marketHoursEntry.DataTimeZone);
116  var request = new HistoryRequest(
117  previousTradingDate.ConvertToUtc(marketHoursEntry.ExchangeHours.TimeZone),
118  date.ConvertToUtc(marketHoursEntry.ExchangeHours.TimeZone),
119  optionUniverseType,
120  canonicalSymbol,
121  Resolution.Daily,
122  marketHoursEntry.ExchangeHours,
123  marketHoursEntry.DataTimeZone,
124  Resolution.Daily,
125  false,
126  false,
128  TickType.Quote);
129  var history = historyProvider.GetHistory(new[] { request }, marketHoursEntry.DataTimeZone).ToList();
130 
131  if (history == null || history.Count == 0)
132  {
133  return Enumerable.Empty<Symbol>();
134  }
135 
136  return history.GetUniverseData().SelectMany(x => x.Values.Single().Where(x => x.Symbol.SecurityType.IsOption())).Select(x => x.Symbol);
137  }
138 
139  /// <summary>
140  /// Helper method to determine if a contract is expired for the requested date
141  /// </summary>
142  protected static bool IsContractExpired(Symbol symbol, DateTime date)
143  {
144  return symbol.ID.Date.Date < date.Date;
145  }
146 
147  private IEnumerable<string> GetZipEntries(Symbol canonicalSymbol, DateTime date, Resolution resolution)
148  {
149  foreach (var tickType in DataTypes)
150  {
151  // build the zip file name and fetch it with our provider
152  var zipFileName = LeanData.GenerateZipFilePath(Globals.DataFolder, canonicalSymbol, date, resolution, tickType);
153  try
154  {
155  return DataCacheProvider.GetZipEntries(zipFileName);
156  }
157  catch
158  {
159  // the cache provider will throw if the file isn't available TODO: it's api should be more like TryGetZipEntries
160  }
161  }
162 
163  return null;
164  }
165  }
166 }