diff --git a/DemonstrationUniverse.cs b/DemonstrationUniverse.cs index d8b962c..15c89ed 100644 --- a/DemonstrationUniverse.cs +++ b/DemonstrationUniverse.cs @@ -14,9 +14,7 @@ * */ -using System; using System.Linq; -using QuantConnect.Data; using QuantConnect.Data.UniverseSelection; using QuantConnect.DataSource; diff --git a/MyCustomDataDownloader.cs b/MyCustomDataDownloader.cs new file mode 100644 index 0000000..c545329 --- /dev/null +++ b/MyCustomDataDownloader.cs @@ -0,0 +1,59 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 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 QuantConnect.Data; +using System.Collections.Generic; +using QuantConnect.Util; + +namespace QuantConnect.Lean.DataSource.MyCustom +{ + /// + /// Data downloader class for pulling data from Data Provider + /// + public class MyCustomDataDownloader : IDataDownloader, IDisposable + { + /// + private readonly MyCustomDataProvider _myCustomDataProvider; + + /// + /// Initializes a new instance of the + /// + public MyCustomDataDownloader() + { + _myCustomDataProvider = new MyCustomDataProvider(); + } + + /// + /// Get historical data enumerable for a single symbol, type and resolution given this start and end time (in UTC). + /// + /// Parameters for the historical data request + /// Enumerable of base data for this symbol + /// + public IEnumerable Get(DataDownloaderGetParameters dataDownloaderGetParameters) + { + throw new NotImplementedException(); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + _myCustomDataProvider?.DisposeSafely(); + } + } +} diff --git a/MyCustomDataProvider.cs b/MyCustomDataProvider.cs new file mode 100644 index 0000000..2ff770d --- /dev/null +++ b/MyCustomDataProvider.cs @@ -0,0 +1,161 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 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 NodaTime; +using QuantConnect.Data; +using QuantConnect.Util; +using QuantConnect.Interfaces; +using System.Collections.Generic; +using QuantConnect.Lean.Engine.DataFeeds; +using QuantConnect.Lean.Engine.HistoricalData; + +namespace QuantConnect.Lean.DataSource.MyCustom +{ + /// + /// Implementation of Custom Data Provider + /// + public class MyCustomDataProvider : SynchronizingHistoryProvider, IDataQueueHandler + { + /// + /// + /// + private readonly IDataAggregator _dataAggregator; + + /// + /// + /// + private readonly EventBasedDataQueueHandlerSubscriptionManager _subscriptionManager; + + /// + /// Returns true if we're currently connected to the Data Provider + /// + public bool IsConnected { get; } + + /// + public override void Initialize(HistoryProviderInitializeParameters parameters) + { } + + /// + public override IEnumerable GetHistory(IEnumerable requests, DateTimeZone sliceTimeZone) + { + // Create subscription objects from the configs + var subscriptions = new List(); + foreach (var request in requests) + { + // Retrieve the history for the current request + var history = GetHistory(request); + + if (history == null) + { + // If history is null, it indicates that the request contains wrong parameters + // Handle the case where the request parameters are incorrect + continue; + } + + var subscription = CreateSubscription(request, history); + subscriptions.Add(subscription); + } + + // Validate that at least one subscription is valid; otherwise, return null + if (subscriptions.Count == 0) + { + return null; + } + + return CreateSliceEnumerableFromSubscriptions(subscriptions, sliceTimeZone); + } + + /// + /// Subscribe to the specified configuration + /// + /// defines the parameters to subscribe to a data feed + /// handler to be fired on new data available + /// The new enumerator for this subscription request + public IEnumerator Subscribe(SubscriptionDataConfig dataConfig, EventHandler newDataAvailableHandler) + { + if (!CanSubscribe(dataConfig.Symbol)) + { + return null; + } + + var enumerator = _dataAggregator.Add(dataConfig, newDataAvailableHandler); + _subscriptionManager.Subscribe(dataConfig); + + return enumerator; + } + + /// + /// Removes the specified configuration + /// + /// Subscription config to be removed + public void Unsubscribe(SubscriptionDataConfig dataConfig) + { + _subscriptionManager.Unsubscribe(dataConfig); + _dataAggregator.Remove(dataConfig); + } + + /// + /// Sets the job we're subscribing for + /// + /// Job we're subscribing for + /// + public void SetJob(Packets.LiveNodePacket job) + { + throw new NotImplementedException(); + } + + /// + /// Dispose of unmanaged resources. + /// + public void Dispose() + { + _dataAggregator?.DisposeSafely(); + _subscriptionManager?.DisposeSafely(); + throw new NotImplementedException(); + } + + /// + /// Gets the history for the requested security + /// + /// The historical data request + /// An enumerable of BaseData points + private IEnumerable GetHistory(HistoryRequest request) + { + if (!CanSubscribe(request.Symbol)) + { + return null; + } + + throw new NotImplementedException(); + } + + /// + /// Checks if this Data provider supports the specified symbol + /// + /// The symbol + /// returns true if Data Provider supports the specified symbol; otherwise false + private bool CanSubscribe(Symbol symbol) + { + if (symbol.Value.IndexOfInvariant("universe", true) != -1 || symbol.IsCanonical()) + { + return false; + } + + throw new NotImplementedException(); + } + } +} diff --git a/QuantConnect.DataSource.csproj b/QuantConnect.DataSource.csproj index f379576..d181134 100644 --- a/QuantConnect.DataSource.csproj +++ b/QuantConnect.DataSource.csproj @@ -1,4 +1,4 @@ - + net6.0 QuantConnect.DataSource @@ -9,6 +9,7 @@ + diff --git a/tests/MyCustomDataDownloaderTests.cs b/tests/MyCustomDataDownloaderTests.cs new file mode 100644 index 0000000..d2dde6c --- /dev/null +++ b/tests/MyCustomDataDownloaderTests.cs @@ -0,0 +1,41 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 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 NUnit.Framework; +using System.Collections.Generic; +using QuantConnect.Lean.DataSource.MyCustom; + +namespace QuantConnect.DataLibrary.Tests +{ + [TestFixture] + public class MyCustomDataDownloaderTests + { + private static IEnumerable DownloadTestParameters => MyCustomDataProviderHistoryTests.TestParameters; + + [TestCaseSource(nameof(DownloadTestParameters))] + public void DownloadHistory(Symbol symbol, Resolution resolution, TickType tickType, TimeSpan period, bool isThrowNotImplementedException) + { + var myCustomDownloader = new MyCustomDataDownloader(); + + var request = MyCustomDataProviderHistoryTests.GetHistoryRequest(resolution, tickType, symbol, period); + + var parameters = new DataDownloaderGetParameters(symbol, resolution, request.StartTimeUtc, request.EndTimeUtc, tickType); + + Assert.Throws(() => myCustomDownloader.Get(parameters)); + } + } +} diff --git a/tests/MyCustomDataProviderHistoryTests.cs b/tests/MyCustomDataProviderHistoryTests.cs new file mode 100644 index 0000000..7ce68ae --- /dev/null +++ b/tests/MyCustomDataProviderHistoryTests.cs @@ -0,0 +1,109 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 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 NUnit.Framework; +using QuantConnect.Data; +using QuantConnect.Util; +using QuantConnect.Tests; +using QuantConnect.Lean.DataSource.MyCustom; +using QuantConnect.Securities; +using System.Collections.Generic; +using QuantConnect.Tests.Common.Exceptions; + +namespace QuantConnect.DataLibrary.Tests +{ + [TestFixture] + public class MyCustomDataProviderHistoryTests + { + /// + private readonly MyCustomDataProvider _historyDataProvider = new(); + + internal static IEnumerable TestParameters + { + get + { + TestGlobals.Initialize(); + var equity = Symbol.Create("SPY", SecurityType.Equity, Market.USA); + var option = Symbol.Create("SPY", SecurityType.Option, Market.USA); + + yield return new TestCaseData(equity, Resolution.Daily, TickType.Trade, TimeSpan.FromDays(15), true) + .SetDescription("Valid parameters - Daily resolution, 15 days period.") + .SetCategory("Valid"); + + yield return new TestCaseData(equity, Resolution.Hour, TickType.Quote, TimeSpan.FromDays(2), true) + .SetDescription("Valid parameters - Hour resolution, 2 days period.") + .SetCategory("Valid"); + + yield return new TestCaseData(option, Resolution.Second, TickType.Trade, TimeSpan.FromMinutes(60), false) + .SetDescription("Invalid Symbol - Canonical doesn't support") + .SetCategory("Invalid"); + + /// + yield return new TestCaseData(Symbol.Create("AAA.1", SecurityType.Equity, Market.USA), Resolution.Hour, TickType.Trade, TimeSpan.FromDays(2), true) + .SetDescription("Delisted Symbol - the DataSource supports the history of delisted ones or not") + .SetCategory("Valid/Invalid"); + + /// + yield return new TestCaseData(Symbol.Create("SPWR", SecurityType.Equity, Market.USA), Resolution.Hour, TickType.Trade, TimeSpan.FromDays(2), true) + .SetDescription("Mapping Symbol") + .SetCategory("Valid"); + } + } + + [Test, TestCaseSource(nameof(TestParameters))] + public void GetsHistory(Symbol symbol, Resolution resolution, TickType tickType, TimeSpan period, bool isThrowNotImplementedException) + { + var request = GetHistoryRequest(resolution, tickType, symbol, period); + + try + { + IEnumerable slices = _historyDataProvider.GetHistory(new[] { request }, TimeZones.Utc)?.ToList(); + Assert.IsNull(slices); + } + catch (NotImplementedException) + { + Assert.IsTrue(isThrowNotImplementedException); + } + } + + internal static HistoryRequest GetHistoryRequest(Resolution resolution, TickType tickType, Symbol symbol, TimeSpan period) + { + var utcNow = DateTime.UtcNow; + var dataType = LeanData.GetDataType(resolution, tickType); + var marketHoursDatabase = MarketHoursDatabase.FromDataFolder(); + + var exchangeHours = marketHoursDatabase.GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType); + var dataTimeZone = marketHoursDatabase.GetDataTimeZone(symbol.ID.Market, symbol, symbol.SecurityType); + + return new HistoryRequest( + startTimeUtc: utcNow.Add(-period), + endTimeUtc: utcNow, + dataType: dataType, + symbol: symbol, + resolution: resolution, + exchangeHours: exchangeHours, + dataTimeZone: dataTimeZone, + fillForwardResolution: resolution, + includeExtendedMarketHours: true, + isCustomData: false, + DataNormalizationMode.Raw, + tickType: tickType + ); + } + } +} diff --git a/tests/MyCustomDataQueueHandlerTests.cs b/tests/MyCustomDataQueueHandlerTests.cs new file mode 100644 index 0000000..2e0afbd --- /dev/null +++ b/tests/MyCustomDataQueueHandlerTests.cs @@ -0,0 +1,127 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 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 NUnit.Framework; +using System.Threading; +using QuantConnect.Data; +using QuantConnect.Tests; +using QuantConnect.Logging; +using System.Threading.Tasks; +using QuantConnect.Data.Market; +using System.Collections.Generic; +using QuantConnect.Lean.DataSource.MyCustom; + +namespace QuantConnect.DataLibrary.Tests +{ + [TestFixture] + public class MyCustomDataQueueHandlerTests + { + private static IEnumerable TestParameters + { + get + { + yield return new TestCaseData(Symbols.AAPL, Resolution.Tick); + yield return new TestCaseData(Symbols.SPY, Resolution.Second); + yield return new TestCaseData(Symbols.BTCUSD, Resolution.Minute); + } + } + + [Test, TestCaseSource(nameof(TestParameters))] + public void StreamsData(Symbol symbol, Resolution resolution) + { + Assert.Pass(); + + var dataQueueHandlerProvider = new MyCustomDataProvider(); + + var configs = GetSubscriptionDataConfigs(symbol, resolution).ToList(); + + foreach (var config in configs) + { + ProcessFeed( + dataQueueHandlerProvider.Subscribe(config, (s, e) => { }), + (baseData) => + { + if (baseData != null) + { + Log.Trace($"{baseData}"); + } + }); + } + + Thread.Sleep(20_000); + + foreach (var config in configs) + { + dataQueueHandlerProvider.Unsubscribe(config); + } + + Thread.Sleep(1_000); + } + + private IEnumerable GetSubscriptionDataConfigs(Symbol symbol, Resolution resolution) + { + if (resolution == Resolution.Tick) + { + yield return new SubscriptionDataConfig(GetSubscriptionDataConfig(symbol, resolution), tickType: TickType.Trade); + yield return new SubscriptionDataConfig(GetSubscriptionDataConfig(symbol, resolution), tickType: TickType.Quote); + } + else + { + yield return GetSubscriptionDataConfig(symbol, resolution); + yield return GetSubscriptionDataConfig(symbol, resolution); + } + } + + private static SubscriptionDataConfig GetSubscriptionDataConfig(Symbol symbol, Resolution resolution) + { + return new SubscriptionDataConfig( + typeof(T), + symbol, + resolution, + TimeZones.Utc, + TimeZones.Utc, + true, + extendedHours: false, + false); + } + + private Task ProcessFeed(IEnumerator enumerator, Action callback = null) + { + return Task.Factory.StartNew(() => + { + try + { + while (enumerator.MoveNext()) + { + BaseData tick = enumerator.Current; + + if (tick != null) + { + callback?.Invoke(tick); + } + } + } + catch + { + throw; + } + }); + } + } +} diff --git a/tests/TestSetup.cs b/tests/TestSetup.cs new file mode 100644 index 0000000..fa84f3b --- /dev/null +++ b/tests/TestSetup.cs @@ -0,0 +1,79 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 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.IO; +using NUnit.Framework; +using System.Collections; +using QuantConnect.Logging; +using QuantConnect.Configuration; + +namespace QuantConnect.DataLibrary.Tests +{ + [TestFixture] + public class TestSetup + { + [Test, TestCaseSource(nameof(TestParameters))] + public void TestSetupCase() + { + } + + public static void ReloadConfiguration() + { + // nunit 3 sets the current folder to a temp folder we need it to be the test bin output folder + var dir = TestContext.CurrentContext.TestDirectory; + Environment.CurrentDirectory = dir; + Directory.SetCurrentDirectory(dir); + // reload config from current path + Config.Reset(); + + var environment = Environment.GetEnvironmentVariables(); + foreach (DictionaryEntry entry in environment) + { + var envKey = entry.Key.ToString(); + var value = entry.Value.ToString(); + + if (envKey.StartsWith("QC_")) + { + var key = envKey.Substring(3).Replace("_", "-").ToLower(); + + Log.Trace($"TestSetup(): Updating config setting '{key}' from environment var '{envKey}'"); + Config.Set(key, value); + } + } + + // resets the version among other things + Globals.Reset(); + } + + private static void SetUp() + { + Log.LogHandler = new CompositeLogHandler(); + Log.Trace("TestSetup(): starting..."); + ReloadConfiguration(); + Log.DebuggingEnabled = Config.GetBool("debug-mode"); + } + + private static TestCaseData[] TestParameters + { + get + { + SetUp(); + return new[] { new TestCaseData() }; + } + } + } +} diff --git a/tests/Tests.csproj b/tests/Tests.csproj index 8e308d0..b271b23 100644 --- a/tests/Tests.csproj +++ b/tests/Tests.csproj @@ -19,5 +19,11 @@ + + + + + PreserveNewest + diff --git a/tests/config.json b/tests/config.json new file mode 100644 index 0000000..7c2043b --- /dev/null +++ b/tests/config.json @@ -0,0 +1,3 @@ +{ + "data-folder": "../../../../../Lean/Data/" +}