Files
2026-07-13 13:02:50 +08:00

293 lines
14 KiB
C#

/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2026 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Linq;
using System.Threading;
using QuantConnect.Util;
using QuantConnect.Data;
using QuantConnect.Logging;
using System.Threading.Tasks;
using QuantConnect.Interfaces;
using QuantConnect.Configuration;
using System.Collections.Generic;
using System.Collections.Concurrent;
using QuantConnect.Lean.Engine.HistoricalData;
namespace QuantConnect.Lean.Engine.DataFeeds.DataDownloader
{
/// <summary>
/// Decorates an <see cref="IDataDownloader"/> to support canonical symbols by automatically
/// resolving their option or future contract chains and downloading data for each constituent contract.
/// </summary>
public class CanonicalDataDownloaderDecorator : IDataDownloader
{
/// <summary>
/// Prevents multiple warnings being fired when the underlying data downloader doesn't support canonical symbols.
/// </summary>
private bool _firedCanonicalNotSupportedWarning;
/// <summary>
/// Lazily initialized option chain provider for resolving option contract lists.
/// </summary>
private readonly IOptionChainProvider _optionChainProvider;
/// <summary>
/// Lazily initialized future chain provider for resolving future contract lists.
/// </summary>
private readonly IFutureChainProvider _futureChainProvider;
/// <summary>
/// The underlying data downloader that performs the actual data retrieval.
/// </summary>
private readonly IDataDownloader _dataDownloader;
/// <summary>
/// Controls parallelism for concurrent operations,
/// limiting execution to a configurable number of threads (default: 4) on the default task scheduler.
/// </summary>
private readonly ParallelOptions _parallelOptions = new()
{
MaxDegreeOfParallelism = Config.GetInt("downloader-thread-count", 4),
TaskScheduler = TaskScheduler.Default
};
/// <summary>
/// Configurable look-back period for canonical option symbols, used to limit the date range of underlying contract data downloads.
/// </summary>
private static readonly int _optionLookbackYeard = Config.GetInt("options-lookback-years", 1);
/// <summary>
/// Configurable look-back period for canonical future symbols, used to limit the date range of underlying contract data downloads.
/// </summary>
private static readonly int _futureLookbackYeard = Config.GetInt("futures-lookback-years", 2);
/// <summary>
/// Initializes a new instance of the <see cref="CanonicalDataDownloaderDecorator"/> class.
/// </summary>
/// <param name="dataDownloader">The underlying data downloader to decorate with canonical symbol support.</param>
/// <param name="dataProvider">The data provider used for initializing chain providers.</param>
/// <param name="mapFileProvider">The map file provider used for initializing chain providers.</param>
/// <param name="factorFileProvider">The factor file provider used for initializing chain providers.</param>
public CanonicalDataDownloaderDecorator(IDataDownloader dataDownloader, IDataProvider dataProvider, IMapFileProvider mapFileProvider, IFactorFileProvider factorFileProvider)
{
_dataDownloader = dataDownloader;
var historyManager = new HistoryProviderManager();
historyManager.Initialize(
new HistoryProviderInitializeParameters(
job: null,
api: null,
dataProvider,
new SingleEntryDataCacheProvider(dataProvider, isDataEphemeral: true),
mapFileProvider,
factorFileProvider: factorFileProvider, // Probably not needed since canonical data doesn't require factor files
statusUpdateAction: null,
parallelHistoryRequestsEnabled: false,
new DataPermissionManager(),
objectStore: null,
new AlgorithmSettings()));
_optionChainProvider = Composer.Instance.GetPart<IOptionChainProvider>();
if (_optionChainProvider == null)
{
var baseOptionChainProvider = new LiveOptionChainProvider();
baseOptionChainProvider.Initialize(new(mapFileProvider, historyManager));
_optionChainProvider = new CachingOptionChainProvider(baseOptionChainProvider);
Composer.Instance.AddPart(_optionChainProvider);
}
_futureChainProvider = Composer.Instance.GetPart<IFutureChainProvider>();
if (_futureChainProvider == null)
{
var baseFutureChainProvider = new BacktestingFutureChainProvider();
baseFutureChainProvider.Initialize(new(mapFileProvider, historyManager));
_futureChainProvider = new CachingFutureChainProvider(baseFutureChainProvider);
Composer.Instance.AddPart(_futureChainProvider);
}
}
/// <summary>
/// Get historical data enumerable for a single symbol, type and resolution given this start and end time (in UTC).
/// For canonical symbols, automatically resolves and downloads data for all underlying contracts.
/// </summary>
/// <param name="dataDownloaderGetParameters">model class for passing in parameters for historical data</param>
/// <returns>Enumerable of base data for this symbol</returns>
public IEnumerable<BaseData>? Get(DataDownloaderGetParameters dataDownloaderGetParameters)
{
var downloadedData = default(IEnumerable<BaseData>?);
try
{
downloadedData = _dataDownloader.Get(dataDownloaderGetParameters);
}
catch (Exception ex)
{
Log.Error($"{nameof(CanonicalDataDownloaderDecorator)}.{nameof(Get)}.Exceptoin: {ex.Message}");
}
if (downloadedData == null && dataDownloaderGetParameters.Symbol.IsCanonical())
{
if (!_firedCanonicalNotSupportedWarning)
{
_firedCanonicalNotSupportedWarning = true;
Log.Trace($"{nameof(CanonicalDataDownloaderDecorator)}.{nameof(Get)}: {_dataDownloader.GetType().Name} does not support canonical symbols. Falling back to chain provider.");
}
downloadedData = GetContractsData(dataDownloaderGetParameters);
}
return downloadedData;
}
/// <summary>
/// Tries to adjust the date range for a given contract based on its security type and expiry date.
/// The start date is clamped to a minimum look-back period and the end date is clamped to the contract expiry date.
/// Returns false if the minimum look-back period exceeds the requested end date, meaning no valid range exists.
/// </summary>
/// <param name="contract">The contract symbol containing the security type and expiry date.</param>
/// <param name="originalStartDateUtc">The requested start date in UTC.</param>
/// <param name="originalEndDateUtc">The requested end date in UTC.</param>
/// <param name="adjustedStartDateUtc">The adjusted start date in UTC, or default if no valid range exists.</param>
/// <param name="adjustedEndDateUtc">The adjusted end date in UTC, or default if no valid range exists.</param>
/// <returns>True if a valid adjusted date range was found, false otherwise.</returns>
public static bool TryAdjustDateRangeForContract(Symbol contract, DateTime originalStartDateUtc, DateTime originalEndDateUtc,
out DateTime adjustedStartDateUtc, out DateTime adjustedEndDateUtc)
{
var expiryDate = contract.ID.Date;
var minLookBack = expiryDate;
if (contract.ID.SecurityType.IsOption())
{
minLookBack = expiryDate.AddYears(-_optionLookbackYeard);
}
else if (contract.ID.SecurityType == SecurityType.Future)
{
minLookBack = expiryDate.AddYears(-_futureLookbackYeard);
}
if (minLookBack > originalEndDateUtc || expiryDate < originalStartDateUtc)
{
adjustedStartDateUtc = default;
adjustedEndDateUtc = default;
return false;
}
adjustedStartDateUtc = minLookBack >= originalStartDateUtc ? minLookBack : originalStartDateUtc;
adjustedEndDateUtc = expiryDate <= originalEndDateUtc ? expiryDate.AddDays(1) : originalEndDateUtc;
return true;
}
/// <summary>
/// Downloads data for all contracts of a canonical symbol in parallel, streaming results as they arrive.
/// </summary>
private IEnumerable<BaseData>? GetContractsData(DataDownloaderGetParameters parameters)
{
var contracts = GetContracts(parameters.Symbol, parameters.StartUtc, parameters.EndUtc);
var blockingCollection = new BlockingCollection<BaseData>();
var processedContracts = 0L;
var producerTask = Task.Run(() =>
{
try
{
Parallel.ForEach(
contracts,
_parallelOptions,
contract =>
{
Interlocked.Increment(ref processedContracts);
if (!TryAdjustDateRangeForContract(contract, parameters.StartUtc, parameters.EndUtc, out var adjustedStartDateUtc, out var adjustedEndDateUtc))
{
return;
}
var contractParameters = new DataDownloaderGetParameters(
contract,
parameters.Resolution,
adjustedStartDateUtc,
adjustedEndDateUtc,
parameters.TickType);
try
{
var contractData = _dataDownloader.Get(contractParameters);
foreach (var data in contractData)
{
blockingCollection.Add(data);
}
}
catch (Exception ex)
{
Log.Debug($"{nameof(CanonicalDataDownloaderDecorator)}.{nameof(GetContractsData)}: " +
$"Error downloading data for {contractParameters}. Exception: {ex.Message}. Continuing...");
return;
}
});
}
finally
{
Log.Debug($"{nameof(CanonicalDataDownloaderDecorator)}.{nameof(GetContractsData)}: Finished downloading {processedContracts} for canonical symbol.");
blockingCollection.CompleteAdding();
}
});
var consumingEnumerable = blockingCollection.GetConsumingEnumerable();
if (!consumingEnumerable.Any())
{
if (Interlocked.Read(ref processedContracts) == 0)
{
Log.Error($"{nameof(CanonicalDataDownloaderDecorator)}.{nameof(GetContractsData)}: No contracts were found. Do you have universe data?");
}
return null;
}
return consumingEnumerable;
}
/// <summary>
/// Retrieves unique contracts for the given canonical symbol across the specified date range.
/// </summary>
private IEnumerable<Symbol> GetContracts(Symbol symbol, DateTime startUtc, DateTime endUtc)
{
var chainProvider = default(Func<Symbol, DateTime, IEnumerable<Symbol>>);
if (symbol.SecurityType == SecurityType.Future)
{
chainProvider = _futureChainProvider.GetFutureContractList;
}
else if (symbol.SecurityType.IsOption())
{
chainProvider = _optionChainProvider.GetOptionContractList;
}
else
{
throw new ArgumentException($"Unsupported security type {symbol.SecurityType} for canonical data downloader", nameof(symbol));
}
var contractsCache = new HashSet<Symbol>();
foreach (var date in Time.EachDay(startUtc.Date, endUtc.Date))
{
foreach (var contract in chainProvider(symbol, date))
{
if (contractsCache.Add(contract))
{
yield return contract;
}
}
}
}
}
}