/* * 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.Common.Data.UniverseSelection; 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; [OneTimeSetUp] 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); } } [TearDown] public void TearDown() => _algorithm.Insights.Clear(_algorithm.Securities.Keys.ToArray()); [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 = SecurityChangesTests.RemovedNonInternal(_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 = SecurityChangesTests.RemovedNonInternal(_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)}); } [Test] [TestCase(Language.CSharp)] [TestCase(Language.Python)] public void DoesNotThrowWithAlternativeOverloads(Language language) { Assert.DoesNotThrow(() => SetPortfolioConstruction(language, _algorithm, Resolution.Minute)); Assert.DoesNotThrow(() => SetPortfolioConstruction(language, _algorithm, TimeSpan.FromDays(1))); Assert.DoesNotThrow(() => SetPortfolioConstruction(language, _algorithm, Expiry.EndOfWeek)); } 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, new SecurityCache() ); } private Insight GetInsight(Symbol symbol, InsightDirection direction, DateTime generatedTimeUtc, TimeSpan? period = null, double? confidence = Confidence) { period ??= TimeSpan.FromDays(1); var insight = Insight.Price(symbol, period.Value, direction, confidence: confidence); insight.GeneratedTimeUtc = generatedTimeUtc; insight.CloseTimeUtc = generatedTimeUtc.Add(period.Value); _algorithm.Insights.Add(insight); return insight; } private void SetPortfolioConstruction(Language language, QCAlgorithm algorithm, dynamic paramenter = null) { paramenter = paramenter ?? Resolution.Daily; algorithm.SetPortfolioConstruction(new ConfidenceWeightedPortfolioConstructionModel(paramenter)); if (language == Language.Python) { using (Py.GIL()) { var name = nameof(ConfidenceWeightedPortfolioConstructionModel); var instance = Py.Import(name).GetAttr(name).Invoke(((object)paramenter).ToPython()); 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 = SecurityChangesTests.AddedNonInternal(_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); } } } }