chore: import upstream snapshot with attribution
This commit is contained in:
+516
@@ -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));
|
||||
}
|
||||
}
|
||||
+546
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+438
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
@@ -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) };
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -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) };
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+324
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+334
@@ -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 },
|
||||
};
|
||||
}
|
||||
}
|
||||
+76
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user