19 using System.Net.Http;
20 using Newtonsoft.Json;
21 using System.Threading;
26 using System.Collections.Generic;
30 using System.Net.Http.Headers;
40 private static readonly HttpClient _client;
41 private static readonly DateTime _epoch =
new DateTime(1970, 1, 1);
43 private static RateGate _cmeRateGate;
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}}";
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?_=";
55 private const int MaxDownloadAttempts = 5;
64 ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
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));
79 : base(dataCacheProvider, mapFileProvider)
93 HashSet<Symbol> result =
null;
96 result = base.GetOptionContractList(symbol, date).ToHashSet();
106 if (date.Date >= DateTime.UtcNow.Date.AddDays(-5) || result.Count == 0)
108 var underlyingSymbol = symbol;
117 var expectedOptionTicker = underlyingSymbol.
Value;
118 if (underlyingSymbol.SecurityType ==
SecurityType.Index)
120 expectedOptionTicker = symbol.
ID.
Symbol;
124 foreach (var optionSymbol
in GetEquityIndexOptionContractList(underlyingSymbol, expectedOptionTicker).Where(symbol => !
IsContractExpired(symbol, date)))
126 result.Add(optionSymbol);
129 else if (underlyingSymbol.SecurityType ==
SecurityType.Future)
132 foreach (var optionSymbol
in GetFutureOptionContractList(underlyingSymbol, date).Where(symbol => !
IsContractExpired(symbol, date)))
134 result.Add(optionSymbol);
139 throw new ArgumentException(
"Option Underlying SecurityType is not supported. Supported types are: Equity, Index, Future");
143 foreach (var optionSymbol
in result)
145 yield
return optionSymbol;
149 private IEnumerable<Symbol> GetFutureOptionContractList(
Symbol futureContractSymbol, DateTime date)
151 var symbols =
new List<Symbol>();
156 _cmeRateGate ??=
new RateGate(1, TimeSpan.FromSeconds(0.5));
158 while (++retries <= maxRetries)
164 var productResponse = _client.GetAsync(CMEProductSlateURL.Replace(CMESymbolReplace, futureContractSymbol.
ID.
Symbol))
165 .SynchronouslyAwaitTaskResult();
167 productResponse.EnsureSuccessStatusCode();
171 .SynchronouslyAwaitTaskResult());
173 productResponse.Dispose();
176 var futureProductId = productResults.
Products.Where(p => p.Globex == futureContractSymbol.
ID.
Symbol && p.GlobexTraded && p.Cleared ==
"Futures")
181 var optionsTradesAndExpiries = CMEOptionsTradeDateAndExpirations.Replace(CMEProductCodeReplace, futureProductId.ToStringInvariant());
185 var optionsTradesAndExpiriesResponse = _client.GetAsync(optionsTradesAndExpiries).SynchronouslyAwaitTaskResult();
186 optionsTradesAndExpiriesResponse.EnsureSuccessStatusCode();
188 var tradesAndExpiriesResponse = JsonConvert.DeserializeObject<List<CMEOptionsTradeDatesAndExpiration>>(optionsTradesAndExpiriesResponse.Content
190 .SynchronouslyAwaitTaskResult());
192 optionsTradesAndExpiriesResponse.Dispose();
195 var selectedOption = tradesAndExpiriesResponse
196 .FirstOrDefault(x => !x.Daily && !x.Weekly && !x.Sto && x.OptionType ==
"AME");
198 if (selectedOption ==
null)
200 Log.
Error($
"LiveOptionChainProvider.GetFutureOptionContractList(): Found no matching future options for contract {futureContractSymbol}");
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)
213 if (futureContractExpiration ==
null)
215 Log.
Error($
"LiveOptionChainProvider.GetFutureOptionContractList(): Found no future options with matching expiry year and month for contract {futureContractSymbol}");
219 var futureContractMonthCode = futureContractExpiration.Expiration.Code;
224 var optionChainQuotesResponseResult = _client.GetAsync(CMEOptionChainQuotesURL
225 .Replace(CMEProductCodeReplace, selectedOption.ProductId.ToStringInvariant())
226 .Replace(CMEProductExpirationReplace, futureContractMonthCode)
227 + Math.Floor((DateTime.UtcNow - _epoch).TotalMilliseconds).ToStringInvariant());
229 optionChainQuotesResponseResult.Result.EnsureSuccessStatusCode();
231 var futureOptionChain = JsonConvert.DeserializeObject<
CMEOptionChainQuotes>(optionChainQuotesResponseResult.Result.Content
233 .SynchronouslyAwaitTaskResult())
235 .DistinctBy(s => s.StrikePrice)
238 optionChainQuotesResponseResult.Dispose();
244 futureContractSymbol,
251 foreach (var optionChainEntry
in futureOptionChain)
254 var scaledStrikePrice = optionChainEntry.StrikePrice / optionStrikePriceScaleFactor;
258 futureContractSymbol,
263 futureOptionExpiry));
266 futureContractSymbol,
271 futureOptionExpiry));
276 catch (HttpRequestException err)
278 if (retries != maxRetries)
280 Log.
Error(err, $
"Failed to retrieve futures options chain from CME, retrying ({retries} / {maxRetries})");
284 Log.
Error(err, $
"Failed to retrieve futures options chain from CME, returning empty result ({retries} / {retries})");
288 foreach (var symbol
in symbols)
300 private static IEnumerable<Symbol> GetEquityIndexOptionContractList(Symbol symbol,
string expectedOptionTicker)
303 IEnumerable<Symbol> contracts;
309 Log.
Trace($
"LiveOptionChainProvider.GetOptionContractList(): Fetching option chain for option {expectedOptionTicker} underlying {symbol.Value} [Attempt {attempt}]");
311 contracts = FindOptionContracts(symbol, expectedOptionTicker);
314 catch (WebException exception)
318 if (++attempt > MaxDownloadAttempts)
333 private static IEnumerable<Symbol> FindOptionContracts(Symbol underlyingSymbol,
string expectedOptionTicker)
335 var symbols =
new List<Symbol>();
338 var url =
"https://www.quantconnect.com/api/v2/theocc/series-search?symbolType=U&symbol=" + underlyingSymbol.Value;
341 var fileContent = _client.DownloadData(url);
344 var lines = fileContent.Split(
new[] {
"\r\n" }, StringSplitOptions.None).Skip(7);
350 expectedOptionTicker = expectedOptionTicker.LazyToUpper();
352 var optionStyle = underlyingSymbol.SecurityType.DefaultOptionStyle();
355 foreach (var line
in lines)
357 var fields = line.Split(
'\t');
359 var ticker = fields[0].Trim();
360 if (ticker != expectedOptionTicker)
366 var expiryDate =
new DateTime(fields[2].ToInt32(), fields[3].ToInt32(), fields[4].ToInt32());
367 var strike = (fields[5] +
"." + fields[6]).ToDecimal();
369 foreach (var right
in fields[7].Trim().Split(
' '))
373 if (right.Equals(
"C", StringComparison.OrdinalIgnoreCase))
377 else if (right.Equals(
"P", StringComparison.OrdinalIgnoreCase))
382 if (targetRight.HasValue)
384 symbols.Add(Symbol.CreateOption(
386 expectedOptionTicker,
387 underlyingSymbol.ID.Market,