/* * 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 aaplicable 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.Collections.Generic; using System.Linq; using NUnit.Framework; using Python.Runtime; using QuantConnect.Algorithm; using QuantConnect.Algorithm.Framework.Alphas; using QuantConnect.Algorithm.Framework.Portfolio; using QuantConnect.Data; using QuantConnect.Data.Market; using QuantConnect.Lean.Engine.DataFeeds; using QuantConnect.Lean.Engine.HistoricalData; using QuantConnect.Packets; using QuantConnect.Tests.Common.Data.UniverseSelection; using QuantConnect.Tests.Engine.DataFeeds; namespace QuantConnect.Tests.Algorithm.Framework.Portfolio { [TestFixture] public class MeanReversionPortfolioConstructionModelTests { private DateTime _nowUtc; private QCAlgorithm _algorithm; private List _simplexTestArray; private double[] _simplexExpectedArray1, _simplexExpectedArray2; [SetUp] public virtual void SetUp() { _nowUtc = new DateTime(2021, 1, 10); _algorithm = new AlgorithmStub(); _algorithm.SetFinishedWarmingUp(); _algorithm.Settings.MinimumOrderMarginPortfolioPercentage = 0; _algorithm.Settings.FreePortfolioValue = 250; _algorithm.SetDateTime(_nowUtc.ConvertToUtc(_algorithm.TimeZone)); _algorithm.SetCash(1200); var historyProvider = new SubscriptionDataReaderHistoryProvider(); _algorithm.SetHistoryProvider(historyProvider); historyProvider.Initialize(new HistoryProviderInitializeParameters( new BacktestNodePacket(), null, TestGlobals.DataProvider, TestGlobals.DataCacheProvider, TestGlobals.MapFileProvider, TestGlobals.FactorFileProvider, i => { }, true, new DataPermissionManager(), _algorithm.ObjectStore, _algorithm.Settings)); _simplexTestArray = new List {0.2d, 0.5d, 0.4d, -0.1d, 0d}; _simplexExpectedArray1 = new double[] {1d/6, 7d/15, 11d/30, 0d, 0d}; _simplexExpectedArray2 = new double[] {0d, 0.3d, 0.2d, 0d, 0d}; } [TestCase(Language.CSharp)] [TestCase(Language.Python)] public void DoesNotReturnTargetsIfSecurityPriceIsZero(Language language) { _algorithm.AddEquity(Symbols.SPY.Value); _algorithm.SetDateTime(_nowUtc.ConvertToUtc(_algorithm.TimeZone)); SetPortfolioConstruction(language, PortfolioBias.Long); var insights = new[] { new Insight(_nowUtc, Symbols.SPY, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, null, null) }; var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights); Assert.AreEqual(0, actualTargets.Count()); } [TestCase(Language.CSharp, PortfolioBias.Long)] [TestCase(Language.Python, PortfolioBias.Long)] [TestCase(Language.CSharp, PortfolioBias.Short)] [TestCase(Language.Python, PortfolioBias.Short)] public void PortfolioBiasIsRespected(Language language, PortfolioBias bias) { if (bias == PortfolioBias.Short) { var throwsConstraint = language == Language.CSharp ? Throws.InstanceOf() : Throws.InstanceOf().With.InnerException.InstanceOf(); Assert.That(() => GetPortfolioConstructionModel(language, bias, Resolution.Daily), throwsConstraint.And.Message.EqualTo("Long position must be allowed in MeanReversionPortfolioConstructionModel.")); return; } var targets = GeneratePortfolioTargets(language, InsightDirection.Up, InsightDirection.Up, 1, 1); foreach (var target in targets) { if (target.Quantity == 0) { continue; } Assert.AreEqual(Math.Sign((int)bias), Math.Sign(target.Quantity)); } } [TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, null, null, 47, 47)] [TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, null, null, 47, 47)] [TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, 0, 0, 47, 47)] [TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, 0, 0, 47, 47)] [TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, 1, -0.5, 31, 63)] [TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, 1, -0.5, 31, 63)] [TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Down, 1, 0.5, 31, 63)] [TestCase(Language.Python, InsightDirection.Up, InsightDirection.Down, 1, 0.5, 31, 63)] [TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, 0, -0.5, 47, 47)] [TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, 0, -0.5, 47, 47)] [TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, 0, 1, 94, 0)] [TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, 0, 1, 94, 0)] [TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, 0.5, -1, 47, 47)] [TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, 0.5, -1, 47, 47)] public void CorrectWeightings(Language language, InsightDirection direction1, InsightDirection direction2, double? magnitude1, double? magnitude2, decimal expectedQty1, decimal expectedQty2) { var targets = GeneratePortfolioTargets(language, direction1, direction2, magnitude1, magnitude2); var quantities = targets.ToDictionary(target => { QuantConnect.Logging.Log.Debug($"{target.Symbol}: {target.Quantity}"); return target.Symbol.Value; }, target => target.Quantity); Assert.AreEqual(expectedQty1, quantities["AAPL"]); Assert.AreEqual(expectedQty2, quantities.ContainsKey("SPY") ? quantities["SPY"] : 0); } [Test] public void CumulativeSum() { var list = new List{1.1d, 2.5d, 0.7d, 13.6d, -5.2d, 3.9d, -1.6d}; var expected = new List{1.1d, 3.6d, 4.3d, 17.9d, 12.7d, 16.6d, 15.0d}; var result = MeanReversionPortfolioConstructionModel.CumulativeSum(list) .Select(x => Math.Round(x, 1)); Assert.AreEqual(expected, result); } [Test] public void GetPriceRelatives() { var model = new TestMeanReversionPortfolioConstructionModel(); SetPortfolioConstruction(Language.CSharp, PortfolioBias.Long, model); var aapl = _algorithm.AddEquity("AAPL"); var spy = _algorithm.AddEquity("SPY"); var insights = new List { new Insight(_nowUtc, aapl.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, null, null), new Insight(_nowUtc, spy.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, null, null), }; _algorithm.PortfolioConstruction.OnSecuritiesChanged(_algorithm, SecurityChangesTests.AddedNonInternal(aapl, spy)); var history = _algorithm.History(new[] {aapl.Symbol, spy.Symbol}, 2, Resolution.Daily); var aaplHist = history.Select(slice => slice[aapl.Symbol].Close); var spyHist = history.Select(slice => slice[spy.Symbol].Close); var aaplRelative = (double) (aaplHist.Last() / aaplHist.Average()); var spyRelative = (double) (spyHist.Last() / spyHist.Average()); var result = model.TestGetPriceRelatives(insights).Select(x => Math.Round(x, 8)).ToArray(); var expected = new double[] {aaplRelative, spyRelative}; expected = expected.Select(x => Math.Round(x, 8)).ToArray(); Assert.AreEqual(expected, result); } [Test] public void GetPriceRelativesPython() { SetPortfolioConstruction(Language.Python, PortfolioBias.Long); var aapl = _algorithm.AddEquity("AAPL"); var spy = _algorithm.AddEquity("SPY"); var insights = new List { new Insight(_nowUtc, aapl.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, null, null), new Insight(_nowUtc, spy.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, null, null), }; var history = _algorithm.History(new[] {aapl.Symbol, spy.Symbol}, 2, Resolution.Daily); var aaplHist = history.Select(slice => slice[aapl.Symbol].Close); var spyHist = history.Select(slice => slice[spy.Symbol].Close); var aaplRelative = (double) (aaplHist.Last() / aaplHist.Average()); var spyRelative = (double) (spyHist.Last() / spyHist.Average()); using (Py.GIL()) { const string name = nameof(MeanReversionPortfolioConstructionModel); var model = Py.Import(name).GetAttr(name).Invoke(((int)Resolution.Daily).ToPython(), ((int)PortfolioBias.LongShort).ToPython(), 1.ToPython(), 2.ToPython()); model.InvokeMethod("OnSecuritiesChanged", _algorithm.ToPython(), SecurityChangesTests.AddedNonInternal(aapl, spy).ToPython()); var result = PyList.AsList(model.InvokeMethod("GetPriceRelatives", insights.ToPython())); var resultArray = result.Select(x => Math.Round(Convert.ToDouble(x), 8)).ToArray(); var expected = new double[] {aaplRelative, spyRelative}; expected = expected.Select(x => Math.Round(x, 8)).ToArray(); Assert.AreEqual(expected, resultArray); } } [Test] public void GetPriceRelativesWithInsightMagnitude() { var model = new TestMeanReversionPortfolioConstructionModel(); SetPortfolioConstruction(Language.CSharp, PortfolioBias.Long, model); var aapl = _algorithm.AddEquity("AAPL"); var spy = _algorithm.AddEquity("SPY"); var insights = new List { new Insight(_nowUtc, aapl.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, 1, null), new Insight(_nowUtc, spy.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, -0.5, null), }; _algorithm.PortfolioConstruction.OnSecuritiesChanged(_algorithm, SecurityChangesTests.AddedNonInternal(aapl, spy)); var result = model.TestGetPriceRelatives(insights); var expected = new double[] {2d, 0.5d}; Assert.AreEqual(expected, result); } [Test] public void GetPriceRelativesWithInsightMagnitudePython() { SetPortfolioConstruction(Language.Python, PortfolioBias.Long); var aapl = _algorithm.AddEquity("AAPL"); var spy = _algorithm.AddEquity("SPY"); var insights = new List { new Insight(_nowUtc, aapl.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, 1, null), new Insight(_nowUtc, spy.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, -0.5, null), }; using (Py.GIL()) { const string name = nameof(MeanReversionPortfolioConstructionModel); var model = Py.Import(name).GetAttr(name).Invoke(((int)Resolution.Daily).ToPython()); model.InvokeMethod("OnSecuritiesChanged", _algorithm.ToPython(), SecurityChangesTests.AddedNonInternal(aapl, spy).ToPython()); var result = PyList.AsList(model.InvokeMethod("GetPriceRelatives", insights.ToPython())); var resultArray = result.Select(x => Convert.ToDouble(x)).ToArray(); var expected = new double[] {2d, 0.5d}; Assert.AreEqual(expected, resultArray); } } [TestCase(1)] [TestCase(0.5)] [TestCase(0)] [TestCase(-0.5)] public void SimplexProjection(double regulator) { if (regulator <= 0) { var exception = Assert.Throws(() => MeanReversionPortfolioConstructionModel.SimplexProjection(_simplexTestArray, regulator)); Assert.That(exception.Message, Is.EqualTo("Total must be > 0 for Euclidean Projection onto the Simplex.")); return; } double[] expected; if (regulator == 1d) { expected = _simplexExpectedArray1; } else { expected = _simplexExpectedArray2; } expected = expected.Select(x => Math.Round(x, 8)).ToArray(); var result = MeanReversionPortfolioConstructionModel.SimplexProjection(_simplexTestArray, regulator); result = result.Select(x => Math.Round(x, 8)).ToArray(); Assert.AreEqual(expected, result); } [TestCase(1)] [TestCase(0.5)] [TestCase(0)] [TestCase(-0.5)] public void SimplexProjectionPython(double regulator) { using (Py.GIL()) { const string name = nameof(MeanReversionPortfolioConstructionModel); var model = Py.Import(name).GetAttr(name).Invoke(((int)Resolution.Daily).ToPython()); if (regulator <= 0) { Assert.That(() => model.InvokeMethod("SimplexProjection", _simplexTestArray.ToPython(), new PyFloat(regulator)), Throws.InstanceOf() .With.InnerException.InstanceOf() .And.Message.EqualTo("Total must be > 0 for Euclidean Projection onto the Simplex.")); return; } double[] expected; if (regulator == 1d) { expected = _simplexExpectedArray1; } else { expected = _simplexExpectedArray2; } var expectedArray = expected.Select(x => Math.Round(x, 8)).ToArray(); var result = PyList.AsList(model.InvokeMethod("SimplexProjection", _simplexTestArray.ToPython(), new PyFloat(regulator))); var resultArray = result.Select(x => Math.Round(Convert.ToDouble(x), 8)).ToArray(); Assert.AreEqual(expectedArray, resultArray); } } private IEnumerable GeneratePortfolioTargets(Language language, InsightDirection direction1, InsightDirection direction2, double? magnitude1, double? magnitude2) { SetPortfolioConstruction(language, PortfolioBias.Long); var aapl = _algorithm.AddEquity("AAPL"); var spy = _algorithm.AddEquity("SPY"); foreach (var equity in new[] { aapl, spy }) { equity.SetMarketPrice(new Tick(_nowUtc, equity.Symbol, 10, 10)); } var insights = new[] { new Insight(_nowUtc, aapl.Symbol, TimeSpan.FromDays(1), InsightType.Price, direction1, magnitude1, null), new Insight(_nowUtc, spy.Symbol, TimeSpan.FromDays(1), InsightType.Price, direction2, magnitude2, null), }; _algorithm.Insights.AddRange(insights); _algorithm.PortfolioConstruction.OnSecuritiesChanged(_algorithm, SecurityChangesTests.AddedNonInternal(aapl, spy)); return _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights); } protected void SetPortfolioConstruction(Language language, PortfolioBias bias, IPortfolioConstructionModel defaultModel = null) { var model = defaultModel ?? GetPortfolioConstructionModel(language, bias, Resolution.Daily); _algorithm.SetPortfolioConstruction(model); foreach (var kvp in _algorithm.Portfolio) { kvp.Value.SetHoldings(kvp.Value.Price, 0); } var changes = SecurityChangesTests.AddedNonInternal(_algorithm.Securities.Values.ToArray()); _algorithm.PortfolioConstruction.OnSecuritiesChanged(_algorithm, changes); } public IPortfolioConstructionModel GetPortfolioConstructionModel(Language language, PortfolioBias bias, Resolution resolution) { if (language == Language.CSharp) { return new MeanReversionPortfolioConstructionModel(resolution, bias, 1, 1, resolution); } using (Py.GIL()) { const string name = nameof(MeanReversionPortfolioConstructionModel); var instance = Py.Import(name).GetAttr(name) .Invoke(((int)resolution).ToPython(), ((int)bias).ToPython(), 1.ToPython(), 1.ToPython(), ((int)resolution).ToPython()); return new PortfolioConstructionModelPythonWrapper(instance); } } private class TestMeanReversionPortfolioConstructionModel : MeanReversionPortfolioConstructionModel { public TestMeanReversionPortfolioConstructionModel() : base(Resolution.Daily, windowSize: 2) { } public double[] TestGetPriceRelatives(List insights) { return base.GetPriceRelatives(insights); } } } }