Lean  $LEAN_TAG$
PortfolioConstructionModel.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 Python.Runtime;
24 
26 {
27  /// <summary>
28  /// Provides a base class for portfolio construction models
29  /// </summary>
31  {
32  private Func<DateTime, DateTime?> _rebalancingFunc;
33  private DateTime? _rebalancingTime;
34  private bool _securityChanges;
35 
36  /// <summary>
37  /// True if should rebalance portfolio on security changes. True by default
38  /// </summary>
39  public virtual bool RebalanceOnSecurityChanges { get; set; } = true;
40 
41  /// <summary>
42  /// True if should rebalance portfolio on new insights or expiration of insights. True by default
43  /// </summary>
44  public virtual bool RebalanceOnInsightChanges { get; set; } = true;
45 
46  /// <summary>
47  /// The algorithm instance
48  /// </summary>
49  protected IAlgorithm Algorithm { get; private set; }
50 
51  /// <summary>
52  /// This is required due to a limitation in PythonNet to resolved overriden methods.
53  /// When Python calls a C# method that calls a method that's overriden in python it won't
54  /// run the python implementation unless the call is performed through python too.
55  /// </summary>
57 
58  /// <summary>
59  /// Initialize a new instance of <see cref="PortfolioConstructionModel"/>
60  /// </summary>
61  /// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance time
62  /// or null if unknown, in which case the function will be called again in the next loop. Returning current time
63  /// will trigger rebalance. If null will be ignored</param>
64  public PortfolioConstructionModel(Func<DateTime, DateTime?> rebalancingFunc)
65  {
66  _rebalancingFunc = rebalancingFunc;
67  }
68 
69  /// <summary>
70  /// Initialize a new instance of <see cref="PortfolioConstructionModel"/>
71  /// </summary>
72  /// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance UTC time.
73  /// Returning current time will trigger rebalance. If null will be ignored</param>
74  public PortfolioConstructionModel(Func<DateTime, DateTime> rebalancingFunc = null)
75  : this(rebalancingFunc != null ? (Func<DateTime, DateTime?>)(timeUtc => rebalancingFunc(timeUtc)) : null)
76  {
77  }
78 
79  /// <summary>
80  /// Used to set the <see cref="PortfolioConstructionModelPythonWrapper"/> instance if any
81  /// </summary>
83  {
84  PythonWrapper = pythonWrapper;
85  }
86 
87  /// <summary>
88  /// Create portfolio targets from the specified insights
89  /// </summary>
90  /// <param name="algorithm">The algorithm instance</param>
91  /// <param name="insights">The insights to create portfolio targets from</param>
92  /// <returns>An enumerable of portfolio targets to be sent to the execution model</returns>
93  public virtual IEnumerable<IPortfolioTarget> CreateTargets(QCAlgorithm algorithm, Insight[] insights)
94  {
95  Algorithm = algorithm;
96 
97  if (!(PythonWrapper?.IsRebalanceDue(insights, algorithm.UtcTime)
98  ?? IsRebalanceDue(insights, algorithm.UtcTime)))
99  {
100  return Enumerable.Empty<IPortfolioTarget>();
101  }
102 
103  var targets = new List<IPortfolioTarget>();
104 
105  var lastActiveInsights = PythonWrapper?.GetTargetInsights()
106  ?? GetTargetInsights();
107 
108  var errorSymbols = new HashSet<Symbol>();
109 
110  // Determine target percent for the given insights
111  var percents = PythonWrapper?.DetermineTargetPercent(lastActiveInsights)
112  ?? DetermineTargetPercent(lastActiveInsights);
113 
114  foreach (var insight in lastActiveInsights)
115  {
116  if (!percents.TryGetValue(insight, out var percent))
117  {
118  continue;
119  }
120 
121  var target = PortfolioTarget.Percent(algorithm, insight.Symbol, percent);
122  if (target != null)
123  {
124  targets.Add(target);
125  }
126  else
127  {
128  errorSymbols.Add(insight.Symbol);
129  }
130  }
131 
132  // Get expired insights and create flatten targets for each symbol
133  var expiredInsights = Algorithm.Insights.RemoveExpiredInsights(algorithm.UtcTime);
134 
135  var expiredTargets = from insight in expiredInsights
136  group insight.Symbol by insight.Symbol into g
137  where !Algorithm.Insights.HasActiveInsights(g.Key, algorithm.UtcTime) && !errorSymbols.Contains(g.Key)
138  select new PortfolioTarget(g.Key, 0);
139 
140  targets.AddRange(expiredTargets);
141 
142  return targets;
143  }
144 
145  /// <summary>
146  /// Event fired each time the we add/remove securities from the data feed
147  /// </summary>
148  /// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
149  /// <param name="changes">The security additions and removals from the algorithm</param>
150  public virtual void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
151  {
152  Algorithm ??= algorithm;
153 
154  _securityChanges = changes != SecurityChanges.None;
155  // Get removed symbol and invalidate them in the insight collection
156  var removedSymbols = changes.RemovedSecurities.Select(x => x.Symbol);
157  algorithm?.Insights.Expire(removedSymbols);
158  }
159 
160  /// <summary>
161  /// Gets the target insights to calculate a portfolio target percent for
162  /// </summary>
163  /// <returns>An enumerable of the target insights</returns>
164  protected virtual List<Insight> GetTargetInsights()
165  {
166  // Validate we should create a target for this insight
167  bool IsValidInsight(Insight insight) => PythonWrapper?.ShouldCreateTargetForInsight(insight)
168  ?? ShouldCreateTargetForInsight(insight);
169 
170  // Get insight that haven't expired of each symbol that is still in the universe
171  var activeInsights = Algorithm.Insights.GetActiveInsights(Algorithm.UtcTime).Where(IsValidInsight);
172 
173  // Get the last generated active insight for each symbol
174  return (from insight in activeInsights
175  group insight by insight.Symbol into g
176  select g.OrderBy(x => x.GeneratedTimeUtc).Last()).ToList();
177  }
178 
179  /// <summary>
180  /// Method that will determine if the portfolio construction model should create a
181  /// target for this insight
182  /// </summary>
183  /// <param name="insight">The insight to create a target for</param>
184  /// <returns>True if the portfolio should create a target for the insight</returns>
185  protected virtual bool ShouldCreateTargetForInsight(Insight insight)
186  {
187  return true;
188  }
189 
190  /// <summary>
191  /// Will determine the target percent for each insight
192  /// </summary>
193  /// <param name="activeInsights">The active insights to generate a target for</param>
194  /// <returns>A target percent for each insight</returns>
195  protected virtual Dictionary<Insight, double> DetermineTargetPercent(List<Insight> activeInsights)
196  {
197  throw new NotImplementedException("Types deriving from 'PortfolioConstructionModel' must implement the 'Dictionary<Insight, double> DetermineTargetPercent(ICollection<Insight>)' method.");
198  }
199 
200  /// <summary>
201  /// Python helper method to set the rebalancing function.
202  /// This is required due to a python net limitation not being able to use the base type constructor, and also because
203  /// when python algorithms use C# portfolio construction models, it can't convert python methods into func nor resolve
204  /// the correct constructor for the date rules, timespan parameter.
205  /// For performance we prefer python algorithms using the C# implementation
206  /// </summary>
207  /// <param name="rebalance">Rebalancing func or if a date rule, timedelta will be converted into func.
208  /// For a given algorithm UTC DateTime the func returns the next expected rebalance time
209  /// or null if unknown, in which case the function will be called again in the next loop. Returning current time
210  /// will trigger rebalance. If null will be ignored</param>
211  protected void SetRebalancingFunc(PyObject rebalance)
212  {
213  IDateRule dateRules;
214  TimeSpan timeSpan;
215  if (rebalance.TryConvert(out dateRules))
216  {
217  _rebalancingFunc = dateRules.ToFunc();
218  }
219  else if (!rebalance.TryConvertToDelegate(out _rebalancingFunc))
220  {
221  try
222  {
223  using (Py.GIL())
224  {
225  // try convert does not work for timespan
226  timeSpan = rebalance.As<TimeSpan>();
227  if (timeSpan != default(TimeSpan))
228  {
229  _rebalancingFunc = time => time.Add(timeSpan);
230  }
231  }
232  }
233  catch
234  {
235  _rebalancingFunc = null;
236  }
237  }
238  }
239 
240  /// <summary>
241  /// Determines if the portfolio should be rebalanced base on the provided rebalancing func,
242  /// if any security change have been taken place or if an insight has expired or a new insight arrived
243  /// If the rebalancing function has not been provided will return true.
244  /// </summary>
245  /// <param name="insights">The insights to create portfolio targets from</param>
246  /// <param name="algorithmUtc">The current algorithm UTC time</param>
247  /// <returns>True if should rebalance</returns>
248  protected virtual bool IsRebalanceDue(Insight[] insights, DateTime algorithmUtc)
249  {
250  // if there is no rebalance func set, just return true but refresh state
251  // just in case the rebalance func is going to be set.
252  if (_rebalancingFunc == null)
253  {
254  RefreshRebalance(algorithmUtc);
255  return true;
256  }
257 
258  // we always get the next expiry time
259  // we don't know if a new insight was added or removed
260  var nextInsightExpiryTime = Algorithm.Insights.GetNextExpiryTime();
261 
262  if (_rebalancingTime == null)
263  {
264  _rebalancingTime = _rebalancingFunc(algorithmUtc);
265 
266  if (_rebalancingTime != null && _rebalancingTime <= algorithmUtc)
267  {
268  // if the rebalancing time stopped being null and is current time
269  // we will ask for the next rebalance time in the next loop.
270  // we don't want to call the '_rebalancingFunc' twice in the same loop,
271  // since its internal state machine will probably be in the same state.
272  _rebalancingTime = null;
273  _securityChanges = false;
274  return true;
275  }
276  }
277 
278  if (_rebalancingTime != null && _rebalancingTime <= algorithmUtc
279  || RebalanceOnSecurityChanges && _securityChanges
281  && (insights.Length != 0
282  || nextInsightExpiryTime != null && nextInsightExpiryTime < algorithmUtc))
283  {
284  RefreshRebalance(algorithmUtc);
285  return true;
286  }
287 
288  return false;
289  }
290 
291  /// <summary>
292  /// Refresh the next rebalance time and clears the security changes flag
293  /// </summary>
294  protected void RefreshRebalance(DateTime algorithmUtc)
295  {
296  if (_rebalancingFunc != null)
297  {
298  _rebalancingTime = _rebalancingFunc(algorithmUtc);
299  }
300  _securityChanges = false;
301  }
302 
303  /// <summary>
304  /// Helper class that can be used by the different <see cref="IPortfolioConstructionModel"/>
305  /// implementations to filter <see cref="Insight"/> instances with an invalid
306  /// <see cref="Insight.Magnitude"/> value based on the <see cref="IAlgorithmSettings"/>
307  /// </summary>
308  /// <param name="algorithm">The algorithm instance</param>
309  /// <param name="insights">The insight collection to filter</param>
310  /// <returns>Returns a new array of insights removing invalid ones</returns>
311  protected static Insight[] FilterInvalidInsightMagnitude(IAlgorithm algorithm, Insight[] insights)
312  {
313  var result = insights.Where(insight =>
314  {
315  if (!insight.Magnitude.HasValue || insight.Magnitude == 0)
316  {
317  return true;
318  }
319 
320  var absoluteMagnitude = Math.Abs(insight.Magnitude.Value);
321  if (absoluteMagnitude > (double)algorithm.Settings.MaxAbsolutePortfolioTargetPercentage
322  || absoluteMagnitude < (double)algorithm.Settings.MinAbsolutePortfolioTargetPercentage)
323  {
324  algorithm.Error("PortfolioConstructionModel.FilterInvalidInsightMagnitude():" +
325  $"The insight target Magnitude: {insight.Magnitude}, will not comply with the current " +
326  $"'Algorithm.Settings' 'MaxAbsolutePortfolioTargetPercentage': {algorithm.Settings.MaxAbsolutePortfolioTargetPercentage}" +
327  $" or 'MinAbsolutePortfolioTargetPercentage': {algorithm.Settings.MinAbsolutePortfolioTargetPercentage}. Skipping insight."
328  );
329  return false;
330  }
331 
332  return true;
333  });
334  return result.ToArray();
335  }
336  }
337 }