Lean  $LEAN_TAG$
LiveOptionChainProvider.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.Net;
18 using System.Linq;
19 using System.Net.Http;
20 using Newtonsoft.Json;
21 using System.Threading;
22 using QuantConnect.Util;
23 using QuantConnect.Logging;
26 using System.Collections.Generic;
30 using System.Net.Http.Headers;
31 
33 {
34  /// <summary>
35  /// An implementation of <see cref="IOptionChainProvider"/> that fetches the list of contracts
36  /// from the Options Clearing Corporation (OCC) website
37  /// </summary>
39  {
40  private static readonly HttpClient _client;
41  private static readonly DateTime _epoch = new DateTime(1970, 1, 1);
42 
43  private static RateGate _cmeRateGate;
44 
45  private const string CMESymbolReplace = "{{SYMBOL}}";
46  private const string CMEProductCodeReplace = "{{PRODUCT_CODE}}";
47  private const string CMEContractCodeReplace = "{{CONTRACT_CODE}}";
48  private const string CMEProductExpirationReplace = "{{PRODUCT_EXPIRATION}}";
49  private const string CMEDateTimeReplace = "{{DT_REPLACE}}";
50 
51  private const string CMEProductSlateURL = "https://www.cmegroup.com/CmeWS/mvc/ProductSlate/V2/List?pageNumber=1&sortAsc=false&sortField=rank&searchString=" + CMESymbolReplace + "&pageSize=5";
52  private const string CMEOptionsTradeDateAndExpirations = "https://www.cmegroup.com/CmeWS/mvc/Settlements/Options/TradeDateAndExpirations/" + CMEProductCodeReplace;
53  private const string CMEOptionChainQuotesURL = "https://www.cmegroup.com/CmeWS/mvc/Quotes/Option/" + CMEProductCodeReplace + "/G/" + CMEProductExpirationReplace + "/ALL?_=";
54 
55  private const int MaxDownloadAttempts = 5;
56 
57  /// <summary>
58  /// Static constructor for the <see cref="LiveOptionChainProvider"/> class
59  /// </summary>
61  {
62  // The OCC website now requires at least TLS 1.1 for API requests.
63  // NET 4.5.2 and below does not enable these more secure protocols by default, so we add them in here
64  ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
65 
66  _client = new HttpClient(new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate });
67  _client.DefaultRequestHeaders.Connection.Add("keep-alive");
68  _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*", 0.8));
69  _client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0");
70  _client.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("en-US", 0.5));
71  }
72 
73  /// <summary>
74  /// Creates a new instance
75  /// </summary>
76  /// <param name="dataCacheProvider">The data cache provider instance to use</param>
77  /// <param name="mapFileProvider">The map file provider instance to use</param>
78  public LiveOptionChainProvider(IDataCacheProvider dataCacheProvider, IMapFileProvider mapFileProvider)
79  : base(dataCacheProvider, mapFileProvider)
80  {
81  }
82 
83  /// <summary>
84  /// Gets the option chain associated with the underlying Symbol
85  /// </summary>
86  /// <param name="symbol">The option or the underlying symbol to get the option chain for.
87  /// Providing the option allows targetting an option ticker different than the default e.g. SPXW</param>
88  /// <param name="date">The date to ask for the option contract list for</param>
89  /// <returns>Option chain</returns>
90  /// <exception cref="ArgumentException">Option underlying Symbol is not Future or Equity</exception>
91  public override IEnumerable<Symbol> GetOptionContractList(Symbol symbol, DateTime date)
92  {
93  HashSet<Symbol> result = null;
94  try
95  {
96  result = base.GetOptionContractList(symbol, date).ToHashSet();
97  }
98  catch (Exception ex)
99  {
100  result = new();
101  // this shouldn't happen but just in case let's log it
102  Log.Error(ex);
103  }
104 
105  // during warmup we rely on the backtesting provider, but as we get closer to current time let's join the data with our live chain sources
106  if (date.Date >= DateTime.UtcNow.Date.AddDays(-5) || result.Count == 0)
107  {
108  var underlyingSymbol = symbol;
109  if (symbol.SecurityType.IsOption())
110  {
111  // we were given the option
112  underlyingSymbol = symbol.Underlying;
113  }
114 
115  if (underlyingSymbol.SecurityType == SecurityType.Equity || underlyingSymbol.SecurityType == SecurityType.Index)
116  {
117  var expectedOptionTicker = underlyingSymbol.Value;
118  if (underlyingSymbol.SecurityType == SecurityType.Index)
119  {
120  expectedOptionTicker = symbol.ID.Symbol;
121  }
122 
123  // Source data from TheOCC if we're trading equity or index options
124  foreach (var optionSymbol in GetEquityIndexOptionContractList(underlyingSymbol, expectedOptionTicker).Where(symbol => !IsContractExpired(symbol, date)))
125  {
126  result.Add(optionSymbol);
127  }
128  }
129  else if (underlyingSymbol.SecurityType == SecurityType.Future)
130  {
131  // We get our data from CME if we're trading future options
132  foreach (var optionSymbol in GetFutureOptionContractList(underlyingSymbol, date).Where(symbol => !IsContractExpired(symbol, date)))
133  {
134  result.Add(optionSymbol);
135  }
136  }
137  else
138  {
139  throw new ArgumentException("Option Underlying SecurityType is not supported. Supported types are: Equity, Index, Future");
140  }
141  }
142 
143  foreach (var optionSymbol in result)
144  {
145  yield return optionSymbol;
146  }
147  }
148 
149  private IEnumerable<Symbol> GetFutureOptionContractList(Symbol futureContractSymbol, DateTime date)
150  {
151  var symbols = new List<Symbol>();
152  var retries = 0;
153  var maxRetries = 5;
154 
155  // rate gate will start a timer in the background, so let's avoid it we if don't need it
156  _cmeRateGate ??= new RateGate(1, TimeSpan.FromSeconds(0.5));
157 
158  while (++retries <= maxRetries)
159  {
160  try
161  {
162  _cmeRateGate.WaitToProceed();
163 
164  var productResponse = _client.GetAsync(CMEProductSlateURL.Replace(CMESymbolReplace, futureContractSymbol.ID.Symbol))
165  .SynchronouslyAwaitTaskResult();
166 
167  productResponse.EnsureSuccessStatusCode();
168 
169  var productResults = JsonConvert.DeserializeObject<CMEProductSlateV2ListResponse>(productResponse.Content
170  .ReadAsStringAsync()
171  .SynchronouslyAwaitTaskResult());
172 
173  productResponse.Dispose();
174 
175  // We want to gather the future product to get the future options ID
176  var futureProductId = productResults.Products.Where(p => p.Globex == futureContractSymbol.ID.Symbol && p.GlobexTraded && p.Cleared == "Futures")
177  .Select(p => p.Id)
178  .Single();
179 
180 
181  var optionsTradesAndExpiries = CMEOptionsTradeDateAndExpirations.Replace(CMEProductCodeReplace, futureProductId.ToStringInvariant());
182 
183  _cmeRateGate.WaitToProceed();
184 
185  var optionsTradesAndExpiriesResponse = _client.GetAsync(optionsTradesAndExpiries).SynchronouslyAwaitTaskResult();
186  optionsTradesAndExpiriesResponse.EnsureSuccessStatusCode();
187 
188  var tradesAndExpiriesResponse = JsonConvert.DeserializeObject<List<CMEOptionsTradeDatesAndExpiration>>(optionsTradesAndExpiriesResponse.Content
189  .ReadAsStringAsync()
190  .SynchronouslyAwaitTaskResult());
191 
192  optionsTradesAndExpiriesResponse.Dispose();
193 
194  // For now, only support American options on CME
195  var selectedOption = tradesAndExpiriesResponse
196  .FirstOrDefault(x => !x.Daily && !x.Weekly && !x.Sto && x.OptionType == "AME");
197 
198  if (selectedOption == null)
199  {
200  Log.Error($"LiveOptionChainProvider.GetFutureOptionContractList(): Found no matching future options for contract {futureContractSymbol}");
201  yield break;
202  }
203 
204  // Gather the month code and the year's last number to query the next API, which expects an expiration as `<MONTH_CODE><YEAR_LAST_NUMBER>`
205  var canonicalFuture = Symbol.Create(futureContractSymbol.ID.Symbol, SecurityType.Future, futureContractSymbol.ID.Market);
206  var expiryFunction = FuturesExpiryFunctions.FuturesExpiryFunction(canonicalFuture);
207 
208  var futureContractExpiration = selectedOption.Expirations
209  .Select(x => new KeyValuePair<CMEOptionsExpiration, DateTime>(x, expiryFunction(new DateTime(x.Expiration.Year, x.Expiration.Month, 1))))
210  .FirstOrDefault(x => x.Value.Year == futureContractSymbol.ID.Date.Year && x.Value.Month == futureContractSymbol.ID.Date.Month)
211  .Key;
212 
213  if (futureContractExpiration == null)
214  {
215  Log.Error($"LiveOptionChainProvider.GetFutureOptionContractList(): Found no future options with matching expiry year and month for contract {futureContractSymbol}");
216  yield break;
217  }
218 
219  var futureContractMonthCode = futureContractExpiration.Expiration.Code;
220 
221  _cmeRateGate.WaitToProceed();
222 
223  // Subtract one day from now for settlement API since settlement may not be available for today yet
224  var optionChainQuotesResponseResult = _client.GetAsync(CMEOptionChainQuotesURL
225  .Replace(CMEProductCodeReplace, selectedOption.ProductId.ToStringInvariant())
226  .Replace(CMEProductExpirationReplace, futureContractMonthCode)
227  + Math.Floor((DateTime.UtcNow - _epoch).TotalMilliseconds).ToStringInvariant());
228 
229  optionChainQuotesResponseResult.Result.EnsureSuccessStatusCode();
230 
231  var futureOptionChain = JsonConvert.DeserializeObject<CMEOptionChainQuotes>(optionChainQuotesResponseResult.Result.Content
232  .ReadAsStringAsync()
233  .SynchronouslyAwaitTaskResult())
234  .Quotes
235  .DistinctBy(s => s.StrikePrice)
236  .ToList();
237 
238  optionChainQuotesResponseResult.Dispose();
239 
240  // Each CME contract can have arbitrary scaling applied to the strike price, so we normalize it to the
241  // underlying's price via static entries.
242  var optionStrikePriceScaleFactor = CMEStrikePriceScalingFactors.GetScaleFactor(futureContractSymbol);
243  var canonicalOption = Symbol.CreateOption(
244  futureContractSymbol,
245  futureContractSymbol.ID.Market,
246  futureContractSymbol.SecurityType.DefaultOptionStyle(),
247  default(OptionRight),
248  default(decimal),
250 
251  foreach (var optionChainEntry in futureOptionChain)
252  {
253  var futureOptionExpiry = FuturesOptionsExpiryFunctions.GetFutureOptionExpiryFromFutureExpiry(futureContractSymbol, canonicalOption);
254  var scaledStrikePrice = optionChainEntry.StrikePrice / optionStrikePriceScaleFactor;
255 
256  // Calls and puts share the same strike, create two symbols per each to avoid iterating twice.
257  symbols.Add(Symbol.CreateOption(
258  futureContractSymbol,
259  futureContractSymbol.ID.Market,
260  OptionStyle.American,
261  OptionRight.Call,
262  scaledStrikePrice,
263  futureOptionExpiry));
264 
265  symbols.Add(Symbol.CreateOption(
266  futureContractSymbol,
267  futureContractSymbol.ID.Market,
268  OptionStyle.American,
269  OptionRight.Put,
270  scaledStrikePrice,
271  futureOptionExpiry));
272  }
273 
274  break;
275  }
276  catch (HttpRequestException err)
277  {
278  if (retries != maxRetries)
279  {
280  Log.Error(err, $"Failed to retrieve futures options chain from CME, retrying ({retries} / {maxRetries})");
281  continue;
282  }
283 
284  Log.Error(err, $"Failed to retrieve futures options chain from CME, returning empty result ({retries} / {retries})");
285  }
286  }
287 
288  foreach (var symbol in symbols)
289  {
290  yield return symbol;
291  }
292  }
293 
294  /// <summary>
295  /// Gets the list of option contracts for a given underlying equity symbol
296  /// </summary>
297  /// <param name="symbol">The underlying symbol</param>
298  /// <param name="expectedOptionTicker">The expected option ticker</param>
299  /// <returns>The list of option contracts</returns>
300  private static IEnumerable<Symbol> GetEquityIndexOptionContractList(Symbol symbol, string expectedOptionTicker)
301  {
302  var attempt = 1;
303  IEnumerable<Symbol> contracts;
304 
305  while (true)
306  {
307  try
308  {
309  Log.Trace($"LiveOptionChainProvider.GetOptionContractList(): Fetching option chain for option {expectedOptionTicker} underlying {symbol.Value} [Attempt {attempt}]");
310 
311  contracts = FindOptionContracts(symbol, expectedOptionTicker);
312  break;
313  }
314  catch (WebException exception)
315  {
316  Log.Error(exception);
317 
318  if (++attempt > MaxDownloadAttempts)
319  {
320  throw;
321  }
322 
323  Thread.Sleep(1000);
324  }
325  }
326 
327  return contracts;
328  }
329 
330  /// <summary>
331  /// Retrieve the list of option contracts for an underlying symbol from the OCC website
332  /// </summary>
333  private static IEnumerable<Symbol> FindOptionContracts(Symbol underlyingSymbol, string expectedOptionTicker)
334  {
335  var symbols = new List<Symbol>();
336 
337  // use QC url to bypass TLS issues with Mono pre-4.8 version
338  var url = "https://www.quantconnect.com/api/v2/theocc/series-search?symbolType=U&symbol=" + underlyingSymbol.Value;
339 
340  // download the text file
341  var fileContent = _client.DownloadData(url);
342 
343  // read the lines, skipping the headers
344  var lines = fileContent.Split(new[] { "\r\n" }, StringSplitOptions.None).Skip(7);
345 
346  // Example of a line:
347  // SPY 2021 03 26 190 000 C P 0 612 360000000
348 
349  // avoid being sensitive to case
350  expectedOptionTicker = expectedOptionTicker.LazyToUpper();
351 
352  var optionStyle = underlyingSymbol.SecurityType.DefaultOptionStyle();
353 
354  // parse the lines, creating the Lean option symbols
355  foreach (var line in lines)
356  {
357  var fields = line.Split('\t');
358 
359  var ticker = fields[0].Trim();
360  if (ticker != expectedOptionTicker)
361  {
362  // skip undesired options. For example SPX underlying has SPX & SPXW option tickers
363  continue;
364  }
365 
366  var expiryDate = new DateTime(fields[2].ToInt32(), fields[3].ToInt32(), fields[4].ToInt32());
367  var strike = (fields[5] + "." + fields[6]).ToDecimal();
368 
369  foreach (var right in fields[7].Trim().Split(' '))
370  {
371  OptionRight? targetRight = null;
372 
373  if (right.Equals("C", StringComparison.OrdinalIgnoreCase))
374  {
375  targetRight = OptionRight.Call;
376  }
377  else if (right.Equals("P", StringComparison.OrdinalIgnoreCase))
378  {
379  targetRight = OptionRight.Put;
380  }
381 
382  if (targetRight.HasValue)
383  {
384  symbols.Add(Symbol.CreateOption(
385  underlyingSymbol,
386  expectedOptionTicker,
387  underlyingSymbol.ID.Market,
388  optionStyle,
389  targetRight.Value,
390  strike,
391  expiryDate));
392  }
393  }
394  }
395 
396  return symbols;
397  }
398  }
399 }