Lean  $LEAN_TAG$
MeanVarianceOptimizationPortfolioConstructionModel.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
17 using System.Collections.Generic;
18 using System.Linq;
19 using Accord.Math;
20 using Python.Runtime;
22 using QuantConnect.Data;
25 using QuantConnect.Util;
26 
28 {
29  /// <summary>
30  /// Provides an implementation of Mean-Variance portfolio optimization based on modern portfolio theory.
31  /// The interval of weights in optimization method can be changed based on the long-short algorithm.
32  /// The default model uses the last three months daily price to calculate the optimal weight
33  /// with the weight range from -1 to 1 and minimize the portfolio variance with a target return of 2%
34  /// </summary>
36  {
37  private readonly int _lookback;
38  private readonly int _period;
39  private readonly Resolution _resolution;
40  private readonly PortfolioBias _portfolioBias;
41  private readonly IPortfolioOptimizer _optimizer;
42  private readonly Dictionary<Symbol, ReturnsSymbolData> _symbolDataDict;
43 
44  /// <summary>
45  /// Initialize the model
46  /// </summary>
47  /// <param name="rebalancingDateRules">The date rules used to define the next expected rebalance time
48  /// in UTC</param>
49  /// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
50  /// <param name="lookback">Historical return lookback period</param>
51  /// <param name="period">The time interval of history price to calculate the weight</param>
52  /// <param name="resolution">The resolution of the history price</param>
53  /// <param name="targetReturn">The target portfolio return</param>
54  /// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
56  PortfolioBias portfolioBias = PortfolioBias.LongShort,
57  int lookback = 1,
58  int period = 63,
59  Resolution resolution = Resolution.Daily,
60  double targetReturn = 0.02,
61  IPortfolioOptimizer optimizer = null)
62  : this(rebalancingDateRules.ToFunc(), portfolioBias, lookback, period, resolution, targetReturn, optimizer)
63  {
64  }
65 
66  /// <summary>
67  /// Initialize the model
68  /// </summary>
69  /// <param name="rebalanceResolution">Rebalancing frequency</param>
70  /// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
71  /// <param name="lookback">Historical return lookback period</param>
72  /// <param name="period">The time interval of history price to calculate the weight</param>
73  /// <param name="resolution">The resolution of the history price</param>
74  /// <param name="targetReturn">The target portfolio return</param>
75  /// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
77  PortfolioBias portfolioBias = PortfolioBias.LongShort,
78  int lookback = 1,
79  int period = 63,
80  Resolution resolution = Resolution.Daily,
81  double targetReturn = 0.02,
82  IPortfolioOptimizer optimizer = null)
83  : this(rebalanceResolution.ToTimeSpan(), portfolioBias, lookback, period, resolution, targetReturn, optimizer)
84  {
85  }
86 
87  /// <summary>
88  /// Initialize the model
89  /// </summary>
90  /// <param name="timeSpan">Rebalancing frequency</param>
91  /// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
92  /// <param name="lookback">Historical return lookback period</param>
93  /// <param name="period">The time interval of history price to calculate the weight</param>
94  /// <param name="resolution">The resolution of the history price</param>
95  /// <param name="targetReturn">The target portfolio return</param>
96  /// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
98  PortfolioBias portfolioBias = PortfolioBias.LongShort,
99  int lookback = 1,
100  int period = 63,
101  Resolution resolution = Resolution.Daily,
102  double targetReturn = 0.02,
103  IPortfolioOptimizer optimizer = null)
104  : this(dt => dt.Add(timeSpan), portfolioBias, lookback, period, resolution, targetReturn, optimizer)
105  {
106  }
107 
108  /// <summary>
109  /// Initialize the model
110  /// </summary>
111  /// <param name="rebalance">Rebalancing func or if a date rule, timedelta will be converted into func.
112  /// For a given algorithm UTC DateTime the func returns the next expected rebalance time
113  /// or null if unknown, in which case the function will be called again in the next loop. Returning current time
114  /// will trigger rebalance. If null will be ignored</param>
115  /// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
116  /// <param name="lookback">Historical return lookback period</param>
117  /// <param name="period">The time interval of history price to calculate the weight</param>
118  /// <param name="resolution">The resolution of the history price</param>
119  /// <param name="targetReturn">The target portfolio return</param>
120  /// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
121  /// <remarks>This is required since python net can not convert python methods into func nor resolve the correct
122  /// constructor for the date rules parameter.
123  /// For performance we prefer python algorithms using the C# implementation</remarks>
125  PortfolioBias portfolioBias = PortfolioBias.LongShort,
126  int lookback = 1,
127  int period = 63,
128  Resolution resolution = Resolution.Daily,
129  double targetReturn = 0.02,
130  PyObject optimizer = null)
131  : this((Func<DateTime, DateTime?>)null, portfolioBias, lookback, period, resolution, targetReturn, null)
132  {
133  SetRebalancingFunc(rebalance);
134 
135  if (optimizer != null)
136  {
137  if (optimizer.TryConvert<IPortfolioOptimizer>(out var csharpOptimizer))
138  {
139  _optimizer = csharpOptimizer;
140  }
141  else
142  {
143  _optimizer = new PortfolioOptimizerPythonWrapper(optimizer);
144  }
145  }
146  }
147 
148  /// <summary>
149  /// Initialize the model
150  /// </summary>
151  /// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance UTC time.
152  /// Returning current time will trigger rebalance. If null will be ignored</param>
153  /// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
154  /// <param name="lookback">Historical return lookback period</param>
155  /// <param name="period">The time interval of history price to calculate the weight</param>
156  /// <param name="resolution">The resolution of the history price</param>
157  /// <param name="targetReturn">The target portfolio return</param>
158  /// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
159  public MeanVarianceOptimizationPortfolioConstructionModel(Func<DateTime, DateTime> rebalancingFunc,
160  PortfolioBias portfolioBias = PortfolioBias.LongShort,
161  int lookback = 1,
162  int period = 63,
163  Resolution resolution = Resolution.Daily,
164  double targetReturn = 0.02,
165  IPortfolioOptimizer optimizer = null)
166  : this(rebalancingFunc != null ? (Func<DateTime, DateTime?>)(timeUtc => rebalancingFunc(timeUtc)) : null,
167  portfolioBias,
168  lookback,
169  period,
170  resolution,
171  targetReturn,
172  optimizer)
173  {
174  }
175 
176  /// <summary>
177  /// Initialize the model
178  /// </summary>
179  /// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance time
180  /// or null if unknown, in which case the function will be called again in the next loop. Returning current time
181  /// will trigger rebalance.</param>
182  /// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
183  /// <param name="lookback">Historical return lookback period</param>
184  /// <param name="period">The time interval of history price to calculate the weight</param>
185  /// <param name="resolution">The resolution of the history price</param>
186  /// <param name="targetReturn">The target portfolio return</param>
187  /// <param name="optimizer">The portfolio optimization algorithm. If the algorithm is not provided then the default will be mean-variance optimization.</param>
188  public MeanVarianceOptimizationPortfolioConstructionModel(Func<DateTime, DateTime?> rebalancingFunc,
189  PortfolioBias portfolioBias = PortfolioBias.LongShort,
190  int lookback = 1,
191  int period = 63,
192  Resolution resolution = Resolution.Daily,
193  double targetReturn = 0.02,
194  IPortfolioOptimizer optimizer = null)
195  : base(rebalancingFunc)
196  {
197  _lookback = lookback;
198  _period = period;
199  _resolution = resolution;
200  _portfolioBias = portfolioBias;
201 
202  var lower = portfolioBias == PortfolioBias.Long ? 0 : -1;
203  var upper = portfolioBias == PortfolioBias.Short ? 0 : 1;
204  _optimizer = optimizer ?? new MinimumVariancePortfolioOptimizer(lower, upper, targetReturn);
205 
206  _symbolDataDict = new Dictionary<Symbol, ReturnsSymbolData>();
207  }
208 
209  /// <summary>
210  /// Method that will determine if the portfolio construction model should create a
211  /// target for this insight
212  /// </summary>
213  /// <param name="insight">The insight to create a target for</param>
214  /// <returns>True if the portfolio should create a target for the insight</returns>
215  protected override bool ShouldCreateTargetForInsight(Insight insight)
216  {
217  var filteredInsight = FilterInvalidInsightMagnitude(Algorithm, new[] { insight }).FirstOrDefault();
218  if (filteredInsight == null)
219  {
220  return false;
221  }
222 
223  ReturnsSymbolData data;
224  if (_symbolDataDict.TryGetValue(insight.Symbol, out data))
225  {
226  if (!insight.Magnitude.HasValue)
227  {
228  Algorithm.SetRunTimeError(
229  new ArgumentNullException(
230  insight.Symbol.Value,
231  "MeanVarianceOptimizationPortfolioConstructionModel does not accept 'null' as Insight.Magnitude. " +
232  "Please checkout the selected Alpha Model specifications: " + insight.SourceModel));
233  return false;
234  }
235  data.Add(Algorithm.Time, insight.Magnitude.Value.SafeDecimalCast());
236  }
237 
238  return true;
239  }
240 
241  /// <summary>
242  /// Will determine the target percent for each insight
243  /// </summary>
244  /// <param name="activeInsights">The active insights to generate a target for</param>
245  /// <returns>A target percent for each insight</returns>
246  protected override Dictionary<Insight, double> DetermineTargetPercent(List<Insight> activeInsights)
247  {
248  var targets = new Dictionary<Insight, double>();
249 
250  // If we have no insights just return an empty target list
251  if (activeInsights.IsNullOrEmpty())
252  {
253  return targets;
254  }
255 
256  var symbols = activeInsights.Select(x => x.Symbol).ToList();
257 
258  // Get symbols' returns, we use simple return according to
259  // Meucci, Attilio, Quant Nugget 2: Linear vs. Compounded Returns – Common Pitfalls in Portfolio Management (May 1, 2010).
260  // GARP Risk Professional, pp. 49-51, April 2010 , Available at SSRN: https://ssrn.com/abstract=1586656
261  var returns = _symbolDataDict.FormReturnsMatrix(symbols);
262 
263  // The optimization method processes the data frame
264  var w = _optimizer.Optimize(returns);
265 
266  // process results
267  if (w.Length > 0)
268  {
269  var sidx = 0;
270  foreach (var symbol in symbols)
271  {
272  var weight = w[sidx];
273 
274  // don't trust the optimizer
275  if (_portfolioBias != PortfolioBias.LongShort
276  && Math.Sign(weight) != (int)_portfolioBias)
277  {
278  weight = 0;
279  }
280 
281  targets[activeInsights.First(insight => insight.Symbol == symbol)] = weight;
282 
283  sidx++;
284  }
285  }
286 
287  return targets;
288  }
289 
290  /// <summary>
291  /// Event fired each time the we add/remove securities from the data feed
292  /// </summary>
293  /// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
294  /// <param name="changes">The security additions and removals from the algorithm</param>
295  public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
296  {
297  base.OnSecuritiesChanged(algorithm, changes);
298  // clean up data for removed securities
299  foreach (var removed in changes.RemovedSecurities)
300  {
301  ReturnsSymbolData data;
302  if (_symbolDataDict.TryGetValue(removed.Symbol, out data))
303  {
304  _symbolDataDict.Remove(removed.Symbol);
305  }
306  }
307 
308  if (changes.AddedSecurities.Count == 0)
309  return;
310 
311  // initialize data for added securities
312  foreach (var added in changes.AddedSecurities)
313  {
314  if (!_symbolDataDict.ContainsKey(added.Symbol))
315  {
316  var symbolData = new ReturnsSymbolData(added.Symbol, _lookback, _period);
317  _symbolDataDict[added.Symbol] = symbolData;
318  }
319  }
320 
321  // warmup our indicators by pushing history through the consolidators
322  algorithm.History(changes.AddedSecurities.Select(security => security.Symbol), _lookback * _period, _resolution)
323  .PushThrough(bar =>
324  {
325  ReturnsSymbolData symbolData;
326  if (_symbolDataDict.TryGetValue(bar.Symbol, out symbolData))
327  {
328  symbolData.Update(bar.EndTime, bar.Value);
329  }
330  });
331  }
332  }
333 }