chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
|
||||
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Algorithm.Framework.Alphas;
|
||||
using QuantConnect.Scheduling;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an implementation of <see cref="IPortfolioConstructionModel"/> that allocates percent of account
|
||||
/// to each insight, defaulting to 3%.
|
||||
/// For insights of direction <see cref="InsightDirection.Up"/>, long targets are returned and
|
||||
/// for insights of direction <see cref="InsightDirection.Down"/>, short targets are returned.
|
||||
/// By default, no rebalancing shall be done.
|
||||
/// Rules:
|
||||
/// 1. On active Up insight, increase position size by percent
|
||||
/// 2. On active Down insight, decrease position size by percent
|
||||
/// 3. On active Flat insight, move by percent towards 0
|
||||
/// 4. On expired insight, and no other active insight, emits a 0 target'''
|
||||
/// </summary>
|
||||
public class AccumulativeInsightPortfolioConstructionModel : PortfolioConstructionModel
|
||||
{
|
||||
private readonly PortfolioBias _portfolioBias;
|
||||
private readonly double _percent;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="AccumulativeInsightPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalancingDateRules">The date rules used to define the next expected rebalance time
|
||||
/// in UTC</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="percent">The percentage amount of the portfolio value to allocate
|
||||
/// to a single insight. The value of percent should be in the range [0,1].
|
||||
/// The default value is 0.03.</param>
|
||||
public AccumulativeInsightPortfolioConstructionModel(IDateRule rebalancingDateRules,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
double percent = 0.03)
|
||||
: this(rebalancingDateRules.ToFunc(), portfolioBias, percent)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="AccumulativeInsightPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="percent">The percentage amount of the portfolio value to allocate
|
||||
/// to a single insight. The value of percent should be in the range [0,1].
|
||||
/// The default value is 0.03.</param>
|
||||
public AccumulativeInsightPortfolioConstructionModel(Func<DateTime, DateTime?> rebalancingFunc = null,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
double percent = 0.03)
|
||||
: base(rebalancingFunc)
|
||||
{
|
||||
_portfolioBias = portfolioBias;
|
||||
_percent = Math.Abs(percent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="AccumulativeInsightPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance UTC time.
|
||||
/// Returning current time will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="percent">The percentage amount of the portfolio value to allocate
|
||||
/// to a single insight. The value of percent should be in the range [0,1].
|
||||
/// The default value is 0.03.</param>
|
||||
public AccumulativeInsightPortfolioConstructionModel(Func<DateTime, DateTime> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
double percent = 0.03)
|
||||
: this(rebalancingFunc != null ? (Func<DateTime, DateTime?>)(timeUtc => rebalancingFunc(timeUtc)) : null,
|
||||
portfolioBias,
|
||||
percent)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="AccumulativeInsightPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalance">Rebalancing func or if a date rule, timedelta will be converted into func.
|
||||
/// For a given algorithm UTC DateTime the func returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <remarks>This is required since python net can not convert python methods into func nor resolve the correct
|
||||
/// constructor for the date rules parameter.
|
||||
/// For performance we prefer python algorithms using the C# implementation</remarks>
|
||||
/// <param name="percent">The percentage amount of the portfolio value to allocate
|
||||
/// to a single insight. The value of percent should be in the range [0,1].
|
||||
/// The default value is 0.03.</param>
|
||||
public AccumulativeInsightPortfolioConstructionModel(PyObject rebalance,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
double percent = 0.03)
|
||||
: this((Func<DateTime, DateTime?>)null,
|
||||
portfolioBias,
|
||||
percent)
|
||||
{
|
||||
SetRebalancingFunc(rebalance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="AccumulativeInsightPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="percent">The percentage amount of the portfolio value to allocate
|
||||
/// to a single insight. The value of percent should be in the range [0,1].
|
||||
/// The default value is 0.03.</param>
|
||||
public AccumulativeInsightPortfolioConstructionModel(TimeSpan timeSpan,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
double percent = 0.03)
|
||||
: this(dt => dt.Add(timeSpan), portfolioBias, percent)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="AccumulativeInsightPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="resolution">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="percent">The percentage amount of the portfolio value to allocate
|
||||
/// to a single insight. The value of percent should be in the range [0,1].
|
||||
/// The default value is 0.03.</param>
|
||||
public AccumulativeInsightPortfolioConstructionModel(Resolution resolution,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
double percent = 0.03)
|
||||
: this(resolution.ToTimeSpan(), portfolioBias, percent)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target insights to calculate a portfolio target percent for
|
||||
/// </summary>
|
||||
/// <returns>An enumerable of the target insights</returns>
|
||||
protected override List<Insight> GetTargetInsights()
|
||||
{
|
||||
return Algorithm.Insights.GetActiveInsights(Algorithm.UtcTime).Where(ShouldCreateTargetForInsight)
|
||||
.OrderBy(insight => insight.GeneratedTimeUtc)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the target percent for each insight
|
||||
/// </summary>
|
||||
/// <param name="activeInsights">The active insights to generate a target for</param>
|
||||
/// <returns>A target percent for each insight</returns>
|
||||
protected override Dictionary<Insight, double> DetermineTargetPercent(List<Insight> activeInsights)
|
||||
{
|
||||
var percentPerSymbol = new Dictionary<Symbol, double>();
|
||||
|
||||
foreach (var insight in activeInsights)
|
||||
{
|
||||
double targetPercent;
|
||||
if (percentPerSymbol.TryGetValue(insight.Symbol, out targetPercent))
|
||||
{
|
||||
if (insight.Direction == InsightDirection.Flat)
|
||||
{
|
||||
// We received a Flat
|
||||
// if adding or subtracting will push past 0, then make it 0
|
||||
if (Math.Abs(targetPercent) < _percent)
|
||||
{
|
||||
targetPercent = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// otherwise, we flatten by percent
|
||||
targetPercent += (targetPercent > 0 ? -_percent : _percent);
|
||||
}
|
||||
}
|
||||
}
|
||||
targetPercent += _percent * (int)insight.Direction;
|
||||
|
||||
// adjust to respect portfolio bias
|
||||
if (_portfolioBias != PortfolioBias.LongShort
|
||||
&& Math.Sign(targetPercent) != (int)_portfolioBias)
|
||||
{
|
||||
targetPercent = 0;
|
||||
}
|
||||
|
||||
percentPerSymbol[insight.Symbol] = targetPercent;
|
||||
}
|
||||
|
||||
return activeInsights.DistinctBy(insight => insight.Symbol)
|
||||
.ToDictionary(insight => insight, insight => percentPerSymbol[insight.Symbol]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
# 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.
|
||||
|
||||
from AlgorithmImports import *
|
||||
from EqualWeightingPortfolioConstructionModel import EqualWeightingPortfolioConstructionModel
|
||||
|
||||
class AccumulativeInsightPortfolioConstructionModel(EqualWeightingPortfolioConstructionModel):
|
||||
'''Provides an implementation of IPortfolioConstructionModel that allocates percent of account
|
||||
to each insight, defaulting to 3%.
|
||||
For insights of direction InsightDirection.UP, long targets are returned and
|
||||
for insights of direction InsightDirection.DOWN, short targets are returned.
|
||||
By default, no rebalancing shall be done.
|
||||
Rules:
|
||||
1. On active Up insight, increase position size by percent
|
||||
2. On active Down insight, decrease position size by percent
|
||||
3. On active Flat insight, move by percent towards 0
|
||||
4. On expired insight, and no other active insight, emits a 0 target'''
|
||||
|
||||
def __init__(self, rebalance = None, portfolio_bias = PortfolioBias.LONG_SHORT, percent = 0.03):
|
||||
'''Initialize a new instance of AccumulativeInsightPortfolioConstructionModel
|
||||
Args:
|
||||
rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function.
|
||||
If None will be ignored.
|
||||
The function returns the next expected rebalance time for a given algorithm UTC DateTime.
|
||||
The function returns null if unknown, in which case the function will be called again in the
|
||||
next loop. Returning current time will trigger rebalance.
|
||||
portfolio_bias: Specifies the bias of the portfolio (Short, Long/Short, Long)
|
||||
percent: percent of portfolio to allocate to each position'''
|
||||
super().__init__(rebalance)
|
||||
self.portfolio_bias = portfolio_bias
|
||||
self.percent = abs(percent)
|
||||
self.sign = lambda x: -1 if x < 0 else (1 if x > 0 else 0)
|
||||
|
||||
def determine_target_percent(self, active_insights):
|
||||
'''Will determine the target percent for each insight
|
||||
Args:
|
||||
active_insights: The active insights to generate a target for'''
|
||||
percent_per_symbol = {}
|
||||
|
||||
insights = sorted(self.algorithm.insights.get_active_insights(self.current_utc_time), key=lambda insight: insight.generated_time_utc)
|
||||
|
||||
for insight in insights:
|
||||
target_percent = 0
|
||||
if insight.symbol in percent_per_symbol:
|
||||
target_percent = percent_per_symbol[insight.symbol]
|
||||
if insight.direction == InsightDirection.FLAT:
|
||||
# We received a Flat
|
||||
# if adding or subtracting will push past 0, then make it 0
|
||||
if abs(target_percent) < self.percent:
|
||||
target_percent = 0
|
||||
else:
|
||||
# otherwise, we flatten by percent
|
||||
target_percent += (-self.percent if target_percent > 0 else self.percent)
|
||||
target_percent += self.percent * insight.direction
|
||||
|
||||
# adjust to respect portfolio bias
|
||||
if self.portfolio_bias != PortfolioBias.LONG_SHORT and self.sign(target_percent) != self.portfolio_bias:
|
||||
target_percent = 0
|
||||
|
||||
percent_per_symbol[insight.symbol] = target_percent
|
||||
|
||||
return dict((insight, percent_per_symbol[insight.symbol]) for insight in active_insights)
|
||||
|
||||
def create_targets(self, algorithm, insights):
|
||||
'''Create portfolio targets from the specified insights
|
||||
Args:
|
||||
algorithm: The algorithm instance
|
||||
insights: The insights to create portfolio targets from
|
||||
Returns:
|
||||
An enumerable of portfolio targets to be sent to the execution model'''
|
||||
self.current_utc_time = algorithm.utc_time
|
||||
return super().create_targets(algorithm, insights)
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
|
||||
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using QuantConnect.Data.UniverseSelection;
|
||||
using QuantConnect.Algorithm.Framework.Alphas;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Base alpha streams portfolio construction model
|
||||
/// </summary>
|
||||
public class AlphaStreamsPortfolioConstructionModel : IPortfolioConstructionModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Get's the weight for an alpha
|
||||
/// </summary>
|
||||
/// <param name="alphaId">The algorithm instance that experienced the change in securities</param>
|
||||
/// <returns>The alphas weight</returns>
|
||||
public virtual decimal GetAlphaWeight(string alphaId)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event fired each time the we add/remove securities from the data feed
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
|
||||
/// <param name="changes">The security additions and removals from the algorithm</param>
|
||||
public virtual void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create portfolio targets from the specified insights
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The algorithm instance</param>
|
||||
/// <param name="insights">The insights to create portfolio targets from</param>
|
||||
/// <returns>An enumerable of portfolio targets to be sent to the execution model</returns>
|
||||
public virtual IEnumerable<IPortfolioTarget> CreateTargets(QCAlgorithm algorithm, Insight[] insights)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
+455
@@ -0,0 +1,455 @@
|
||||
/*
|
||||
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
|
||||
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
using QuantConnect.Algorithm.Framework.Alphas;
|
||||
using QuantConnect.Data;
|
||||
using QuantConnect.Data.UniverseSelection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Accord.Statistics;
|
||||
using Accord.Math;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Scheduling;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an implementation of Black-Litterman portfolio optimization. The model adjusts equilibrium market
|
||||
/// returns by incorporating views from multiple alpha models and therefore to get the optimal risky portfolio
|
||||
/// reflecting those views. If insights of all alpha models have None magnitude or there are linearly dependent
|
||||
/// vectors in link matrix of views, the expected return would be the implied excess equilibrium return.
|
||||
/// The interval of weights in optimization method can be changed based on the long-short algorithm.
|
||||
/// The default model uses the 0.0025 as weight-on-views scalar parameter tau. The optimization method
|
||||
/// maximizes the Sharpe ratio with the weight range from -1 to 1.
|
||||
/// </summary>
|
||||
public class BlackLittermanOptimizationPortfolioConstructionModel : PortfolioConstructionModel
|
||||
{
|
||||
private readonly IPortfolioOptimizer _optimizer;
|
||||
private readonly PortfolioBias _portfolioBias;
|
||||
private readonly Resolution _resolution;
|
||||
private readonly double _riskFreeRate;
|
||||
private readonly double _delta;
|
||||
private readonly int _lookback;
|
||||
private readonly double _tau;
|
||||
private readonly int _period;
|
||||
|
||||
private readonly Dictionary<Symbol, ReturnsSymbolData> _symbolDataDict;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="riskFreeRate">The risk free rate</param>
|
||||
/// <param name="delta">The risk aversion coeffficient of the market portfolio</param>
|
||||
/// <param name="tau">The model parameter indicating the uncertainty of the CAPM prior</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If no algorithm is explicitly provided then the default will be max Sharpe ratio optimization.</param>
|
||||
public BlackLittermanOptimizationPortfolioConstructionModel(TimeSpan timeSpan,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 63,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
double riskFreeRate = 0.0,
|
||||
double delta = 2.5,
|
||||
double tau = 0.05,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: this(dt => dt.Add(timeSpan), portfolioBias, lookback, period, resolution, riskFreeRate, delta, tau, optimizer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="rebalanceResolution">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="riskFreeRate">The risk free rate</param>
|
||||
/// <param name="delta">The risk aversion coeffficient of the market portfolio</param>
|
||||
/// <param name="tau">The model parameter indicating the uncertainty of the CAPM prior</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If no algorithm is explicitly provided then the default will be max Sharpe ratio optimization.</param>
|
||||
public BlackLittermanOptimizationPortfolioConstructionModel(Resolution rebalanceResolution = Resolution.Daily,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 63,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
double riskFreeRate = 0.0,
|
||||
double delta = 2.5,
|
||||
double tau = 0.05,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: this(rebalanceResolution.ToTimeSpan(), portfolioBias, lookback, period, resolution, riskFreeRate, delta, tau, optimizer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance UTC time.
|
||||
/// Returning current time will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="riskFreeRate">The risk free rate</param>
|
||||
/// <param name="delta">The risk aversion coeffficient of the market portfolio</param>
|
||||
/// <param name="tau">The model parameter indicating the uncertainty of the CAPM prior</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If no algorithm is explicitly provided then the default will be max Sharpe ratio optimization.</param>
|
||||
public BlackLittermanOptimizationPortfolioConstructionModel(Func<DateTime, DateTime> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 63,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
double riskFreeRate = 0.0,
|
||||
double delta = 2.5,
|
||||
double tau = 0.05,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: this(rebalancingFunc != null ? (Func<DateTime, DateTime?>)(timeUtc => rebalancingFunc(timeUtc)) : null,
|
||||
portfolioBias,
|
||||
lookback,
|
||||
period,
|
||||
resolution,
|
||||
riskFreeRate,
|
||||
delta,
|
||||
tau,
|
||||
optimizer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="rebalancingDateRules">The date rules used to define the next expected rebalance time
|
||||
/// in UTC</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="riskFreeRate">The risk free rate</param>
|
||||
/// <param name="delta">The risk aversion coeffficient of the market portfolio</param>
|
||||
/// <param name="tau">The model parameter indicating the uncertainty of the CAPM prior</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If no algorithm is explicitly provided then the default will be max Sharpe ratio optimization.</param>
|
||||
public BlackLittermanOptimizationPortfolioConstructionModel(IDateRule rebalancingDateRules,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 63,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
double riskFreeRate = 0.0,
|
||||
double delta = 2.5,
|
||||
double tau = 0.05,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: this(rebalancingDateRules.ToFunc(), portfolioBias, lookback, period, resolution, riskFreeRate, delta, tau, optimizer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="rebalance">Rebalancing func or if a date rule, timedelta will be converted into func.
|
||||
/// For a given algorithm UTC DateTime the func returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="riskFreeRate">The risk free rate</param>
|
||||
/// <param name="delta">The risk aversion coeffficient of the market portfolio</param>
|
||||
/// <param name="tau">The model parameter indicating the uncertainty of the CAPM prior</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If no algorithm is explicitly provided then the default will be max Sharpe ratio optimization.</param>
|
||||
/// <remarks>This is required since python net can not convert python methods into func nor resolve the correct
|
||||
/// constructor for the date rules parameter.
|
||||
/// For performance we prefer python algorithms using the C# implementation</remarks>
|
||||
public BlackLittermanOptimizationPortfolioConstructionModel(PyObject rebalance,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 63,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
double riskFreeRate = 0.0,
|
||||
double delta = 2.5,
|
||||
double tau = 0.05,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: this((Func<DateTime, DateTime?>)null, portfolioBias, lookback, period, resolution, riskFreeRate, delta, tau, optimizer)
|
||||
{
|
||||
SetRebalancingFunc(rebalance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance.</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="riskFreeRate">The risk free rate</param>
|
||||
/// <param name="delta">The risk aversion coeffficient of the market portfolio</param>
|
||||
/// <param name="tau">The model parameter indicating the uncertainty of the CAPM prior</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If no algorithm is explicitly provided then the default will be max Sharpe ratio optimization.</param>
|
||||
public BlackLittermanOptimizationPortfolioConstructionModel(Func<DateTime, DateTime?> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 63,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
double riskFreeRate = 0.0,
|
||||
double delta = 2.5,
|
||||
double tau = 0.05,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: base(rebalancingFunc)
|
||||
{
|
||||
_lookback = lookback;
|
||||
_period = period;
|
||||
_resolution = resolution;
|
||||
_riskFreeRate = riskFreeRate;
|
||||
_delta = delta;
|
||||
_tau = tau;
|
||||
|
||||
var lower = portfolioBias == PortfolioBias.Long ? 0 : -1;
|
||||
var upper = portfolioBias == PortfolioBias.Short ? 0 : 1;
|
||||
_optimizer = optimizer ?? new MaximumSharpeRatioPortfolioOptimizer(lower, upper, riskFreeRate);
|
||||
_portfolioBias = portfolioBias;
|
||||
_symbolDataDict = new Dictionary<Symbol, ReturnsSymbolData>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method that will determine if the portfolio construction model should create a
|
||||
/// target for this insight
|
||||
/// </summary>
|
||||
/// <param name="insight">The insight to create a target for</param>
|
||||
/// <returns>True if the portfolio should create a target for the insight</returns>
|
||||
protected override bool ShouldCreateTargetForInsight(Insight insight)
|
||||
{
|
||||
return FilterInvalidInsightMagnitude(Algorithm, new []{ insight }).Length != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will determine the target percent for each insight
|
||||
/// </summary>
|
||||
/// <param name="activeInsights">The active insights to generate a target for</param>
|
||||
/// <returns>A target percent for each insight</returns>
|
||||
protected override Dictionary<Insight, double> DetermineTargetPercent(List<Insight> activeInsights)
|
||||
{
|
||||
var targets = new Dictionary<Insight, double>();
|
||||
|
||||
if (TryGetViews(activeInsights, out var P, out var Q))
|
||||
{
|
||||
// Updates the ReturnsSymbolData with insights
|
||||
foreach (var insight in activeInsights)
|
||||
{
|
||||
if (_symbolDataDict.TryGetValue(insight.Symbol, out var symbolData))
|
||||
{
|
||||
if (insight.Magnitude == null)
|
||||
{
|
||||
Algorithm.SetRunTimeError(new ArgumentNullException("BlackLittermanOptimizationPortfolioConstructionModel does not accept \'null\' as Insight.Magnitude. Please make sure your Alpha Model is generating Insights with the Magnitude property set."));
|
||||
return targets;
|
||||
}
|
||||
symbolData.Add(insight.GeneratedTimeUtc, insight.Magnitude.Value.SafeDecimalCast());
|
||||
}
|
||||
}
|
||||
// Get symbols' returns
|
||||
var symbols = activeInsights.Select(x => x.Symbol).Distinct().ToList();
|
||||
var returns = _symbolDataDict.FormReturnsMatrix(symbols);
|
||||
|
||||
// Calculate posterior estimate of the mean and uncertainty in the mean
|
||||
var Π = GetEquilibriumReturns(returns, out var Σ);
|
||||
|
||||
ApplyBlackLittermanMasterFormula(ref Π, ref Σ, P, Q);
|
||||
|
||||
// Create portfolio targets from the specified insights
|
||||
var W = _optimizer.Optimize(returns, Π, Σ);
|
||||
var sidx = 0;
|
||||
foreach (var symbol in symbols)
|
||||
{
|
||||
var weight = W[sidx];
|
||||
|
||||
// don't trust the optimizer
|
||||
if (_portfolioBias != PortfolioBias.LongShort
|
||||
&& Math.Sign(weight) != (int)_portfolioBias)
|
||||
{
|
||||
weight = 0;
|
||||
}
|
||||
targets[activeInsights.First(insight => insight.Symbol == symbol)] = weight;
|
||||
|
||||
sidx++;
|
||||
}
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target insights to calculate a portfolio target percent for
|
||||
/// </summary>
|
||||
/// <returns>An enumerable of the target insights</returns>
|
||||
protected override List<Insight> GetTargetInsights()
|
||||
{
|
||||
// Get insight that haven't expired of each symbol that is still in the universe
|
||||
var activeInsights = Algorithm.Insights.GetActiveInsights(Algorithm.UtcTime).Where(ShouldCreateTargetForInsight);
|
||||
|
||||
// Get the last generated active insight for each symbol
|
||||
return (from insight in activeInsights
|
||||
group insight by new { insight.Symbol, insight.SourceModel } into g
|
||||
select g.OrderBy(x => x.GeneratedTimeUtc).Last())
|
||||
.OrderBy(x => x.Symbol).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event fired each time the we add/remove securities from the data feed
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
|
||||
/// <param name="changes">The security additions and removals from the algorithm</param>
|
||||
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
|
||||
{
|
||||
base.OnSecuritiesChanged(algorithm, changes);
|
||||
|
||||
foreach (var symbol in changes.RemovedSecurities.Select(x => x.Symbol))
|
||||
{
|
||||
if (_symbolDataDict.ContainsKey(symbol))
|
||||
{
|
||||
_symbolDataDict[symbol].Reset();
|
||||
_symbolDataDict.Remove(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
// initialize data for added securities
|
||||
var addedSymbols = changes.AddedSecurities.ToDictionary(x => x.Symbol, x => x.Exchange.TimeZone);
|
||||
algorithm.History(addedSymbols.Keys, _lookback * _period, _resolution)
|
||||
.PushThrough(bar =>
|
||||
{
|
||||
ReturnsSymbolData symbolData;
|
||||
if (!_symbolDataDict.TryGetValue(bar.Symbol, out symbolData))
|
||||
{
|
||||
symbolData = new ReturnsSymbolData(bar.Symbol, _lookback, _period);
|
||||
_symbolDataDict.Add(bar.Symbol, symbolData);
|
||||
}
|
||||
// Convert the data timestamp to UTC
|
||||
var utcTime = bar.EndTime.ConvertToUtc(addedSymbols[bar.Symbol]);
|
||||
symbolData.Update(utcTime, bar.Value);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate equilibrium returns and covariance
|
||||
/// </summary>
|
||||
/// <param name="returns">Matrix of returns where each column represents a security and each row returns for the given date/time (size: K x N)</param>
|
||||
/// <param name="Σ">Multi-dimensional array of double with the portfolio covariance of returns (size: K x K).</param>
|
||||
/// <returns>Array of double of equilibrium returns</returns>
|
||||
public virtual double[] GetEquilibriumReturns(double[,] returns, out double[,] Σ)
|
||||
{
|
||||
// equal weighting scheme
|
||||
var W = Vector.Create(returns.GetLength(1), 1.0 / returns.GetLength(1));
|
||||
// annualized covariance
|
||||
Σ = returns.Covariance().Multiply(252);
|
||||
//annualized return
|
||||
var annualReturn = W.Dot(Elementwise.Add(returns.Mean(0), 1.0).Pow(252.0).Subtract(1.0));
|
||||
//annualized variance of return
|
||||
var annualVariance = W.Dot(Σ.Dot(W));
|
||||
// the risk aversion coefficient
|
||||
var riskAversion = (annualReturn - _riskFreeRate) / annualVariance;
|
||||
// the implied excess equilibrium return Vector (N x 1 column vector)
|
||||
return Σ.Dot(W).Multiply(riskAversion);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate views from multiple alpha models
|
||||
/// </summary>
|
||||
/// <param name="insights">Array of insight that represent the investors' views</param>
|
||||
/// <param name="P">A matrix that identifies the assets involved in the views (size: K x N)</param>
|
||||
/// <param name="Q">A view vector (size: K x 1)</param>
|
||||
protected bool TryGetViews(ICollection<Insight> insights, out double[,] P, out double[] Q)
|
||||
{
|
||||
try
|
||||
{
|
||||
var symbols = insights.Select(insight => insight.Symbol).ToHashSet();
|
||||
|
||||
var tmpQ = insights.GroupBy(insight => insight.SourceModel)
|
||||
.Select(values =>
|
||||
{
|
||||
var upInsightsSum = values.Where(i => i.Direction == InsightDirection.Up).Sum(i => Math.Abs(i.Magnitude.Value));
|
||||
var dnInsightsSum = values.Where(i => i.Direction == InsightDirection.Down).Sum(i => Math.Abs(i.Magnitude.Value));
|
||||
return new { View = values.Key, Q = upInsightsSum > dnInsightsSum ? upInsightsSum : dnInsightsSum };
|
||||
})
|
||||
.Where(x => x.Q != 0)
|
||||
.ToDictionary(k => k.View, v => v.Q);
|
||||
|
||||
var tmpP = insights.GroupBy(insight => insight.SourceModel)
|
||||
.Select(values =>
|
||||
{
|
||||
var q = tmpQ[values.Key];
|
||||
var results = values.ToDictionary(x => x.Symbol, insight =>
|
||||
{
|
||||
var value = (int)insight.Direction * Math.Abs(insight.Magnitude.Value);
|
||||
return value / q;
|
||||
});
|
||||
// Add zero for other symbols that are listed but active insight
|
||||
foreach (var symbol in symbols)
|
||||
{
|
||||
if (!results.ContainsKey(symbol))
|
||||
{
|
||||
results.Add(symbol, 0d);
|
||||
}
|
||||
}
|
||||
return new { View = values.Key, Results = results };
|
||||
})
|
||||
.Where(r => !r.Results.Select(v => Math.Abs(v.Value)).Sum().IsNaNOrZero())
|
||||
.ToDictionary(k => k.View, v => v.Results);
|
||||
|
||||
P = Matrix.Create(tmpP.Select(d => d.Value.Values.ToArray()).ToArray());
|
||||
Q = tmpQ.Values.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
P = null;
|
||||
Q = null;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply Black-Litterman master formula
|
||||
/// http://www.blacklitterman.org/cookbook.html
|
||||
/// </summary>
|
||||
/// <param name="Π">Prior/Posterior mean array</param>
|
||||
/// <param name="Σ">Prior/Posterior covariance matrix</param>
|
||||
/// <param name="P">A matrix that identifies the assets involved in the views (size: K x N)</param>
|
||||
/// <param name="Q">A view vector (size: K x 1)</param>
|
||||
private void ApplyBlackLittermanMasterFormula(ref double[] Π, ref double[,] Σ, double[,] P, double[] Q)
|
||||
{
|
||||
// Create the diagonal covariance matrix of error terms from the expressed views
|
||||
var eye = Matrix.Diagonal(Q.GetLength(0), 1);
|
||||
var Ω = Elementwise.Multiply(P.Dot(Σ).DotWithTransposed(P).Multiply(_tau), eye);
|
||||
if (Ω.Determinant() != 0)
|
||||
{
|
||||
// Define matrices Στ and A to avoid recalculations
|
||||
var Στ = Σ.Multiply(_tau);
|
||||
var A = Στ.DotWithTransposed(P).Dot(P.Dot(Στ).DotWithTransposed(P).Add(Ω).Inverse());
|
||||
|
||||
// Compute posterior estimate of the mean: Black-Litterman "master equation"
|
||||
Π = Π.Add(A.Dot(Q.Subtract(P.Dot(Π))));
|
||||
|
||||
// Compute posterior estimate of the uncertainty in the mean
|
||||
var M = Στ.Subtract(A.Dot(P).Dot(Στ));
|
||||
Σ = Σ.Add(M).Multiply(_delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
# 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.
|
||||
|
||||
from AlgorithmImports import *
|
||||
from Portfolio.MaximumSharpeRatioPortfolioOptimizer import MaximumSharpeRatioPortfolioOptimizer
|
||||
from itertools import groupby
|
||||
from numpy import dot, transpose
|
||||
from numpy.linalg import inv
|
||||
|
||||
### <summary>
|
||||
### Provides an implementation of Black-Litterman portfolio optimization. The model adjusts equilibrium market
|
||||
### returns by incorporating views from multiple alpha models and therefore to get the optimal risky portfolio
|
||||
### reflecting those views. If insights of all alpha models have None magnitude or there are linearly dependent
|
||||
### vectors in link matrix of views, the expected return would be the implied excess equilibrium return.
|
||||
### The interval of weights in optimization method can be changed based on the long-short algorithm.
|
||||
### The default model uses the 0.0025 as weight-on-views scalar parameter tau and
|
||||
### MaximumSharpeRatioPortfolioOptimizer that accepts a 63-row matrix of 1-day returns.
|
||||
### </summary>
|
||||
class BlackLittermanOptimizationPortfolioConstructionModel(PortfolioConstructionModel):
|
||||
def __init__(self,
|
||||
rebalance = Resolution.DAILY,
|
||||
portfolio_bias = PortfolioBias.LONG_SHORT,
|
||||
lookback = 1,
|
||||
period = 63,
|
||||
resolution = Resolution.DAILY,
|
||||
risk_free_rate = 0,
|
||||
delta = 2.5,
|
||||
tau = 0.05,
|
||||
optimizer = None):
|
||||
"""Initialize the model
|
||||
Args:
|
||||
rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function.
|
||||
If None will be ignored.
|
||||
The function returns the next expected rebalance time for a given algorithm UTC DateTime.
|
||||
The function returns null if unknown, in which case the function will be called again in the
|
||||
next loop. Returning current time will trigger rebalance.
|
||||
portfolio_bias: Specifies the bias of the portfolio (Short, Long/Short, Long)
|
||||
lookback(int): Historical return lookback period
|
||||
period(int): The time interval of history price to calculate the weight
|
||||
resolution: The resolution of the history price
|
||||
risk_free_rate(float): The risk free rate
|
||||
delta(float): The risk aversion coeffficient of the market portfolio
|
||||
tau(float): The model parameter indicating the uncertainty of the CAPM prior"""
|
||||
super().__init__()
|
||||
self.lookback = lookback
|
||||
self.period = period
|
||||
self.resolution = resolution
|
||||
self.risk_free_rate = risk_free_rate
|
||||
self.delta = delta
|
||||
self.tau = tau
|
||||
self.portfolio_bias = portfolio_bias
|
||||
|
||||
lower = 0 if portfolio_bias == PortfolioBias.LONG else -1
|
||||
upper = 0 if portfolio_bias == PortfolioBias.SHORT else 1
|
||||
self.optimizer = MaximumSharpeRatioPortfolioOptimizer(lower, upper, risk_free_rate) if optimizer is None else optimizer
|
||||
|
||||
self.sign = lambda x: -1 if x < 0 else (1 if x > 0 else 0)
|
||||
self.symbol_data_by_symbol = {}
|
||||
|
||||
# If the argument is an instance of Resolution or Timedelta
|
||||
# Redefine rebalancing_func
|
||||
rebalancing_func = rebalance
|
||||
if isinstance(rebalance, Resolution):
|
||||
rebalance = Extensions.to_time_span(rebalance)
|
||||
if isinstance(rebalance, timedelta):
|
||||
rebalancing_func = lambda dt: dt + rebalance
|
||||
if rebalancing_func:
|
||||
self.set_rebalancing_func(rebalancing_func)
|
||||
|
||||
def should_create_target_for_insight(self, insight):
|
||||
return PortfolioConstructionModel.filter_invalid_insight_magnitude(self.algorithm, [ insight ])
|
||||
|
||||
def determine_target_percent(self, last_active_insights):
|
||||
targets = {}
|
||||
|
||||
# Get view vectors
|
||||
p, q = self.get_views(last_active_insights)
|
||||
if p is not None:
|
||||
returns = dict()
|
||||
# Updates the BlackLittermanSymbolData with insights
|
||||
# Create a dictionary keyed by the symbols in the insights with an pandas.Series as value to create a data frame
|
||||
for insight in last_active_insights:
|
||||
symbol = insight.symbol
|
||||
symbol_data = self.symbol_data_by_symbol.get(symbol, self.BlackLittermanSymbolData(symbol, self.lookback, self.period))
|
||||
if insight.magnitude is None:
|
||||
self.algorithm.set_run_time_error(ArgumentNullException('BlackLittermanOptimizationPortfolioConstructionModel does not accept \'None\' as Insight.magnitude. Please make sure your Alpha Model is generating Insights with the Magnitude property set.'))
|
||||
return targets
|
||||
symbol_data.add(insight.generated_time_utc, insight.magnitude)
|
||||
returns[symbol] = symbol_data.return_
|
||||
|
||||
returns = pd.DataFrame(returns)
|
||||
|
||||
# Calculate prior estimate of the mean and covariance
|
||||
pi, sigma = self.get_equilibrium_return(returns)
|
||||
|
||||
# Calculate posterior estimate of the mean and covariance
|
||||
pi, sigma = self.apply_blacklitterman_master_formula(pi, sigma, p, q)
|
||||
|
||||
# Create portfolio targets from the specified insights
|
||||
weights = self.optimizer.optimize(returns, pi, sigma)
|
||||
weights = pd.Series(weights, index = sigma.columns)
|
||||
|
||||
for symbol, weight in weights.items():
|
||||
for insight in last_active_insights:
|
||||
if str(insight.symbol) == str(symbol):
|
||||
# don't trust the optimizer
|
||||
if self.portfolio_bias != PortfolioBias.LONG_SHORT and self.sign(weight) != self.portfolio_bias:
|
||||
weight = 0
|
||||
targets[insight] = weight
|
||||
break
|
||||
|
||||
return targets
|
||||
|
||||
def get_target_insights(self):
|
||||
# Get insight that haven't expired of each symbol that is still in the universe
|
||||
active_insights = filter(self.should_create_target_for_insight,
|
||||
self.algorithm.insights.get_active_insights(self.algorithm.utc_time))
|
||||
|
||||
# Get the last generated active insight for each symbol
|
||||
last_active_insights = []
|
||||
for source_model, f in groupby(sorted(active_insights, key = lambda ff: ff.source_model), lambda fff: fff.source_model):
|
||||
for symbol, g in groupby(sorted(list(f), key = lambda gg: gg.symbol), lambda ggg: ggg.symbol):
|
||||
last_active_insights.append(sorted(g, key = lambda x: x.generated_time_utc)[-1])
|
||||
return last_active_insights
|
||||
|
||||
def on_securities_changed(self, algorithm, changes):
|
||||
'''Event fired each time the we add/remove securities from the data feed
|
||||
Args:
|
||||
algorithm: The algorithm instance that experienced the change in securities
|
||||
changes: The security additions and removals from the algorithm'''
|
||||
|
||||
# Get removed symbol and invalidate them in the insight collection
|
||||
super().on_securities_changed(algorithm, changes)
|
||||
|
||||
for security in changes.removed_securities:
|
||||
symbol = security.symbol
|
||||
symbol_data = self.symbol_data_by_symbol.pop(symbol, None)
|
||||
if symbol_data is not None:
|
||||
symbol_data.reset()
|
||||
|
||||
# initialize data for added securities
|
||||
added_symbols = { x.symbol: x.exchange.time_zone for x in changes.added_securities }
|
||||
history = algorithm.history(list(added_symbols.keys()), self.lookback * self.period, self.resolution)
|
||||
|
||||
if history.empty:
|
||||
return
|
||||
|
||||
history = history.close.unstack(0)
|
||||
symbols = history.columns
|
||||
|
||||
for symbol, timezone in added_symbols.items():
|
||||
if str(symbol) not in symbols:
|
||||
continue
|
||||
|
||||
symbol_data = self.symbol_data_by_symbol.get(symbol, self.BlackLittermanSymbolData(symbol, self.lookback, self.period))
|
||||
for time, close in history[symbol].items():
|
||||
utc_time = Extensions.convert_to_utc(time, timezone)
|
||||
symbol_data.update(utc_time, close)
|
||||
|
||||
self.symbol_data_by_symbol[symbol] = symbol_data
|
||||
|
||||
def apply_blacklitterman_master_formula(self, Pi, Sigma, P, Q):
|
||||
'''Apply Black-Litterman master formula
|
||||
http://www.blacklitterman.org/cookbook.html
|
||||
Args:
|
||||
Pi: Prior/Posterior mean array
|
||||
Sigma: Prior/Posterior covariance matrix
|
||||
P: A matrix that identifies the assets involved in the views (size: K x N)
|
||||
Q: A view vector (size: K x 1)'''
|
||||
ts = self.tau * Sigma
|
||||
|
||||
# Create the diagonal Sigma matrix of error terms from the expressed views
|
||||
omega = np.dot(np.dot(P, ts), P.T) * np.eye(Q.shape[0])
|
||||
if np.linalg.det(omega) == 0:
|
||||
return Pi, Sigma
|
||||
|
||||
A = np.dot(np.dot(ts, P.T), inv(np.dot(np.dot(P, ts), P.T) + omega))
|
||||
|
||||
Pi = np.squeeze(np.asarray((
|
||||
np.expand_dims(Pi, axis=0).T +
|
||||
np.dot(A, (Q - np.expand_dims(np.dot(P, Pi.T), axis=1))))
|
||||
))
|
||||
|
||||
M = ts - np.dot(np.dot(A, P), ts)
|
||||
Sigma = (Sigma + M) * self.delta
|
||||
|
||||
return Pi, Sigma
|
||||
|
||||
def get_equilibrium_return(self, returns):
|
||||
'''Calculate equilibrium returns and covariance
|
||||
Args:
|
||||
returns: Matrix of returns where each column represents a security and each row returns for the given date/time (size: K x N)
|
||||
Returns:
|
||||
equilibrium_return: Array of double of equilibrium returns
|
||||
cov: Multi-dimensional array of double with the portfolio covariance of returns (size: K x K)'''
|
||||
|
||||
size = len(returns.columns)
|
||||
# equal weighting scheme
|
||||
W = np.array([1/size]*size)
|
||||
# the covariance matrix of excess returns (N x N matrix)
|
||||
cov = returns.cov()*252
|
||||
# annualized return
|
||||
annual_return = np.sum(((1 + returns.mean())**252 -1) * W)
|
||||
# annualized variance of return
|
||||
annual_variance = dot(W.T, dot(cov, W))
|
||||
# the risk aversion coefficient
|
||||
risk_aversion = (annual_return - self.risk_free_rate ) / annual_variance
|
||||
# the implied excess equilibrium return Vector (N x 1 column vector)
|
||||
equilibrium_return = dot(dot(risk_aversion, cov), W)
|
||||
|
||||
return equilibrium_return, cov
|
||||
|
||||
def get_views(self, insights):
|
||||
'''Generate views from multiple alpha models
|
||||
Args
|
||||
insights: Array of insight that represent the investors' views
|
||||
Returns
|
||||
P: A matrix that identifies the assets involved in the views (size: K x N)
|
||||
Q: A view vector (size: K x 1)'''
|
||||
try:
|
||||
P = {}
|
||||
Q = {}
|
||||
symbols = set(insight.symbol for insight in insights)
|
||||
|
||||
for model, group in groupby(insights, lambda x: x.source_model):
|
||||
group = list(group)
|
||||
|
||||
up_insights_sum = 0.0
|
||||
dn_insights_sum = 0.0
|
||||
for insight in group:
|
||||
if insight.direction == InsightDirection.UP:
|
||||
up_insights_sum = up_insights_sum + np.abs(insight.magnitude)
|
||||
if insight.direction == InsightDirection.DOWN:
|
||||
dn_insights_sum = dn_insights_sum + np.abs(insight.magnitude)
|
||||
|
||||
q = up_insights_sum if up_insights_sum > dn_insights_sum else dn_insights_sum
|
||||
if q == 0:
|
||||
continue
|
||||
|
||||
Q[model] = q
|
||||
|
||||
# generate the link matrix of views: P
|
||||
P[model] = dict()
|
||||
for insight in group:
|
||||
value = insight.direction * np.abs(insight.magnitude)
|
||||
P[model][insight.symbol] = value / q
|
||||
# Add zero for other symbols that are listed but active insight
|
||||
for symbol in symbols:
|
||||
if symbol not in P[model]:
|
||||
P[model][symbol] = 0
|
||||
|
||||
Q = np.array([[x] for x in Q.values()])
|
||||
if len(Q) > 0:
|
||||
P = np.array([list(x.values()) for x in P.values()])
|
||||
return P, Q
|
||||
except:
|
||||
pass
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
class BlackLittermanSymbolData:
|
||||
'''Contains data specific to a symbol required by this model'''
|
||||
def __init__(self, symbol, lookback, period):
|
||||
self._symbol = symbol
|
||||
self.roc = RateOfChange(f'{symbol}.roc({lookback})', lookback)
|
||||
self.roc.updated += self.on_rate_of_change_updated
|
||||
self.window = RollingWindow(period)
|
||||
|
||||
def reset(self):
|
||||
self.roc.updated -= self.on_rate_of_change_updated
|
||||
self.roc.reset()
|
||||
self.window.reset()
|
||||
|
||||
def update(self, utc_time, close):
|
||||
self.roc.update(utc_time, close)
|
||||
|
||||
def on_rate_of_change_updated(self, roc, value):
|
||||
if roc.is_ready:
|
||||
self.window.add(value)
|
||||
|
||||
def add(self, time, value):
|
||||
if self.window.samples > 0 and self.window[0].end_time == time:
|
||||
return
|
||||
|
||||
item = IndicatorDataPoint(self._symbol, time, value)
|
||||
self.window.add(item)
|
||||
|
||||
@property
|
||||
def return_(self):
|
||||
return pd.Series(
|
||||
data = [x.value for x in self.window],
|
||||
index = [x.end_time for x in self.window])
|
||||
|
||||
@property
|
||||
def is_ready(self):
|
||||
return self.window.is_ready
|
||||
|
||||
def __str__(self, **kwargs):
|
||||
return f'{self.roc.name}: {(1 + self.window[0])**252 - 1:.2%}'
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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 Python.Runtime;
|
||||
using QuantConnect.Algorithm.Framework.Alphas;
|
||||
using QuantConnect.Scheduling;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an implementation of <see cref="IPortfolioConstructionModel"/> that generates percent targets based on the
|
||||
/// <see cref="Insight.Confidence"/>. The target percent holdings of each Symbol is given by the <see cref="Insight.Confidence"/>
|
||||
/// from the last active <see cref="Insight"/> for that symbol.
|
||||
/// For insights of direction <see cref="InsightDirection.Up"/>, long targets are returned and for insights of direction
|
||||
/// <see cref="InsightDirection.Down"/>, short targets are returned.
|
||||
/// If the sum of all the last active <see cref="Insight"/> per symbol is bigger than 1, it will factor down each target
|
||||
/// percent holdings proportionally so the sum is 1.
|
||||
/// It will ignore <see cref="Insight"/> that have no <see cref="Insight.Confidence"/> value.
|
||||
/// </summary>
|
||||
public class ConfidenceWeightedPortfolioConstructionModel : InsightWeightingPortfolioConstructionModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="ConfidenceWeightedPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalancingDateRules">The date rules used to define the next expected rebalance time
|
||||
/// in UTC</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
public ConfidenceWeightedPortfolioConstructionModel(IDateRule rebalancingDateRules,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: base(rebalancingDateRules, portfolioBias)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="ConfidenceWeightedPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalance">Rebalancing func or if a date rule, timedelta will be converted into func.
|
||||
/// For a given algorithm UTC DateTime the func returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <remarks>This is required since python net can not convert python methods into func nor resolve the correct
|
||||
/// constructor for the date rules parameter.
|
||||
/// For performance we prefer python algorithms using the C# implementation</remarks>
|
||||
public ConfidenceWeightedPortfolioConstructionModel(PyObject rebalance,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: base(rebalance, portfolioBias)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="ConfidenceWeightedPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
public ConfidenceWeightedPortfolioConstructionModel(Func<DateTime, DateTime?> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: base(rebalancingFunc, portfolioBias)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="ConfidenceWeightedPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance UTC time.
|
||||
/// Returning current time will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
public ConfidenceWeightedPortfolioConstructionModel(Func<DateTime, DateTime> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: base(rebalancingFunc, portfolioBias)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="ConfidenceWeightedPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
public ConfidenceWeightedPortfolioConstructionModel(TimeSpan timeSpan,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: base(timeSpan, portfolioBias)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="ConfidenceWeightedPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="resolution">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
public ConfidenceWeightedPortfolioConstructionModel(Resolution resolution = Resolution.Daily,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: base(resolution, portfolioBias)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method that will determine if the portfolio construction model should create a
|
||||
/// target for this insight
|
||||
/// </summary>
|
||||
/// <param name="insight">The insight to create a target for</param>
|
||||
/// <returns>True if the portfolio should create a target for the insight</returns>
|
||||
protected override bool ShouldCreateTargetForInsight(Insight insight)
|
||||
{
|
||||
return insight.Confidence.HasValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method that will determine which member will be used to compute the weights and gets its value
|
||||
/// </summary>
|
||||
/// <param name="insight">The insight to create a target for</param>
|
||||
/// <returns>The value of the selected insight member</returns>
|
||||
protected override double GetValue(Insight insight) => insight.Confidence ?? 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
# 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.
|
||||
|
||||
from AlgorithmImports import *
|
||||
from InsightWeightingPortfolioConstructionModel import InsightWeightingPortfolioConstructionModel
|
||||
|
||||
class ConfidenceWeightedPortfolioConstructionModel(InsightWeightingPortfolioConstructionModel):
|
||||
'''Provides an implementation of IPortfolioConstructionModel that generates percent targets based on the
|
||||
Insight.CONFIDENCE. The target percent holdings of each Symbol is given by the Insight.CONFIDENCE from the last
|
||||
active Insight for that symbol.
|
||||
For insights of direction InsightDirection.UP, long targets are returned and for insights of direction
|
||||
InsightDirection.DOWN, short targets are returned.
|
||||
If the sum of all the last active Insight per symbol is bigger than 1, it will factor down each target
|
||||
percent holdings proportionally so the sum is 1.
|
||||
It will ignore Insight that have no Insight.CONFIDENCE value.'''
|
||||
|
||||
def __init__(self, rebalance = Resolution.DAILY, portfolio_bias = PortfolioBias.LONG_SHORT):
|
||||
'''Initialize a new instance of ConfidenceWeightedPortfolioConstructionModel
|
||||
Args:
|
||||
rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function.
|
||||
If None will be ignored.
|
||||
The function returns the next expected rebalance time for a given algorithm UTC DateTime.
|
||||
The function returns null if unknown, in which case the function will be called again in the
|
||||
next loop. Returning current time will trigger rebalance.
|
||||
portfolio_bias: Specifies the bias of the portfolio (Short, Long/Short, Long)'''
|
||||
super().__init__(rebalance, portfolio_bias)
|
||||
|
||||
def should_create_target_for_insight(self, insight):
|
||||
'''Method that will determine if the portfolio construction model should create a
|
||||
target for this insight
|
||||
Args:
|
||||
insight: The insight to create a target for'''
|
||||
# Ignore insights that don't have Confidence value
|
||||
return insight.confidence is not None
|
||||
|
||||
def get_value(self, insight):
|
||||
'''Method that will determine which member will be used to compute the weights and gets its value
|
||||
Args:
|
||||
insight: The insight to create a target for
|
||||
Returns:
|
||||
The value of the selected insight member'''
|
||||
return insight.confidence
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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 Python.Runtime;
|
||||
using QuantConnect.Algorithm.Framework.Alphas;
|
||||
using QuantConnect.Scheduling;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an implementation of <see cref="IPortfolioConstructionModel"/> that gives equal weighting to all
|
||||
/// securities. The target percent holdings of each security is 1/N where N is the number of securities. For
|
||||
/// insights of direction <see cref="InsightDirection.Up"/>, long targets are returned and for insights of direction
|
||||
/// <see cref="InsightDirection.Down"/>, short targets are returned.
|
||||
/// </summary>
|
||||
public class EqualWeightingPortfolioConstructionModel : PortfolioConstructionModel
|
||||
{
|
||||
private readonly PortfolioBias _portfolioBias;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="EqualWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalancingDateRules">The date rules used to define the next expected rebalance time
|
||||
/// in UTC</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
public EqualWeightingPortfolioConstructionModel(IDateRule rebalancingDateRules,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: this(rebalancingDateRules.ToFunc(), portfolioBias)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="EqualWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
public EqualWeightingPortfolioConstructionModel(Func<DateTime, DateTime?> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: base(rebalancingFunc)
|
||||
{
|
||||
_portfolioBias = portfolioBias;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="EqualWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance UTC time.
|
||||
/// Returning current time will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
public EqualWeightingPortfolioConstructionModel(Func<DateTime, DateTime> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: this(rebalancingFunc != null ? (Func<DateTime, DateTime?>)(timeUtc => rebalancingFunc(timeUtc)) : null, portfolioBias)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="EqualWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalance">Rebalancing func or if a date rule, timedelta will be converted into func.
|
||||
/// For a given algorithm UTC DateTime the func returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <remarks>This is required since python net can not convert python methods into func nor resolve the correct
|
||||
/// constructor for the date rules parameter.
|
||||
/// For performance we prefer python algorithms using the C# implementation</remarks>
|
||||
public EqualWeightingPortfolioConstructionModel(PyObject rebalance,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: this((Func<DateTime, DateTime?>)null, portfolioBias)
|
||||
{
|
||||
SetRebalancingFunc(rebalance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="EqualWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
public EqualWeightingPortfolioConstructionModel(TimeSpan timeSpan,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: this(dt => dt.Add(timeSpan), portfolioBias)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="EqualWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="resolution">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
public EqualWeightingPortfolioConstructionModel(Resolution resolution = Resolution.Daily,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: this(resolution.ToTimeSpan(), portfolioBias)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will determine the target percent for each insight
|
||||
/// </summary>
|
||||
/// <param name="activeInsights">The active insights to generate a target for</param>
|
||||
/// <returns>A target percent for each insight</returns>
|
||||
protected override Dictionary<Insight, double> DetermineTargetPercent(List<Insight> activeInsights)
|
||||
{
|
||||
var result = new Dictionary<Insight, double>(activeInsights.Count);
|
||||
|
||||
// give equal weighting to each security
|
||||
var count = activeInsights.Count(x => x.Direction != InsightDirection.Flat && RespectPortfolioBias(x));
|
||||
var percent = count == 0 ? 0 : 1m / count;
|
||||
foreach (var insight in activeInsights)
|
||||
{
|
||||
result[insight] =
|
||||
(double)((int)(RespectPortfolioBias(insight) ? insight.Direction : InsightDirection.Flat)
|
||||
* percent);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method that will determine if a given insight respects the portfolio bias
|
||||
/// </summary>
|
||||
/// <param name="insight">The insight to create a target for</param>
|
||||
/// <returns>True if the insight respects the portfolio bias</returns>
|
||||
protected bool RespectPortfolioBias(Insight insight)
|
||||
{
|
||||
return _portfolioBias == PortfolioBias.LongShort || (int)insight.Direction == (int)_portfolioBias;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
# 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.
|
||||
|
||||
from AlgorithmImports import *
|
||||
|
||||
class EqualWeightingPortfolioConstructionModel(PortfolioConstructionModel):
|
||||
'''Provides an implementation of IPortfolioConstructionModel that gives equal weighting to all securities.
|
||||
The target percent holdings of each security is 1/N where N is the number of securities.
|
||||
For insights of direction InsightDirection.UP, long targets are returned and
|
||||
for insights of direction InsightDirection.DOWN, short targets are returned.'''
|
||||
|
||||
def __init__(self, rebalance = Resolution.DAILY, portfolio_bias = PortfolioBias.LONG_SHORT):
|
||||
'''Initialize a new instance of EqualWeightingPortfolioConstructionModel
|
||||
Args:
|
||||
rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function.
|
||||
If None will be ignored.
|
||||
The function returns the next expected rebalance time for a given algorithm UTC DateTime.
|
||||
The function returns null if unknown, in which case the function will be called again in the
|
||||
next loop. Returning current time will trigger rebalance.
|
||||
portfolio_bias: Specifies the bias of the portfolio (Short, Long/Short, Long)'''
|
||||
super().__init__()
|
||||
self.portfolio_bias = portfolio_bias
|
||||
|
||||
# If the argument is an instance of Resolution or Timedelta
|
||||
# Redefine rebalancing_func
|
||||
rebalancing_func = rebalance
|
||||
if isinstance(rebalance, Resolution):
|
||||
rebalance = Extensions.to_time_span(rebalance)
|
||||
if isinstance(rebalance, timedelta):
|
||||
rebalancing_func = lambda dt: dt + rebalance
|
||||
if rebalancing_func:
|
||||
self.set_rebalancing_func(rebalancing_func)
|
||||
|
||||
def determine_target_percent(self, active_insights):
|
||||
'''Will determine the target percent for each insight
|
||||
Args:
|
||||
active_insights: The active insights to generate a target for'''
|
||||
result = {}
|
||||
|
||||
# give equal weighting to each security
|
||||
count = sum(x.direction != InsightDirection.FLAT and self.respect_portfolio_bias(x) for x in active_insights)
|
||||
percent = 0 if count == 0 else 1.0 / count
|
||||
for insight in active_insights:
|
||||
result[insight] = (insight.direction if self.respect_portfolio_bias(insight) else InsightDirection.FLAT) * percent
|
||||
return result
|
||||
|
||||
def respect_portfolio_bias(self, insight):
|
||||
'''Method that will determine if a given insight respects the portfolio bias
|
||||
Args:
|
||||
insight: The insight to create a target for
|
||||
'''
|
||||
return self.portfolio_bias == PortfolioBias.LONG_SHORT or insight.direction == self.portfolio_bias
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
|
||||
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Algorithm.Framework.Alphas;
|
||||
using QuantConnect.Scheduling;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an implementation of <see cref="IPortfolioConstructionModel"/> that generates percent targets based on the
|
||||
/// <see cref="Insight.Weight"/>. The target percent holdings of each Symbol is given by the <see cref="Insight.Weight"/>
|
||||
/// from the last active <see cref="Insight"/> for that symbol.
|
||||
/// For insights of direction <see cref="InsightDirection.Up"/>, long targets are returned and for insights of direction
|
||||
/// <see cref="InsightDirection.Down"/>, short targets are returned.
|
||||
/// If the sum of all the last active <see cref="Insight"/> per symbol is bigger than 1, it will factor down each target
|
||||
/// percent holdings proportionally so the sum is 1.
|
||||
/// It will ignore <see cref="Insight"/> that have no <see cref="Insight.Weight"/> value.
|
||||
/// </summary>
|
||||
public class InsightWeightingPortfolioConstructionModel : EqualWeightingPortfolioConstructionModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="InsightWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalancingDateRules">The date rules used to define the next expected rebalance time
|
||||
/// in UTC</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
public InsightWeightingPortfolioConstructionModel(IDateRule rebalancingDateRules,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: base(rebalancingDateRules, portfolioBias)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="InsightWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalance">Rebalancing func or if a date rule, timedelta will be converted into func.
|
||||
/// For a given algorithm UTC DateTime the func returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <remarks>This is required since python net can not convert python methods into func nor resolve the correct
|
||||
/// constructor for the date rules parameter.
|
||||
/// For performance we prefer python algorithms using the C# implementation</remarks>
|
||||
public InsightWeightingPortfolioConstructionModel(PyObject rebalance,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: base(rebalance, portfolioBias)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="InsightWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance.</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
public InsightWeightingPortfolioConstructionModel(Func<DateTime, DateTime?> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: base(rebalancingFunc, portfolioBias)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="InsightWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance UTC time.
|
||||
/// Returning current time will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
public InsightWeightingPortfolioConstructionModel(Func<DateTime, DateTime> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: base(rebalancingFunc, portfolioBias)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="InsightWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
public InsightWeightingPortfolioConstructionModel(TimeSpan timeSpan,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: base(timeSpan, portfolioBias)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="InsightWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="resolution">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
public InsightWeightingPortfolioConstructionModel(Resolution resolution = Resolution.Daily,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort)
|
||||
: base(resolution, portfolioBias)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method that will determine if the portfolio construction model should create a
|
||||
/// target for this insight
|
||||
/// </summary>
|
||||
/// <param name="insight">The insight to create a target for</param>
|
||||
/// <returns>True if the portfolio should create a target for the insight</returns>
|
||||
protected override bool ShouldCreateTargetForInsight(Insight insight)
|
||||
{
|
||||
return insight.Weight.HasValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will determine the target percent for each insight
|
||||
/// </summary>
|
||||
/// <param name="activeInsights">The active insights to generate a target for</param>
|
||||
/// <returns>A target percent for each insight</returns>
|
||||
protected override Dictionary<Insight, double> DetermineTargetPercent(List<Insight> activeInsights)
|
||||
{
|
||||
var result = new Dictionary<Insight, double>();
|
||||
// We will adjust weights proportionally in case the sum is > 1 so it sums to 1.
|
||||
var weightSums = activeInsights.Where(RespectPortfolioBias).Sum(insight => GetValue(insight));
|
||||
var weightFactor = 1.0;
|
||||
if (weightSums > 1)
|
||||
{
|
||||
weightFactor = 1 / weightSums;
|
||||
}
|
||||
foreach (var insight in activeInsights)
|
||||
{
|
||||
result[insight] = (int)(RespectPortfolioBias(insight) ? insight.Direction : InsightDirection.Flat)
|
||||
* GetValue(insight)
|
||||
* weightFactor;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method that will determine which member will be used to compute the weights and gets its value
|
||||
/// </summary>
|
||||
/// <param name="insight">The insight to create a target for</param>
|
||||
/// <returns>The value of the selected insight member</returns>
|
||||
protected virtual double GetValue(Insight insight) => insight.Weight != null ? Math.Abs((double)insight.Weight) : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
# 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.
|
||||
|
||||
from AlgorithmImports import *
|
||||
from EqualWeightingPortfolioConstructionModel import EqualWeightingPortfolioConstructionModel
|
||||
|
||||
class InsightWeightingPortfolioConstructionModel(EqualWeightingPortfolioConstructionModel):
|
||||
'''Provides an implementation of IPortfolioConstructionModel that generates percent targets based on the
|
||||
Insight.WEIGHT. The target percent holdings of each Symbol is given by the Insight.WEIGHT from the last
|
||||
active Insight for that symbol.
|
||||
For insights of direction InsightDirection.UP, long targets are returned and for insights of direction
|
||||
InsightDirection.DOWN, short targets are returned.
|
||||
If the sum of all the last active Insight per symbol is bigger than 1, it will factor down each target
|
||||
percent holdings proportionally so the sum is 1.
|
||||
It will ignore Insight that have no Insight.WEIGHT value.'''
|
||||
|
||||
def __init__(self, rebalance = Resolution.DAILY, portfolio_bias = PortfolioBias.LONG_SHORT):
|
||||
'''Initialize a new instance of InsightWeightingPortfolioConstructionModel
|
||||
Args:
|
||||
rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function.
|
||||
If None will be ignored.
|
||||
The function returns the next expected rebalance time for a given algorithm UTC DateTime.
|
||||
The function returns null if unknown, in which case the function will be called again in the
|
||||
next loop. Returning current time will trigger rebalance.
|
||||
portfolio_bias: Specifies the bias of the portfolio (Short, Long/Short, Long)'''
|
||||
super().__init__(rebalance, portfolio_bias)
|
||||
|
||||
def should_create_target_for_insight(self, insight):
|
||||
'''Method that will determine if the portfolio construction model should create a
|
||||
target for this insight
|
||||
Args:
|
||||
insight: The insight to create a target for'''
|
||||
# Ignore insights that don't have Weight value
|
||||
return insight.weight is not None
|
||||
|
||||
def determine_target_percent(self, active_insights):
|
||||
'''Will determine the target percent for each insight
|
||||
Args:
|
||||
active_insights: The active insights to generate a target for'''
|
||||
result = {}
|
||||
|
||||
# We will adjust weights proportionally in case the sum is > 1 so it sums to 1.
|
||||
weight_sums = sum(self.get_value(insight) for insight in active_insights if self.respect_portfolio_bias(insight))
|
||||
weight_factor = 1.0
|
||||
if weight_sums > 1:
|
||||
weight_factor = 1 / weight_sums
|
||||
for insight in active_insights:
|
||||
result[insight] = (insight.direction if self.respect_portfolio_bias(insight) else InsightDirection.FLAT) * self.get_value(insight) * weight_factor
|
||||
return result
|
||||
|
||||
def get_value(self, insight):
|
||||
'''Method that will determine which member will be used to compute the weights and gets its value
|
||||
Args:
|
||||
insight: The insight to create a target for
|
||||
Returns:
|
||||
The value of the selected insight member'''
|
||||
return abs(insight.weight)
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
|
||||
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Accord.Math;
|
||||
using Accord.Math.Optimization;
|
||||
using Accord.Statistics;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an implementation of a portfolio optimizer that maximizes the portfolio Sharpe Ratio.
|
||||
/// The interval of weights in optimization method can be changed based on the long-short algorithm.
|
||||
/// The default model uses flat risk free rate and weight for an individual security range from -1 to 1.
|
||||
/// </summary>
|
||||
public class MaximumSharpeRatioPortfolioOptimizer : IPortfolioOptimizer
|
||||
{
|
||||
private double _lower;
|
||||
private double _upper;
|
||||
private double _riskFreeRate;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="MaximumSharpeRatioPortfolioOptimizer"/>
|
||||
/// </summary>
|
||||
/// <param name="lower">Lower constraint</param>
|
||||
/// <param name="upper">Upper constraint</param>
|
||||
/// <param name="riskFreeRate"></param>
|
||||
public MaximumSharpeRatioPortfolioOptimizer(double lower = -1, double upper = 1, double riskFreeRate = 0.0)
|
||||
{
|
||||
_lower = lower;
|
||||
_upper = upper;
|
||||
_riskFreeRate = riskFreeRate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Boundary constraints on weights: lw ≤ w ≤ up
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Expressed in the substituted variable y = κw (κ = 1ᵀy > 0), the per-weight bounds
|
||||
/// become linear: yᵢ − up·(1ᵀy) ≤ 0 and yᵢ − lw·(1ᵀy) ≥ 0.
|
||||
/// </remarks>
|
||||
/// <param name="size">number of variables</param>
|
||||
/// <returns>enumeration of linear constraint objects</returns>
|
||||
protected IEnumerable<LinearConstraint> GetBoundaryConditions(int size)
|
||||
{
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
// yᵢ − up·(1ᵀy) ≤ 0
|
||||
var upper = Vector.Create(size, -_upper);
|
||||
upper[i] += 1.0;
|
||||
yield return new LinearConstraint(size)
|
||||
{
|
||||
CombinedAs = upper,
|
||||
ShouldBe = ConstraintType.LesserThanOrEqualTo,
|
||||
Value = 0.0
|
||||
};
|
||||
// yᵢ − lw·(1ᵀy) ≥ 0
|
||||
var lower = Vector.Create(size, -_lower);
|
||||
lower[i] += 1.0;
|
||||
yield return new LinearConstraint(size)
|
||||
{
|
||||
CombinedAs = lower,
|
||||
ShouldBe = ConstraintType.GreaterThanOrEqualTo,
|
||||
Value = 0.0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform portfolio optimization for a provided matrix of historical returns and an array of expected returns
|
||||
/// </summary>
|
||||
/// <param name="historicalReturns">Matrix of annualized historical returns where each column represents a security and each row returns for the given date/time (size: K x N).</param>
|
||||
/// <param name="expectedReturns">Array of double with the portfolio annualized expected returns (size: K x 1).</param>
|
||||
/// <param name="covariance">Multi-dimensional array of double with the portfolio covariance of annualized returns (size: K x K).</param>
|
||||
/// <returns>Array of double with the portfolio weights (size: K x 1)</returns>
|
||||
public double[] Optimize(double[,] historicalReturns, double[] expectedReturns = null, double[,] covariance = null)
|
||||
{
|
||||
covariance = covariance ?? historicalReturns.Covariance();
|
||||
var returns = (expectedReturns ?? historicalReturns.Mean(0)).Subtract(_riskFreeRate);
|
||||
|
||||
var size = covariance.GetLength(0);
|
||||
var equalWeights = Vector.Create(size, 1.0 / size);
|
||||
|
||||
// The Charnes-Cooper substitution needs a portfolio with positive expected excess
|
||||
// return to exist, otherwise the Sharpe ratio cannot be maximized.
|
||||
var feasible = _lower >= 0 ? returns.Any(x => x > 0) : returns.Any(x => x != 0);
|
||||
if (!feasible)
|
||||
{
|
||||
return equalWeights;
|
||||
}
|
||||
|
||||
// Charnes-Cooper substitution y = κw (κ = 1ᵀy): maximizing the Sharpe ratio
|
||||
// (µ − r_f)ᵀw / √(wᵀΣw) becomes minimizing wᵀΣw subject to (µ − r_f)ᵀy = 1,
|
||||
// recovering the weights afterwards as w = y / (1ᵀy).
|
||||
// https://quant.stackexchange.com/questions/18521/sharpe-maximization-under-quadratic-constraints
|
||||
var constraints = new List<LinearConstraint>
|
||||
{
|
||||
// (µ − r_f)ᵀy = 1
|
||||
new LinearConstraint(size)
|
||||
{
|
||||
CombinedAs = returns,
|
||||
ShouldBe = ConstraintType.EqualTo,
|
||||
Value = 1.0
|
||||
}
|
||||
};
|
||||
|
||||
// lw ≤ w ≤ up
|
||||
constraints.AddRange(GetBoundaryConditions(size));
|
||||
|
||||
// Setup solver: minimize yᵀΣy
|
||||
var optfunc = new QuadraticObjectiveFunction(covariance, Vector.Create(size, 0.0));
|
||||
var solver = new GoldfarbIdnani(optfunc, constraints);
|
||||
|
||||
// Solve problem
|
||||
var success = solver.Minimize(Vector.Copy(equalWeights));
|
||||
if (!success)
|
||||
{
|
||||
return equalWeights;
|
||||
}
|
||||
|
||||
// Recover the portfolio weights: w = y / (1ᵀy)
|
||||
var y = solver.Solution;
|
||||
var sum = y.Sum();
|
||||
return sum > 0 ? y.Divide(sum) : equalWeights;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
# 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.
|
||||
|
||||
from AlgorithmImports import *
|
||||
from scipy.optimize import minimize
|
||||
|
||||
### <summary>
|
||||
### Provides an implementation of a portfolio optimizer that maximizes the portfolio Sharpe Ratio.
|
||||
### The interval of weights in optimization method can be changed based on the long-short algorithm.
|
||||
### The default model uses flat risk free rate and weight for an individual security range from -1 to 1.'''
|
||||
### </summary>
|
||||
class MaximumSharpeRatioPortfolioOptimizer:
|
||||
'''Provides an implementation of a portfolio optimizer that maximizes the portfolio Sharpe Ratio.
|
||||
The interval of weights in optimization method can be changed based on the long-short algorithm.
|
||||
The default model uses flat risk free rate and weight for an individual security range from -1 to 1.'''
|
||||
def __init__(self,
|
||||
minimum_weight = -1,
|
||||
maximum_weight = 1,
|
||||
risk_free_rate = 0):
|
||||
'''Initialize the MaximumSharpeRatioPortfolioOptimizer
|
||||
Args:
|
||||
minimum_weight(float): The lower bounds on portfolio weights
|
||||
maximum_weight(float): The upper bounds on portfolio weights
|
||||
risk_free_rate(float): The risk free rate'''
|
||||
self.minimum_weight = minimum_weight
|
||||
self.maximum_weight = maximum_weight
|
||||
self.risk_free_rate = risk_free_rate
|
||||
self.expected_returns = []
|
||||
|
||||
def optimize(self, historical_returns, expected_returns = None, covariance = None):
|
||||
'''
|
||||
Perform portfolio optimization for a provided matrix of historical returns and an array of expected returns
|
||||
args:
|
||||
historical_returns: Matrix of annualized historical returns where each column represents a security and each row returns for the given date/time (size: K x N).
|
||||
expected_returns: Array of double with the portfolio annualized expected returns (size: K x 1).
|
||||
covariance: Multi-dimensional array of double with the portfolio covariance of annualized returns (size: K x K).
|
||||
Returns:
|
||||
Array of double with the portfolio weights (size: K x 1)
|
||||
'''
|
||||
if covariance is None:
|
||||
covariance = historical_returns.cov()
|
||||
if expected_returns is None:
|
||||
expected_returns = historical_returns.mean()
|
||||
expected_returns = expected_returns - self.risk_free_rate
|
||||
|
||||
size = covariance.columns.size # K x 1
|
||||
x0 = np.array(size * [1. / size])
|
||||
|
||||
# SLSQP maximizes the Sharpe ratio (µ − r_f)^T w / √(w^T Σ w) directly, so the fractional
|
||||
# objective is optimized in place without any substitution. The budget constraint Σw = 1 and
|
||||
# the per-weight bounds lw ≤ w ≤ up are applied as-is. The previous implementation instead
|
||||
# fixed (µ − r_f)^T w to the equal-weight return, which collapsed the optimizer to minimum
|
||||
# variance. The C# implementation uses the Charnes-Cooper QP substitution because its solver
|
||||
# only handles quadratic objectives.
|
||||
# https://quant.stackexchange.com/questions/18521/sharpe-maximization-under-quadratic-constraints
|
||||
constraints = [
|
||||
# Σw = 1
|
||||
{'type': 'eq', 'fun': lambda weights: self.get_budget_constraint(weights)}]
|
||||
|
||||
opt = minimize(lambda weights: -expected_returns.dot(weights) / np.sqrt(self.portfolio_variance(weights, covariance)), # Objective function: −Sharpe ratio
|
||||
x0, # Initial guess
|
||||
bounds = self.get_boundary_conditions(size), # Bounds for variables: lw ≤ w ≤ up
|
||||
constraints = constraints, # Constraints definition
|
||||
method='SLSQP') # Optimization method: Sequential Least SQuares Programming
|
||||
|
||||
return opt['x'] if opt['success'] else x0
|
||||
|
||||
def portfolio_variance(self, weights, covariance):
|
||||
'''Computes the portfolio variance
|
||||
Args:
|
||||
weighs: Portfolio weights
|
||||
covariance: Covariance matrix of historical returns'''
|
||||
variance = np.dot(weights.T, np.dot(covariance, weights))
|
||||
if variance == 0 and np.any(weights):
|
||||
# variance can't be zero, with non zero weights
|
||||
raise ValueError(f'MaximumSharpeRatioPortfolioOptimizer.portfolio_variance: Volatility cannot be zero. Weights: {weights}')
|
||||
return variance
|
||||
|
||||
def get_boundary_conditions(self, size):
|
||||
'''Creates the boundary condition for the portfolio weights'''
|
||||
return tuple((self.minimum_weight, self.maximum_weight) for x in range(size))
|
||||
|
||||
def get_budget_constraint(self, weights):
|
||||
'''Defines a budget constraint: the sum of the weights equals unity'''
|
||||
return np.sum(weights) - 1
|
||||
@@ -0,0 +1,350 @@
|
||||
/*
|
||||
* 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 Accord.Math;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Algorithm.Framework.Alphas;
|
||||
using QuantConnect.Data.UniverseSelection;
|
||||
using QuantConnect.Indicators;
|
||||
using QuantConnect.Scheduling;
|
||||
using QuantConnect.Util;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of On-Line Moving Average Reversion (OLMAR)
|
||||
/// </summary>
|
||||
/// <remarks>Li, B., Hoi, S. C. (2012). On-line portfolio selection with moving average reversion. arXiv preprint arXiv:1206.4626.
|
||||
/// Available at https://arxiv.org/ftp/arxiv/papers/1206/1206.4626.pdf</remarks>
|
||||
/// <remarks>Using windowSize = 1 => Passive Aggressive Mean Reversion (PAMR) Portfolio</remarks>
|
||||
public class MeanReversionPortfolioConstructionModel : PortfolioConstructionModel
|
||||
{
|
||||
private int _numOfAssets;
|
||||
private double[] _weightVector;
|
||||
private decimal _reversionThreshold;
|
||||
private int _windowSize;
|
||||
private Resolution _resolution;
|
||||
private Dictionary<Symbol, MeanReversionSymbolData> _symbolData = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
|
||||
/// </summary>
|
||||
/// <param name="rebalancingDateRules">The date rules used to define the next expected rebalance time
|
||||
/// in UTC</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="reversionThreshold">Reversion threshold</param>
|
||||
/// <param name="windowSize">Window size of mean price</param>
|
||||
/// <param name="resolution">The resolution of the history price and rebalancing</param>
|
||||
public MeanReversionPortfolioConstructionModel(IDateRule rebalancingDateRules,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
decimal reversionThreshold = 1,
|
||||
int windowSize = 20,
|
||||
Resolution resolution = Resolution.Daily)
|
||||
: this(rebalancingDateRules.ToFunc(), portfolioBias, reversionThreshold, windowSize, resolution)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
|
||||
/// </summary>
|
||||
/// <param name="rebalanceResolution">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="reversionThreshold">Reversion threshold</param>
|
||||
/// <param name="windowSize">Window size of mean price</param>
|
||||
/// <param name="resolution">The resolution of the history price and rebalancing</param>
|
||||
public MeanReversionPortfolioConstructionModel(Resolution rebalanceResolution = Resolution.Daily,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
decimal reversionThreshold = 1,
|
||||
int windowSize = 20,
|
||||
Resolution resolution = Resolution.Daily)
|
||||
: this(rebalanceResolution.ToTimeSpan(), portfolioBias, reversionThreshold, windowSize, resolution)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="reversionThreshold">Reversion threshold</param>
|
||||
/// <param name="windowSize">Window size of mean price</param>
|
||||
/// <param name="resolution">The resolution of the history price and rebalancing</param>
|
||||
public MeanReversionPortfolioConstructionModel(TimeSpan timeSpan,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
decimal reversionThreshold = 1,
|
||||
int windowSize = 20,
|
||||
Resolution resolution = Resolution.Daily)
|
||||
: this(dt => dt.Add(timeSpan), portfolioBias, reversionThreshold, windowSize, resolution)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
|
||||
/// </summary>
|
||||
/// <param name="rebalance">Rebalancing func or if a date rule, timedelta will be converted into func.
|
||||
/// For a given algorithm UTC DateTime the func returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="reversionThreshold">Reversion threshold</param>
|
||||
/// <param name="windowSize">Window size of mean price</param>
|
||||
/// <param name="resolution">The resolution of the history price and rebalancing</param>
|
||||
public MeanReversionPortfolioConstructionModel(PyObject rebalance,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
decimal reversionThreshold = 1,
|
||||
int windowSize = 20,
|
||||
Resolution resolution = Resolution.Daily)
|
||||
: this((Func<DateTime, DateTime?>)null, portfolioBias, reversionThreshold, windowSize, resolution)
|
||||
{
|
||||
SetRebalancingFunc(rebalance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance. If null will be ignored.</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="reversionThreshold">Reversion threshold</param>
|
||||
/// <param name="windowSize">Window size of mean price</param>
|
||||
/// <param name="resolution">The resolution of the history price and rebalancing</param>
|
||||
public MeanReversionPortfolioConstructionModel(Func<DateTime, DateTime> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
decimal reversionThreshold = 1,
|
||||
int windowSize = 20,
|
||||
Resolution resolution = Resolution.Daily)
|
||||
: this(rebalancingFunc != null ? (Func<DateTime, DateTime?>)(timeUtc => rebalancingFunc(timeUtc)) : null,
|
||||
portfolioBias, reversionThreshold, windowSize, resolution)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance.</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="reversionThreshold">Reversion threshold</param>
|
||||
/// <param name="windowSize">Window size of mean price</param>
|
||||
/// <param name="resolution">The resolution of the history price and rebalancing</param>
|
||||
public MeanReversionPortfolioConstructionModel(Func<DateTime, DateTime?> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
decimal reversionThreshold = 1,
|
||||
int windowSize = 20,
|
||||
Resolution resolution = Resolution.Daily)
|
||||
: base(rebalancingFunc)
|
||||
{
|
||||
if (portfolioBias == PortfolioBias.Short)
|
||||
{
|
||||
throw new ArgumentException("Long position must be allowed in MeanReversionPortfolioConstructionModel.");
|
||||
}
|
||||
|
||||
_reversionThreshold = reversionThreshold;
|
||||
_resolution = resolution;
|
||||
_windowSize = windowSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will determine the target percent for each insight
|
||||
/// </summary>
|
||||
/// <param name="activeInsights">list of active insights</param>
|
||||
/// <return>dictionary of insight and respective target weight</return>
|
||||
protected override Dictionary<Insight, double> DetermineTargetPercent(List<Insight> activeInsights)
|
||||
{
|
||||
var targets = new Dictionary<Insight, double>();
|
||||
|
||||
// If we have no insights or non-ready just return an empty target list
|
||||
if (activeInsights.IsNullOrEmpty() ||
|
||||
!activeInsights.All(x => _symbolData[x.Symbol].IsReady()))
|
||||
{
|
||||
return targets;
|
||||
}
|
||||
|
||||
var numOfAssets = activeInsights.Count;
|
||||
if (_numOfAssets != numOfAssets)
|
||||
{
|
||||
_numOfAssets = numOfAssets;
|
||||
// Initialize price vector and portfolio weightings vector
|
||||
_weightVector = Enumerable.Repeat((double) 1/_numOfAssets, _numOfAssets).ToArray();
|
||||
}
|
||||
|
||||
// Get price relatives vs expected price (SMA)
|
||||
var priceRelatives = GetPriceRelatives(activeInsights); // \tilde{x}_{t+1}
|
||||
|
||||
// Get step size of next portfolio
|
||||
// \bar{x}_{t+1} = 1^T * \tilde{x}_{t+1} / m
|
||||
// \lambda_{t+1} = max( 0, ( b_t * \tilde{x}_{t+1} - \epsilon ) / ||\tilde{x}_{t+1} - \bar{x}_{t+1} * 1|| ^ 2 )
|
||||
var nextPrediction = priceRelatives.Average(); // \bar{x}_{t+1}
|
||||
var assetsMeanDev = priceRelatives.Select(x => x - nextPrediction).ToArray();
|
||||
var secondNorm = Math.Pow(assetsMeanDev.Euclidean(), 2);
|
||||
double stepSize; // \lambda_{t+1}
|
||||
|
||||
if (secondNorm == 0d)
|
||||
{
|
||||
stepSize = 0d;
|
||||
}
|
||||
else
|
||||
{
|
||||
stepSize = (_weightVector.InnerProduct(priceRelatives) - (double)_reversionThreshold) / secondNorm;
|
||||
stepSize = Math.Max(0d, stepSize);
|
||||
}
|
||||
|
||||
// Get next portfolio weightings
|
||||
// b_{t+1} = b_t - step_size * ( \tilde{x}_{t+1} - \bar{x}_{t+1} * 1 )
|
||||
var nextPortfolio = _weightVector.Select((x, i) => x - assetsMeanDev[i] * stepSize);
|
||||
// Normalize
|
||||
var normalizedPortfolioWeightVector = SimplexProjection(nextPortfolio);
|
||||
// Save normalized result for the next portfolio step
|
||||
_weightVector = normalizedPortfolioWeightVector;
|
||||
|
||||
// Update portfolio state
|
||||
for (int i = 0; i < _numOfAssets; i++)
|
||||
{
|
||||
targets.Add(activeInsights[i], normalizedPortfolioWeightVector[i]);
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get price relatives with reference level of SMA
|
||||
/// </summary>
|
||||
/// <param name="activeInsights">list of active insights</param>
|
||||
/// <return>array of price relatives vector</return>
|
||||
protected virtual double[] GetPriceRelatives(List<Insight> activeInsights)
|
||||
{
|
||||
var numOfInsights = activeInsights.Count;
|
||||
|
||||
// Initialize a price vector of the next prices relatives' projection
|
||||
var nextPriceRelatives = new double[numOfInsights];
|
||||
|
||||
for (int i = 0; i < numOfInsights; i++)
|
||||
{
|
||||
var insight = activeInsights[i];
|
||||
var symbolData = _symbolData[insight.Symbol];
|
||||
|
||||
nextPriceRelatives[i] = insight.Magnitude != null ?
|
||||
1 + (double)insight.Magnitude * (int)insight.Direction:
|
||||
(double)symbolData.Identity.Current.Value / (double)symbolData.Sma.Current.Value;
|
||||
}
|
||||
|
||||
return nextPriceRelatives;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event fired each time the we add/remove securities from the data feed
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
|
||||
/// <param name="changes">The security additions and removals from the algorithm</param>
|
||||
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
|
||||
{
|
||||
base.OnSecuritiesChanged(algorithm, changes);
|
||||
|
||||
// clean up data for removed securities
|
||||
foreach (var removed in changes.RemovedSecurities)
|
||||
{
|
||||
_symbolData.Remove(removed.Symbol, out var symbolData);
|
||||
symbolData.Reset();
|
||||
}
|
||||
|
||||
// initialize data for added securities
|
||||
var symbols = changes.AddedSecurities.Select(x => x.Symbol);
|
||||
|
||||
foreach(var symbol in symbols)
|
||||
{
|
||||
if (!_symbolData.ContainsKey(symbol))
|
||||
{
|
||||
_symbolData.Add(symbol, new MeanReversionSymbolData(algorithm, symbol, _windowSize, _resolution));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cumulative Sum of a given sequence
|
||||
/// </summary>
|
||||
/// <param name="sequence">sequence to obtain cumulative sum</param>
|
||||
/// <return>cumulative sum</return>
|
||||
public static IEnumerable<double> CumulativeSum(IEnumerable<double> sequence)
|
||||
{
|
||||
double sum = 0;
|
||||
foreach(var item in sequence)
|
||||
{
|
||||
sum += item;
|
||||
yield return sum;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalize the updated portfolio into weight vector:
|
||||
/// v_{t+1} = arg min || v - v_{t+1} || ^ 2
|
||||
/// </summary>
|
||||
/// <remark>Duchi, J., Shalev-Shwartz, S., Singer, Y., and Chandra, T. (2008, July).
|
||||
/// Efficient projections onto the l1-ball for learning in high dimensions.
|
||||
/// In Proceedings of the 25th international conference on Machine learning (pp. 272-279).</remark>
|
||||
/// <param name="vector">unnormalized weight vector</param>
|
||||
/// <param name="total">regulator, default to be 1, making it a probabilistic simplex</param>
|
||||
/// <return>normalized weight vector</return>
|
||||
public static double[] SimplexProjection(IEnumerable<double> vector, double total = 1)
|
||||
{
|
||||
if (total <= 0)
|
||||
{
|
||||
throw new ArgumentException("Total must be > 0 for Euclidean Projection onto the Simplex.");
|
||||
}
|
||||
|
||||
// Sort v into u in descending order
|
||||
var mu = vector.OrderByDescending(x => x).ToArray();
|
||||
var sv = CumulativeSum(mu).ToArray();
|
||||
|
||||
var rho = Enumerable.Range(0, vector.Count()).Where(i => mu[i] > (sv[i] - total) / (i+1)).Last();
|
||||
var theta = (sv[rho] - total) / (rho + 1);
|
||||
var w = vector.Select(x => Math.Max(x - theta, 0d)).ToArray();
|
||||
return w;
|
||||
}
|
||||
|
||||
private class MeanReversionSymbolData
|
||||
{
|
||||
public Identity Identity;
|
||||
public SimpleMovingAverage Sma;
|
||||
|
||||
public MeanReversionSymbolData(QCAlgorithm algo, Symbol symbol, int windowSize, Resolution resolution)
|
||||
{
|
||||
// Indicator of price
|
||||
Identity = algo.Identity(symbol, resolution);
|
||||
// Moving average indicator for mean reversion level
|
||||
Sma = algo.SMA(symbol, windowSize, resolution);
|
||||
|
||||
// Warmup indicator
|
||||
algo.WarmUpIndicator(symbol, Identity, resolution);
|
||||
algo.WarmUpIndicator(symbol, Sma, resolution);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Identity.Reset();
|
||||
Sma.Reset();
|
||||
}
|
||||
|
||||
public bool IsReady()
|
||||
{
|
||||
return (Identity.IsReady & Sma.IsReady);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
# 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.
|
||||
|
||||
from AlgorithmImports import *
|
||||
|
||||
### <summary>
|
||||
### Implementation of On-Line Moving Average Reversion (OLMAR)
|
||||
### </summary>
|
||||
### <remarks>Li, B., Hoi, S. C. (2012). On-line portfolio selection with moving average reversion. arXiv preprint arXiv:1206.4626.
|
||||
### Available at https://arxiv.org/ftp/arxiv/papers/1206/1206.4626.pdf</remarks>
|
||||
### <remarks>Using windowSize = 1 => Passive Aggressive Mean Reversion (PAMR) Portfolio</remarks>
|
||||
class MeanReversionPortfolioConstructionModel(PortfolioConstructionModel):
|
||||
|
||||
def __init__(self,
|
||||
rebalance = Resolution.Daily,
|
||||
portfolioBias = PortfolioBias.LongShort,
|
||||
reversion_threshold = 1,
|
||||
window_size = 20,
|
||||
resolution = Resolution.Daily):
|
||||
"""Initialize the model
|
||||
Args:
|
||||
rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function.
|
||||
If None will be ignored.
|
||||
The function returns the next expected rebalance time for a given algorithm UTC DateTime.
|
||||
The function returns null if unknown, in which case the function will be called again in the
|
||||
next loop. Returning current time will trigger rebalance.
|
||||
portfolioBias: Specifies the bias of the portfolio (Short, Long/Short, Long)
|
||||
reversion_threshold: Reversion threshold
|
||||
window_size: Window size of mean price calculation
|
||||
resolution: The resolution of the history price and rebalancing
|
||||
"""
|
||||
super().__init__()
|
||||
if portfolioBias == PortfolioBias.Short:
|
||||
raise ArgumentException("Long position must be allowed in MeanReversionPortfolioConstructionModel.")
|
||||
|
||||
self.reversion_threshold = reversion_threshold
|
||||
self.window_size = window_size
|
||||
self.resolution = resolution
|
||||
|
||||
self.num_of_assets = 0
|
||||
# Initialize a dictionary to store stock data
|
||||
self.symbol_data = {}
|
||||
|
||||
# If the argument is an instance of Resolution or Timedelta
|
||||
# Redefine rebalancingFunc
|
||||
rebalancingFunc = rebalance
|
||||
if isinstance(rebalance, int):
|
||||
rebalance = Extensions.ToTimeSpan(rebalance)
|
||||
if isinstance(rebalance, timedelta):
|
||||
rebalancingFunc = lambda dt: dt + rebalance
|
||||
if rebalancingFunc:
|
||||
self.SetRebalancingFunc(rebalancingFunc)
|
||||
|
||||
def DetermineTargetPercent(self, activeInsights):
|
||||
"""Will determine the target percent for each insight
|
||||
Args:
|
||||
activeInsights: list of active insights
|
||||
Returns:
|
||||
dictionary of insight and respective target weight
|
||||
"""
|
||||
targets = {}
|
||||
|
||||
# If we have no insights or non-ready just return an empty target list
|
||||
if len(activeInsights) == 0 or not all([self.symbol_data[x.Symbol].IsReady for x in activeInsights]):
|
||||
return targets
|
||||
|
||||
num_of_assets = len(activeInsights)
|
||||
if self.num_of_assets != num_of_assets:
|
||||
self.num_of_assets = num_of_assets
|
||||
# Initialize portfolio weightings vector
|
||||
self.weight_vector = np.ones(num_of_assets) * (1/num_of_assets)
|
||||
|
||||
### Get price relatives vs expected price (SMA)
|
||||
price_relatives = self.GetPriceRelatives(activeInsights) # \tilde{x}_{t+1}
|
||||
|
||||
### Get step size of next portfolio
|
||||
# \bar{x}_{t+1} = 1^T * \tilde{x}_{t+1} / m
|
||||
# \lambda_{t+1} = max( 0, ( b_t * \tilde{x}_{t+1} - \epsilon ) / ||\tilde{x}_{t+1} - \bar{x}_{t+1} * 1|| ^ 2 )
|
||||
next_prediction = price_relatives.mean() # \bar{x}_{t+1}
|
||||
assets_mean_dev = price_relatives - next_prediction
|
||||
second_norm = (np.linalg.norm(assets_mean_dev)) ** 2
|
||||
|
||||
if second_norm == 0.0:
|
||||
step_size = 0
|
||||
else:
|
||||
step_size = (np.dot(self.weight_vector, price_relatives) - self.reversion_threshold) / second_norm
|
||||
step_size = max(0, step_size) # \lambda_{t+1}
|
||||
|
||||
### Get next portfolio weightings
|
||||
# b_{t+1} = b_t - step_size * ( \tilde{x}_{t+1} - \bar{x}_{t+1} * 1 )
|
||||
next_portfolio = self.weight_vector - step_size * assets_mean_dev
|
||||
# Normalize
|
||||
normalized_portfolio_weight_vector = self.SimplexProjection(next_portfolio)
|
||||
# Save normalized result for the next portfolio step
|
||||
self.weight_vector = normalized_portfolio_weight_vector
|
||||
|
||||
# Update portfolio state
|
||||
for i, insight in enumerate(activeInsights):
|
||||
targets[insight] = normalized_portfolio_weight_vector[i]
|
||||
|
||||
return targets
|
||||
|
||||
def GetPriceRelatives(self, activeInsights):
|
||||
"""Get price relatives with reference level of SMA
|
||||
Args:
|
||||
activeInsights: list of active insights
|
||||
Returns:
|
||||
array of price relatives vector
|
||||
"""
|
||||
# Initialize a price vector of the next prices relatives' projection
|
||||
next_price_relatives = np.zeros(len(activeInsights))
|
||||
|
||||
### Get next price relative predictions
|
||||
# Using the previous price to simulate assumption of instant reversion
|
||||
for i, insight in enumerate(activeInsights):
|
||||
symbol_data = self.symbol_data[insight.Symbol]
|
||||
next_price_relatives[i] = 1 + insight.Magnitude * insight.Direction \
|
||||
if insight.Magnitude is not None \
|
||||
else symbol_data.Identity.Current.Value / symbol_data.Sma.Current.Value
|
||||
|
||||
return next_price_relatives
|
||||
|
||||
def OnSecuritiesChanged(self, algorithm, changes):
|
||||
"""Event fired each time the we add/remove securities from the data feed
|
||||
Args:
|
||||
algorithm: The algorithm instance that experienced the change in securities
|
||||
changes: The security additions and removals from the algorithm
|
||||
"""
|
||||
# clean up data for removed securities
|
||||
super().OnSecuritiesChanged(algorithm, changes)
|
||||
for removed in changes.RemovedSecurities:
|
||||
symbol_data = self.symbol_data.pop(removed.Symbol, None)
|
||||
symbol_data.Reset()
|
||||
|
||||
# initialize data for added securities
|
||||
symbols = [ x.Symbol for x in changes.AddedSecurities ]
|
||||
|
||||
for symbol in symbols:
|
||||
if symbol not in self.symbol_data:
|
||||
self.symbol_data[symbol] = self.MeanReversionSymbolData(algorithm, symbol, self.window_size, self.resolution)
|
||||
|
||||
def SimplexProjection(self, vector, total=1):
|
||||
"""Normalize the updated portfolio into weight vector:
|
||||
v_{t+1} = arg min || v - v_{t+1} || ^ 2
|
||||
Implementation from:
|
||||
Duchi, J., Shalev-Shwartz, S., Singer, Y., & Chandra, T. (2008, July).
|
||||
Efficient projections onto the l 1-ball for learning in high dimensions.
|
||||
In Proceedings of the 25th international conference on Machine learning
|
||||
(pp. 272-279).
|
||||
Args:
|
||||
vector: unnormalized weight vector
|
||||
total: total weight of output, default to be 1, making it a probabilistic simplex
|
||||
"""
|
||||
if total <= 0:
|
||||
raise ArgumentException("Total must be > 0 for Euclidean Projection onto the Simplex.")
|
||||
|
||||
vector = np.asarray(vector)
|
||||
|
||||
# Sort v into u in descending order
|
||||
mu = np.sort(vector)[::-1]
|
||||
sv = np.cumsum(mu)
|
||||
|
||||
rho = np.where(mu > (sv - total) / np.arange(1, len(vector) + 1))[0][-1]
|
||||
theta = (sv[rho] - total) / (rho + 1)
|
||||
w = (vector - theta)
|
||||
w[w < 0] = 0
|
||||
return w
|
||||
|
||||
class MeanReversionSymbolData:
|
||||
def __init__(self, algo, symbol, window_size, resolution):
|
||||
# Indicator of price
|
||||
self.Identity = algo.Identity(symbol, resolution)
|
||||
# Moving average indicator for mean reversion level
|
||||
self.Sma = algo.SMA(symbol, window_size, resolution)
|
||||
|
||||
# Warmup indicator
|
||||
algo.WarmUpIndicator(symbol, self.Identity, resolution)
|
||||
algo.WarmUpIndicator(symbol, self.Sma, resolution)
|
||||
|
||||
def Reset(self):
|
||||
self.Identity.Reset()
|
||||
self.Sma.Reset()
|
||||
|
||||
@property
|
||||
def IsReady(self):
|
||||
return self.Identity.IsReady and self.Sma.IsReady
|
||||
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* 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 Accord.Math;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Algorithm.Framework.Alphas;
|
||||
using QuantConnect.Data;
|
||||
using QuantConnect.Data.UniverseSelection;
|
||||
using QuantConnect.Scheduling;
|
||||
using QuantConnect.Util;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an implementation of Mean-Variance portfolio optimization based on modern portfolio theory.
|
||||
/// The interval of weights in optimization method can be changed based on the long-short algorithm.
|
||||
/// The default model uses the last three months daily price to calculate the optimal weight
|
||||
/// with the weight range from -1 to 1 and minimize the portfolio variance with a target return of 2%
|
||||
/// </summary>
|
||||
public class MeanVarianceOptimizationPortfolioConstructionModel : PortfolioConstructionModel
|
||||
{
|
||||
private readonly int _lookback;
|
||||
private readonly int _period;
|
||||
private readonly Resolution _resolution;
|
||||
private readonly PortfolioBias _portfolioBias;
|
||||
private readonly IPortfolioOptimizer _optimizer;
|
||||
private readonly Dictionary<Symbol, ReturnsSymbolData> _symbolDataDict;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="rebalancingDateRules">The date rules used to define the next expected rebalance time
|
||||
/// in UTC</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="targetReturn">The target portfolio return</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
|
||||
public MeanVarianceOptimizationPortfolioConstructionModel(IDateRule rebalancingDateRules,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 63,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
double targetReturn = 0.02,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: this(rebalancingDateRules.ToFunc(), portfolioBias, lookback, period, resolution, targetReturn, optimizer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="rebalanceResolution">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="targetReturn">The target portfolio return</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
|
||||
public MeanVarianceOptimizationPortfolioConstructionModel(Resolution rebalanceResolution = Resolution.Daily,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 63,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
double targetReturn = 0.02,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: this(rebalanceResolution.ToTimeSpan(), portfolioBias, lookback, period, resolution, targetReturn, optimizer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="targetReturn">The target portfolio return</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
|
||||
public MeanVarianceOptimizationPortfolioConstructionModel(TimeSpan timeSpan,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 63,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
double targetReturn = 0.02,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: this(dt => dt.Add(timeSpan), portfolioBias, lookback, period, resolution, targetReturn, optimizer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="rebalance">Rebalancing func or if a date rule, timedelta will be converted into func.
|
||||
/// For a given algorithm UTC DateTime the func returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="targetReturn">The target portfolio return</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
|
||||
/// <remarks>This is required since python net can not convert python methods into func nor resolve the correct
|
||||
/// constructor for the date rules parameter.
|
||||
/// For performance we prefer python algorithms using the C# implementation</remarks>
|
||||
public MeanVarianceOptimizationPortfolioConstructionModel(PyObject rebalance,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 63,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
double targetReturn = 0.02,
|
||||
PyObject optimizer = null)
|
||||
: this((Func<DateTime, DateTime?>)null, portfolioBias, lookback, period, resolution, targetReturn, null)
|
||||
{
|
||||
SetRebalancingFunc(rebalance);
|
||||
|
||||
if (optimizer != null)
|
||||
{
|
||||
if (optimizer.TryConvert<IPortfolioOptimizer>(out var csharpOptimizer))
|
||||
{
|
||||
_optimizer = csharpOptimizer;
|
||||
}
|
||||
else
|
||||
{
|
||||
_optimizer = new PortfolioOptimizerPythonWrapper(optimizer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance UTC time.
|
||||
/// Returning current time will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="targetReturn">The target portfolio return</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
|
||||
public MeanVarianceOptimizationPortfolioConstructionModel(Func<DateTime, DateTime> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 63,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
double targetReturn = 0.02,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: this(rebalancingFunc != null ? (Func<DateTime, DateTime?>)(timeUtc => rebalancingFunc(timeUtc)) : null,
|
||||
portfolioBias,
|
||||
lookback,
|
||||
period,
|
||||
resolution,
|
||||
targetReturn,
|
||||
optimizer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance.</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="targetReturn">The target portfolio return</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
|
||||
public MeanVarianceOptimizationPortfolioConstructionModel(Func<DateTime, DateTime?> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 63,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
double targetReturn = 0.02,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: base(rebalancingFunc)
|
||||
{
|
||||
_lookback = lookback;
|
||||
_period = period;
|
||||
_resolution = resolution;
|
||||
_portfolioBias = portfolioBias;
|
||||
|
||||
var lower = portfolioBias == PortfolioBias.Long ? 0 : -1;
|
||||
var upper = portfolioBias == PortfolioBias.Short ? 0 : 1;
|
||||
_optimizer = optimizer ?? new MinimumVariancePortfolioOptimizer(lower, upper, targetReturn);
|
||||
|
||||
_symbolDataDict = new Dictionary<Symbol, ReturnsSymbolData>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method that will determine if the portfolio construction model should create a
|
||||
/// target for this insight
|
||||
/// </summary>
|
||||
/// <param name="insight">The insight to create a target for</param>
|
||||
/// <returns>True if the portfolio should create a target for the insight</returns>
|
||||
protected override bool ShouldCreateTargetForInsight(Insight insight)
|
||||
{
|
||||
var filteredInsight = FilterInvalidInsightMagnitude(Algorithm, new[] { insight }).FirstOrDefault();
|
||||
if (filteredInsight == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ReturnsSymbolData data;
|
||||
if (_symbolDataDict.TryGetValue(insight.Symbol, out data))
|
||||
{
|
||||
if (!insight.Magnitude.HasValue)
|
||||
{
|
||||
Algorithm.SetRunTimeError(
|
||||
new ArgumentNullException(
|
||||
insight.Symbol.Value,
|
||||
"MeanVarianceOptimizationPortfolioConstructionModel does not accept 'null' as Insight.Magnitude. " +
|
||||
"Please checkout the selected Alpha Model specifications: " + insight.SourceModel));
|
||||
return false;
|
||||
}
|
||||
data.Add(Algorithm.Time, insight.Magnitude.Value.SafeDecimalCast());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will determine the target percent for each insight
|
||||
/// </summary>
|
||||
/// <param name="activeInsights">The active insights to generate a target for</param>
|
||||
/// <returns>A target percent for each insight</returns>
|
||||
protected override Dictionary<Insight, double> DetermineTargetPercent(List<Insight> activeInsights)
|
||||
{
|
||||
var targets = new Dictionary<Insight, double>();
|
||||
|
||||
// If we have no insights just return an empty target list
|
||||
if (activeInsights.IsNullOrEmpty())
|
||||
{
|
||||
return targets;
|
||||
}
|
||||
|
||||
var symbols = activeInsights.Select(x => x.Symbol).ToList();
|
||||
|
||||
// Get symbols' returns, we use simple return according to
|
||||
// Meucci, Attilio, Quant Nugget 2: Linear vs. Compounded Returns – Common Pitfalls in Portfolio Management (May 1, 2010).
|
||||
// GARP Risk Professional, pp. 49-51, April 2010 , Available at SSRN: https://ssrn.com/abstract=1586656
|
||||
var returns = _symbolDataDict.FormReturnsMatrix(symbols);
|
||||
|
||||
// The optimization method processes the data frame
|
||||
var w = _optimizer.Optimize(returns);
|
||||
|
||||
// process results
|
||||
if (w.Length > 0)
|
||||
{
|
||||
var sidx = 0;
|
||||
foreach (var symbol in symbols)
|
||||
{
|
||||
var weight = w[sidx];
|
||||
|
||||
// don't trust the optimizer
|
||||
if (_portfolioBias != PortfolioBias.LongShort
|
||||
&& Math.Sign(weight) != (int)_portfolioBias)
|
||||
{
|
||||
weight = 0;
|
||||
}
|
||||
|
||||
targets[activeInsights.First(insight => insight.Symbol == symbol)] = weight;
|
||||
|
||||
sidx++;
|
||||
}
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event fired each time the we add/remove securities from the data feed
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
|
||||
/// <param name="changes">The security additions and removals from the algorithm</param>
|
||||
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
|
||||
{
|
||||
base.OnSecuritiesChanged(algorithm, changes);
|
||||
// clean up data for removed securities
|
||||
foreach (var removed in changes.RemovedSecurities)
|
||||
{
|
||||
ReturnsSymbolData data;
|
||||
if (_symbolDataDict.TryGetValue(removed.Symbol, out data))
|
||||
{
|
||||
_symbolDataDict.Remove(removed.Symbol);
|
||||
}
|
||||
}
|
||||
|
||||
if (changes.AddedSecurities.Count == 0)
|
||||
return;
|
||||
|
||||
// initialize data for added securities
|
||||
foreach (var added in changes.AddedSecurities)
|
||||
{
|
||||
if (!_symbolDataDict.ContainsKey(added.Symbol))
|
||||
{
|
||||
var symbolData = new ReturnsSymbolData(added.Symbol, _lookback, _period);
|
||||
_symbolDataDict[added.Symbol] = symbolData;
|
||||
}
|
||||
}
|
||||
|
||||
// warmup our indicators by pushing history through the consolidators
|
||||
algorithm.History(changes.AddedSecurities.Select(security => security.Symbol), _lookback * _period, _resolution)
|
||||
.PushThrough(bar =>
|
||||
{
|
||||
ReturnsSymbolData symbolData;
|
||||
if (_symbolDataDict.TryGetValue(bar.Symbol, out symbolData))
|
||||
{
|
||||
symbolData.Update(bar.EndTime, bar.Value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
# 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.
|
||||
|
||||
from AlgorithmImports import *
|
||||
from Portfolio.MinimumVariancePortfolioOptimizer import MinimumVariancePortfolioOptimizer
|
||||
|
||||
### <summary>
|
||||
### Provides an implementation of Mean-Variance portfolio optimization based on modern portfolio theory.
|
||||
### The default model uses the MinimumVariancePortfolioOptimizer that accepts a 63-row matrix of 1-day returns.
|
||||
### </summary>
|
||||
class MeanVarianceOptimizationPortfolioConstructionModel(PortfolioConstructionModel):
|
||||
def __init__(self,
|
||||
rebalance = Resolution.DAILY,
|
||||
portfolio_bias = PortfolioBias.LONG_SHORT,
|
||||
lookback = 1,
|
||||
period = 63,
|
||||
resolution = Resolution.DAILY,
|
||||
target_return = 0.02,
|
||||
optimizer = None):
|
||||
"""Initialize the model
|
||||
Args:
|
||||
rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function.
|
||||
If None will be ignored.
|
||||
The function returns the next expected rebalance time for a given algorithm UTC DateTime.
|
||||
The function returns null if unknown, in which case the function will be called again in the
|
||||
next loop. Returning current time will trigger rebalance.
|
||||
portfolio_bias: Specifies the bias of the portfolio (Short, Long/Short, Long)
|
||||
lookback(int): Historical return lookback period
|
||||
period(int): The time interval of history price to calculate the weight
|
||||
resolution: The resolution of the history price
|
||||
optimizer(class): Method used to compute the portfolio weights"""
|
||||
super().__init__()
|
||||
self.lookback = lookback
|
||||
self.period = period
|
||||
self.resolution = resolution
|
||||
self.portfolio_bias = portfolio_bias
|
||||
self.sign = lambda x: -1 if x < 0 else (1 if x > 0 else 0)
|
||||
|
||||
lower = 0 if portfolio_bias == PortfolioBias.LONG else -1
|
||||
upper = 0 if portfolio_bias == PortfolioBias.SHORT else 1
|
||||
self.optimizer = MinimumVariancePortfolioOptimizer(lower, upper, target_return) if optimizer is None else optimizer
|
||||
|
||||
self.symbol_data_by_symbol = {}
|
||||
|
||||
# If the argument is an instance of Resolution or Timedelta
|
||||
# Redefine rebalancing_func
|
||||
rebalancing_func = rebalance
|
||||
if isinstance(rebalance, Resolution):
|
||||
rebalance = Extensions.to_time_span(rebalance)
|
||||
if isinstance(rebalance, timedelta):
|
||||
rebalancing_func = lambda dt: dt + rebalance
|
||||
if rebalancing_func:
|
||||
self.set_rebalancing_func(rebalancing_func)
|
||||
|
||||
def should_create_target_for_insight(self, insight):
|
||||
if len(PortfolioConstructionModel.filter_invalid_insight_magnitude(self.algorithm, [insight])) == 0:
|
||||
return False
|
||||
|
||||
symbol_data = self.symbol_data_by_symbol.get(insight.symbol)
|
||||
if insight.magnitude is None:
|
||||
self.algorithm.set_run_time_error(ArgumentNullException('MeanVarianceOptimizationPortfolioConstructionModel does not accept \'None\' as Insight.magnitude. Please checkout the selected Alpha Model specifications.'))
|
||||
return False
|
||||
symbol_data.add(self.algorithm.time, insight.magnitude)
|
||||
|
||||
return True
|
||||
|
||||
def determine_target_percent(self, active_insights):
|
||||
"""
|
||||
Will determine the target percent for each insight
|
||||
Args:
|
||||
Returns:
|
||||
"""
|
||||
targets = {}
|
||||
|
||||
# If we have no insights just return an empty target list
|
||||
if len(active_insights) == 0:
|
||||
return targets
|
||||
|
||||
symbols = [insight.symbol for insight in active_insights]
|
||||
|
||||
# Create a dictionary keyed by the symbols in the insights with an pandas.series as value to create a data frame
|
||||
returns = { str(symbol.id) : data.return_ for symbol, data in self.symbol_data_by_symbol.items() if symbol in symbols }
|
||||
returns = pd.DataFrame(returns)
|
||||
|
||||
# The portfolio optimizer finds the optional weights for the given data
|
||||
weights = self.optimizer.optimize(returns)
|
||||
weights = pd.Series(weights, index = returns.columns)
|
||||
|
||||
# Create portfolio targets from the specified insights
|
||||
for insight in active_insights:
|
||||
weight = weights[str(insight.symbol.id)]
|
||||
|
||||
# don't trust the optimizer
|
||||
if self.portfolio_bias != PortfolioBias.LONG_SHORT and self.sign(weight) != self.portfolio_bias:
|
||||
weight = 0
|
||||
targets[insight] = weight
|
||||
|
||||
return targets
|
||||
|
||||
def on_securities_changed(self, algorithm, changes):
|
||||
'''Event fired each time the we add/remove securities from the data feed
|
||||
Args:
|
||||
algorithm: The algorithm instance that experienced the change in securities
|
||||
changes: The security additions and removals from the algorithm'''
|
||||
|
||||
# clean up data for removed securities
|
||||
super().on_securities_changed(algorithm, changes)
|
||||
for removed in changes.removed_securities:
|
||||
symbol_data = self.symbol_data_by_symbol.pop(removed.symbol, None)
|
||||
symbol_data.reset()
|
||||
|
||||
# initialize data for added securities
|
||||
symbols = [x.symbol for x in changes.added_securities]
|
||||
for symbol in [x for x in symbols if x not in self.symbol_data_by_symbol]:
|
||||
self.symbol_data_by_symbol[symbol] = self.MeanVarianceSymbolData(symbol, self.lookback, self.period)
|
||||
|
||||
history = algorithm.history[TradeBar](symbols, self.lookback * self.period, self.resolution)
|
||||
for bars in history:
|
||||
for symbol, bar in bars.items():
|
||||
symbol_data = self.symbol_data_by_symbol.get(symbol).update(bar.end_time, bar.value)
|
||||
|
||||
class MeanVarianceSymbolData:
|
||||
'''Contains data specific to a symbol required by this model'''
|
||||
def __init__(self, symbol, lookback, period):
|
||||
self._symbol = symbol
|
||||
self.roc = RateOfChange(f'{symbol}.roc({lookback})', lookback)
|
||||
self.roc.updated += self.on_rate_of_change_updated
|
||||
self.window = RollingWindow(period)
|
||||
|
||||
def reset(self):
|
||||
self.roc.updated -= self.on_rate_of_change_updated
|
||||
self.roc.reset()
|
||||
self.window.reset()
|
||||
|
||||
def update(self, time, value):
|
||||
return self.roc.update(time, value)
|
||||
|
||||
def on_rate_of_change_updated(self, roc, value):
|
||||
if roc.is_ready:
|
||||
self.window.add(value)
|
||||
|
||||
def add(self, time, value):
|
||||
item = IndicatorDataPoint(self._symbol, time, value)
|
||||
self.window.add(item)
|
||||
|
||||
# Get symbols' returns, we use simple return according to
|
||||
# Meucci, Attilio, Quant Nugget 2: Linear vs. Compounded Returns – Common Pitfalls in Portfolio Management (May 1, 2010).
|
||||
# GARP Risk Professional, pp. 49-51, April 2010 , Available at SSRN: https://ssrn.com/abstract=1586656
|
||||
@property
|
||||
def return_(self):
|
||||
return pd.Series(
|
||||
data = [x.value for x in self.window],
|
||||
index = [x.end_time for x in self.window])
|
||||
|
||||
@property
|
||||
def is_ready(self):
|
||||
return self.window.is_ready
|
||||
|
||||
def __str__(self, **kwargs):
|
||||
return '{}: {:.2%}'.format(self.roc.name, self.window[0])
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
|
||||
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Accord.Math;
|
||||
using Accord.Math.Optimization;
|
||||
using Accord.Statistics;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an implementation of a minimum variance portfolio optimizer that calculate the optimal weights
|
||||
/// with the weight range from -1 to 1 and minimize the portfolio variance with a target return of 2%
|
||||
/// </summary>
|
||||
/// <remarks>The budged constrain is scaled down/up to ensure that the sum of the absolute value of the weights is 1.</remarks>
|
||||
public class MinimumVariancePortfolioOptimizer : IPortfolioOptimizer
|
||||
{
|
||||
private double _lower;
|
||||
private double _upper;
|
||||
private double _targetReturn;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="MinimumVariancePortfolioOptimizer"/>
|
||||
/// </summary>
|
||||
/// <param name="lower">Lower bound</param>
|
||||
/// <param name="upper">Upper bound</param>
|
||||
/// <param name="targetReturn">Target return</param>
|
||||
public MinimumVariancePortfolioOptimizer(double lower = -1, double upper = 1, double targetReturn = 0.02)
|
||||
{
|
||||
_lower = lower;
|
||||
_upper = upper;
|
||||
_targetReturn = targetReturn;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sum of all weight is one: 1^T w = 1 / Σw = 1
|
||||
/// </summary>
|
||||
/// <param name="size">number of variables</param>
|
||||
/// <returns>linear constaraint object</returns>
|
||||
protected LinearConstraint GetBudgetConstraint(int size)
|
||||
{
|
||||
return new LinearConstraint(size)
|
||||
{
|
||||
CombinedAs = Vector.Create(size, 1.0),
|
||||
ShouldBe = ConstraintType.EqualTo,
|
||||
Value = 1.0
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Boundary constraints on weights: lw ≤ w ≤ up
|
||||
/// </summary>
|
||||
/// <param name="size">number of variables</param>
|
||||
/// <returns>enumeration of linear constaraint objects</returns>
|
||||
protected IEnumerable<LinearConstraint> GetBoundaryConditions(int size)
|
||||
{
|
||||
for (var i = 0; i < size; i++)
|
||||
{
|
||||
yield return new LinearConstraint(1)
|
||||
{
|
||||
VariablesAtIndices = new[] { i },
|
||||
ShouldBe = ConstraintType.GreaterThanOrEqualTo,
|
||||
Value = _lower
|
||||
};
|
||||
yield return new LinearConstraint(1)
|
||||
{
|
||||
VariablesAtIndices = new[] { i },
|
||||
ShouldBe = ConstraintType.LesserThanOrEqualTo,
|
||||
Value = _upper
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform portfolio optimization for a provided matrix of historical returns and an array of expected returns
|
||||
/// </summary>
|
||||
/// <param name="historicalReturns">Matrix of annualized historical returns where each column represents a security and each row returns for the given date/time (size: K x N).</param>
|
||||
/// <param name="expectedReturns">Array of double with the portfolio annualized expected returns (size: K x 1).</param>
|
||||
/// <param name="covariance">Multi-dimensional array of double with the portfolio covariance of annualized returns (size: K x K).</param>
|
||||
/// <returns>Array of double with the portfolio weights (size: K x 1)</returns>
|
||||
public double[] Optimize(double[,] historicalReturns, double[] expectedReturns = null, double[,] covariance = null)
|
||||
{
|
||||
covariance ??= historicalReturns.Covariance();
|
||||
var size = covariance.GetLength(0);
|
||||
var returns = expectedReturns ?? historicalReturns.Mean(0);
|
||||
|
||||
var constraints = new List<LinearConstraint>
|
||||
{
|
||||
// w^T µ ≥ β
|
||||
new (size)
|
||||
{
|
||||
CombinedAs = returns,
|
||||
ShouldBe = ConstraintType.EqualTo,
|
||||
Value = _targetReturn
|
||||
},
|
||||
// Σw = 1
|
||||
GetBudgetConstraint(size),
|
||||
};
|
||||
|
||||
// lw ≤ w ≤ up
|
||||
constraints.AddRange(GetBoundaryConditions(size));
|
||||
|
||||
// Setup solver
|
||||
var optfunc = new QuadraticObjectiveFunction(covariance, Vector.Create(size, 0.0));
|
||||
var solver = new GoldfarbIdnani(optfunc, constraints);
|
||||
|
||||
// Solve problem
|
||||
var x0 = Vector.Create(size, 1.0 / size);
|
||||
var success = solver.Minimize(Vector.Copy(x0));
|
||||
if (!success) return x0;
|
||||
|
||||
// We cannot accept NaN
|
||||
var solution = solver.Solution
|
||||
.Select(x => x.IsNaNOrInfinity() ? 0 : x).ToArray();
|
||||
|
||||
// Scale the solution to ensure that the sum of the absolute weights is 1
|
||||
var sumOfAbsoluteWeights = solution.Abs().Sum();
|
||||
if (sumOfAbsoluteWeights.IsNaNOrZero()) return x0;
|
||||
|
||||
return solution.Divide(sumOfAbsoluteWeights);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# 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.
|
||||
|
||||
from AlgorithmImports import *
|
||||
from scipy.optimize import minimize
|
||||
|
||||
### <summary>
|
||||
### Provides an implementation of a portfolio optimizer that calculate the optimal weights
|
||||
### with the weight range from -1 to 1 and minimize the portfolio variance with a target return of 2%
|
||||
### </summary>
|
||||
### <remarks>The budged constrain is scaled down/up to ensure that the sum of the absolute value of the weights is 1.</remarks>
|
||||
class MinimumVariancePortfolioOptimizer:
|
||||
'''Provides an implementation of a portfolio optimizer that calculate the optimal weights
|
||||
with the weight range from -1 to 1 and minimize the portfolio variance with a target return of 2%'''
|
||||
def __init__(self,
|
||||
minimum_weight = -1,
|
||||
maximum_weight = 1,
|
||||
target_return = 0.02):
|
||||
'''Initialize the MinimumVariancePortfolioOptimizer
|
||||
Args:
|
||||
minimum_weight(float): The lower bounds on portfolio weights
|
||||
maximum_weight(float): The upper bounds on portfolio weights
|
||||
target_return(float): The target portfolio return'''
|
||||
self.minimum_weight = minimum_weight
|
||||
self.maximum_weight = maximum_weight
|
||||
self.target_return = target_return
|
||||
|
||||
def optimize(self, historical_returns, expected_returns = None, covariance = None):
|
||||
'''
|
||||
Perform portfolio optimization for a provided matrix of historical returns and an array of expected returns
|
||||
args:
|
||||
historical_returns: Matrix of annualized historical returns where each column represents a security and each row returns for the given date/time (size: K x N).
|
||||
expected_returns: Array of double with the portfolio annualized expected returns (size: K x 1).
|
||||
covariance: Multi-dimensional array of double with the portfolio covariance of annualized returns (size: K x K).
|
||||
Returns:
|
||||
Array of double with the portfolio weights (size: K x 1)
|
||||
'''
|
||||
if covariance is None:
|
||||
covariance = historical_returns.cov()
|
||||
if expected_returns is None:
|
||||
expected_returns = historical_returns.mean()
|
||||
|
||||
size = historical_returns.columns.size # K x 1
|
||||
x0 = np.array(size * [1. / size])
|
||||
|
||||
constraints = [
|
||||
{'type': 'eq', 'fun': lambda weights: self.get_budget_constraint(weights)},
|
||||
{'type': 'eq', 'fun': lambda weights: self.get_target_constraint(weights, expected_returns)}]
|
||||
|
||||
# https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html
|
||||
opt = minimize(lambda weights: self.portfolio_variance(weights, covariance), # Objective function
|
||||
x0, # Initial guess
|
||||
bounds = self.get_boundary_conditions(size), # Bounds for variables
|
||||
constraints = constraints, # Constraints definition
|
||||
method='SLSQP') # Optimization method: Sequential Least Squares Programming (SLSQP)
|
||||
|
||||
if not opt['success']: return x0
|
||||
|
||||
# Scale the solution to ensure that the sum of the absolute weights is 1
|
||||
sum_of_absolute_weights = np.sum(np.abs(opt['x']))
|
||||
return opt['x'] / sum_of_absolute_weights
|
||||
|
||||
def portfolio_variance(self, weights, covariance):
|
||||
'''Computes the portfolio variance
|
||||
Args:
|
||||
weighs: Portfolio weights
|
||||
covariance: Covariance matrix of historical returns'''
|
||||
variance = np.dot(weights.T, np.dot(covariance, weights))
|
||||
if variance == 0 and np.any(weights):
|
||||
# variance can't be zero, with non zero weights
|
||||
raise ValueError(f'MinimumVariancePortfolioOptimizer.portfolio_variance: Volatility cannot be zero. Weights: {weights}')
|
||||
return variance
|
||||
|
||||
def get_boundary_conditions(self, size):
|
||||
'''Creates the boundary condition for the portfolio weights'''
|
||||
return tuple((self.minimum_weight, self.maximum_weight) for x in range(size))
|
||||
|
||||
def get_budget_constraint(self, weights):
|
||||
'''Defines a budget constraint: the sum of the weights equals unity'''
|
||||
return np.sum(weights) - 1
|
||||
|
||||
def get_target_constraint(self, weights, expected_returns):
|
||||
'''Ensure that the portfolio return target a given return'''
|
||||
return np.dot(np.matrix(expected_returns), np.matrix(weights).T).item() - self.target_return
|
||||
@@ -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 Python.Runtime;
|
||||
using QuantConnect.Python;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Python wrapper for custom portfolio optimizer
|
||||
/// </summary>
|
||||
public class PortfolioOptimizerPythonWrapper : BasePythonWrapper<IPortfolioOptimizer>, IPortfolioOptimizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance
|
||||
/// </summary>
|
||||
/// <param name="portfolioOptimizer">The python model to wrapp</param>
|
||||
public PortfolioOptimizerPythonWrapper(PyObject portfolioOptimizer)
|
||||
: base(portfolioOptimizer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform portfolio optimization for a provided matrix of historical returns and an array of expected returns
|
||||
/// </summary>
|
||||
/// <param name="historicalReturns">Matrix of annualized historical returns where each column represents a security and each row returns for the given date/time (size: K x N).</param>
|
||||
/// <param name="expectedReturns">Array of double with the portfolio annualized expected returns (size: K x 1).</param>
|
||||
/// <param name="covariance">Multi-dimensional array of double with the portfolio covariance of annualized returns (size: K x K).</param>
|
||||
/// <returns>Array of double with the portfolio weights (size: K x 1)</returns>
|
||||
public double[] Optimize(double[,] historicalReturns, double[] expectedReturns = null, double[,] covariance = null)
|
||||
{
|
||||
return InvokeMethod<double[]>(nameof(Optimize), historicalReturns, expectedReturns, covariance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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 QuantConnect.Indicators;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains returns specific to a symbol required for optimization model
|
||||
/// </summary>
|
||||
public class ReturnsSymbolData
|
||||
{
|
||||
private readonly Symbol _symbol;
|
||||
private readonly RateOfChange _roc;
|
||||
private readonly RollingWindow<IndicatorDataPoint> _window;
|
||||
|
||||
/// <summary>
|
||||
/// The symbol's asset rate of change indicator
|
||||
/// </summary>
|
||||
public RateOfChange ROC { get { return _roc; } }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ReturnsSymbolData"/> class
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol of the data that updates the indicators</param>
|
||||
/// <param name="lookback">Look-back period for the RateOfChange indicator</param>
|
||||
/// <param name="period">Size of rolling window that contains historical RateOfChange</param>
|
||||
public ReturnsSymbolData(Symbol symbol, int lookback, int period)
|
||||
{
|
||||
_symbol = symbol;
|
||||
_roc = new RateOfChange($"{_symbol}.ROC({lookback})", lookback);
|
||||
_window = new RollingWindow<IndicatorDataPoint>(period);
|
||||
_roc.Updated += OnRateOfChangeUpdated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Historical returns
|
||||
/// </summary>
|
||||
public Dictionary<DateTime, double> Returns => _window.ToDictionary(x => x.EndTime, x => (double) x.Value);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an item to this window and shifts all other elements
|
||||
/// </summary>
|
||||
/// <param name="time">The time associated with the value</param>
|
||||
/// <param name="value">The value to use to update this window</param>
|
||||
public void Add(DateTime time, decimal value)
|
||||
{
|
||||
var item = new IndicatorDataPoint(_symbol, time, value);
|
||||
AddToWindow(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the state of the RateOfChange with the given value and returns true
|
||||
/// if this indicator is ready, false otherwise
|
||||
/// </summary>
|
||||
/// <param name="time">The time associated with the value</param>
|
||||
/// <param name="value">The value to use to update this indicator</param>
|
||||
/// <returns>True if this indicator is ready, false otherwise</returns>
|
||||
public bool Update(DateTime time, decimal value)
|
||||
{
|
||||
return _roc.Update(time, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets all indicators of this object to its initial state
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_roc.Updated -= OnRateOfChangeUpdated;
|
||||
_roc.Reset();
|
||||
_window.Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When the RateOfChange is updated, adds the new value to the RollingWindow
|
||||
/// </summary>
|
||||
/// <param name="roc"></param>
|
||||
/// <param name="updated"></param>
|
||||
private void OnRateOfChangeUpdated(object roc, IndicatorDataPoint updated)
|
||||
{
|
||||
if (_roc.IsReady)
|
||||
{
|
||||
AddToWindow(updated);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddToWindow(IndicatorDataPoint updated)
|
||||
{
|
||||
if (_window.Samples > 0 && _window[0].EndTime == updated.EndTime)
|
||||
{
|
||||
// this could happen with fill forward bars in the history request
|
||||
return;
|
||||
}
|
||||
|
||||
_window.Add(updated);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="ReturnsSymbolData"/>
|
||||
/// </summary>
|
||||
public static class ReturnsSymbolDataExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a dictionary of <see cref="ReturnsSymbolData"/> keyed by <see cref="Symbol"/> into a matrix
|
||||
/// </summary>
|
||||
/// <param name="symbolData">Dictionary of <see cref="ReturnsSymbolData"/> keyed by <see cref="Symbol"/> to be converted into a matrix</param>
|
||||
/// <param name="symbols">List of <see cref="Symbol"/> to be included in the matrix</param>
|
||||
public static double[,] FormReturnsMatrix(this Dictionary<Symbol, ReturnsSymbolData> symbolData, IEnumerable<Symbol> symbols)
|
||||
{
|
||||
var returnsByDate = (from s in symbols join sd in symbolData on s equals sd.Key select sd.Value.Returns).ToList();
|
||||
|
||||
// Consolidate by date
|
||||
var alldates = returnsByDate.SelectMany(r => r.Keys).Distinct().ToList();
|
||||
|
||||
var max = symbolData.Count == 0 ? 0 : symbolData.Max(kvp => kvp.Value.Returns.Count);
|
||||
|
||||
// Perfect match between the dates in the ReturnsSymbolData objects
|
||||
if (max == alldates.Count)
|
||||
{
|
||||
return Accord.Math.Matrix.Create(alldates
|
||||
// if a return date isn't found for a symbol we use 'double.NaN'
|
||||
.Select(d => returnsByDate.Select(s => s.GetValueOrDefault(d, double.NaN)).ToArray())
|
||||
.Where(r => !r.Select(Math.Abs).Sum().IsNaNOrZero()) // remove empty rows
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
// If it is not a match, we assume that each index correspond to the same point in time
|
||||
var returnsByIndex = returnsByDate.Select((doubles, i) => doubles.Values.ToArray());
|
||||
|
||||
return Accord.Math.Matrix.Create(Enumerable.Range(0, max)
|
||||
// there is no guarantee that all symbols have the same amount of returns so we need to check range and use 'double.NaN' if required as above
|
||||
.Select(d => returnsByIndex.Select(s => s.Length < (d + 1) ? double.NaN : s[d]).ToArray())
|
||||
.Where(r => !r.Select(Math.Abs).Sum().IsNaNOrZero()) // remove empty rows
|
||||
.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
|
||||
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Accord.Math;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Algorithm.Framework.Alphas;
|
||||
using QuantConnect.Data;
|
||||
using QuantConnect.Data.UniverseSelection;
|
||||
using QuantConnect.Scheduling;
|
||||
using QuantConnect.Util;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Risk Parity Portfolio Construction Model
|
||||
/// </summary>
|
||||
/// <remarks>Spinu, F. (2013). An algorithm for computing risk parity weights. Available at SSRN 2297383.
|
||||
/// Available at https://papers.ssrn.com/sol3/Papers.cfm?abstract_id=2297383</remarks>
|
||||
public class RiskParityPortfolioConstructionModel : PortfolioConstructionModel
|
||||
{
|
||||
private readonly int _lookback;
|
||||
private readonly int _period;
|
||||
private readonly Resolution _resolution;
|
||||
private readonly IPortfolioOptimizer _optimizer;
|
||||
private readonly Dictionary<Symbol, ReturnsSymbolData> _symbolDataDict;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="rebalancingDateRules">The date rules used to define the next expected rebalance time in UTC</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
|
||||
public RiskParityPortfolioConstructionModel(IDateRule rebalancingDateRules,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 252,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: this(rebalancingDateRules.ToFunc(), portfolioBias, lookback, period, resolution, optimizer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="rebalanceResolution">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
|
||||
public RiskParityPortfolioConstructionModel(Resolution rebalanceResolution = Resolution.Daily,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 252,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: this(rebalanceResolution.ToTimeSpan(), portfolioBias, lookback, period, resolution, optimizer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">Rebalancing frequency</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
|
||||
public RiskParityPortfolioConstructionModel(TimeSpan timeSpan,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 252,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: this(dt => dt.Add(timeSpan), portfolioBias, lookback, period, resolution, optimizer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="rebalance">Rebalancing func or if a date rule, timedelta will be converted into func.
|
||||
/// For a given algorithm UTC DateTime the func returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
|
||||
/// <remarks>This is required since python net can not convert python methods into func nor resolve the correct
|
||||
/// constructor for the date rules parameter.
|
||||
/// For performance we prefer python algorithms using the C# implementation</remarks>
|
||||
public RiskParityPortfolioConstructionModel(PyObject rebalance,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 252,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: this((Func<DateTime, DateTime?>)null, portfolioBias, lookback, period, resolution, optimizer)
|
||||
{
|
||||
SetRebalancingFunc(rebalance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance UTC time.
|
||||
/// Returning current time will trigger rebalance. If null will be ignored</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
|
||||
public RiskParityPortfolioConstructionModel(Func<DateTime, DateTime> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 252,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: this(rebalancingFunc != null ? (Func<DateTime, DateTime?>)(timeUtc => rebalancingFunc(timeUtc)) : null,
|
||||
portfolioBias,
|
||||
lookback,
|
||||
period,
|
||||
resolution,
|
||||
optimizer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the model
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance.</param>
|
||||
/// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
|
||||
/// <param name="lookback">Historical return lookback period</param>
|
||||
/// <param name="period">The time interval of history price to calculate the weight</param>
|
||||
/// <param name="resolution">The resolution of the history price</param>
|
||||
/// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
|
||||
public RiskParityPortfolioConstructionModel(Func<DateTime, DateTime?> rebalancingFunc,
|
||||
PortfolioBias portfolioBias = PortfolioBias.LongShort,
|
||||
int lookback = 1,
|
||||
int period = 252,
|
||||
Resolution resolution = Resolution.Daily,
|
||||
IPortfolioOptimizer optimizer = null)
|
||||
: base(rebalancingFunc)
|
||||
{
|
||||
if (portfolioBias == PortfolioBias.Short)
|
||||
{
|
||||
throw new ArgumentException("Long position must be allowed in RiskParityPortfolioConstructionModel.");
|
||||
}
|
||||
|
||||
_lookback = lookback;
|
||||
_period = period;
|
||||
_resolution = resolution;
|
||||
|
||||
_optimizer = optimizer ?? new RiskParityPortfolioOptimizer();
|
||||
|
||||
_symbolDataDict = new Dictionary<Symbol, ReturnsSymbolData>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will determine the target percent for each insight
|
||||
/// </summary>
|
||||
/// <param name="activeInsights">The active insights to generate a target for</param>
|
||||
/// <returns>A target percent for each insight</returns>
|
||||
protected override Dictionary<Insight, double> DetermineTargetPercent(List<Insight> activeInsights)
|
||||
{
|
||||
var targets = new Dictionary<Insight, double>();
|
||||
|
||||
// If we have no insights just return an empty target list
|
||||
if (activeInsights.IsNullOrEmpty())
|
||||
{
|
||||
return targets;
|
||||
}
|
||||
|
||||
var symbols = activeInsights.Select(x => x.Symbol).ToList();
|
||||
|
||||
// Get symbols' returns
|
||||
var returns = _symbolDataDict.FormReturnsMatrix(symbols);
|
||||
|
||||
// The optimization method processes the data frame
|
||||
var w = _optimizer.Optimize(returns);
|
||||
|
||||
// process results
|
||||
if (w.Length > 0)
|
||||
{
|
||||
var sidx = 0;
|
||||
foreach (var symbol in symbols)
|
||||
{
|
||||
var weight = w[sidx];
|
||||
targets[activeInsights.First(insight => insight.Symbol == symbol)] = weight;
|
||||
|
||||
sidx++;
|
||||
}
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event fired each time the we add/remove securities from the data feed
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
|
||||
/// <param name="changes">The security additions and removals from the algorithm</param>
|
||||
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
|
||||
{
|
||||
base.OnSecuritiesChanged(algorithm, changes);
|
||||
// clean up data for removed securities
|
||||
foreach (var removed in changes.RemovedSecurities)
|
||||
{
|
||||
_symbolDataDict.Remove(removed.Symbol, out var removedSymbolData);
|
||||
algorithm.UnregisterIndicator(removedSymbolData.ROC);
|
||||
}
|
||||
|
||||
if (changes.AddedSecurities.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// initialize data for added securities
|
||||
foreach (var added in changes.AddedSecurities)
|
||||
{
|
||||
if (!_symbolDataDict.ContainsKey(added.Symbol))
|
||||
{
|
||||
var symbolData = new ReturnsSymbolData(added.Symbol, _lookback, _period);
|
||||
_symbolDataDict[added.Symbol] = symbolData;
|
||||
algorithm.RegisterIndicator(added.Symbol, symbolData.ROC, _resolution);
|
||||
}
|
||||
}
|
||||
|
||||
// warmup our indicators by pushing history through the consolidators
|
||||
algorithm.History(changes.AddedSecurities.Select(security => security.Symbol), _lookback * _period, _resolution)
|
||||
.PushThrough(bar =>
|
||||
{
|
||||
ReturnsSymbolData symbolData;
|
||||
if (_symbolDataDict.TryGetValue(bar.Symbol, out symbolData))
|
||||
{
|
||||
symbolData.Update(bar.EndTime, bar.Value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
# 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.
|
||||
|
||||
from AlgorithmImports import *
|
||||
from Portfolio.RiskParityPortfolioOptimizer import RiskParityPortfolioOptimizer
|
||||
|
||||
### <summary>
|
||||
### Risk Parity Portfolio Construction Model
|
||||
### </summary>
|
||||
### <remarks>Spinu, F. (2013). An algorithm for computing risk parity weights. Available at SSRN 2297383.
|
||||
### Available at https://papers.ssrn.com/sol3/Papers.cfm?abstract_id=2297383</remarks>
|
||||
class RiskParityPortfolioConstructionModel(PortfolioConstructionModel):
|
||||
def __init__(self,
|
||||
rebalance = Resolution.DAILY,
|
||||
portfolio_bias = PortfolioBias.LONG_SHORT,
|
||||
lookback = 1,
|
||||
period = 252,
|
||||
resolution = Resolution.DAILY,
|
||||
optimizer = None):
|
||||
"""Initialize the model
|
||||
Args:
|
||||
rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function.
|
||||
If None will be ignored.
|
||||
The function returns the next expected rebalance time for a given algorithm UTC DateTime.
|
||||
The function returns null if unknown, in which case the function will be called again in the
|
||||
next loop. Returning current time will trigger rebalance.
|
||||
portfolio_bias: Specifies the bias of the portfolio (Short, Long/Short, Long)
|
||||
lookback(int): Historical return lookback period
|
||||
period(int): The time interval of history price to calculate the weight
|
||||
resolution: The resolution of the history price
|
||||
optimizer(class): Method used to compute the portfolio weights"""
|
||||
super().__init__()
|
||||
if portfolio_bias == PortfolioBias.SHORT:
|
||||
raise ArgumentException("Long position must be allowed in RiskParityPortfolioConstructionModel.")
|
||||
|
||||
self.lookback = lookback
|
||||
self.period = period
|
||||
self.resolution = resolution
|
||||
self.sign = lambda x: -1 if x < 0 else (1 if x > 0 else 0)
|
||||
|
||||
self.optimizer = RiskParityPortfolioOptimizer() if optimizer is None else optimizer
|
||||
|
||||
self._symbol_data_by_symbol = {}
|
||||
|
||||
# If the argument is an instance of Resolution or Timedelta
|
||||
# Redefine rebalancing_func
|
||||
rebalancing_func = rebalance
|
||||
if isinstance(rebalance, int):
|
||||
rebalance = Extensions.to_time_span(rebalance)
|
||||
if isinstance(rebalance, timedelta):
|
||||
rebalancing_func = lambda dt: dt + rebalance
|
||||
if rebalancing_func:
|
||||
self.set_rebalancing_func(rebalancing_func)
|
||||
|
||||
def determine_target_percent(self, active_insights):
|
||||
"""Will determine the target percent for each insight
|
||||
Args:
|
||||
active_insights: list of active insights
|
||||
Returns:
|
||||
dictionary of insight and respective target weight
|
||||
"""
|
||||
targets = {}
|
||||
|
||||
# If we have no insights just return an empty target list
|
||||
if len(active_insights) == 0:
|
||||
return targets
|
||||
|
||||
symbols = [insight.symbol for insight in active_insights]
|
||||
|
||||
# Create a dictionary keyed by the symbols in the insights with an pandas.series as value to create a data frame
|
||||
returns = { str(symbol) : data.return_ for symbol, data in self._symbol_data_by_symbol.items() if symbol in symbols }
|
||||
returns = pd.DataFrame(returns)
|
||||
|
||||
# The portfolio optimizer finds the optional weights for the given data
|
||||
weights = self.optimizer.optimize(returns)
|
||||
weights = pd.Series(weights, index = returns.columns)
|
||||
|
||||
# Create portfolio targets from the specified insights
|
||||
for insight in active_insights:
|
||||
targets[insight] = weights[str(insight.symbol)]
|
||||
|
||||
return targets
|
||||
|
||||
def on_securities_changed(self, algorithm, changes):
|
||||
'''Event fired each time the we add/remove securities from the data feed
|
||||
Args:
|
||||
algorithm: The algorithm instance that experienced the change in securities
|
||||
changes: The security additions and removals from the algorithm'''
|
||||
|
||||
# clean up data for removed securities
|
||||
super().on_securities_changed(algorithm, changes)
|
||||
for removed in changes.removed_securities:
|
||||
symbol_data = self._symbol_data_by_symbol.pop(removed.symbol, None)
|
||||
symbol_data.reset()
|
||||
algorithm.unregister_indicator(symbol_data.roc)
|
||||
|
||||
# initialize data for added securities
|
||||
symbols = [ x.symbol for x in changes.added_securities ]
|
||||
history = algorithm.history(symbols, self.lookback * self.period, self.resolution)
|
||||
if history.empty: return
|
||||
|
||||
tickers = history.index.levels[0]
|
||||
for ticker in tickers:
|
||||
symbol = SymbolCache.get_symbol(ticker)
|
||||
|
||||
if symbol not in self._symbol_data_by_symbol:
|
||||
symbol_data = self.RiskParitySymbolData(symbol, self.lookback, self.period)
|
||||
symbol_data.warm_up_indicators(history.loc[ticker])
|
||||
self._symbol_data_by_symbol[symbol] = symbol_data
|
||||
algorithm.register_indicator(symbol, symbol_data.roc, self.resolution)
|
||||
|
||||
class RiskParitySymbolData:
|
||||
'''Contains data specific to a symbol required by this model'''
|
||||
def __init__(self, symbol, lookback, period):
|
||||
self._symbol = symbol
|
||||
self.roc = RateOfChange(f'{symbol}.roc({lookback})', lookback)
|
||||
self.roc.updated += self.on_rate_of_change_updated
|
||||
self.window = RollingWindow(period)
|
||||
|
||||
def reset(self):
|
||||
self.roc.updated -= self.on_rate_of_change_updated
|
||||
self.roc.reset()
|
||||
self.window.reset()
|
||||
|
||||
def warm_up_indicators(self, history):
|
||||
for tuple in history.itertuples():
|
||||
self.roc.update(tuple.Index, tuple.close)
|
||||
|
||||
def on_rate_of_change_updated(self, roc, value):
|
||||
if roc.is_ready:
|
||||
self.window.add(value)
|
||||
|
||||
def add(self, time, value):
|
||||
item = IndicatorDataPoint(self._symbol, time, value)
|
||||
self.window.add(item)
|
||||
|
||||
@property
|
||||
def return_(self):
|
||||
return pd.Series(
|
||||
data = [x.value for x in self.window],
|
||||
index = [x.end_time for x in self.window])
|
||||
|
||||
@property
|
||||
def is_ready(self):
|
||||
return self.window.is_ready
|
||||
|
||||
def __str__(self, **kwargs):
|
||||
return '{}: {:.2%}'.format(self.roc.name, self.window[0])
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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 Accord.Math;
|
||||
using Accord.Statistics;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an implementation of a risk parity portfolio optimizer that calculate the optimal weights
|
||||
/// with the weight range from 0 to 1 and equalize the risk carried by each asset
|
||||
/// </summary>
|
||||
public class RiskParityPortfolioOptimizer : IPortfolioOptimizer
|
||||
{
|
||||
private double _lower = 1e-05;
|
||||
private double _upper = Double.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="RiskParityPortfolioOptimizer"/>
|
||||
/// </summary>
|
||||
/// <param name="lower">The lower bounds on portfolio weights</param>
|
||||
/// <param name="upper">The upper bounds on portfolio weights</param>
|
||||
public RiskParityPortfolioOptimizer(double? lower = null, double? upper = null)
|
||||
{
|
||||
_lower = lower ?? _lower; // has to be greater than or equal to 0
|
||||
_upper = upper ?? _upper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform portfolio optimization for a provided matrix of historical returns and an array of expected returns
|
||||
/// </summary>
|
||||
/// <param name="historicalReturns">Matrix of annualized historical returns where each column represents a security and each row returns for the given date/time (size: K x N).</param>
|
||||
/// <param name="expectedReturns">Risk budget vector (size: K x 1).</param>
|
||||
/// <param name="covariance">Multi-dimensional array of double with the portfolio covariance of annualized returns (size: K x K).</param>
|
||||
/// <returns>Array of double with the portfolio weights (size: K x 1)</returns>
|
||||
public double[] Optimize(double[,] historicalReturns, double[] expectedReturns = null, double[,] covariance = null)
|
||||
{
|
||||
covariance = covariance ?? historicalReturns.Covariance();
|
||||
var size = covariance.GetLength(0);
|
||||
|
||||
// Optimization Problem
|
||||
// minimize_{x >= 0} f(x) = 1/2 * x^T.S.x - b^T.log(x)
|
||||
// b = 1 / num_of_assets (equal budget of risk)
|
||||
// df(x)/dx = S.x - b / x
|
||||
// H(x) = S + Diag(b / x^2)
|
||||
expectedReturns = expectedReturns ?? Vector.Create(size, 1d / size);
|
||||
var solution = RiskParityNewtonMethodOptimization(size, covariance, expectedReturns);
|
||||
|
||||
// Normalize weights: w = x / x^T.1
|
||||
solution = Elementwise.Divide(solution, solution.Sum());
|
||||
// Make sure the vector is within range
|
||||
return solution.Select(x => Math.Clamp(x, _lower, _upper)).ToArray();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Newton method of minimization
|
||||
/// </summary>
|
||||
/// <param name="numberOfVariables">The number of variables (size of weight vector).</param>
|
||||
/// <param name="covariance">Covariance matrix (size: K x K).</param>
|
||||
/// <param name="budget">The risk budget (size: K x 1).</param>
|
||||
/// <param name="tolerance">Tolerance level of objective difference with previous steps to accept minimization result.</param>
|
||||
/// <param name="maximumIteration">Maximum iteration per optimization.</param>
|
||||
/// <returns>Array of double of argumented minimization</returns>
|
||||
protected double[] RiskParityNewtonMethodOptimization(int numberOfVariables, double[,] covariance, double[] budget, double tolerance = 1e-11, int maximumIteration = 15000)
|
||||
{
|
||||
if (numberOfVariables < 1 || numberOfVariables > 1000)
|
||||
{
|
||||
throw new ArgumentException("Argument \"numberOfVariables\" must be a positive integer between 1 and 1000");
|
||||
}
|
||||
else if (numberOfVariables == 1)
|
||||
{
|
||||
return new double[]{1d};
|
||||
}
|
||||
|
||||
Func<double[], double> objective = (x) => 0.5 * Matrix.Dot(Matrix.Dot(x, covariance), x) - Matrix.Dot(budget, Elementwise.Log(x));
|
||||
Func<double[], double[]> gradient = (x) => Elementwise.Subtract(Matrix.Dot(covariance, x), Elementwise.Divide(budget, x));
|
||||
Func<double[], double[,]> hessian = (x) => Elementwise.Add(covariance, Matrix.Diagonal(Elementwise.Divide(budget, Elementwise.Multiply(x, x))));
|
||||
var weight = Vector.Create(numberOfVariables, 1d / numberOfVariables);
|
||||
var newObjective = Double.MinValue;
|
||||
var oldObjective = Double.MaxValue;
|
||||
var iter = 0;
|
||||
|
||||
while (Math.Abs(newObjective - oldObjective) > tolerance && iter < maximumIteration)
|
||||
{
|
||||
// Store old objective value
|
||||
oldObjective = newObjective;
|
||||
|
||||
// Get parameters for Newton method gradient descend
|
||||
var invHess = Matrix.Inverse(hessian(weight));
|
||||
var jacobian = gradient(weight);
|
||||
|
||||
// Get next weight vector
|
||||
// x^{k + 1} = x^{k} - H^{-1}(x^{k}).df(x^{k}))
|
||||
weight = Elementwise.Subtract(weight, Matrix.Dot(invHess, jacobian));
|
||||
|
||||
// Store new objective value
|
||||
newObjective = objective(weight);
|
||||
|
||||
iter++;
|
||||
}
|
||||
|
||||
return weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
# 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.
|
||||
|
||||
from AlgorithmImports import *
|
||||
from scipy.optimize import *
|
||||
|
||||
### <summary>
|
||||
### Provides an implementation of a risk parity portfolio optimizer that calculate the optimal weights
|
||||
### with the weight range from 0 to 1 and equalize the risk carried by each asset
|
||||
### </summary>
|
||||
class RiskParityPortfolioOptimizer:
|
||||
|
||||
def __init__(self,
|
||||
minimum_weight = 1e-05,
|
||||
maximum_weight = sys.float_info.max):
|
||||
'''Initialize the RiskParityPortfolioOptimizer
|
||||
Args:
|
||||
minimum_weight(float): The lower bounds on portfolio weights
|
||||
maximum_weight(float): The upper bounds on portfolio weights'''
|
||||
self.minimum_weight = minimum_weight if minimum_weight >= 1e-05 else 1e-05
|
||||
self.maximum_weight = maximum_weight if maximum_weight >= minimum_weight else minimum_weight
|
||||
|
||||
def optimize(self, historical_returns, budget = None, covariance = None):
|
||||
'''
|
||||
Perform portfolio optimization for a provided matrix of historical returns and an array of expected returns
|
||||
args:
|
||||
historical_returns: Matrix of annualized historical returns where each column represents a security and each row returns for the given date/time (size: K x N).
|
||||
budget: Risk budget vector (size: K x 1).
|
||||
covariance: Multi-dimensional array of double with the portfolio covariance of annualized returns (size: K x K).
|
||||
Returns:
|
||||
Array of double with the portfolio weights (size: K x 1)
|
||||
'''
|
||||
if covariance is None:
|
||||
covariance = np.cov(historical_returns.T)
|
||||
|
||||
size = historical_returns.columns.size # K x 1
|
||||
|
||||
# Optimization Problem
|
||||
# minimize_{x >= 0} f(x) = 1/2 * x^T.S.x - b^T.log(x)
|
||||
# b = 1 / num_of_assets (equal budget of risk)
|
||||
# df(x)/dx = S.x - b / x
|
||||
# H(x) = S + Diag(b / x^2)
|
||||
# lw <= x <= up
|
||||
x0 = np.array(size * [1. / size])
|
||||
budget = budget if budget is not None else x0
|
||||
objective = lambda weights: 0.5 * weights.T @ covariance @ weights - budget.T @ np.log(weights)
|
||||
gradient = lambda weights: covariance @ weights - budget / weights
|
||||
hessian = lambda weights: covariance + np.diag((budget / weights**2).flatten())
|
||||
solver = minimize(objective, jac=gradient, hess=hessian, x0=x0, method="Newton-CG")
|
||||
|
||||
if not solver["success"]: return x0
|
||||
# Normalize weights: w = x / x^T.1
|
||||
return np.clip(solver["x"]/np.sum(solver["x"]), self.minimum_weight, self.maximum_weight)
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* 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 Python.Runtime;
|
||||
using QuantConnect.Algorithm.Framework.Alphas;
|
||||
using QuantConnect.Data.Fundamental;
|
||||
using QuantConnect.Data.UniverseSelection;
|
||||
using QuantConnect.Scheduling;
|
||||
using QuantConnect.Securities;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an implementation of <see cref="IPortfolioConstructionModel"/> that generates percent targets based on the
|
||||
/// <see cref="CompanyReference.IndustryTemplateCode"/>.
|
||||
/// The target percent holdings of each sector is 1/S where S is the number of sectors and
|
||||
/// the target percent holdings of each security is 1/N where N is the number of securities of each sector.
|
||||
/// For insights of direction <see cref="InsightDirection.Up"/>, long targets are returned and for insights of direction
|
||||
/// <see cref="InsightDirection.Down"/>, short targets are returned.
|
||||
/// It will ignore <see cref="Insight"/> for symbols that have no <see cref="CompanyReference.IndustryTemplateCode"/> value.
|
||||
/// </summary>
|
||||
public class SectorWeightingPortfolioConstructionModel : EqualWeightingPortfolioConstructionModel
|
||||
{
|
||||
private readonly Dictionary<Symbol, string> _sectorCodeBySymbol = new Dictionary<Symbol, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="SectorWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalancingDateRules">The date rules used to define the next expected rebalance time
|
||||
/// in UTC</param>
|
||||
public SectorWeightingPortfolioConstructionModel(IDateRule rebalancingDateRules)
|
||||
: base(rebalancingDateRules.ToFunc())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="SectorWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance. If null will be ignored</param>
|
||||
public SectorWeightingPortfolioConstructionModel(Func<DateTime, DateTime?> rebalancingFunc)
|
||||
: base(rebalancingFunc)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="SectorWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance UTC time.
|
||||
/// Returning current time will trigger rebalance. If null will be ignored</param>
|
||||
public SectorWeightingPortfolioConstructionModel(Func<DateTime, DateTime> rebalancingFunc)
|
||||
: this(rebalancingFunc != null ? (Func<DateTime, DateTime?>)(timeUtc => rebalancingFunc(timeUtc)) : null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="SectorWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="rebalance">Rebalancing func or if a date rule, timedelta will be converted into func.
|
||||
/// For a given algorithm UTC DateTime the func returns the next expected rebalance time
|
||||
/// or null if unknown, in which case the function will be called again in the next loop. Returning current time
|
||||
/// will trigger rebalance. If null will be ignored</param>
|
||||
/// <remarks>This is required since python net can not convert python methods into func nor resolve the correct
|
||||
/// constructor for the date rules parameter.
|
||||
/// For performance we prefer python algorithms using the C# implementation</remarks>
|
||||
public SectorWeightingPortfolioConstructionModel(PyObject rebalance)
|
||||
: this((Func<DateTime, DateTime?>)null)
|
||||
{
|
||||
SetRebalancingFunc(rebalance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="SectorWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">Rebalancing frequency</param>
|
||||
public SectorWeightingPortfolioConstructionModel(TimeSpan timeSpan)
|
||||
: this(dt => dt.Add(timeSpan))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="SectorWeightingPortfolioConstructionModel"/>
|
||||
/// </summary>
|
||||
/// <param name="resolution">Rebalancing frequency</param>
|
||||
public SectorWeightingPortfolioConstructionModel(Resolution resolution = Resolution.Daily)
|
||||
: this(resolution.ToTimeSpan())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method that will determine if the portfolio construction model should create a
|
||||
/// target for this insight
|
||||
/// </summary>
|
||||
/// <param name="insight">The insight to create a target for</param>
|
||||
/// <returns>True if the portfolio should create a target for the insight</returns>
|
||||
protected override bool ShouldCreateTargetForInsight(Insight insight)
|
||||
{
|
||||
return _sectorCodeBySymbol.ContainsKey(insight.Symbol);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will determine the target percent for each insight
|
||||
/// </summary>
|
||||
/// <param name="activeInsights">The active insights to generate a target for</param>
|
||||
/// <returns>A target percent for each insight</returns>
|
||||
protected override Dictionary<Insight, double> DetermineTargetPercent(List<Insight> activeInsights)
|
||||
{
|
||||
var result = new Dictionary<Insight, double>();
|
||||
|
||||
var insightBySectorCode = new Dictionary<string, List<Insight>>();
|
||||
|
||||
foreach (var insight in activeInsights)
|
||||
{
|
||||
if (insight.Direction == InsightDirection.Flat)
|
||||
{
|
||||
result[insight] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Insight> insights;
|
||||
var sectorCode = _sectorCodeBySymbol[insight.Symbol];
|
||||
if (insightBySectorCode.TryGetValue(sectorCode, out insights))
|
||||
{
|
||||
insights.Add(insight);
|
||||
}
|
||||
else
|
||||
{
|
||||
insightBySectorCode[sectorCode] = new List<Insight> { insight };
|
||||
}
|
||||
}
|
||||
|
||||
// give equal weighting to each sector
|
||||
var sectorPercent = insightBySectorCode.Count == 0 ? 0 : 1m / insightBySectorCode.Count;
|
||||
|
||||
foreach (var kvp in insightBySectorCode)
|
||||
{
|
||||
var insights = kvp.Value;
|
||||
|
||||
// give equal weighting to each security
|
||||
var count = insights.Count;
|
||||
var percent = count == 0 ? 0 : sectorPercent / count;
|
||||
|
||||
foreach (var insight in insights)
|
||||
{
|
||||
result[insight] = (double)((int)insight.Direction * percent);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event fired each time the we add/remove securities from the data feed
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
|
||||
/// <param name="changes">The security additions and removals from the algorithm</param>
|
||||
public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
|
||||
{
|
||||
foreach (var security in changes.RemovedSecurities)
|
||||
{
|
||||
// Removes the symbol from the _sectorCodeBySymbol dictionary
|
||||
// since we cannot emit PortfolioTarget for removed securities
|
||||
var symbol = security.Symbol;
|
||||
if (_sectorCodeBySymbol.ContainsKey(symbol))
|
||||
{
|
||||
_sectorCodeBySymbol.Remove(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var security in changes.AddedSecurities)
|
||||
{
|
||||
var sectorCode = GetSectorCode(security);
|
||||
if (!string.IsNullOrEmpty(sectorCode))
|
||||
{
|
||||
_sectorCodeBySymbol[security.Symbol] = sectorCode;
|
||||
}
|
||||
}
|
||||
base.OnSecuritiesChanged(algorithm, changes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sector code
|
||||
/// </summary>
|
||||
/// <param name="security">The security to create a sector code for</param>
|
||||
/// <returns>The value of the sector code for the security</returns>
|
||||
/// <remarks>Other sectors can be defined using <see cref="AssetClassification"/></remarks>
|
||||
protected virtual string GetSectorCode(Security security)
|
||||
{
|
||||
return security.Fundamentals?.CompanyReference.IndustryTemplateCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
|
||||
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from AlgorithmImports import *
|
||||
from EqualWeightingPortfolioConstructionModel import EqualWeightingPortfolioConstructionModel
|
||||
|
||||
class SectorWeightingPortfolioConstructionModel(EqualWeightingPortfolioConstructionModel):
|
||||
'''Provides an implementation of IPortfolioConstructionModel that
|
||||
generates percent targets based on the CompanyReference.industry_template_code.
|
||||
The target percent holdings of each sector is 1/S where S is the number of sectors and
|
||||
the target percent holdings of each security is 1/N where N is the number of securities of each sector.
|
||||
For insights of direction InsightDirection.UP, long targets are returned and for insights of direction
|
||||
InsightDirection.DOWN, short targets are returned.
|
||||
It will ignore Insight for symbols that have no CompanyReference.industry_template_code'''
|
||||
|
||||
def __init__(self, rebalance = Resolution.DAILY):
|
||||
'''Initialize a new instance of InsightWeightingPortfolioConstructionModel
|
||||
Args:
|
||||
rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function.
|
||||
If None will be ignored.
|
||||
The function returns the next expected rebalance time for a given algorithm UTC DateTime.
|
||||
The function returns null if unknown, in which case the function will be called again in the
|
||||
next loop. Returning current time will trigger rebalance.'''
|
||||
super().__init__(rebalance)
|
||||
self.sector_code_by_symbol = dict()
|
||||
|
||||
def should_create_target_for_insight(self, insight):
|
||||
'''Method that will determine if the portfolio construction model should create a
|
||||
target for this insight
|
||||
Args:
|
||||
insight: The insight to create a target for'''
|
||||
return insight.symbol in self.sector_code_by_symbol
|
||||
|
||||
def determine_target_percent(self, active_insights):
|
||||
'''Will determine the target percent for each insight
|
||||
Args:
|
||||
active_insights: The active insights to generate a target for'''
|
||||
result = dict()
|
||||
|
||||
insight_by_sector_code = dict()
|
||||
|
||||
for insight in active_insights:
|
||||
if insight.direction == InsightDirection.FLAT:
|
||||
result[insight] = 0
|
||||
continue
|
||||
|
||||
sector_code = self.sector_code_by_symbol.get(insight.symbol)
|
||||
insights = insight_by_sector_code.pop(sector_code, list())
|
||||
|
||||
insights.append(insight)
|
||||
insight_by_sector_code[sector_code] = insights
|
||||
|
||||
# give equal weighting to each sector
|
||||
sector_percent = 0 if len(insight_by_sector_code) == 0 else 1.0 / len(insight_by_sector_code)
|
||||
|
||||
for _, insights in insight_by_sector_code.items():
|
||||
# give equal weighting to each security
|
||||
count = len(insights)
|
||||
percent = 0 if count == 0 else sector_percent / count
|
||||
for insight in insights:
|
||||
result[insight] = insight.direction * percent
|
||||
|
||||
return result
|
||||
|
||||
def on_securities_changed(self, algorithm, changes):
|
||||
'''Event fired each time the we add/remove securities from the data feed
|
||||
Args:
|
||||
algorithm: The algorithm instance that experienced the change in securities
|
||||
changes: The security additions and removals from the algorithm'''
|
||||
for security in changes.removed_securities:
|
||||
# Removes the symbol from the self.sector_code_by_symbol dictionary
|
||||
# since we cannot emit PortfolioTarget for removed securities
|
||||
self.sector_code_by_symbol.pop(security.symbol, None)
|
||||
|
||||
for security in changes.added_securities:
|
||||
sector_code = self.get_sector_code(security)
|
||||
if sector_code:
|
||||
self.sector_code_by_symbol[security.symbol] = sector_code
|
||||
|
||||
super().on_securities_changed(algorithm, changes)
|
||||
|
||||
def get_sector_code(self, security):
|
||||
'''Gets the sector code
|
||||
Args:
|
||||
security: The security to create a sector code for
|
||||
Returns:
|
||||
The value of the sector code for the security
|
||||
Remarks:
|
||||
Other sectors can be defined using AssetClassification'''
|
||||
fundamentals = security.fundamentals
|
||||
company_reference = security.fundamentals.company_reference if fundamentals else None
|
||||
return company_reference.industry_template_code if company_reference else None
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 Accord.Statistics;
|
||||
|
||||
namespace QuantConnect.Algorithm.Framework.Portfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an implementation of a portfolio optimizer with unconstrained mean variance.
|
||||
/// </summary>
|
||||
public class UnconstrainedMeanVariancePortfolioOptimizer : IPortfolioOptimizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Perform portfolio optimization for a provided matrix of historical returns and an array of expected returns
|
||||
/// </summary>
|
||||
/// <param name="historicalReturns">Matrix of historical returns where each column represents a security and each row returns for the given date/time (size: K x N).</param>
|
||||
/// <param name="expectedReturns">Array of double with the portfolio annualized expected returns (size: K x 1).</param>
|
||||
/// <param name="covariance">Multi-dimensional array of double with the portfolio covariance of annualized returns (size: K x K).</param>
|
||||
/// <returns>Array of double with the portfolio weights (size: K x 1)</returns>
|
||||
public double[] Optimize(double[,] historicalReturns, double[] expectedReturns = null, double[,] covariance = null)
|
||||
{
|
||||
var Π = (expectedReturns ?? historicalReturns.Mean(0));
|
||||
var Σ = covariance ?? historicalReturns.Covariance();
|
||||
return Π.Dot(Σ.Inverse());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# 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.
|
||||
|
||||
from numpy import dot
|
||||
from numpy.linalg import inv
|
||||
|
||||
### <summary>
|
||||
### Provides an implementation of a portfolio optimizer with unconstrained mean variance.'''
|
||||
### </summary>
|
||||
class UnconstrainedMeanVariancePortfolioOptimizer:
|
||||
'''Provides an implementation of a portfolio optimizer with unconstrained mean variance.'''
|
||||
def optimize(self, historical_returns, expected_returns = None, covariance = None):
|
||||
'''
|
||||
Perform portfolio optimization for a provided matrix of historical returns and an array of expected returns
|
||||
args:
|
||||
historical_returns: Matrix of historical returns where each column represents a security and each row returns for the given date/time (size: K x N).
|
||||
expected_returns: Array of double with the portfolio annualized expected returns (size: K x 1).
|
||||
covariance: Multi-dimensional array of double with the portfolio covariance of annualized returns (size: K x K).</param>
|
||||
Returns:
|
||||
Array of double with the portfolio weights (size: K x 1)
|
||||
'''
|
||||
if expected_returns is None:
|
||||
expected_returns = historical_returns.mean()
|
||||
if covariance is None:
|
||||
covariance = historical_returns.cov()
|
||||
|
||||
return expected_returns.dot(inv(covariance))
|
||||
Reference in New Issue
Block a user