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 CMEProductExpirationReplace =
"{{PRODUCT_EXPIRATION}}";
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?_=";
53 private const int MaxDownloadAttempts = 5;
62 ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
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));
77 : base(dataCacheProvider, mapFileProvider)
91 HashSet<Symbol> result =
null;
94 result = base.GetOptionContractList(symbol, date).ToHashSet();
104 if (date.Date >= DateTime.UtcNow.Date.AddDays(-5) || result.Count == 0)
106 var underlyingSymbol = symbol;
115 var expectedOptionTicker = underlyingSymbol.
Value;
116 if (underlyingSymbol.SecurityType ==
SecurityType.Index)
118 expectedOptionTicker = symbol.
ID.
Symbol;
122 foreach (var optionSymbol
in GetEquityIndexOptionContractList(underlyingSymbol, expectedOptionTicker).Where(symbol => !
IsContractExpired(symbol, date)))
124 result.Add(optionSymbol);
127 else if (underlyingSymbol.SecurityType ==
SecurityType.Future)
130 foreach (var optionSymbol
in GetFutureOptionContractList(underlyingSymbol, date).Where(symbol => !
IsContractExpired(symbol, date)))
132 result.Add(optionSymbol);
137 throw new ArgumentException(
"Option Underlying SecurityType is not supported. Supported types are: Equity, Index, Future");
141 foreach (var optionSymbol
in result)
143 yield
return optionSymbol;
147 private IEnumerable<Symbol> GetFutureOptionContractList(
Symbol futureContractSymbol, DateTime date)
149 var symbols =
new List<Symbol>();
154 _cmeRateGate ??=
new RateGate(1, TimeSpan.FromSeconds(0.5));
156 while (++retries <= maxRetries)
162 var productResponse = _client.GetAsync(CMEProductSlateURL.Replace(CMESymbolReplace, futureContractSymbol.
ID.
Symbol))
163 .SynchronouslyAwaitTaskResult();
165 productResponse.EnsureSuccessStatusCode();
169 .SynchronouslyAwaitTaskResult());
171 productResponse.Dispose();
174 var futureProductId = productResults.
Products.Where(p => p.Globex == futureContractSymbol.
ID.
Symbol && p.GlobexTraded && p.Cleared ==
"Futures")
179 var optionsTradesAndExpiries = CMEOptionsTradeDateAndExpirations.Replace(CMEProductCodeReplace, futureProductId.ToStringInvariant());
183 var optionsTradesAndExpiriesResponse = _client.GetAsync(optionsTradesAndExpiries).SynchronouslyAwaitTaskResult();
184 optionsTradesAndExpiriesResponse.EnsureSuccessStatusCode();
186 var tradesAndExpiriesResponse = JsonConvert.DeserializeObject<List<CMEOptionsTradeDatesAndExpiration>>(optionsTradesAndExpiriesResponse.Content
188 .SynchronouslyAwaitTaskResult());
190 optionsTradesAndExpiriesResponse.Dispose();
193 var selectedOption = tradesAndExpiriesResponse
194 .FirstOrDefault(x => !x.Daily && !x.Weekly && !x.Sto && x.OptionType ==
"AME");
196 if (selectedOption ==
null)
198 Log.
Error($
"LiveOptionChainProvider.GetFutureOptionContractList(): Found no matching future options for contract {futureContractSymbol}");
206 var futureContractExpiration = selectedOption.Expirations
207 .Select(x =>
new KeyValuePair<CMEOptionsExpiration, DateTime>(x, expiryFunction(
new DateTime(x.Expiration.Year, x.Expiration.Month, 1))))
208 .FirstOrDefault(x => x.Value.Year == futureContractSymbol.
ID.
Date.Year && x.Value.Month == futureContractSymbol.
ID.
Date.Month)
211 if (futureContractExpiration ==
null)
213 Log.
Error($
"LiveOptionChainProvider.GetFutureOptionContractList(): Found no future options with matching expiry year and month for contract {futureContractSymbol}");
217 var futureContractMonthCode = futureContractExpiration.Expiration.Code;
222 var optionChainQuotesResponseResult = _client.GetAsync(CMEOptionChainQuotesURL
223 .Replace(CMEProductCodeReplace, selectedOption.ProductId.ToStringInvariant())
224 .Replace(CMEProductExpirationReplace, futureContractMonthCode)
225 + Math.Floor((DateTime.UtcNow - _epoch).TotalMilliseconds).ToStringInvariant());
227 optionChainQuotesResponseResult.Result.EnsureSuccessStatusCode();
229 var futureOptionChain = JsonConvert.DeserializeObject<
CMEOptionChainQuotes>(optionChainQuotesResponseResult.Result.Content
231 .SynchronouslyAwaitTaskResult())
233 .DistinctBy(s => s.StrikePrice)
236 optionChainQuotesResponseResult.Dispose();
242 futureContractSymbol,
249 foreach (var optionChainEntry
in futureOptionChain)
252 var scaledStrikePrice = optionChainEntry.StrikePrice / optionStrikePriceScaleFactor;
256 futureContractSymbol,
261 futureOptionExpiry));
264 futureContractSymbol,
269 futureOptionExpiry));
274 catch (HttpRequestException err)
276 if (retries != maxRetries)
278 Log.
Error(err, $
"Failed to retrieve futures options chain from CME, retrying ({retries} / {maxRetries})");
282 Log.
Error(err, $
"Failed to retrieve futures options chain from CME, returning empty result ({retries} / {retries})");
286 foreach (var symbol
in symbols)
298 private static IEnumerable<Symbol> GetEquityIndexOptionContractList(Symbol symbol,
string expectedOptionTicker)
301 IEnumerable<Symbol> contracts;
307 Log.
Trace($
"LiveOptionChainProvider.GetOptionContractList(): Fetching option chain for option {expectedOptionTicker} underlying {symbol.Value} [Attempt {attempt}]");
309 contracts = FindOptionContracts(symbol, expectedOptionTicker);
312 catch (WebException exception)
316 if (++attempt > MaxDownloadAttempts)
331 private static IEnumerable<Symbol> FindOptionContracts(Symbol underlyingSymbol,
string expectedOptionTicker)
333 var symbols =
new List<Symbol>();
336 var url =
"https://www.quantconnect.com/api/v2/theocc/series-search?symbolType=U&symbol=" + underlyingSymbol.Value;
339 var fileContent = _client.DownloadData(url);
342 var lines = fileContent.Split(
new[] {
"\r\n" }, StringSplitOptions.None).Skip(7);
348 expectedOptionTicker = expectedOptionTicker.LazyToUpper();
350 var optionStyle = underlyingSymbol.SecurityType.DefaultOptionStyle();
353 foreach (var line
in lines)
355 var fields = line.Split(
'\t');
357 var ticker = fields[0].Trim();
358 if (ticker != expectedOptionTicker)
364 var expiryDate =
new DateTime(fields[2].ToInt32(), fields[3].ToInt32(), fields[4].ToInt32());
365 var strike = (fields[5] +
"." + fields[6]).ToDecimal();
367 foreach (var right
in fields[7].Trim().Split(
' '))
371 if (right.Equals(
"C", StringComparison.OrdinalIgnoreCase))
375 else if (right.Equals(
"P", StringComparison.OrdinalIgnoreCase))
380 if (targetRight.HasValue)
382 symbols.Add(Symbol.CreateOption(
384 expectedOptionTicker,
385 underlyingSymbol.ID.Market,