diff --git a/Algorithm.CSharp/ConfidenceWeightedFrameworkAlgorithm.cs b/Algorithm.CSharp/ConfidenceWeightedFrameworkAlgorithm.cs new file mode 100644 index 000000000000..c7e618e214bf --- /dev/null +++ b/Algorithm.CSharp/ConfidenceWeightedFrameworkAlgorithm.cs @@ -0,0 +1,111 @@ +/* + * 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.Collections.Generic; +using QuantConnect.Algorithm.Framework.Alphas; +using QuantConnect.Algorithm.Framework.Execution; +using QuantConnect.Algorithm.Framework.Portfolio; +using QuantConnect.Algorithm.Framework.Selection; +using QuantConnect.Interfaces; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Test algorithm using and + /// generating a constant with a 0.25 confidence + /// + public class ConfidenceWeightedFrameworkAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + /// + /// Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized. + /// + public override void Initialize() + { + // Set requested data resolution + UniverseSettings.Resolution = Resolution.Minute; + + SetStartDate(2013, 10, 07); //Set Start Date + SetEndDate(2013, 10, 11); //Set End Date + SetCash(100000); //Set Strategy Cash + + // set algorithm framework models + SetUniverseSelection(new ManualUniverseSelectionModel(QuantConnect.Symbol.Create("SPY", SecurityType.Equity, Market.USA))); + SetAlpha(new ConstantAlphaModel(InsightType.Price, InsightDirection.Up, TimeSpan.FromMinutes(20), 0.025, 0.25)); + SetPortfolioConstruction(new ConfidenceWeightedPortfolioConstructionModel()); + SetExecution(new ImmediateExecutionModel()); + } + + public override void OnEndOfAlgorithm() + { + if (// holdings value should be 0.25 - to avoid price fluctuation issue we compare with 0.28 and 0.23 + Portfolio.TotalHoldingsValue > Portfolio.TotalPortfolioValue * 0.28m + || + Portfolio.TotalHoldingsValue < Portfolio.TotalPortfolioValue * 0.23m) + { + throw new Exception($"Unexpected Total Holdings Value: {Portfolio.TotalHoldingsValue}"); + } + } + + /// + /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm. + /// + public bool CanRunLocally { get; } = true; + + /// + /// This is used by the regression test system to indicate which languages this algorithm is written in. + /// + public Language[] Languages { get; } = { Language.CSharp, Language.Python }; + + /// + /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm + /// + public Dictionary ExpectedStatistics => new Dictionary + { + {"Total Trades", "6"}, + {"Average Win", "0.00%"}, + {"Average Loss", "0.00%"}, + {"Compounding Annual Return", "34.982%"}, + {"Drawdown", "0.600%"}, + {"Expectancy", "-0.495"}, + {"Net Profit", "0.412%"}, + {"Sharpe Ratio", "4.016"}, + {"Loss Rate", "67%"}, + {"Win Rate", "33%"}, + {"Profit-Loss Ratio", "0.52"}, + {"Alpha", "0.146"}, + {"Beta", "0.077"}, + {"Annual Standard Deviation", "0.043"}, + {"Annual Variance", "0.002"}, + {"Information Ratio", "-1.027"}, + {"Tracking Error", "0.179"}, + {"Treynor Ratio", "2.239"}, + {"Total Fees", "$6.00"}, + {"Total Insights Generated", "100"}, + {"Total Insights Closed", "99"}, + {"Total Insights Analysis Completed", "99"}, + {"Long Insight Count", "100"}, + {"Short Insight Count", "0"}, + {"Long/Short Ratio", "100%"}, + {"Estimated Monthly Alpha Value", "$148197.8440"}, + {"Total Accumulated Estimated Alpha Value", "$25522.9620"}, + {"Mean Population Estimated Insight Value", "$257.8077"}, + {"Mean Population Direction", "54.5455%"}, + {"Mean Population Magnitude", "54.5455%"}, + {"Rolling Averaged Population Direction", "59.8056%"}, + {"Rolling Averaged Population Magnitude", "59.8056%"} + }; + } +} diff --git a/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj b/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj index 3c314238616c..32e92ec1a4bd 100644 --- a/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj +++ b/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj @@ -167,6 +167,7 @@ + diff --git a/Algorithm.Framework/Portfolio/ConfidenceWeightedPortfolioConstructionModel.cs b/Algorithm.Framework/Portfolio/ConfidenceWeightedPortfolioConstructionModel.cs new file mode 100644 index 000000000000..3b45ed2e6392 --- /dev/null +++ b/Algorithm.Framework/Portfolio/ConfidenceWeightedPortfolioConstructionModel.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 QuantConnect.Algorithm.Framework.Alphas; + +namespace QuantConnect.Algorithm.Framework.Portfolio +{ + /// + /// Provides an implementation of that generates percent targets based on the + /// . The target percent holdings of each Symbol is given by the + /// from the last active for that symbol. + /// For insights of direction , long targets are returned and for insights of direction + /// , short targets are returned. + /// If the sum of all the last active per symbol is bigger than 1, it will factor down each target + /// percent holdings proportionally so the sum is 1. + /// It will ignore that have no value. + /// + public class ConfidenceWeightedPortfolioConstructionModel : InsightWeightingPortfolioConstructionModel + { + /// + /// Initialize a new instance of + /// + /// Rebalancing frequency + public ConfidenceWeightedPortfolioConstructionModel(Resolution resolution = Resolution.Daily) + : base(resolution) + { + } + + /// + /// Method that will determine if the portfolio construction model should create a + /// target for this insight + /// + /// The insight to create a target for + /// True if the portfolio should create a target for the insight + public override bool ShouldCreateTargetForInsight(Insight insight) + { + return insight.Confidence.HasValue; + } + + /// + /// Method that will determine which member will be used to compute the weights and gets its value + /// + /// The insight to create a target for + /// The value of the selected insight member + protected override double GetValue(Insight insight) => insight.Confidence ?? 0; + } +} diff --git a/Algorithm.Framework/Portfolio/ConfidenceWeightedPortfolioConstructionModel.py b/Algorithm.Framework/Portfolio/ConfidenceWeightedPortfolioConstructionModel.py new file mode 100644 index 000000000000..50aa871dde2a --- /dev/null +++ b/Algorithm.Framework/Portfolio/ConfidenceWeightedPortfolioConstructionModel.py @@ -0,0 +1,51 @@ +# 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. + +from clr import AddReference +AddReference("QuantConnect.Common") +AddReference("QuantConnect.Algorithm.Framework") + +from QuantConnect import Resolution +from InsightWeightingPortfolioConstructionModel import InsightWeightingPortfolioConstructionModel + +class ConfidenceWeightedPortfolioConstructionModel(InsightWeightingPortfolioConstructionModel): + '''Provides an implementation of IPortfolioConstructionModel that generates percent targets based on the + Insight.Confidence. The target percent holdings of each Symbol is given by the Insight.Confidence from the last + active Insight for that symbol. + For insights of direction InsightDirection.Up, long targets are returned and for insights of direction + InsightDirection.Down, short targets are returned. + If the sum of all the last active Insight per symbol is bigger than 1, it will factor down each target + percent holdings proportionally so the sum is 1. + It will ignore Insight that have no Insight.Confidence value.''' + + def __init__(self, resolution = Resolution.Daily): + '''Initialize a new instance of ConfidenceWeightedPortfolioConstructionModel + Args: + resolution: Rebalancing frequency''' + super().__init__(resolution) + + def ShouldCreateTargetForInsight(self, insight): + '''Method that will determine if the portfolio construction model should create a + target for this insight + Args: + insight: The insight to create a target for''' + # Ignore insights that don't have Confidence value + return insight.Confidence is not None + + def GetValue(self, insight): + '''Method that will determine which member will be used to compute the weights and gets its value + Args: + insight: The insight to create a target for + Returns: + The value of the selected insight member''' + return insight.Confidence \ No newline at end of file diff --git a/Algorithm.Framework/Portfolio/InsightWeightingPortfolioConstructionModel.cs b/Algorithm.Framework/Portfolio/InsightWeightingPortfolioConstructionModel.cs index 5038aa7b52e8..7c633beda43b 100644 --- a/Algorithm.Framework/Portfolio/InsightWeightingPortfolioConstructionModel.cs +++ b/Algorithm.Framework/Portfolio/InsightWeightingPortfolioConstructionModel.cs @@ -60,7 +60,7 @@ public override Dictionary DetermineTargetPercent(ICollection(); // We will adjust weights proportionally in case the sum is > 1 so it sums to 1. - var weightSums = activeInsights.Sum(insight => insight.Weight.Value); + var weightSums = activeInsights.Sum(insight => GetValue(insight)); var weightFactor = 1.0; if (weightSums > 1) { @@ -68,9 +68,16 @@ public override Dictionary DetermineTargetPercent(ICollection + /// Method that will determine which member will be used to compute the weights and gets its value + /// + /// The insight to create a target for + /// The value of the selected insight member + protected virtual double GetValue(Insight insight) => insight.Weight ?? 0; } } diff --git a/Algorithm.Framework/Portfolio/InsightWeightingPortfolioConstructionModel.py b/Algorithm.Framework/Portfolio/InsightWeightingPortfolioConstructionModel.py index 435d26aa86fd..c21140920ee7 100644 --- a/Algorithm.Framework/Portfolio/InsightWeightingPortfolioConstructionModel.py +++ b/Algorithm.Framework/Portfolio/InsightWeightingPortfolioConstructionModel.py @@ -15,12 +15,8 @@ AddReference("QuantConnect.Common") AddReference("QuantConnect.Algorithm.Framework") -from QuantConnect import Resolution, Extensions +from QuantConnect import Resolution from QuantConnect.Algorithm.Framework.Alphas import * -from itertools import groupby -from datetime import datetime, timedelta -from pytz import utc -UTCMIN = datetime.min.replace(tzinfo=utc) from EqualWeightingPortfolioConstructionModel import EqualWeightingPortfolioConstructionModel class InsightWeightingPortfolioConstructionModel(EqualWeightingPortfolioConstructionModel): @@ -53,10 +49,18 @@ def DetermineTargetPercent(self, activeInsights): activeInsights: The active insights to generate a target for''' result = {} # We will adjust weights proportionally in case the sum is > 1 so it sums to 1. - weightSums = sum(insight.Weight for insight in activeInsights) + weightSums = sum(self.GetValue(insight) for insight in activeInsights) weightFactor = 1.0 if weightSums > 1: weightFactor = 1 / weightSums for insight in activeInsights: - result[insight] = insight.Direction * insight.Weight * weightFactor - return result \ No newline at end of file + result[insight] = insight.Direction * self.GetValue(insight) * weightFactor + return result + + def GetValue(self, insight): + '''Method that will determine which member will be used to compute the weights and gets its value + Args: + insight: The insight to create a target for + Returns: + The value of the selected insight member''' + return insight.Weight \ No newline at end of file diff --git a/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj b/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj index 78139e09b4ce..982b89296c11 100644 --- a/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj +++ b/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj @@ -124,6 +124,7 @@ + @@ -175,6 +176,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/Algorithm.Python/ConfidenceWeightedFrameworkAlgorithm.py b/Algorithm.Python/ConfidenceWeightedFrameworkAlgorithm.py new file mode 100644 index 000000000000..8be073a66856 --- /dev/null +++ b/Algorithm.Python/ConfidenceWeightedFrameworkAlgorithm.py @@ -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. + +from clr import AddReference +AddReference("System") +AddReference("QuantConnect.Algorithm") +AddReference("QuantConnect.Algorithm.Framework") +AddReference("QuantConnect.Common") + +from System import * +from QuantConnect import * +from QuantConnect.Orders import * +from QuantConnect.Algorithm import * +from QuantConnect.Algorithm.Framework import * +from QuantConnect.Algorithm.Framework.Alphas import * +from QuantConnect.Algorithm.Framework.Execution import * +from QuantConnect.Algorithm.Framework.Portfolio import * +from QuantConnect.Algorithm.Framework.Risk import * +from QuantConnect.Algorithm.Framework.Selection import * +from datetime import timedelta + +### +### Test algorithm using 'ConfidenceWeightedPortfolioConstructionModel' and 'ConstantAlphaModel' +### generating a constant 'Insight' with a 0.25 confidence +### +class ConfidenceWeightedFrameworkAlgorithm(QCAlgorithm): + def Initialize(self): + ''' Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.''' + + # Set requested data resolution + self.UniverseSettings.Resolution = Resolution.Minute + + self.SetStartDate(2013,10,7) #Set Start Date + self.SetEndDate(2013,10,11) #Set End Date + self.SetCash(100000) #Set Strategy Cash + + symbols = [ Symbol.Create("SPY", SecurityType.Equity, Market.USA) ] + + # set algorithm framework models + self.SetUniverseSelection(ManualUniverseSelectionModel(symbols)) + self.SetAlpha(ConstantAlphaModel(InsightType.Price, InsightDirection.Up, timedelta(minutes = 20), 0.025, 0.25)) + self.SetPortfolioConstruction(ConfidenceWeightedPortfolioConstructionModel()) + self.SetExecution(ImmediateExecutionModel()) + + def OnEndOfAlgorithm(self): + # holdings value should be 0.25 - to avoid price fluctuation issue we compare with 0.28 and 0.23 + if (self.Portfolio.TotalHoldingsValue > self.Portfolio.TotalPortfolioValue * 0.28 + or self.Portfolio.TotalHoldingsValue < self.Portfolio.TotalPortfolioValue * 0.23): + raise ValueError("Unexpected Total Holdings Value: " + str(self.Portfolio.TotalHoldingsValue)) diff --git a/Algorithm.Python/QuantConnect.Algorithm.Python.csproj b/Algorithm.Python/QuantConnect.Algorithm.Python.csproj index b7d78a87ede0..80e19e98c29b 100644 --- a/Algorithm.Python/QuantConnect.Algorithm.Python.csproj +++ b/Algorithm.Python/QuantConnect.Algorithm.Python.csproj @@ -79,6 +79,7 @@ + diff --git a/Tests/Algorithm/Framework/Portfolio/ConfidenceWeightedPortfolioConstructionModelTests.cs b/Tests/Algorithm/Framework/Portfolio/ConfidenceWeightedPortfolioConstructionModelTests.cs new file mode 100644 index 000000000000..40e11fbc521e --- /dev/null +++ b/Tests/Algorithm/Framework/Portfolio/ConfidenceWeightedPortfolioConstructionModelTests.cs @@ -0,0 +1,422 @@ +/* + * 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.Collections.Generic; +using System.Linq; +using NodaTime; +using NUnit.Framework; +using Python.Runtime; +using QuantConnect.Algorithm; +using QuantConnect.Algorithm.Framework.Alphas; +using QuantConnect.Algorithm.Framework.Portfolio; +using QuantConnect.Data.Market; +using QuantConnect.Data.UniverseSelection; +using QuantConnect.Orders.Fees; +using QuantConnect.Securities; +using QuantConnect.Securities.Equity; +using QuantConnect.Tests.Engine.DataFeeds; + +namespace QuantConnect.Tests.Algorithm.Framework.Portfolio +{ + [TestFixture] + public class ConfidenceWeightedPortfolioConstructionModelTests + { + private QCAlgorithm _algorithm; + private const decimal _startingCash = 100000; + private const double Confidence = 0.01; + + [TestFixtureSetUp] + public void SetUp() + { + _algorithm = new QCAlgorithm(); + _algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(_algorithm)); + + var prices = new Dictionary + { + { Symbol.Create("AIG", SecurityType.Equity, Market.USA), 55.22m }, + { Symbol.Create("IBM", SecurityType.Equity, Market.USA), 145.17m }, + { Symbol.Create("SPY", SecurityType.Equity, Market.USA), 281.79m }, + }; + + foreach (var kvp in prices) + { + var symbol = kvp.Key; + var security = GetSecurity(symbol); + security.SetMarketPrice(new Tick(_algorithm.Time, symbol, kvp.Value, kvp.Value)); + _algorithm.Securities.Add(symbol, security); + } + } + + + [Test] + [TestCase(Language.CSharp)] + [TestCase(Language.Python)] + public void EmptyInsightsReturnsEmptyTargets(Language language) + { + SetPortfolioConstruction(language, _algorithm); + + var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, new Insight[0]); + + Assert.AreEqual(0, actualTargets.Count()); + } + + [Test] + [TestCase(Language.CSharp, InsightDirection.Up)] + [TestCase(Language.CSharp, InsightDirection.Down)] + [TestCase(Language.CSharp, InsightDirection.Flat)] + [TestCase(Language.Python, InsightDirection.Up)] + [TestCase(Language.Python, InsightDirection.Down)] + [TestCase(Language.Python, InsightDirection.Flat)] + public void InsightsReturnsTargetsConsistentWithDirection(Language language, InsightDirection direction) + { + SetPortfolioConstruction(language, _algorithm); + + var amount = _algorithm.Portfolio.TotalPortfolioValue * (decimal)Confidence; + var expectedTargets = _algorithm.Securities + .Select(x => new PortfolioTarget(x.Key, (int)direction + * Math.Floor(amount * (1 - _algorithm.Settings.FreePortfolioValuePercentage) + / x.Value.Price))); + + var insights = _algorithm.Securities.Keys.Select(x => GetInsight(x, direction, _algorithm.UtcTime)); + var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights.ToArray()); + + AssertTargets(expectedTargets, actualTargets); + } + + [TestCase(Language.CSharp, InsightDirection.Up)] + [TestCase(Language.CSharp, InsightDirection.Down)] + [TestCase(Language.CSharp, InsightDirection.Flat)] + [TestCase(Language.Python, InsightDirection.Up)] + [TestCase(Language.Python, InsightDirection.Down)] + [TestCase(Language.Python, InsightDirection.Flat)] + public void FlatDirectionNotAccountedToAllocation(Language language, InsightDirection direction) + { + SetPortfolioConstruction(language, _algorithm); + + // Modifying fee model for a constant one so numbers are simplified + foreach (var security in _algorithm.Securities) + { + security.Value.FeeModel = new ConstantFeeModel(1); + } + + // Equity, minus $1 for fees + var amount = (_algorithm.Portfolio.TotalPortfolioValue - 1 * (_algorithm.Securities.Count - 1)) * (decimal)Confidence; + var expectedTargets = _algorithm.Securities.Select(x => + { + // Expected target quantity for SPY is zero, since its insight will have flat direction + var quantity = x.Key.Value == "SPY" ? 0 : (int)direction + * Math.Floor(amount * (1 - _algorithm.Settings.FreePortfolioValuePercentage) + / x.Value.Price); + return new PortfolioTarget(x.Key, quantity); + }); + + var insights = _algorithm.Securities.Keys.Select(x => + { + // SPY insight direction is flat + var actualDirection = x.Value == "SPY" ? InsightDirection.Flat : direction; + return GetInsight(x, actualDirection, _algorithm.UtcTime); + }); + var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights.ToArray()); + + AssertTargets(expectedTargets, actualTargets); + } + + [Test] + [TestCase(Language.CSharp, InsightDirection.Up)] + [TestCase(Language.CSharp, InsightDirection.Down)] + [TestCase(Language.CSharp, InsightDirection.Flat)] + [TestCase(Language.Python, InsightDirection.Up)] + [TestCase(Language.Python, InsightDirection.Down)] + [TestCase(Language.Python, InsightDirection.Flat)] + public void AutomaticallyRemoveInvestedWithNewInsights(Language language, InsightDirection direction) + { + SetPortfolioConstruction(language, _algorithm); + + // Let's create a position for SPY + var insights = new[] { GetInsight(Symbols.SPY, direction, _algorithm.UtcTime) }; + + foreach (var target in _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights)) + { + var holding = _algorithm.Portfolio[target.Symbol]; + holding.SetHoldings(holding.Price, target.Quantity); + _algorithm.Portfolio.SetCash(_startingCash - holding.HoldingsValue); + } + + SetUtcTime(_algorithm.UtcTime.AddDays(2)); + + var amount = _algorithm.Portfolio.TotalPortfolioValue * (decimal)Confidence; + var expectedTargets = _algorithm.Securities.Select(x => + { + // Expected target quantity for SPY is zero, since it will be removed + var quantity = x.Key.Value == "SPY" ? 0 : (int)direction + * Math.Floor(amount * (1 - _algorithm.Settings.FreePortfolioValuePercentage) + / x.Value.Price); + return new PortfolioTarget(x.Key, quantity); + }); + + // Do no include SPY in the insights + insights = _algorithm.Securities.Keys.Where(x => x.Value != "SPY") + .Select(x => GetInsight(x, direction, _algorithm.UtcTime)).ToArray(); + + var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights); + + AssertTargets(expectedTargets, actualTargets); + } + + [Test] + [TestCase(Language.CSharp)] + [TestCase(Language.Python)] + public void AutomaticallyRemoveInvestedWithoutNewInsights(Language language) + { + SetPortfolioConstruction(language, _algorithm); + + // Let's create a position for SPY + var insights = new[] { GetInsight(Symbols.SPY, InsightDirection.Up, _algorithm.UtcTime) }; + + foreach (var target in _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights)) + { + var holding = _algorithm.Portfolio[target.Symbol]; + holding.SetHoldings(holding.Price, target.Quantity); + _algorithm.Portfolio.SetCash(_startingCash - holding.HoldingsValue); + } + + SetUtcTime(_algorithm.UtcTime.AddDays(2)); + + var expectedTargets = new List { new PortfolioTarget(Symbols.SPY, 0) }; + + // Create target from an empty insights array + var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, new Insight[0]); + + AssertTargets(expectedTargets, actualTargets); + } + + [Test] + [TestCase(Language.CSharp)] + [TestCase(Language.Python)] + public void LongTermInsightPreservesPosition(Language language) + { + SetPortfolioConstruction(language, _algorithm); + + // First emit long term insight + var insights = new[] { GetInsight(Symbols.SPY, InsightDirection.Down, _algorithm.UtcTime) }; + var targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList(); + Assert.AreEqual(1, targets.Count); + + // One minute later, emits short term insight + SetUtcTime(_algorithm.UtcTime.AddMinutes(1)); + insights = new[] { GetInsight(Symbols.SPY, InsightDirection.Up, _algorithm.UtcTime, Time.OneMinute) }; + targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList(); + Assert.AreEqual(1, targets.Count); + + // One minute later, emit empty insights array + SetUtcTime(_algorithm.UtcTime.AddMinutes(1.1)); + + var expectedTargets = new List { PortfolioTarget.Percent(_algorithm, Symbols.SPY, -1m * (decimal)Confidence) }; + + // Create target from an empty insights array + var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, new Insight[0]); + + AssertTargets(expectedTargets, actualTargets); + } + + [Test] + [TestCase(Language.CSharp)] + [TestCase(Language.Python)] + public void DelistedSecurityEmitsFlatTargetWithoutNewInsights(Language language) + { + SetPortfolioConstruction(language, _algorithm); + + var insights = new[] { GetInsight(Symbols.SPY, InsightDirection.Down, _algorithm.UtcTime) }; + var targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList(); + Assert.AreEqual(1, targets.Count); + + var changes = SecurityChanges.Removed(_algorithm.Securities[Symbols.SPY]); + _algorithm.PortfolioConstruction.OnSecuritiesChanged(_algorithm, changes); + + var expectedTargets = new List { new PortfolioTarget(Symbols.SPY, 0) }; + + // Create target from an empty insights array + var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, new Insight[0]); + + AssertTargets(expectedTargets, actualTargets); + } + + [Test] + [TestCase(Language.CSharp, InsightDirection.Up)] + [TestCase(Language.CSharp, InsightDirection.Down)] + [TestCase(Language.CSharp, InsightDirection.Flat)] + [TestCase(Language.Python, InsightDirection.Up)] + [TestCase(Language.Python, InsightDirection.Down)] + [TestCase(Language.Python, InsightDirection.Flat)] + public void DelistedSecurityEmitsFlatTargetWithNewInsights(Language language, InsightDirection direction) + { + SetPortfolioConstruction(language, _algorithm); + + var insights = new[] { GetInsight(Symbols.SPY, InsightDirection.Down, _algorithm.UtcTime) }; + var targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList(); + Assert.AreEqual(1, targets.Count); + + // Removing SPY should clear the key in the insight collection + var changes = SecurityChanges.Removed(_algorithm.Securities[Symbols.SPY]); + _algorithm.PortfolioConstruction.OnSecuritiesChanged(_algorithm, changes); + + var amount = _algorithm.Portfolio.TotalPortfolioValue * (decimal)Confidence; + var expectedTargets = _algorithm.Securities.Select(x => + { + // Expected target quantity for SPY is zero, since it will be removed + var quantity = x.Key.Value == "SPY" ? 0 : (int)direction + * Math.Floor(amount * (1 - _algorithm.Settings.FreePortfolioValuePercentage) + / x.Value.Price); + return new PortfolioTarget(x.Key, quantity); + }); + + // Do no include SPY in the insights + insights = _algorithm.Securities.Keys.Where(x => x.Value != "SPY") + .Select(x => GetInsight(x, direction, _algorithm.UtcTime)).ToArray(); + + // Create target from an empty insights array + var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights); + + AssertTargets(expectedTargets, actualTargets); + } + + [Test] + [TestCase(Language.CSharp)] + [TestCase(Language.Python)] + public void WeightsProportionally(Language language) + { + SetPortfolioConstruction(language, _algorithm); + + // create two insights whose confidences sums up to 2 + var insights = new[] + { + GetInsight(Symbols.SPY, InsightDirection.Down, _algorithm.UtcTime, confidence:1), + GetInsight(Symbol.Create("IBM", SecurityType.Equity, Market.USA), + InsightDirection.Down, _algorithm.UtcTime, confidence:1) + }; + + // they will each share, proportionally, the total portfolio value + var amount = _algorithm.Portfolio.TotalPortfolioValue * (decimal)0.5; + var expectedTargets = _algorithm.Securities.Where(pair => insights.Any(insight => pair.Key == insight.Symbol)) + .Select(x => new PortfolioTarget(x.Key, (int)InsightDirection.Down + * Math.Floor(amount * (1 - _algorithm.Settings.FreePortfolioValuePercentage) + / x.Value.Price))); + + var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList(); + Assert.AreEqual(2, actualTargets.Count); + + AssertTargets(expectedTargets, actualTargets); + } + + [Test] + [TestCase(Language.CSharp)] + [TestCase(Language.Python)] + public void GeneratesNoTargetsForInsightsWithNoConfidence(Language language) + { + SetPortfolioConstruction(language, _algorithm); + + var insights = new[] + { + GetInsight(Symbols.SPY, InsightDirection.Down, _algorithm.UtcTime, confidence:null) + }; + + var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList(); + Assert.AreEqual(0, actualTargets.Count); + } + + [Test] + [TestCase(Language.CSharp)] + [TestCase(Language.Python)] + public void GeneratesZeroTargetForZeroInsightConfidence(Language language) + { + SetPortfolioConstruction(language, _algorithm); + + var insights = new[] + { + GetInsight(Symbols.SPY, InsightDirection.Down, _algorithm.UtcTime, confidence:0) + }; + + var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList(); + Assert.AreEqual(1, actualTargets.Count); + AssertTargets(actualTargets, new[] {new PortfolioTarget(Symbols.SPY, 0)}); + } + + private Security GetSecurity(Symbol symbol) + { + var config = SecurityExchangeHours.AlwaysOpen(DateTimeZone.Utc); + return new Equity( + symbol, + config, + new Cash(Currencies.USD, 0, 1), + SymbolProperties.GetDefault(Currencies.USD), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null + ); + } + + private Insight GetInsight(Symbol symbol, InsightDirection direction, DateTime generatedTimeUtc, TimeSpan? period = null, double? confidence = Confidence) + { + period = period ?? TimeSpan.FromDays(1); + var insight = Insight.Price(symbol, period.Value, direction, confidence: confidence); + insight.GeneratedTimeUtc = generatedTimeUtc; + insight.CloseTimeUtc = generatedTimeUtc.Add(period.Value); + return insight; + } + + private void SetPortfolioConstruction(Language language, QCAlgorithm algorithm) + { + algorithm.SetPortfolioConstruction(new ConfidenceWeightedPortfolioConstructionModel()); + if (language == Language.Python) + { + using (Py.GIL()) + { + var name = nameof(ConfidenceWeightedPortfolioConstructionModel); + var instance = Py.Import(name).GetAttr(name).Invoke(); + var model = new PortfolioConstructionModelPythonWrapper(instance); + algorithm.SetPortfolioConstruction(model); + } + } + + foreach (var kvp in _algorithm.Portfolio) + { + kvp.Value.SetHoldings(kvp.Value.Price, 0); + } + _algorithm.Portfolio.SetCash(_startingCash); + SetUtcTime(new DateTime(2018, 7, 31)); + + var changes = SecurityChanges.Added(_algorithm.Securities.Values.ToArray()); + algorithm.PortfolioConstruction.OnSecuritiesChanged(_algorithm, changes); + } + + private void SetUtcTime(DateTime dateTime) + { + _algorithm.SetDateTime(dateTime.ConvertToUtc(_algorithm.TimeZone)); + } + + private void AssertTargets(IEnumerable expectedTargets, IEnumerable actualTargets) + { + var list = actualTargets.ToList(); + Assert.AreEqual(expectedTargets.Count(), list.Count); + + foreach (var expected in expectedTargets) + { + var actual = list.FirstOrDefault(x => x.Symbol == expected.Symbol); + Assert.IsNotNull(actual); + Assert.AreEqual(expected.Quantity, actual.Quantity); + } + } + } +} diff --git a/Tests/QuantConnect.Tests.csproj b/Tests/QuantConnect.Tests.csproj index d561df058904..3f66dded4058 100644 --- a/Tests/QuantConnect.Tests.csproj +++ b/Tests/QuantConnect.Tests.csproj @@ -276,6 +276,7 @@ +