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 @@
+