chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:02:50 +08:00
commit 0fc60fdcb1
5008 changed files with 910633 additions and 0 deletions
@@ -0,0 +1,66 @@
/*
* 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.Collections.Generic;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Selection;
namespace QuantConnect.Tests.Algorithm.Framework.Alphas
{
[TestFixture]
public class BasePairsTradingAlphaModelTests : CommonAlphaModelTests
{
private const int _lookback = 15;
private const Resolution _resolution = Resolution.Minute;
protected override int MaxSliceCount => 1500;
protected override IAlphaModel CreateCSharpAlphaModel()
{
return new BasePairsTradingAlphaModel(_lookback, _resolution);
}
protected override void InitializeAlgorithm(QCAlgorithm algorithm)
{
algorithm.SetUniverseSelection(new ManualUniverseSelectionModel(
Symbol.Create("AIG", SecurityType.Equity, Market.USA),
Symbol.Create("BAC", SecurityType.Equity, Market.USA)));
}
protected override string GetExpectedModelName(IAlphaModel model)
{
return $"{nameof(BasePairsTradingAlphaModel)}({_lookback},{_resolution},1)";
}
protected override IAlphaModel CreatePythonAlphaModel()
{
using (Py.GIL())
{
dynamic model = Py.Import("BasePairsTradingAlphaModel").GetAttr("BasePairsTradingAlphaModel");
var instance = model(_lookback, _resolution);
return new AlphaModelPythonWrapper(instance);
}
}
protected override IEnumerable<Insight> ExpectedInsights()
{
Assert.Ignore("The CommonAlphaModelTests need to be refactored to support multiple securities with different prices for each security");
return null;
}
}
}
@@ -0,0 +1,395 @@
/*
* 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 QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Lean.Engine.DataFeeds;
using QuantConnect.Lean.Engine.HistoricalData;
using QuantConnect.Securities;
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Algorithm;
using QuantConnect.Python;
using QuantConnect.Tests.Common.Data.UniverseSelection;
using QuantConnect.Tests.Engine.DataFeeds;
using QuantConnect.Util;
namespace QuantConnect.Tests.Algorithm.Framework.Alphas
{
/// <summary>
/// Provides a framework for testing alpha models.
/// </summary>
public abstract class CommonAlphaModelTests
{
protected QCAlgorithm Algorithm { get; set; }
[OneTimeSetUp]
public void Initialize()
{
PythonInitializer.Initialize();
Algorithm = new QCAlgorithm();
Algorithm.PortfolioConstruction = new NullPortfolioConstructionModel();
Algorithm.HistoryProvider = new SineHistoryProvider(Algorithm.Securities);
Algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(Algorithm));
InitializeAlgorithm(Algorithm);
}
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void AddAlphaModel(Language language)
{
IAlphaModel model;
IAlphaModel model2 = null;
IAlphaModel model3 = null;
if (!TryCreateModel(language, out model)
|| !TryCreateModel(language, out model2)
|| !TryCreateModel(language, out model3))
{
Assert.Ignore($"Ignore {GetType().Name}: Could not create {language} model.");
}
// Set the alpha model
Algorithm.SetAlpha(model);
Algorithm.AddAlpha(model2);
Algorithm.AddAlpha(model3);
Algorithm.SetUniverseSelection(new ManualUniverseSelectionModel());
var changes = SecurityChangesTests.CreateNonInternal(AddedSecurities, RemovedSecurities);
Algorithm.OnFrameworkSecuritiesChanged(changes);
var actualInsights = new List<Insight>();
Algorithm.InsightsGenerated += (s, e) => actualInsights.AddRange(e.Insights);
var expectedInsights = ExpectedInsights().ToList();
var consolidators = Algorithm.Securities.SelectMany(kvp => kvp.Value.Subscriptions).SelectMany(x => x.Consolidators);
var slices = CreateSlices();
foreach (var slice in slices.ToList())
{
Algorithm.SetDateTime(slice.Time);
foreach (var symbol in slice.Keys)
{
var data = slice[symbol];
Algorithm.Securities[symbol].SetMarketPrice(data);
foreach (var consolidator in consolidators)
{
consolidator.Update(data);
}
}
Algorithm.OnFrameworkData(slice);
}
Assert.AreEqual(expectedInsights.Count * 3, actualInsights.Count);
for (var i = 0; i < actualInsights.Count; i = i + 3)
{
var expected = expectedInsights[i / 3];
for (int j = i; j < 3; j++)
{
var actual = actualInsights[j];
Assert.AreEqual(expected.Symbol, actual.Symbol);
Assert.AreEqual(expected.Type, actual.Type);
Assert.AreEqual(expected.Direction, actual.Direction);
Assert.LessOrEqual(expected.Period, actual.Period); // It can be canceled and discarded early
Assert.AreEqual(expected.Magnitude, actual.Magnitude);
Assert.AreEqual(expected.Confidence, actual.Confidence);
}
}
}
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void InsightsGenerationTest(Language language)
{
IAlphaModel model;
if (!TryCreateModel(language, out model))
{
Assert.Ignore($"Ignore {GetType().Name}: Could not create {language} model.");
}
// Set the alpha model
Algorithm.SetAlpha(model);
Algorithm.SetUniverseSelection(new ManualUniverseSelectionModel());
var changes = SecurityChangesTests.CreateNonInternal(AddedSecurities, RemovedSecurities);
Algorithm.OnFrameworkSecuritiesChanged(changes);
var actualInsights = new List<Insight>();
Algorithm.InsightsGenerated += (s, e) => actualInsights.AddRange(e.Insights);
var expectedInsights = ExpectedInsights().ToList();
var consolidators = Algorithm.Securities.SelectMany(kvp => kvp.Value.Subscriptions).SelectMany(x => x.Consolidators);
var slices = CreateSlices();
foreach (var slice in slices.ToList())
{
Algorithm.SetDateTime(slice.Time);
foreach (var symbol in slice.Keys)
{
var data = slice[symbol];
Algorithm.Securities[symbol].SetMarketPrice(data);
foreach (var consolidator in consolidators)
{
consolidator.Update(data);
}
}
Algorithm.OnFrameworkData(slice);
}
Assert.AreEqual(expectedInsights.Count, actualInsights.Count);
for (var i = 0; i < actualInsights.Count; i++)
{
var actual = actualInsights[i];
var expected = expectedInsights[i];
Assert.AreEqual(expected.Symbol, actual.Symbol);
Assert.AreEqual(expected.Type, actual.Type);
Assert.AreEqual(expected.Direction, actual.Direction);
Assert.LessOrEqual(expected.Period, actual.Period); // It can be canceled and discarded early
Assert.AreEqual(expected.Magnitude, actual.Magnitude);
Assert.AreEqual(expected.Confidence, actual.Confidence);
}
}
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void AddedSecuritiesTest(Language language)
{
IAlphaModel model;
if (!TryCreateModel(language, out model))
{
Assert.Ignore($"Ignore {GetType().Name}: Could not create {language} model.");
}
var changes = SecurityChangesTests.CreateNonInternal(AddedSecurities, RemovedSecurities);
Assert.DoesNotThrow(() => model.OnSecuritiesChanged(Algorithm, changes));
}
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void RemovedSecuritiesTest(Language language)
{
IAlphaModel model;
if (!TryCreateModel(language, out model))
{
Assert.Ignore($"Ignore {GetType().Name}: Could not create {language} model.");
}
var removedSecurities = Algorithm.Securities.Values;
// We have to add some security if we then want to remove it, that's why we cannot use here
// RemovedSecurities, because it doesn't contain any security
var changes = SecurityChangesTests.CreateNonInternal(removedSecurities, AddedSecurities);
Assert.DoesNotThrow(() => model.OnSecuritiesChanged(Algorithm, changes));
}
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void ModelNameTest(Language language)
{
IAlphaModel model;
if (!TryCreateModel(language, out model))
{
Assert.Ignore($"Ignore {GetType().Name}: Could not create {language} model.");
}
var actual = model.GetModelName();
var expected = GetExpectedModelName(model);
Assert.AreEqual(expected, actual);
}
/// <summary>
/// Returns a new instance of the alpha model to test
/// </summary>
protected abstract IAlphaModel CreateCSharpAlphaModel();
/// <summary>
/// Returns a new instance of the alpha model to test
/// </summary>
protected abstract IAlphaModel CreatePythonAlphaModel();
/// <summary>
/// Returns an enumerable with the expected insights
/// </summary>
protected abstract IEnumerable<Insight> ExpectedInsights();
/// <summary>
/// List of securities to be added to the model
/// </summary>
protected virtual IEnumerable<Security> AddedSecurities => Algorithm.Securities.Values;
/// <summary>
/// List of securities to be removed to the model
/// </summary>
protected virtual IEnumerable<Security> RemovedSecurities => Enumerable.Empty<Security>();
/// <summary>
/// To be override for model types that implement <see cref="INamedModel"/>
/// </summary>
protected abstract string GetExpectedModelName(IAlphaModel model);
/// <summary>
/// Provides derived types a chance to initialize anything special they require
/// </summary>
protected virtual void InitializeAlgorithm(QCAlgorithm algorithm)
{
Algorithm.SetStartDate(2018, 1, 4);
Algorithm.AddEquity(Symbols.SPY.Value, Resolution.Daily);
}
/// <summary>
/// Creates an enumerable of Slice to update the alpha model
/// </summary>
protected virtual IEnumerable<Slice> CreateSlices()
{
var timeSliceFactory = new TimeSliceFactory(TimeZones.NewYork);
var changes = SecurityChanges.None;
var sliceDateTimes = GetSliceDateTimes(MaxSliceCount);
for (var i = 0; i < sliceDateTimes.Count; i++)
{
var utcDateTime = sliceDateTimes[i];
var packets = new List<DataFeedPacket>();
// TODO : Give securities different values -- will require updating all derived types
var last = Convert.ToDecimal(100 + 10 * Math.Sin(Math.PI * i / 180.0));
var high = last * 1.005m;
var low = last / 1.005m;
foreach (var kvp in Algorithm.Securities)
{
var security = kvp.Value;
var exchange = security.Exchange.Hours;
var configs = Algorithm.SubscriptionManager.SubscriptionDataConfigService
.GetSubscriptionDataConfigs(security.Symbol);
var extendedMarket = configs.IsExtendedMarketHours();
var localDateTime = utcDateTime.ConvertFromUtc(exchange.TimeZone);
if (!exchange.IsOpen(localDateTime, extendedMarket))
{
continue;
}
var configuration = security.Subscriptions.FirstOrDefault();
var period = configs.GetHighestResolution().ToTimeSpan();
var time = (utcDateTime - period).ConvertFromUtc(configuration.DataTimeZone);
var tradeBar = new TradeBar(time, security.Symbol, last, high, low, last, 1000, period);
packets.Add(new DataFeedPacket(security, configuration, new List<BaseData> { tradeBar }));
}
if (packets.Count > 0)
{
yield return timeSliceFactory.Create(utcDateTime, packets, changes, new Dictionary<Universe, BaseDataCollection>()).Slice;
}
}
}
/// <summary>
/// Set up the HistoryProvider for algorithm
/// </summary>
protected void SetUpHistoryProvider()
{
Algorithm.HistoryProvider = new SubscriptionDataReaderHistoryProvider();
Algorithm.HistoryProvider.Initialize(new HistoryProviderInitializeParameters(
null,
null,
TestGlobals.DataProvider,
TestGlobals.DataCacheProvider,
TestGlobals.MapFileProvider,
TestGlobals.FactorFileProvider,
null,
false,
new DataPermissionManager(),
Algorithm.ObjectStore,
Algorithm.Settings));
}
/// <summary>
/// Gets the maximum number of slice objects to generate
/// </summary>
protected virtual int MaxSliceCount => 360;
private List<DateTime> GetSliceDateTimes(int maxCount)
{
var i = 0;
var sliceDateTimes = new List<DateTime>();
var utcDateTime = Algorithm.StartDate;
while (sliceDateTimes.Count < maxCount)
{
foreach (var kvp in Algorithm.Securities)
{
var security = kvp.Value;
var configs = Algorithm.SubscriptionManager.SubscriptionDataConfigService
.GetSubscriptionDataConfigs(security.Symbol);
var resolution = configs.GetHighestResolution().ToTimeSpan();
utcDateTime = utcDateTime.Add(resolution);
if (resolution == Time.OneDay && utcDateTime.TimeOfDay == TimeSpan.Zero)
{
utcDateTime = utcDateTime.AddHours(17);
}
var exchange = security.Exchange.Hours;
var extendedMarket = configs.IsExtendedMarketHours();
var localDateTime = utcDateTime.ConvertFromUtc(exchange.TimeZone);
if (exchange.IsOpen(localDateTime, extendedMarket))
{
sliceDateTimes.Add(utcDateTime);
}
i++;
}
}
return sliceDateTimes;
}
private bool TryCreateModel(Language language, out IAlphaModel model)
{
model = default(IAlphaModel);
switch (language)
{
case Language.CSharp:
model = CreateCSharpAlphaModel();
return true;
case Language.Python:
Algorithm.SetPandasConverter();
model = CreatePythonAlphaModel();
return true;
default:
return false;
}
}
}
}
@@ -0,0 +1,99 @@
/*
* 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 System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using static System.FormattableString;
namespace QuantConnect.Tests.Algorithm.Framework.Alphas
{
[TestFixture]
public class ConstantAlphaModelTests : CommonAlphaModelTests
{
private InsightType _type = InsightType.Price;
private InsightDirection _direction = InsightDirection.Up;
private TimeSpan _period = Time.OneDay;
private double? _magnitude = 0.025;
private double? _confidence = null;
protected override IAlphaModel CreateCSharpAlphaModel() => new ConstantAlphaModel(_type, _direction, _period, _magnitude, _confidence);
protected override IAlphaModel CreatePythonAlphaModel()
{
using (Py.GIL())
{
dynamic model = Py.Import("ConstantAlphaModel").GetAttr("ConstantAlphaModel");
var instance = model(_type, _direction, _period, _magnitude, _confidence);
return new AlphaModelPythonWrapper(instance);
}
}
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void ConstructorWithWeightOnlySetsWeightCorrectly(Language language)
{
IAlphaModel alpha;
if (language == Language.CSharp)
{
alpha = new ConstantAlphaModel(InsightType.Price, InsightDirection.Up, TimeSpan.FromDays(1), weight: 0.1);
}
else
{
using (Py.GIL())
{
var testModule = PyModule.FromString("test_module",
@"
from AlgorithmImports import *
def test_constructor():
model = ConstantAlphaModel(InsightType.Price, InsightDirection.Up, timedelta(1), weight=0.1)
return model
");
alpha = testModule.GetAttr("test_constructor").Invoke().As<ConstantAlphaModel>();
}
}
var magnitude = GetPrivateField(alpha, "_magnitude");
var confidence = GetPrivateField(alpha, "_confidence");
var weight = GetPrivateField(alpha, "_weight");
Assert.IsNull(magnitude);
Assert.IsNull(confidence);
Assert.AreEqual(0.1, weight);
}
private static object GetPrivateField(object obj, string fieldName)
{
var field = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
return field?.GetValue(obj);
}
protected override IEnumerable<Insight> ExpectedInsights()
{
return Enumerable.Range(0, 360).Select(x => new Insight(Symbols.SPY, _period, _type, _direction, _magnitude, _confidence));
}
protected override string GetExpectedModelName(IAlphaModel model)
{
return Invariant($"{nameof(ConstantAlphaModel)}({_type},{_direction},{_period},{_magnitude})");
}
}
}
@@ -0,0 +1,137 @@
/*
* 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 System;
using System.Collections.Generic;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Util;
using QuantConnect.Tests.Common.Data.UniverseSelection;
namespace QuantConnect.Tests.Algorithm.Framework.Alphas
{
[TestFixture]
public class EmaCrossAlphaModelTests : CommonAlphaModelTests
{
protected override IAlphaModel CreateCSharpAlphaModel() => new EmaCrossAlphaModel();
protected override IAlphaModel CreatePythonAlphaModel()
{
using (Py.GIL())
{
dynamic model = Py.Import("EmaCrossAlphaModel").GetAttr("EmaCrossAlphaModel");
var instance = model();
return new AlphaModelPythonWrapper(instance);
}
}
protected override IEnumerable<Insight> ExpectedInsights()
{
var period = TimeSpan.FromDays(12);
return new[]
{
Insight.Price(Symbols.SPY, period, InsightDirection.Down),
Insight.Price(Symbols.SPY, period, InsightDirection.Up)
};
}
protected override string GetExpectedModelName(IAlphaModel model)
{
return $"{nameof(EmaCrossAlphaModel)}(12,26,Daily)";
}
[Test]
public void WarmsUpProperly()
{
SetUpHistoryProvider();
Algorithm.SetStartDate(2013, 10, 08);
Algorithm.SetUniverseSelection(new ManualUniverseSelectionModel());
// Create a EmaCrossAlphaModel for the test
var model = new TestEmaCrossAlphaModel();
// Set the alpha model
Algorithm.SetAlpha(model);
Algorithm.SetUniverseSelection(new ManualUniverseSelectionModel());
var changes = SecurityChangesTests.CreateNonInternal(AddedSecurities, RemovedSecurities);
Algorithm.OnFrameworkSecuritiesChanged(changes);
// Get the dictionary of macd indicators
var symbolData = model.GetSymbolData();
// Check the symbolData dictionary is not empty
Assert.NotZero(symbolData.Count);
// Check all EmaCross indicators from the alpha are ready and have at least
// one datapoint
foreach (var item in symbolData)
{
var fast = item.Value.Fast;
var slow = item.Value.Slow;
Assert.IsTrue(fast.IsReady);
Assert.NotZero(fast.Samples);
Assert.IsTrue(slow.IsReady);
Assert.NotZero(slow.Samples);
}
}
[Test]
public void PythonVersionWarmsUpProperly()
{
using (Py.GIL())
{
SetUpHistoryProvider();
Algorithm.SetStartDate(2013, 10, 08);
Algorithm.SetUniverseSelection(new ManualUniverseSelectionModel());
// Create and set alpha model
dynamic model = Py.Import("EmaCrossAlphaModel").GetAttr("EmaCrossAlphaModel");
var instance = model();
Algorithm.SetAlpha(instance);
var changes = SecurityChangesTests.CreateNonInternal(AddedSecurities, RemovedSecurities);
Algorithm.OnFrameworkSecuritiesChanged(changes);
// Get the dictionary of ema cross indicators
var symbolData = instance.symbol_data_by_symbol;
// Check the dictionary is not empty
Assert.NotZero(symbolData.Length());
// Check all Ema Cross indicators from the alpha are ready and have at least
// one datapoint
foreach (var item in symbolData)
{
var fast = symbolData[item].fast;
var slow = symbolData[item].slow;
Assert.IsTrue(fast.IsReady.IsTrue());
Assert.NotZero(((PyObject)fast.Samples).GetAndDispose<int>());
Assert.IsTrue(slow.IsReady.IsTrue());
Assert.NotZero(((PyObject)slow.Samples).GetAndDispose<int>());
}
}
}
}
}
@@ -0,0 +1,270 @@
/*
* 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.Linq;
using NUnit.Framework;
using System.Collections.Generic;
using QuantConnect.Algorithm.Framework.Alphas;
namespace QuantConnect.Tests.Algorithm.Framework.Alphas
{
[TestFixture]
public class InsightCollectionTests
{
private static readonly DateTime _referenceTime = new DateTime(2019, 1, 1);
[Test]
public void InsightCollectionShouldBeAbleToBeConvertedToListWithoutStackOverflow()
{
var insightCollection = new InsightCollection
{
new Insight(Symbols.AAPL, new TimeSpan(1, 0, 0, 0), InsightType.Price, InsightDirection.Up)
{
CloseTimeUtc = new DateTime(2019, 1, 1),
},
new Insight(Symbols.AAPL, new TimeSpan(1, 0, 0, 0), InsightType.Volatility, InsightDirection.Up)
{
CloseTimeUtc = new DateTime(2019, 1, 2),
}
};
Assert.DoesNotThrow(() => insightCollection.OrderBy(x => x.CloseTimeUtc).ToList());
}
[Test]
public void HasActiveInsights()
{
var collection = new InsightCollection();
Assert.IsFalse(collection.HasActiveInsights(Symbols.AAPL, DateTime.MinValue));
collection.AddRange(GetTestInsight());
Assert.IsFalse(collection.HasActiveInsights(Symbols.AAPL, DateTime.MaxValue));
Assert.IsTrue(collection.HasActiveInsights(Symbols.AAPL, _referenceTime));
}
[Test]
public void GetNextExpiryTime()
{
var collection = new InsightCollection();
Assert.AreEqual(null, collection.GetNextExpiryTime());
collection.AddRange(GetTestInsight());
Assert.AreEqual(_referenceTime, collection.GetNextExpiryTime());
var nextDay = _referenceTime.AddDays(1);
Assert.AreEqual(1, collection.RemoveExpiredInsights(nextDay).Count);
Assert.AreEqual(nextDay, collection.GetNextExpiryTime());
}
[Test]
public void TryGetValue()
{
var collection = new InsightCollection();
Assert.IsFalse(collection.TryGetValue(Symbols.AAPL, out var _));
collection.AddRange(GetTestInsight());
Assert.IsTrue(collection.TryGetValue(Symbols.AAPL, out var insights));
Assert.AreEqual(2, insights.Count);
Assert.AreEqual(2, insights.Count(insight => insight.Symbol == Symbols.AAPL));
}
[Test]
public void KeyNotFoundException()
{
var collection = new InsightCollection();
Assert.Throws<KeyNotFoundException>(() =>
{
var insight = collection[Symbols.AAPL];
});
}
[Test]
public void Contains()
{
var collection = new InsightCollection();
var insights = GetTestInsight();
collection.AddRange(insights);
foreach (var insight in insights)
{
Assert.IsTrue(collection.Contains(insight));
Assert.IsTrue(collection.ContainsKey(insight.Symbol));
}
Assert.IsFalse(collection.ContainsKey(Symbols.BTCEUR));
var anotherInsight = new Insight(Symbols.BTCEUR, new TimeSpan(1, 0, 0, 0), InsightType.Price, InsightDirection.Up);
Assert.IsFalse(collection.Contains(anotherInsight));
}
[Test]
public void Addition()
{
var collection = new InsightCollection();
var insight = new Insight(Symbols.AAPL, new TimeSpan(1, 0, 0, 0), InsightType.Price, InsightDirection.Up) { CloseTimeUtc = _referenceTime };
collection.Add(insight);
collection.Add(new Insight(Symbols.SPY, new TimeSpan(1, 0, 0, 0), InsightType.Price, InsightDirection.Up) { CloseTimeUtc = _referenceTime });
collection.Add(new Insight(Symbols.IBM, new TimeSpan(1, 0, 0, 0), InsightType.Price, InsightDirection.Down) { CloseTimeUtc = _referenceTime.AddDays(-1) });
var beforeExpiration = insight.CloseTimeUtc.AddDays(-1);
Assert.AreEqual(3, collection.Count);
Assert.IsTrue(collection.TryGetValue(Symbols.AAPL, out var insightInCollection));
Assert.IsTrue(collection.HasActiveInsights(Symbols.AAPL, beforeExpiration));
Assert.AreEqual(insight, insightInCollection.Single());
Assert.AreEqual(insight, collection[Symbols.AAPL].Single());
Assert.AreEqual(3, collection.Count);
Assert.AreEqual(3, collection.GetActiveInsights(beforeExpiration).Count);
Assert.AreEqual(3, collection.GetInsights().Count);
Assert.AreEqual(insight, collection.GetInsights(x => insight == x).Single());
Assert.AreEqual(0, collection.GetActiveInsights(_referenceTime.AddYears(1)).Count);
Assert.AreEqual(3, collection.TotalCount);
}
[Test]
public void GetInsights()
{
var collection = new InsightCollection();
var insights = GetTestInsight();
collection.AddRange(insights);
Assert.AreEqual(5, collection.Count);
collection.RemoveInsights(x => x == insights[0]);
Assert.AreEqual(4, collection.GetInsights().Count);
Assert.AreEqual(4, collection.Count);
}
[Test]
public void Removal()
{
var collection = new InsightCollection();
var insights = GetTestInsight();
collection.AddRange(insights);
var insightCount = collection.Count;
foreach (var insight in insights)
{
Assert.IsTrue(collection.Remove(insight));
Assert.AreEqual(--insightCount, collection.Count);
}
// readd the first insight
var firstInsight = insights[0];
collection.Add(firstInsight);
Assert.AreEqual(1, collection.Count);
// we only remove 'firstInsight' from the global collection
collection.RemoveInsights(x => x == firstInsight);
Assert.AreEqual(4, collection.GetInsights().Count);
Assert.AreEqual(0, collection.Count);
Assert.AreEqual(6, collection.TotalCount);
}
[Test]
public void ExpiredRemoval()
{
var collection = new InsightCollection();
var insights = GetTestInsight();
collection.AddRange(insights);
Assert.AreEqual(5, collection.Count);
Assert.AreEqual(0, collection.RemoveExpiredInsights(_referenceTime.AddDays(-1)).Count);
// expire 1 insight
Assert.AreEqual(insights[0], collection.RemoveExpiredInsights(_referenceTime.AddDays(1)).Single());
// expire 2 insights
Assert.AreEqual(2, collection.RemoveExpiredInsights(_referenceTime.AddDays(2)).Count);
Assert.AreEqual(2, collection.Count);
Assert.AreEqual(5, collection.TotalCount);
}
[Test]
public void IndexAccess()
{
var collection = new InsightCollection();
collection.AddRange(GetTestInsight());
collection[Symbols.AAPL] = null;
Assert.AreEqual(3, collection.Count);
var insight = new Insight(Symbols.AAPL, new TimeSpan(1, 0, 0, 0), InsightType.Price, InsightDirection.Up) { CloseTimeUtc = new DateTime(2019, 1, 1) };
var insight2 = new Insight(Symbols.AAPL, new TimeSpan(1, 0, 0, 0), InsightType.Price, InsightDirection.Up) { CloseTimeUtc = new DateTime(2019, 1, 2) };
collection[Symbols.AAPL] = new() { insight, insight2 };
Assert.AreEqual(5, collection.Count);
collection[Symbols.AAPL] = null;
Assert.AreEqual(3, collection.Count);
Assert.AreEqual(7, collection.TotalCount);
}
[Test]
public void AddRange()
{
var collection = new InsightCollection();
var insights = GetTestInsight();
collection.AddRange(insights);
Assert.AreEqual(5, collection.Count);
foreach (var insight in insights)
{
Assert.IsTrue(collection.Contains(insight));
Assert.IsTrue(collection.ContainsKey(insight.Symbol));
}
Assert.AreEqual(5, collection.TotalCount);
}
[Test]
public void ClearSymbols()
{
var collection = new InsightCollection();
collection.AddRange(GetTestInsight());
collection.Clear(Array.Empty<Symbol>());
Assert.AreEqual(5, collection.Count);
collection.Clear(new[] { Symbols.AAPL });
Assert.AreEqual(3, collection.Count);
Assert.IsTrue(collection.ContainsKey(Symbols.SPY));
Assert.IsTrue(collection.ContainsKey(Symbols.IBM));
Assert.IsFalse(collection.ContainsKey(Symbols.AAPL));
Assert.AreEqual(5, collection.TotalCount);
}
private static List<Insight> GetTestInsight()
{
var insight = new Insight(Symbols.AAPL, new TimeSpan(1, 0, 0, 0), InsightType.Price, InsightDirection.Up) { CloseTimeUtc = _referenceTime };
var insight2 = new Insight(Symbols.AAPL, new TimeSpan(1, 0, 0, 0), InsightType.Price, InsightDirection.Up) { CloseTimeUtc = _referenceTime.AddDays(1) };
var insight3 = new Insight(Symbols.SPY, new TimeSpan(1, 0, 0, 0), InsightType.Price, InsightDirection.Up) { CloseTimeUtc = _referenceTime.AddDays(1) };
var insight4 = new Insight(Symbols.SPY, new TimeSpan(1, 0, 0, 0), InsightType.Price, InsightDirection.Down) { CloseTimeUtc = _referenceTime.AddMonths(1) };
var insight5 = new Insight(Symbols.IBM, new TimeSpan(1, 0, 0, 0), InsightType.Price, InsightDirection.Down) { CloseTimeUtc = _referenceTime.AddMonths(1) };
return new List<Insight> { insight, insight2, insight3, insight4, insight5 };
}
}
}
@@ -0,0 +1,136 @@
/*
* 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.Linq;
using NUnit.Framework;
using QuantConnect.Tests.Engine.DataFeeds;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Alphas.Analysis;
namespace QuantConnect.Tests.Algorithm.Framework.Alphas
{
[TestFixture]
public class InsightManagerTests
{
private static readonly DateTime _utcNow = new (2019, 1, 1);
[TestCase(false)]
[TestCase(true)]
public void ExpireSameTime(bool useCancelApi)
{
var algorithm = new AlgorithmStub();
algorithm.SetDateTime(_utcNow);
var insightManager = new InsightManager(algorithm);
insightManager.AddRange(GetInsights());
Assert.IsTrue(insightManager.All(insight => insight.IsActive(_utcNow)));
if (useCancelApi)
{
insightManager.Cancel(new[] { Symbols.IBM, Symbols.SPY });
}
else
{
insightManager.Expire(new[] { Symbols.IBM, Symbols.SPY });
}
Assert.IsTrue(insightManager[Symbols.IBM].All(insight => insight.IsExpired(algorithm.UtcTime) && insight.Period == TimeSpan.FromSeconds(-1)));
Assert.IsTrue(insightManager[Symbols.SPY].All(insight => insight.IsExpired(algorithm.UtcTime) && insight.Period == TimeSpan.FromSeconds(-1)));
Assert.IsTrue(insightManager[Symbols.AAPL].All(insight => insight.IsActive(algorithm.UtcTime)));
}
[TestCase(false)]
[TestCase(true)]
public void ExpireBySymbol(bool useCancelApi)
{
var algorithm = new AlgorithmStub();
algorithm.SetDateTime(_utcNow);
var insightManager = new InsightManager(algorithm);
insightManager.AddRange(GetInsights());
Assert.IsTrue(insightManager.All(insight => insight.IsActive(_utcNow)));
algorithm.SetDateTime(algorithm.UtcTime.AddMinutes(1));
if (useCancelApi)
{
insightManager.Cancel(new[] { Symbols.IBM, Symbols.SPY });
}
else
{
insightManager.Expire(new[] { Symbols.IBM, Symbols.SPY });
}
var expectedPeriod = Time.OneMinute.Subtract(Time.OneSecond);
Assert.IsTrue(insightManager[Symbols.IBM].All(insight => insight.IsExpired(algorithm.UtcTime) && insight.Period == expectedPeriod));
Assert.IsTrue(insightManager[Symbols.SPY].All(insight => insight.IsExpired(algorithm.UtcTime) && insight.Period == expectedPeriod));
Assert.IsTrue(insightManager[Symbols.AAPL].All(insight => insight.IsActive(algorithm.UtcTime)));
}
[TestCase(false)]
[TestCase(true)]
public void ExpireByInsight(bool useCancelApi)
{
var algorithm = new AlgorithmStub();
algorithm.SetDateTime(_utcNow);
var insights = GetInsights();
var insightManager = new InsightManager(algorithm);
insightManager.AddRange(insights);
Assert.IsTrue(insightManager.All(insight => insight.IsActive(_utcNow)));
algorithm.SetDateTime(algorithm.UtcTime.AddMinutes(1));
if (useCancelApi)
{
insightManager.Cancel(new[] { insights[2], insights[3] });
}
else
{
insightManager.Expire(new[] { insights[2], insights[3] });
}
var expectedPeriod = Time.OneMinute.Subtract(Time.OneSecond);
Assert.IsTrue(insightManager[Symbols.IBM].All(insight => insight.IsExpired(algorithm.UtcTime) && insight.Period == expectedPeriod));
Assert.AreEqual(1, insightManager[Symbols.SPY].Count(insight => insight.IsExpired(algorithm.UtcTime) && insight.Period == expectedPeriod));
Assert.AreEqual(1, insightManager[Symbols.SPY].Count(insight => insight.IsActive(algorithm.UtcTime)));
Assert.IsTrue(insightManager[Symbols.AAPL].All(insight => insight.IsActive(algorithm.UtcTime)));
}
private static Insight[] GetInsights()
{
return new[] {
new Insight(Symbols.AAPL, new TimeSpan(1, 0, 0, 0), InsightType.Price, InsightDirection.Up)
{
GeneratedTimeUtc = _utcNow,
CloseTimeUtc = _utcNow.AddDays(1),
},
new Insight(Symbols.SPY, new TimeSpan(2, 0, 0, 0), InsightType.Volatility, InsightDirection.Up)
{
GeneratedTimeUtc = _utcNow,
CloseTimeUtc = _utcNow.AddDays(2),
},
new Insight(Symbols.SPY, new TimeSpan(3, 0, 0, 0), InsightType.Volatility, InsightDirection.Up)
{
GeneratedTimeUtc = _utcNow,
CloseTimeUtc = _utcNow.AddDays(3),
},
new Insight(Symbols.IBM, new TimeSpan(4, 0, 0, 0), InsightType.Volatility, InsightDirection.Up)
{
GeneratedTimeUtc = _utcNow,
CloseTimeUtc = _utcNow.AddDays(4),
} };
}
}
}
@@ -0,0 +1,127 @@
/*
* 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 System;
using System.Collections.Generic;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Tests.Common.Data.UniverseSelection;
using QuantConnect.Util;
namespace QuantConnect.Tests.Algorithm.Framework.Alphas
{
[TestFixture]
public class MacdAlphaModelTests : CommonAlphaModelTests
{
protected override IAlphaModel CreateCSharpAlphaModel() => new MacdAlphaModel();
protected override IAlphaModel CreatePythonAlphaModel()
{
using (Py.GIL())
{
dynamic model = Py.Import("MacdAlphaModel").GetAttr("MacdAlphaModel");
var instance = model();
return new AlphaModelPythonWrapper(instance);
}
}
protected override IEnumerable<Insight> ExpectedInsights()
{
var period = TimeSpan.FromDays(12);
return new[]
{
Insight.Price(Symbols.SPY, period, InsightDirection.Up),
Insight.Price(Symbols.SPY, period, InsightDirection.Down),
Insight.Price(Symbols.SPY, period, InsightDirection.Up)
};
}
protected override string GetExpectedModelName(IAlphaModel model)
{
return $"{nameof(MacdAlphaModel)}(12,26,9,Exponential,Daily)";
}
[Test]
public void MacdAlphaModelWarmsUpProperly()
{
SetUpHistoryProvider();
Algorithm.SetStartDate(2013, 10, 08);
// Create a MacdAlphaModel for the test
var model = new TestMacdAlphaModel();
// Set the alpha model
Algorithm.SetAlpha(model);
Algorithm.SetUniverseSelection(new ManualUniverseSelectionModel());
var changes = SecurityChangesTests.CreateNonInternal(AddedSecurities, RemovedSecurities);
Algorithm.OnFrameworkSecuritiesChanged(changes);
// Get the dictionary of macd indicators
var symbolData = model.GetSymbolData();
// Check the symbolData dictionary is not empty
Assert.NotZero(symbolData.Count);
// Check all MACD indicators from the alpha are ready and have at least
// one datapoint
foreach (var item in symbolData)
{
var macd = item.Value.MACD;
Assert.IsTrue(macd.IsReady);
Assert.NotZero(macd.Samples);
}
}
[Test]
public void PythonMacdAlphaModelWarmsUpProperly()
{
using (Py.GIL())
{
SetUpHistoryProvider();
Algorithm.SetStartDate(2013, 10, 08);
Algorithm.SetUniverseSelection(new ManualUniverseSelectionModel());
// Create and set alpha model
dynamic model = Py.Import("MacdAlphaModel").GetAttr("MacdAlphaModel");
var instance = model();
Algorithm.SetAlpha(instance);
var changes = SecurityChangesTests.CreateNonInternal(AddedSecurities, RemovedSecurities);
Algorithm.OnFrameworkSecuritiesChanged(changes);
// Get the dictionary of macd indicators
var symbolData = instance.symbolData;
// Check the dictionary is not empty
Assert.NotZero(symbolData.Length());
// Check all MACD indicators from the alpha are ready and have at least
// one datapoint
foreach (var item in symbolData)
{
var macd = symbolData[item].MACD;
Assert.IsTrue(macd.IsReady.IsTrue());
Assert.NotZero(((PyObject)macd.Samples).GetAndDispose<int>());
}
}
}
}
}
@@ -0,0 +1,53 @@
/*
* 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 System;
using System.Collections.Generic;
namespace QuantConnect.Tests.Algorithm.Framework.Alphas
{
[TestFixture]
public class RsiAlphaModelTests : CommonAlphaModelTests
{
protected override IAlphaModel CreateCSharpAlphaModel() => new RsiAlphaModel();
protected override IAlphaModel CreatePythonAlphaModel()
{
using (Py.GIL())
{
dynamic model = Py.Import("RsiAlphaModel").GetAttr("RsiAlphaModel");
var instance = model();
return new AlphaModelPythonWrapper(instance);
}
}
protected override IEnumerable<Insight> ExpectedInsights()
{
var period = TimeSpan.FromDays(14);
foreach (var direction in new[] { InsightDirection.Up, InsightDirection.Down })
{
yield return Insight.Price(Symbols.SPY, period, direction);
}
}
protected override string GetExpectedModelName(IAlphaModel model)
{
return $"{nameof(RsiAlphaModel)}(14,Daily)";
}
}
}
@@ -0,0 +1,301 @@
/*
* 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 Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using NUnit.Framework;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Alphas.Serialization;
namespace QuantConnect.Tests.Algorithm.Framework.Alphas.Serialization
{
[TestFixture, Parallelizable(ParallelScope.All)]
public class InsightJsonConverterTests
{
private JsonSerializerSettings _serializerSettings = new()
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy
{
ProcessDictionaryKeys = false,
OverrideSpecifiedNames = true
}
}
};
[Test]
public void DeserializesInsightWithoutScore()
{
var jObject = JObject.Parse(jsonNoScoreBackwardsCompatible);
var result = JsonConvert.DeserializeObject<Insight>(jsonNoScoreBackwardsCompatible);
Assert.AreEqual(jObject["id"].Value<string>(), result.Id.ToStringInvariant("N"));
Assert.AreEqual(jObject["source-model"].Value<string>(), result.SourceModel);
Assert.AreEqual(jObject["group-id"]?.Value<string>(), result.GroupId?.ToStringInvariant("N"));
Assert.AreEqual(jObject["created-time"].Value<double>(), Time.DateTimeToUnixTimeStamp(result.GeneratedTimeUtc), 5e-4);
Assert.AreEqual(jObject["close-time"].Value<double>(), Time.DateTimeToUnixTimeStamp(result.CloseTimeUtc), 5e-4);
Assert.AreEqual(jObject["symbol"].Value<string>(), result.Symbol.ID.ToString());
Assert.AreEqual(jObject["ticker"].Value<string>(), result.Symbol.Value);
Assert.AreEqual(jObject["type"].Value<string>(), result.Type.ToLower());
Assert.AreEqual(jObject["reference"].Value<decimal>(), result.ReferenceValue);
Assert.AreEqual(jObject["direction"].Value<string>(), result.Direction.ToLower());
Assert.AreEqual(jObject["period"].Value<double>(), result.Period.TotalSeconds);
Assert.AreEqual(jObject["magnitude"].Value<double>(), result.Magnitude);
Assert.AreEqual(null, result.Confidence);
// default values for scores
Assert.AreEqual(false, result.Score.IsFinalScore);
Assert.AreEqual(0, result.ReferenceValueFinal);
Assert.AreEqual(0, result.Score.Magnitude);
Assert.AreEqual(0, result.Score.Direction);
}
[Test]
public void DeserializesInsightWithScore()
{
var jObject = JObject.Parse(jsonWithScoreBackwardsCompatible);
var result = JsonConvert.DeserializeObject<Insight>(jsonWithScoreBackwardsCompatible);
Assert.AreEqual(jObject["id"].Value<string>(), result.Id.ToStringInvariant("N"));
Assert.AreEqual(jObject["source-model"].Value<string>(), result.SourceModel);
Assert.AreEqual(jObject["group-id"]?.Value<string>(), result.GroupId?.ToStringInvariant("N"));
Assert.AreEqual(jObject["created-time"].Value<double>(), Time.DateTimeToUnixTimeStamp(result.GeneratedTimeUtc), 5e-4);
Assert.AreEqual(jObject["close-time"].Value<double>(), Time.DateTimeToUnixTimeStamp(result.CloseTimeUtc), 5e-4);
Assert.AreEqual(jObject["symbol"].Value<string>(), result.Symbol.ID.ToString());
Assert.AreEqual(jObject["ticker"].Value<string>(), result.Symbol.Value);
Assert.AreEqual(jObject["type"].Value<string>(), result.Type.ToLower());
Assert.AreEqual(jObject["reference"].Value<decimal>(), result.ReferenceValue);
Assert.AreEqual(jObject["direction"].Value<string>(), result.Direction.ToLower());
Assert.AreEqual(jObject["period"].Value<double>(), result.Period.TotalSeconds);
Assert.AreEqual(jObject["magnitude"].Value<double>(), result.Magnitude);
Assert.AreEqual(null, result.Confidence);
Assert.AreEqual(true, result.Score.IsFinalScore);
Assert.AreEqual(jObject["score-magnitude"].Value<double>(), result.Score.Magnitude);
Assert.AreEqual(jObject["score-direction"].Value<double>(), result.Score.Direction);
Assert.AreEqual(jObject["reference-final"].Value<decimal>(), result.ReferenceValueFinal);
}
[TestCase(true)]
[TestCase(false)]
public void SerializesInsightWithoutScore(bool backwardsCompatible)
{
var serializedInsight = JsonConvert.DeserializeObject<SerializedInsight>(backwardsCompatible ? jsonNoScoreBackwardsCompatible : jsonNoScore2);
var insight = Insight.FromSerializedInsight(serializedInsight);
var result = JsonConvert.SerializeObject(insight, Formatting.None, _serializerSettings);
Assert.AreEqual(jsonNoScore2, result);
}
[TestCase(true)]
[TestCase(false)]
public void SerializesInsightWithScore(bool backwardsCompatible)
{
var serializedInsight = JsonConvert.DeserializeObject<SerializedInsight>(backwardsCompatible ? jsonWithScoreBackwardsCompatible : jsonWithScore);
var insight = Insight.FromSerializedInsight(serializedInsight);
var result = JsonConvert.SerializeObject(insight, Formatting.None, _serializerSettings);
Assert.AreEqual(jsonWithScore, result);
}
[Test]
public void SerializesOldInsightWithMissingCreatedTime()
{
var serializedInsight = JsonConvert.DeserializeObject<SerializedInsight>(jsonWithMissingCreatedTimeBackwardsCompatible);
var insight = Insight.FromSerializedInsight(serializedInsight);
var result = JsonConvert.SerializeObject(insight, Formatting.None, _serializerSettings);
Assert.AreEqual(serializedInsight.CreatedTime, serializedInsight.GeneratedTime);
Assert.AreEqual(jsonWithExpectedOutputFromMissingCreatedTimeValue, result);
}
[TestCase(true)]
[TestCase(false)]
public void SerializesInsightWithTag(bool backwardsCompatible)
{
var serializedInsight = JsonConvert.DeserializeObject<SerializedInsight>(backwardsCompatible ? jsonWithTagBackwardsCompatible : jsonWithTag);
var insight = Insight.FromSerializedInsight(serializedInsight);
var result = JsonConvert.SerializeObject(insight, Formatting.None, _serializerSettings);
Assert.AreEqual(jsonWithTag, result);
}
[TestCase(true)]
[TestCase(false)]
public void SerializesInsightWithoutTag(bool backwardsCompatible)
{
var serializedInsight = JsonConvert.DeserializeObject<SerializedInsight>(backwardsCompatible ? jsonWithoutTagBackwardsCompatible : jsonWithoutTag);
var insight = Insight.FromSerializedInsight(serializedInsight);
var result = JsonConvert.SerializeObject(insight, Formatting.None, _serializerSettings);
Assert.AreEqual(jsonWithoutTag, result);
}
[Test]
public void DeserializesInsightWithTag()
{
var jObject = JObject.Parse(jsonWithTagBackwardsCompatible);
var result = JsonConvert.DeserializeObject<Insight>(jsonWithTagBackwardsCompatible);
Assert.AreEqual(jObject["tag"].Value<string>(), result.Tag);
}
[Test]
public void DeserializesInsightWithoutTag()
{
var result = JsonConvert.DeserializeObject<Insight>(jsonWithoutTagBackwardsCompatible);
Assert.IsNull(result.Tag);
}
private string jsonNoScore2 = @"{""id"":""e02be50f56a8496b9ba995d19a904ada"",""groupId"":null,""sourceModel"":""mySourceModel-1"",""generatedTime"":1520711961.00055,
""createdTime"":1520711961.00055,""closeTime"":1520711961.00055,""symbol"":""BTCUSD XJ"",""ticker"":""BTCUSD"",""type"":""price"",""reference"":9143.53,""referenceValueFinal"":0.0,
""direction"":""up"",""period"":5.0,""magnitude"":""0.025"",""confidence"":null,""weight"":null,""scoreIsFinal"":false,""scoreMagnitude"":""0"",""scoreDirection"":""0"",
""estimatedValue"":""0"",""tag"":null}".ReplaceLineEndings(string.Empty);
private const string jsonNoScoreBackwardsCompatible =
"{" +
"\"id\":\"e02be50f56a8496b9ba995d19a904ada\"," +
"\"group-id\":null," +
"\"source-model\":\"mySourceModel-1\"," +
"\"generated-time\":1520711961.00055," +
"\"created-time\":1520711961.00055," +
"\"close-time\":1520711961.00055," +
"\"symbol\":\"BTCUSD XJ\"," +
"\"ticker\":\"BTCUSD\"," +
"\"type\":\"price\"," +
"\"reference\":9143.53," +
"\"reference-final\":0.0," +
"\"direction\":\"up\"," +
"\"period\":5.0," +
"\"magnitude\":\"0.025\"," +
"\"confidence\":null," +
"\"weight\":null," +
"\"score-final\":false," +
"\"score-magnitude\":\"0\"," +
"\"score-direction\":\"0\"," +
"\"estimated-value\":\"0\"," +
"\"tag\":null}";
private string jsonWithScore = @"{""id"":""e02be50f56a8496b9ba995d19a904ada"",""groupId"":""a02be50f56a8496b9ba995d19a904ada"",""sourceModel"":""mySourceModel-1"",
""generatedTime"":1520711961.00055,""createdTime"":1520711961.00055,""closeTime"":1520711961.00055,""symbol"":""BTCUSD XJ"",""ticker"":""BTCUSD"",""type"":""price"",
""reference"":9143.53,""referenceValueFinal"":9243.53,""direction"":""up"",""period"":5.0,""magnitude"":""0.025"",""confidence"":null,""weight"":null,
""scoreIsFinal"":true,""scoreMagnitude"":""1"",""scoreDirection"":""1"",""estimatedValue"":""1113.2484"",""tag"":null}".ReplaceLineEndings(string.Empty);
private const string jsonWithScoreBackwardsCompatible =
"{" +
"\"id\":\"e02be50f56a8496b9ba995d19a904ada\"," +
"\"group-id\":\"a02be50f56a8496b9ba995d19a904ada\"," +
"\"source-model\":\"mySourceModel-1\"," +
"\"generated-time\":1520711961.00055," +
"\"created-time\":1520711961.00055," +
"\"close-time\":1520711961.00055," +
"\"symbol\":\"BTCUSD XJ\"," +
"\"ticker\":\"BTCUSD\"," +
"\"type\":\"price\"," +
"\"reference\":9143.53," +
"\"reference-final\":9243.53," +
"\"direction\":\"up\"," +
"\"period\":5.0," +
"\"magnitude\":\"0.025\"," +
"\"confidence\":null," +
"\"weight\":null," +
"\"score-final\":true," +
"\"score-magnitude\":\"1\"," +
"\"score-direction\":\"1\"," +
"\"estimated-value\":\"1113.2484\"," +
"\"tag\":null}";
private const string jsonWithMissingCreatedTimeBackwardsCompatible =
"{" +
"\"id\":\"e02be50f56a8496b9ba995d19a904ada\"," +
"\"group-id\":\"a02be50f56a8496b9ba995d19a904ada\"," +
"\"source-model\":\"mySourceModel-1\"," +
"\"generated-time\":1520711961.00055," +
"\"close-time\":1520711961.00055," +
"\"symbol\":\"BTCUSD XJ\"," +
"\"ticker\":\"BTCUSD\"," +
"\"type\":\"price\"," +
"\"reference\":9143.53," +
"\"reference-final\":9243.53," +
"\"direction\":\"up\"," +
"\"period\":5.0," +
"\"magnitude\":0.025," +
"\"confidence\":null," +
"\"weight\":null," +
"\"score-final\":true," +
"\"score-magnitude\":\"1\"," +
"\"score-direction\":\"1\"," +
"\"estimated-value\":\"1113.2484\"," +
"\"tag\":null}";
private string jsonWithExpectedOutputFromMissingCreatedTimeValue = @"{""id"":""e02be50f56a8496b9ba995d19a904ada"",""groupId"":""a02be50f56a8496b9ba995d19a904ada"",
""sourceModel"":""mySourceModel-1"",""generatedTime"":1520711961.00055,""createdTime"":1520711961.00055,""closeTime"":1520711961.00055,""symbol"":""BTCUSD XJ"",""ticker"":
""BTCUSD"",""type"":""price"",""reference"":9143.53,""referenceValueFinal"":9243.53,""direction"":""up"",""period"":5.0,""magnitude"":""0.025"",""confidence"":null,
""weight"":null,""scoreIsFinal"":true,""scoreMagnitude"":""1"",""scoreDirection"":""1"",""estimatedValue"":""1113.2484"",""tag"":null}".ReplaceLineEndings(string.Empty);
private string jsonWithTag = @"{""id"":""e02be50f56a8496b9ba995d19a904ada"",""groupId"":""a02be50f56a8496b9ba995d19a904ada"",""sourceModel"":""mySourceModel-1"",
""generatedTime"":1520711961.00055,""createdTime"":1520711961.00055,""closeTime"":1520711961.00055,""symbol"":""BTCUSD XJ"",""ticker"":""BTCUSD"",""type"":
""price"",""reference"":9143.53,""referenceValueFinal"":9243.53,""direction"":""up"",""period"":5.0,""magnitude"":null,""confidence"":null,""weight"":null,
""scoreIsFinal"":true,""scoreMagnitude"":""1"",""scoreDirection"":""1"",""estimatedValue"":""1113.2484"",""tag"":""additional information""}".ReplaceLineEndings(string.Empty);
private const string jsonWithTagBackwardsCompatible =
"{" +
"\"id\":\"e02be50f56a8496b9ba995d19a904ada\"," +
"\"group-id\":\"a02be50f56a8496b9ba995d19a904ada\"," +
"\"source-model\":\"mySourceModel-1\"," +
"\"generated-time\":1520711961.00055," +
"\"created-time\":1520711961.00055," +
"\"close-time\":1520711961.00055," +
"\"symbol\":\"BTCUSD XJ\"," +
"\"ticker\":\"BTCUSD\"," +
"\"type\":\"price\"," +
"\"reference\":9143.53," +
"\"reference-final\":9243.53," +
"\"direction\":\"up\"," +
"\"period\":5.0," +
"\"magnitude\":null," +
"\"confidence\":null," +
"\"weight\":null," +
"\"score-final\":true," +
"\"score-magnitude\":\"1\"," +
"\"score-direction\":\"1\"," +
"\"estimated-value\":\"1113.2484\"," +
"\"tag\":\"additional information\"}";
private string jsonWithoutTag = @"{""id"":""e02be50f56a8496b9ba995d19a904ada"",""groupId"":""a02be50f56a8496b9ba995d19a904ada"",
""sourceModel"":""mySourceModel-1"",""generatedTime"":1520711961.00055,""createdTime"":1520711961.00055,""closeTime"":1520711961.00055,""symbol"":""BTCUSD XJ"",
""ticker"":""BTCUSD"",""type"":""price"",""reference"":9143.53,""referenceValueFinal"":9243.53,""direction"":""up"",""period"":5.0,""magnitude"":null,
""confidence"":null,""weight"":null,""scoreIsFinal"":true,""scoreMagnitude"":""1"",""scoreDirection"":""1"",""estimatedValue"":""1113.2484"",""tag"":null}".ReplaceLineEndings(string.Empty);
private const string jsonWithoutTagBackwardsCompatible =
"{" +
"\"id\":\"e02be50f56a8496b9ba995d19a904ada\"," +
"\"group-id\":\"a02be50f56a8496b9ba995d19a904ada\"," +
"\"source-model\":\"mySourceModel-1\"," +
"\"generated-time\":1520711961.00055," +
"\"created-time\":1520711961.00055," +
"\"close-time\":1520711961.00055," +
"\"symbol\":\"BTCUSD XJ\"," +
"\"ticker\":\"BTCUSD\"," +
"\"type\":\"price\"," +
"\"reference\":9143.53," +
"\"reference-final\":9243.53," +
"\"direction\":\"up\"," +
"\"period\":5.0," +
"\"magnitude\":null," +
"\"confidence\":null," +
"\"weight\":null," +
"\"score-final\":true," +
"\"score-magnitude\":\"1\"," +
"\"score-direction\":\"1\"," +
"\"estimated-value\":\"1113.2484\"," +
"\"tag\":null}";
}
}
@@ -0,0 +1,32 @@
/*
* 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 QuantConnect.Algorithm.Framework.Alphas;
using System.Collections.Generic;
namespace QuantConnect.Tests.Algorithm.Framework.Alphas
{
class TestEmaCrossAlphaModel : EmaCrossAlphaModel
{
/// <summary>
/// Get the _symbolDataBySymbol dictionary from EmaCrossAlphaModel
/// </summary>
/// <returns>_symbolDataBySymbol dictionary from EmaCrossAlphaModel</returns>
public Dictionary<Symbol, SymbolData> GetSymbolData()
{
return SymbolDataBySymbol;
}
}
}
@@ -0,0 +1,32 @@
/*
* 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 QuantConnect.Algorithm.Framework.Alphas;
using System.Collections.Generic;
namespace QuantConnect.Tests.Algorithm.Framework.Alphas
{
class TestMacdAlphaModel: MacdAlphaModel
{
/// <summary>
/// Get the _symbolData dictionary from MacdAlphaModel
/// </summary>
/// <returns>_symbolData dictionary from MacdAlphaModel</returns>
public Dictionary<Symbol, SymbolData> GetSymbolData()
{
return _symbolData;
}
}
}
@@ -0,0 +1,311 @@
/*
* 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 Moq;
using NodaTime;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Interfaces;
using QuantConnect.Lean.Engine.Results;
using QuantConnect.Lean.Engine.TransactionHandlers;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Python;
using QuantConnect.Securities;
using QuantConnect.Tests.Common.Data.UniverseSelection;
using QuantConnect.Tests.Engine.DataFeeds;
namespace QuantConnect.Tests.Algorithm.Framework.Execution
{
[TestFixture]
public class ImmediateExecutionModelTests
{
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void OrdersAreNotSubmittedWhenNoTargetsToExecute(Language language)
{
var actualOrdersSubmitted = new List<SubmitOrderRequest>();
var orderProcessor = new Mock<IOrderProcessor>();
orderProcessor.Setup(m => m.Process(It.IsAny<SubmitOrderRequest>()))
.Returns((OrderTicket)null)
.Callback((OrderRequest request) => actualOrdersSubmitted.Add((SubmitOrderRequest)request));
var algorithm = new AlgorithmStub();
algorithm.Transactions.SetOrderProcessor(orderProcessor.Object);
var model = GetExecutionModel(language);
algorithm.SetExecution(model);
var changes = SecurityChangesTests.CreateNonInternal(Enumerable.Empty<Security>(), Enumerable.Empty<Security>());
model.OnSecuritiesChanged(algorithm, changes);
model.Execute(algorithm, new IPortfolioTarget[0]);
Assert.AreEqual(0, actualOrdersSubmitted.Count);
}
[TestCase(Language.CSharp, new[] { 270d, 260d, 250d }, 0, 1, 10)]
[TestCase(Language.CSharp, new[] { 270d, 260d, 250d }, 3, 1, 7)]
[TestCase(Language.Python, new[] { 270d, 260d, 250d }, 0, 1, 10)]
[TestCase(Language.Python, new[] { 270d, 260d, 250d }, 3, 1, 7)]
public void OrdersAreSubmittedImmediatelyForTargetsToExecute(
Language language,
double[] historicalPrices,
decimal openOrdersQuantity,
int expectedOrdersSubmitted,
decimal expectedTotalQuantity)
{
var time = new DateTime(2018, 8, 2, 16, 0, 0);
var historyProvider = new Mock<IHistoryProvider>();
historyProvider.Setup(m => m.GetHistory(It.IsAny<IEnumerable<HistoryRequest>>(), It.IsAny<DateTimeZone>()))
.Returns(historicalPrices.Select((x, i) =>
new Slice(time.AddMinutes(i),
new List<BaseData>
{
new TradeBar
{
Time = time.AddMinutes(i),
Symbol = Symbols.AAPL,
Open = Convert.ToDecimal(x),
High = Convert.ToDecimal(x),
Low = Convert.ToDecimal(x),
Close = Convert.ToDecimal(x),
Volume = 100m
}
}, time.AddMinutes(i))));
var algorithm = new AlgorithmStub();
algorithm.SetHistoryProvider(historyProvider.Object);
algorithm.SetDateTime(time.AddMinutes(5));
var security = algorithm.AddEquity(Symbols.AAPL.Value);
security.SetMarketPrice(new TradeBar { Value = 250 });
algorithm.SetFinishedWarmingUp();
var orderProcessor = GetAndSetBrokerageTransactionHandler(algorithm, out var brokerage);
try
{
var openOrderRequest = new SubmitOrderRequest(OrderType.Market, SecurityType.Equity, Symbols.AAPL, openOrdersQuantity, 0, 0, DateTime.MinValue, "");
openOrderRequest.SetOrderId(1);
var order = Order.CreateOrder(openOrderRequest);
orderProcessor.AddOpenOrder(order, algorithm);
var model = GetExecutionModel(language, false);
algorithm.SetExecution(model);
var changes = SecurityChangesTests.CreateNonInternal(new[] { security }, Enumerable.Empty<Security>());
model.OnSecuritiesChanged(algorithm, changes);
var targets = new IPortfolioTarget[] { new PortfolioTarget(Symbols.AAPL, 10) };
model.Execute(algorithm, targets);
orderProcessor.ProcessSynchronousEvents();
Assert.AreEqual(expectedOrdersSubmitted + 1, orderProcessor.GetOpenOrders().Count);
var executionOrder = orderProcessor.GetOpenOrders().OrderByDescending(o => o.Id).First();
Assert.AreEqual(expectedTotalQuantity, executionOrder.Quantity);
Assert.AreEqual(algorithm.UtcTime, executionOrder.Time);
}
finally
{
orderProcessor.Exit();
brokerage.Dispose();
}
}
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void PartiallyFilledOrdersAreTakenIntoAccount(Language language)
{
var algorithm = new AlgorithmStub();
var security = algorithm.AddEquity(Symbols.AAPL.Value);
security.SetMarketPrice(new TradeBar { Value = 250 });
algorithm.SetFinishedWarmingUp();
var orderProcessor = GetAndSetBrokerageTransactionHandler(algorithm, out var brokerage);
try
{
var openOrderRequest = new SubmitOrderRequest(OrderType.Market, SecurityType.Equity, Symbols.AAPL, 100, 0, 0, DateTime.MinValue, "");
openOrderRequest.SetOrderId(1);
var order = Order.CreateOrder(openOrderRequest);
orderProcessor.AddOpenOrder(order, algorithm);
brokerage.OnOrderEvent(new OrderEvent(order.Id, order.Symbol, DateTime.MinValue, OrderStatus.PartiallyFilled, OrderDirection.Buy, 250, 70, OrderFee.Zero));
var model = GetExecutionModel(language);
algorithm.SetExecution(model);
var changes = SecurityChangesTests.CreateNonInternal(Enumerable.Empty<Security>(), Enumerable.Empty<Security>());
model.OnSecuritiesChanged(algorithm, changes);
var targets = new IPortfolioTarget[] { new PortfolioTarget(Symbols.AAPL, 80) };
model.Execute(algorithm, targets);
orderProcessor.ProcessSynchronousEvents();
Assert.AreEqual(2, orderProcessor.OrdersCount);
// Remaining quantity for partially filled order = 100 - 70 = 30
// Holdings from partially filled order = 70
// Quantity submitted = target - holdings - remaining open orders quantity = 80 - 70 - 30 = -20
Assert.AreEqual(-20, orderProcessor.GetOpenOrders().OrderByDescending(o => o.Id).First().Quantity);
}
finally
{
orderProcessor.Exit();
brokerage.Dispose();
}
}
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void NonFilledAsyncOrdersAreTakenIntoAccount(Language language)
{
var algorithm = new AlgorithmStub();
var security = algorithm.AddEquity(Symbols.AAPL.Value);
security.SetMarketPrice(new TradeBar { Value = 250 });
algorithm.SetFinishedWarmingUp();
var orderProcessor = GetAndSetBrokerageTransactionHandler(algorithm, out var brokerage);
try
{
var model = GetExecutionModel(language, true);
algorithm.SetExecution(model);
var changes = SecurityChangesTests.CreateNonInternal(Enumerable.Empty<Security>(), Enumerable.Empty<Security>());
model.OnSecuritiesChanged(algorithm, changes);
var targetQuantity = 80;
var targets = new IPortfolioTarget[] { new PortfolioTarget(Symbols.AAPL, targetQuantity) };
model.Execute(algorithm, targets);
orderProcessor.ProcessSynchronousEvents();
Assert.AreEqual(1, orderProcessor.OrdersCount);
// Quantity submitted = 80
Assert.AreEqual(targetQuantity, orderProcessor.GetOpenOrders().First().Quantity);
var newTargetQuantity = 100;
var newTargets = new IPortfolioTarget[] { new PortfolioTarget(Symbols.AAPL, newTargetQuantity) };
model.Execute(algorithm, newTargets);
orderProcessor.ProcessSynchronousEvents();
Assert.AreEqual(2, orderProcessor.OrdersCount);
// Remaining quantity for non-filled order = targetQuantity = 80
// Quantity submitted = newTargetQuantity - targetQuantity = 100 - 80 = 20
Assert.AreEqual(newTargetQuantity - targetQuantity, orderProcessor.GetOpenOrders().OrderByDescending(o => o.Id).First().Quantity);
}
finally
{
orderProcessor.Exit();
brokerage.Dispose();
}
}
[TestCase(Language.CSharp, -1)]
[TestCase(Language.Python, -1)]
[TestCase(Language.CSharp, 1)]
[TestCase(Language.Python, 1)]
public void LotSizeIsRespected(Language language, int side)
{
var algorithm = new AlgorithmStub();
algorithm.Settings.MinimumOrderMarginPortfolioPercentage = 0;
var security = algorithm.AddForex(Symbols.EURUSD.Value);
algorithm.Portfolio.SetCash("EUR", 1, 1);
security.SetMarketPrice(new TradeBar { Value = 250 });
algorithm.SetFinishedWarmingUp();
var orderProcessor = GetAndSetBrokerageTransactionHandler(algorithm, out var brokerage);
try
{
var model = GetExecutionModel(language);
algorithm.SetExecution(model);
model.Execute(algorithm,
new IPortfolioTarget[] { new PortfolioTarget(Symbols.EURUSD, security.SymbolProperties.LotSize * 1.5m * side) });
orderProcessor.ProcessSynchronousEvents();
var orders = orderProcessor.GetOrders().ToList();
Assert.AreEqual(1, orders.Count);
Assert.AreEqual(security.SymbolProperties.LotSize * side, orders.Single().Quantity);
}
finally
{
orderProcessor.Exit();
brokerage.Dispose();
}
}
[Test]
public void CustomPythonExecutionModelDoesNotRequireOnOrderEventMethod()
{
using var _ = Py.GIL();
const string pythonCode = @"
class CustomExecutionModel:
def execute(self, algorithm, targets):
pass
def on_securities_changed(self, algorithm, changes):
pass
";
using var module = PyModule.FromString("CustomExecutionModelModule", pythonCode);
using var instance = module.GetAttr("CustomExecutionModel").Invoke();
var model = new ExecutionModelPythonWrapper(instance);
Assert.DoesNotThrow(() => model.OnOrderEvent(new AlgorithmStub(), new OrderEvent()));
}
private static IExecutionModel GetExecutionModel(Language language, bool asynchronous = false)
{
if (language == Language.Python)
{
using (Py.GIL())
{
const string name = nameof(ImmediateExecutionModel);
var instance = Py.Import(name).GetAttr(name).Invoke(asynchronous);
return new ExecutionModelPythonWrapper(instance);
}
}
return new ImmediateExecutionModel(asynchronous);
}
internal static BrokerageTransactionHandler GetAndSetBrokerageTransactionHandler(IAlgorithm algorithm, out NullBrokerage brokerage)
{
brokerage = new NullBrokerage();
var orderProcessor = new BrokerageTransactionHandler();
orderProcessor.Initialize(algorithm, brokerage, new BacktestingResultHandler());
algorithm.Transactions.SetOrderProcessor(orderProcessor);
algorithm.Transactions.MarketOrderFillTimeout = TimeSpan.Zero;
return orderProcessor;
}
}
}
@@ -0,0 +1,241 @@
/*
* 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 Moq;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Tests.Common.Data.UniverseSelection;
using QuantConnect.Tests.Engine.DataFeeds;
namespace QuantConnect.Tests.Algorithm.Framework.Execution
{
[TestFixture]
public class SpreadExecutionModelTests
{
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void OrdersAreNotSubmittedWhenNoTargetsToExecute(Language language)
{
var actualOrdersSubmitted = new List<SubmitOrderRequest>();
var orderProcessor = new Mock<IOrderProcessor>();
orderProcessor.Setup(m => m.Process(It.IsAny<SubmitOrderRequest>()))
.Returns((OrderTicket)null)
.Callback((OrderRequest request) => actualOrdersSubmitted.Add((SubmitOrderRequest)request));
var algorithm = new QCAlgorithm();
algorithm.SetPandasConverter();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.Transactions.SetOrderProcessor(orderProcessor.Object);
var model = GetExecutionModel(language);
algorithm.SetExecution(model);
var changes = SecurityChangesTests.CreateNonInternal(Enumerable.Empty<Security>(), Enumerable.Empty<Security>());
model.OnSecuritiesChanged(algorithm, changes);
model.Execute(algorithm, new IPortfolioTarget[0]);
Assert.AreEqual(0, actualOrdersSubmitted.Count);
}
[TestCase(Language.CSharp, 240, 1, 10)]
[TestCase(Language.CSharp, 250, 0, 0)]
[TestCase(Language.Python, 240, 1, 10)]
[TestCase(Language.Python, 250, 0, 0)]
public void OrdersAreSubmittedWhenRequiredForTargetsToExecute(
Language language,
decimal currentPrice,
int expectedOrdersSubmitted,
decimal expectedTotalQuantity)
{
var time = new DateTime(2018, 8, 2, 14, 0, 0);
var algorithm = new QCAlgorithm();
algorithm.SetPandasConverter();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.SetDateTime(time.AddMinutes(5));
var security = algorithm.AddEquity(Symbols.AAPL.Value);
security.SetMarketPrice(new TradeBar { Value = 250 });
// pushing the ask higher will cause the spread the widen and no trade to happen
var ask = expectedOrdersSubmitted == 0 ? currentPrice * 1.1m : currentPrice;
security.SetMarketPrice(new QuoteBar
{
Time = time,
Symbol = Symbols.AAPL,
Ask = new Bar(ask, ask, ask, ask),
Bid = new Bar(currentPrice, currentPrice, currentPrice, currentPrice)
});
algorithm.SetFinishedWarmingUp();
var orderProcessor = ImmediateExecutionModelTests.GetAndSetBrokerageTransactionHandler(algorithm, out var brokerage);
try
{
var model = GetExecutionModel(language);
algorithm.SetExecution(model);
var changes = SecurityChangesTests.CreateNonInternal(new[] { security }, Enumerable.Empty<Security>());
model.OnSecuritiesChanged(algorithm, changes);
var targets = new IPortfolioTarget[] { new PortfolioTarget(Symbols.AAPL, 10) };
model.Execute(algorithm, targets);
orderProcessor.ProcessSynchronousEvents();
var orders = orderProcessor.GetOrders().ToList();
Assert.AreEqual(expectedOrdersSubmitted, orders.Count);
Assert.AreEqual(expectedTotalQuantity, orders.Sum(x => x.Quantity));
if (expectedOrdersSubmitted == 1)
{
var order = orders[0];
Assert.AreEqual(expectedTotalQuantity, order.Quantity);
Assert.AreEqual(algorithm.UtcTime, order.Time);
}
}
finally
{
orderProcessor.Exit();
brokerage.Dispose();
}
}
[TestCase(Language.CSharp, 1, 10, true)]
[TestCase(Language.Python, 1, 10, true)]
[TestCase(Language.CSharp, 0, 0, false)]
[TestCase(Language.Python, 0, 0, false)]
public void FillsOnTradesOnlyRespectingExchangeOpen(Language language, int expectedOrdersSubmitted, decimal expectedTotalQuantity, bool exchangeOpen)
{
var time = new DateTime(2018, 8, 2, 0, 0, 0);
if (exchangeOpen)
{
time = time.AddHours(14);
}
var algorithm = new QCAlgorithm();
algorithm.SetPandasConverter();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.SetDateTime(time.AddMinutes(5));
var security = algorithm.AddEquity(Symbols.AAPL.Value);
security.SetMarketPrice(new TradeBar { Value = 250 });
algorithm.SetFinishedWarmingUp();
var orderProcessor = ImmediateExecutionModelTests.GetAndSetBrokerageTransactionHandler(algorithm, out var brokerage);
try
{
var model = GetExecutionModel(language);
algorithm.SetExecution(model);
var changes = SecurityChangesTests.CreateNonInternal(new[] { security }, Enumerable.Empty<Security>());
model.OnSecuritiesChanged(algorithm, changes);
var targets = new IPortfolioTarget[] { new PortfolioTarget(Symbols.AAPL, 10) };
model.Execute(algorithm, targets);
orderProcessor.ProcessSynchronousEvents();
var orders = orderProcessor.GetOrders().ToList();
Assert.AreEqual(expectedOrdersSubmitted, orders.Count);
Assert.AreEqual(expectedTotalQuantity, orders.Sum(x => x.Quantity));
if (expectedOrdersSubmitted == 1)
{
var order = orders[0];
Assert.AreEqual(expectedTotalQuantity, order.Quantity);
Assert.AreEqual(algorithm.UtcTime, order.Time);
}
}
finally
{
orderProcessor.Exit();
brokerage.Dispose();
}
}
[TestCase(Language.CSharp, MarketDataType.TradeBar)]
[TestCase(Language.Python, MarketDataType.TradeBar)]
[TestCase(Language.CSharp, MarketDataType.QuoteBar)]
[TestCase(Language.Python, MarketDataType.QuoteBar)]
public void OnSecuritiesChangeDoesNotThrow(
Language language,
MarketDataType marketDataType)
{
var time = new DateTime(2018, 8, 2, 16, 0, 0);
Func<double, int, BaseData> func = (x, i) =>
{
var price = Convert.ToDecimal(x);
switch (marketDataType)
{
case MarketDataType.TradeBar:
return new TradeBar(time.AddMinutes(i), Symbols.AAPL, price, price, price, price, 100m);
case MarketDataType.QuoteBar:
var bar = new Bar(price, price, price, price);
return new QuoteBar(time.AddMinutes(i), Symbols.AAPL, bar, 10m, bar, 10m);
default:
throw new ArgumentException($"Invalid MarketDataType: {marketDataType}");
}
};
var algorithm = new QCAlgorithm();
algorithm.SetPandasConverter();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.SetDateTime(time.AddMinutes(5));
var security = algorithm.AddEquity(Symbols.AAPL.Value);
security.SetMarketPrice(new TradeBar { Value = 250 });
algorithm.SetFinishedWarmingUp();
var model = GetExecutionModel(language);
algorithm.SetExecution(model);
var changes = SecurityChangesTests.CreateNonInternal(new[] { security }, Enumerable.Empty<Security>());
Assert.DoesNotThrow(() => model.OnSecuritiesChanged(algorithm, changes));
}
private static IExecutionModel GetExecutionModel(Language language)
{
const decimal acceptingSpreadPercent = 0.005m;
if (language == Language.Python)
{
using (Py.GIL())
{
const string name = nameof(SpreadExecutionModel);
var instance = Py.Import(name).GetAttr(name).Invoke(acceptingSpreadPercent.ToPython());
return new ExecutionModelPythonWrapper(instance);
}
}
return new SpreadExecutionModel(acceptingSpreadPercent);
}
}
}
@@ -0,0 +1,205 @@
/*
* 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 Moq;
using NodaTime;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Interfaces;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Tests.Common.Data.UniverseSelection;
using QuantConnect.Tests.Engine.DataFeeds;
namespace QuantConnect.Tests.Algorithm.Framework.Execution
{
[TestFixture]
public class StandardDeviationExecutionModelTests
{
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void OrdersAreNotSubmittedWhenNoTargetsToExecute(Language language)
{
var actualOrdersSubmitted = new List<SubmitOrderRequest>();
var orderProcessor = new Mock<IOrderProcessor>();
orderProcessor.Setup(m => m.Process(It.IsAny<SubmitOrderRequest>()))
.Returns((OrderTicket)null)
.Callback((OrderRequest request) => actualOrdersSubmitted.Add((SubmitOrderRequest)request));
var algorithm = new QCAlgorithm();
algorithm.SetPandasConverter();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.Transactions.SetOrderProcessor(orderProcessor.Object);
var model = GetExecutionModel(language);
algorithm.SetExecution(model);
var changes = SecurityChangesTests.CreateNonInternal(Enumerable.Empty<Security>(), Enumerable.Empty<Security>());
model.OnSecuritiesChanged(algorithm, changes);
model.Execute(algorithm, new IPortfolioTarget[0]);
Assert.AreEqual(0, actualOrdersSubmitted.Count);
}
[TestCase(Language.CSharp, new[] { 270d, 260d, 250d }, 240, 1, 10)]
[TestCase(Language.CSharp, new[] { 250d, 250d, 250d }, 250, 0, 0)]
[TestCase(Language.Python, new[] { 270d, 260d, 250d }, 240, 1, 10)]
[TestCase(Language.Python, new[] { 250d, 250d, 250d }, 250, 0, 0)]
public void OrdersAreSubmittedWhenRequiredForTargetsToExecute(
Language language,
double[] historicalPrices,
decimal currentPrice,
int expectedOrdersSubmitted,
decimal expectedTotalQuantity)
{
var time = new DateTime(2018, 8, 2, 16, 0, 0);
var historyProvider = new Mock<IHistoryProvider>();
historyProvider.Setup(m => m.GetHistory(It.IsAny<IEnumerable<HistoryRequest>>(), It.IsAny<DateTimeZone>()))
.Returns(historicalPrices.Select((x,i) =>
new Slice(time.AddMinutes(i),
new List<BaseData>
{
new TradeBar
{
Time = time.AddMinutes(i),
Symbol = Symbols.AAPL,
Open = Convert.ToDecimal(x),
High = Convert.ToDecimal(x),
Low = Convert.ToDecimal(x),
Close = Convert.ToDecimal(x),
Volume = 100m
}
}, time.AddMinutes(i))));
var algorithm = new QCAlgorithm();
algorithm.SetPandasConverter();
algorithm.SetHistoryProvider(historyProvider.Object);
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.SetDateTime(time.AddMinutes(5));
var security = algorithm.AddEquity(Symbols.AAPL.Value);
security.SetMarketPrice(new TradeBar { Value = currentPrice });
algorithm.SetFinishedWarmingUp();
var orderProcessor = ImmediateExecutionModelTests.GetAndSetBrokerageTransactionHandler(algorithm, out var brokerage);
try
{
var model = GetExecutionModel(language);
algorithm.SetExecution(model);
var changes = SecurityChangesTests.CreateNonInternal(new[] { security }, Enumerable.Empty<Security>());
model.OnSecuritiesChanged(algorithm, changes);
var targets = new IPortfolioTarget[] { new PortfolioTarget(Symbols.AAPL, 10) };
model.Execute(algorithm, targets);
orderProcessor.ProcessSynchronousEvents();
var orders = orderProcessor.GetOrders().ToList();
Assert.AreEqual(expectedOrdersSubmitted, orders.Count);
Assert.AreEqual(expectedTotalQuantity, orders.Sum(x => x.Quantity));
if (expectedOrdersSubmitted == 1)
{
var order = orders[0];
Assert.AreEqual(expectedTotalQuantity, order.Quantity);
Assert.AreEqual(algorithm.UtcTime, order.Time);
}
}
finally
{
orderProcessor.Exit();
brokerage?.Dispose();
}
}
[TestCase(Language.CSharp, new[] { 270d, 260d, 250d }, MarketDataType.TradeBar)]
[TestCase(Language.Python, new[] { 250d, 250d, 250d }, MarketDataType.TradeBar)]
[TestCase(Language.CSharp, new[] { 270d, 260d, 250d }, MarketDataType.QuoteBar)]
[TestCase(Language.Python, new[] { 250d, 250d, 250d }, MarketDataType.QuoteBar)]
public void OnSecuritiesChangeDoesNotThrow(
Language language,
double[] historicalPrices,
MarketDataType marketDataType)
{
var time = new DateTime(2018, 8, 2, 16, 0, 0);
Func<double, int, BaseData> func = (x, i) =>
{
var price = Convert.ToDecimal(x);
switch (marketDataType)
{
case MarketDataType.TradeBar:
return new TradeBar(time.AddMinutes(i), Symbols.AAPL, price, price, price, price, 100m);
case MarketDataType.QuoteBar:
var bar = new Bar(price, price, price, price);
return new QuoteBar(time.AddMinutes(i), Symbols.AAPL, bar, 10m, bar, 10m);
default:
throw new ArgumentException($"Invalid MarketDataType: {marketDataType}");
}
};
var historyProvider = new Mock<IHistoryProvider>();
historyProvider.Setup(m => m.GetHistory(It.IsAny<IEnumerable<HistoryRequest>>(), It.IsAny<DateTimeZone>()))
.Returns(historicalPrices.Select((x, i) => new Slice(time.AddMinutes(i), new List<BaseData> { func(x, i) }, time.AddMinutes(i))));
var algorithm = new QCAlgorithm();
algorithm.SetPandasConverter();
algorithm.SetHistoryProvider(historyProvider.Object);
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.SetDateTime(time.AddMinutes(5));
var security = algorithm.AddEquity(Symbols.AAPL.Value);
security.SetMarketPrice(new TradeBar { Value = 250 });
algorithm.SetFinishedWarmingUp();
var model = GetExecutionModel(language);
algorithm.SetExecution(model);
var changes = SecurityChangesTests.CreateNonInternal(new[] { security }, Enumerable.Empty<Security>());
Assert.DoesNotThrow(() => model.OnSecuritiesChanged(algorithm, changes));
}
private static IExecutionModel GetExecutionModel(Language language)
{
const int period = 2;
const decimal deviations = 1.5m;
if (language == Language.Python)
{
using (Py.GIL())
{
const string name = nameof(StandardDeviationExecutionModel);
var instance = Py.Import(name).GetAttr(name).Invoke(period.ToPython(), deviations.ToPython());
return new ExecutionModelPythonWrapper(instance);
}
}
return new StandardDeviationExecutionModel(period, deviations);
}
}
}
@@ -0,0 +1,164 @@
/*
* 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 Moq;
using NodaTime;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Interfaces;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Tests.Common.Data.UniverseSelection;
using QuantConnect.Tests.Engine.DataFeeds;
using QuantConnect.Util;
namespace QuantConnect.Tests.Algorithm.Framework.Execution
{
[TestFixture]
public class VolumeWeightedAveragePriceExecutionModelTests
{
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void OrdersAreNotSubmittedWhenNoTargetsToExecute(Language language)
{
var actualOrdersSubmitted = new List<SubmitOrderRequest>();
var orderProcessor = new Mock<IOrderProcessor>();
orderProcessor.Setup(m => m.Process(It.IsAny<SubmitOrderRequest>()))
.Returns((OrderTicket)null)
.Callback((OrderRequest request) => actualOrdersSubmitted.Add((SubmitOrderRequest)request));
var algorithm = new QCAlgorithm();
algorithm.SetPandasConverter();
algorithm.Transactions.SetOrderProcessor(orderProcessor.Object);
var model = GetExecutionModel(language);
algorithm.SetExecution(model);
var changes = SecurityChangesTests.CreateNonInternal(Enumerable.Empty<Security>(), Enumerable.Empty<Security>());
model.OnSecuritiesChanged(algorithm, changes);
model.Execute(algorithm, new IPortfolioTarget[0]);
Assert.AreEqual(0, actualOrdersSubmitted.Count);
}
[TestCase(Language.CSharp, new[] { 270d, 260d, 250d }, 5000, 1, 10)]
[TestCase(Language.Python, new[] { 270d, 260d, 250d }, 5000, 1, 10)]
[TestCase(Language.CSharp, new[] { 270d, 260d, 250d }, 500, 1, 5)]
[TestCase(Language.Python, new[] { 270d, 260d, 250d }, 500, 1, 5)]
[TestCase(Language.CSharp, new[] { 270d, 260d, 250d }, 50, 0, 0)]
[TestCase(Language.Python, new[] { 270d, 260d, 250d }, 50, 0, 0)]
[TestCase(Language.CSharp, new[] { 230d, 240d, 250d }, 50000, 0, 0)]
[TestCase(Language.Python, new[] { 230d, 240d, 250d }, 50000, 0, 0)]
public void OrdersAreSubmittedWhenRequiredForTargetsToExecute(
Language language,
double[] historicalPrices,
decimal lastVolume,
int expectedOrdersSubmitted,
decimal expectedTotalQuantity)
{
var actualOrdersSubmitted = new List<SubmitOrderRequest>();
var time = new DateTime(2018, 8, 2, 16, 0, 0);
var historyProvider = new Mock<IHistoryProvider>();
historyProvider.Setup(m => m.GetHistory(It.IsAny<IEnumerable<HistoryRequest>>(), It.IsAny<DateTimeZone>()))
.Returns(historicalPrices.Select((x, i) =>
new Slice(time.AddMinutes(i),
new List<BaseData>
{
new TradeBar
{
Time = time.AddMinutes(i),
Symbol = Symbols.AAPL,
Open = Convert.ToDecimal(x),
High = Convert.ToDecimal(x),
Low = Convert.ToDecimal(x),
Close = Convert.ToDecimal(x),
Volume = 100m
}
}, time.AddMinutes(i))));
var algorithm = new QCAlgorithm();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.SetPandasConverter();
algorithm.SetHistoryProvider(historyProvider.Object);
algorithm.SetDateTime(time.AddMinutes(5));
var security = algorithm.AddEquity(Symbols.AAPL.Value);
security.SetMarketPrice(new TradeBar { Value = 250, Volume = lastVolume });
algorithm.SetFinishedWarmingUp();
var orderProcessor = ImmediateExecutionModelTests.GetAndSetBrokerageTransactionHandler(algorithm, out var brokerage);
try
{
var model = GetExecutionModel(language);
algorithm.SetExecution(model);
var changes = SecurityChangesTests.CreateNonInternal(new[] { security }, Enumerable.Empty<Security>());
model.OnSecuritiesChanged(algorithm, changes);
algorithm.History(new List<Symbol> { security.Symbol }, historicalPrices.Length, Resolution.Minute)
.PushThroughConsolidators(symbol => algorithm.Securities[symbol].Subscriptions.Single(s => s.TickType == LeanData.GetCommonTickType(SecurityType.Equity)).Consolidators.First());
var targets = new IPortfolioTarget[] { new PortfolioTarget(security.Symbol, 10) };
model.Execute(algorithm, targets);
orderProcessor.ProcessSynchronousEvents();
var orders = orderProcessor.GetOrders().ToList();
Assert.AreEqual(expectedOrdersSubmitted, orders.Count);
Assert.AreEqual(expectedTotalQuantity, orders.Sum(x => x.Quantity));
if (expectedOrdersSubmitted == 1)
{
var order = orders[0];
Assert.AreEqual(expectedTotalQuantity, order.Quantity);
Assert.AreEqual(algorithm.UtcTime, order.Time);
}
}
finally
{
orderProcessor.Exit();
brokerage?.Dispose();
}
}
private static IExecutionModel GetExecutionModel(Language language)
{
if (language == Language.Python)
{
using (Py.GIL())
{
const string name = nameof(VolumeWeightedAveragePriceExecutionModel);
var instance = Py.Import(name).GetAttr(name).Invoke();
return new ExecutionModelPythonWrapper(instance);
}
}
return new VolumeWeightedAveragePriceExecutionModel();
}
}
}
@@ -0,0 +1,246 @@
/*
* 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;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.UniverseSelection;
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
namespace QuantConnect.Tests.Algorithm.Framework
{
[TestFixture]
public class FrameworkModelsPythonInheritanceTests
{
[Test]
public void ManualUniverseSelectionModelCanBeInherited()
{
var code = @"
from clr import AddReference
AddReference('QuantConnect.Common')
from QuantConnect import Market, SecurityType, Symbol
from Selection.ManualUniverseSelectionModel import ManualUniverseSelectionModel
class MockUniverseSelectionModel(ManualUniverseSelectionModel):
def __init__(self):
super().__init__([Symbol.Create('SPY', SecurityType.Equity, Market.USA)])";
using (Py.GIL())
{
dynamic pyModel = PyModule.FromString(Guid.NewGuid().ToString(), code)
.GetAttr("MockUniverseSelectionModel");
var model = new UniverseSelectionModelPythonWrapper(pyModel());
var universes = model.CreateUniverses(new QCAlgorithm()).ToList();
Assert.AreEqual(1, universes.Count);
var universe = universes.First();
var symbols = universe.SelectSymbols(DateTime.Now, null).ToList();
Assert.AreEqual(1, symbols.Count);
var expected = Symbol.Create("SPY", SecurityType.Equity, Market.USA);
var symbol = symbols.First();
Assert.AreEqual(expected, symbol);
}
}
[Test]
public void FundamentalUniverseSelectionModelCanBeInherited()
{
var code = @"
from AlgorithmImports import *
from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel
class MockUniverseSelectionModel(FundamentalUniverseSelectionModel):
def __init__(self):
super().__init__(False)
def SelectCoarse(self, algorithm, coarse):
return [Symbol.Create('SPY', SecurityType.Equity, Market.USA)]";
using (Py.GIL())
{
dynamic pyModel = PyModule.FromString(Guid.NewGuid().ToString(), code)
.GetAttr("MockUniverseSelectionModel");
var model = new UniverseSelectionModelPythonWrapper(pyModel());
var universes = model.CreateUniverses(new QCAlgorithm()).ToList();
Assert.AreEqual(1, universes.Count);
var data = new BaseDataCollection();
data.Add(new CoarseFundamental());
var universe = universes.First();
var symbols = universe.SelectSymbols(DateTime.Now, data).ToList();
Assert.AreEqual(1, symbols.Count);
var expected = Symbol.Create("SPY", SecurityType.Equity, Market.USA);
var symbol = symbols.First();
Assert.AreEqual(expected, symbol);
}
}
[Test]
public void PythonCanInheritFromFundamentalUniverseSelectionModelAndOverrideMethods()
{
var code = @"
from AlgorithmImports import *
class MockUniverseSelectionModel(FundamentalUniverseSelectionModel):
def __init__(self):
super().__init__()
self.select_call_count = 0
self.select_coarse_call_count = 0
self.select_fine_call_count = 0
self.create_coarse_call_count = 0
def select(self, algorithm: QCAlgorithm, fundamental: list[Fundamental]) -> list[Symbol]:
self.select_call_count += 1
return [Futures.Metals.GOLD]
def select_coarse(self, algorithm, coarse):
self.select_coarse_call_count += 1
self.select_coarse_called = True
filtered = [c for c in coarse if c.price > 10]
return [c.symbol for c in filtered[:2]]
def select_fine(self, algorithm, fine):
self.select_fine_call_count += 1
self.select_fine_called = True
return [f.symbol for f in fine[:2]]
def create_coarse_fundamental_universe(self, algorithm):
self.create_coarse_call_count += 1
self.create_coarse_called = True
return CoarseFundamentalUniverse(
algorithm.universe_settings,
self.custom_coarse_selector
)
def custom_coarse_selector(self, coarse):
filtered = [c for c in coarse if c.has_fundamental_data]
return [c.symbol for c in filtered[:5]]";
using (Py.GIL())
{
dynamic pyModel = PyModule.FromString(Guid.NewGuid().ToString(), code)
.GetAttr("MockUniverseSelectionModel");
PyObject pyModelInstance = pyModel();
var algorithm = new QCAlgorithm();
var model = new FundamentalUniverseSelectionModel();
model.SetPythonInstance(pyModelInstance);
// call the create_universes method
var universes = model.CreateUniverses(algorithm).ToList();
var universe = universes.First();
var selectedSymbols = universe.SelectSymbols(DateTime.Now, new BaseDataCollection()).ToList();
int selectCount = pyModelInstance.GetAttr("select_call_count").As<int>();
Assert.Greater(selectCount, 0);
// call the select method
model.Select(algorithm, new List<Fundamental>());
selectCount = pyModelInstance.GetAttr("select_call_count").As<int>();
Assert.Greater(selectCount, 1);
// call the select_coarse method
model.SelectCoarse(algorithm, new List<CoarseFundamental>());
int selectCoarseCount = pyModelInstance.GetAttr("select_coarse_call_count").As<int>();
Assert.Greater(selectCoarseCount, 0);
// call the select_fine method
model.SelectFine(algorithm, new List<FineFundamental>());
int selectFineCount = pyModelInstance.GetAttr("select_fine_call_count").As<int>();
Assert.Greater(selectFineCount, 0);
// call the create_coarse_fundamental_universe method
model.CreateCoarseFundamentalUniverse(algorithm);
int createCoarseCount = pyModelInstance.GetAttr("create_coarse_call_count").As<int>();
Assert.Greater(createCoarseCount, 0);
}
}
[Test]
public void PythonCanInheritFromBasePairsTradingAlphaModelAndOverrideMethods()
{
var code = @"
from AlgorithmImports import *
class MockPairsTradingAlphaModel(BasePairsTradingAlphaModel):
def __init__(self):
super().__init__()
self.has_passed_test_call_count = 0
def has_passed_test(self, algorithm, asset1, asset2):
self.has_passed_test_call_count += 1
return False";
using (Py.GIL())
{
var pyModel = PyModule.FromString("test", code).GetAttr("MockPairsTradingAlphaModel");
var pyInstance = pyModel.Invoke();
var algorithm = new QCAlgorithm();
var model = new BasePairsTradingAlphaModel();
model.SetPythonInstance(pyInstance);
var security1 = new Equity(
Symbols.SPY,
SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork),
new Cash("USD", 1m, 1m),
SymbolProperties.GetDefault("USD"),
ErrorCurrencyConverter.Instance,
new RegisteredSecurityDataTypesProvider(),
new SecurityCache()
);
var security2 = new Equity(
Symbols.AAPL,
SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork),
new Cash("USD", 1m, 1m),
SymbolProperties.GetDefault("USD"),
ErrorCurrencyConverter.Instance,
new RegisteredSecurityDataTypesProvider(),
new SecurityCache()
);
var changes = SecurityChanges.Create(
new List<Security> { security1, security2 },
new List<Security>(),
new List<Security>(),
new List<Security>()
);
model.OnSecuritiesChanged(new QCAlgorithm(), changes);
int hasPassedTestCallCount = pyInstance.GetAttr("has_passed_test_call_count").As<int>();
Assert.AreEqual(1, hasPassedTestCallCount);
}
}
}
}
+482
View File
@@ -0,0 +1,482 @@
/*
* 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.Linq;
using Newtonsoft.Json;
using NUnit.Framework;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Securities;
namespace QuantConnect.Tests.Algorithm.Framework
{
[TestFixture, Parallelizable(ParallelScope.All)]
public class InsightTests
{
[Test]
public void HasReferenceTypeEqualitySemantics()
{
var one = Insight.Price(Symbols.SPY, Time.OneSecond, InsightDirection.Up);
var two = Insight.Price(Symbols.SPY, Time.OneSecond, InsightDirection.Up);
Assert.AreNotEqual(one, two);
Assert.AreEqual(one, one);
Assert.AreEqual(two, two);
}
[Test]
public void SurvivesRoundTripSerializationUsingJsonConvert()
{
var time = new DateTime(2000, 01, 02, 03, 04, 05, 06);
var insight = new Insight(time, Symbols.SPY, Time.OneMinute, InsightType.Volatility, InsightDirection.Up, 1, 2, "source-model", 1);
Insight.Group(insight);
insight.ReferenceValueFinal = 10;
var serialized = JsonConvert.SerializeObject(insight);
var deserialized = JsonConvert.DeserializeObject<Insight>(serialized);
Assert.AreEqual(insight.CloseTimeUtc, deserialized.CloseTimeUtc);
Assert.AreEqual(insight.Confidence, deserialized.Confidence);
Assert.AreEqual(insight.Direction, deserialized.Direction);
Assert.AreEqual(insight.EstimatedValue, deserialized.EstimatedValue);
Assert.AreEqual(insight.GeneratedTimeUtc, deserialized.GeneratedTimeUtc);
Assert.AreEqual(insight.GroupId, deserialized.GroupId);
Assert.AreEqual(insight.Id, deserialized.Id);
Assert.AreEqual(insight.Magnitude, deserialized.Magnitude);
Assert.AreEqual(insight.Period, deserialized.Period);
Assert.AreEqual(insight.SourceModel, deserialized.SourceModel);
Assert.AreEqual(insight.Score.Direction, deserialized.Score.Direction);
Assert.AreEqual(insight.Score.Magnitude, deserialized.Score.Magnitude);
Assert.AreEqual(insight.Score.UpdatedTimeUtc, deserialized.Score.UpdatedTimeUtc);
Assert.AreEqual(insight.Score.IsFinalScore, deserialized.Score.IsFinalScore);
Assert.AreEqual(insight.Symbol, deserialized.Symbol);
Assert.AreEqual(insight.Type, deserialized.Type);
Assert.AreEqual(insight.Weight, deserialized.Weight);
Assert.AreEqual(insight.ReferenceValueFinal, deserialized.ReferenceValueFinal);
}
[Test]
public void CancelInsight()
{
var time = new DateTime(2000, 01, 02, 03, 04, 05, 06);
var insight = new Insight(time, Symbols.SPY, Time.OneMinute, InsightType.Volatility, InsightDirection.Up, 1, 2, "source-model");
insight.Cancel(time.AddMinutes(1));
Assert.AreEqual(TimeSpan.FromSeconds(59), insight.Period);
insight.Cancel(time.AddDays(1));
Assert.AreEqual(TimeSpan.FromSeconds(59), insight.Period);
}
[Test]
public void SerializationUsingJsonConvertTrimsEstimatedValue()
{
var time = new DateTime(2000, 01, 02, 03, 04, 05, 06);
var insight = new Insight(time, Symbols.SPY, Time.OneMinute, InsightType.Volatility, InsightDirection.Up, 1, 2, "source-model");
insight.EstimatedValue = 0.00001m;
insight.Score.SetScore(InsightScoreType.Direction, 0.00001, DateTime.UtcNow);
insight.Score.SetScore(InsightScoreType.Magnitude, 0.00001, DateTime.UtcNow);
var serialized = JsonConvert.SerializeObject(insight);
var deserialized = JsonConvert.DeserializeObject<Insight>(serialized);
Assert.AreEqual(0, deserialized.EstimatedValue);
Assert.AreEqual(0, deserialized.Score.Direction);
Assert.AreEqual(0, deserialized.Score.Magnitude);
}
[Test]
public void SurvivesRoundTripCopy()
{
var time = new DateTime(2000, 01, 02, 03, 04, 05, 06);
var original = new Insight(time, Symbols.SPY, Time.OneMinute, InsightType.Volatility, InsightDirection.Up, 1, 2, "source-model", 1);
original.ReferenceValueFinal = 10;
Insight.Group(original);
var copy = original.Clone();
Assert.AreEqual(original.CloseTimeUtc, copy.CloseTimeUtc);
Assert.AreEqual(original.Confidence, copy.Confidence);
Assert.AreEqual(original.Direction, copy.Direction);
Assert.AreEqual(original.EstimatedValue, copy.EstimatedValue);
Assert.AreEqual(original.GeneratedTimeUtc, copy.GeneratedTimeUtc);
Assert.AreEqual(original.GroupId, copy.GroupId);
Assert.AreEqual(original.Id, copy.Id);
Assert.AreEqual(original.Magnitude, copy.Magnitude);
Assert.AreEqual(original.Period, copy.Period);
Assert.AreEqual(original.SourceModel, copy.SourceModel);
Assert.AreEqual(original.Score.Direction, copy.Score.Direction);
Assert.AreEqual(original.Score.Magnitude, copy.Score.Magnitude);
Assert.AreEqual(original.Score.UpdatedTimeUtc, copy.Score.UpdatedTimeUtc);
Assert.AreEqual(original.Score.IsFinalScore, copy.Score.IsFinalScore);
Assert.AreEqual(original.Symbol, copy.Symbol);
Assert.AreEqual(original.Type, copy.Type);
Assert.AreEqual(original.Weight, copy.Weight);
Assert.AreEqual(original.ReferenceValueFinal, copy.ReferenceValueFinal);
}
[Test]
public void GroupAssignsGroupId()
{
var insight1 = Insight.Price(Symbols.SPY, Time.OneMinute, InsightDirection.Up);
var insight2 = Insight.Price(Symbols.SPY, Time.OneMinute, InsightDirection.Up);
var group = Insight.Group(insight1, insight2).ToList();
foreach (var member in group)
{
Assert.IsTrue(member.GroupId.HasValue);
}
var groupId = insight1.GroupId.Value;
foreach (var member in group)
{
Assert.AreEqual(groupId, member.GroupId);
}
}
[Test]
public void GroupThrowsExceptionIfInsightAlreadyHasGroupId()
{
var insight1 = Insight.Price(Symbols.SPY, Time.OneMinute, InsightDirection.Up);
Insight.Group(insight1);
Assert.That(() => Insight.Group(insight1), Throws.InvalidOperationException);
}
[Test]
[TestCase(Resolution.Tick, 1)]
[TestCase(Resolution.Tick, 10)]
[TestCase(Resolution.Tick, 100)]
[TestCase(Resolution.Second, 1)]
[TestCase(Resolution.Second, 10)]
[TestCase(Resolution.Second, 100)]
[TestCase(Resolution.Minute, 1)]
[TestCase(Resolution.Minute, 10)]
[TestCase(Resolution.Minute, 100)]
[TestCase(Resolution.Hour, 1)]
[TestCase(Resolution.Hour, 10)]
[TestCase(Resolution.Hour, 100)]
[TestCase(Resolution.Daily, 1)]
[TestCase(Resolution.Daily, 10)]
[TestCase(Resolution.Daily, 100)]
public void SetPeriodAndCloseTimeUsingResolutionBarCount(Resolution resolution, int barCount)
{
var generatedTimeUtc = new DateTime(2018, 08, 06, 13, 31, 0).ConvertToUtc(TimeZones.NewYork);
var symbol = Symbols.SPY;
var insight = Insight.Price(symbol, resolution, barCount, InsightDirection.Up);
insight.GeneratedTimeUtc = generatedTimeUtc;
var exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType);
insight.SetPeriodAndCloseTime(exchangeHours);
var expectedCloseTime = Insight.ComputeCloseTime(exchangeHours, insight.GeneratedTimeUtc, resolution, barCount);
Assert.AreEqual(expectedCloseTime, insight.CloseTimeUtc);
Assert.AreEqual(expectedCloseTime - generatedTimeUtc, insight.Period);
}
[Test]
[TestCase(Resolution.Second, 1)]
[TestCase(Resolution.Second, 10)]
[TestCase(Resolution.Second, 100)]
[TestCase(Resolution.Minute, 1)]
[TestCase(Resolution.Minute, 10)]
[TestCase(Resolution.Minute, 100)]
[TestCase(Resolution.Hour, 1)]
[TestCase(Resolution.Hour, 10)]
[TestCase(Resolution.Hour, 100)]
[TestCase(Resolution.Daily, 1)]
[TestCase(Resolution.Daily, 10)]
[TestCase(Resolution.Daily, 100)]
public void SetPeriodAndCloseTimeUsingPeriod(Resolution resolution, int barCount)
{
var period = resolution.ToTimeSpan().Multiply(barCount);
var generatedTimeUtc = new DateTime(2018, 08, 06, 13, 31, 0).ConvertToUtc(TimeZones.NewYork);
var symbol = Symbols.SPY;
var insight = Insight.Price(symbol, period, InsightDirection.Up);
insight.GeneratedTimeUtc = generatedTimeUtc;
var exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType);
insight.SetPeriodAndCloseTime(exchangeHours);
Assert.AreEqual(Time.Max(period, Time.OneSecond), insight.Period);
var expectedCloseTime = Insight.ComputeCloseTime(exchangeHours, insight.GeneratedTimeUtc, period);
Assert.AreEqual(expectedCloseTime, insight.CloseTimeUtc);
}
[Test]
[TestCase(Resolution.Tick, 1)]
[TestCase(Resolution.Tick, 10)]
[TestCase(Resolution.Tick, 100)]
[TestCase(Resolution.Second, 1)]
[TestCase(Resolution.Second, 10)]
[TestCase(Resolution.Second, 100)]
[TestCase(Resolution.Minute, 1)]
[TestCase(Resolution.Minute, 10)]
[TestCase(Resolution.Minute, 100)]
[TestCase(Resolution.Hour, 1)]
[TestCase(Resolution.Hour, 10)]
[TestCase(Resolution.Hour, 100)]
[TestCase(Resolution.Daily, 1)]
[TestCase(Resolution.Daily, 10)]
[TestCase(Resolution.Daily, 100)]
public void SetPeriodAndCloseTimeUsingCloseTime(Resolution resolution, int barCount)
{
// consistency test -- first compute expected close time and then back-compute period to verify
var symbol = Symbols.SPY;
var generatedTimeUtc = new DateTime(2018, 08, 06, 13, 31, 0).ConvertToUtc(TimeZones.NewYork);
var exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType);
var baseline = Insight.Price(symbol, resolution, barCount, InsightDirection.Up);
baseline.GeneratedTimeUtc = generatedTimeUtc;
baseline.SetPeriodAndCloseTime(exchangeHours);
var baselineCloseTimeLocal = baseline.CloseTimeUtc.ConvertFromUtc(TimeZones.NewYork);
var insight = Insight.Price(symbol, baselineCloseTimeLocal, baseline.Direction);
insight.GeneratedTimeUtc = generatedTimeUtc;
insight.SetPeriodAndCloseTime(exchangeHours);
Assert.AreEqual(baseline.Period, insight.Period);
Assert.AreEqual(baseline.CloseTimeUtc, insight.CloseTimeUtc);
}
[Test]
[TestCase("SPY", SecurityType.Equity, Market.USA, 2018, 12, 4, 9, 30)]
[TestCase("EURUSD", SecurityType.Forex, Market.FXCM, 2018, 12, 4, 0, 0)]
public void SetPeriodAndCloseTimeUsingExpiryEndOfDay(string ticker, SecurityType securityType, string market, int year, int month, int day, int hour, int minute)
{
var symbol = Symbol.Create(ticker, securityType, market);
var generatedTimeUtc = new DateTime(2018, 12, 3, 9, 31, 0).ConvertToUtc(TimeZones.NewYork);
SetPeriodAndCloseTimeUsingExpiryFuncOrDateTime(
Insight.Price(symbol, Expiry.EndOfDay, InsightDirection.Up),
generatedTimeUtc,
new DateTime(year, month, day, hour, minute, 0).ConvertToUtc(TimeZones.NewYork));
}
[Test]
[TestCase("SPY", SecurityType.Equity, Market.USA, 2018, 12, 10, 9, 30)]
[TestCase("EURUSD", SecurityType.Forex, Market.FXCM, 2018, 12, 10, 0, 0)]
public void SetPeriodAndCloseTimeUsingExpiryEndOfWeek(string ticker, SecurityType securityType, string market, int year, int month, int day, int hour, int minute)
{
var symbol = Symbol.Create(ticker, securityType, market);
var generatedTime = new DateTime(2018, 12, 3, 9, 31, 0);
var generatedTimeUtc = generatedTime.ConvertToUtc(TimeZones.NewYork);
// Using Expiry Func
SetPeriodAndCloseTimeUsingExpiryFuncOrDateTime(
Insight.Price(symbol, Expiry.EndOfWeek, InsightDirection.Up),
generatedTimeUtc,
new DateTime(year, month, day, hour, minute, 0).ConvertToUtc(TimeZones.NewYork));
// Using DateTime
SetPeriodAndCloseTimeUsingExpiryFuncOrDateTime(
Insight.Price(symbol, Expiry.EndOfWeek(generatedTime), InsightDirection.Up),
generatedTimeUtc,
new DateTime(year, month, day, hour, minute, 0).ConvertToUtc(TimeZones.NewYork));
}
[Test]
[TestCase("SPY", SecurityType.Equity, Market.USA, 2019, 1, 2, 9, 30)]
[TestCase("EURUSD", SecurityType.Forex, Market.FXCM, 2019, 1, 2, 0, 0)]
public void SetPeriodAndCloseTimeUsingExpiryEndOfMonth(string ticker, SecurityType securityType, string market, int year, int month, int day, int hour, int minute)
{
var symbol = Symbol.Create(ticker, securityType, market);
var generatedTime = new DateTime(2018, 12, 3, 9, 31, 0);
var generatedTimeUtc = generatedTime.ConvertToUtc(TimeZones.NewYork);
// Using Expiry Func
SetPeriodAndCloseTimeUsingExpiryFuncOrDateTime(
Insight.Price(symbol, Expiry.EndOfMonth, InsightDirection.Up),
generatedTimeUtc,
new DateTime(year, month, day, hour, minute, 0).ConvertToUtc(TimeZones.NewYork));
// Using DateTime
SetPeriodAndCloseTimeUsingExpiryFuncOrDateTime(
Insight.Price(symbol, Expiry.EndOfMonth(generatedTime), InsightDirection.Up),
generatedTimeUtc,
new DateTime(year, month, day, hour, minute, 0).ConvertToUtc(TimeZones.NewYork));
}
[Test]
[TestCase("SPY", SecurityType.Equity, Market.USA, 2019, 1, 3, 9, 31)]
[TestCase("EURUSD", SecurityType.Forex, Market.FXCM, 2019, 1, 3, 9, 31)]
public void SetPeriodAndCloseTimeUsingExpiryOneMonth(string ticker, SecurityType securityType, string market, int year, int month, int day, int hour, int minute)
{
var symbol = Symbol.Create(ticker, securityType, market);
var generatedTime = new DateTime(2018, 12, 3, 9, 31, 0);
var generatedTimeUtc = generatedTime.ConvertToUtc(TimeZones.NewYork);
// Using Expiry Func
SetPeriodAndCloseTimeUsingExpiryFuncOrDateTime(
Insight.Price(symbol, Expiry.OneMonth, InsightDirection.Up),
generatedTimeUtc,
new DateTime(year, month, day, hour, minute, 0).ConvertToUtc(TimeZones.NewYork));
// Using DateTime
SetPeriodAndCloseTimeUsingExpiryFuncOrDateTime(
Insight.Price(symbol, Expiry.OneMonth(generatedTime), InsightDirection.Up),
generatedTimeUtc,
new DateTime(year, month, day, hour, minute, 0).ConvertToUtc(TimeZones.NewYork));
}
private void SetPeriodAndCloseTimeUsingExpiryFuncOrDateTime(Insight insight, DateTime generatedTimeUtc, DateTime expected)
{
var symbol = insight.Symbol;
insight.GeneratedTimeUtc = generatedTimeUtc;
var exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType);
insight.SetPeriodAndCloseTime(exchangeHours);
Assert.AreEqual(expected, insight.CloseTimeUtc);
}
[Test]
public void ComputeCloseTimeHandlesFractionalDays()
{
var symbol = Symbols.SPY;
// Friday @ 3PM + 2.5 days => Wednesday @ 12:45 by counting 2 dates (Mon, Tues@3PM) and then half a trading day (+3.25hrs) => Wed@11:45AM
var generatedTimeUtc = new DateTime(2018, 08, 03, 12+3, 0, 0).ConvertToUtc(TimeZones.NewYork);
var expectedClosedTimeUtc = new DateTime(2018, 08, 08, 11, 45, 0).ConvertToUtc(TimeZones.NewYork);
var exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType);
var actualCloseTimeUtc = Insight.ComputeCloseTime(exchangeHours, generatedTimeUtc, TimeSpan.FromDays(2.5));
Assert.AreEqual(expectedClosedTimeUtc, actualCloseTimeUtc);
}
[Test]
public void ComputeCloseTimeHandlesFractionalHours()
{
var symbol = Symbols.SPY;
// Friday @ 3PM + 2.5 hours => Monday @ 11:00 (1 hr on Friday, 1.5 hours on Monday)
var generatedTimeUtc = new DateTime(2018, 08, 03, 12 + 3, 0, 0).ConvertToUtc(TimeZones.NewYork);
var expectedClosedTimeUtc = new DateTime(2018, 08, 06, 11, 0, 0).ConvertToUtc(TimeZones.NewYork);
var exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType);
var actualCloseTimeUtc = Insight.ComputeCloseTime(exchangeHours, generatedTimeUtc, TimeSpan.FromHours(2.5));
Assert.AreEqual(expectedClosedTimeUtc, actualCloseTimeUtc);
}
[Test]
public void ComputeCloseHandlesOffsetHourOverMarketClosuresUsingTimeSpan()
{
var symbol = Symbols.SPY;
// Friday @ 3:59PM + 1 hours => Monday @ 10:29 (1 min on Friday, 59 min on Monday)
var generatedTimeUtc = new DateTime(2018, 08, 03, 12 + 3, 59, 0).ConvertToUtc(TimeZones.NewYork);
var expectedClosedTimeUtc = new DateTime(2018, 08, 06, 10, 29, 0).ConvertToUtc(TimeZones.NewYork);
var exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType);
var actualCloseTimeUtc = Insight.ComputeCloseTime(exchangeHours, generatedTimeUtc, TimeSpan.FromHours(1));
Assert.AreEqual(expectedClosedTimeUtc, actualCloseTimeUtc);
}
[Test]
public void ComputeCloseHandlesOffsetHourOverMarketClosuresUsingResolutionBarCount()
{
var symbol = Symbols.SPY;
// Friday @ 3:59PM + 1 hours => Monday @ 10:29 (1 min on Friday, 59 min on Monday)
var generatedTimeUtc = new DateTime(2018, 08, 03, 12 + 3, 59, 0).ConvertToUtc(TimeZones.NewYork);
var expectedClosedTimeUtc = new DateTime(2018, 08, 06, 10, 29, 0).ConvertToUtc(TimeZones.NewYork);
var exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType);
var actualCloseTimeUtc = Insight.ComputeCloseTime(exchangeHours, generatedTimeUtc, Resolution.Hour, 1);
Assert.AreEqual(expectedClosedTimeUtc, actualCloseTimeUtc);
}
[Test]
public void SetPeriodAndCloseTimeThrowsWhenGeneratedTimeUtcNotSet()
{
var insight = Insight.Price(Symbols.SPY, Time.OneDay, InsightDirection.Up);
var exchangeHours = SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork);
Assert.That(() => insight.SetPeriodAndCloseTime(exchangeHours),
Throws.InvalidOperationException);
}
[Test]
public void SetPeriodAndCloseTimeDoesNotThrowWhenGeneratedTimeUtcIsSet()
{
var insight = Insight.Price(Symbols.SPY, Time.OneDay, InsightDirection.Up);
var exchangeHours = SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork);
insight.GeneratedTimeUtc = new DateTime(2018, 08, 07, 00, 33, 00).ConvertToUtc(TimeZones.NewYork);
Assert.That(() => insight.SetPeriodAndCloseTime(exchangeHours),
Throws.Nothing);
}
[Test]
public void IsExpiredUsesOpenIntervalSemantics()
{
var generatedTime = new DateTime(2000, 01, 01);
var insight = Insight.Price(Symbols.SPY, Time.OneMinute, InsightDirection.Up);
insight.GeneratedTimeUtc = generatedTime;
insight.CloseTimeUtc = insight.GeneratedTimeUtc + insight.Period;
Assert.IsFalse(insight.IsExpired(insight.CloseTimeUtc));
Assert.IsTrue(insight.IsExpired(insight.CloseTimeUtc.AddTicks(1)));
}
[Test]
public void IsActiveUsesClosedIntervalSemantics()
{
var generatedTime = new DateTime(2000, 01, 01);
var insight = Insight.Price(Symbols.SPY, Time.OneMinute, InsightDirection.Up);
insight.GeneratedTimeUtc = generatedTime;
insight.CloseTimeUtc = insight.GeneratedTimeUtc + insight.Period;
Assert.IsTrue(insight.IsActive(insight.CloseTimeUtc));
Assert.IsFalse(insight.IsActive(insight.CloseTimeUtc.AddTicks(1)));
}
[Test]
public void ConstructorsSetsCorrectValues()
{
var tag = "Insight Tag";
Assert.Multiple(() =>
{
var insight1 = new Insight(Symbols.SPY, Time.OneDay, InsightType.Price, InsightDirection.Up, tag);
AssertInsigthValues(insight1, Symbols.SPY, Time.OneDay, InsightType.Price, InsightDirection.Up, null, null, null, null, tag,
default(DateTime), default(DateTime));
var insight2 = new Insight(Symbols.SPY, Time.OneDay, InsightType.Price, InsightDirection.Up, 1.0, 0.5, "Model", 0.25, tag);
AssertInsigthValues(insight2, Symbols.SPY, Time.OneDay, InsightType.Price, InsightDirection.Up, 1.0, 0.5, "Model", 0.25, tag,
default(DateTime), default(DateTime));
var insight3 = new Insight(Symbols.SPY, time => time.AddDays(1), InsightType.Price, InsightDirection.Up, tag);
AssertInsigthValues(insight3, Symbols.SPY, new TimeSpan(0), InsightType.Price, InsightDirection.Up, null, null, null, null, tag,
default(DateTime), default(DateTime));
var insight4 = new Insight(Symbols.SPY, time => time.AddDays(1), InsightType.Price, InsightDirection.Up, 1.0, 0.5, "Model", 0.25, tag);
AssertInsigthValues(insight4, Symbols.SPY, new TimeSpan(0), InsightType.Price, InsightDirection.Up, 1.0, 0.5, "Model", 0.25, tag,
default(DateTime), default(DateTime));
var generatedTime = new DateTime(2024, 05, 23);
var insight5 = new Insight(generatedTime, Symbols.SPY, Time.OneDay, InsightType.Price, InsightDirection.Up, 1.0, 0.5, "Model", 0.25, tag);
AssertInsigthValues(insight5, Symbols.SPY, Time.OneDay, InsightType.Price, InsightDirection.Up, 1.0, 0.5, "Model", 0.25, tag,
generatedTime, generatedTime + Time.OneDay);
});
}
private static void AssertInsigthValues(Insight insight, Symbol symbol, TimeSpan period, InsightType type, InsightDirection direction,
double? magnitude, double? confidence, string sourceModel, double? weight, string tag, DateTime generatedTimeUtc, DateTime closeTimeUtc)
{
Assert.AreEqual(symbol, insight.Symbol);
Assert.AreEqual(period, insight.Period);
Assert.AreEqual(type, insight.Type);
Assert.AreEqual(direction, insight.Direction);
Assert.AreEqual(magnitude, insight.Magnitude);
Assert.AreEqual(confidence, insight.Confidence);
Assert.AreEqual(sourceModel, insight.SourceModel);
Assert.AreEqual(weight, insight.Weight);
Assert.AreEqual(tag, insight.Tag);
Assert.AreEqual(generatedTimeUtc, insight.GeneratedTimeUtc);
Assert.AreEqual(closeTimeUtc, insight.CloseTimeUtc);
}
}
}
@@ -0,0 +1,72 @@
/*
* 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 NUnit.Framework;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Tests.Common.Data.UniverseSelection;
namespace QuantConnect.Tests.Algorithm.Framework
{
[TestFixture]
public class NotifiedSecurityChangesTests
{
[Test]
public void UpdateCallsDisposeOnDisposableInstances()
{
var disposable = new Disposable(Symbols.SPY);
var dictionary = new Dictionary<Symbol, Disposable>
{
[Symbols.SPY] = disposable
};
var SPY = new Equity(
Symbols.SPY,
SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork),
new Cash("USD", 1m, 1m),
SymbolProperties.GetDefault("USD"),
ErrorCurrencyConverter.Instance,
new RegisteredSecurityDataTypesProvider(),
new SecurityCache()
);
var changes = SecurityChangesTests.RemovedNonInternal(SPY);
NotifiedSecurityChanges.UpdateDictionary(dictionary, changes,
security => security.Symbol,
security => new Disposable(security.Symbol)
);
Assert.IsTrue(disposable.Disposed);
}
private class Disposable : IDisposable
{
public bool Disposed { get; private set; }
public Symbol Symbol { get; private set; }
public Disposable(Symbol symbol)
{
Symbol = symbol;
}
public void Dispose()
{
Disposed = true;
}
}
}
}
@@ -0,0 +1,516 @@
/*
* 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.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 AccumulativeInsightPortfolioConstructionModelTests
{
private QCAlgorithm _algorithm;
private const decimal _startingCash = 100000;
private const double DefaultPercent = 0.03;
[SetUp]
public void SetUp()
{
_algorithm = new QCAlgorithm();
_algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(_algorithm));
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.Python)]
[TestCase(Language.CSharp)]
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.Python, InsightDirection.Up)]
[TestCase(Language.Python, InsightDirection.Down)]
[TestCase(Language.Python, InsightDirection.Flat)]
[TestCase(Language.CSharp, InsightDirection.Up)]
[TestCase(Language.CSharp, InsightDirection.Down)]
[TestCase(Language.CSharp, InsightDirection.Flat)]
public void InsightsReturnsTargetsConsistentWithDirection(Language language, InsightDirection direction)
{
SetPortfolioConstruction(language, _algorithm);
var amount = _algorithm.Portfolio.TotalPortfolioValue * (decimal)DefaultPercent;
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));
var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights.ToArray());
AssertTargets( expectedTargets, actualTargets);
}
[Test]
[TestCase(Language.Python)]
[TestCase(Language.CSharp)]
public void LongTermInsightCanceledByNew(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();
var expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, -1m * (decimal)DefaultPercent) };
AssertTargets(expectedTargets, targets);
// One minute later, emits short term insight to cancel long
SetUtcTime(_algorithm.Time.AddMinutes(1));
insights = new[] { GetInsight(Symbols.SPY, InsightDirection.Up, _algorithm.UtcTime, Time.OneMinute) };
targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList();
expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, 0) };
AssertTargets(expectedTargets, targets);
// One minute later, emit empty insights array, short term insight expires but should stay -1 since long term insight is still valid
SetUtcTime(_algorithm.Time.AddMinutes(1.1));
expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, -1m * (decimal)DefaultPercent) };
var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, new Insight[0]);
AssertTargets(expectedTargets, actualTargets);
// should stay 0 *after* the long expires
SetUtcTime(_algorithm.Time.AddYears(1));
expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, 0) };
actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, new Insight[0]);
AssertTargets(expectedTargets, actualTargets);
}
[Test]
[TestCase(Language.Python, InsightDirection.Up)]
[TestCase(Language.Python, InsightDirection.Down)]
[TestCase(Language.CSharp, InsightDirection.Up)]
[TestCase(Language.CSharp, InsightDirection.Down)]
public void LongTermInsightAccumulatesByNew(Language language, InsightDirection direction)
{
SetPortfolioConstruction(language, _algorithm);
// First emit long term insight
var insights = new[] { GetInsight(Symbols.SPY, direction, _algorithm.UtcTime) };
var targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList();
var expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, (int)direction * 1m * (decimal)DefaultPercent) };
AssertTargets(expectedTargets, targets);
// One minute later, emits short term insight to add long
SetUtcTime(_algorithm.UtcTime.AddMinutes(1));
insights = new[] { GetInsight(Symbols.SPY, direction, _algorithm.UtcTime, Time.OneMinute) };
targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList();
expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, (int)direction * 1m * 2 * (decimal)DefaultPercent) };
AssertTargets(expectedTargets, targets);
// One minute later, emit empty insights array, should return to nomral after the long expires
SetUtcTime(_algorithm.UtcTime.AddMinutes(1.1));
expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, (int)direction * 1m * (decimal)DefaultPercent) };
// Create target from an empty insights array
var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, new Insight[0]);
AssertTargets(expectedTargets, actualTargets);
}
[Test]
[TestCase(Language.Python, InsightDirection.Up)]
[TestCase(Language.Python, InsightDirection.Down)]
[TestCase(Language.CSharp, InsightDirection.Up)]
[TestCase(Language.CSharp, InsightDirection.Down)]
public void FlatUndoesAccumulation(Language language, InsightDirection direction)
{
SetPortfolioConstruction(language, _algorithm);
// First emit long term insight
var insights = new[] { GetInsight(Symbols.SPY, direction, _algorithm.UtcTime) };
var targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList();
var expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, (int)direction * 1m * (decimal)DefaultPercent) };
AssertTargets(expectedTargets, targets);
// One minute later, emits insight to add to portfolio
SetUtcTime(_algorithm.UtcTime.AddMinutes(1));
insights = new[] { GetInsight(Symbols.SPY, direction, _algorithm.UtcTime) };
targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList();
expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, (int)direction * 1m * 2 * (decimal)DefaultPercent) };
AssertTargets(expectedTargets, targets);
// One minute later, emits flat insight
SetUtcTime(_algorithm.UtcTime.AddMinutes(1));
insights = new[] { GetInsight(Symbols.SPY, InsightDirection.Flat, _algorithm.UtcTime) };
targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList();
expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, (int)direction * 1m * (decimal)DefaultPercent) };
AssertTargets(expectedTargets, targets);
// now we should reach 0 percent
insights = new[] { GetInsight(Symbols.SPY, InsightDirection.Flat, _algorithm.UtcTime) };
targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList();
expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, 0) };
AssertTargets(expectedTargets, targets);
}
[Test]
[TestCase(Language.Python, InsightDirection.Up)]
[TestCase(Language.Python, InsightDirection.Down)]
[TestCase(Language.CSharp, InsightDirection.Up)]
[TestCase(Language.CSharp, InsightDirection.Down)]
public void InsightExpirationUndoesAccumulationBySteps(Language language, InsightDirection direction)
{
SetPortfolioConstruction(language, _algorithm);
// First emit long term insight
SetUtcTime(_algorithm.Time);
var insights = new[] { GetInsight(Symbols.SPY, direction, _algorithm.UtcTime, TimeSpan.FromMinutes(10)) };
var targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList();
var expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, (int)direction * 1m * (decimal)DefaultPercent) };
AssertTargets(expectedTargets, targets);
// One minute later, emits insight to add to portfolio
SetUtcTime(_algorithm.Time.AddMinutes(1));
insights = new[] { GetInsight(Symbols.SPY, direction, _algorithm.UtcTime, TimeSpan.FromMinutes(10)) };
targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList();
expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, (int)direction * 1m * 2 * (decimal)DefaultPercent) };
AssertTargets(expectedTargets, targets);
// the first insight should expire
SetUtcTime(_algorithm.Time.AddMinutes(10));
targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, new Insight[0]).ToList();
expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, (int)direction * 1m * (decimal)DefaultPercent) };
AssertTargets(expectedTargets, targets);
// the second insight should expire
SetUtcTime(_algorithm.Time.AddMinutes(1));
targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, new Insight[0]).ToList();
expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, 0) };
AssertTargets(expectedTargets, targets);
}
[TestCase(Language.Python, InsightDirection.Up)]
[TestCase(Language.Python, InsightDirection.Down)]
[TestCase(Language.CSharp, InsightDirection.Up)]
[TestCase(Language.CSharp, InsightDirection.Down)]
public void RespectsRebalancingPeriod(Language language, InsightDirection direction)
{
PortfolioConstructionModel model = new AccumulativeInsightPortfolioConstructionModel(Resolution.Daily);
if (language == Language.Python)
{
using (Py.GIL())
{
var name = nameof(AccumulativeInsightPortfolioConstructionModel);
dynamic instance = Py.Import(name).GetAttr(name);
model = new PortfolioConstructionModelPythonWrapper(instance(Resolution.Daily));
}
}
model.RebalanceOnSecurityChanges = false;
model.RebalanceOnInsightChanges = false;
SetUtcTime(new DateTime(2018, 7, 31));
// First emit long term insight
var insights = new[] { GetInsight(Symbols.SPY, direction, _algorithm.UtcTime, TimeSpan.FromDays(10)) };
AssertTargets(new List<IPortfolioTarget>(), model.CreateTargets(_algorithm, insights).ToList());
// One minute later, emits insight to add to portfolio
SetUtcTime(_algorithm.Time.AddMinutes(1));
insights = new[] { GetInsight(Symbols.SPY, direction, _algorithm.UtcTime, TimeSpan.FromMinutes(10)) };
AssertTargets(new List<IPortfolioTarget>(), model.CreateTargets(_algorithm, insights));
// the second insight should expire
SetUtcTime(_algorithm.Time.AddMinutes(1));
AssertTargets(new List<IPortfolioTarget>(), model.CreateTargets(_algorithm, insights));
// the rebalancing period is due and the first insight is still valid
SetUtcTime(_algorithm.Time.AddDays(1));
var targets = model.CreateTargets(_algorithm, new Insight[0]);
var expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, (int)direction * 1m * (decimal)DefaultPercent) };
AssertTargets(expectedTargets, targets);
// the rebalancing period is due and no insight is valid
SetUtcTime(_algorithm.Time.AddDays(10));
AssertTargets(
new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, 0) },
model.CreateTargets(_algorithm, new Insight[0]));
AssertTargets(new List<IPortfolioTarget>(), model.CreateTargets(_algorithm, new Insight[0]));
}
[Test]
[TestCase(Language.Python, InsightDirection.Up)]
[TestCase(Language.Python, InsightDirection.Down)]
[TestCase(Language.CSharp, InsightDirection.Up)]
[TestCase(Language.CSharp, InsightDirection.Down)]
public void InsightExpirationUndoesAccumulation(Language language, InsightDirection direction)
{
SetPortfolioConstruction(language, _algorithm);
// First emit long term insight
SetUtcTime(_algorithm.Time);
var insights = new[]
{
GetInsight(Symbols.SPY, direction, _algorithm.UtcTime, TimeSpan.FromMinutes(10)),
GetInsight(Symbols.SPY, direction, _algorithm.UtcTime, TimeSpan.FromMinutes(10))
};
var targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList();
var expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, (int)direction * 1m * 2 * (decimal)DefaultPercent) };
AssertTargets(expectedTargets, targets);
// both insights should expire
SetUtcTime(_algorithm.Time.AddMinutes(11));
targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, new Insight[0]).ToList();
expectedTargets = new List<IPortfolioTarget> { PortfolioTarget.Percent(_algorithm, Symbols.SPY, 0) };
AssertTargets(expectedTargets, targets);
// we expect no target
targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, new Insight[0]).ToList();
AssertTargets(new List<IPortfolioTarget>(), targets);
}
[Test]
[TestCase(Language.Python)]
[TestCase(Language.CSharp)]
public void WeightsProportionally(Language language)
{
SetPortfolioConstruction(language, _algorithm);
var insights = new[]
{
GetInsight(Symbols.SPY, InsightDirection.Down, _algorithm.UtcTime),
GetInsight(Symbol.Create("IBM", SecurityType.Equity, Market.USA),
InsightDirection.Down, _algorithm.UtcTime)
};
// they will each share, proportionally, the total portfolio value
var amount = _algorithm.Portfolio.TotalPortfolioValue * (decimal)DefaultPercent;
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 / x.Value.Price)));
var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList();
Assert.AreEqual(2, actualTargets.Count);
AssertTargets(expectedTargets, actualTargets);
}
[Test]
[TestCase(Language.Python)]
[TestCase(Language.CSharp)]
public void GeneratesTargetsForInsightsWithNoConfidence(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(1, actualTargets.Count);
}
[Test]
[TestCase(Language.Python)]
[TestCase(Language.CSharp)]
public void GeneratesNormalTargetForZeroInsightConfidence(Language language)
{
SetPortfolioConstruction(language, _algorithm);
var insights = new[]
{
GetInsight(Symbols.SPY, InsightDirection.Down, _algorithm.UtcTime, confidence:0)
};
// they will each share, proportionally, the total portfolio value
var amount = _algorithm.Portfolio.TotalPortfolioValue * (decimal)DefaultPercent;
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 / x.Value.Price)));
var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToList();
AssertTargets(expectedTargets, actualTargets);
}
[TestCase(Language.CSharp, PortfolioBias.Long)]
[TestCase(Language.Python, PortfolioBias.Long)]
[TestCase(Language.CSharp, PortfolioBias.Short)]
[TestCase(Language.Python, PortfolioBias.Short)]
public void PortfolioBiasIsRespected(Language language, PortfolioBias bias)
{
SetPortfolioConstruction(language, _algorithm, bias);
var now = new DateTime(2018, 7, 31);
SetUtcTime(now.ConvertFromUtc(_algorithm.TimeZone));
var appl = _algorithm.AddEquity("AAPL");
appl.SetMarketPrice(new Tick(now, appl.Symbol, 10, 10));
var spy = _algorithm.AddEquity("SPY");
spy.SetMarketPrice(new Tick(now, spy.Symbol, 20, 20));
var ibm = _algorithm.AddEquity("IBM");
ibm.SetMarketPrice(new Tick(now, ibm.Symbol, 30, 30));
var aig = _algorithm.AddEquity("AIG");
aig.SetMarketPrice(new Tick(now, aig.Symbol, 30, 30));
var qqq = _algorithm.AddEquity("QQQ");
qqq.SetMarketPrice(new Tick(now, qqq.Symbol, 30, 30));
var insights = new[]
{
new Insight(now, appl.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, 0.1d, null),
new Insight(now, spy.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Down, -0.1d, null),
new Insight(now, ibm.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Down, 0d, null),
new Insight(now, aig.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Down, -0.1d, null),
new Insight(now, qqq.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, 0.1d, null)
};
_algorithm.PortfolioConstruction.OnSecuritiesChanged(_algorithm, SecurityChangesTests.AddedNonInternal(appl, spy, ibm, aig, qqq));
var createdValidTarget = false;
_algorithm.Insights.AddRange(insights);
foreach (var target in _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights))
{
QuantConnect.Logging.Log.Trace($"{target.Symbol}: {target.Quantity}");
if (target.Quantity == 0)
{
continue;
}
createdValidTarget = true;
Assert.AreEqual(Math.Sign((int)bias), Math.Sign(target.Quantity));
}
Assert.IsTrue(createdValidTarget);
}
private Security GetSecurity(Symbol symbol)
{
var exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType);
return new Equity(
symbol,
exchangeHours,
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 = DefaultPercent)
{
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, PortfolioBias bias= PortfolioBias.LongShort)
{
algorithm.SetPortfolioConstruction(new AccumulativeInsightPortfolioConstructionModel((Func<DateTime,DateTime>)null, bias));
if (language == Language.Python)
{
using (Py.GIL())
{
var name = nameof(AccumulativeInsightPortfolioConstructionModel);
var instance = Py.Import(name).GetAttr(name).Invoke(((object)null).ToPython(), ((int)bias).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<IPortfolioTarget> expectedTargets, IEnumerable<IPortfolioTarget> 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);
}
}
}
}
@@ -0,0 +1,243 @@
/*
* 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 NodaTime;
using NUnit.Framework;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Tests.Engine.DataFeeds;
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Tests.Common.Data.UniverseSelection;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public abstract class BaseWeightingPortfolioConstructionModelTests
{
protected decimal StartingCash => 100000;
protected QCAlgorithm Algorithm { get; set; }
public virtual double? Weight => Algorithm.Securities.Count == 0 ? default(double) : 1d / Algorithm.Securities.Count;
[OneTimeSetUp]
public virtual void SetUp()
{
Algorithm = new QCAlgorithm();
Algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(Algorithm));
}
[TearDown]
public void TearDown() => Algorithm.Insights.Clear(Algorithm.Securities.Keys.ToArray());
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void AutomaticallyRemoveInvestedWithoutNewInsights(Language language)
{
SetPortfolioConstruction(language);
// 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<IPortfolioTarget> { 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 DelistedSecurityEmitsFlatTargetWithoutNewInsights(Language language)
{
SetPortfolioConstruction(language);
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<IPortfolioTarget> { 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 DoesNotReturnTargetsIfSecurityPriceIsZero(Language language)
{
var algorithm = new QCAlgorithm();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.AddEquity(Symbols.SPY.Value);
algorithm.SetDateTime(DateTime.MinValue.ConvertToUtc(Algorithm.TimeZone));
SetPortfolioConstruction(language);
var insights = new[] { GetInsight(Symbols.SPY, InsightDirection.Up, algorithm.UtcTime) };
var actualTargets = algorithm.PortfolioConstruction.CreateTargets(algorithm, insights);
Assert.AreEqual(0, actualTargets.Count());
}
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void DoesNotThrowWithAlternativeOverloads(Language language)
{
Assert.DoesNotThrow(() => SetPortfolioConstruction(language, Resolution.Minute));
Assert.DoesNotThrow(() => SetPortfolioConstruction(language, TimeSpan.FromDays(1)));
Assert.DoesNotThrow(() => SetPortfolioConstruction(language, Expiry.EndOfWeek));
}
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void EmptyInsightsReturnsEmptyTargets(Language language)
{
SetPortfolioConstruction(language);
var actualTargets = Algorithm.PortfolioConstruction.CreateTargets(Algorithm, new Insight[0]);
Assert.AreEqual(0, actualTargets.Count());
}
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void LongTermInsightPreservesPosition(Language language)
{
SetPortfolioConstruction(language);
// 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 = GetTargetsForSPY();
// 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 abstract void AutomaticallyRemoveInvestedWithNewInsights(Language language, InsightDirection direction);
[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 abstract void DelistedSecurityEmitsFlatTargetWithNewInsights(Language language, InsightDirection direction);
[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 abstract void FlatDirectionNotAccountedToAllocation(Language language, InsightDirection direction);
public abstract IPortfolioConstructionModel GetPortfolioConstructionModel(Language language, dynamic paramenter = null);
public abstract Insight GetInsight(Symbol symbol, InsightDirection direction, DateTime generatedTimeUtc, TimeSpan? period = null, double? weight = 0.01);
public void AssertTargets(IEnumerable<IPortfolioTarget> expectedTargets, IEnumerable<IPortfolioTarget> 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);
}
}
protected Security GetSecurity(Symbol symbol) =>
new Equity(
symbol,
SecurityExchangeHours.AlwaysOpen(DateTimeZone.Utc),
new Cash(Currencies.USD, 0, 1),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
public virtual List<IPortfolioTarget> GetTargetsForSPY()
{
return new List<IPortfolioTarget> { PortfolioTarget.Percent(Algorithm, Symbols.SPY, -1m) };
}
protected void SetPortfolioConstruction(Language language, dynamic paramenter = null)
{
var model = GetPortfolioConstructionModel(language, paramenter ?? Resolution.Daily);
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);
}
protected void SetUtcTime(DateTime dateTime) => Algorithm.SetDateTime(dateTime.ConvertToUtc(Algorithm.TimeZone));
}
}
@@ -0,0 +1,546 @@
/*
* 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 Accord.Math;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Securities;
using System;
using System.Linq;
using QuantConnect.Algorithm;
using QuantConnect.Tests.Engine.DataFeeds;
using System.Collections.Generic;
using QuantConnect.Tests.Common.Data.UniverseSelection;
using QuantConnect.Interfaces;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class BlackLittermanOptimizationPortfolioConstructionModelTests
{
private QCAlgorithm _algorithm;
private Insight[] _view1Insights;
private Insight[] _view2Insights;
[SetUp]
public void SetUp()
{
_algorithm = new QCAlgorithm();
_algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(_algorithm));
SetUtcTime(new DateTime(2018, 8, 7));
// Germany will outperform the other European markets by 5%
_view1Insights = new[]
{
GetInsight("View 1", "AUS", 0),
GetInsight("View 1", "CAN", 0),
GetInsight("View 1", "FRA", -0.01475),
GetInsight("View 1", "GER", 0.05000),
GetInsight("View 1", "JAP", 0),
GetInsight("View 1", "UK" , -0.03525),
GetInsight("View 1", "USA", 0)
};
// Canadian Equities will outperform US equities by 3 %
_view2Insights = new[]
{
GetInsight("View 2", "AUS", 0),
GetInsight("View 2", "CAN", 0.03),
GetInsight("View 2", "FRA", 0),
GetInsight("View 2", "GER", 0),
GetInsight("View 2", "JAP", 0),
GetInsight("View 2", "UK" , 0),
GetInsight("View 2", "USA", -0.03)
};
foreach (var symbol in _view1Insights.Select(x => x.Symbol))
{
var security = GetSecurity(symbol, Resolution.Daily);
security.SetMarketPrice(new Tick(_algorithm.Time, symbol, 1m, 1m));
_algorithm.Securities.Add(symbol, security);
}
}
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void EmptyInsightsReturnsEmptyTargets(Language language)
{
SetPortfolioConstruction(language);
var insights = new Insight[0];
var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights);
Assert.AreEqual(0, actualTargets.Count());
}
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void OneViewTest(Language language)
{
SetPortfolioConstruction(language);
// Add outdated insight to check if only the latest one was considered
var outdatedInsight = GetInsight("View 1", "CAN", 0.05);
outdatedInsight.GeneratedTimeUtc -= TimeSpan.FromHours(1);
outdatedInsight.CloseTimeUtc -= TimeSpan.FromHours(1);
// Results from http://www.blacklitterman.org/code/hl_py.html (View 1)
var expectedTargets = new[]
{
PortfolioTarget.Percent(_algorithm, GetSymbol("AUS"), 0.0152381),
PortfolioTarget.Percent(_algorithm, GetSymbol("CAN"), 0.02095238),
PortfolioTarget.Percent(_algorithm, GetSymbol("FRA"), -0.03948465),
PortfolioTarget.Percent(_algorithm, GetSymbol("GER"), 0.35410454),
PortfolioTarget.Percent(_algorithm, GetSymbol("JAP"), 0.11047619),
PortfolioTarget.Percent(_algorithm, GetSymbol("UK"), -0.09461989),
PortfolioTarget.Percent(_algorithm, GetSymbol("USA"), 0.58571429)
};
var insights = _view1Insights.Concat(new[] { outdatedInsight }).ToArray();
Clear();
_algorithm.Insights.AddRange(insights);
var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights);
Assert.AreEqual(expectedTargets.Length, actualTargets.Count());
foreach (var expected in expectedTargets)
{
var actual = actualTargets.FirstOrDefault(x => x.Symbol == expected.Symbol);
Assert.IsNotNull(actual);
Assert.AreEqual(expected.Quantity, actual.Quantity);
}
}
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void TwoViewsTest(Language language)
{
SetPortfolioConstruction(language);
// Results from http://www.blacklitterman.org/code/hl_py.html (View 1+2)
var expectedTargets = new[]
{
PortfolioTarget.Percent(_algorithm, GetSymbol("AUS"), 0.0152381),
PortfolioTarget.Percent(_algorithm, GetSymbol("CAN"), 0.41863571),
PortfolioTarget.Percent(_algorithm, GetSymbol("FRA"), -0.03409321),
PortfolioTarget.Percent(_algorithm, GetSymbol("GER"), 0.33582847),
PortfolioTarget.Percent(_algorithm, GetSymbol("JAP"), 0.11047619),
PortfolioTarget.Percent(_algorithm, GetSymbol("UK"), -0.08173526),
PortfolioTarget.Percent(_algorithm, GetSymbol("USA"), 0.18803095)
};
// Add outdated insight to check if only the latest one was considered
var outdatedInsight = GetInsight("View 2", "USA", 0.05);
outdatedInsight.GeneratedTimeUtc -= TimeSpan.FromHours(1);
outdatedInsight.CloseTimeUtc -= TimeSpan.FromHours(1);
var insights = _view1Insights.Concat(_view2Insights).Concat(new[] { outdatedInsight });
var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights.ToArray());
Assert.AreEqual(expectedTargets.Length, actualTargets.Count());
foreach (var expected in expectedTargets)
{
var actual = actualTargets.FirstOrDefault(x => x.Symbol == expected.Symbol);
Assert.IsNotNull(actual);
Assert.AreEqual(expected.Quantity, actual.Quantity);
}
}
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void OneViewDimensionTest(Language language)
{
SetPortfolioConstruction(language);
if (language == Language.CSharp)
{
double[,] P;
double[] Q;
((BLOPCM)_algorithm.PortfolioConstruction).TestTryGetViews(_view1Insights, out P, out Q);
Assert.AreEqual(P.GetLength(0), 1);
Assert.AreEqual(P.GetLength(1), 7);
Assert.AreEqual(Q.GetLength(0), 1);
return;
}
using (Py.GIL())
{
var name = nameof(BLOPCM);
var instance = PyModule.FromString(name, GetPythonBLOPCM()).GetAttr(name).Invoke(((int)PortfolioBias.LongShort).ToPython());
var result = PyList.AsList(instance.InvokeMethod("get_views", _view1Insights.ToPython()));
Assert.AreEqual(result[0].Length(), 1);
Assert.AreEqual(result[0][0].Length(), 7);
Assert.AreEqual(result[1].Length(), 1);
}
}
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void TwoViewsDimensionTest(Language language)
{
SetPortfolioConstruction(language);
// Test if a symbol has no view in one of the source models
var insights = _view1Insights.Concat(_view2Insights.Skip(1)).ToList();
if (language == Language.CSharp)
{
double[,] P;
double[] Q;
((BLOPCM)_algorithm.PortfolioConstruction).TestTryGetViews(insights, out P, out Q);
Assert.AreEqual(P.GetLength(0), 2);
Assert.AreEqual(P.GetLength(1), 7);
Assert.AreEqual(Q.GetLength(0), 2);
return;
}
using (Py.GIL())
{
var name = nameof(BLOPCM);
var instance = PyModule.FromString(name, GetPythonBLOPCM()).GetAttr(name).Invoke(((int)PortfolioBias.LongShort).ToPython());
var result = PyList.AsList(instance.InvokeMethod("get_views", insights.ToPython()));
Assert.AreEqual(result[0].Length(), 2);
Assert.AreEqual(result[0][0].Length(), 7);
Assert.AreEqual(result[1].Length(), 2);
}
}
[Test]
[TestCase(Language.CSharp, 11, true)]
[TestCase(Language.CSharp, -11, true)]
[TestCase(Language.CSharp, 0.001d, true)]
[TestCase(Language.CSharp, -0.001d, true)]
[TestCase(Language.CSharp, 0.1, false)]
[TestCase(Language.CSharp, -0.1, false)]
[TestCase(Language.CSharp, 0.011d, false)]
[TestCase(Language.CSharp, -0.011d, false)]
[TestCase(Language.CSharp, 0, true)]
[TestCase(Language.Python, 0, true)]
[TestCase(Language.Python, 11, true)]
[TestCase(Language.Python, -11, true)]
[TestCase(Language.Python, 0.001d, true)]
[TestCase(Language.Python, -0.001d, true)]
[TestCase(Language.Python, 0.1, false)]
[TestCase(Language.Python, -0.1, false)]
[TestCase(Language.Python, 0.011d, false)]
[TestCase(Language.Python, -0.011d, false)]
public void IgnoresInsightsWithInvalidMagnitudeValue(Language language, double magnitude, bool expectZero)
{
SetPortfolioConstruction(language);
_algorithm.Settings.MaxAbsolutePortfolioTargetPercentage = 10;
_algorithm.Settings.MinAbsolutePortfolioTargetPercentage = 0.01m;
Clear();
var insights = new[]
{
GetInsight("View 1", "AUS", magnitude),
GetInsight("View 1", "CAN", magnitude),
GetInsight("View 1", "FRA", magnitude),
GetInsight("View 1", "GER", magnitude),
GetInsight("View 1", "JAP", magnitude),
GetInsight("View 1", "UK" , magnitude),
GetInsight("View 1", "USA", magnitude)
};
var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights);
if (expectZero)
{
Assert.AreEqual(0, actualTargets.Count());
}
else
{
Assert.AreNotEqual(0, actualTargets.Count());
}
}
[TestCase(Language.CSharp, PortfolioBias.Long)]
[TestCase(Language.Python, PortfolioBias.Long)]
[TestCase(Language.CSharp, PortfolioBias.Short)]
[TestCase(Language.Python, PortfolioBias.Short)]
public void PortfolioBiasIsRespected(Language language, PortfolioBias bias)
{
SetPortfolioConstruction(language, bias);
var insights = new[]
{
GetInsight("View 1", "AUS", -10.1),
GetInsight("View 1", "CAN", -0.1),
GetInsight("View 1", "FRA", 0.1),
GetInsight("View 1", "GER", -0.1),
GetInsight("View 1", "JAP", -0.1),
GetInsight("View 1", "UK" , 0.1),
GetInsight("View 1", "USA", -0.1)
};
var createdValidTarget = false;
foreach (var target in _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights))
{
QuantConnect.Logging.Log.Trace($"{target.Symbol}: {target.Quantity}");
if (target.Quantity == 0)
{
continue;
}
createdValidTarget = true;
Assert.AreEqual(Math.Sign((int)bias), Math.Sign(target.Quantity));
}
Assert.IsTrue(createdValidTarget);
}
[Test]
public void NewSymbolPortfolioConstructionModelDoesNotThrow()
{
var algorithm = new QCAlgorithm();
var timezone = algorithm.TimeZone;
algorithm.SetDateTime(new DateTime(2018, 8, 7).ConvertToUtc(timezone));
algorithm.SetPortfolioConstruction(new NewSymbolPortfolioConstructionModel());
var spySymbol = Symbols.SPY;
var spy = GetSecurity(spySymbol, Resolution.Daily);
spy.SetMarketPrice(new Tick(algorithm.Time, spySymbol, 1m, 1m));
algorithm.Securities.Add(spySymbol, spy);
algorithm.PortfolioConstruction.OnSecuritiesChanged(algorithm, SecurityChangesTests.AddedNonInternal(spy));
var insights = new[] { Insight.Price(spySymbol, Time.OneMinute, InsightDirection.Up, .1) };
Assert.DoesNotThrow(() => algorithm.PortfolioConstruction.CreateTargets(algorithm, insights));
algorithm.SetDateTime(algorithm.Time.AddDays(1));
var aaplSymbol = Symbols.AAPL;
var aapl = GetSecurity(spySymbol, Resolution.Daily);
aapl.SetMarketPrice(new Tick(algorithm.Time, aaplSymbol, 1m, 1m));
algorithm.Securities.Add(aaplSymbol, aapl);
algorithm.PortfolioConstruction.OnSecuritiesChanged(algorithm, SecurityChangesTests.AddedNonInternal(aapl));
insights = new[] { spySymbol, aaplSymbol }
.Select(x => Insight.Price(x, Time.OneMinute, InsightDirection.Up, .1)).ToArray();
Assert.DoesNotThrow(() => algorithm.PortfolioConstruction.CreateTargets(algorithm, insights));
}
private Security GetSecurity(Symbol symbol, Resolution resolution)
{
var timezone = _algorithm.TimeZone;
var exchangeHours = SecurityExchangeHours.AlwaysOpen(timezone);
var config = new SubscriptionDataConfig(typeof(TradeBar), symbol, resolution, timezone, timezone, true, false, false);
return new Security(
exchangeHours,
config,
new Cash(Currencies.USD, 0, 1),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
}
private Symbol GetSymbol(string ticker) => Symbol.Create(ticker, SecurityType.Equity, Market.USA);
private Insight GetInsight(string SourceModel, string ticker, double magnitude)
{
var period = Time.OneDay;
var direction = (InsightDirection)Math.Sign(magnitude);
var insight = Insight.Price(GetSymbol(ticker), period, direction, magnitude, sourceModel: SourceModel);
insight.GeneratedTimeUtc = _algorithm.UtcTime;
insight.CloseTimeUtc = _algorithm.UtcTime.Add(insight.Period);
_algorithm.Insights.Add(insight);
return insight;
}
private void SetPortfolioConstruction(Language language, PortfolioBias portfolioBias = PortfolioBias.LongShort)
{
_algorithm.SetPortfolioConstruction(new BLOPCM(new UnconstrainedMeanVariancePortfolioOptimizer(), portfolioBias));
if (language == Language.Python)
{
try
{
using (Py.GIL())
{
var name = nameof(BLOPCM);
var instance = PyModule.FromString(name, GetPythonBLOPCM()).GetAttr(name).Invoke(((int)portfolioBias).ToPython());
var model = new PortfolioConstructionModelPythonWrapper(instance);
_algorithm.SetPortfolioConstruction(model);
}
}
catch (Exception e)
{
Assert.Ignore(e.Message);
}
}
var changes = SecurityChangesTests.AddedNonInternal(_algorithm.Securities.Values.ToList().ToArray());
_algorithm.PortfolioConstruction.OnSecuritiesChanged(_algorithm, changes);
}
private void SetUtcTime(DateTime dateTime)
{
_algorithm.SetDateTime(dateTime.ConvertToUtc(_algorithm.TimeZone));
}
private class BLOPCM : BlackLittermanOptimizationPortfolioConstructionModel
{
public BLOPCM(IPortfolioOptimizer optimizer, PortfolioBias portfolioBias)
: base(optimizer: optimizer, portfolioBias: portfolioBias)
{
}
public override double[] GetEquilibriumReturns(double[,] returns, out double[,] Σ)
{
// Take the values from He & Litterman, 1999.
var C = new[,]
{
{ 1.000, 0.488, 0.478, 0.515, 0.439, 0.512, 0.491 },
{ 0.488, 1.000, 0.664, 0.655, 0.310, 0.608, 0.779 },
{ 0.478, 0.664, 1.000, 0.861, 0.355, 0.783, 0.668 },
{ 0.515, 0.655, 0.861, 1.000, 0.354, 0.777, 0.653 },
{ 0.439, 0.310, 0.355, 0.354, 1.000, 0.405, 0.306 },
{ 0.512, 0.608, 0.783, 0.777, 0.405, 1.000, 0.652 },
{ 0.491, 0.779, 0.668, 0.653, 0.306, 0.652, 1.000 }
};
var σ = new[] { 0.160, 0.203, 0.248, 0.271, 0.210, 0.200, 0.187 };
var w = new[] { 0.016, 0.022, 0.052, 0.055, 0.116, 0.124, 0.615 };
var delta = 2.5;
// Equilibrium covariance matrix
Σ = Elementwise.Multiply(C, σ.Outer(σ));
return w.Dot(Σ.Multiply(delta));
}
public bool TestTryGetViews(ICollection<Insight> insights, out double[,] P, out double[] Q)
{
return base.TryGetViews(insights, out P, out Q);
}
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
{
}
}
private string GetPythonBLOPCM()
{
return @"
from AlgorithmImports import *
from Portfolio.BlackLittermanOptimizationPortfolioConstructionModel import BlackLittermanOptimizationPortfolioConstructionModel
from Portfolio.UnconstrainedMeanVariancePortfolioOptimizer import UnconstrainedMeanVariancePortfolioOptimizer
def GetSymbol(ticker):
return str(Symbol.Create(ticker, SecurityType.Equity, Market.USA))
class BLOPCM(BlackLittermanOptimizationPortfolioConstructionModel):
def __init__(self, portfolioBias):
super().__init__(portfolio_bias = portfolioBias, optimizer = UnconstrainedMeanVariancePortfolioOptimizer())
def get_equilibrium_return(self, returns):
# Take the values from He & Litterman, 1999.
weq = np.array([0.016, 0.022, 0.052, 0.055, 0.116, 0.124, 0.615])
C = np.array([[ 1.000, 0.488, 0.478, 0.515, 0.439, 0.512, 0.491],
[0.488, 1.000, 0.664, 0.655, 0.310, 0.608, 0.779],
[0.478, 0.664, 1.000, 0.861, 0.355, 0.783, 0.668],
[0.515, 0.655, 0.861, 1.000, 0.354, 0.777, 0.653],
[0.439, 0.310, 0.355, 0.354, 1.000, 0.405, 0.306],
[0.512, 0.608, 0.783, 0.777, 0.405, 1.000, 0.652],
[0.491, 0.779, 0.668, 0.653, 0.306, 0.652, 1.000]])
Sigma = np.array([0.160, 0.203, 0.248, 0.271, 0.210, 0.200, 0.187])
refPi = np.array([0.039, 0.069, 0.084, 0.090, 0.043, 0.068, 0.076])
assets= [GetSymbol(x) for x in ['AUS', 'CAN', 'FRA', 'GER', 'JAP', 'UK', 'USA']]
delta = 2.5
# Equilibrium covariance matrix
V = np.multiply(np.outer(Sigma,Sigma), C)
return weq.dot(V * delta), pd.DataFrame(V, columns=assets, index=assets)
def on_securities_changed(self, algorithm, changes):
pass";
}
private void Clear() => _algorithm.Insights.Clear(_algorithm.Securities.Keys.ToArray());
private class NewSymbolPortfolioConstructionModel : BlackLittermanOptimizationPortfolioConstructionModel
{
private readonly Dictionary<Symbol, ReturnsSymbolData> _symbolDataDict = new Dictionary<Symbol, ReturnsSymbolData>();
public override IEnumerable<IPortfolioTarget> CreateTargets(QCAlgorithm algorithm, Insight[] insights)
{
// Updates the ReturnsSymbolData with insights
foreach (var insight in insights)
{
ReturnsSymbolData symbolData;
if (_symbolDataDict.TryGetValue(insight.Symbol, out symbolData))
{
symbolData.Add(algorithm.Time, .1m);
}
}
double[,] returns = null;
Assert.DoesNotThrow(() => returns = _symbolDataDict.FormReturnsMatrix(insights.Select(x => x.Symbol)));
// Calculate posterior estimate of the mean and uncertainty in the mean
double[,] Σ;
var Π = GetEquilibriumReturns(returns, out Σ);
Assert.IsFalse(double.IsNaN(Π[0]));
return Enumerable.Empty<PortfolioTarget>();
}
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
{
const int period = 2;
var reference = algorithm.Time.AddDays(-period);
foreach (var security in changes.AddedSecurities)
{
var symbol = security.Symbol;
var symbolData = new ReturnsSymbolData(symbol, 1, period);
for (var i = 0; i <= period * 2; i++)
{
symbolData.Update(reference.AddDays(i), i);
}
_symbolDataDict[symbol] = symbolData;
}
}
}
}
}
@@ -0,0 +1,438 @@
/*
* 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, 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);
}
}
[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<IPortfolioTarget> { 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<IPortfolioTarget> { 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<IPortfolioTarget> { 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<IPortfolioTarget> expectedTargets, IEnumerable<IPortfolioTarget> 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);
}
}
}
}
@@ -0,0 +1,243 @@
/*
* 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);
}
}
}
}
@@ -0,0 +1,123 @@
/*
* 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 System;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class InsightWeightingPortfolioConstructionModelTests : EqualWeightingPortfolioConstructionModelTests
{
private const double _weight = 0.01;
public override double? Weight => _weight;
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void WeightsProportionally(Language language)
{
SetPortfolioConstruction(language);
// create two insights whose weights sums up to 2
var insights = new[]
{
GetInsight(Symbols.SPY, InsightDirection.Up, Algorithm.UtcTime, weight:1),
GetInsight(Symbol.Create("IBM", SecurityType.Equity, Market.USA),
InsightDirection.Up, Algorithm.UtcTime, weight: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.Up
* 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 GeneratesNoTargetsForInsightsWithNoWeight(Language language)
{
SetPortfolioConstruction(language);
var insights = new[]
{
GetInsight(Symbols.SPY, InsightDirection.Down, Algorithm.UtcTime, weight:null)
};
var actualTargets = Algorithm.PortfolioConstruction.CreateTargets(Algorithm, insights).ToList();
Assert.AreEqual(0, actualTargets.Count);
}
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void GeneratesZeroTargetForZeroInsightWeight(Language language)
{
SetPortfolioConstruction(language);
var insights = new[]
{
GetInsight(Symbols.SPY, InsightDirection.Down, Algorithm.UtcTime, weight:0)
};
var actualTargets = Algorithm.PortfolioConstruction.CreateTargets(Algorithm, insights).ToList();
Assert.AreEqual(1, actualTargets.Count);
AssertTargets(actualTargets, new[] {new PortfolioTarget(Symbols.SPY, 0)});
}
public override IPortfolioConstructionModel GetPortfolioConstructionModel(Language language, dynamic paramenter = null)
{
if (language == Language.CSharp)
{
return new InsightWeightingPortfolioConstructionModel(paramenter);
}
using (Py.GIL())
{
const string name = nameof(InsightWeightingPortfolioConstructionModel);
var instance = Py.Import(name).GetAttr(name).Invoke(((object)paramenter).ToPython());
return new PortfolioConstructionModelPythonWrapper(instance);
}
}
public override Insight GetInsight(Symbol symbol, InsightDirection direction, DateTime generatedTimeUtc, TimeSpan? period = null, double? weight = _weight)
{
period ??= TimeSpan.FromDays(1);
var insight = Insight.Price(symbol, period.Value, direction, weight: weight);
insight.GeneratedTimeUtc = generatedTimeUtc;
insight.CloseTimeUtc = generatedTimeUtc.Add(period.Value);
Algorithm.Insights.Add(insight);
return insight;
}
public override List<IPortfolioTarget> GetTargetsForSPY()
{
return new List<IPortfolioTarget> { PortfolioTarget.Percent(Algorithm, Symbols.SPY, -_weight) };
}
}
}
@@ -0,0 +1,48 @@
/*
* 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.Portfolio;
using System.Collections.Generic;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class LongOnlyEqualWeightingPortfolioConstructionModelTests : EqualWeightingPortfolioConstructionModelTests
{
public override PortfolioBias PortfolioBias => PortfolioBias.Long;
public override IPortfolioConstructionModel GetPortfolioConstructionModel(Language language, dynamic paramenter = null)
{
if (language == Language.CSharp)
{
return new EqualWeightingPortfolioConstructionModel(paramenter, PortfolioBias.Long);
}
using (Py.GIL())
{
const string name = nameof(EqualWeightingPortfolioConstructionModel);
var instance = Py.Import(name).GetAttr(name).Invoke(((object)paramenter).ToPython(), ((int) PortfolioBias.Long).ToPython());
return new PortfolioConstructionModelPythonWrapper(instance);
}
}
public override List<IPortfolioTarget> GetTargetsForSPY()
{
return new List<IPortfolioTarget> { PortfolioTarget.Percent(Algorithm, Symbols.SPY, 0m) };
}
}
}
@@ -0,0 +1,48 @@
/*
* 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.Portfolio;
using System.Collections.Generic;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class LongOnlyInsightWeightingPortfolioConstructionModelTests : InsightWeightingPortfolioConstructionModelTests
{
public override PortfolioBias PortfolioBias => PortfolioBias.Long;
public override IPortfolioConstructionModel GetPortfolioConstructionModel(Language language, dynamic paramenter = null)
{
if (language == Language.CSharp)
{
return new InsightWeightingPortfolioConstructionModel(paramenter, PortfolioBias.Long);
}
using (Py.GIL())
{
const string name = nameof(InsightWeightingPortfolioConstructionModel);
var instance = Py.Import(name).GetAttr(name).Invoke(((object)paramenter).ToPython(), ((int)PortfolioBias.Long).ToPython());
return new PortfolioConstructionModelPythonWrapper(instance);
}
}
public override List<IPortfolioTarget> GetTargetsForSPY()
{
return new List<IPortfolioTarget> { PortfolioTarget.Percent(Algorithm, Symbols.SPY, 0m) };
}
}
}
@@ -0,0 +1,252 @@
/*
* 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 aaplicable 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 Accord.Math;
using Accord.Statistics;
using NUnit.Framework;
using QuantConnect.Algorithm.Framework.Portfolio;
using System;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class MaximumSharpeRatioPortfolioOptimizerTests : PortfolioOptimizerTestsBase
{
[OneTimeSetUp]
public void SetUp()
{
HistoricalReturns = new List<double[,]>
{
new double[,] { { 0.02, -0.02, 0.28 }, { -0.50, -0.29, -0.13 }, { 0.81, 0.29, 0.31 }, { -0.03, -0.00, 0.01 } },
new double[,] { { 0.10, 0.20, 0.4 }, { 0.12, 0.25, 0.4 }, { 0.11, 0.22, 0.4 } },
new double[,] { { -0.19, 0.50, 0.45 }, { -0.62, -0.65, 0.07 }, { -0.14, 1.02, 0.01 }, { 0.00, -0.03, 0.01 } },
new double[,] { { 0.46, 0.28, 0.58, 0.26, 0.14 }, { 0.52, 0.31, 0.43, 7.43, -0.00 }, { 0.13, 0.65, 0.52, 0.50, -0.08 }, { -0.41, -0.39, -0.28, -0.65, -0.20 }, { 0.77, 0.58, 0.58, 1.02, 0.03 }, { -0.03, -0.01, -0.01, -0.03, 0.07 } },
new double[,] { { -0.50, -0.13 }, { 0.81, 0.31 }, { -0.02, 0.01 } },
new double[,] { { 0.31, 0.25, 0.43 }, { 0.65, 0.60, 0.52 }, { -0.39, -0.22, -0.28 }, { 0.58, 0.13, 0.58 }, { -0.01, -0.00, -0.01 } },
new double[,] { { 0.13, 0.65, 1.25 }, { -0.41, -0.39, -0.50 }, { 0.77, 0.58, 2.39 }, { -0.03, -0.01, 0.04 } },
new double[,] { { 0.31, 0.43, 1.22, 0.03 }, { 0.65, 0.52, 1.25, 0.67 }, { -0.39, -0.28, -0.50, -0.10 }, { 0.58, 0.58, 2.39, -0.41 }, { -0.01, -0.01, 0.04, 0.03 } }
};
ExpectedReturns = new List<double[]>
{
new double[] { 0.08, -0.01, 0.12 },
new double[] { 0.11, 0.23, 0.4 },
new double[] { -0.24, 0.21, 0.14 },
null,
new double[] { 0.10, 0.06 },
new double[] { 0.23, 0.15, 0.25 },
null,
new double[] { 0.23, 0.25, 0.88, 0.04 }
};
Covariances = new List<double[,]>
{
new double[,] { { 0.29, 0.13, 0.10 }, { 0.13, 0.06, 0.04 }, { 0.10, 0.04, 0.05 } },
null,
new double[,] { { 0.07, 0.12, -0.00 }, { 0.12, 0.51, 0.03 }, { -0.00, 0.03, 0.04 } },
null,
new double[,] { { 0.44, 0.15 }, { 0.15, 0.05 } },
new double[,] { { 0.19, 0.11, 0.16 }, { 0.11, 0.09, 0.09 }, { 0.16, 0.09, 0.14 } },
new double[,] { { 0.24, 0.20, 0.61 }, { 0.20, 0.25, 0.58 }, { 0.61, 0.58, 1.67 } },
new double[,] { { 0.19, 0.16, 0.44, 0.05 }, { 0.16, 0.14, 0.40, 0.02 }, { 0.44, 0.40, 1.29, -0.06 }, { 0.05, 0.02, -0.06, 0.15 } }
};
ExpectedResults = new List<double[]>
{
new double[] { -0.5, 0.5, 1 },
new double[] { 0, 0, 1 },
new double[] { -0.404692, 0.404692, 1 },
new double[] { -0.418338, 0.023261, 1, 0.040668, 0.35441 },
new double[] { 0.5, 0.5 },
new double[] { -0.670213, 0.670213, 1 },
new double[] { -1, 1, 1 },
new double[] { -1, 0.315476, 0.684524, 1 },
};
}
protected override IPortfolioOptimizer CreateOptimizer()
{
return new MaximumSharpeRatioPortfolioOptimizer();
}
[TestCase(0)]
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
[TestCase(4)]
[TestCase(5)]
[TestCase(6)]
[TestCase(7)]
public override void OptimizeWeightings(int testCaseNumber)
{
base.OptimizeWeightings(testCaseNumber);
}
[TestCase(0)]
public void OptimizeWeightingsSpecifyingLowerBoundAndRiskFreeRate(int testCaseNumber)
{
var testOptimizer = new MaximumSharpeRatioPortfolioOptimizer(lower: 0, riskFreeRate: 0.04);
var expectedResult = new double[] { 0, 0, 1 };
var result = testOptimizer.Optimize(HistoricalReturns[testCaseNumber]);
Assert.AreEqual(expectedResult, result.Select(x => Math.Round(x, 6)));
}
[Test]
public void SingleSecurityPortfolioReturnsFullWeight()
{
var testOptimizer = new MaximumSharpeRatioPortfolioOptimizer();
var historicalReturns = new double[,] { { -0.1 } };
var expectedReturns = new double[] { -0.1 };
// With a single security the budget constraint Σw = 1 leaves it fully invested
var expectedResult = new double[] { 1 };
var result = testOptimizer.Optimize(historicalReturns, expectedReturns);
Assert.AreEqual(result, expectedResult);
}
[Test]
public void EqualWeightingsWhenNoSolutionFound()
{
var testOptimizer = new MaximumSharpeRatioPortfolioOptimizer();
var historicalReturns = new double[,] { { -0.10, -0.20 }, { -0.12, -0.25 } };
var expectedReturns = new double[] { -0.10, -0.25 };
var covariance = new double[,] { { 0.25, 0.12 }, { 0.45, 0.2 } }; // non positive definite
var expectedResult = new double[] { 0.5, 0.5 };
var result = testOptimizer.Optimize(historicalReturns, expectedReturns, covariance);
Assert.AreEqual(result, expectedResult);
}
[Test]
public void BoundariesAreNotViolated()
{
var testCaseNumber = 1;
var lower = 0d;
var upper = 0.5d;
var testOptimizer = new MaximumSharpeRatioPortfolioOptimizer(lower, upper);
var result = testOptimizer.Optimize(HistoricalReturns[testCaseNumber], null, Covariances[testCaseNumber]);
foreach (var x in result)
{
var rounded = Math.Round(x, 6);
Assert.GreaterOrEqual(rounded, lower);
Assert.LessOrEqual(rounded, upper);
};
}
// Every case whose maximum Sharpe ratio portfolio is well defined. Case 1 has a
// zero-variance asset (the ratio is unbounded) and case 4 an indefinite covariance
// (it falls back to equal weights), so neither has a finite interior optimum and
// both are excluded.
[TestCase(0)]
[TestCase(2)]
[TestCase(3)]
[TestCase(5)]
[TestCase(6)]
[TestCase(7)]
public void OptimizedWeightsMaximizeSharpeRatio(int testCaseNumber)
{
// Independent of the hardcoded ExpectedResults: the returned portfolio must
// achieve a Sharpe ratio no lower than equal weights or any other feasible
// portfolio drawn from the constraint set. The mean and covariance are
// reconstructed exactly as the optimizer does so the check uses the same inputs.
var testOptimizer = new MaximumSharpeRatioPortfolioOptimizer();
var historicalReturns = HistoricalReturns[testCaseNumber];
var expectedReturns = ExpectedReturns[testCaseNumber] ?? historicalReturns.Mean(0);
var covariance = Covariances[testCaseNumber] ?? historicalReturns.Covariance();
var result = testOptimizer.Optimize(historicalReturns, ExpectedReturns[testCaseNumber], Covariances[testCaseNumber]);
var optimalSharpe = SharpeRatio(result, expectedReturns, covariance);
var size = result.Length;
var equalWeights = Enumerable.Repeat(1.0 / size, size).ToArray();
Assert.GreaterOrEqual(optimalSharpe, SharpeRatio(equalWeights, expectedReturns, covariance));
// The Sharpe ratio cannot exceed the unconstrained tangency portfolio, a hard
// analytic ceiling (Cauchy-Schwarz) that no feasible portfolio can beat.
Assert.LessOrEqual(optimalSharpe, TangencySharpe(expectedReturns, covariance) + 1e-6);
var random = new Random(0);
for (var i = 0; i < 10000; i++)
{
var candidate = RandomFeasibleWeights(random, size, lower: -1.0, upper: 1.0);
Assert.GreaterOrEqual(optimalSharpe + 1e-6, SharpeRatio(candidate, expectedReturns, covariance));
}
}
private static double SharpeRatio(double[] weights, double[] expectedReturns, double[,] covariance)
{
var size = weights.Length;
var portfolioReturn = 0.0;
var portfolioVariance = 0.0;
for (var i = 0; i < size; i++)
{
portfolioReturn += weights[i] * expectedReturns[i];
for (var j = 0; j < size; j++)
{
portfolioVariance += weights[i] * covariance[i, j] * weights[j];
}
}
return portfolioReturn / Math.Sqrt(portfolioVariance);
}
private static double TangencySharpe(double[] expectedReturns, double[,] covariance)
{
// The unconstrained maximum Sharpe ratio is sqrt(mu' inv(S) mu), the largest
// value any portfolio can reach regardless of the budget or weight bounds.
var inverse = covariance.Inverse();
var size = expectedReturns.Length;
var quadraticForm = 0.0;
for (var i = 0; i < size; i++)
{
for (var j = 0; j < size; j++)
{
quadraticForm += expectedReturns[i] * inverse[i, j] * expectedReturns[j];
}
}
return Math.Sqrt(quadraticForm);
}
private static double[] RandomFeasibleWeights(Random random, int size, double lower, double upper)
{
// Draw weights uniformly from the box and keep only those summing to one.
while (true)
{
var weights = new double[size];
var sum = 0.0;
for (var i = 0; i < size - 1; i++)
{
weights[i] = lower + random.NextDouble() * (upper - lower);
sum += weights[i];
}
var last = 1.0 - sum;
if (last >= lower && last <= upper)
{
weights[size - 1] = last;
return weights;
}
}
}
}
}
@@ -0,0 +1,401 @@
/*
* 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 aaplicable 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 MeanReversionPortfolioConstructionModelTests
{
private DateTime _nowUtc;
private QCAlgorithm _algorithm;
private List<double> _simplexTestArray;
private double[] _simplexExpectedArray1, _simplexExpectedArray2;
[SetUp]
public virtual void SetUp()
{
_nowUtc = new DateTime(2021, 1, 10);
_algorithm = new AlgorithmStub();
_algorithm.SetFinishedWarmingUp();
_algorithm.Settings.MinimumOrderMarginPortfolioPercentage = 0;
_algorithm.Settings.FreePortfolioValue = 250;
_algorithm.SetDateTime(_nowUtc.ConvertToUtc(_algorithm.TimeZone));
_algorithm.SetCash(1200);
var historyProvider = new SubscriptionDataReaderHistoryProvider();
_algorithm.SetHistoryProvider(historyProvider);
historyProvider.Initialize(new HistoryProviderInitializeParameters(
new BacktestNodePacket(),
null,
TestGlobals.DataProvider,
TestGlobals.DataCacheProvider,
TestGlobals.MapFileProvider,
TestGlobals.FactorFileProvider,
i => { },
true,
new DataPermissionManager(),
_algorithm.ObjectStore,
_algorithm.Settings));
_simplexTestArray = new List<double> {0.2d, 0.5d, 0.4d, -0.1d, 0d};
_simplexExpectedArray1 = new double[] {1d/6, 7d/15, 11d/30, 0d, 0d};
_simplexExpectedArray2 = new double[] {0d, 0.3d, 0.2d, 0d, 0d};
}
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void DoesNotReturnTargetsIfSecurityPriceIsZero(Language language)
{
_algorithm.AddEquity(Symbols.SPY.Value);
_algorithm.SetDateTime(_nowUtc.ConvertToUtc(_algorithm.TimeZone));
SetPortfolioConstruction(language, PortfolioBias.Long);
var insights = new[] { new Insight(_nowUtc, Symbols.SPY, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, null, null) };
var actualTargets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights);
Assert.AreEqual(0, actualTargets.Count());
}
[TestCase(Language.CSharp, PortfolioBias.Long)]
[TestCase(Language.Python, PortfolioBias.Long)]
[TestCase(Language.CSharp, PortfolioBias.Short)]
[TestCase(Language.Python, PortfolioBias.Short)]
public void PortfolioBiasIsRespected(Language language, PortfolioBias bias)
{
if (bias == PortfolioBias.Short)
{
var throwsConstraint = language == Language.CSharp
? Throws.InstanceOf<ArgumentException>()
: Throws.InstanceOf<ClrBubbledException>().With.InnerException.InstanceOf<ArgumentException>();
Assert.That(() => GetPortfolioConstructionModel(language, bias, Resolution.Daily),
throwsConstraint.And.Message.EqualTo("Long position must be allowed in MeanReversionPortfolioConstructionModel."));
return;
}
var targets = GeneratePortfolioTargets(language, InsightDirection.Up, InsightDirection.Up, 1, 1);
foreach (var target in targets)
{
if (target.Quantity == 0)
{
continue;
}
Assert.AreEqual(Math.Sign((int)bias), Math.Sign(target.Quantity));
}
}
[TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, null, null, 47, 47)]
[TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, null, null, 47, 47)]
[TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, 0, 0, 47, 47)]
[TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, 0, 0, 47, 47)]
[TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, 1, -0.5, 31, 63)]
[TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, 1, -0.5, 31, 63)]
[TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Down, 1, 0.5, 31, 63)]
[TestCase(Language.Python, InsightDirection.Up, InsightDirection.Down, 1, 0.5, 31, 63)]
[TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, 0, -0.5, 47, 47)]
[TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, 0, -0.5, 47, 47)]
[TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, 0, 1, 94, 0)]
[TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, 0, 1, 94, 0)]
[TestCase(Language.CSharp, InsightDirection.Up, InsightDirection.Up, 0.5, -1, 47, 47)]
[TestCase(Language.Python, InsightDirection.Up, InsightDirection.Up, 0.5, -1, 47, 47)]
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);
var quantities = targets.ToDictionary(target => {
QuantConnect.Logging.Log.Debug($"{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]
public void CumulativeSum()
{
var list = new List<double>{1.1d, 2.5d, 0.7d, 13.6d, -5.2d, 3.9d, -1.6d};
var expected = new List<double>{1.1d, 3.6d, 4.3d, 17.9d, 12.7d, 16.6d, 15.0d};
var result = MeanReversionPortfolioConstructionModel.CumulativeSum(list)
.Select(x => Math.Round(x, 1));
Assert.AreEqual(expected, result);
}
[Test]
public void GetPriceRelatives()
{
var model = new TestMeanReversionPortfolioConstructionModel();
SetPortfolioConstruction(Language.CSharp, PortfolioBias.Long, model);
var aapl = _algorithm.AddEquity("AAPL");
var spy = _algorithm.AddEquity("SPY");
var insights = new List<Insight>
{
new Insight(_nowUtc, aapl.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, null, null),
new Insight(_nowUtc, spy.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, null, null),
};
_algorithm.PortfolioConstruction.OnSecuritiesChanged(_algorithm, SecurityChangesTests.AddedNonInternal(aapl, spy));
var history = _algorithm.History<TradeBar>(new[] {aapl.Symbol, spy.Symbol}, 2, Resolution.Daily);
var aaplHist = history.Select(slice => slice[aapl.Symbol].Close);
var spyHist = history.Select(slice => slice[spy.Symbol].Close);
var aaplRelative = (double) (aaplHist.Last() / aaplHist.Average());
var spyRelative = (double) (spyHist.Last() / spyHist.Average());
var result = model.TestGetPriceRelatives(insights).Select(x => Math.Round(x, 8)).ToArray();
var expected = new double[] {aaplRelative, spyRelative};
expected = expected.Select(x => Math.Round(x, 8)).ToArray();
Assert.AreEqual(expected, result);
}
[Test]
public void GetPriceRelativesPython()
{
SetPortfolioConstruction(Language.Python, PortfolioBias.Long);
var aapl = _algorithm.AddEquity("AAPL");
var spy = _algorithm.AddEquity("SPY");
var insights = new List<Insight>
{
new Insight(_nowUtc, aapl.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, null, null),
new Insight(_nowUtc, spy.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, null, null),
};
var history = _algorithm.History<TradeBar>(new[] {aapl.Symbol, spy.Symbol}, 2, Resolution.Daily);
var aaplHist = history.Select(slice => slice[aapl.Symbol].Close);
var spyHist = history.Select(slice => slice[spy.Symbol].Close);
var aaplRelative = (double) (aaplHist.Last() / aaplHist.Average());
var spyRelative = (double) (spyHist.Last() / spyHist.Average());
using (Py.GIL())
{
const string name = nameof(MeanReversionPortfolioConstructionModel);
var model = Py.Import(name).GetAttr(name).Invoke(((int)Resolution.Daily).ToPython(), ((int)PortfolioBias.LongShort).ToPython(), 1.ToPython(), 2.ToPython());
model.InvokeMethod("OnSecuritiesChanged", _algorithm.ToPython(), SecurityChangesTests.AddedNonInternal(aapl, spy).ToPython());
var result = PyList.AsList(model.InvokeMethod("GetPriceRelatives", insights.ToPython()));
var resultArray = result.Select(x => Math.Round(Convert.ToDouble(x), 8)).ToArray();
var expected = new double[] {aaplRelative, spyRelative};
expected = expected.Select(x => Math.Round(x, 8)).ToArray();
Assert.AreEqual(expected, resultArray);
}
}
[Test]
public void GetPriceRelativesWithInsightMagnitude()
{
var model = new TestMeanReversionPortfolioConstructionModel();
SetPortfolioConstruction(Language.CSharp, PortfolioBias.Long, model);
var aapl = _algorithm.AddEquity("AAPL");
var spy = _algorithm.AddEquity("SPY");
var insights = new List<Insight>
{
new Insight(_nowUtc, aapl.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, 1, null),
new Insight(_nowUtc, spy.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, -0.5, null),
};
_algorithm.PortfolioConstruction.OnSecuritiesChanged(_algorithm, SecurityChangesTests.AddedNonInternal(aapl, spy));
var result = model.TestGetPriceRelatives(insights);
var expected = new double[] {2d, 0.5d};
Assert.AreEqual(expected, result);
}
[Test]
public void GetPriceRelativesWithInsightMagnitudePython()
{
SetPortfolioConstruction(Language.Python, PortfolioBias.Long);
var aapl = _algorithm.AddEquity("AAPL");
var spy = _algorithm.AddEquity("SPY");
var insights = new List<Insight>
{
new Insight(_nowUtc, aapl.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, 1, null),
new Insight(_nowUtc, spy.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, -0.5, null),
};
using (Py.GIL())
{
const string name = nameof(MeanReversionPortfolioConstructionModel);
var model = Py.Import(name).GetAttr(name).Invoke(((int)Resolution.Daily).ToPython());
model.InvokeMethod("OnSecuritiesChanged", _algorithm.ToPython(), SecurityChangesTests.AddedNonInternal(aapl, spy).ToPython());
var result = PyList.AsList(model.InvokeMethod("GetPriceRelatives", insights.ToPython()));
var resultArray = result.Select(x => Convert.ToDouble(x)).ToArray();
var expected = new double[] {2d, 0.5d};
Assert.AreEqual(expected, resultArray);
}
}
[TestCase(1)]
[TestCase(0.5)]
[TestCase(0)]
[TestCase(-0.5)]
public void SimplexProjection(double regulator)
{
if (regulator <= 0)
{
var exception = Assert.Throws<ArgumentException>(() => MeanReversionPortfolioConstructionModel.SimplexProjection(_simplexTestArray, regulator));
Assert.That(exception.Message, Is.EqualTo("Total must be > 0 for Euclidean Projection onto the Simplex."));
return;
}
double[] expected;
if (regulator == 1d)
{
expected = _simplexExpectedArray1;
}
else
{
expected = _simplexExpectedArray2;
}
expected = expected.Select(x => Math.Round(x, 8)).ToArray();
var result = MeanReversionPortfolioConstructionModel.SimplexProjection(_simplexTestArray, regulator);
result = result.Select(x => Math.Round(x, 8)).ToArray();
Assert.AreEqual(expected, result);
}
[TestCase(1)]
[TestCase(0.5)]
[TestCase(0)]
[TestCase(-0.5)]
public void SimplexProjectionPython(double regulator)
{
using (Py.GIL())
{
const string name = nameof(MeanReversionPortfolioConstructionModel);
var model = Py.Import(name).GetAttr(name).Invoke(((int)Resolution.Daily).ToPython());
if (regulator <= 0)
{
Assert.That(() => model.InvokeMethod("SimplexProjection", _simplexTestArray.ToPython(), new PyFloat(regulator)),
Throws.InstanceOf<ClrBubbledException>()
.With.InnerException.InstanceOf<ArgumentException>()
.And.Message.EqualTo("Total must be > 0 for Euclidean Projection onto the Simplex."));
return;
}
double[] expected;
if (regulator == 1d)
{
expected = _simplexExpectedArray1;
}
else
{
expected = _simplexExpectedArray2;
}
var expectedArray = expected.Select(x => Math.Round(x, 8)).ToArray();
var result = PyList.AsList(model.InvokeMethod("SimplexProjection", _simplexTestArray.ToPython(), new PyFloat(regulator)));
var resultArray = result.Select(x => Math.Round(Convert.ToDouble(x), 8)).ToArray();
Assert.AreEqual(expectedArray, resultArray);
}
}
private IEnumerable<IPortfolioTarget> GeneratePortfolioTargets(Language language, InsightDirection direction1, InsightDirection direction2, double? magnitude1, double? magnitude2)
{
SetPortfolioConstruction(language, PortfolioBias.Long);
var aapl = _algorithm.AddEquity("AAPL");
var spy = _algorithm.AddEquity("SPY");
foreach (var equity in new[] { aapl, spy })
{
equity.SetMarketPrice(new Tick(_nowUtc, equity.Symbol, 10, 10));
}
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);
}
protected void SetPortfolioConstruction(Language language, PortfolioBias bias, IPortfolioConstructionModel defaultModel = null)
{
var model = defaultModel ?? GetPortfolioConstructionModel(language, bias, Resolution.Daily);
_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, PortfolioBias bias, Resolution resolution)
{
if (language == Language.CSharp)
{
return new MeanReversionPortfolioConstructionModel(resolution, bias, 1, 1, resolution);
}
using (Py.GIL())
{
const string name = nameof(MeanReversionPortfolioConstructionModel);
var instance = Py.Import(name).GetAttr(name)
.Invoke(((int)resolution).ToPython(), ((int)bias).ToPython(), 1.ToPython(), 1.ToPython(), ((int)resolution).ToPython());
return new PortfolioConstructionModelPythonWrapper(instance);
}
}
private class TestMeanReversionPortfolioConstructionModel : MeanReversionPortfolioConstructionModel
{
public TestMeanReversionPortfolioConstructionModel()
: base(Resolution.Daily, windowSize: 2)
{
}
public double[] TestGetPriceRelatives(List<Insight> insights)
{
return base.GetPriceRelatives(insights);
}
}
}
}
@@ -0,0 +1,324 @@
/*
* 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);
}
}
}
@@ -0,0 +1,181 @@
/*
* 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 aaplicable 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 QuantConnect.Algorithm.Framework.Portfolio;
using System;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class MinimumVariancePortfolioOptimizerTests : PortfolioOptimizerTestsBase
{
private Dictionary<int, double> _targetReturns;
[OneTimeSetUp]
public void Setup()
{
var historicalReturns1 = new double[,] { { 0.76, -0.06, 1.22, 0.17 }, { 0.02, 0.28, 1.25, -0.00 }, { -0.50, -0.13, -0.50, -0.03 }, { 0.81, 0.31, 2.39, 0.26 }, { -0.02, 0.02, 0.06, 0.01 } };
var historicalReturns2 = new double[,] { { -0.15, 0.67, 0.45 }, { -0.44, -0.10, 0.07 }, { 0.04, -0.41, 0.01 }, { 0.01, 0.03, 0.02 } };
var historicalReturns3 = new double[,] { { -0.02, 0.65, 1.25 }, { -0.29, -0.39, -0.50 }, { 0.29, 0.58, 2.39 }, { 0.00, -0.01, 0.06 } };
var historicalReturns4 = new double[,] { { 0.76, 0.25, 0.21 }, { 0.02, -0.15, 0.45 }, { -0.50, -0.44, 0.07 }, { 0.81, 0.04, 0.01 }, { -0.02, 0.01, 0.02 } };
var expectedReturns1 = new double[] { 0.21, 0.08, 0.88, 0.08 };
var expectedReturns2 = new double[] { -0.13, 0.05, 0.14 };
var expectedReturns3 = (double[])null;
var expectedReturns4 = (double[])null;
var covariance1 = new double[,] { { 0.31, 0.05, 0.55, 0.07 }, { 0.05, 0.04, 0.18, 0.01 }, { 0.55, 0.18, 1.28, 0.12 }, { 0.07, 0.01, 0.12, 0.02 } };
var covariance2 = new double[,] { { 0.05, -0.02, -0.01 }, { -0.02, 0.21, 0.09 }, { -0.01, 0.09, 0.04 } };
var covariance3 = new double[,] { { 0.06, 0.09, 0.28 }, { 0.09, 0.25, 0.58 }, { 0.28, 0.58, 1.66 } };
var covariance4 = (double[,])null;
HistoricalReturns = new List<double[,]>
{
historicalReturns1,
historicalReturns2,
historicalReturns3,
historicalReturns4,
historicalReturns1,
historicalReturns2,
historicalReturns3,
historicalReturns4
};
ExpectedReturns = new List<double[]>
{
expectedReturns1,
expectedReturns2,
expectedReturns3,
expectedReturns4,
expectedReturns1,
expectedReturns2,
expectedReturns3,
expectedReturns4
};
Covariances = new List<double[,]>
{
covariance1,
covariance2,
covariance3,
covariance4,
covariance1,
covariance2,
covariance3,
covariance4
};
ExpectedResults = new List<double[]>
{
new double[] { -0.089212, 0.23431, -0.040975, 0.635503 },
new double[] { 0.366812, -0.139738, 0.49345 },
new double[] { 0.562216, 0.36747, -0.070314 },
new double[] { -0.119241, 0.443464, 0.437295 },
new double[] { -0.215505, 0.130699, 0.084806, 0.56899 },
new double[] { -0.275, 0.275, 0.45 },
new double[] { -0.129512, 0.551139, 0.319349 },
new double[] { 0.052859, 0.144177, 0.802964 },
};
_targetReturns = new Dictionary<int, double>
{
{ 4, 0.15d },
{ 5, 0.25d },
{ 6, 0.5d },
{ 7, 0.125d }
};
}
protected override IPortfolioOptimizer CreateOptimizer()
{
return new MinimumVariancePortfolioOptimizer();
}
[TestCase(0)]
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
public override void OptimizeWeightings(int testCaseNumber)
{
base.OptimizeWeightings(testCaseNumber);
}
[TestCase(4)]
[TestCase(5)]
[TestCase(6)]
[TestCase(7)]
public void OptimizeWeightingsSpecifyingTargetReturns(int testCaseNumber)
{
var testOptimizer = new MinimumVariancePortfolioOptimizer(targetReturn: _targetReturns[testCaseNumber]);
var result = testOptimizer.Optimize(
HistoricalReturns[testCaseNumber],
ExpectedReturns[testCaseNumber],
Covariances[testCaseNumber]);
Assert.AreEqual(ExpectedResults[testCaseNumber], result.Select(x => Math.Round(x, 6)));
Assert.AreEqual(1d, result.Select(x => Math.Round(Math.Abs(x), 6)).Sum());
}
[TestCase(0)]
public void EqualWeightingsWhenNoSolutionFound(int testCaseNumber)
{
var testOptimizer = new MinimumVariancePortfolioOptimizer(upper: -1);
var expectedResult = new double[] { 0.25, 0.25, 0.25, 0.25 };
var result = testOptimizer.Optimize(HistoricalReturns[testCaseNumber]);
Assert.AreEqual(expectedResult, result);
}
[TestCase(0)]
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
public void BoundariesAreNotViolated(int testCaseNumber)
{
var lower = 0d;
var upper = 0.5d;
var testOptimizer = new MinimumVariancePortfolioOptimizer(lower, upper);
var result = testOptimizer.Optimize(
HistoricalReturns[testCaseNumber],
ExpectedReturns[testCaseNumber],
Covariances[testCaseNumber]);
foreach (var x in result)
{
var rounded = Math.Round(x, 6);
Assert.GreaterOrEqual(rounded, lower);
Assert.LessOrEqual(rounded, upper);
};
}
[Test]
public void SingleSecurityPortfolioReturnsOne()
{
var testOptimizer = new MinimumVariancePortfolioOptimizer();
var historicalReturns = new double[,] { { 0.76 }, { 0.02 }, { -0.50 } };
var expectedResult = new double[] { 1 };
var result = testOptimizer.Optimize(historicalReturns);
Assert.AreEqual(expectedResult, result);
}
}
}
@@ -0,0 +1,167 @@
/*
* 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.Linq;
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 QuantConnect.Tests.Common.Data.UniverseSelection;
using QuantConnect.Tests.Engine.DataFeeds;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class PortfolioConstructionModelPythonWrapperTests
{
[Test]
public void PythonCompleteImplementation()
{
var algorithm = new AlgorithmStub();
using (Py.GIL())
{
dynamic model = PyModule.FromString(
"TestPCM",
@"
from AlgorithmImports import *
class PyPCM(EqualWeightingPortfolioConstructionModel):
def __init__(self):
super().__init__()
self.CreateTargets_WasCalled = False
self.OnSecuritiesChanged_WasCalled = False
self.ShouldCreateTargetForInsight_WasCalled = False
self.IsRebalanceDue_WasCalled = False
self.GetTargetInsights_WasCalled = False
self.DetermineTargetPercent_WasCalled = False
def CreateTargets(self, algorithm, insights):
self.CreateTargets_WasCalled = True
return super().CreateTargets(algorithm, insights)
def OnSecuritiesChanged(self, algorithm, changes):
self.OnSecuritiesChanged_WasCalled = True
super().OnSecuritiesChanged(algorithm, changes)
def ShouldCreateTargetForInsight(self, insight):
self.ShouldCreateTargetForInsight_WasCalled = True
return super().ShouldCreateTargetForInsight(insight)
def IsRebalanceDue(self, insights, algorithmUtc):
self.IsRebalanceDue_WasCalled = True
return True
def GetTargetInsights(self):
self.GetTargetInsights_WasCalled = True
return super().GetTargetInsights()
def DetermineTargetPercent(self, activeInsights):
self.DetermineTargetPercent_WasCalled = True
return super().DetermineTargetPercent(activeInsights)
"
).GetAttr("PyPCM").Invoke();
var now = new DateTime(2020, 1, 10);
var wrappedModel = new PortfolioConstructionModelPythonWrapper(model);
var aapl = algorithm.AddEquity("AAPL");
aapl.SetMarketPrice(new Tick(now, aapl.Symbol, 10, 10));
algorithm.SetDateTime(now);
wrappedModel.OnSecuritiesChanged(algorithm, new SecurityChanges(SecurityChangesTests.AddedNonInternal(aapl)));
Assert.IsTrue((bool)model.OnSecuritiesChanged_WasCalled);
var insight = new Insight(now, aapl.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Down, null, null);
algorithm.Insights.Add(insight);
var result = wrappedModel.CreateTargets(algorithm, new[] { insight }).ToList();
Assert.AreEqual(1, result.Count);
Assert.IsTrue((bool)model.CreateTargets_WasCalled);
Assert.IsTrue((bool)model.GetTargetInsights_WasCalled);
Assert.IsTrue((bool)model.IsRebalanceDue_WasCalled);
Assert.IsTrue((bool)model.ShouldCreateTargetForInsight_WasCalled);
Assert.IsTrue((bool)model.DetermineTargetPercent_WasCalled);
}
}
[Test]
public void PythonDoesNotImplementDetermineTargetPercent()
{
var algorithm = new AlgorithmStub();
using (Py.GIL())
{
dynamic model = PyModule.FromString(
"TestPCM",
@"
from clr import AddReference
AddReference(""QuantConnect.Algorithm.Framework"")
from QuantConnect.Algorithm.Framework.Portfolio import *
class PyPCM(EqualWeightingPortfolioConstructionModel):
def __init__(self):
super().__init__()
self.CreateTargets_WasCalled = False
self.OnSecuritiesChanged_WasCalled = False
self.ShouldCreateTargetForInsight_WasCalled = False
self.IsRebalanceDue_WasCalled = False
self.GetTargetInsights_WasCalled = False
def CreateTargets(self, algorithm, insights):
self.CreateTargets_WasCalled = True
return super().CreateTargets(algorithm, insights)
def OnSecuritiesChanged(self, algorithm, changes):
self.OnSecuritiesChanged_WasCalled = True
super().OnSecuritiesChanged(algorithm, changes)
def ShouldCreateTargetForInsight(self, insight):
self.ShouldCreateTargetForInsight_WasCalled = True
return super().ShouldCreateTargetForInsight(insight)
def IsRebalanceDue(self, insights, algorithmUtc):
self.IsRebalanceDue_WasCalled = True
return True
def GetTargetInsights(self):
self.GetTargetInsights_WasCalled = True
return super().GetTargetInsights()
"
).GetAttr("PyPCM").Invoke();
var now = new DateTime(2020, 1, 10);
var wrappedModel = new PortfolioConstructionModelPythonWrapper(model);
var aapl = algorithm.AddEquity("AAPL");
aapl.SetMarketPrice(new Tick(now, aapl.Symbol, 10, 10));
algorithm.SetDateTime(now);
wrappedModel.OnSecuritiesChanged(algorithm, new SecurityChanges(SecurityChangesTests.AddedNonInternal(aapl)));
Assert.IsTrue((bool)model.OnSecuritiesChanged_WasCalled);
var insight = new Insight(now, aapl.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Down, null, null);
algorithm.Insights.Add(insight);
var result = wrappedModel.CreateTargets(algorithm, new[] { insight }).ToList();
Assert.AreEqual(1, result.Count);
Assert.IsTrue((bool)model.CreateTargets_WasCalled);
Assert.IsTrue((bool)model.GetTargetInsights_WasCalled);
Assert.IsTrue((bool)model.IsRebalanceDue_WasCalled);
Assert.IsTrue((bool)model.ShouldCreateTargetForInsight_WasCalled);
}
}
}
}
@@ -0,0 +1,433 @@
/*
* 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));
}
}
}
}
@@ -0,0 +1,76 @@
/*
* 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 aaplicable 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.Portfolio;
using System;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio;
[TestFixture]
public class PortfolioOptimizerPythonWrapperTests
{
[Test]
public void OptimizeIsCalled()
{
using (Py.GIL())
{
var module = PyModule.FromString(Guid.NewGuid().ToString(),
@$"
from AlgorithmImports import *
class CustomPortfolioOptimizer:
def __init__(self):
self.OptimizeWasCalled = False
def Optimize(self, historicalReturns, expectedReturns = None, covariance = None):
self.OptimizeWasCalled= True");
var pyCustomOptimizer = module.GetAttr("CustomPortfolioOptimizer").Invoke();
var wrapper = new PortfolioOptimizerPythonWrapper(pyCustomOptimizer);
var historicalReturns = new double[,] { { -0.50, -0.13 }, { 0.81, 0.31 }, { -0.02, 0.01 } };
wrapper.Optimize(historicalReturns);
pyCustomOptimizer
.GetAttr("OptimizeWasCalled")
.TryConvert(out bool optimizerWasCalled);
Assert.IsTrue(optimizerWasCalled);
}
}
[Test]
public void WrapperThrowsIfOptimizerDoesNotImplementInterface()
{
using (Py.GIL())
{
var module = PyModule.FromString(Guid.NewGuid().ToString(),
@$"
from AlgorithmImports import *
class CustomPortfolioOptimizer:
def __init__(self):
self.OptimizeWasCalled = False
def Calculate(self, historicalReturns, expectedReturns = None, covariance = None):
pass");
var pyCustomOptimizer = module.GetAttr("CustomPortfolioOptimizer").Invoke();
Assert.Throws<NotImplementedException>(() => new PortfolioOptimizerPythonWrapper(pyCustomOptimizer));
}
}
}
@@ -0,0 +1,59 @@
/*
* 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 aaplicable 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 QuantConnect.Algorithm.Framework.Portfolio;
using System;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio;
public abstract class PortfolioOptimizerTestsBase
{
protected IList<double[,]> HistoricalReturns { get; set; }
protected IList<double[]> ExpectedReturns { get; set; }
protected IList<double[,]> Covariances { get; set; }
protected IList<double[]> ExpectedResults { get; set; }
protected abstract IPortfolioOptimizer CreateOptimizer();
public virtual void OptimizeWeightings(int testCaseNumber)
{
var testOptimizer = CreateOptimizer();
var result = testOptimizer.Optimize(
HistoricalReturns[testCaseNumber],
ExpectedReturns[testCaseNumber],
Covariances[testCaseNumber]);
Assert.AreEqual(ExpectedResults[testCaseNumber], result.Select(x => Math.Round(x, 6)));
}
[Test]
public virtual void EmptyPortfolioReturnsEmptyArrayOfDouble()
{
var testOptimizer = CreateOptimizer();
var historicalReturns = new double[,] { { } };
var expectedResult = Array.Empty<double>();
var result = testOptimizer.Optimize(historicalReturns);
Assert.AreEqual(result, expectedResult);
}
}
@@ -0,0 +1,345 @@
/*
* 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.Linq;
using NUnit.Framework;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Data.Market;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Tests.Common.Securities;
using QuantConnect.Tests.Engine.DataFeeds;
using QuantConnect.Util;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class PortfolioTargetCollectionTests
{
private string _symbol = "SPY";
[Test]
public void TryGetValue()
{
var collection = new PortfolioTargetCollection();
Assert.IsFalse(collection.TryGetValue(Symbols.SPY, out var target));
collection[Symbols.SPY] = new PortfolioTarget(Symbols.SPY, 1);
Assert.IsTrue(collection.TryGetValue(Symbols.SPY, out target));
Assert.AreEqual(target, collection[Symbols.SPY]);
}
[Test]
public void IndexAccess()
{
var collection = new PortfolioTargetCollection();
collection[Symbols.SPY] = new PortfolioTarget(Symbols.SPY, 1);
Assert.AreEqual(1, collection.Count);
Assert.AreEqual(1, collection.Values.Count);
Assert.AreEqual(1, collection.Keys.Count);
collection[Symbols.IBM] = new PortfolioTarget(Symbols.IBM, 1);
Assert.AreEqual(2, collection.Count);
Assert.AreEqual(2, collection.Values.Count);
Assert.AreEqual(2, collection.Keys.Count);
collection[Symbols.IBM] = null;
Assert.AreEqual(2, collection.Count);
Assert.AreEqual(2, collection.Values.Count);
Assert.AreEqual(2, collection.Keys.Count);
}
[Test]
public void Count()
{
var collection = new PortfolioTargetCollection();
var targets = new[] { new PortfolioTarget(Symbols.SPY, 1) };
collection.AddRange(targets);
Assert.AreEqual(1, collection.Count);
collection.AddRange(new[] { new PortfolioTarget(Symbols.IBM, 1), new PortfolioTarget(Symbols.AAPL, 1) });
Assert.AreEqual(3, collection.Count);
collection.Clear();
}
[Test]
public void IsEmpty()
{
var collection = new PortfolioTargetCollection();
Assert.IsTrue(collection.IsEmpty);
Assert.IsFalse(collection.ContainsKey(Symbols.SPY));
collection.Add(new PortfolioTarget(Symbols.SPY, 1));
Assert.AreEqual(1, collection.Count);
Assert.IsFalse(collection.IsEmpty);
Assert.IsTrue(collection.ContainsKey(Symbols.SPY));
}
[Test]
public void AddRange()
{
var collection = new PortfolioTargetCollection();
var targets = new[] { new PortfolioTarget(Symbols.SPY, 1), new PortfolioTarget(Symbols.AAPL, 1) };
collection.AddRange(targets);
Assert.AreEqual(2, collection.Count);
Assert.IsTrue(collection.ContainsKey(Symbols.SPY));
Assert.IsTrue(collection.ContainsKey(Symbols.AAPL));
Assert.AreEqual(targets[0], collection[Symbols.SPY]);
Assert.AreEqual(targets[1], collection[Symbols.AAPL]);
Assert.AreEqual(1, collection.Values.Count(target => target == targets[0]));
Assert.AreEqual(1, collection.Values.Count(target => target == targets[1]));
Assert.AreEqual(1, collection.Keys.Count(symbol => symbol == Symbols.SPY));
Assert.AreEqual(1, collection.Keys.Count(symbol => symbol == Symbols.AAPL));
}
[Test]
public void RemoveTargetRespectsReference()
{
var symbol = new Symbol(SecurityIdentifier.GenerateBase(null, _symbol, Market.USA), _symbol);
var collection = new PortfolioTargetCollection();
var target = new PortfolioTarget(symbol, 1);
collection.Add(target);
Assert.AreEqual(collection.Count, 1);
Assert.IsTrue(collection.Contains(target));
// removes by reference even if same symbol
Assert.IsFalse(collection.Remove(new PortfolioTarget(symbol, 1)));
Assert.AreEqual(collection.Count, 1);
}
[Test]
public void AddContainsAndRemoveWork()
{
var symbol = new Symbol(SecurityIdentifier.GenerateBase(null, _symbol, Market.USA), _symbol);
var collection = new PortfolioTargetCollection();
var target = new PortfolioTarget(symbol, 1);
collection.Add(target);
Assert.AreEqual(collection.Count, 1);
Assert.IsTrue(collection.Contains(target));
Assert.IsTrue(collection.Remove(target));
Assert.AreEqual(collection.Count, 0);
}
[Test]
public void ClearFulfilledDoesNotRemoveUnreachedTarget()
{
var algorithm = new FakeAlgorithm();
algorithm.SetFinishedWarmingUp();
var symbol = new Symbol(SecurityIdentifier.GenerateEquity(_symbol, Market.USA), _symbol);
#pragma warning disable CS0618
var equity = algorithm.AddEquity(symbol);
var dummySecurityHolding = new FakeSecurityHolding(equity);
equity.Holdings = dummySecurityHolding;
var collection = new PortfolioTargetCollection();
var target = new PortfolioTarget(symbol, -1);
collection.Add(target);
collection.ClearFulfilled(algorithm);
Assert.AreEqual(collection.Count, 1);
}
[Test]
public void ClearRemovesUnreachedTarget()
{
var algorithm = new FakeAlgorithm();
algorithm.SetFinishedWarmingUp();
var symbol = new Symbol(SecurityIdentifier.GenerateEquity(_symbol, Market.USA), _symbol);
var equity = algorithm.AddEquity(symbol);
var dummySecurityHolding = new FakeSecurityHolding(equity);
equity.Holdings = dummySecurityHolding;
var collection = new PortfolioTargetCollection();
var target = new PortfolioTarget(symbol, -1);
collection.Add(target);
collection.Clear();
Assert.AreEqual(collection.Count, 0);
}
[Test]
public void ClearFulfilledRemovesPositiveTarget()
{
var algorithm = new FakeAlgorithm();
algorithm.SetFinishedWarmingUp();
var symbol = new Symbol(SecurityIdentifier.GenerateEquity(_symbol, Market.USA), _symbol);
var equity = algorithm.AddEquity(symbol);
var dummySecurityHolding = new FakeSecurityHolding(equity);
equity.Holdings = dummySecurityHolding;
var collection = new PortfolioTargetCollection();
var target = new PortfolioTarget(symbol, 1);
collection.Add(target);
dummySecurityHolding.SetQuantity(1);
collection.ClearFulfilled(algorithm);
Assert.AreEqual(collection.Count, 0);
}
[Test]
public void ClearFulfilledRemovesNegativeTarget()
{
var algorithm = new FakeAlgorithm();
algorithm.SetFinishedWarmingUp();
var symbol = new Symbol(SecurityIdentifier.GenerateEquity(_symbol, Market.USA), _symbol);
var equity = algorithm.AddEquity(symbol);
var dummySecurityHolding = new FakeSecurityHolding(equity);
equity.Holdings = dummySecurityHolding;
var collection = new PortfolioTargetCollection();
var target = new PortfolioTarget(symbol, -1);
collection.Add(target);
dummySecurityHolding.SetQuantity(-1);
collection.ClearFulfilled(algorithm);
Assert.AreEqual(collection.Count, 0);
}
[Test]
public void OrderByMarginImpactDoesNotReturnTargetsWithNoData()
{
var algorithm = new FakeAlgorithm();
algorithm.SetFinishedWarmingUp();
algorithm.Transactions.SetOrderProcessor(new FakeOrderProcessor());
var symbol = new Symbol(SecurityIdentifier.GenerateEquity(_symbol, Market.USA), _symbol);
algorithm.AddEquity(symbol);
var collection = new PortfolioTargetCollection();
var target = new PortfolioTarget(symbol, -1);
collection.Add(target);
var targets = collection.OrderByMarginImpact(algorithm);
Assert.AreEqual(collection.Count, 1);
Assert.IsTrue(targets.IsNullOrEmpty());
}
[Test]
public void OrderByMarginImpactReturnsExpectedTargets()
{
var algorithm = new FakeAlgorithm();
algorithm.SetFinishedWarmingUp();
algorithm.Transactions.SetOrderProcessor(new FakeOrderProcessor());
var symbol = new Symbol(SecurityIdentifier.GenerateEquity(_symbol, Market.USA), _symbol);
var equity = algorithm.AddEquity(symbol);
equity.Cache.AddData(new TradeBar(DateTime.UtcNow, symbol, 1, 1, 1, 1, 1));
var collection = new PortfolioTargetCollection();
var target = new PortfolioTarget(symbol, -1);
collection.Add(target);
var targets = collection.OrderByMarginImpact(algorithm);
Assert.AreEqual(collection.Count, 1);
Assert.AreEqual(targets.Count(), 1);
Assert.AreEqual(targets.First(), target);
}
[Test]
public void OrderByMarginImpactDoesNotReturnTargetsForWhichUnorderedQuantityIsZeroBecauseTargetIsZero()
{
var algorithm = new FakeAlgorithm();
algorithm.SetFinishedWarmingUp();
algorithm.Transactions.SetOrderProcessor(new FakeOrderProcessor());
var symbol = new Symbol(SecurityIdentifier.GenerateEquity(_symbol, Market.USA), _symbol);
var equity = algorithm.AddEquity(symbol);
equity.Cache.AddData(new TradeBar(DateTime.UtcNow, symbol, 1, 1, 1, 1, 1));
var collection = new PortfolioTargetCollection();
var target = new PortfolioTarget(symbol, 0);
collection.Add(target);
var targets = collection.OrderByMarginImpact(algorithm);
Assert.AreEqual(collection.Count, 1);
Assert.IsTrue(targets.IsNullOrEmpty());
}
[Test]
public void OrderByMarginImpactDoesNotReturnTargetsForWhichUnorderedQuantityIsZeroBecauseTargetReached()
{
var algorithm = new FakeAlgorithm();
algorithm.SetFinishedWarmingUp();
algorithm.Transactions.SetOrderProcessor(new FakeOrderProcessor());
var symbol = new Symbol(SecurityIdentifier.GenerateEquity(_symbol, Market.USA), _symbol);
var equity = algorithm.AddEquity(symbol);
var dummySecurityHolding = new FakeSecurityHolding(equity);
equity.Holdings = dummySecurityHolding;
equity.Cache.AddData(new TradeBar(DateTime.UtcNow, symbol, 1, 1, 1, 1, 1));
var collection = new PortfolioTargetCollection();
var target = new PortfolioTarget(symbol, 1);
collection.Add(target);
dummySecurityHolding.SetQuantity(1);
var targets = collection.OrderByMarginImpact(algorithm);
Assert.AreEqual(collection.Count, 1);
Assert.IsTrue(targets.IsNullOrEmpty());
}
[Test]
public void OrderByMarginImpactDoesNotReturnTargetsForWhichUnorderedQuantityIsZeroBecauseOpenOrder()
{
var orderProcessor = new FakeOrderProcessor();
var algorithm = GetAlgorithm(orderProcessor);
var symbol = new Symbol(SecurityIdentifier.GenerateEquity(_symbol, Market.USA), _symbol);
var equity = algorithm.AddEquity(symbol);
#pragma warning restore CS0618
equity.Cache.AddData(new TradeBar(DateTime.UtcNow, symbol, 1, 1, 1, 1, 1));
var collection = new PortfolioTargetCollection();
var target = new PortfolioTarget(symbol, 1);
collection.Add(target);
var openOrderRequest = new SubmitOrderRequest(OrderType.Market, symbol.SecurityType, symbol, 1, 0, 0, DateTime.UtcNow, "");
openOrderRequest.SetOrderId(1);
var openOrderTicket = new OrderTicket(algorithm.Transactions, openOrderRequest);
orderProcessor.AddOrder(new MarketOrder(symbol, 1, DateTime.UtcNow));
orderProcessor.AddTicket(openOrderTicket);
var targets = collection.OrderByMarginImpact(algorithm);
Assert.AreEqual(collection.Count, 1);
Assert.IsTrue(targets.IsNullOrEmpty());
}
private QCAlgorithm GetAlgorithm(IOrderProcessor orderProcessor)
{
var algorithm = new FakeAlgorithm();
algorithm.SetFinishedWarmingUp();
algorithm.Transactions.SetOrderProcessor(orderProcessor);
return algorithm;
}
private class FakeSecurityHolding : SecurityHolding
{
public FakeSecurityHolding(Security security) :
base(security, new IdentityCurrencyConverter(security.QuoteCurrency.Symbol))
{
}
public void SetQuantity(int quantity)
{
Quantity = quantity;
}
}
private class FakeAlgorithm : QCAlgorithm
{
public FakeAlgorithm()
{
SubscriptionManager.SetDataManager(new DataManagerStub(this));
}
}
}
}
@@ -0,0 +1,134 @@
/*
* 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.Linq;
using Moq;
using NUnit.Framework;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Data.Market;
using QuantConnect.Securities;
using QuantConnect.Tests.Engine;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class PortfolioTargetTests
{
[Test]
public void PercentInvokesBuyingPowerModelAndAddsInExistingHoldings()
{
const decimal bpmQuantity = 100;
const decimal holdings = 50;
const decimal targetPercent = 0.5m;
var algorithm = PerformanceBenchmarkAlgorithms.SingleSecurity_Second;
algorithm.Initialize();
algorithm.PostInitialize();
var security = algorithm.Securities.Single().Value;
security.SetMarketPrice(new Tick{Value = 1m});
security.Holdings.SetHoldings(1m, holdings);
var buyingPowerMock = new Mock<IBuyingPowerModel>();
buyingPowerMock.Setup(bpm => bpm.GetMaximumOrderQuantityForTargetBuyingPower(It.IsAny<GetMaximumOrderQuantityForTargetBuyingPowerParameters>()))
.Returns(new GetMaximumOrderQuantityResult(bpmQuantity, null, false));
security.BuyingPowerModel = buyingPowerMock.Object;
buyingPowerMock.Setup(bpm => bpm.GetLeverage(It.IsAny<Security>())).Returns(1);
var target = PortfolioTarget.Percent(algorithm, security.Symbol, targetPercent);
Assert.AreEqual(security.Symbol, target.Symbol);
Assert.AreEqual(bpmQuantity + holdings, target.Quantity);
}
[Test]
public void PercentReturnsNullIfPriceIsZero()
{
const decimal holdings = 50;
const decimal targetPercent = 1m;
var algorithm = PerformanceBenchmarkAlgorithms.SingleSecurity_Second;
algorithm.Initialize();
algorithm.PostInitialize();
var security = algorithm.Securities.Single().Value;
security.SetMarketPrice(new Tick { Value = 0m });
security.Holdings.SetHoldings(1m, holdings);
var target = PortfolioTarget.Percent(algorithm, security.Symbol, targetPercent);
Assert.IsNull(target);
}
[Test]
public void PercentReturnsNullIfBuyingPowerModelError()
{
const decimal holdings = 50;
const decimal targetPercent = 1m;
var algorithm = PerformanceBenchmarkAlgorithms.SingleSecurity_Second;
algorithm.Initialize();
algorithm.PostInitialize();
var security = algorithm.Securities.Single().Value;
security.SetMarketPrice(new Tick { Value = 1m });
security.Holdings.SetHoldings(1m, holdings);
var buyingPowerMock = new Mock<IBuyingPowerModel>();
buyingPowerMock.Setup(bpm => bpm.GetMaximumOrderQuantityForTargetBuyingPower(It.IsAny<GetMaximumOrderQuantityForTargetBuyingPowerParameters>()))
.Returns(new GetMaximumOrderQuantityResult(0, "The portfolio does not have enough margin available."));
security.BuyingPowerModel = buyingPowerMock.Object;
buyingPowerMock.Setup(bpm => bpm.GetLeverage(It.IsAny<Security>())).Returns(1);
var target = PortfolioTarget.Percent(algorithm, security.Symbol, targetPercent);
Assert.IsNull(target);
}
[TestCase(-3, true)]
[TestCase(3, true)]
[TestCase(2, false)]
[TestCase(-2, false)]
[TestCase(0.01, true)]
[TestCase(-0.01, true)]
[TestCase(0.1, false)]
[TestCase(-0.1, false)]
[TestCase(0, false)]
public void PercentIgnoresExtremeValuesBasedOnSettings(double value, bool shouldBeNull)
{
var algorithm = PerformanceBenchmarkAlgorithms.SingleSecurity_Second;
algorithm.Settings.MaxAbsolutePortfolioTargetPercentage = 2;
algorithm.Settings.MinAbsolutePortfolioTargetPercentage = 0.1m;
algorithm.Initialize();
algorithm.PostInitialize();
var security = algorithm.Securities.Single().Value;
security.SetMarketPrice(new Tick { Value = 1m });
var buyingPowerMock = new Mock<IBuyingPowerModel>();
buyingPowerMock.Setup(bpm => bpm.GetMaximumOrderQuantityForTargetBuyingPower(It.IsAny<GetMaximumOrderQuantityForTargetBuyingPowerParameters>()))
.Returns(new GetMaximumOrderQuantityResult(1, null, false));
buyingPowerMock.Setup(bpm => bpm.GetLeverage(It.IsAny<Security>())).Returns(1);
security.BuyingPowerModel = buyingPowerMock.Object;
var target = PortfolioTarget.Percent(algorithm, security.Symbol, value);
if (shouldBeNull)
{
Assert.IsNull(target);
}
else
{
Assert.IsNotNull(target);
}
}
}
}
@@ -0,0 +1,252 @@
/*
* 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 Accord.Math;
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.Data.UniverseSelection;
using QuantConnect.Python;
using QuantConnect.Securities;
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Tests.Common.Data.UniverseSelection;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class ReturnsSymbolDataTests
{
[Test]
public void ReturnsDoesNotThrowIndexOutOfRangeException()
{
var dateTime = new DateTime(2020, 02, 01);
var returnsSymbolData = new ReturnsSymbolData(Symbols.EURGBP, 1, 5);
returnsSymbolData.Add(dateTime, 10);
returnsSymbolData.Add(dateTime.AddDays(1), 100);
returnsSymbolData.Add(dateTime.AddDays(2), 5);
var returnsSymbolData2 = new ReturnsSymbolData(Symbols.BTCUSD, 1, 5);
returnsSymbolData2.Add(dateTime.AddDays(1), 33);
returnsSymbolData2.Add(dateTime.AddDays(2), 2);
var returnsSymbolDatas = new Dictionary<Symbol, ReturnsSymbolData>
{
{Symbols.EURGBP, returnsSymbolData},
{Symbols.BTCUSD, returnsSymbolData2},
};
var result = returnsSymbolDatas.FormReturnsMatrix(new List<Symbol> {Symbols.EURGBP, Symbols.BTCUSD });
Assert.AreEqual(4, result.Length);
Assert.AreEqual(5m, result[0, 0]);
Assert.AreEqual(2m, result[0, 1]);
Assert.AreEqual(100m, result[1, 0]);
Assert.AreEqual(33m, result[1, 1]);
}
[Test]
public void ReturnsDoesNotThrowKeyAlreadyExistsException()
{
var returnsSymbolData = new ReturnsSymbolData(Symbols.EURGBP, 1, 5);
var dateTime = new DateTime(2020, 02, 01);
returnsSymbolData.Add(dateTime, 10);
returnsSymbolData.Add(dateTime, 100);
Assert.AreEqual(1, returnsSymbolData.Returns.Count);
Assert.AreEqual(returnsSymbolData.Returns[dateTime], 10);
}
[Test]
[TestCase(false)]
[TestCase(true)]
public void ReturnsSymbolDataMatrixIsSimilarToPandasDataFrame(bool odd)
{
var symbols = new[] { "A", "B", "C", "D", "E" }
.Select(x => Symbol.Create(x, SecurityType.Equity, Market.USA, x));
var slices = GetHistory(symbols, odd);
var expected = GetExpectedValue(slices);
var symbolDataDict = new Dictionary<Symbol, ReturnsSymbolData>();
slices.PushThrough(bar =>
{
ReturnsSymbolData symbolData;
if (!symbolDataDict.TryGetValue(bar.Symbol, out symbolData))
{
symbolData = new ReturnsSymbolData(bar.Symbol, 1, 5);
symbolDataDict.Add(bar.Symbol, symbolData);
}
symbolData.Update(bar.EndTime, bar.Value);
});
var matrix = symbolDataDict.FormReturnsMatrix(symbols);
var actual = matrix.Determinant();
Assert.AreEqual(expected, actual, 0.000001);
}
[Test]
public void DuplicateKeyPortfolioConstructionModelDoesNotThrow()
{
var algorithm = new QCAlgorithm();
var timezone = algorithm.TimeZone;
algorithm.SetDateTime(new DateTime(2018, 8, 7).ConvertToUtc(timezone));
algorithm.SetPortfolioConstruction(new DuplicateKeyPortfolioConstructionModel());
var symbol = Symbols.SPY;
var security = new Security(
SecurityExchangeHours.AlwaysOpen(timezone),
new SubscriptionDataConfig(
typeof(TradeBar),
symbol,
Resolution.Daily,
timezone,
timezone,
true,
false,
false
),
new Cash(Currencies.USD, 0, 1),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
security.SetMarketPrice(new Tick(algorithm.Time, symbol, 1m, 1m));
algorithm.Securities.Add(symbol, security);
algorithm.PortfolioConstruction.OnSecuritiesChanged(algorithm, SecurityChangesTests.AddedNonInternal(security));
var insights = new[] {Insight.Price(symbol, Time.OneMinute, InsightDirection.Up, .1)};
Assert.DoesNotThrow(() => algorithm.PortfolioConstruction.CreateTargets(algorithm, insights));
}
private List<Slice> GetHistory(IEnumerable<Symbol> symbols, bool odd)
{
var reference = DateTime.Now;
var rnd = new Random(reference.Second);
var symbolsArray = symbols.ToArray();
return Enumerable.Range(0, 10).Select(t =>
{
var time = reference.AddDays(t);
var data = Enumerable.Range(0, symbolsArray.Length).Select(i =>
{
// Odd data means that one of the symbols have a different timestamp
if (odd && i == symbolsArray.Length - 1)
{
time = time.AddMinutes(10);
}
return new Tick(time, symbolsArray[i], (decimal)rnd.NextDouble(), 0, 0) {TickType = TickType.Trade};
});
return new Slice(time, data, time);
}).ToList();
}
private double GetExpectedValue(List<Slice> history)
{
var code = @"
import numpy as np
import pandas as pd
import math
from BlackLittermanOptimizationPortfolioConstructionModel import BlackLittermanOptimizationPortfolioConstructionModel as blopcm
def GetDeterminantFromHistory(history):
returns = dict()
history = history.lastprice.unstack(0)
for symbol, df in history.items():
symbolData = blopcm.BlackLittermanSymbolData(symbol, 1, 5)
for time, close in df.dropna().items():
symbolData.update(time, close)
returns[symbol] = symbolData.return_
df = pd.DataFrame(returns)
if df.isna().sum().sum() > 0:
df[df.columns[-1]] = df[df.columns[-1]].bfill()
df = df.dropna()
return np.linalg.det(df)";
using (Py.GIL())
{
dynamic GetDeterminantFromHistory = PyModule.FromString("GetDeterminantFromHistory", code)
.GetAttr("GetDeterminantFromHistory");
dynamic df = new PandasConverter().GetDataFrame(history);
return GetDeterminantFromHistory(df);
}
}
private class DuplicateKeyPortfolioConstructionModel : IPortfolioConstructionModel
{
private readonly Dictionary<Symbol, ReturnsSymbolData> _symbolDataDict = new Dictionary<Symbol, ReturnsSymbolData>();
public IEnumerable<IPortfolioTarget> CreateTargets(QCAlgorithm algorithm, Insight[] insights)
{
// Updates the ReturnsSymbolData with insights
foreach (var insight in insights)
{
ReturnsSymbolData symbolData;
if (_symbolDataDict.TryGetValue(insight.Symbol, out symbolData))
{
symbolData.Add(algorithm.Time, .1m);
}
}
Assert.DoesNotThrow(() => _symbolDataDict.FormReturnsMatrix(insights.Select(x => x.Symbol)));
return Enumerable.Empty<PortfolioTarget>();
}
public void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
{
const int period = 2;
var reference = algorithm.Time.AddDays(-period);
foreach (var security in changes.AddedSecurities)
{
var symbol = security.Symbol;
var symbolData = new ReturnsSymbolData(symbol, 1, period);
for (var i = 0; i <= period; i++)
{
symbolData.Update(reference.AddDays(i), 1);
}
_symbolDataDict[symbol] = symbolData;
}
}
}
}
}
@@ -0,0 +1,188 @@
/*
* 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 aaplicable 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.Linq;
using Accord.Math;
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 RiskParityPortfolioConstructionModelTests
{
private DateTime _nowUtc;
private QCAlgorithm _algorithm;
[SetUp]
public virtual void SetUp()
{
_nowUtc = new DateTime(2021, 1, 10);
_algorithm = new AlgorithmStub();
_algorithm.SetFinishedWarmingUp();
_algorithm.Settings.MinimumOrderMarginPortfolioPercentage = 0;
_algorithm.Settings.FreePortfolioValue = 250;
_algorithm.SetDateTime(_nowUtc.ConvertToUtc(_algorithm.TimeZone));
_algorithm.SetCash(1200);
var historyProvider = new SubscriptionDataReaderHistoryProvider();
_algorithm.SetHistoryProvider(historyProvider);
historyProvider.Initialize(new HistoryProviderInitializeParameters(
new BacktestNodePacket(),
null,
TestGlobals.DataProvider,
TestGlobals.DataCacheProvider,
TestGlobals.MapFileProvider,
TestGlobals.FactorFileProvider,
i => { },
true,
new DataPermissionManager(),
_algorithm.ObjectStore,
_algorithm.Settings));
}
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void DoesNotReturnTargetsIfSecurityPriceIsZero(Language language)
{
var algorithm = new QCAlgorithm();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.AddEquity(Symbols.SPY.Value);
algorithm.SetDateTime(DateTime.MinValue.ConvertToUtc(algorithm.TimeZone));
SetPortfolioConstruction(language, PortfolioBias.Long);
var insights = new[] { new Insight(_nowUtc, Symbols.SPY, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, null, null) };
var actualTargets = algorithm.PortfolioConstruction.CreateTargets(algorithm, insights);
Assert.AreEqual(0, actualTargets.Count());
}
[TestCase(Language.CSharp, PortfolioBias.Long)]
[TestCase(Language.Python, PortfolioBias.Long)]
[TestCase(Language.CSharp, PortfolioBias.Short)]
[TestCase(Language.Python, PortfolioBias.Short)]
public void PortfolioBiasIsRespected(Language language, PortfolioBias bias)
{
if (bias == PortfolioBias.Short)
{
var throwsConstraint = language == Language.CSharp
? Throws.InstanceOf<ArgumentException>()
: Throws.InstanceOf<ClrBubbledException>().With.InnerException.InstanceOf<ArgumentException>();
Assert.That(() => GetPortfolioConstructionModel(language, bias, Resolution.Daily),
throwsConstraint.And.Message.EqualTo("Long position must be allowed in RiskParityPortfolioConstructionModel."));
return;
}
SetPortfolioConstruction(language, bias);
var aapl = _algorithm.AddEquity("AAPL");
var spy = _algorithm.AddEquity("SPY");
foreach (var equity in new[] { aapl, spy })
{
equity.SetMarketPrice(new Tick(_nowUtc, equity.Symbol, 10, 10));
}
_algorithm.PortfolioConstruction.OnSecuritiesChanged(_algorithm, SecurityChangesTests.AddedNonInternal(aapl, spy));
var insights = new[]
{
new Insight(_nowUtc, aapl.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, null, null),
new Insight(_nowUtc, spy.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Down, null, null)
};
foreach (var target in _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights))
{
if (target.Quantity == 0)
{
continue;
}
Assert.AreEqual(Math.Sign((int)bias), Math.Sign(target.Quantity));
}
}
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void CorrectWeightings(Language language)
{
SetPortfolioConstruction(language, PortfolioBias.Long);
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, 12, 12));
spy.SetMarketPrice(new Tick(_nowUtc.AddDays(1), spy.Symbol, 20, 20));
aapl.SetMarketPrice(new Tick(_nowUtc.AddDays(2), aapl.Symbol, 13, 13));
spy.SetMarketPrice(new Tick(_nowUtc.AddDays(2), spy.Symbol, 30, 30));
_algorithm.PortfolioConstruction.OnSecuritiesChanged(_algorithm, SecurityChangesTests.AddedNonInternal(aapl, spy));
var insights = new[]
{
new Insight(_nowUtc.AddDays(2), aapl.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, null, null),
new Insight(_nowUtc.AddDays(2), spy.Symbol, TimeSpan.FromDays(1), InsightType.Price, InsightDirection.Up, null, null)
};
_algorithm.Insights.AddRange(insights);
var targets = _algorithm.PortfolioConstruction.CreateTargets(_algorithm, insights).ToArray();
Assert.AreEqual(targets[0].Quantity, 30m); // AAPL
Assert.AreEqual(targets[1].Quantity, 18m); // SPY
}
protected void SetPortfolioConstruction(Language language, PortfolioBias bias, IPortfolioConstructionModel defaultModel = null)
{
var model = defaultModel ?? GetPortfolioConstructionModel(language, bias, Resolution.Daily);
_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, PortfolioBias bias, Resolution resolution)
{
if (language == Language.CSharp)
{
return new RiskParityPortfolioConstructionModel(resolution, bias, 1, 252, resolution);
}
using (Py.GIL())
{
const string name = nameof(RiskParityPortfolioConstructionModel);
var instance = Py.Import(name).GetAttr(name)
.Invoke(((int)resolution).ToPython(), ((int)bias).ToPython(), 1.ToPython(), 252.ToPython(), ((int)resolution).ToPython());
return new PortfolioConstructionModelPythonWrapper(instance);
}
}
}
}
@@ -0,0 +1,165 @@
/*
* 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 aaplicable 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 Accord.Math;
using NUnit.Framework;
using QuantConnect.Algorithm.Framework.Portfolio;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class RiskParityPortfolioOptimizerTests
{
private Dictionary<int, double[][]> _covariances = new();
private Dictionary<int, double[]> _riskBudgets = new();
private Dictionary<int, double[]> _expectedResults = new();
[SetUp]
public virtual void SetUp()
{
var riskBudget1 = new double[] {0.5d, 0.5d}; // equal risk distribution
var riskBudget2 = new double[] {0.25d, 0.75d}; // 25% risk assigned to A, 75% risk assigned to B
var covariance1 = new double[][] {new[]{0.25, -0.2}, new[]{-0.2, 0.25}}; // both 50% variance, -80% correlation
var covariance2 = new double[][] {new[]{0.01, 0}, new[]{0, 0.04}}; // sigma(A) 10%, sigma(B) 20%, 0% correlation
var covariance3 = new double[][] {new[]{1, 0.45}, new[]{0.45, 0.25}}; // sigma(A) 100%, sigma(B) 50%, 90% correlation
var covariance4 = new double[][] {new[]{0.25, 0.05}, new[]{0.05, 0.04}}; // sigma(A) 50%, sigma(B) 10%, 25% correlation
var expectedResult1 = new double[] {3.162278d, 3.162278d};
var expectedResult2 = new double[] {7.071068d, 3.535534d};
var expectedResult3 = new double[] {0.512989d, 1.025978d};
var expectedResult4 = new double[] {1.154701d, 2.886751d};
var expectedResult5 = new double[] {2.965685d, 3.285618d};
var expectedResult6 = new double[] {5d, 4.330127d};
var expectedResult7 = new double[] {0.264749d, 1.510089d};
var expectedResult8 = new double[] {0.681774d, 3.924933d};
_covariances.TryAdd(1, covariance1);
_covariances.TryAdd(2, covariance2);
_covariances.TryAdd(3, covariance3);
_covariances.TryAdd(4, covariance4);
_covariances.TryAdd(5, covariance1);
_covariances.TryAdd(6, covariance2);
_covariances.TryAdd(7, covariance3);
_covariances.TryAdd(8, covariance4);
_riskBudgets.TryAdd(1, riskBudget1);
_riskBudgets.TryAdd(2, riskBudget1);
_riskBudgets.TryAdd(3, riskBudget1);
_riskBudgets.TryAdd(4, riskBudget1);
_riskBudgets.TryAdd(5, riskBudget2);
_riskBudgets.TryAdd(6, riskBudget2);
_riskBudgets.TryAdd(7, riskBudget2);
_riskBudgets.TryAdd(8, riskBudget2);
_expectedResults.TryAdd(1, expectedResult1);
_expectedResults.TryAdd(2, expectedResult2);
_expectedResults.TryAdd(3, expectedResult3);
_expectedResults.TryAdd(4, expectedResult4);
_expectedResults.TryAdd(5, expectedResult5);
_expectedResults.TryAdd(6, expectedResult6);
_expectedResults.TryAdd(7, expectedResult7);
_expectedResults.TryAdd(8, expectedResult8);
}
[TestCase(1)]
[TestCase(0)]
[TestCase(1001)]
public void TestForExtremeNumberOfVariablesRiskParityNewtonMethodOptimization(int numberOfVariables)
{
var testOptimizer = new TestRiskParityPortfolioOptimizer();
if (numberOfVariables < 1 || numberOfVariables > 1000)
{
var exception = Assert.Throws<ArgumentException>(() => testOptimizer.TestOptimization(numberOfVariables, new double[,]{}, new double[]{}));
Assert.That(exception.Message, Is.EqualTo("Argument \"numberOfVariables\" must be a positive integer between 1 and 1000"));
return;
}
var result = testOptimizer.TestOptimization(numberOfVariables, new double[,]{}, new double[]{});
Assert.AreEqual(new double[] {1d}, result);
}
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
[TestCase(4)]
[TestCase(5)]
[TestCase(6)]
[TestCase(7)]
[TestCase(8)]
public void TestRiskParityNewtonMethodOptimizationWeightings(int testCaseNumber)
{
var testOptimizer = new TestRiskParityPortfolioOptimizer();
var covariance = JaggedArrayTo2DArray(_covariances[testCaseNumber]);
var result = testOptimizer.TestOptimization(2, covariance, _riskBudgets[testCaseNumber]);
result = result.Select(x => Math.Round(x, 6)).ToArray();
Assert.AreEqual(_expectedResults[testCaseNumber], result);
}
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
[TestCase(4)]
[TestCase(5)]
[TestCase(6)]
[TestCase(7)]
[TestCase(8)]
public void TestOptimizeWeightings(int testCaseNumber)
{
var testOptimizer = new TestRiskParityPortfolioOptimizer();
var covariance = JaggedArrayTo2DArray(_covariances[testCaseNumber]);
var result = testOptimizer.Optimize(new double[,]{}, _riskBudgets[testCaseNumber], covariance);
result = result.Select(x => Math.Round(x, 6)).ToArray();
var expected = _expectedResults[testCaseNumber];
expected = Elementwise.Divide(expected, expected.Sum()).Select(x => Math.Clamp(x, 1e-05, double.MaxValue)).ToArray();
expected = expected.Select(x => Math.Round(x, 6)).ToArray();
Assert.AreEqual(expected, result);
}
private T[,] JaggedArrayTo2DArray<T>(T[][] source)
{
int FirstDim = source.Length;
int SecondDim = source.GroupBy(row => row.Length).Single().Key;
var result = new T[FirstDim, SecondDim];
for (int i = 0; i < FirstDim; ++i)
for (int j = 0; j < SecondDim; ++j)
result[i, j] = source[i][j];
return result;
}
private class TestRiskParityPortfolioOptimizer : RiskParityPortfolioOptimizer
{
public TestRiskParityPortfolioOptimizer()
: base()
{
}
public double[] TestOptimization(int numOfVar, double[,] covariance, double[] budget)
{
return base.RiskParityNewtonMethodOptimization(numOfVar, covariance, budget);
}
}
}
}
@@ -0,0 +1,334 @@
/*
* 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 NodaTime;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Tests.Common.Data.UniverseSelection;
using QuantConnect.Interfaces;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class SectorWeightingPortfolioConstructionModelTests : BaseWeightingPortfolioConstructionModelTests
{
private const double _weight = 0.01;
public override double? Weight => Algorithm.Securities.Count == 0 ? default(double) : 1d / Algorithm.Securities.Count;
[OneTimeSetUp]
public override void SetUp()
{
base.SetUp();
var prices = new Dictionary<Symbol, Tuple<decimal, string>>
{
{ Symbol.Create("XXX", SecurityType.Equity, Market.USA), Tuple.Create(55.22m, "") },
{ Symbol.Create("B01", SecurityType.Equity, Market.USA), Tuple.Create(55.22m, "B") },
{ Symbol.Create("B02", SecurityType.Equity, Market.USA), Tuple.Create(55.22m, "B") },
{ Symbol.Create("B03", SecurityType.Equity, Market.USA), Tuple.Create(55.22m, "B") },
{ Symbol.Create("T01", SecurityType.Equity, Market.USA), Tuple.Create(145.17m, "T") },
{ Symbol.Create("T02", SecurityType.Equity, Market.USA), Tuple.Create(145.17m, "T") },
{ Symbol.Create("SPY", SecurityType.Equity, Market.USA), Tuple.Create(281.79m, "X") },
};
Func<Symbol, Security> GetSecurity = symbol =>
new Equity(
symbol,
SecurityExchangeHours.AlwaysOpen(DateTimeZone.Utc),
new Cash(Currencies.USD, 0, 1),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
var industryTemplateCodeDict = new Dictionary<SecurityIdentifier, string>();
foreach (var kvp in prices)
{
var symbol = kvp.Key;
var security = GetSecurity(symbol);
var price = kvp.Value.Item1;
var sectorCode = kvp.Value.Item2;
// The first item does not have a valid sector code,
// This procedure shows that the model ignores the securities without it
if (!string.IsNullOrEmpty(sectorCode))
{
security.SetMarketPrice(new Fundamental(Algorithm.Time, symbol)
{
Value = price
});
industryTemplateCodeDict[symbol.ID] = kvp.Value.Item2;
}
security.SetMarketPrice(new Tick(Algorithm.Time, symbol, price, price));
Algorithm.Securities.Add(symbol, security);
}
FundamentalService.Initialize(TestGlobals.DataProvider, new TestFundamentalDataProvider(industryTemplateCodeDict), false);
}
[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);
// 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));
// Since we have 3 B, 2 T and 1 X, each security in each sector will get
// B => .166%, T => .25, X => 0% (removed)
var sectorCount = 2;
var groupedBySector = Algorithm.Securities
.Where(pair => pair.Value.Fundamentals?.CompanyReference.IndustryTemplateCode != null)
.GroupBy(pair => pair.Value.Fundamentals.CompanyReference.IndustryTemplateCode);
var expectedTargets = new List<PortfolioTarget>();
foreach (var securities in groupedBySector)
{
var list = securities.ToList();
var amount = Algorithm.Portfolio.TotalPortfolioValue / list.Count / sectorCount *
(1 - Algorithm.Settings.FreePortfolioValuePercentage);
expectedTargets.AddRange(list
.Select(x => new PortfolioTarget(x.Key, x.Key.Value == "SPY" ? 0
: (int)direction * Math.Floor(amount / x.Value.Price))));
}
// 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);
Assert.Greater(Algorithm.Securities.Count, expectedTargets.Count);
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);
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);
// Since we have 3 B, 2 T and 1 X, each security in each sector will get
// B => .166%, T => .25, X => 0% (removed)
var sectorCount = 2;
var groupedBySector = Algorithm.Securities
.Where(pair => pair.Value.Fundamentals?.CompanyReference.IndustryTemplateCode != null)
.GroupBy(pair => pair.Value.Fundamentals.CompanyReference.IndustryTemplateCode);
var expectedTargets = new List<PortfolioTarget>();
foreach (var securities in groupedBySector)
{
var list = securities.ToList();
var amount = Algorithm.Portfolio.TotalPortfolioValue / list.Count / sectorCount *
(1 - Algorithm.Settings.FreePortfolioValuePercentage);
expectedTargets.AddRange(list
.Select(x => new PortfolioTarget(x.Key, x.Key.Value == "SPY" ? 0
: (int)direction * Math.Floor(amount / x.Value.Price))));
}
// 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);
Assert.Greater(Algorithm.Securities.Count, expectedTargets.Count);
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);
// Since we have 3 B, 2 T and 1 X, each security in each sector will get
// B => .166%, T => .25, X => 0% (removed)
var sectorCount = 2;
var groupedBySector = Algorithm.Securities
.Where(pair => pair.Value.Fundamentals?.CompanyReference.IndustryTemplateCode != null)
.GroupBy(pair => pair.Value.Fundamentals.CompanyReference.IndustryTemplateCode);
var expectedTargets = new List<PortfolioTarget>();
foreach (var securities in groupedBySector)
{
var list = securities.ToList();
var amount = Algorithm.Portfolio.TotalPortfolioValue / list.Count / sectorCount *
(1 - Algorithm.Settings.FreePortfolioValuePercentage);
expectedTargets.AddRange(list
.Select(x => new PortfolioTarget(x.Key, x.Key.Value == "SPY" ? 0
: (int)direction * Math.Floor(amount / x.Value.Price))));
}
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());
Assert.Greater(Algorithm.Securities.Count, expectedTargets.Count);
AssertTargets(expectedTargets, actualTargets);
}
[Test]
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void WeightsBySectorProportionally(Language language)
{
SetPortfolioConstruction(language);
// create two insights whose weights sums up to 2
var insights = Algorithm.Securities
.Select(x => GetInsight(x.Key, InsightDirection.Up, Algorithm.UtcTime))
.ToArray();
// Since we have 3 B, 2 T and 1 X, each security in each secotr will get B => .11%, T => .165, X => .33%
var sectorCount = 3;
var groupedBySector = Algorithm.Securities
.Where(pair => pair.Value.Fundamentals?.CompanyReference.IndustryTemplateCode != null)
.Where(pair => insights.Any(insight => pair.Key == insight.Symbol))
.GroupBy(pair => pair.Value.Fundamentals.CompanyReference.IndustryTemplateCode);
var expectedTargets = new List<PortfolioTarget>();
foreach (var securities in groupedBySector)
{
var list = securities.ToList();
var amount = Algorithm.Portfolio.TotalPortfolioValue / list.Count / sectorCount *
(1 - Algorithm.Settings.FreePortfolioValuePercentage);
expectedTargets.AddRange(list
.Select(x => new PortfolioTarget(x.Key, (int)InsightDirection.Up * Math.Floor(amount / x.Value.Price))));
}
var actualTargets = Algorithm.PortfolioConstruction.CreateTargets(Algorithm, insights).ToList();
Assert.AreEqual(6, actualTargets.Count);
Assert.Greater(Algorithm.Securities.Count, expectedTargets.Count);
AssertTargets(expectedTargets, actualTargets);
}
public override Insight GetInsight(Symbol symbol, InsightDirection direction, DateTime generatedTimeUtc, TimeSpan? period = null, double? weight = _weight)
{
period ??= TimeSpan.FromDays(1);
var insight = Insight.Price(symbol, period.Value, direction, weight: weight);
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 SectorWeightingPortfolioConstructionModel(paramenter);
}
using (Py.GIL())
{
const string name = nameof(SectorWeightingPortfolioConstructionModel);
var instance = Py.Import(name).GetAttr(name).Invoke(((object)paramenter).ToPython());
return new PortfolioConstructionModelPythonWrapper(instance);
}
}
private class TestFundamentalDataProvider : IFundamentalDataProvider
{
private readonly Dictionary<SecurityIdentifier, string> _industryTemplateCodeDict;
public TestFundamentalDataProvider(Dictionary<SecurityIdentifier, string> industryTemplateCodeDict)
{
_industryTemplateCodeDict = industryTemplateCodeDict;
}
public T Get<T>(DateTime time, SecurityIdentifier securityIdentifier, FundamentalProperty name)
{
if (securityIdentifier == SecurityIdentifier.Empty)
{
return default;
}
return Get(time, securityIdentifier, name);
}
private dynamic Get(DateTime time, SecurityIdentifier securityIdentifier, FundamentalProperty enumName)
{
var name = Enum.GetName(enumName);
switch (name)
{
case "CompanyReference_IndustryTemplateCode":
if(_industryTemplateCodeDict.TryGetValue(securityIdentifier, out var result))
{
return result;
}
return null;
}
return null;
}
public void Initialize(IDataProvider dataProvider, bool liveMode)
{
}
}
}
}
@@ -0,0 +1,642 @@
/*
* 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 QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Portfolio.SignalExports;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Interfaces;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Securities;
using QuantConnect.Tests.Engine.DataFeeds;
using System;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class SignalExportTargetTests
{
[TestCaseSource(nameof(SendsTargetsToCollective2AppropriatelyTestCases))]
public void SendsTargetsToCollective2Appropriately(string currency, Symbol symbol, decimal quantity, string expectedMessage)
{
var targetList = new List<PortfolioTarget>() { new(symbol, quantity) };
var algorithm = new AlgorithmStub();
algorithm.SetDateTime(new DateTime(2016, 02, 16, 11, 53, 30));
algorithm.Portfolio.SetAccountCurrency(currency);
var security = algorithm.AddSecurity(symbol);
security.QuoteCurrency.ConversionRate = 1;
security.FeeModel = new ConstantFeeModel(0);
security.SetMarketPrice(new Tick { Value = 100 });
var initialMarginRequirement = security.BuyingPowerModel.GetInitialMarginRequirement(new(security, 1));
if (initialMarginRequirement.Value == 0)
{
security.SetBuyingPowerModel(BuyingPowerModel.Null);
}
algorithm.Portfolio.SetCash(1500000);
using var manager = new Collective2SignalExportHandler("", 0);
var message = manager.GetMessageSent(new SignalExportTargetParameters { Targets = targetList, Algorithm = algorithm });
Assert.AreEqual(expectedMessage, message);
}
[Test]
public void Collective2SignalExportManagerDoesNotLogMoreThanOnceWhenUnknownMarket()
{
var targetList = new List<PortfolioTarget>() { new(Symbols.EURUSD, 1), new PortfolioTarget(Symbols.GBPUSD, 1) };
var algorithm = new AlgorithmStub();
algorithm.SetDateTime(new DateTime(2016, 02, 16, 11, 53, 30));
algorithm.Portfolio.SetAccountCurrency("USD");
var security = algorithm.AddSecurity(Symbols.EURUSD);
var security2 = algorithm.AddSecurity(Symbols.GBPUSD);
security.SetMarketPrice(new Tick { Value = 100 });
security2.SetMarketPrice(new Tick { Value = 100 });
algorithm.Portfolio.SetCash(50000);
using var manager = new Collective2SignalExportHandler("", 0);
for (int count = 0; count < 100; count++)
{
manager.GetMessageSent(new SignalExportTargetParameters { Targets = targetList, Algorithm = algorithm });
}
Assert.AreEqual(3, algorithm.DebugMessages.Count);
var debugMessages = algorithm.DebugMessages.ToList();
Assert.IsTrue(debugMessages[0].Contains($"The market of the symbol {Symbols.EURUSD.Value} was unexpected: {Symbols.EURUSD.ID.Market}. Using 'DEFAULT' as market", StringComparison.InvariantCultureIgnoreCase));
Assert.IsTrue(debugMessages[1].Contains($"Warning: Collective2 failed to calculate target quantity for {targetList[0]}. The smallest quantity C2 trades is \"1\" which is a mini-lot (10,000 currency units), and the target quantity is 498. Will return 0 for all similar cases.", StringComparison.InvariantCultureIgnoreCase));
Assert.IsTrue(debugMessages[2].Contains($"The market of the symbol {Symbols.GBPUSD.Value} was unexpected: {Symbols.GBPUSD.ID.Market}. Using 'DEFAULT' as market", StringComparison.InvariantCultureIgnoreCase));
}
[Test]
public void Collective2SignalExportManagerDoesNotLogMoreThanOnceWhenUnknownSecurityType()
{
var targetList = new List<PortfolioTarget>() { new(Symbols.BTCUSD, 1), new PortfolioTarget(Symbols.XAUJPY, 1) };
var algorithm = new AlgorithmStub();
algorithm.SetDateTime(new DateTime(2016, 02, 16, 11, 53, 30));
algorithm.Portfolio.SetAccountCurrency("USD");
var security = algorithm.AddSecurity(Symbols.BTCUSD);
var security2 = algorithm.AddSecurity(Symbols.XAUJPY);
security.SetMarketPrice(new Tick { Value = 100 });
security2.SetMarketPrice(new Tick { Value = 100 });
algorithm.Portfolio.SetCash(50000);
using var manager = new Collective2SignalExportHandler("", 0);
for (int count = 0; count < 100; count++)
{
manager.GetMessageSent(new SignalExportTargetParameters { Targets = targetList, Algorithm = algorithm });
}
Assert.AreEqual(2, algorithm.DebugMessages.Count);
var debugMessages = algorithm.DebugMessages.ToList();
Assert.IsTrue(debugMessages[0].Contains($"Unexpected security type found: {security.Symbol.SecurityType}. Collective2 just accepts: Equity, Future, Option, Index Option and Stock", StringComparison.InvariantCultureIgnoreCase));
Assert.IsTrue(debugMessages[1].Contains($"Unexpected security type found: {security2.Symbol.SecurityType}. Collective2 just accepts: Equity, Future, Option, Index Option and Stock", StringComparison.InvariantCultureIgnoreCase));
}
[Test]
public void Collective2ConvertsPercentageToQuantityAppropriately()
{
var symbols = new List<Symbol>()
{
Symbols.SPY,
Symbols.EURUSD,
Symbols.AAPL,
Symbols.IBM,
Symbols.GOOG,
Symbols.MSFT,
Symbols.CAT
};
var targetList = new List<PortfolioTarget>()
{
new PortfolioTarget(Symbols.SPY, (decimal)0.01),
new PortfolioTarget(Symbols.EURUSD, (decimal)0.99),
new PortfolioTarget(Symbols.AAPL, (decimal)(-0.01)),
new PortfolioTarget(Symbols.IBM, (decimal)(-0.99)),
new PortfolioTarget(Symbols.GOOG, (decimal)0.0),
new PortfolioTarget(Symbols.MSFT, (decimal)1.0),
new PortfolioTarget(Symbols.CAT, (decimal)(-1.0))
};
var algorithm = new AlgorithmStub();
AddSymbols(symbols, algorithm);
algorithm.Portfolio.SetCash(50000);
algorithm.Settings.MinimumOrderMarginPortfolioPercentage = 0;
algorithm.Settings.FreePortfolioValue = 250;
using var manager = new Collective2SignalExportHandler("", 0);
var expectedQuantities = new Dictionary<string, int>()
{
{ "SPY", 4 },
// 492 was rounded down to zero because 1 is a minilot of 10,000 USD
// https://support.collective2.com/hc/en-us/articles/360038042774-Forex-minilots
{ "EURUSD", 0},
{ "AAPL", -4 },
{ "IBM", -492 },
{ "GOOG", 0 },
{ "MSFT", 497 },
{ "CAT", -497 }
};
foreach (var target in targetList)
{
var quantity = manager.ConvertPercentageToQuantity(algorithm, target);
Assert.AreEqual(expectedQuantities[target.Symbol.Value], quantity);
}
}
[Test]
public void SendsTargetsToCrunchDAOAppropriately()
{
var symbols = new List<Symbol>()
{
Symbols.SPY,
Symbols.AAPL,
Symbols.CAT
};
var targetList = new List<PortfolioTarget>()
{
new PortfolioTarget(Symbols.SPY, (decimal)0.2),
new PortfolioTarget(Symbols.AAPL, (decimal)0.2),
new PortfolioTarget(Symbols.CAT, (decimal)0.2),
};
var algorithm = new AlgorithmStub();
AddSymbols(symbols, algorithm);
using var manager = new CrunchDAOSignalExportHandler("", "");
var message = manager.GetMessageSent(new SignalExportTargetParameters { Targets = targetList, Algorithm = algorithm });
var expectedMessage = "ticker,date,signal\nSPY,2016-02-16,0.2\nAAPL,2016-02-16,0.2\nCAT,2016-02-16,0.2\n";
Assert.AreEqual(expectedMessage, message);
}
[Test]
public void CrunchDAOSignalExportReturnsFalseWhenSymbolIsNotAllowed()
{
var symbols = new List<Symbol>()
{
Symbols.ES_Future_Chain,
Symbols.SPY_Option_Chain,
Symbols.EURUSD,
Symbols.BTCUSD,
};
var targetList = new List<PortfolioTarget>();
foreach (var symbol in symbols)
{
targetList.Add(new PortfolioTarget(symbol, (decimal)0.1));
}
var algorithm = new AlgorithmStub();
AddSymbols(symbols, algorithm);
algorithm.Portfolio.SetCash(50000);
using var manager = new CrunchDAOSignalExport("", "");
var result = manager.Send(new SignalExportTargetParameters { Targets = targetList, Algorithm = algorithm });
Assert.IsFalse(result);
}
[Test]
public void CrunchDAOSignalExportReturnsFalseWhenPortfolioTargetListIsEmpty()
{
var targetList = new List<PortfolioTarget>();
using var manager = new CrunchDAOSignalExport("", "");
var algorithm = new QCAlgorithm();
var result = manager.Send(new SignalExportTargetParameters { Targets = targetList, Algorithm = algorithm });
Assert.IsFalse(result);
}
[Test]
public void SendsTargetsToNumeraiAppropriately()
{
var targets = new List<PortfolioTarget>()
{
new PortfolioTarget(Symbols.SGX, (decimal)0.05),
new PortfolioTarget(Symbols.AAPL, (decimal)0.1),
new PortfolioTarget(Symbols.MSFT, (decimal)0.1),
new PortfolioTarget(Symbols.ZNGA, (decimal)0.05),
new PortfolioTarget(Symbols.FXE, (decimal)0.05),
new PortfolioTarget(Symbols.LODE, (decimal)0.05),
new PortfolioTarget(Symbols.IBM, (decimal)0.05),
new PortfolioTarget(Symbols.GOOG, (decimal)0.1),
new PortfolioTarget(Symbols.NFLX, (decimal)0.1),
new PortfolioTarget(Symbols.CAT, (decimal)0.1)
};
using var manager = new NumeraiSignalExportHandler("", "", "");
var algorithm = new QCAlgorithm();
algorithm.SetDateTime(new DateTime(2023, 03, 03));
var message = manager.GetMessageSent(new SignalExportTargetParameters { Targets = targets, Algorithm = algorithm });
var expectedMessage = "numerai_ticker,signal\nSGX SP,0.05\nAAPL US,0.1\nMSFT US,0.1\nZNGA US,0.05\nFXE US,0.05\nLODE US,0.05\nIBM US,0.05\nGOOG US,0.1\nNFLX US,0.1\nCAT US,0.1\n";
Assert.AreEqual(expectedMessage, message);
}
[Test]
public void NumeraiReturnsFalseWhenSymbolIsNotAllowed()
{
var targets = new List<PortfolioTarget>()
{
new PortfolioTarget(Symbols.SGX, (decimal)0.05),
new PortfolioTarget(Symbols.AAPL, (decimal)0.1),
new PortfolioTarget(Symbols.MSFT, (decimal)0.1),
new PortfolioTarget(Symbols.ZNGA, (decimal)0.05),
new PortfolioTarget(Symbols.FXE, (decimal)0.05),
new PortfolioTarget(Symbols.LODE, (decimal)0.05),
new PortfolioTarget(Symbols.IBM, (decimal)0.05),
new PortfolioTarget(Symbols.GOOG, (decimal)0.1),
new PortfolioTarget(Symbols.NFLX, (decimal)0.1),
new PortfolioTarget(Symbols.EURUSD, (decimal)0.1)
};
using var manager = new NumeraiSignalExport("", "", "");
var algorithm = new QCAlgorithm();
var result = manager.Send(new SignalExportTargetParameters { Targets = targets, Algorithm = algorithm });
Assert.IsFalse(result);
}
[Test]
public void NumeraiReturnsFalseWhenNumberOfTargetsIsLessThanTen()
{
var targets = new List<PortfolioTarget>()
{
new PortfolioTarget(Symbols.SGX, (decimal)0.05),
new PortfolioTarget(Symbols.AAPL, (decimal)0.1),
new PortfolioTarget(Symbols.MSFT, (decimal)0.1),
new PortfolioTarget(Symbols.ZNGA, (decimal)0.05),
new PortfolioTarget(Symbols.FXE, (decimal)0.05),
new PortfolioTarget(Symbols.LODE, (decimal)0.05),
new PortfolioTarget(Symbols.IBM, (decimal)0.05),
new PortfolioTarget(Symbols.GOOG, (decimal)0.1),
new PortfolioTarget(Symbols.NFLX, (decimal)0.1),
};
using var manager = new NumeraiSignalExport("", "", "");
var algorithm = new QCAlgorithm();
var result = manager.Send(new SignalExportTargetParameters { Targets = targets, Algorithm = algorithm });
Assert.IsFalse(result);
}
[TestCase(SecurityType.Equity, "SPY", 68)]
[TestCase(SecurityType.Equity, "AAPL", -68)]
[TestCase(SecurityType.Equity, "GOOG", 345)]
[TestCase(SecurityType.Equity, "IBM", 0)]
[TestCase(SecurityType.Forex, "EURUSD", 90)]
[TestCase(SecurityType.Forex, "EURUSD", -90)]
[TestCase(SecurityType.Future, "ES", 4)]
[TestCase(SecurityType.Future, "ES", -4)]
public void SignalExportManagerGetsCorrectPortfolioTargetArray(SecurityType securityType, string ticker, int quantity)
{
var algorithm = new AlgorithmStub(true);
algorithm.SetFinishedWarmingUp();
algorithm.SetCash(100000);
var security = algorithm.AddSecurity(securityType, ticker);
security.SetMarketPrice(new Tick(new DateTime(2022, 01, 04), security.Symbol, 144.80m, 144.82m));
security.Holdings.SetHoldings(144.81m, quantity);
var signalExportManagerHandler = new SignalExportManagerHandler(algorithm);
var result = signalExportManagerHandler.GetPortfolioTargets(out PortfolioTarget[] portfolioTargets);
Assert.IsTrue(result);
var target = portfolioTargets[0];
var targetQuantity = (int)PortfolioTarget.Percent(algorithm, target.Symbol, target.Quantity).Quantity;
// The quantites can differ by one because of the number of lots for certain securities
Assert.AreEqual(quantity, targetQuantity, 1);
}
[Test]
public void SignalExportManagerDoesNotThrowOnZeroPrice()
{
var algorithm = new AlgorithmStub(true);
algorithm.SetDateTime(new DateTime(2024, 02, 16, 11, 53, 30));
var security = algorithm.AddSecurity(Symbols.SPY);
// Set the market price to 0 to simulate the edge case being tested
security.SetMarketPrice(new Tick { Value = 0 });
using var manager = new Collective2SignalExportHandler("", 0);
// Ensure ConvertPercentageToQuantity does not throw when price is 0
Assert.DoesNotThrow(() =>
{
var result = manager.ConvertPercentageToQuantity(algorithm, new PortfolioTarget(Symbols.SPY, 0));
Assert.AreEqual(0, result);
});
}
[Test]
public void SignalExportManagerHandlesIndexOptions()
{
var algorithm = new AlgorithmStub(true);
algorithm.SetFinishedWarmingUp();
algorithm.SetCash(100000);
int quantity = 123;
var underlying = algorithm.AddIndex("SPX", Resolution.Minute).Symbol;
// Create the option contract (IndexOption) with specific parameters
var option = Symbol.CreateOption(
underlying,
"SPXW",
Market.USA,
OptionStyle.European,
OptionRight.Call,
3800m,
new DateTime(2021, 1, 04));
var security = algorithm.AddIndexOptionContract(option, Resolution.Minute);
security.SetMarketPrice(new Tick(new DateTime(2022, 01, 04), security.Symbol, 144.80m, 144.82m));
security.Holdings.SetHoldings(144.81m, quantity);
// Initialize the SignalExportManagerHandler and get portfolio targets
var signalExportManagerHandler = new SignalExportManagerHandler(algorithm);
var result = signalExportManagerHandler.GetPortfolioTargets(out PortfolioTarget[] portfolioTargets);
// Assert that the result is successful
Assert.IsTrue(result);
// Get the portfolio target and verify the quantity matches
var target = portfolioTargets[0];
var targetQuantity = (int)PortfolioTarget.Percent(algorithm, target.Symbol, target.Quantity).Quantity;
Assert.AreEqual(quantity, targetQuantity);
// Ensure the symbol is of type IndexOption
Assert.IsTrue(target.Symbol.SecurityType == SecurityType.IndexOption);
}
[Test]
public void SignalExportManagerIgnoresIndexSecurities()
{
var algorithm = new AlgorithmStub(true);
algorithm.SetFinishedWarmingUp();
algorithm.SetCash(100000);
var security = algorithm.AddIndexOption("SPX", "SPXW");
security.SetMarketPrice(new Tick(new DateTime(2022, 01, 04), security.Symbol, 144.80m, 144.82m));
security.Holdings.SetHoldings(144.81m, 10);
var signalExportManagerHandler = new SignalExportManagerHandler(algorithm);
var result = signalExportManagerHandler.GetPortfolioTargets(out PortfolioTarget[] portfolioTargets);
Assert.IsTrue(result);
Assert.IsFalse(portfolioTargets.Where(x => x.Symbol.SecurityType == SecurityType.Index).Any());
}
[TestCaseSource(nameof(SignalExportManagerSkipsNonTradeableFuturesTestCase))]
public void SignalExportManagerSkipsNonTradeableFutures(IEnumerable<Symbol> symbols, int expectedNumberOfTargets)
{
var algorithm = new AlgorithmStub(true);
algorithm.SetFinishedWarmingUp();
algorithm.SetCash(100000);
foreach (var symbol in symbols)
{
var security = algorithm.AddSecurity(symbol);
security.SetMarketPrice(new Tick(new DateTime(2022, 01, 04), security.Symbol, 144.80m, 144.82m));
security.Holdings.SetHoldings(144.81m, 100);
}
var signalExportManagerHandler = new SignalExportManagerHandler(algorithm);
var result = signalExportManagerHandler.GetPortfolioTargets(out PortfolioTarget[] portfolioTargets);
Assert.IsTrue(result);
Assert.AreEqual(expectedNumberOfTargets, portfolioTargets.Length);
}
[Test]
public void SignalExportManagerReturnsFalseWhenNegativeTotalPortfolioValue()
{
var algorithm = new AlgorithmStub(true);
algorithm.SetFinishedWarmingUp();
algorithm.SetCash(-100000);
var signalExportManagerHandler = new SignalExportManagerHandler(algorithm);
Assert.IsFalse(signalExportManagerHandler.GetPortfolioTargets(out _));
}
[Test]
public void EmptySignalExportList()
{
var algorithm = new AlgorithmStub(true);
algorithm.SetLiveMode(true);
algorithm.SetFinishedWarmingUp();
algorithm.SetCash(100000);
var security = algorithm.AddSecurity(Symbols.SPY);
security.SetMarketPrice(new Tick(new DateTime(2022, 01, 04), security.Symbol, 144.80m, 144.82m));
security.Holdings.SetHoldings(144.81m, 100);
var utcTime = DateTime.UtcNow;
var signalExportManagerHandler = new SignalExportManagerHandler(algorithm);
signalExportManagerHandler.OnOrderEvent(new OrderEvent(0, security.Symbol, utcTime.AddMinutes(-1), OrderStatus.Filled, OrderDirection.Buy, 100, 100, new Orders.Fees.OrderFee(new Securities.CashAmount(1, "USD"))));
Assert.DoesNotThrow(() => signalExportManagerHandler.SetTargetPortfolioFromPortfolio());
Assert.DoesNotThrow(() => signalExportManagerHandler.Flush(utcTime));
}
private static object[] SendsTargetsToCollective2AppropriatelyTestCases =
{
new object[] { "USD", Symbols.SPY, 0.2m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""SPY"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""CS""},""quantity"":2992.0}]}" },
new object[] { "USD", Symbols.EURUSD, 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""EUR/USD"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""FOR""},""quantity"":1.0}]}" },
new object[] { "USD", Symbols.EURGBP, 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""EUR/GBP"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""FOR""},""quantity"":1.0}]}" },
new object[] { "USD", Symbols.GBPJPY, 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""GBP/JPY"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""FOR""},""quantity"":1.0}]}" },
new object[] { "USD", Symbols.GBPUSD, 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""GBP/USD"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""FOR""},""quantity"":1.0}]}" },
new object[] { "USD", Symbols.USDJPY, 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""USD/JPY"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""FOR""},""quantity"":1.0}]}" },
new object[] { "USD", Symbols.CreateOptionSymbol("SPY", OptionRight.Call, 192m, new DateTime(2016, 02, 15)), 0.2m, @"{""StrategyId"":0,""Positions"":[]}" },
new object[] { "USD", Symbols.CreateOptionSymbol("SPY", OptionRight.Call, 192m, new DateTime(2056, 02, 19)), 0.2m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""SPY"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""OPT"",""maturityMonthYear"":""20560218"",""putOrCall"":1,""strikePrice"":192.0},""quantity"":29.0}]}" },
new object[] { "USD", Symbols.CreateOptionSymbol("SPY", OptionRight.Put, 192m, new DateTime(2056, 02, 19)), 0.2m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""SPY"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""OPT"",""maturityMonthYear"":""20560218"",""putOrCall"":0,""strikePrice"":192.0},""quantity"":29.0}]}" },
new object[] { "USD", Symbol.Create("EURUSD", SecurityType.Forex, Market.FXCM), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""EUR/USD"",""currency"":""USD"",""securityExchange"":""FXCM"",""securityType"":""FOR""},""quantity"":1.0}]}" },
new object[] { "USD", Symbol.Create("NQX", SecurityType.IndexOption, Market.USA), 0.2m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""NQX"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""OPT"",""putOrCall"":1,""strikePrice"":0.0},""quantity"":29.0}]}" },
new object[] { "USD", Symbol.CreateFuture("NIFTY", Market.India, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""NIFTY"",""currency"":""INR"",""securityExchange"":""XNSE"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":299250.0}]}" },
new object[] { "USD", Symbol.CreateFuture("HSI", Market.HKFE, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""HSI"",""currency"":""HKD"",""securityExchange"":""XHKF"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":24.0}]}" },
new object[] { "USD", Symbol.CreateFuture("ZG", Market.NYSELIFFE, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""ZG"",""currency"":""USD"",""securityExchange"":""XNLI"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":73.0}]}" },
new object[] { "USD", Symbol.CreateFuture("FESX", Market.EUREX, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""FESX"",""currency"":""EUR"",""securityExchange"":""XEUR"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":498.0}]}" },
new object[] { "USD", Symbol.CreateFuture("KC", Market.ICE, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""KC"",""currency"":""USD"",""securityExchange"":""IEPA"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":415.0}]}" },
new object[] { "USD", Symbol.CreateFuture("VIX", Market.CFE, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""VIX"",""currency"":""USD"",""securityExchange"":""XCBF"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":14962.0}]}" },
new object[] { "USD", Symbol.CreateFuture("ZC", Market.CBOT, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""ZC"",""currency"":""USD"",""securityExchange"":""XCBT"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":2770.0}]}" },
new object[] { "USD", Symbol.CreateFuture("GC", Market.COMEX, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""GC"",""currency"":""USD"",""securityExchange"":""XCEC"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":316.0}]}" },
new object[] { "USD", Symbol.CreateFuture("CL", Market.NYMEX, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""CL"",""currency"":""USD"",""securityExchange"":""XNYM"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":201.0}]}" },
new object[] { "USD", Symbol.CreateFuture("NK", Market.SGX, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""NK"",""currency"":""JPY"",""securityExchange"":""XSES"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":425.0}]}" },
new object[] { "EUR", Symbols.SPY, 0.2m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""SPY"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""CS""},""quantity"":2992.0}]}" },
new object[] { "EUR", Symbols.EURUSD, 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""EUR/USD"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""FOR""},""quantity"":1.0}]}" },
new object[] { "EUR", Symbols.EURGBP, 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""EUR/GBP"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""FOR""},""quantity"":1.0}]}" },
new object[] { "EUR", Symbols.GBPJPY, 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""GBP/JPY"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""FOR""},""quantity"":1.0}]}" },
new object[] { "EUR", Symbols.GBPUSD, 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""GBP/USD"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""FOR""},""quantity"":1.0}]}" },
new object[] { "EUR", Symbols.USDJPY, 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""USD/JPY"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""FOR""},""quantity"":1.0}]}" },
new object[] { "EUR", Symbols.CreateOptionSymbol("SPY", OptionRight.Call, 192m, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""SPY"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""OPT"",""maturityMonthYear"":""20560218"",""putOrCall"":1,""strikePrice"":192.0},""quantity"":149.0}]}" },
new object[] { "EUR", Symbols.CreateOptionSymbol("SPY", OptionRight.Put, 192m, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""SPY"",""currency"":""USD"",""securityExchange"":""DEFAULT"",""securityType"":""OPT"",""maturityMonthYear"":""20560218"",""putOrCall"":0,""strikePrice"":192.0},""quantity"":149.0}]}" },
new object[] { "EUR", Symbols.CreateOptionSymbol("SPY", OptionRight.Call, 192m, new DateTime(2016, 02, 15)), 0.2m, @"{""StrategyId"":0,""Positions"":[]}" },
new object[] { "EUR", Symbol.Create("EURUSD", SecurityType.Forex, Market.FXCM), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""EUR/USD"",""currency"":""USD"",""securityExchange"":""FXCM"",""securityType"":""FOR""},""quantity"":1.0}]}" },
new object[] { "EUR", Symbol.CreateFuture("NIFTY", Market.India, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""NIFTY"",""currency"":""INR"",""securityExchange"":""XNSE"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":299250.0}]}" },
new object[] { "EUR", Symbol.CreateFuture("HSI", Market.HKFE, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""HSI"",""currency"":""HKD"",""securityExchange"":""XHKF"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":24.0}]}" },
new object[] { "EUR", Symbol.CreateFuture("ZG", Market.NYSELIFFE, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""ZG"",""currency"":""USD"",""securityExchange"":""XNLI"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":73.0}]}" },
new object[] { "EUR", Symbol.CreateFuture("FESX", Market.EUREX, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""FESX"",""currency"":""EUR"",""securityExchange"":""XEUR"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":498.0}]}" },
new object[] { "EUR", Symbol.CreateFuture("KC", Market.ICE, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""KC"",""currency"":""USD"",""securityExchange"":""IEPA"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":415.0}]}" },
new object[] { "EUR", Symbol.CreateFuture("VIX", Market.CFE, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""VIX"",""currency"":""EUR"",""securityExchange"":""XCBF"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":14962.0}]}" },
new object[] { "EUR", Symbol.CreateFuture("ZC", Market.CBOT, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""ZC"",""currency"":""USD"",""securityExchange"":""XCBT"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":2770.0}]}" },
new object[] { "EUR", Symbol.CreateFuture("GC", Market.COMEX, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""GC"",""currency"":""USD"",""securityExchange"":""XCEC"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":316.0}]}" },
new object[] { "EUR", Symbol.CreateFuture("CL", Market.NYMEX, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""CL"",""currency"":""USD"",""securityExchange"":""XNYM"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":201.0}]}" },
new object[] { "EUR", Symbol.CreateFuture("NK", Market.SGX, new DateTime(2056, 02, 19)), 1m, @"{""StrategyId"":0,""Positions"":[{""exchangeSymbol"":{""symbol"":""NK"",""currency"":""JPY"",""securityExchange"":""XSES"",""securityType"":""FUT"",""maturityMonthYear"":""20560219""},""quantity"":425.0}]}" },
};
private static void AddSymbols(List<Symbol> symbols, QCAlgorithm algorithm)
{
algorithm.SetDateTime(new DateTime(2016, 02, 16, 11, 53, 30));
foreach (var symbol in symbols)
{
var security = algorithm.AddSecurity(symbol);
security.SetMarketPrice(new Tick { Value = 100 });
}
}
/// <summary>
/// Creates a TradebarConfiguration for the given symbol
/// </summary>
/// <param name="symbol">Symbol for which we want to create a TradeBarConfiguration</param>
/// <returns>A new TradebarConfiguration for the given symbol</returns>
private static SubscriptionDataConfig CreateTradeBarConfig(Symbol symbol)
{
return new SubscriptionDataConfig(typeof(TradeBar), symbol, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, false);
}
/// <summary>
/// Handler class to test how SignalExportManager obtains the portfolio targets from the algorithm's portfolio
/// </summary>
private class SignalExportManagerHandler : SignalExportManager
{
public SignalExportManagerHandler(IAlgorithm algorithm) : base(algorithm)
{
}
/// <summary>
/// Handler method to obtain portfolio targets from algorithm's portfolio
/// </summary>
/// <param name="targets">An array of portfolio targets from the algorithm's Portfolio</param>
/// <returns>True if TotalPortfolioValue was bigger than zero</returns>
public bool GetPortfolioTargets(out PortfolioTarget[] targets)
{
return base.GetPortfolioTargets(out targets);
}
}
/// <summary>
/// Handler class to test how Collective2SignalExport converts target percentage to number of shares. Additionally,
/// to test the message sent to Collective2 API
/// </summary>
private class Collective2SignalExportHandler : Collective2SignalExport
{
public Collective2SignalExportHandler(string apiKey, int systemId) : base(apiKey, systemId)
{
}
/// <summary>
/// Handler method to test how Collective2SignalExport converts target percentage to number of shares
/// </summary>
/// <param name="algorithm">Algorithm being ran</param>
/// <param name="target">Target with quantity as percentage</param>
/// <returns>Number of shares for the given target percentage</returns>
public int ConvertPercentageToQuantity(IAlgorithm algorithm, PortfolioTarget target)
{
return base.ConvertPercentageToQuantity(algorithm, target);
}
/// <summary>
/// Handler method to test the message sent to Collective2 API
/// </summary>
/// <param name="parameters">A list of holdings from the portfolio
/// expected to be sent to Collective2 API and the algorithm being ran</param>
/// <returns>Message sent to Collective2 API</returns>
public string GetMessageSent(SignalExportTargetParameters parameters)
{
ConvertHoldingsToCollective2(parameters, out List<Collective2Position> positions);
var message = CreateMessage(positions);
return message;
}
}
/// <summary>
/// Handler class to test the message sent to CrunchDAO API
/// </summary>
private class CrunchDAOSignalExportHandler : CrunchDAOSignalExport
{
public CrunchDAOSignalExportHandler(string apiKey, string model, string submissionName = "", string comment = "") : base(apiKey, model, submissionName, comment)
{
}
/// <summary>
/// Handler method to test the message sent to CrunchDAO API
/// </summary>
/// <param name="parameters">A list of holdings from the portfolio
/// expected to be sent to CrunchDAO2 API and the algorithm being ran</param>
/// <returns>Message sent to CrunchDAO API</returns>
public string GetMessageSent(SignalExportTargetParameters parameters)
{
ConvertToCSVFormat(parameters, out string message);
return message;
}
}
/// <summary>
/// Handler class to test message sent to Numerai API
/// </summary>
private class NumeraiSignalExportHandler : NumeraiSignalExport
{
public NumeraiSignalExportHandler(string publicId, string secretId, string modelId, string fileName = "predictions.csv") : base(publicId, secretId, modelId, fileName)
{
}
/// <summary>
/// Handler method to test the message sent to Numerai API
/// </summary>
/// <param name="parameters">A list of holdings from the portfolio
/// expected to be sent to Numerai API and the algorithm being ran</param>
/// <returns>Message sent to Numerai API</returns>
public string GetMessageSent(SignalExportTargetParameters parameters)
{
ConvertTargetsToNumerai(parameters, out string message);
return message;
}
}
private static object[] SignalExportManagerSkipsNonTradeableFuturesTestCase =
{
new object[] { new List<Symbol>() { Symbols.AAPL, Symbols.SPY, Symbols.SPX }, 2 },
new object[] { new List<Symbol>() { Symbols.AAPL, Symbols.SPY, Symbols.NFLX }, 3 },
};
}
}
@@ -0,0 +1,76 @@
/*
* 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 aaplicable 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 QuantConnect.Algorithm.Framework.Portfolio;
using System.Collections.Generic;
namespace QuantConnect.Tests.Algorithm.Framework.Portfolio
{
[TestFixture]
public class UnconstrainedMeanVariancePortfolioOptimizerTests : PortfolioOptimizerTestsBase
{
[OneTimeSetUp]
public void Setup()
{
HistoricalReturns = new List<double[,]>
{
new double[,] { { 0.76, -0.06, 1.22, 0.17 }, { 0.02, 0.28, 1.25, -0.00 }, { -0.50, -0.13, -0.50, -0.03 }, { 0.81, 0.31, 2.39, 0.26 }, { -0.02, 0.02, 0.06, 0.01 } },
new double[,] { { -0.15, 0.67, 0.45 }, { -0.44, -0.10, 0.07 }, { 0.04, -0.41, 0.01 }, { 0.01, 0.03, 0.02 } },
new double[,] { { -0.02, 0.65, 1.25 }, { -0.29, -0.39, -0.50 }, { 0.29, 0.58, 2.39 }, { 0.00, -0.01, 0.06 } },
new double[,] { { 0.76, 0.25, 0.21 }, { 0.02, -0.15, 0.45 }, { -0.50, -0.44, 0.07 }, { 0.81, 0.04, 0.01 }, { -0.02, 0.01, 0.02 } }
};
ExpectedReturns = new List<double[]>
{
new double[] { 0.21, 0.08, 0.88, 0.08 },
new double[] { -0.13, 0.05, 0.14 },
null,
null
};
Covariances = new List<double[,]>
{
new double[,] { { 0.31, 0.05, 0.55, 0.07 }, { 0.05, 0.04, 0.18, 0.01 }, { 0.55, 0.18, 1.28, 0.12 }, { 0.07, 0.01, 0.12, 0.02 } },
new double[,] { { 0.05, -0.02, -0.01 }, { -0.02, 0.21, 0.09 }, { -0.01, 0.09, 0.04 } },
new double[,] { { 0.06, 0.09, 0.28 }, { 0.09, 0.25, 0.58 }, { 0.28, 0.58, 1.66 } },
null
};
ExpectedResults = new List<double[]>
{
new double[] { -13.288136, -23.322034, 8.79661, 9.389831 },
new double[] { -0.142857, -35.285714, 82.857143 },
new double[] { -13.232262, -3.709534, 4.009978 },
new double[] { 4.621852, -9.651736, 5.098332 },
};
}
protected override IPortfolioOptimizer CreateOptimizer()
{
return new UnconstrainedMeanVariancePortfolioOptimizer();
}
[TestCase(0)]
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
public override void OptimizeWeightings(int testCaseNumber)
{
base.OptimizeWeightings(testCaseNumber);
}
}
}
@@ -0,0 +1,133 @@
/*
* 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 QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Tests.Common.Securities;
using QuantConnect.Tests.Engine.DataFeeds;
namespace QuantConnect.Tests.Algorithm.Framework
{
[TestFixture]
public class QCAlgorithmFrameworkTests
{
[Test]
public void SetsInsightGeneratedAndCloseTimes()
{
var eventFired = false;
var algo = new QCAlgorithm();
algo.SubscriptionManager.SetDataManager(new DataManagerStub(algo));
algo.Transactions.SetOrderProcessor(new FakeOrderProcessor());
algo.InsightsGenerated += (algorithm, data) =>
{
eventFired = true;
var insights = data.Insights;
Assert.AreEqual(1, insights.Length);
Assert.IsTrue(insights.All(insight => insight.GeneratedTimeUtc != default(DateTime)));
Assert.IsTrue(insights.All(insight => insight.CloseTimeUtc != default(DateTime)));
};
var security = algo.AddEquity("SPY");
algo.SetUniverseSelection(new ManualUniverseSelectionModel());
var alpha = new FakeAlpha();
algo.SetAlpha(alpha);
var construction = new FakePortfolioConstruction();
algo.SetPortfolioConstruction(construction);
var tick = new Tick
{
Symbol = security.Symbol,
Value = 1,
Quantity = 2
};
security.SetMarketPrice(tick);
algo.OnFrameworkData(new Slice(new DateTime(2000, 01, 01), algo.Securities.Select(s => tick), new DateTime(2000, 01, 01)));
Assert.IsTrue(eventFired);
Assert.AreEqual(1, construction.Insights.Count);
Assert.IsTrue(construction.Insights.All(insight => insight.GeneratedTimeUtc != default(DateTime)));
Assert.IsTrue(construction.Insights.All(insight => insight.CloseTimeUtc != default(DateTime)));
}
[TestCase(true, 0)]
[TestCase(false, 2)]
public void DelistedSecuritiesInsightsTest(bool isDelisted, int expectedCount)
{
var algorithm = new QCAlgorithm();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.Transactions.SetOrderProcessor(new FakeOrderProcessor());
algorithm.SetStartDate(2007, 5, 16);
algorithm.SetUniverseSelection(new ManualUniverseSelectionModel());
algorithm.SetFinishedWarmingUp();
var alpha = new FakeAlpha();
algorithm.SetAlpha(alpha);
var construction = new FakePortfolioConstruction();
algorithm.SetPortfolioConstruction(construction);
var actualInsights = new List<Insight>();
algorithm.InsightsGenerated += (s, e) => actualInsights.AddRange(e.Insights);
var security = algorithm.AddEquity("SPY", Resolution.Daily);
var tick = new Tick
{
Symbol = security.Symbol,
Value = 1,
Quantity = 2
};
security.SetMarketPrice(tick);
security.IsDelisted = isDelisted;
// Trigger Alpha to emit insight
algorithm.OnFrameworkData(new Slice(new DateTime(2000, 01, 01), new List<BaseData>() { tick }, new DateTime(2000, 01, 01)));
// Manually emit insight
algorithm.EmitInsights(Insight.Price(Symbols.SPY, TimeSpan.FromDays(1), InsightDirection.Up, .5, .75));
// Should be zero because security is delisted
Assert.AreEqual(expectedCount, actualInsights.Count);
}
class FakeAlpha : AlphaModel
{
public override IEnumerable<Insight> Update(QCAlgorithm algorithm, Slice data)
{
yield return Insight.Price(Symbols.SPY, TimeSpan.FromDays(1), InsightDirection.Up, .5, .75);
}
}
class FakePortfolioConstruction : PortfolioConstructionModel
{
public IReadOnlyCollection<Insight> Insights { get; private set; }
public override IEnumerable<IPortfolioTarget> CreateTargets(QCAlgorithm algorithm, Insight[] insights)
{
Insights = insights;
return insights.Select(insight => PortfolioTarget.Percent(algorithm, insight.Symbol, 0.01m));
}
}
}
}
@@ -0,0 +1,102 @@
/*
* 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.Linq;
using Moq;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
namespace QuantConnect.Tests.Algorithm.Framework.Risk
{
[TestFixture]
public class MaximumDrawdownPercentPerSecurityTests
{
[Test]
[TestCase(Language.CSharp, 0.1, false, 0, 0, false)]
[TestCase(Language.CSharp, 0.1, true, -50, 1000, false)]
[TestCase(Language.CSharp, 0.1, true, -100, 1000, false)]
[TestCase(Language.CSharp, 0.1, true, -150, 1000, true)]
[TestCase(Language.Python, 0.1, false, 0, 0, false)]
[TestCase(Language.Python, 0.1, true, -50, 1000, false)]
[TestCase(Language.Python, 0.1, true, -100, 1000, false)]
[TestCase(Language.Python, 0.1, true, -150, 1000, true)]
public void ReturnsExpectedPortfolioTarget(
Language language,
decimal maxDrawdownPercent,
bool invested,
decimal unrealizedProfit,
decimal absoluteHoldingsCost,
bool shouldLiquidate)
{
var security = new Mock<Equity>(
Symbols.AAPL,
SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork),
new Cash(Currencies.USD, 0, 1),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache(),
Exchange.UNKNOWN
);
security.Setup(m => m.Invested).Returns(invested);
var holding = new Mock<EquityHolding>(security.Object,
new IdentityCurrencyConverter(Currencies.USD));
holding.Setup(m => m.UnrealizedProfit).Returns(unrealizedProfit);
holding.Setup(m => m.AbsoluteHoldingsCost).Returns(absoluteHoldingsCost);
holding.Setup(m => m.UnrealizedProfitPercent).Returns(absoluteHoldingsCost == 0m? 0m : unrealizedProfit / absoluteHoldingsCost);
security.Object.Holdings = holding.Object;
var algorithm = new QCAlgorithm();
algorithm.SetPandasConverter();
algorithm.Securities.Add(Symbols.AAPL, security.Object);
if (language == Language.Python)
{
using (Py.GIL())
{
const string name = nameof(MaximumDrawdownPercentPerSecurity);
var instance = Py.Import(name).GetAttr(name).Invoke(maxDrawdownPercent.ToPython());
var model = new RiskManagementModelPythonWrapper(instance);
algorithm.SetRiskManagement(model);
}
}
else
{
var model = new MaximumDrawdownPercentPerSecurity(maxDrawdownPercent);
algorithm.SetRiskManagement(model);
}
var targets = algorithm.RiskManagement.ManageRisk(algorithm, null).ToList();
if (shouldLiquidate)
{
Assert.AreEqual(1, targets.Count);
Assert.AreEqual(Symbols.AAPL, targets[0].Symbol);
Assert.AreEqual(0, targets[0].Quantity);
}
else
{
Assert.AreEqual(0, targets.Count);
}
}
}
}
@@ -0,0 +1,155 @@
/*
* 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 Moq;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using System.Linq;
namespace QuantConnect.Tests.Algorithm.Framework.Risk
{
[TestFixture]
public class MaximumDrawdownPercentPortfolioTests
{
[Test]
[TestCase(Language.CSharp, false, 0, false)]
[TestCase(Language.CSharp, true, -1000, false)]
[TestCase(Language.CSharp, true, -10000, false)]
[TestCase(Language.CSharp, true, -10001, true)]
[TestCase(Language.Python, false, 0, false)]
[TestCase(Language.Python, true, -1000, false)]
[TestCase(Language.Python, true, -10000, false)]
[TestCase(Language.Python, true, -10001, true)]
public void ReturnsExpectedPortfolioTarget(
Language language,
bool invested,
decimal absoluteHoldingsCost,
bool shouldLiquidate)
{
var algorithm = CreateAlgorithm(language, 0.1m);
var targets = algorithm.RiskManagement.ManageRisk(algorithm, new PortfolioTarget[] { new PortfolioTarget(Symbols.AAPL, 10) }).ToList();
Assert.AreEqual(0, targets.Count);
algorithm.Securities.Add(Symbols.AAPL, GetSecurity(Symbols.AAPL, invested, absoluteHoldingsCost));
algorithm.Portfolio.InvalidateTotalPortfolioValue();
targets = algorithm.RiskManagement.ManageRisk(algorithm, new PortfolioTarget[] { new PortfolioTarget(Symbols.AAPL, 10)}).ToList();
if (shouldLiquidate)
{
Assert.AreEqual(1, targets.Count);
Assert.AreEqual(Symbols.AAPL, targets[0].Symbol);
Assert.AreEqual(0, targets[0].Quantity);
}
else
{
Assert.AreEqual(0, targets.Count);
}
}
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void ReturnsExpectedPortfolioTargetsAfterReset(Language language)
{
var algorithm = CreateAlgorithm(language, 0.1m);
var targets = algorithm.RiskManagement.ManageRisk(algorithm, new PortfolioTarget[] { new PortfolioTarget(Symbols.AAPL, 10) }).ToList();
algorithm.Securities.Add(Symbols.AAPL, GetSecurity(Symbols.AAPL, true, -10001));
algorithm.Portfolio.InvalidateTotalPortfolioValue();
targets = algorithm.RiskManagement.ManageRisk(algorithm, new PortfolioTarget[] { new PortfolioTarget(Symbols.AAPL, 10) }).ToList();
Assert.AreEqual(1, targets.Count);
Assert.AreEqual(Symbols.AAPL, targets[0].Symbol);
Assert.AreEqual(0, targets[0].Quantity);
algorithm.Securities.Add(Symbols.AAPL, GetSecurity(Symbols.AAPL, true, 10001));
targets = algorithm.RiskManagement.ManageRisk(algorithm, new PortfolioTarget[] { new PortfolioTarget(Symbols.AAPL, 10) }).ToList();
Assert.AreEqual(0, targets.Count);
}
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void ReturnsMoreThanOnePortfolioTarget(Language language)
{
var targetSymbols = new PortfolioTarget[] {
new PortfolioTarget(Symbols.AAPL, 10),
new PortfolioTarget(Symbols.SPY, 100),
new PortfolioTarget(Symbols.MSFT, 1000),
new PortfolioTarget(Symbols.GOOG, 10000),
new PortfolioTarget(Symbols.IBM, 100000)};
var algorithm = CreateAlgorithm(language, 0.1m);
var returnedTargets = algorithm.RiskManagement.ManageRisk(algorithm, targetSymbols).ToList();
targetSymbols.ToList().ForEach(x => algorithm.Securities.Add(x.Symbol, GetSecurity( x.Symbol, true, -x.Quantity)));
algorithm.Portfolio.InvalidateTotalPortfolioValue();
returnedTargets = algorithm.RiskManagement.ManageRisk(algorithm, targetSymbols).ToList();
Assert.AreEqual(targetSymbols.Length, returnedTargets.Count);
Assert.AreEqual(targetSymbols.Select(x => x.Symbol), returnedTargets.Select(x => x.Symbol));
Assert.IsTrue(returnedTargets.All(x => x.Quantity == 0));
}
private QCAlgorithm CreateAlgorithm(Language language, decimal maxDrawdownPercent)
{
var algorithm = new QCAlgorithm();
algorithm.SetPandasConverter();
if (language == Language.Python)
{
using (Py.GIL())
{
const string name = nameof(MaximumDrawdownPercentPortfolio);
var instance = Py.Import(name).GetAttr(name).Invoke(maxDrawdownPercent.ToPython());
var model = new RiskManagementModelPythonWrapper(instance);
algorithm.SetRiskManagement(model);
}
}
else
{
var model = new MaximumDrawdownPercentPortfolio(maxDrawdownPercent);
algorithm.SetRiskManagement(model);
}
return algorithm;
}
private Security GetSecurity(Symbol symbol, bool invested, decimal absoluteHoldingsCost)
{
// Add security
var security = new Mock<Equity>(
symbol,
SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork),
new Cash(Currencies.USD, 0, 1),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache(),
Exchange.UNKNOWN
);
var holding = new Mock<EquityHolding>(security.Object,
new IdentityCurrencyConverter(Currencies.USD));
holding.Setup(m => m.Invested).Returns(invested);
holding.Setup(m => m.HoldingsValue).Returns(absoluteHoldingsCost);
security.Object.Holdings = holding.Object;
return security.Object;
}
}
}
@@ -0,0 +1,271 @@
/*
* 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.Linq;
using System.Collections.Generic;
using Moq;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Data.Market;
using QuantConnect.Orders.Fees;
namespace QuantConnect.Tests.Algorithm.Framework.Risk
{
[TestFixture]
public class TrailingStopRiskManagementModelTests
{
[Test, TestCaseSource(nameof(GenerateTestData))]
public void ReturnsExpectedPortfolioTarget(
TrailingStopRiskManagementModelTestParameters parameters)
{
var decimalPrices = System.Array.ConvertAll(parameters.Prices, x => (decimal) x);
var security = new Mock<Security>(
Symbols.AAPL,
SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork),
new Cash(Currencies.USD, 1000m, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
security.CallBase = true;
security.Object.FeeModel = new ConstantFeeModel(0);
var holding = new SecurityHolding(security.Object, new IdentityCurrencyConverter(Currencies.USD));
holding.SetHoldings(parameters.InitialPrice, parameters.Quantity);
security.Object.Holdings = holding;
var algorithm = new QCAlgorithm();
algorithm.SetPandasConverter();
algorithm.Securities.Add(Symbols.AAPL, security.Object);
if (parameters.Language == Language.Python)
{
using (Py.GIL())
{
const string name = nameof(TrailingStopRiskManagementModel);
var instance = Py.Import(name).GetAttr(name).Invoke(parameters.MaxDrawdownPercent.ToPython());
var model = new RiskManagementModelPythonWrapper(instance);
algorithm.SetRiskManagement(model);
}
}
else
{
var model = new TrailingStopRiskManagementModel(parameters.MaxDrawdownPercent);
algorithm.SetRiskManagement(model);
}
var quantity = parameters.Quantity;
for (int i = 0; i < decimalPrices.Length; i++)
{
var price = decimalPrices[i];
security.Object.SetMarketPrice(new Tick(DateTime.Now, security.Object.Symbol, price, price));
security.Setup((m => m.Invested)).Returns(parameters.InvestedArray[i]);
var targets = algorithm.RiskManagement.ManageRisk(algorithm, null).ToList();
var shouldLiquidate = parameters.ShouldLiquidateArray[i];
if (shouldLiquidate)
{
Assert.AreEqual(1, targets.Count);
Assert.AreEqual(Symbols.AAPL, targets[0].Symbol);
Assert.AreEqual(0, targets[0].Quantity);
}
else
{
Assert.AreEqual(0, targets.Count);
}
if (shouldLiquidate || parameters.ChangePosition[i])
{
// Go from long to short or viceversa
holding.SetHoldings(price, quantity = -quantity);
}
}
}
static IEnumerable<TestCaseData> GenerateTestData()
{
Language[] languages = new Language[] { Language.CSharp, Language.Python };
TrailingStopRiskManagementModelTestParameters[] datasets = new TrailingStopRiskManagementModelTestParameters[]
{
new TrailingStopRiskManagementModelTestParameters(
"LiquidatesOnCorrectPriceChangeInLongPosition",
0.05m,
1m,
1m,
new decimal[] { 100m, 99.95m, 99.94m, 95m, 94.99m },
new bool[] { true, true, true, true, true },
new bool[] { false, false, false, false, false },
new bool[] { false, false, false, false, true }
),
new TrailingStopRiskManagementModelTestParameters(
"LiquidatesOnCorrectPriceChangeInShortPosition",
0.1m,
100m,
-1m,
new decimal[] { 50m, 54.99m, 55m, 55.01m },
new bool[] { true, true, true, true },
new bool[] { false, false, false, false },
new bool[] { false, false, false, true }
),
new TrailingStopRiskManagementModelTestParameters(
"DoesntLiquidateIfSecurityIsNotInvested",
0.05m,
1m,
1m,
new decimal[] { 100m, 94.99m, 90m },
new bool[] { false, false, false },
new bool[] { false, false, false },
new bool[] { false, false, false }
),
new TrailingStopRiskManagementModelTestParameters(
"LiquidatesOnCorrectPriceChangeInLongPositionWithUnivestedSecurityInFirstPrices",
0.05m,
1m,
1m,
new decimal[] { 10m, 100m, 99.95m, 99.94m, 95m, 94.99m },
new bool[] { false, true, true, true, true, true },
new bool[] { false, false, false, false, false, false },
new bool[] { false, false, false, false, false, true }
),
new TrailingStopRiskManagementModelTestParameters(
"LiquidatesOnCorrectPriceChangeInShortPositionWithUnivestedSecurityInFirstPrices",
0.1m,
100m,
-1m,
new decimal[] { 90m, 100m, 50m, 54.99m, 55m, 55.01m },
new bool[] { false, true, true, true, true, true },
new bool[] { false, false, false, false, false, false },
new bool[] { false, false, false, false, false, true }
),
new TrailingStopRiskManagementModelTestParameters(
"DoesntLiquidateIfPricesDontChangeInLongPosition",
0.05m,
1m,
1m,
new decimal[] { 1m, 1m, 1m, 1m },
new bool[] { true, true, true, true },
new bool[] { false, false, false, false },
new bool[] { false, false, false, false }
),
new TrailingStopRiskManagementModelTestParameters(
"DoesntLiquidateIfPricesDontChangeInShortPosition",
0.05m,
1m,
-1m,
new decimal[] { 1m, 1m, 1m, 1m },
new bool[] { true, true, true, true },
new bool[] { false, false, false, false },
new bool[] { false, false, false, false }
),
new TrailingStopRiskManagementModelTestParameters(
"LiquidatesAfterSwitchingToShortPosition",
0.05m,
1m,
1m,
new decimal[] { 100m, 90m, 70m, 50m, 52.6m },
new bool[] { true, true, true, true, true },
new bool[] { true, false, false, false, false },
new bool[] { false, false, false, false, true }
),
new TrailingStopRiskManagementModelTestParameters(
"LiquidatesOnFirstCallForLongPosition",
0.1m,
100m,
1m,
new decimal[] { 89.99m },
new bool[] { true },
new bool[] { false },
new bool[] { true }
),
new TrailingStopRiskManagementModelTestParameters(
"LiquidatesOnFirstCallForShortPosition",
0.1m,
100m,
-1m,
new decimal[] { 110.01m },
new bool[] { true },
new bool[] { false },
new bool[] { true }
)
};
return (
from parameters in datasets
from language in languages
select new TrailingStopRiskManagementModelTestParameters(
parameters.Name,
parameters.MaxDrawdownPercent,
parameters.InitialPrice,
parameters.Quantity,
parameters.Prices,
parameters.InvestedArray,
parameters.ChangePosition,
parameters.ShouldLiquidateArray,
language
)
)
.OrderBy(c => c.Language)
// generate test cases from test parameters
.Select(x => new TestCaseData(x).SetName(x.Language + "/" + x.Name))
.ToArray();
}
public class TrailingStopRiskManagementModelTestParameters
{
public string Name { get; init; }
public Language Language { get; init; }
public decimal MaxDrawdownPercent { get; init; }
public decimal InitialPrice { get; init; }
public decimal Quantity { get; init; }
public decimal[] Prices { get; init; }
public bool[] InvestedArray { get; init; }
public bool[] ChangePosition { get; init; }
public bool[] ShouldLiquidateArray { get; init; }
public TrailingStopRiskManagementModelTestParameters(
string name,
decimal maxDrawdownPercent,
decimal initialPrice,
decimal quantity,
decimal[] prices,
bool[] investedArray,
bool[] changePosition,
bool[] shouldLiquidateArray,
Language language = Language.CSharp
)
{
Name = name;
Language = language;
MaxDrawdownPercent = maxDrawdownPercent;
InitialPrice = initialPrice;
Quantity = quantity;
Prices = prices;
InvestedArray = investedArray;
ChangePosition = changePosition;
ShouldLiquidateArray = shouldLiquidateArray;
}
}
}
}
@@ -0,0 +1,198 @@
/*
* 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.Linq;
using Python.Runtime;
using NUnit.Framework;
using QuantConnect.Algorithm;
using QuantConnect.Securities;
using System.Collections.Generic;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Algorithm.Framework.Selection;
using Moq;
namespace QuantConnect.Tests.Algorithm.Framework.Selection
{
[TestFixture]
public class ETFConstituentsUniverseSelectionModelTests
{
[TestCase("from Selection.ETFConstituentsUniverseSelectionModel import *", "Selection.ETFConstituentsUniverseSelectionModel.ETFConstituentsUniverseSelectionModel")]
[TestCase("from QuantConnect.Algorithm.Framework.Selection import *", "QuantConnect.Algorithm.Framework.Selection.ETFConstituentsUniverseSelectionModel")]
public void TestPythonAndCSharpImports(string importStatement, string expected)
{
using (Py.GIL())
{
dynamic module = PyModule.FromString("testModule",
@$"{importStatement}
class ETFConstituentsFrameworkAlgorithm(QCAlgorithm):
def initialize(self):
self.universe_settings.resolution = Resolution.DAILY
symbol = Symbol.create('SPY', SecurityType.EQUITY, Market.USA)
selection_model = ETFConstituentsUniverseSelectionModel(symbol, self.universe_settings, self.etf_constituents_filter)
self.universe_type = str(type(selection_model))
def etf_constituents_filter(self, constituents):
return [c.symbol for c in constituents]");
dynamic algorithm = module.GetAttr("ETFConstituentsFrameworkAlgorithm").Invoke();
algorithm.initialize();
string universeTypeStr = algorithm.universe_type.ToString();
Assert.IsTrue(universeTypeStr.Contains(expected, StringComparison.InvariantCulture));
}
}
[TestCase("'SPY'")]
[TestCase("'SPY', None")]
[TestCase("'SPY', None, None")]
[TestCase("'SPY', self.universe_settings")]
[TestCase("'SPY', self.universe_settings, None")]
[TestCase("'SPY', None, self.etf_constituents_filter")]
[TestCase("'SPY', self.universe_settings, self.etf_constituents_filter")]
[TestCase("Symbol.create('SPY', SecurityType.EQUITY, Market.USA)")]
[TestCase("Symbol.create('SPY', SecurityType.EQUITY, Market.USA), None, None")]
[TestCase("Symbol.create('SPY', SecurityType.EQUITY, Market.USA), self.universe_settings")]
[TestCase("Symbol.create('SPY', SecurityType.EQUITY, Market.USA), self.universe_settings, None")]
[TestCase("Symbol.create('SPY', SecurityType.EQUITY, Market.USA), None, self.etf_constituents_filter")]
[TestCase("Symbol.create('SPY', SecurityType.EQUITY, Market.USA), self.universe_settings, self.etf_constituents_filter")]
[TestCase("Symbol.create('SPY', SecurityType.EQUITY, Market.USA), universe_filter_func=self.etf_constituents_filter")]
public void ETFConstituentsUniverseSelectionModelWithVariousConstructor(string constructorParameters)
{
using (Py.GIL())
{
dynamic module = PyModule.FromString("testModule",
@$"from AlgorithmImports import *
from Selection.ETFConstituentsUniverseSelectionModel import *
class ETFConstituentsFrameworkAlgorithm(QCAlgorithm):
selection_model = None
def initialize(self):
self.universe_settings.resolution = Resolution.DAILY
self.selection_model = ETFConstituentsUniverseSelectionModel({constructorParameters})
def etf_constituents_filter(self, constituents):
return [c.symbol for c in constituents]");
dynamic algorithm = module.GetAttr("ETFConstituentsFrameworkAlgorithm").Invoke();
algorithm.initialize();
Assert.IsNotNull(algorithm.selection_model);
Assert.IsTrue(algorithm.selection_model.etf_symbol.GetPythonType().ToString().Contains($"{nameof(Symbol)}", StringComparison.InvariantCulture));
Assert.IsTrue(algorithm.selection_model.etf_symbol.ToString().Contains(Symbols.SPY, StringComparison.InvariantCulture));
}
}
[TestCase("TSLA")]
public void ETFConstituentsUniverseSelectionModelGetNoCachedSymbol(string ticker)
{
using (Py.GIL())
{
var etfAlgorithm = GetETFConstituentsFrameworkAlgorithm(ticker);
etfAlgorithm.initialize();
Assert.IsNotNull(etfAlgorithm.selection_model);
Assert.IsTrue(etfAlgorithm.selection_model.etf_symbol.GetPythonType().ToString().Contains($"{nameof(Symbol)}", StringComparison.InvariantCulture));
var etfSymbol = (Symbol)etfAlgorithm.selection_model.etf_symbol;
Assert.IsTrue(etfSymbol.Value.Contains(ticker, StringComparison.InvariantCulture));
Assert.IsTrue(etfSymbol.SecurityType == SecurityType.Equity);
}
}
[TestCase("SPY", "CACHED")]
public void ETFConstituentsUniverseSelectionModelGetCachedSymbol(string ticker, string expectedAlias)
{
using (Py.GIL())
{
var etfAlgorithm = GetETFConstituentsFrameworkAlgorithm(ticker);
etfAlgorithm.initialize();
Assert.IsNotNull(etfAlgorithm.selection_model);
Assert.IsTrue(etfAlgorithm.selection_model.etf_symbol.GetPythonType().ToString().Contains($"{nameof(Symbol)}", StringComparison.InvariantCulture));
var etfSymbol = (Symbol)etfAlgorithm.selection_model.etf_symbol;
Assert.IsTrue(etfSymbol.Value.Contains(expectedAlias, StringComparison.InvariantCulture));
Assert.IsTrue(etfSymbol.ID == Symbols.SPY.ID);
Assert.IsTrue(etfSymbol.SecurityType == SecurityType.Equity);
}
}
[Test]
public void ETFConstituentsUniverseSelectionModelTestAllConstructor()
{
int numberOfOperation = 0;
var ticker = "SPY";
var symbol = Symbol.Create(ticker, SecurityType.Equity, Market.USA);
var universeSettings = new UniverseSettings(Resolution.Minute, Security.NullLeverage, true, false, TimeSpan.FromDays(1));
do
{
ETFConstituentsUniverseSelectionModel etfConstituents = numberOfOperation switch
{
0 => new ETFConstituentsUniverseSelectionModel(ticker),
1 => new ETFConstituentsUniverseSelectionModel(ticker, universeSettings),
2 => new ETFConstituentsUniverseSelectionModel(ticker, ETFConstituentsFilter),
3 => new ETFConstituentsUniverseSelectionModel(ticker, universeSettings, ETFConstituentsFilter),
4 => new ETFConstituentsUniverseSelectionModel(ticker, universeSettings, default(PyObject)),
5 => new ETFConstituentsUniverseSelectionModel(symbol),
6 => new ETFConstituentsUniverseSelectionModel(symbol, universeSettings),
7 => new ETFConstituentsUniverseSelectionModel(symbol, ETFConstituentsFilter),
8 => new ETFConstituentsUniverseSelectionModel(symbol, universeSettings, ETFConstituentsFilter),
9 => new ETFConstituentsUniverseSelectionModel(symbol, universeSettings, default(PyObject)),
_ => throw new ArgumentException("Not recognize number of operation")
};
var universe = etfConstituents.CreateUniverses(new QCAlgorithm()).First();
Assert.IsNotNull(etfConstituents);
Assert.IsNotNull(universe);
Assert.IsTrue(universe.Configuration.Symbol.HasUnderlying);
Assert.AreEqual(symbol, universe.Configuration.Symbol.Underlying);
Assert.AreEqual(symbol.SecurityType, universe.Configuration.Symbol.SecurityType);
Assert.IsTrue(universe.Configuration.Symbol.ID.Symbol.StartsWithInvariant("qc-universe-"));
var data = new Mock<BaseDataCollection>();
Assert.DoesNotThrow(() => universe.PerformSelection(DateTime.UtcNow, data.Object));
} while (++numberOfOperation <= 9) ;
}
private IEnumerable<Symbol> ETFConstituentsFilter(IEnumerable<ETFConstituentUniverse> constituents)
{
return constituents.Select(c => c.Symbol);
}
private static dynamic GetETFConstituentsFrameworkAlgorithm(string etfTicker, string cachedAlias = "CACHED")
{
dynamic module = PyModule.FromString("testModule",
@$"from AlgorithmImports import *
from Selection.ETFConstituentsUniverseSelectionModel import *
class ETFConstituentsFrameworkAlgorithm(QCAlgorithm):
selection_model = None
def initialize(self):
SymbolCache.set('SPY', Symbol.create('SPY', SecurityType.EQUITY, Market.USA, '{cachedAlias}'))
self.universe_settings.resolution = Resolution.DAILY
self.selection_model = ETFConstituentsUniverseSelectionModel(""{etfTicker}"")"
);
dynamic algorithm = module.GetAttr("ETFConstituentsFrameworkAlgorithm").Invoke();
return algorithm;
}
}
}
@@ -0,0 +1,45 @@
/*
* 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.Linq;
using NUnit.Framework;
using QuantConnect.Algorithm;
using QuantConnect.Algorithm.Framework.Selection;
namespace QuantConnect.Tests.Algorithm.Framework.Selection
{
[TestFixture]
public class ManualUniverseSelectionModelTests
{
[Test]
public void ExcludesCanonicalSymbols()
{
var symbols = new[]
{
Symbols.SPY,
Symbol.CreateOption(Symbols.SPY, Market.USA, default(OptionStyle), default(OptionRight), 0m, SecurityIdentifier.DefaultDate, "?SPY")
};
var model = new ManualUniverseSelectionModel(symbols);
var universe = model.CreateUniverses(new QCAlgorithm()).Single();
var selectedSymbols = universe.SelectSymbols(default(DateTime), null).ToList();
Assert.AreEqual(1, selectedSymbols.Count);
Assert.AreEqual(Symbols.SPY, selectedSymbols[0]);
Assert.IsFalse(selectedSymbols.Any(s => s.IsCanonical()));
}
}
}
@@ -0,0 +1,159 @@
/*
* 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 Moq;
using NodaTime;
using NUnit.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Interfaces;
using QuantConnect.Securities;
namespace QuantConnect.Tests.Algorithm.Framework.Selection
{
[TestFixture]
public class OpenInterestFutureUniverseSelectionModelTests
{
private static readonly Symbol Jan = Symbol.CreateFuture(Futures.Metals.Gold, Market.COMEX, new DateTime(2020, 01, 01));
private static readonly Symbol Feb = Symbol.CreateFuture(Futures.Metals.Gold, Market.COMEX, new DateTime(2020, 02, 01));
private static readonly Symbol March = Symbol.CreateFuture(Futures.Metals.Gold, Market.COMEX, new DateTime(2020, 03, 01));
private static readonly Symbol April = Symbol.CreateFuture(Futures.Metals.Gold, Market.COMEX, new DateTime(2020, 04, 01));
private static readonly DateTime TestDate = new DateTime(2020, 05, 11, 0, 0, 0, DateTimeKind.Utc);
private static readonly DateTime ExpectedPreviousDate = new DateTime(2020, 05, 09, 20, 0, 0, DateTimeKind.Utc);
private static readonly IReadOnlyDictionary<Symbol, decimal> OpenInterestData = new Dictionary<Symbol, decimal>
{
[Jan] = 3,
[Feb] = 6,
[March] = 3, // Same as Jan.
[April] = 1
};
private static readonly MarketHoursDatabase.Entry MarketHours = MarketHoursDatabase.FromDataFolder().GetEntry(Jan.ID.Market, Jan, Jan.SecurityType);
private Mock<IHistoryProvider> _mockHistoryProvider;
private OpenInterestFutureUniverseSelectionModel _underTest;
[Test]
public void No_Open_Interest_Returns_Empty()
{
SetupSubject(OpenInterestData.Count, OpenInterestData.Count);
_mockHistoryProvider.Setup(x => x.GetHistory(It.IsAny<IEnumerable<HistoryRequest>>(), It.IsAny<DateTimeZone>()))
.Returns<IEnumerable<HistoryRequest>, DateTimeZone>((r, tz) => new Slice[0])
.Verifiable();
var data = OpenInterestData.Keys.ToDictionary(x => x, x => MarketHours);
var results = _underTest.FilterByOpenInterest(data).ToList();
_mockHistoryProvider.Verify();
Assert.IsEmpty(results);
}
[Test]
public void Can_Sort_By_Open_Interest()
{
SetupSubject(OpenInterestData.Count, OpenInterestData.Count);
_mockHistoryProvider.Setup(x => x.GetHistory(It.IsAny<IEnumerable<HistoryRequest>>(), It.IsAny<DateTimeZone>()))
.Returns<IEnumerable<HistoryRequest>, DateTimeZone>(
(r, tz) =>
{
var requests = r.ToList();
Assert.AreEqual(4, requests.Count);
var slices = new List<Slice>(requests.Count);
foreach (var request in requests)
{
Assert.NotNull(request.Symbol);
Assert.AreEqual(typeof(Tick), request.DataType);
Assert.AreEqual(DataNormalizationMode.Raw, request.DataNormalizationMode);
Assert.AreEqual(ExpectedPreviousDate, request.StartTimeUtc);
Assert.AreEqual(TestDate, request.EndTimeUtc);
Assert.AreEqual(Resolution.Tick, request.Resolution);
Assert.AreEqual(TickType.OpenInterest, request.TickType);
Assert.AreEqual(tz, MarketHours.ExchangeHours.TimeZone);
slices.Add(CreateReplySlice(request.Symbol, OpenInterestData[request.Symbol]));
}
return slices;
}
)
.Verifiable();
var data = OpenInterestData.Keys.ToDictionary(x => x, x => MarketHours);
var results = _underTest.FilterByOpenInterest(data).ToList();
// Results should be sorted by open interest (descending), and then by the date.
_mockHistoryProvider.Verify();
Assert.AreEqual(4, results.Count);
Assert.AreEqual(Feb, results[0]);
Assert.AreEqual(Jan, results[1]);
Assert.AreEqual(March, results[2]);
Assert.AreEqual(April, results[3]);
}
[Test]
public void Can_Limit_Number_Of_Contracts()
{
SetupSubject(6, 4);
var expected = Enumerable.Range(1, 4).Select(d => Symbol.CreateFuture(Futures.Metals.Gold, Market.COMEX, new DateTime(2020, 01, d))).ToList();
// Create 7 requests. Reverse the list so the order isn't correct, but remains consistent for tests.
var data = expected.Concat(Enumerable.Range(5, 3).Select(d => Symbol.CreateFuture(Futures.Metals.Gold, Market.COMEX, new DateTime(2020, 01, d))))
.Reverse()
.ToDictionary(x => x, _ => MarketHours);
// 7 input requests, but the look-up should be limited to only 6.
_mockHistoryProvider.Setup(x => x.GetHistory(It.IsAny<IEnumerable<HistoryRequest>>(), It.IsAny<DateTimeZone>()))
.Returns<IEnumerable<HistoryRequest>, DateTimeZone>((rq, tz) => rq.Select(r => CreateReplySlice(r.Symbol, 1)).ToArray());
// Run the test.
var results = _underTest.FilterByOpenInterest(data).ToList();
// Verify the chain limit was applied.
_mockHistoryProvider.Verify(x => x.GetHistory(It.Is<IEnumerable<HistoryRequest>>(r => r.Count() == 6), MarketHours.ExchangeHours.TimeZone), Times.Once);
// Verify the results.
CollectionAssert.AreEqual(expected, results);
}
[Test]
public void Limits_Do_Not_Need_To_Be_Provided()
{
SetupSubject(null, null);
var startDate = new DateTime(2020, 01, 01);
var items = Enumerable.Range(0, 100).ToDictionary(d => Symbol.CreateFuture(Futures.Metals.Gold, Market.COMEX, startDate.AddDays(d)), _ => MarketHours);
_mockHistoryProvider.Setup(x => x.GetHistory(It.IsAny<IEnumerable<HistoryRequest>>(), It.IsAny<DateTimeZone>()))
.Returns<IEnumerable<HistoryRequest>, DateTimeZone>((rq, tz) => rq.Select(r => CreateReplySlice(r.Symbol, 1)).ToArray());
var results = _underTest.FilterByOpenInterest(items).ToList();
_mockHistoryProvider.Verify(x => x.GetHistory(It.Is<IEnumerable<HistoryRequest>>(r => r.Count() == 100), MarketHours.ExchangeHours.TimeZone), Times.Once);
Assert.AreEqual(items.Keys, results);
}
private static Slice CreateReplySlice(Symbol symbol, decimal openInterest)
{
var ticks = new Ticks {{symbol, new List<Tick> {new OpenInterest(TestDate, symbol, openInterest)}}};
return new Slice(TestDate, null, null, null, ticks, null, null, null, null, null, null, null, TestDate, true);
}
private void SetupSubject(int? testChainContractLookupLimit, int? testResultsLimit)
{
_mockHistoryProvider = new Mock<IHistoryProvider>();
var mockAlgorithm = new Mock<IAlgorithm>();
mockAlgorithm.SetupGet(x => x.HistoryProvider).Returns(_mockHistoryProvider.Object);
mockAlgorithm.SetupGet(x => x.UtcTime).Returns(TestDate);
_underTest = new OpenInterestFutureUniverseSelectionModel(mockAlgorithm.Object, _ => OpenInterestData.Keys, testChainContractLookupLimit, testResultsLimit);
}
}
}
@@ -0,0 +1,265 @@
/*
* 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;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Interfaces;
using QuantConnect.Tests.Common.Data.Fundamental;
using System;
using System.Collections.Generic;
using System.Linq;
using static QuantConnect.Data.UniverseSelection.CoarseFundamentalDataProvider;
namespace QuantConnect.Tests.Algorithm.Framework.Selection
{
[TestFixture]
public class QC500UniverseSelectionModelTests
{
private readonly Dictionary<char, string> _industryTemplateCodeDict =
new Dictionary<char, string>
{
{'0', "B"}, {'1', "I"}, {'2', "M"}, {'3', "N"}, {'4', "T"}, {'5', "U"}
};
private readonly List<Symbol> _symbols = Enumerable.Range(0, 6000)
.Select(x => Symbol.Create($"{x:0000}", SecurityType.Equity, Market.USA))
.ToList();
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void FiltersUniverseCorrectlyWithValidData(Language language)
{
QCAlgorithm algorithm;
Dictionary<DateTime, int> coarseCountByDateTime;
Dictionary<DateTime, int> fineCountByDateTime;
RunSimulation(language,
(symbol, time) => new CoarseFundamentalSource
{
Symbol = symbol,
EndTime = time,
Value = 100,
VolumeSetter = 1000,
DollarVolumeSetter = 100000 * double.Parse(symbol.Value.Substring(3)),
HasFundamentalDataSetter = true
},
(symbol, time) => new FineFundamental(time, symbol)
{
Value = 100
},
new TestFundamentalDataProvider(_industryTemplateCodeDict),
out algorithm,
out coarseCountByDateTime,
out fineCountByDateTime);
var months = (algorithm.EndDate - algorithm.StartDate).Days / 30;
// Universe Changed 4 times
Assert.AreEqual(months, coarseCountByDateTime.Count);
Assert.AreEqual(months, fineCountByDateTime.Count);
// Universe Changed on the 1st. Coarse returned 1000 and Fine 500
Assert.IsTrue(coarseCountByDateTime.All(kvp => kvp.Key.Day == 1 && kvp.Value == 1000));
Assert.IsTrue(fineCountByDateTime.All(kvp => kvp.Key.Day == 1 && kvp.Value == 500));
}
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void DoesNotFilterUniverseWithCoarseDataHasFundamentalFalse(Language language)
{
QCAlgorithm algorithm;
Dictionary<DateTime, int> coarseCountByDateTime;
Dictionary<DateTime, int> fineCountByDateTime;
RunSimulation(language,
(symbol, time) => new CoarseFundamentalSource
{
Symbol = symbol,
EndTime = time,
},
(symbol, time) => new FineFundamental(time, symbol),
new TestFundamentalDataProvider(_industryTemplateCodeDict),
out algorithm,
out coarseCountByDateTime,
out fineCountByDateTime); ;
// No Universe Changes
Assert.AreEqual(0, coarseCountByDateTime.Count);
Assert.AreEqual(0, fineCountByDateTime.Count);
}
[TestCase(Language.CSharp)]
[TestCase(Language.Python)]
public void DoesNotFilterUniverseWithInvalidFineData(Language language)
{
QCAlgorithm algorithm;
Dictionary<DateTime, int> coarseCountByDateTime;
Dictionary<DateTime, int> fineCountByDateTime;
RunSimulation(language,
(symbol, time) => new CoarseFundamentalSource
{
Symbol = symbol,
EndTime = time,
Value = 100,
VolumeSetter = 1000,
DollarVolumeSetter = 100000 * double.Parse(symbol.Value.Substring(3)),
HasFundamentalDataSetter = true
},
(symbol, time) => new FineFundamental(time, symbol)
{
Value = 100
},
new NullFundamentalDataProvider(),
out algorithm,
out coarseCountByDateTime,
out fineCountByDateTime);
// Coarse Fundamental called every day.
Assert.Greater(coarseCountByDateTime.Count, 4);
Assert.IsTrue(coarseCountByDateTime.All(kvp => kvp.Value == 1000));
// No Universe Changes
Assert.AreEqual(0, fineCountByDateTime.Count);
}
private void RunSimulation(Language language,
Func<Symbol, DateTime, CoarseFundamental> getCoarseFundamental,
Func<Symbol, DateTime, FineFundamental> getFineFundamental,
IFundamentalDataProvider fundamentalDataProvider,
out QCAlgorithm algorithm,
out Dictionary<DateTime, int> coarseCountByDateTime,
out Dictionary<DateTime, int> fineCountByDateTime)
{
algorithm = new QCAlgorithm();
algorithm.SetStartDate(2019, 10, 1);
algorithm.SetEndDate(2020, 1, 30);
algorithm.SetDateTime(algorithm.StartDate.AddHours(6));
FundamentalService.Initialize(TestGlobals.DataProvider, fundamentalDataProvider, false);
coarseCountByDateTime = new Dictionary<DateTime, int>();
fineCountByDateTime = new Dictionary<DateTime, int>();
Func<QCAlgorithm, IEnumerable<CoarseFundamental>, IEnumerable<Symbol>> SelectCoarse;
Func<QCAlgorithm, IEnumerable<FineFundamental>, IEnumerable<Symbol>> SelectFine;
GetUniverseSelectionModel(language, out SelectCoarse, out SelectFine);
while (algorithm.EndDate > algorithm.UtcTime)
{
var time = algorithm.UtcTime;
var coarse = _symbols.Select(x => getCoarseFundamental(x, time));
var selectSymbolsResult = SelectCoarse(algorithm, coarse);
if (!ReferenceEquals(selectSymbolsResult, Universe.Unchanged))
{
coarseCountByDateTime[time] = selectSymbolsResult.Count();
var fine = selectSymbolsResult.Select(x => getFineFundamental(x, time));
selectSymbolsResult = SelectFine(algorithm, fine);
if (!ReferenceEquals(selectSymbolsResult, Universe.Unchanged))
{
fineCountByDateTime[time] = selectSymbolsResult.Count();
}
}
algorithm.SetDateTime(time.AddDays(1));
}
}
private void GetUniverseSelectionModel(
Language language,
out Func<QCAlgorithm, IEnumerable<CoarseFundamental>, IEnumerable<Symbol>> SelectCoarse,
out Func<QCAlgorithm, IEnumerable<FineFundamental>, IEnumerable<Symbol>> SelectFine)
{
if (language == Language.CSharp)
{
var model = new QC500UniverseSelectionModel();
SelectCoarse = model.SelectCoarse;
SelectFine = model.SelectFine;
return;
}
using (Py.GIL())
{
var name = "QC500UniverseSelectionModel";
dynamic model = Py.Import(name).GetAttr(name).Invoke();
SelectCoarse = ConvertToUniverseSelectionSymbolDelegate<IEnumerable<CoarseFundamental>>(model.select_coarse);
SelectFine = ConvertToUniverseSelectionSymbolDelegate<IEnumerable<FineFundamental>>(model.select_fine);
}
}
public static Func<QCAlgorithm, T, IEnumerable<Symbol>> ConvertToUniverseSelectionSymbolDelegate<T>(PyObject pySelector)
{
Func<QCAlgorithm, T, object> selector;
pySelector.TrySafeAs(out selector);
return (algorithm, data) =>
{
var result = selector(algorithm, data);
return ReferenceEquals(result, Universe.Unchanged)
? Universe.Unchanged
: ((object[])result).Select(x => (Symbol)x);
};
}
private class TestFundamentalDataProvider : IFundamentalDataProvider
{
private readonly Dictionary<char, string> _industryTemplateCodeDict;
public TestFundamentalDataProvider(Dictionary<char, string> industryTemplateCodeDict)
{
_industryTemplateCodeDict = industryTemplateCodeDict;
}
public T Get<T>(DateTime time, SecurityIdentifier securityIdentifier, FundamentalProperty name)
{
if (securityIdentifier == SecurityIdentifier.Empty)
{
return default;
}
return Get(time, securityIdentifier, name);
}
private dynamic Get(DateTime time, SecurityIdentifier securityIdentifier, FundamentalProperty enumName)
{
var name = Enum.GetName(enumName);
switch (name)
{
case "CompanyProfile_MarketCap":
return 500000001;
case "SecurityReference_IPODate":
return time.AddDays(-200);
case "EarningReports_BasicAverageShares_ThreeMonths":
return 5000000.01d;
case "CompanyReference_CountryId":
return "USA";
case "CompanyReference_PrimaryExchangeID":
return "NYS";
case "CompanyReference_IndustryTemplateCode":
return _industryTemplateCodeDict[securityIdentifier.Symbol[0]];
}
return null;
}
public void Initialize(IDataProvider dataProvider, bool liveMode)
{
}
}
}
}