Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 27 additions & 18 deletions Common/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,35 +206,44 @@ public static List<string> DeserializeList(this string jsonArray)
/// <summary>
/// Helper method to download a provided url as a string
/// </summary>
/// <param name="client">The http client to use</param>
/// <param name="url">The url to download data from</param>
/// <param name="headers">Add custom headers for the request</param>
public static string DownloadData(this string url, Dictionary<string, string> headers = null)
public static string DownloadData(this HttpClient client, string url, Dictionary<string, string> headers = null)
{
using (var client = new HttpClient())
if (headers != null)
{
if (headers != null)
foreach (var kvp in headers)
{
foreach (var kvp in headers)
{
client.DefaultRequestHeaders.Add(kvp.Key, kvp.Value);
}
client.DefaultRequestHeaders.Add(kvp.Key, kvp.Value);
}
try
}
try
{
using (var response = client.GetAsync(url).Result)
{
using (var response = client.GetAsync(url).Result)
using (var content = response.Content)
{
using (var content = response.Content)
{
return content.ReadAsStringAsync().Result;
}
return content.ReadAsStringAsync().Result;
}
}
catch (WebException ex)
{
Log.Error(ex, $"DownloadData(): {Messages.Extensions.DownloadDataFailed(url)}");
return null;
}
}
catch (WebException ex)
{
Log.Error(ex, $"DownloadData(): {Messages.Extensions.DownloadDataFailed(url)}");
return null;
}
}

/// <summary>
/// Helper method to download a provided url as a string
/// </summary>
/// <param name="url">The url to download data from</param>
/// <param name="headers">Add custom headers for the request</param>
public static string DownloadData(this string url, Dictionary<string, string> headers = null)
{
using var client = new HttpClient();
return client.DownloadData(url, headers);
}

/// <summary>
Expand Down
41 changes: 22 additions & 19 deletions Engine/DataFeeds/LiveOptionChainProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class LiveOptionChainProvider : BacktestingOptionChainProvider
private static readonly HttpClient _client;
private static readonly DateTime _epoch = new DateTime(1970, 1, 1);

private static readonly RateGate _rateGate = new RateGate(1, TimeSpan.FromSeconds(0.5));
private static RateGate _cmeRateGate;

private const string CMESymbolReplace = "{{SYMBOL}}";
private const string CMEProductCodeReplace = "{{PRODUCT_CODE}}";
Expand Down Expand Up @@ -81,30 +81,25 @@ public LiveOptionChainProvider(IDataCacheProvider dataCacheProvider, IMapFilePro
/// </summary>
/// <param name="symbol">The option or the underlying symbol to get the option chain for.
/// Providing the option allows targetting an option ticker different than the default e.g. SPXW</param>
/// <param name="date">Unused</param>
/// <param name="date">The date to ask for the option contract list for</param>
/// <returns>Option chain</returns>
/// <exception cref="ArgumentException">Option underlying Symbol is not Future or Equity</exception>
public override IEnumerable<Symbol> GetOptionContractList(Symbol symbol, DateTime date)
{
var result = Enumerable.Empty<Symbol>();
HashSet<Symbol> result = null;
try
{
result = base.GetOptionContractList(symbol, date);
result = base.GetOptionContractList(symbol, date).ToHashSet();
}
catch (Exception ex)
{
result = new();
// this shouldn't happen but just in case let's log it
Log.Error(ex);
}

bool yielded = false;
foreach (var optionSymbol in result)
{
yielded = true;
yield return optionSymbol;
}

if (!yielded)
// 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
if (date.Date >= DateTime.UtcNow.Date.AddDays(-5) || result.Count == 0)
{
var underlyingSymbol = symbol;
if (symbol.SecurityType.IsOption())
Expand All @@ -116,30 +111,35 @@ public override IEnumerable<Symbol> GetOptionContractList(Symbol symbol, DateTim
if (underlyingSymbol.SecurityType == SecurityType.Equity || underlyingSymbol.SecurityType == SecurityType.Index)
{
var expectedOptionTicker = underlyingSymbol.Value;
if(underlyingSymbol.SecurityType == SecurityType.Index)
if (underlyingSymbol.SecurityType == SecurityType.Index)
{
expectedOptionTicker = symbol.ID.Symbol;
}

// Source data from TheOCC if we're trading equity or index options
foreach (var optionSymbol in GetEquityIndexOptionContractList(underlyingSymbol, expectedOptionTicker).Where(symbol => !IsContractExpired(symbol, date)))
{
yield return optionSymbol;
result.Add(optionSymbol);
}
}
else if (underlyingSymbol.SecurityType == SecurityType.Future)
{
// We get our data from CME if we're trading future options
foreach (var optionSymbol in GetFutureOptionContractList(underlyingSymbol, date).Where(symbol => !IsContractExpired(symbol, date)))
{
yield return optionSymbol;
result.Add(optionSymbol);
}
}
else
{
throw new ArgumentException("Option Underlying SecurityType is not supported. Supported types are: Equity, Index, Future");
}
}

foreach (var optionSymbol in result)
{
yield return optionSymbol;
}
}

private IEnumerable<Symbol> GetFutureOptionContractList(Symbol futureContractSymbol, DateTime date)
Expand All @@ -148,11 +148,14 @@ private IEnumerable<Symbol> GetFutureOptionContractList(Symbol futureContractSym
var retries = 0;
var maxRetries = 5;

// rate gate will start a timer in the background, so let's avoid it we if don't need it
_cmeRateGate ??= new RateGate(1, TimeSpan.FromSeconds(0.5));

while (++retries <= maxRetries)
{
try
{
_rateGate.WaitToProceed();
_cmeRateGate.WaitToProceed();

var productResponse = _client.GetAsync(CMEProductSlateURL.Replace(CMESymbolReplace, futureContractSymbol.ID.Symbol))
.SynchronouslyAwaitTaskResult();
Expand All @@ -173,7 +176,7 @@ private IEnumerable<Symbol> GetFutureOptionContractList(Symbol futureContractSym

var optionsTradesAndExpiries = CMEOptionsTradeDateAndExpirations.Replace(CMEProductCodeReplace, futureProductId.ToStringInvariant());

_rateGate.WaitToProceed();
_cmeRateGate.WaitToProceed();

var optionsTradesAndExpiriesResponse = _client.GetAsync(optionsTradesAndExpiries).SynchronouslyAwaitTaskResult();
optionsTradesAndExpiriesResponse.EnsureSuccessStatusCode();
Expand Down Expand Up @@ -211,7 +214,7 @@ private IEnumerable<Symbol> GetFutureOptionContractList(Symbol futureContractSym

var futureContractMonthCode = futureContractExpiration.Expiration.Code;

_rateGate.WaitToProceed();
_cmeRateGate.WaitToProceed();

// Subtract one day from now for settlement API since settlement may not be available for today yet
var optionChainQuotesResponseResult = _client.GetAsync(CMEOptionChainQuotesURL
Expand Down Expand Up @@ -331,7 +334,7 @@ private static IEnumerable<Symbol> FindOptionContracts(Symbol underlyingSymbol,
var url = "https://www.quantconnect.com/api/v2/theocc/series-search?symbolType=U&symbol=" + underlyingSymbol.Value;

// download the text file
var fileContent = url.DownloadData();
var fileContent = _client.DownloadData(url);

// read the lines, skipping the headers
var lines = fileContent.Split(new[] { "\r\n" }, StringSplitOptions.None).Skip(7);
Expand Down