Files
quantconnect--lean/Tests/Algorithm/Framework/Portfolio/EqualWeightingPortfolioConstructionModelTests.cs
2026-07-13 13:02:50 +08:00

244 lines
12 KiB
C#

/*
* 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 NUnit.Framework;
using Python.Runtime;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Tests.Common.Data.UniverseSelection;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class EqualWeightingPortfolioConstructionModelTests : BaseWeightingPortfolioConstructionModelTests
{
public override double? Weight => Algorithm.Securities.Count == 0 ? default(double) : 1d / Algorithm.Securities.Count;
public virtual PortfolioBias PortfolioBias => PortfolioBias.LongShort;
[OneTimeSetUp]
public override void SetUp()
{
base.SetUp();
var prices = new Dictionary<Symbol, decimal>
{
{ 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, 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 override void AutomaticallyRemoveInvestedWithNewInsights(Language language, InsightDirection direction)
{
SetPortfolioConstruction(language);
if (PortfolioBias != PortfolioBias.LongShort && (int)direction != (int)PortfolioBias)
{
direction = InsightDirection.Flat;
}
// 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));
// Equity will be divided by all securities minus 1, since SPY is already invested and we want to remove it
var amount = Algorithm.Portfolio.TotalPortfolioValue / (decimal)(1 / Weight - 1) *
(1 - Algorithm.Settings.FreePortfolioValuePercentage);
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 / 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, 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 override void DelistedSecurityEmitsFlatTargetWithNewInsights(Language language, InsightDirection direction)
{
SetPortfolioConstruction(language);
if (PortfolioBias != PortfolioBias.LongShort && (int)direction != (int)PortfolioBias)
{
direction = InsightDirection.Flat;
}
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);
// Equity will be divided by all securities minus 1, since SPY is already invested and we want to remove it
var amount = Algorithm.Portfolio.TotalPortfolioValue / (decimal)(1 / Weight - 1) *
(1 - Algorithm.Settings.FreePortfolioValuePercentage);
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 / 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);
}
[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 override void FlatDirectionNotAccountedToAllocation(Language language, InsightDirection direction)
{
SetPortfolioConstruction(language);
if (PortfolioBias != PortfolioBias.LongShort && (int)direction != (int)PortfolioBias)
{
direction = InsightDirection.Flat;
}
// Equity, minus $1 for fees, will be divided by all securities minus 1, since its insight will have flat direction
var amount = (Algorithm.Portfolio.TotalPortfolioValue - 1 * (Algorithm.Securities.Count - 1)) * 1 /
(decimal)((1 / Weight) - 1) * (1 - Algorithm.Settings.FreePortfolioValuePercentage);
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 / 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, 1)]
[TestCase(Language.CSharp, InsightDirection.Up, -1)]
[TestCase(Language.CSharp, InsightDirection.Down, 1)]
[TestCase(Language.CSharp, InsightDirection.Down, -1)]
[TestCase(Language.CSharp, InsightDirection.Flat, 1)]
[TestCase(Language.CSharp, InsightDirection.Flat, -1)]
[TestCase(Language.Python, InsightDirection.Up, 1)]
[TestCase(Language.Python, InsightDirection.Up, -1)]
[TestCase(Language.Python, InsightDirection.Down, 1)]
[TestCase(Language.Python, InsightDirection.Down, -1)]
[TestCase(Language.Python, InsightDirection.Flat, 1)]
[TestCase(Language.Python, InsightDirection.Flat, -1)]
public virtual void InsightsReturnsTargetsConsistentWithDirection(Language language, InsightDirection direction, int weightSign)
{
SetPortfolioConstruction(language);
if (PortfolioBias != PortfolioBias.LongShort && (int)direction != (int)PortfolioBias)
{
direction = InsightDirection.Flat;
}
// Equity will be divided by all securities
var amount = Algorithm.Portfolio.TotalPortfolioValue * (decimal)Weight *
(1 - Algorithm.Settings.FreePortfolioValuePercentage);
var expectedTargets = Algorithm.Securities
.Select(x => new PortfolioTarget(x.Key, (int)direction * Math.Floor(amount / x.Value.Price)));
var insights = Algorithm.Securities.Keys.Select(x => GetInsight(x, direction, Algorithm.UtcTime, weight: weightSign * Weight));
var actualTargets = Algorithm.PortfolioConstruction.CreateTargets(Algorithm, insights.ToArray());
AssertTargets(expectedTargets, actualTargets);
}
public override Insight GetInsight(Symbol symbol, InsightDirection direction, DateTime generatedTimeUtc, TimeSpan? period = null, double? weight = 0.01)
{
period ??= TimeSpan.FromDays(1);
var insight = Insight.Price(symbol, period.Value, direction, weight: Math.Max(0.01, Algorithm.Securities.Count));
insight.GeneratedTimeUtc = generatedTimeUtc;
insight.CloseTimeUtc = generatedTimeUtc.Add(period.Value);
Algorithm.Insights.Add(insight);
return insight;
}
public override IPortfolioConstructionModel GetPortfolioConstructionModel(Language language, dynamic paramenter = null)
{
if (language == Language.CSharp)
{
return new EqualWeightingPortfolioConstructionModel(paramenter);
}
using (Py.GIL())
{
const string name = nameof(EqualWeightingPortfolioConstructionModel);
var instance = Py.Import(name).GetAttr(name).Invoke(((object)paramenter).ToPython());
return new PortfolioConstructionModelPythonWrapper(instance);
}
}
}
}