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

325 lines
15 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 System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Lean.Engine.DataFeeds;
using QuantConnect.Lean.Engine.HistoricalData;
using QuantConnect.Packets;
using QuantConnect.Tests.Common.Data.UniverseSelection;
using QuantConnect.Tests.Engine.DataFeeds;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class MeanVarianceOptimizationPortfolioConstructionModelTests
{
private DateTime _nowUtc;
private QCAlgorithm _algorithm;
[SetUp]
public virtual void SetUp()
{
_nowUtc = new DateTime(2013, 10, 8);
_algorithm = new QCAlgorithm();
_algorithm.SetFinishedWarmingUp();
_algorithm.SetPandasConverter();
_algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(_algorithm));
_algorithm.SetDateTime(_nowUtc.ConvertToUtc(_algorithm.TimeZone));
var historyProvider = new SubscriptionDataReaderHistoryProvider();
_algorithm.SetHistoryProvider(historyProvider);
historyProvider.Initialize(new HistoryProviderInitializeParameters(
null,
null,
TestGlobals.DataProvider,
TestGlobals.DataCacheProvider,
TestGlobals.MapFileProvider,
TestGlobals.FactorFileProvider,
i => { },
true,
new DataPermissionManager(),
_algorithm.ObjectStore,
_algorithm.Settings));
}
private void Clear() => _algorithm.Insights.Clear(_algorithm.Securities.Keys.ToArray());
[TestCase(Language.CSharp, PortfolioBias.Long, 0.1, -0.1)]
[TestCase(Language.Python, PortfolioBias.Long, 0.1, -0.1)]
[TestCase(Language.CSharp, PortfolioBias.Short, -0.1, 0.1)]
[TestCase(Language.Python, PortfolioBias.Short, -0.1, 0.1)]
[TestCase(Language.CSharp, PortfolioBias.Long, -0.1, 0.1)]
[TestCase(Language.Python, PortfolioBias.Long, -0.1, 0.1)]
[TestCase(Language.CSharp, PortfolioBias.Short, 0.1, -0.1)]
[TestCase(Language.Python, PortfolioBias.Short, 0.1, -0.1)]
public void PortfolioBiasIsRespected(Language language, PortfolioBias bias, double magnitude1, double magnitude2)
{
var targets = GeneratePortfolioTargets(language, InsightDirection.Up, InsightDirection.Down, magnitude1, magnitude2, bias);
foreach (var target in targets)
{
QuantConnect.Logging.Log.Trace($"{target.Symbol}: {target.Quantity}");
if (target.Quantity == 0)
{
continue;
}
Assert.AreEqual(Math.Sign((int)bias), Math.Sign(target.Quantity));
}
}
[TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, 0, 0, 4155, 2493)]
[TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, 0, 0, 4155, 2493)]
[TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Down, 0.1, 0.05, 4155, 2493)]
[TestCase(Language.Python, InsightDirection.Up, InsightDirection.Down, 0.1, 0.05, 4155, 2493)]
[TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, 0.1, 0, 4155, 2493)]
[TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, 0.1, 0, 4155, 2493)]
[TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Down, 0, 0.1, 4155, 2493)]
[TestCase(Language.Python, InsightDirection.Up, InsightDirection.Down, 0, 0.1, 4155, 2493)]
public void CorrectWeightings(Language language,
InsightDirection direction1,
InsightDirection direction2,
double? magnitude1,
double? magnitude2,
decimal expectedQty1,
decimal expectedQty2)
{
var targets = GeneratePortfolioTargets(language, direction1, direction2, magnitude1, magnitude2).ToList();
Clear();
var quantities = targets.ToDictionary(target => {
QuantConnect.Logging.Log.Trace($"{target.Symbol}: {target.Quantity}");
return target.Symbol.Value;
},
target => target.Quantity);
Assert.AreEqual(expectedQty1, quantities["AAPL"]);
Assert.AreEqual(expectedQty2, quantities.ContainsKey("SPY") ? quantities["SPY"] : 0);
}
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void DoesNotReturnTargetsIfNoInsightMagnitude(Language language)
{
SetPortfolioConstruction(language, PortfolioBias.LongShort);
var appl = _algorithm.AddEquity("AAPL");
var insights = new[]
{
new Insight(_nowUtc, appl.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, null, null)
};
var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList();
Assert.AreEqual(0, actualTargets.Count);
}
[TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, 0.1, 0.1)]
[TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, 0.1, 0.1)]
[TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, 0.1, -0.1)]
[TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, 0.1, -0.1)]
[TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, 0.1, 0)]
[TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, 0.1, 0)]
[TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Down, 0.1, 0.1)]
[TestCase(Language.Python, InsightDirection.Up, InsightDirection.Down, 0.1, 0.1)]
public void ObeyBudgetConstraint(Language language,
InsightDirection direction1,
InsightDirection direction2,
double? magnitude1,
double? magnitude2)
{
var targets = GeneratePortfolioTargets(language, direction1, direction2, magnitude1, magnitude2);
var totalCost = targets.Sum(x => Math.Abs(x.Quantity) * 10); // Set market price at $10 in the helper method
Assert.LessOrEqual(totalCost, _algorithm.Portfolio.TotalPortfolioValue);
}
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
[TestCase(4)]
[TestCase(5)]
[TestCase(6)]
[TestCase(7)]
public void PythonConstructorWorksWithDifferentArguments(int arguments)
{
using (Py.GIL())
{
var module = PyModule.FromString(Guid.NewGuid().ToString(),
@"
from AlgorithmImports import *
timeDelta = timedelta(days=1)
class CustomPortfolioOptimizer:
def Optimize(self, historicalReturns, expectedReturns, covariance):
return [0.5]*(np.array(historicalReturns)).shape[1]"
);
var timeDelta = module.GetAttr("timeDelta");
var portfolioBias = PortfolioBias.LongShort;
var lookback = 1;
var period = 63;
var resolution = Resolution.Daily;
var targetReturn = 0.02;
var optimizer = module.GetAttr("CustomPortfolioOptimizer").Invoke();
switch (arguments)
{
case 1:
Assert.DoesNotThrow(() => new MeanVarianceOptimizationPortfolioConstructionModel(timeDelta));
break;
case 2:
Assert.DoesNotThrow(() => new MeanVarianceOptimizationPortfolioConstructionModel(timeDelta, portfolioBias));
break;
case 3:
Assert.DoesNotThrow(() => new MeanVarianceOptimizationPortfolioConstructionModel(timeDelta, portfolioBias, lookback));
break;
case 4:
Assert.DoesNotThrow(() => new MeanVarianceOptimizationPortfolioConstructionModel(timeDelta, portfolioBias, lookback, period));
break;
case 5:
Assert.DoesNotThrow(() => new MeanVarianceOptimizationPortfolioConstructionModel(timeDelta, portfolioBias, lookback, period, resolution));
break;
case 6:
Assert.DoesNotThrow(() => new MeanVarianceOptimizationPortfolioConstructionModel(timeDelta, portfolioBias, lookback, period, resolution, targetReturn));
break;
case 7:
Assert.DoesNotThrow(() => new MeanVarianceOptimizationPortfolioConstructionModel(timeDelta, portfolioBias, lookback, period, resolution, targetReturn, optimizer));
break;
}
}
}
[TestCase("timeDelta")]
[TestCase("pyFunc")]
public void PythonConstructorWorksWithDifferentArgumentRebalance(string rebalanceName)
{
using (Py.GIL())
{
var module = PyModule.FromString(Guid.NewGuid().ToString(),
@"from AlgorithmImports import *
timeDelta = timedelta(days=1)
pyFunc = lambda x: x + timedelta(days=1)");
var rebalance = module.GetAttr(rebalanceName);
Assert.DoesNotThrow(() => new MeanReversionPortfolioConstructionModel(rebalance));
}
}
[TestCase("CustomPortfolioOptimizer")]
[TestCase("csharpOptimizer")]
public void PythonConstructorWorksWithDifferentOptimizers(string optimizerName)
{
using (Py.GIL())
{
var module = PyModule.FromString(Guid.NewGuid().ToString(),
@"from AlgorithmImports import *
rebalance = timedelta(days=1)
csharpOptimizer = MinimumVariancePortfolioOptimizer()
class CustomPortfolioOptimizer:
def Optimize(self, historicalReturns, expectedReturns, covariance):
pass");
var rebalance = module.GetAttr("rebalance");
var optimizer = module.GetAttr(optimizerName);
if (optimizerName == "customOptimizer")
{
optimizer = optimizer.Invoke();
}
Assert.DoesNotThrow(() => new MeanVarianceOptimizationPortfolioConstructionModel(rebalance, optimizer: optimizer));
}
}
[Test]
public void PythonConstructorFailsWhenOptimizerTypeIsInvalid()
{
using (Py.GIL())
{
var module = PyModule.FromString(Guid.NewGuid().ToString(),
@"from AlgorithmImports import *
rebalance = timedelta(days=1)
class CustomPortfolioOptimizer:
pass");
var rebalance = module.GetAttr("rebalance");
var optimizer = module.GetAttr("CustomPortfolioOptimizer").Invoke();
var message = Assert.Throws<NotImplementedException>(() => new MeanVarianceOptimizationPortfolioConstructionModel(rebalance, optimizer: optimizer));
}
}
protected void SetPortfolioConstruction(Language language, PortfolioBias bias)
{
var model = GetPortfolioConstructionModel(language, Resolution.Daily, bias);
_algorithm.SetPortfolioConstruction(model);
foreach (var kvp in _algorithm.Portfolio)
{
kvp.Value.SetHoldings(kvp.Value.Price, 0);
}
var changes = SecurityChangesTests.AddedNonInternal(_algorithm.Securities.Values.ToArray());
_algorithm.PortfolioConstruction.OnSecuritiesChanged(_algorithm, changes);
}
public IPortfolioConstructionModel GetPortfolioConstructionModel(Language language, Resolution resolution, PortfolioBias bias)
{
if (language == Language.CSharp)
{
return new MeanVarianceOptimizationPortfolioConstructionModel(resolution, bias, 1, 63, Resolution.Daily, 0.0001);
}
using (Py.GIL())
{
const string name = nameof(MeanVarianceOptimizationPortfolioConstructionModel);
var instance = Py.Import(name).GetAttr(name)
.Invoke(((int)resolution).ToPython(), ((int)bias).ToPython(), 1.ToPython(), 63.ToPython(), ((int)Resolution.Daily).ToPython(), 0.0001.ToPython());
return new PortfolioConstructionModelPythonWrapper(instance);
}
}
private IEnumerable<IPortfolioTarget> GeneratePortfolioTargets(Language language, InsightDirection direction1, InsightDirection direction2,
double? magnitude1, double? magnitude2, PortfolioBias bias = PortfolioBias.LongShort)
{
SetPortfolioConstruction(language, bias);
var aapl = _algorithm.AddEquity("AAPL");
var spy = _algorithm.AddEquity("SPY");
aapl.SetMarketPrice(new Tick(_nowUtc, aapl.Symbol, 10, 10));
spy.SetMarketPrice(new Tick(_nowUtc, spy.Symbol, 10, 10));
aapl.SetMarketPrice(new Tick(_nowUtc.AddDays(1), aapl.Symbol, 8, 8));
spy.SetMarketPrice(new Tick(_nowUtc.AddDays(1), spy.Symbol, 15, 15));
aapl.SetMarketPrice(new Tick(_nowUtc.AddDays(2), aapl.Symbol, 12, 12));
spy.SetMarketPrice(new Tick(_nowUtc.AddDays(2), spy.Symbol, 20, 20));
var insights = new[]
{
new Insight(_nowUtc, aapl.Symbol, TimeSpan.FromDays(1), InsightType.Price, direction1, magnitude1, null),
new Insight(_nowUtc, spy.Symbol, TimeSpan.FromDays(1), InsightType.Price, direction2, magnitude2, null),
};
_algorithm.Insights.AddRange(insights);
_algorithm.PortfolioConstruction.OnSecuritiesChanged(_algorithm, SecurityChangesTests.AddedNonInternal(aapl, spy));
return _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights);
}
}
}