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