From dab395277b1e2924838fecc1f7ed7957c4329f3b Mon Sep 17 00:00:00 2001 From: Will Tindall Date: Wed, 10 Jan 2024 18:29:53 +0000 Subject: [PATCH] Added UnconstrainedMeanVariancePortfolioOptimizer tests MaxSharpe tests added rounding to boundaries test Clean up pythonnet runtime exception build paths (#7687) - Clean up pythonnet runtime exception build paths Added license info to files --- Common/Extensions.cs | 2 +- ...ximumSharpeRatioPortfolioOptimizerTests.cs | 24 ++++-- .../MinimumVariancePortfolioOptimizerTests.cs | 65 ++++++++++++++-- ...inedMeanVariancePortfolioOptimizerTests.cs | 75 ++++++++++++++++--- Tests/Common/Util/PythonUtilTests.cs | 16 ++++ 5 files changed, 158 insertions(+), 24 deletions(-) diff --git a/Common/Extensions.cs b/Common/Extensions.cs index a9bc182302b8..58482dde0250 100644 --- a/Common/Extensions.cs +++ b/Common/Extensions.cs @@ -66,7 +66,7 @@ namespace QuantConnect /// public static class Extensions { - private static readonly Regex LeanPathRegex = new Regex("(?:\\S*?\\\\Lean\\\\)|(?:\\S*?/Lean/)", RegexOptions.Compiled); + private static readonly Regex LeanPathRegex = new Regex("(?:\\S*?\\\\pythonnet\\\\)|(?:\\S*?\\\\Lean\\\\)|(?:\\S*?/Lean/)|(?:\\S*?/pythonnet/)", RegexOptions.Compiled); private static readonly Dictionary _emptyDirectories = new (); private static readonly HashSet InvalidSecurityTypes = new HashSet(); private static readonly Regex DateCheck = new Regex(@"\d{8}", RegexOptions.Compiled); diff --git a/Tests/Algorithm/Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizerTests.cs b/Tests/Algorithm/Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizerTests.cs index d8c2fbca1d63..3a58639e1bcf 100644 --- a/Tests/Algorithm/Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizerTests.cs +++ b/Tests/Algorithm/Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizerTests.cs @@ -1,12 +1,23 @@ -using Accord.Statistics; +/* + * 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 NUnit.Framework; -using QLNet; using QuantConnect.Algorithm.Framework.Portfolio; using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace QuantConnect.Tests.Algorithm.Framework.Portfolio { @@ -181,8 +192,9 @@ public void BoundariesAreNotViolated() foreach (double x in result) { - Assert.GreaterOrEqual(x, lower); - Assert.LessOrEqual(x, upper); + var rounded = Math.Round(x, 6); + Assert.GreaterOrEqual(rounded, lower); + Assert.LessOrEqual(rounded, upper); }; } } diff --git a/Tests/Algorithm/Framework/Portfolio/MinimumVariancePortfolioOptimizerTests.cs b/Tests/Algorithm/Framework/Portfolio/MinimumVariancePortfolioOptimizerTests.cs index 1c1c9516838b..d4b812480286 100644 --- a/Tests/Algorithm/Framework/Portfolio/MinimumVariancePortfolioOptimizerTests.cs +++ b/Tests/Algorithm/Framework/Portfolio/MinimumVariancePortfolioOptimizerTests.cs @@ -1,11 +1,23 @@ +/* + * 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 NUnit.Framework; using QuantConnect.Algorithm.Framework.Portfolio; using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace QuantConnect.Tests.Algorithm.Framework.Portfolio { @@ -30,13 +42,11 @@ public void Setup() double[,] historicalReturns3 = new double[,] { { -0.02, 0.65, 1.25 }, { -0.29, -0.39, -0.50 }, { 0.29, 0.58, 2.39 }, { 0.00, -0.01, 0.06 } }; double[,] historicalReturns4 = new double[,] { { 0.76, 0.25, 0.21 }, { 0.02, -0.15, 0.45 }, { -0.50, -0.44, 0.07 }, { 0.81, 0.04, 0.01 }, { -0.02, 0.01, 0.02 } }; - double[] expectedReturns1 = new double[] { 0.21, 0.08, 0.88, 0.08 }; double[] expectedReturns2 = new double[] { -0.13, 0.05, 0.14 }; double[] expectedReturns3 = null; double[] expectedReturns4 = null; - double[,] covariance1 = new double[,] { { 0.31, 0.05, 0.55, 0.07 }, { 0.05, 0.04, 0.18, 0.01 }, { 0.55, 0.18, 1.28, 0.12 }, { 0.07, 0.01, 0.12, 0.02 } }; double[,] covariance2 = new double[,] { { 0.05, -0.02, -0.01 }, { -0.02, 0.21, 0.09 }, { -0.01, 0.09, 0.04 } }; double[,] covariance3 = new double[,] { { 0.06, 0.09, 0.28 }, { 0.09, 0.25, 0.58 }, { 0.28, 0.58, 1.66 } }; @@ -107,6 +117,7 @@ public void TestOptimizeWeightings(int testCaseNumber) _covariances[testCaseNumber]); Assert.AreEqual(_expectedResults[testCaseNumber], result.Select(x => Math.Round(x, 6))); + Assert.AreEqual(1d, result.Select(x => Math.Round(Math.Abs(x), 6)).Sum()); } [TestCase(5)] @@ -123,23 +134,61 @@ public void TestOptimizeWeightingsSpecifyingTargetReturns(int testCaseNumber) _covariances[testCaseNumber]); Assert.AreEqual(_expectedResults[testCaseNumber], result.Select(x => Math.Round(x, 6))); + Assert.AreEqual(1d, result.Select(x => Math.Round(Math.Abs(x), 6)).Sum()); } [TestCase(1)] public void EqualWeightingsWhenNoSolutionFound(int testCaseNumber) { - var historicalReturns = new double[,] { { } } - var testOptimizer = new MinimumVariancePortfolioOptimizer(lower: 0); - var expectedResult = new double[] { 0.5d, 0.5d }; + var testOptimizer = new MinimumVariancePortfolioOptimizer(upper: -1); + var expectedResult = new double[] { 0.25, 0.25, 0.25, 0.25 }; var result = testOptimizer.Optimize(_historicalReturns[testCaseNumber]); Assert.AreEqual(expectedResult, result); } + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] public void BoundariesAreNotViolated(int testCaseNumber) { + double lower = 0; + double upper = 0.5; + var testOptimizer = new MinimumVariancePortfolioOptimizer(lower, upper); + + var result = testOptimizer.Optimize( + _historicalReturns[testCaseNumber], + _expectedReturns[testCaseNumber], + _covariances[testCaseNumber]); + + foreach (double x in result) + { + var rounded = Math.Round(x, 6); + Assert.GreaterOrEqual(rounded, lower); + Assert.LessOrEqual(rounded, upper); + }; + } + + public void EmptyPortfolioReturnsEmptyArrayOfDouble() + { + var testOptimizer = new MinimumVariancePortfolioOptimizer(); + var historicalReturns = new double[,] { { } }; + + var result = testOptimizer.Optimize(historicalReturns); + + Assert.AreEqual(Array.Empty(), result); + } + + public void SingleSecurityPortfolioReturnsOne() + { + var testOptimizer = new MinimumVariancePortfolioOptimizer(); + double[,] historicalReturns = new double[,] { { 0.76 }, { 0.02 }, { -0.50 } }; + + var result = testOptimizer.Optimize(historicalReturns); + Assert.AreEqual(1d, result); } } } diff --git a/Tests/Algorithm/Framework/Portfolio/UnconstrainedMeanVariancePortfolioOptimizerTests.cs b/Tests/Algorithm/Framework/Portfolio/UnconstrainedMeanVariancePortfolioOptimizerTests.cs index 391b3fb411ea..a2471f06098d 100644 --- a/Tests/Algorithm/Framework/Portfolio/UnconstrainedMeanVariancePortfolioOptimizerTests.cs +++ b/Tests/Algorithm/Framework/Portfolio/UnconstrainedMeanVariancePortfolioOptimizerTests.cs @@ -1,44 +1,101 @@ -using Accord.Statistics; +/* + * 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 NUnit.Framework; using QuantConnect.Algorithm.Framework.Portfolio; using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace QuantConnect.Tests.Algorithm.Framework.Portfolio { public class UnconstrainedMeanVariancePortfolioOptimizerTests { private Dictionary _historicalReturns = new(); + private Dictionary _expectedReturns = new(); + private Dictionary _covariances = new(); private Dictionary _expectedResults = new(); [OneTimeSetUp] public void Setup() { - var historicalReturns1 = new double[,] { { 0.02, 0.05, -0.10 }, { 0.022, 0.10, 0.20 }, { 0.02, -0.02, 0.15 } }; - var historicalReturns2 = new double[,] { { 0.25, 0.01 }, { 0.21, -0.02 } }; ; + double[,] historicalReturns1 = new double[,] { { 0.76, -0.06, 1.22, 0.17 }, { 0.02, 0.28, 1.25, -0.00 }, { -0.50, -0.13, -0.50, -0.03 }, { 0.81, 0.31, 2.39, 0.26 }, { -0.02, 0.02, 0.06, 0.01 } }; + double[,] historicalReturns2 = new double[,] { { -0.15, 0.67, 0.45 }, { -0.44, -0.10, 0.07 }, { 0.04, -0.41, 0.01 }, { 0.01, 0.03, 0.02 } }; + double[,] historicalReturns3 = new double[,] { { -0.02, 0.65, 1.25 }, { -0.29, -0.39, -0.50 }, { 0.29, 0.58, 2.39 }, { 0.00, -0.01, 0.06 } }; + double[,] historicalReturns4 = new double[,] { { 0.76, 0.25, 0.21 }, { 0.02, -0.15, 0.45 }, { -0.50, -0.44, 0.07 }, { 0.81, 0.04, 0.01 }, { -0.02, 0.01, 0.02 } }; + + double[] expectedReturns1 = new double[] { 0.21, 0.08, 0.88, 0.08 }; + double[] expectedReturns2 = new double[] { -0.13, 0.05, 0.14 }; + double[] expectedReturns3 = null; + double[] expectedReturns4 = null; + + double[,] covariance1 = new double[,] { { 0.31, 0.05, 0.55, 0.07 }, { 0.05, 0.04, 0.18, 0.01 }, { 0.55, 0.18, 1.28, 0.12 }, { 0.07, 0.01, 0.12, 0.02 } }; + double[,] covariance2 = new double[,] { { 0.05, -0.02, -0.01 }, { -0.02, 0.21, 0.09 }, { -0.01, 0.09, 0.04 } }; + double[,] covariance3 = new double[,] { { 0.06, 0.09, 0.28 }, { 0.09, 0.25, 0.58 }, { 0.28, 0.58, 1.66 } }; + double[,] covariance4 = null; - var expectedResult1 = new double[] { -1.4234053790251934E+20, 2.124485640336107E+18, 5.948559792941101E+17 }; - var expectedResult2 = new double[] { 1.0058640608512344E+18, -1.341152081134979E+18 }; + double[] expectedResult1 = new double[] { -13.288136, -23.322034, 8.79661, 9.389831 }; + double[] expectedResult2 = new double[] { -0.142857, -35.285714, 82.857143 }; + double[] expectedResult3 = new double[] { -13.232262, -3.709534, 4.009978 }; + double[] expectedResult4 = new double[] { 4.621852, -9.651736, 5.098332 }; _historicalReturns.Add(1, historicalReturns1); _historicalReturns.Add(2, historicalReturns2); + _historicalReturns.Add(3, historicalReturns3); + _historicalReturns.Add(4, historicalReturns4); + + _expectedReturns.Add(1, expectedReturns1); + _expectedReturns.Add(2, expectedReturns2); + _expectedReturns.Add(3, expectedReturns3); + _expectedReturns.Add(4, expectedReturns4); + + _covariances.Add(1, covariance1); + _covariances.Add(2, covariance2); + _covariances.Add(3, covariance3); + _covariances.Add(4, covariance4); _expectedResults.Add(1, expectedResult1); _expectedResults.Add(2, expectedResult2); + _expectedResults.Add(3, expectedResult3); + _expectedResults.Add(4, expectedResult4); } [TestCase(1)] [TestCase(2)] + [TestCase(3)] + [TestCase(4)] public void TestOptimizeWeightings(int testCaseNumber) { var testOptimizer = new UnconstrainedMeanVariancePortfolioOptimizer(); - var result = testOptimizer.Optimize(_historicalReturns[testCaseNumber]); + var result = testOptimizer.Optimize( + _historicalReturns[testCaseNumber], + _expectedReturns[testCaseNumber], + _covariances[testCaseNumber]); + + Assert.AreEqual(_expectedResults[testCaseNumber], result.Select(x => Math.Round(x, 6))); + } + + public void EmptyPortfolioReturnsEmptyArrayOfDouble() + { + var testOptimizer = new UnconstrainedMeanVariancePortfolioOptimizer(); + var historicalReturns = new double[,] { { } }; + + var result = testOptimizer.Optimize(historicalReturns); - Assert.AreEqual(_expectedResults[testCaseNumber], result); + Assert.AreEqual(Array.Empty(), result); } } } diff --git a/Tests/Common/Util/PythonUtilTests.cs b/Tests/Common/Util/PythonUtilTests.cs index ed351c520314..3a84a15f59de 100644 --- a/Tests/Common/Util/PythonUtilTests.cs +++ b/Tests/Common/Util/PythonUtilTests.cs @@ -194,6 +194,22 @@ at CallSite.Target(Closure , CallSite , Object , PythonSlice ) at QuantConnect.AlgorithmFactory.Python.Wrappers.AlgorithmPythonWrapper.OnData(Slice slice) in /home/jhonathan/QuantConnect/Lean/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs:line 587 at QuantConnect.Lean.Engine.AlgorithmManager.Run(AlgorithmNodePacket job, IAlgorithm algorithm, ISynchronizer synchronizer, ITransactionHandler transactions, IResultHandler results, IRealTimeHandler realtime, ILeanManager leanManager, IAlphaHandler alphas, CancellationToken token) in /home/jhonathan/QuantConnect/Lean/Engine/AlgorithmManager.cs:line 523", 0)] + [TestCase(@" + at OnData + raise ValueError(""""ASD"""") + at Python.Runtime.PythonException.ThrowLastAsClrException() at src\runtime\PythonException.cs:line 52 + at Python.Runtime.PyObject.Invoke(PyTuple args in BasicTemplateAlgorithm.py: line 43 +", + @" File ""D:\QuantConnect/MyLean/Lean/Algorithm.Python\BasicTemplateAlgorithm.py"", line 43, in OnData + raise ValueError(""""ASD"""") + at Python.Runtime.PythonException.ThrowLastAsClrException() in D:\QuantConnect\MyLean\pythonnet\src\runtime\PythonException.cs:line 52 + at Python.Runtime.PyObject.Invoke(PyTuple args, PyDict kw) in D:\QuantConnect\MyLean\pythonnet\src\runtime\PythonTypes\PyObject.cs:line 837 + at Python.Runtime.PyObject.TryInvoke(InvokeBinder binder, Object[] args, Object& result) in D:\QuantConnect\MyLean\pythonnet\src\runtime\PythonTypes\PyObject.cs:line 1320 + at CallSite.Target(Closure , CallSite , Object , PythonSlice ) + at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid2[T0,T1](CallSite site, T0 arg0, T1 arg1) + at QuantConnect.AlgorithmFactory.Python.Wrappers.AlgorithmPythonWrapper.OnData(Slice slice) in D:\QuantConnect\MyLean\Lean\AlgorithmFactory\Python\Wrappers\AlgorithmPythonWrapper.cs:line 693 + at QuantConnect.Lean.Engine.AlgorithmManager.Run(AlgorithmNodePacket job, IAlgorithm algorithm, ISynchronizer synchronizer, ITransactionHandler transactions, IResultHandler results, IRealTimeHandler realtime, ILeanManager leanManager, CancellationToken token) in D:\QuantConnect\MyLean\Lean\Engine\AlgorithmManager.cs:line 526", + 0)] public void ParsesPythonExceptionStackTrace(string expected, string original, int shift) { var originalShiftValue = PythonUtil.ExceptionLineShift;