434 lines
18 KiB
C#
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));
|
|
}
|
|
}
|
|
}
|
|
}
|