diff --git a/Engine/DataFeeds/Enumerators/Factories/LiveCustomDataSubscriptionEnumeratorFactory.cs b/Engine/DataFeeds/Enumerators/Factories/LiveCustomDataSubscriptionEnumeratorFactory.cs index 8de3484b5aa0..7b7f837db017 100644 --- a/Engine/DataFeeds/Enumerators/Factories/LiveCustomDataSubscriptionEnumeratorFactory.cs +++ b/Engine/DataFeeds/Enumerators/Factories/LiveCustomDataSubscriptionEnumeratorFactory.cs @@ -16,11 +16,15 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Python.Runtime; +using QuantConnect.Configuration; using QuantConnect.Data; using QuantConnect.Data.UniverseSelection; using QuantConnect.Interfaces; +using QuantConnect.Logging; +using QuantConnect.Securities; using QuantConnect.Util; namespace QuantConnect.Lean.Engine.DataFeeds.Enumerators.Factories @@ -30,9 +34,15 @@ namespace QuantConnect.Lean.Engine.DataFeeds.Enumerators.Factories /// public class LiveCustomDataSubscriptionEnumeratorFactory : ISubscriptionEnumeratorFactory { + // when the expected universe file is not available yet, we fall back to the backup universe file ("*.backup"), + // if any, as a last resort, when the market is open or within this time span before the next market open + private static readonly TimeSpan UniverseFileBackupFallbackWindow = + TimeSpan.FromMinutes(Config.GetInt("universe-file-backup-fallback-minutes", 30)); + private readonly TimeSpan _minimumIntervalCheck; private readonly ITimeProvider _timeProvider; private readonly Func _dateAdjustment; + private readonly BackupUniverseFileDataProvider _backupUniverseFileDataProvider; private readonly IObjectStore _objectStore; /// @@ -42,13 +52,21 @@ public class LiveCustomDataSubscriptionEnumeratorFactory : ISubscriptionEnumerat /// The object store to use /// Func that allows adjusting the datetime to use /// Allows specifying the minimum interval between each enumerator refresh and data check, default is 30 minutes + /// Whether to fall back to the backup universe file ("*.backup"), if any, as a last resort + /// when the expected universe file is not available and the market is open or close to opening. + /// Only meaningful for universe subscriptions backed by local files public LiveCustomDataSubscriptionEnumeratorFactory(ITimeProvider timeProvider, IObjectStore objectStore, - Func dateAdjustment = null, TimeSpan? minimumIntervalCheck = null) + Func dateAdjustment = null, TimeSpan? minimumIntervalCheck = null, + bool fallBackToBackupUniverseFiles = false) { _timeProvider = timeProvider; _dateAdjustment = dateAdjustment; _minimumIntervalCheck = minimumIntervalCheck ?? TimeSpan.FromMinutes(30); _objectStore = objectStore; + if (fallBackToBackupUniverseFiles) + { + _backupUniverseFileDataProvider = new BackupUniverseFileDataProvider(); + } } /// @@ -66,6 +84,7 @@ public IEnumerator CreateEnumerator(SubscriptionRequest request, IData var frontier = Ref.Create(_dateAdjustment?.Invoke(request.StartTimeLocal) ?? request.StartTimeLocal); var lastSourceRefreshTime = DateTime.MinValue; var sourceFactory = config.GetBaseDataInstance(); + var exchangeHours = request.Security.Exchange.Hours; // this is refreshing the enumerator stack for each new source var refresher = new RefreshEnumerator(() => @@ -79,7 +98,8 @@ public IEnumerator CreateEnumerator(SubscriptionRequest request, IData } lastSourceRefreshTime = utcNow; - var localDate = _dateAdjustment?.Invoke(utcNow.ConvertFromUtc(config.ExchangeTimeZone).Date) ?? utcNow.ConvertFromUtc(config.ExchangeTimeZone).Date; + var localTime = utcNow.ConvertFromUtc(config.ExchangeTimeZone); + var localDate = _dateAdjustment?.Invoke(localTime.Date) ?? localTime.Date; var source = sourceFactory.GetSource(config, localDate, true); if (source == null) { @@ -87,8 +107,18 @@ public IEnumerator CreateEnumerator(SubscriptionRequest request, IData return Enumerable.Empty().GetEnumerator(); } + var sourceDataProvider = dataProvider; + if (_backupUniverseFileDataProvider != null + && source.TransportMedium == SubscriptionTransportMedium.LocalFile + && IsUniverseFileBackupFallbackActive(exchangeHours, localTime)) + { + // if the expected universe file is not available, the backup universe file, if any, will be read as a last resort + _backupUniverseFileDataProvider.SetDataProvider(dataProvider); + sourceDataProvider = _backupUniverseFileDataProvider; + } + // fetch the new source and enumerate the data source reader - var enumerator = EnumerateDataSourceReader(config, dataProvider, frontier, source, localDate, sourceFactory); + var enumerator = EnumerateDataSourceReader(config, sourceDataProvider, frontier, source, localDate, sourceFactory); if (SourceRequiresFastForward(source)) { @@ -202,6 +232,20 @@ IDataProvider dataProvider return SubscriptionDataSourceReader.ForSource(source, dataCacheProvider, config, date, true, baseDataInstance, dataProvider, _objectStore); } + /// + /// Determines whether the backup universe file fallback is active, which is only when the market is open or close to opening + /// (within of the next market open), when the expected universe file should already be available. + /// It is evaluated at the same cadence as the enumerator refreshes + /// + /// The exchange hours of the security + /// The current time in the exchange time zone + private static bool IsUniverseFileBackupFallbackActive(SecurityExchangeHours exchangeHours, DateTime localTime) + { + return exchangeHours.IsOpen(localTime, extendedMarketHours: false) + // if the market is closed, GetNextMarketOpen returns the next day open + || exchangeHours.GetNextMarketOpen(localTime, extendedMarketHours: false) - localTime <= UniverseFileBackupFallbackWindow; + } + private bool SourceRequiresFastForward(SubscriptionDataSource source) { return source.TransportMedium == SubscriptionTransportMedium.LocalFile @@ -217,5 +261,50 @@ private static TimeSpan GetMaximumDataAge(TimeSpan increment) { return TimeSpan.FromTicks(Math.Max(increment.Ticks, TimeSpan.FromSeconds(5).Ticks)); } + + /// + /// Data provider wrapper that falls back to the backup universe file ("*.backup"), if any, + /// when the expected universe file can't be fetched, as a last resort + /// + private sealed class BackupUniverseFileDataProvider : IDataProvider + { + private IDataProvider _dataProvider; + + /// + /// Event raised each time data fetch is finished (successfully or not) + /// + public event EventHandler NewDataRequest + { + add => _dataProvider?.NewDataRequest += value; + remove => _dataProvider?.NewDataRequest -= value; + } + + /// + /// Sets the data provider to wrap, forwarding its events + /// + public void SetDataProvider(IDataProvider dataProvider) + { + _dataProvider = dataProvider; + } + + public Stream Fetch(string key) + { + var stream = _dataProvider.Fetch(key); + if (stream != null) + { + return stream; + } + + var backupKey = key + ".backup"; + stream = _dataProvider.Fetch(backupKey); + if (stream != null) + { + Log.Trace($"LiveCustomDataSubscriptionEnumeratorFactory.BackupUniverseFileDataProvider.Fetch(): universe file '{key}' is not available, " + + $"falling back to backup universe file '{backupKey}'"); + } + + return stream; + } + } } } diff --git a/Engine/DataFeeds/LiveTradingDataFeed.cs b/Engine/DataFeeds/LiveTradingDataFeed.cs index 0dfc824171f4..4884eaa68ca6 100644 --- a/Engine/DataFeeds/LiveTradingDataFeed.cs +++ b/Engine/DataFeeds/LiveTradingDataFeed.cs @@ -356,7 +356,9 @@ request.Universe is OptionChainUniverse || _algorithm.ObjectStore, // we adjust time to the previous tradable date time => Time.GetStartTimeForTradeBars(request.Security.Exchange.Hours, time, Time.OneDay, 1, false, config.DataTimeZone, _algorithm.Settings.DailyPreciseEndTime), - TimeSpan.FromMinutes(10) + TimeSpan.FromMinutes(10), + // when the expected universe file is not available yet, fall back to the backup universe file as a last resort + fallBackToBackupUniverseFiles: true ); var enumeratorStack = factory.CreateEnumerator(request, _dataProvider); diff --git a/Tests/Engine/DataFeeds/Enumerators/Factories/LiveCustomDataSubscriptionEnumeratorFactoryTests.cs b/Tests/Engine/DataFeeds/Enumerators/Factories/LiveCustomDataSubscriptionEnumeratorFactoryTests.cs index a4345c17c6e2..70c9d88303ae 100644 --- a/Tests/Engine/DataFeeds/Enumerators/Factories/LiveCustomDataSubscriptionEnumeratorFactoryTests.cs +++ b/Tests/Engine/DataFeeds/Enumerators/Factories/LiveCustomDataSubscriptionEnumeratorFactoryTests.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Moq; using NUnit.Framework; @@ -549,6 +550,129 @@ public void ToleratesNullSource() enumerator.DisposeSafely(); } + [Test] + public void FallsBackToBackupUniverseFileWhenExpectedSourceIsNotAvailable() + { + // 10 am, the market is open, so the backup fallback is active + var referenceLocal = new DateTime(2017, 10, 12, 10, 0, 0); + var referenceUtc = referenceLocal.ConvertToUtc(TimeZones.NewYork); + + var timeProvider = new ManualTimeProvider(referenceUtc); + + var expectedSourceAvailable = false; + var dataProvider = new Mock(); + dataProvider.Setup(dp => dp.Fetch("local.file.source")).Returns(() => expectedSourceAvailable ? new MemoryStream() : null); + dataProvider.Setup(dp => dp.Fetch("local.file.source.backup")).Returns(() => new MemoryStream()); + + var dataSourceReader = new Mock(); + var factory = new TestableLiveCustomDataSubscriptionEnumeratorFactory(timeProvider, dataSourceReader.Object, + fallBackToBackupUniverseFiles: true); + SetUpDataSourceReader(dataSourceReader, factory, () => new LocalFileData { EndTime = timeProvider.GetUtcNow().ConvertFromUtc(TimeZones.NewYork).AddSeconds(1) }); + + var config = new SubscriptionDataConfig(typeof(LocalFileData), Symbols.SPY, Resolution.Daily, TimeZones.NewYork, TimeZones.NewYork, false, false, false); + var request = GetSubscriptionRequest(config, referenceUtc.AddSeconds(-1), referenceUtc.AddDays(1)); + + using var enumerator = factory.CreateEnumerator(request, dataProvider.Object); + + // the expected source is not available, so the backup file is the one that gets read, in a single read of the source + Assert.IsTrue(enumerator.MoveNext()); + Assert.IsNotNull(enumerator.Current); + VerifyGetSourceInvocationCount(dataSourceReader, 1, "local.file.source", SubscriptionTransportMedium.LocalFile, FileFormat.Csv); + dataProvider.Verify(dp => dp.Fetch("local.file.source"), Times.Once); + dataProvider.Verify(dp => dp.Fetch("local.file.source.backup"), Times.Once); + + // the fallback is rate limited like the source refreshes + Assert.IsTrue(enumerator.MoveNext()); + Assert.IsNull(enumerator.Current); + dataProvider.Verify(dp => dp.Fetch(It.IsAny()), Times.Exactly(2)); + + // the expected source is preferred on the next refresh once it becomes available, without touching the backup file + expectedSourceAvailable = true; + timeProvider.Advance(TimeSpan.FromMinutes(30)); + Assert.IsTrue(enumerator.MoveNext()); + Assert.IsNotNull(enumerator.Current); + VerifyGetSourceInvocationCount(dataSourceReader, 2, "local.file.source", SubscriptionTransportMedium.LocalFile, FileFormat.Csv); + dataProvider.Verify(dp => dp.Fetch("local.file.source"), Times.Exactly(2)); + dataProvider.Verify(dp => dp.Fetch("local.file.source.backup"), Times.Once); + } + + [Test] + public void DoesNotFallBackToBackupUniverseFileFarFromMarketOpen() + { + // midnight, more than the fallback window away from the next market open, so the backup file is never tried + var referenceLocal = new DateTime(2017, 10, 12); + var referenceUtc = referenceLocal.ConvertToUtc(TimeZones.NewYork); + + var timeProvider = new ManualTimeProvider(referenceUtc); + + // the expected source is not available + var dataProvider = new Mock(); + + var dataSourceReader = new Mock(); + var factory = new TestableLiveCustomDataSubscriptionEnumeratorFactory(timeProvider, dataSourceReader.Object, + fallBackToBackupUniverseFiles: true); + SetUpDataSourceReader(dataSourceReader, factory, () => new LocalFileData { EndTime = timeProvider.GetUtcNow().ConvertFromUtc(TimeZones.NewYork).AddSeconds(1) }); + + var config = new SubscriptionDataConfig(typeof(LocalFileData), Symbols.SPY, Resolution.Daily, TimeZones.NewYork, TimeZones.NewYork, false, false, false); + var request = GetSubscriptionRequest(config, referenceUtc.AddSeconds(-1), referenceUtc.AddDays(1)); + + using var enumerator = factory.CreateEnumerator(request, dataProvider.Object); + + Assert.IsTrue(enumerator.MoveNext()); + Assert.IsNull(enumerator.Current); + + // only the expected source is tried + VerifyGetSourceInvocationCount(dataSourceReader, 1, "local.file.source", SubscriptionTransportMedium.LocalFile, FileFormat.Csv); + dataProvider.Verify(dp => dp.Fetch("local.file.source"), Times.Once); + dataProvider.Verify(dp => dp.Fetch("local.file.source.backup"), Times.Never); + } + + [Test] + public void DoesNotFallBackToBackupUniverseFileWhenNotConfigured() + { + // 10 am, the market is open, but the factory is not configured to fall back to backup universe files + var referenceLocal = new DateTime(2017, 10, 12, 10, 0, 0); + var referenceUtc = referenceLocal.ConvertToUtc(TimeZones.NewYork); + + var timeProvider = new ManualTimeProvider(referenceUtc); + + // the expected source is not available + var dataProvider = new Mock(); + + var dataSourceReader = new Mock(); + var factory = new TestableLiveCustomDataSubscriptionEnumeratorFactory(timeProvider, dataSourceReader.Object); + SetUpDataSourceReader(dataSourceReader, factory, () => new LocalFileData { EndTime = timeProvider.GetUtcNow().ConvertFromUtc(TimeZones.NewYork).AddSeconds(1) }); + + var config = new SubscriptionDataConfig(typeof(LocalFileData), Symbols.SPY, Resolution.Daily, TimeZones.NewYork, TimeZones.NewYork, false, false, false); + var request = GetSubscriptionRequest(config, referenceUtc.AddSeconds(-1), referenceUtc.AddDays(1)); + + using var enumerator = factory.CreateEnumerator(request, dataProvider.Object); + + Assert.IsTrue(enumerator.MoveNext()); + Assert.IsNull(enumerator.Current); + + // only the expected source is tried + VerifyGetSourceInvocationCount(dataSourceReader, 1, "local.file.source", SubscriptionTransportMedium.LocalFile, FileFormat.Csv); + dataProvider.Verify(dp => dp.Fetch("local.file.source"), Times.Once); + dataProvider.Verify(dp => dp.Fetch("local.file.source.backup"), Times.Never); + } + + /// + /// Sets up the mocked data source reader to fetch the source through the data cache provider the factory gave it, + /// like the real readers do, yielding a data point only when the source could be fetched + /// + private static void SetUpDataSourceReader(Mock dataSourceReader, + TestableLiveCustomDataSubscriptionEnumeratorFactory factory, Func dataFactory) + { + dataSourceReader.Setup(dsr => dsr.Read(It.IsAny())) + .Returns((SubscriptionDataSource source) => + { + using var stream = factory.DataCacheProvider.Fetch(source.Source); + return stream == null ? Enumerable.Empty() : new[] { dataFactory() }; + }) + .Verifiable(); + } + private static void VerifyGetSourceInvocationCount(Mock dataSourceReader, int count, string source, SubscriptionTransportMedium medium, FileFormat fileFormat) { dataSourceReader.Verify(dsr => dsr.Read(It.Is(sds => @@ -630,8 +754,14 @@ class TestableLiveCustomDataSubscriptionEnumeratorFactory : LiveCustomDataSubscr { private readonly ISubscriptionDataSourceReader _dataSourceReader; - public TestableLiveCustomDataSubscriptionEnumeratorFactory(ITimeProvider timeProvider, ISubscriptionDataSourceReader dataSourceReader, TimeSpan? minimumIntervalCheck = null) - : base(timeProvider, null, minimumIntervalCheck: minimumIntervalCheck) + /// + /// The data cache provider the last data source reader was created with + /// + public IDataCacheProvider DataCacheProvider { get; private set; } + + public TestableLiveCustomDataSubscriptionEnumeratorFactory(ITimeProvider timeProvider, ISubscriptionDataSourceReader dataSourceReader, + TimeSpan? minimumIntervalCheck = null, bool fallBackToBackupUniverseFiles = false) + : base(timeProvider, null, minimumIntervalCheck: minimumIntervalCheck, fallBackToBackupUniverseFiles: fallBackToBackupUniverseFiles) { _dataSourceReader = dataSourceReader; } @@ -643,6 +773,7 @@ protected override ISubscriptionDataSourceReader GetSubscriptionDataSourceReader BaseData baseData, IDataProvider dataProvider) { + DataCacheProvider = dataCacheProvider; return _dataSourceReader; } } diff --git a/Tests/Engine/DataFeeds/LiveTradingDataFeedTests.cs b/Tests/Engine/DataFeeds/LiveTradingDataFeedTests.cs index 74c4d2ad2df5..4c4cc80e799a 100644 --- a/Tests/Engine/DataFeeds/LiveTradingDataFeedTests.cs +++ b/Tests/Engine/DataFeeds/LiveTradingDataFeedTests.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; @@ -282,6 +283,142 @@ public void LiveChainSelection(SecurityType securityType, Resolution resolution, Assert.AreEqual(expectedSelections, selectionHappened); } + [TestCase("OptionChain", false)] + [TestCase("OptionChain", true)] + [TestCase("IndexOptionChain", false)] + [TestCase("IndexOptionChain", true)] + [TestCase("CoarseFundamental", false)] + [TestCase("CoarseFundamental", true)] + [TestCase("EtfConstituents", false)] + [TestCase("EtfConstituents", true)] + public void UniverseSelectionFallsBackToBackupUniverseFileCloseToMarketOpen(string universeKind, bool universeFileAvailable) + { + // start close to the market open (9:15 NY), within the backup universe file fallback window (30 minutes before the open by default) + _startDate = universeKind switch + { + "OptionChain" => new DateTime(2014, 6, 9, 13, 15, 0), + "IndexOptionChain" => new DateTime(2021, 1, 4, 14, 15, 0), + "CoarseFundamental" => new DateTime(2014, 3, 26, 13, 15, 0), + "EtfConstituents" => new DateTime(2020, 12, 1, 14, 15, 0), + _ => throw new ArgumentException($"Unexpected universe kind: {universeKind}") + }; + _manualTimeProvider.SetCurrentTimeUtc(_startDate); + var endDate = _startDate.AddDays(1); + + _algorithm.SetBenchmark(x => 1); + + var dataProvider = new BackupUniverseFileDataProvider(hideUniverseFiles: !universeFileAvailable); + var feed = RunDataFeed(runPostInitialize: false, dataProvider: dataProvider); + + var selectionHappened = 0; + var selectedCount = 0; + + IEnumerable CoarseFilter(IEnumerable coarse) + { + selectionHappened++; + var symbols = coarse.Select(x => x.Symbol).ToList(); + selectedCount = symbols.Count; + return symbols; + } + + switch (universeKind) + { + case "OptionChain": + case "IndexOptionChain": + var option = universeKind == "OptionChain" + ? _algorithm.AddOption("AAPL") + : _algorithm.AddIndexOption("SPX"); + option.SetFilter(universe => + { + selectionHappened++; + selectedCount = universe.Count(); + return universe; + }); + break; + + case "CoarseFundamental": + _algorithm.UniverseSettings.Resolution = Resolution.Daily; + _algorithm.AddUniverse(CoarseFilter); + break; + + case "EtfConstituents": + var spy = _algorithm.AddEquity("SPY").Symbol; + _algorithm.AddUniverse(_algorithm.Universe.ETF(spy, constituentsData => + { + selectionHappened++; + var symbols = constituentsData.Select(x => x.Symbol).ToList(); + selectedCount = symbols.Count; + return symbols; + })); + break; + } + + _algorithm.PostInitialize(); + + // allow time for the exchange to pick up the selection point + Thread.Sleep(50); + + ConsumeBridge(feed, TimeSpan.FromSeconds(30), true, ts => + { + if (selectionHappened > 0) + { + // we got what we wanted shortcut unit test + _manualTimeProvider.SetCurrentTimeUtc(Time.EndOfTime); + } + }, + endDate: endDate, + secondsTimeStep: 60); + + Assert.AreEqual(1, selectionHappened); + Assert.AreNotEqual(0, selectedCount); + + if (universeFileAvailable) + { + // the universe file was available, so the backup file should not have even been checked + Assert.AreEqual(0, dataProvider.BackupUniverseFileRequests); + } + else + { + Assert.AreNotEqual(0, dataProvider.UniverseFileRequests); + Assert.AreNotEqual(0, dataProvider.BackupUniverseFileRequests); + } + } + + [Test] + public void ChainSelectionDoesNotFallBackToBackupUniverseFileFarFromMarketOpen() + { + // start during the night: far from the market open, the missing universe file should not fall back to the backup file + _startDate = new DateTime(2014, 6, 9, 6, 0, 0); + _manualTimeProvider.SetCurrentTimeUtc(_startDate); + // stop before entering the fallback window, 30 minutes (by default) before the 9:30 NY market open + var endDate = new DateTime(2014, 6, 9, 12, 0, 0); + + _algorithm.SetBenchmark(x => 1); + + var dataProvider = new BackupUniverseFileDataProvider(hideUniverseFiles: true); + var feed = RunDataFeed(runPostInitialize: false, dataProvider: dataProvider); + + var selectionHappened = 0; + var option = _algorithm.AddOption("AAPL"); + option.SetFilter(universe => + { + selectionHappened++; + return universe; + }); + + _algorithm.PostInitialize(); + + // allow time for the exchange to pick up the selection point + Thread.Sleep(50); + + ConsumeBridge(feed, TimeSpan.FromSeconds(30), true, ts => { }, endDate: endDate, secondsTimeStep: 60); + + // the universe file was tried but never available, and the backup file should not have been used + Assert.AreEqual(0, selectionHappened); + Assert.AreNotEqual(0, dataProvider.UniverseFileRequests); + Assert.AreEqual(0, dataProvider.BackupUniverseFileRequests); + } + [Test] public void ContinuousFuturesImmediateSelection() { @@ -2914,7 +3051,7 @@ private IDataFeed RunDataFeed(Resolution resolution = Resolution.Second, List> getNextTicksFunction = null, Func> lookupSymbolsFunction = null, Func canPerformSelection = null, IDataQueueHandler dataQueueHandler = null, - bool runPostInitialize = true) + bool runPostInitialize = true, IDataProvider dataProvider = null) { _algorithm.SetStartDate(_startDate); _algorithm.SetDateTime(_manualTimeProvider.GetUtcNow()); @@ -2988,7 +3125,7 @@ private IDataFeed RunDataFeed(Resolution resolution = Resolution.Second, List _universeFileRequests; + public int BackupUniverseFileRequests => _backupUniverseFileRequests; + + public event EventHandler NewDataRequest; + + public BackupUniverseFileDataProvider(bool hideUniverseFiles) + { + _hideUniverseFiles = hideUniverseFiles; + } + + public Stream Fetch(string key) + { + // coarse fundamental files are universe files too, they just don't live under a "universes" folder + if (key.Contains("universes", StringComparison.InvariantCulture) + || key.Replace('\\', '/').Contains("fundamental/coarse", StringComparison.InvariantCulture)) + { + if (key.EndsWith(".csv.backup", StringComparison.InvariantCulture)) + { + Interlocked.Increment(ref _backupUniverseFileRequests); + // serve the backup universe file contents from the actual universe file + return _dataProvider.Fetch(key.Substring(0, key.Length - ".backup".Length)); + } + + if (key.EndsWith(".csv", StringComparison.InvariantCulture)) + { + Interlocked.Increment(ref _universeFileRequests); + if (_hideUniverseFiles) + { + return null; + } + } + } + + return _dataProvider.Fetch(key); + } + } + private static IEnumerable ProduceBenchmarkTicks(FuncDataQueueHandler fdqh, Count count) { for (int i = 0; i < 10000; i++)