Files
2026-07-13 13:02:50 +08:00

434 lines
18 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 NodaTime;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Tests.Common.Data.UniverseSelection;
using DateTime = System.DateTime;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class PortfolioConstructionModelTests
{
private QCAlgorithm _algorithm;
[SetUp]
public void SetUp()
{
_algorithm = new QCAlgorithm();
}
[TestCase(Language.Python)]
[TestCase(Language.CSharp)]
public void RebalanceFunctionPeriodDue(Language language)
{
TestPortfolioConstructionModel constructionModel;
if (language == Language.Python)
{
constructionModel = new TestPortfolioConstructionModel();
using (Py.GIL())
{
var func = PyModule.FromString("RebalanceFunc",
@"
from datetime import timedelta
def RebalanceFunc(time):
return time + timedelta(days=1)").GetAttr("RebalanceFunc");
constructionModel.SetRebalancingFunc(func);
}
}
else
{
constructionModel = new TestPortfolioConstructionModel(time => time.AddDays(1));
}
constructionModel.OnSecuritiesChanged(_algorithm, SecurityChanges.None);
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 1), new Insight[0]));
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(
new DateTime(2020, 1, 1, 23, 0, 0), new Insight[0]));
Assert.IsTrue(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 2), new Insight[0]));
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(
new DateTime(2020, 1, 2, 1, 0, 0), new Insight[0]));
}
[TestCase(Language.Python)]
[TestCase(Language.CSharp)]
public void RebalanceFunctionSecurityChanges(Language language)
{
TestPortfolioConstructionModel constructionModel;
if (language == Language.Python)
{
constructionModel = new TestPortfolioConstructionModel();
using (Py.GIL())
{
var func = PyModule.FromString("RebalanceFunc",
@"
from datetime import timedelta
def RebalanceFunc(time):
return time + timedelta(days=1)").GetAttr("RebalanceFunc");
constructionModel.SetRebalancingFunc(func);
}
}
else
{
constructionModel = new TestPortfolioConstructionModel(time => time.AddDays(1));
}
constructionModel.OnSecuritiesChanged(_algorithm, SecurityChanges.None);
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(
new DateTime(2020, 1, 2, 1, 0, 0), new Insight[0]));
var security = new Security(Symbols.SPY,
SecurityExchangeHours.AlwaysOpen(DateTimeZone.Utc),
new Cash(Currencies.USD, 1, 1),
SymbolProperties.GetDefault(Currencies.USD),
new IdentityCurrencyConverter(Currencies.USD),
new RegisteredSecurityDataTypesProvider(),
new SecurityCache());
constructionModel.OnSecuritiesChanged(_algorithm, SecurityChangesTests.AddedNonInternal(security));
Assert.IsTrue(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 2), new Insight[0]));
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 2), new Insight[0]));
}
[TestCase(Language.Python)]
[TestCase(Language.CSharp)]
public void RebalanceFunctionNewInsights(Language language)
{
TestPortfolioConstructionModel constructionModel;
if (language == Language.Python)
{
constructionModel = new TestPortfolioConstructionModel();
using (Py.GIL())
{
var func = PyModule.FromString("RebalanceFunc",
@"
from datetime import timedelta
def RebalanceFunc(time):
return time + timedelta(days=1)").GetAttr("RebalanceFunc");
constructionModel.SetRebalancingFunc(func);
}
}
else
{
constructionModel = new TestPortfolioConstructionModel(time => time.AddDays(1));
}
constructionModel.OnSecuritiesChanged(_algorithm, SecurityChanges.None);
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 1), new Insight[0]));
var insights = new[] { Insight.Price(Symbols.SPY, Resolution.Daily, 1, InsightDirection.Down) };
constructionModel.RebalanceOnInsightChanges = false;
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 1), insights));
constructionModel.RebalanceOnInsightChanges = true;
Assert.IsTrue(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 1), insights));
}
[TestCase(Language.Python)]
[TestCase(Language.CSharp)]
public void RebalanceFunctionInsightExpiration(Language language)
{
TestPortfolioConstructionModel constructionModel;
if (language == Language.Python)
{
constructionModel = new TestPortfolioConstructionModel();
using (Py.GIL())
{
var func = PyModule.FromString("RebalanceFunc",
@"
from datetime import timedelta
def RebalanceFunc(time):
return time + timedelta(days=10)").GetAttr("RebalanceFunc");
constructionModel.SetRebalancingFunc(func);
}
}
else
{
constructionModel = new TestPortfolioConstructionModel(time => time.AddDays(10));
}
constructionModel.OnSecuritiesChanged(_algorithm, SecurityChanges.None);
constructionModel.SetNextExpiration(new DateTime(2020, 1, 2));
constructionModel.RebalanceOnInsightChanges = false;
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 3), new Insight[0]));
constructionModel.RebalanceOnInsightChanges = true;
Assert.IsTrue(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 3), new Insight[0]));
}
[TestCase(Language.Python)]
[TestCase(Language.CSharp)]
public void NoRebalanceFunction(Language language)
{
TestPortfolioConstructionModel constructionModel;
if (language == Language.Python)
{
constructionModel = new TestPortfolioConstructionModel();
using (Py.GIL())
{
var func = PyModule.FromString(
"RebalanceFunc",
@"
from datetime import timedelta
def RebalanceFunc():
return None"
).GetAttr("RebalanceFunc");
constructionModel.SetRebalancingFunc(func.Invoke());
}
}
else
{
constructionModel = new TestPortfolioConstructionModel();
}
constructionModel.OnSecuritiesChanged(_algorithm, SecurityChanges.None);
Assert.IsTrue(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 1), new Insight[0]));
Assert.IsTrue(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 2), new Insight[0]));
Assert.IsTrue(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 3), new Insight[0]));
var security = new Security(Symbols.SPY,
SecurityExchangeHours.AlwaysOpen(DateTimeZone.Utc),
new Cash(Currencies.USD, 1, 1),
SymbolProperties.GetDefault(Currencies.USD),
new IdentityCurrencyConverter(Currencies.USD),
new RegisteredSecurityDataTypesProvider(),
new SecurityCache());
constructionModel.OnSecuritiesChanged(_algorithm, SecurityChangesTests.AddedNonInternal(security));
Assert.IsTrue(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 1), new Insight[0]));
constructionModel.OnSecuritiesChanged(_algorithm, SecurityChanges.None);
Assert.IsTrue(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 1), new Insight[0]));
}
[TestCase(Language.Python)]
[TestCase(Language.CSharp)]
public void RebalanceFunctionNull(Language language)
{
TestPortfolioConstructionModel constructionModel;
if (language == Language.Python)
{
constructionModel = new TestPortfolioConstructionModel();
using (Py.GIL())
{
var func = PyModule.FromString(
"RebalanceFunc",
@"
from datetime import timedelta
def RebalanceFunc(time):
if time.day == 17:
return time + timedelta(hours=1)
if time.day == 18:
return time
return None"
).GetAttr("RebalanceFunc");
constructionModel.SetRebalancingFunc(func);
}
}
else
{
constructionModel = new TestPortfolioConstructionModel(
time =>
{
if (time.Day == 18)
{
return time;
}
if (time.Day == 17)
{
return time.AddHours(1);
}
return null;
});
}
constructionModel.OnSecuritiesChanged(_algorithm, SecurityChanges.None);
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 1), new Insight[0]));
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(
new DateTime(2020, 1, 1, 23, 0, 0), new Insight[0]));
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 2), new Insight[0]));
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(
new DateTime(2020, 1, 2, 0, 0, 1), new Insight[0]));
// day number '17' should trigger rebalance in the next hour
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 17), new Insight[0]));
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(
new DateTime(2020, 1, 17, 0, 59, 59), new Insight[0]));
Assert.IsTrue(constructionModel.IsRebalanceDueWrapper(
new DateTime(2020, 1, 17, 1, 0, 0), new Insight[0]));
constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 20), new Insight[0]);
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 21), new Insight[0]));
// day number '18' should trigger rebalance immediately
Assert.IsTrue(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 18), new Insight[0]));
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 1, 21), new Insight[0]));
}
[TestCase(Language.Python)]
[TestCase(Language.CSharp)]
public void RebalanceFunctionDateRules(Language language)
{
var mhdb = MarketHoursDatabase.FromDataFolder();
var dateRules = new DateRules(null, new SecurityManager(
new TimeKeeper(new DateTime(2015, 1, 1), DateTimeZone.Utc)), DateTimeZone.Utc, mhdb);
TestPortfolioConstructionModel constructionModel;
if (language == Language.Python)
{
constructionModel = new TestPortfolioConstructionModel();
using (Py.GIL())
{
dynamic func = PyModule.FromString("RebalanceFunc",
@"
import datetime
def RebalanceFunc(dateRules):
return dateRules.On(datetime.datetime(2015, 1, 10), datetime.datetime(2015, 1, 30))").GetAttr("RebalanceFunc");
constructionModel.SetRebalancingFunc(func(dateRules));
}
}
else
{
var dateRule = dateRules.On(new DateTime(2015, 1, 10), new DateTime(2015, 1, 30));
constructionModel = new TestPortfolioConstructionModel(dateRule);
}
constructionModel.OnSecuritiesChanged(_algorithm, SecurityChanges.None);
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2015, 1, 1), new Insight[0]));
Assert.IsTrue(constructionModel.IsRebalanceDueWrapper(new DateTime(2015, 1, 10), new Insight[0]));
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2015, 1, 20), new Insight[0]));
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2015, 1, 29), new Insight[0]));
Assert.IsTrue(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 2, 1), new Insight[0]));
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 2, 2), new Insight[0]));
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2020, 10, 2), new Insight[0]));
}
[TestCase(Language.Python, 2)]
[TestCase(Language.Python, 1)]
[TestCase(Language.Python, 0)]
[TestCase(Language.CSharp, 0)]
public void RebalanceFunctionTimeSpan(Language language, int version)
{
TestPortfolioConstructionModel constructionModel;
if (language == Language.Python)
{
constructionModel = new TestPortfolioConstructionModel();
using (Py.GIL())
{
if (version == 1)
{
dynamic func = PyModule.FromString("RebalanceFunc",
@"
from System import *
def RebalanceFunc(timeSpan):
return timeSpan").GetAttr("RebalanceFunc");
constructionModel.SetRebalancingFunc(func(TimeSpan.FromMinutes(20)));
}
else if(version == 2)
{
dynamic func = PyModule.FromString("RebalanceFunc",
@"
from datetime import timedelta
def RebalanceFunc(constructionModel):
return constructionModel.SetRebalancingFunc(timedelta(minutes = 20))").GetAttr("RebalanceFunc");
func(constructionModel);
}
else
{
dynamic func = PyModule.FromString("RebalanceFunc",
@"
from datetime import timedelta
def RebalanceFunc():
return timedelta(minutes = 20)").GetAttr("RebalanceFunc");
constructionModel.SetRebalancingFunc(func());
}
}
}
else
{
constructionModel = new TestPortfolioConstructionModel(time => time.AddMinutes(20));
}
constructionModel.OnSecuritiesChanged(_algorithm, SecurityChanges.None);
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2015, 1, 1), new Insight[0]));
Assert.IsTrue(constructionModel.IsRebalanceDueWrapper(new DateTime(2015, 1, 1, 0, 20, 0), new Insight[0]));
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2015, 1, 1, 0, 22, 0), new Insight[0]));
Assert.IsFalse(constructionModel.IsRebalanceDueWrapper(new DateTime(2015, 1, 1, 0, 21, 0), new Insight[0]));
Assert.IsTrue(constructionModel.IsRebalanceDueWrapper(new DateTime(2015, 1, 1, 0, 40, 0), new Insight[0]));
}
class TestPortfolioConstructionModel : PortfolioConstructionModel
{
public TestPortfolioConstructionModel(IDateRule dateRule)
: this(dateRule.ToFunc())
{
}
public TestPortfolioConstructionModel(Func<DateTime, DateTime> func = null)
: base(func)
{
}
public TestPortfolioConstructionModel(Func<DateTime, DateTime?> func)
: base(func)
{
}
public new void SetRebalancingFunc(PyObject rebalancingFunc)
{
base.SetRebalancingFunc(rebalancingFunc);
}
public bool IsRebalanceDueWrapper(DateTime now, Insight[] insights)
{
return base.IsRebalanceDue(insights, now);
}
public void SetNextExpiration(DateTime nextExpiration)
{
Algorithm.Insights.Add(
new Insight(Symbols.SPY, time => nextExpiration, InsightType.Price, InsightDirection.Down));
}
}
}
}