Lean  $LEAN_TAG$
MeanReversionPortfolioConstructionModel.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;
25 using QuantConnect.Util;
26 
28 {
29  /// <summary>
30  /// Implementation of On-Line Moving Average Reversion (OLMAR)
31  /// </summary>
32  /// <remarks>Li, B., Hoi, S. C. (2012). On-line portfolio selection with moving average reversion. arXiv preprint arXiv:1206.4626.
33  /// Available at https://arxiv.org/ftp/arxiv/papers/1206/1206.4626.pdf</remarks>
34  /// <remarks>Using windowSize = 1 => Passive Aggressive Mean Reversion (PAMR) Portfolio</remarks>
36  {
37  private int _numOfAssets;
38  private double[] _weightVector;
39  private decimal _reversionThreshold;
40  private int _windowSize;
41  private Resolution _resolution;
42  private Dictionary<Symbol, MeanReversionSymbolData> _symbolData = new();
43 
44  /// <summary>
45  /// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
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="reversionThreshold">Reversion threshold</param>
51  /// <param name="windowSize">Window size of mean price</param>
52  /// <param name="resolution">The resolution of the history price and rebalancing</param>
54  PortfolioBias portfolioBias = PortfolioBias.LongShort,
55  decimal reversionThreshold = 1,
56  int windowSize = 20,
57  Resolution resolution = Resolution.Daily)
58  : this(rebalancingDateRules.ToFunc(), portfolioBias, reversionThreshold, windowSize, resolution)
59  {
60  }
61 
62  /// <summary>
63  /// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
64  /// </summary>
65  /// <param name="rebalanceResolution">Rebalancing frequency</param>
66  /// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
67  /// <param name="reversionThreshold">Reversion threshold</param>
68  /// <param name="windowSize">Window size of mean price</param>
69  /// <param name="resolution">The resolution of the history price and rebalancing</param>
71  PortfolioBias portfolioBias = PortfolioBias.LongShort,
72  decimal reversionThreshold = 1,
73  int windowSize = 20,
74  Resolution resolution = Resolution.Daily)
75  : this(rebalanceResolution.ToTimeSpan(), portfolioBias, reversionThreshold, windowSize, resolution)
76  {
77  }
78 
79  /// <summary>
80  /// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
81  /// </summary>
82  /// <param name="timeSpan">Rebalancing frequency</param>
83  /// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
84  /// <param name="reversionThreshold">Reversion threshold</param>
85  /// <param name="windowSize">Window size of mean price</param>
86  /// <param name="resolution">The resolution of the history price and rebalancing</param>
87  public MeanReversionPortfolioConstructionModel(TimeSpan timeSpan,
88  PortfolioBias portfolioBias = PortfolioBias.LongShort,
89  decimal reversionThreshold = 1,
90  int windowSize = 20,
91  Resolution resolution = Resolution.Daily)
92  : this(dt => dt.Add(timeSpan), portfolioBias, reversionThreshold, windowSize, resolution)
93  {
94  }
95 
96  /// <summary>
97  /// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
98  /// </summary>
99  /// <param name="rebalance">Rebalancing func or if a date rule, timedelta will be converted into func.
100  /// For a given algorithm UTC DateTime the func returns the next expected rebalance time
101  /// or null if unknown, in which case the function will be called again in the next loop. Returning current time
102  /// will trigger rebalance. If null will be ignored</param>
103  /// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
104  /// <param name="reversionThreshold">Reversion threshold</param>
105  /// <param name="windowSize">Window size of mean price</param>
106  /// <param name="resolution">The resolution of the history price and rebalancing</param>
107  public MeanReversionPortfolioConstructionModel(PyObject rebalance,
108  PortfolioBias portfolioBias = PortfolioBias.LongShort,
109  decimal reversionThreshold = 1,
110  int windowSize = 20,
111  Resolution resolution = Resolution.Daily)
112  : this((Func<DateTime, DateTime?>)null, portfolioBias, reversionThreshold, windowSize, resolution)
113  {
114  SetRebalancingFunc(rebalance);
115  }
116 
117  /// <summary>
118  /// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
119  /// </summary>
120  /// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance UTC time.
121  /// Returning current time will trigger rebalance. If null will be ignored</param>
122  /// will trigger rebalance. If null will be ignored</param>
123  /// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
124  /// <param name="reversionThreshold">Reversion threshold</param>
125  /// <param name="windowSize">Window size of mean price</param>
126  /// <param name="resolution">The resolution of the history price and rebalancing</param>
127  public MeanReversionPortfolioConstructionModel(Func<DateTime, DateTime> rebalancingFunc,
128  PortfolioBias portfolioBias = PortfolioBias.LongShort,
129  decimal reversionThreshold = 1,
130  int windowSize = 20,
131  Resolution resolution = Resolution.Daily)
132  : this(rebalancingFunc != null ? (Func<DateTime, DateTime?>)(timeUtc => rebalancingFunc(timeUtc)) : null,
133  portfolioBias, reversionThreshold, windowSize, resolution)
134  {
135  }
136 
137  /// <summary>
138  /// Initializes a new instance of the <see cref="MeanReversionPortfolioConstructionModel"/> class
139  /// </summary>
140  /// <param name="rebalancingFunc">For a given algorithm UTC DateTime returns the next expected rebalance time
141  /// or null if unknown, in which case the function will be called again in the next loop. Returning current time
142  /// will trigger rebalance.</param>
143  /// <param name="portfolioBias">Specifies the bias of the portfolio (Short, Long/Short, Long)</param>
144  /// <param name="reversionThreshold">Reversion threshold</param>
145  /// <param name="windowSize">Window size of mean price</param>
146  /// <param name="resolution">The resolution of the history price and rebalancing</param>
147  public MeanReversionPortfolioConstructionModel(Func<DateTime, DateTime?> rebalancingFunc,
148  PortfolioBias portfolioBias = PortfolioBias.LongShort,
149  decimal reversionThreshold = 1,
150  int windowSize = 20,
151  Resolution resolution = Resolution.Daily)
152  : base(rebalancingFunc)
153  {
154  if (portfolioBias == PortfolioBias.Short)
155  {
156  throw new ArgumentException("Long position must be allowed in MeanReversionPortfolioConstructionModel.");
157  }
158 
159  _reversionThreshold = reversionThreshold;
160  _resolution = resolution;
161  _windowSize = windowSize;
162  }
163 
164  /// <summary>
165  /// Will determine the target percent for each insight
166  /// </summary>
167  /// <param name="activeInsights">list of active insights</param>
168  /// <return>dictionary of insight and respective target weight</return>
169  protected override Dictionary<Insight, double> DetermineTargetPercent(List<Insight> activeInsights)
170  {
171  var targets = new Dictionary<Insight, double>();
172 
173  // If we have no insights or non-ready just return an empty target list
174  if (activeInsights.IsNullOrEmpty() ||
175  !activeInsights.All(x => _symbolData[x.Symbol].IsReady()))
176  {
177  return targets;
178  }
179 
180  var numOfAssets = activeInsights.Count;
181  if (_numOfAssets != numOfAssets)
182  {
183  _numOfAssets = numOfAssets;
184  // Initialize price vector and portfolio weightings vector
185  _weightVector = Enumerable.Repeat((double) 1/_numOfAssets, _numOfAssets).ToArray();
186  }
187 
188  // Get price relatives vs expected price (SMA)
189  var priceRelatives = GetPriceRelatives(activeInsights); // \tilde{x}_{t+1}
190 
191  // Get step size of next portfolio
192  // \bar{x}_{t+1} = 1^T * \tilde{x}_{t+1} / m
193  // \lambda_{t+1} = max( 0, ( b_t * \tilde{x}_{t+1} - \epsilon ) / ||\tilde{x}_{t+1} - \bar{x}_{t+1} * 1|| ^ 2 )
194  var nextPrediction = priceRelatives.Average(); // \bar{x}_{t+1}
195  var assetsMeanDev = priceRelatives.Select(x => x - nextPrediction).ToArray();
196  var secondNorm = Math.Pow(assetsMeanDev.Euclidean(), 2);
197  double stepSize; // \lambda_{t+1}
198 
199  if (secondNorm == 0d)
200  {
201  stepSize = 0d;
202  }
203  else
204  {
205  stepSize = (_weightVector.InnerProduct(priceRelatives) - (double)_reversionThreshold) / secondNorm;
206  stepSize = Math.Max(0d, stepSize);
207  }
208 
209  // Get next portfolio weightings
210  // b_{t+1} = b_t - step_size * ( \tilde{x}_{t+1} - \bar{x}_{t+1} * 1 )
211  var nextPortfolio = _weightVector.Select((x, i) => x - assetsMeanDev[i] * stepSize);
212  // Normalize
213  var normalizedPortfolioWeightVector = SimplexProjection(nextPortfolio);
214  // Save normalized result for the next portfolio step
215  _weightVector = normalizedPortfolioWeightVector;
216 
217  // Update portfolio state
218  for (int i = 0; i < _numOfAssets; i++)
219  {
220  targets.Add(activeInsights[i], normalizedPortfolioWeightVector[i]);
221  }
222 
223  return targets;
224  }
225 
226  /// <summary>
227  /// Get price relatives with reference level of SMA
228  /// </summary>
229  /// <param name="activeInsights">list of active insights</param>
230  /// <return>array of price relatives vector</return>
231  protected virtual double[] GetPriceRelatives(List<Insight> activeInsights)
232  {
233  var numOfInsights = activeInsights.Count;
234 
235  // Initialize a price vector of the next prices relatives' projection
236  var nextPriceRelatives = new double[numOfInsights];
237 
238  for (int i = 0; i < numOfInsights; i++)
239  {
240  var insight = activeInsights[i];
241  var symbolData = _symbolData[insight.Symbol];
242 
243  nextPriceRelatives[i] = insight.Magnitude != null ?
244  1 + (double)insight.Magnitude * (int)insight.Direction:
245  (double)symbolData.Identity.Current.Value / (double)symbolData.Sma.Current.Value;
246  }
247 
248  return nextPriceRelatives;
249  }
250 
251  /// <summary>
252  /// Event fired each time the we add/remove securities from the data feed
253  /// </summary>
254  /// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
255  /// <param name="changes">The security additions and removals from the algorithm</param>
256  public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
257  {
258  base.OnSecuritiesChanged(algorithm, changes);
259 
260  // clean up data for removed securities
261  foreach (var removed in changes.RemovedSecurities)
262  {
263  _symbolData.Remove(removed.Symbol, out var symbolData);
264  symbolData.Reset();
265  }
266 
267  // initialize data for added securities
268  var symbols = changes.AddedSecurities.Select(x => x.Symbol);
269 
270  foreach(var symbol in symbols)
271  {
272  if (!_symbolData.ContainsKey(symbol))
273  {
274  _symbolData.Add(symbol, new MeanReversionSymbolData(algorithm, symbol, _windowSize, _resolution));
275  }
276  }
277  }
278 
279  /// <summary>
280  /// Cumulative Sum of a given sequence
281  /// </summary>
282  /// <param name="sequence">sequence to obtain cumulative sum</param>
283  /// <return>cumulative sum</return>
284  public static IEnumerable<double> CumulativeSum(IEnumerable<double> sequence)
285  {
286  double sum = 0;
287  foreach(var item in sequence)
288  {
289  sum += item;
290  yield return sum;
291  }
292  }
293 
294  /// <summary>
295  /// Normalize the updated portfolio into weight vector:
296  /// v_{t+1} = arg min || v - v_{t+1} || ^ 2
297  /// </summary>
298  /// <remark>Duchi, J., Shalev-Shwartz, S., Singer, Y., and Chandra, T. (2008, July).
299  /// Efficient projections onto the l1-ball for learning in high dimensions.
300  /// In Proceedings of the 25th international conference on Machine learning (pp. 272-279).</remark>
301  /// <param name="vector">unnormalized weight vector</param>
302  /// <param name="total">regulator, default to be 1, making it a probabilistic simplex</param>
303  /// <return>normalized weight vector</return>
304  public static double[] SimplexProjection(IEnumerable<double> vector, double total = 1)
305  {
306  if (total <= 0)
307  {
308  throw new ArgumentException("Total must be > 0 for Euclidean Projection onto the Simplex.");
309  }
310 
311  // Sort v into u in descending order
312  var mu = vector.OrderByDescending(x => x).ToArray();
313  var sv = CumulativeSum(mu).ToArray();
314 
315  var rho = Enumerable.Range(0, vector.Count()).Where(i => mu[i] > (sv[i] - total) / (i+1)).Last();
316  var theta = (sv[rho] - total) / (rho + 1);
317  var w = vector.Select(x => Math.Max(x - theta, 0d)).ToArray();
318  return w;
319  }
320 
321  private class MeanReversionSymbolData
322  {
323  public Identity Identity;
324  public SimpleMovingAverage Sma;
325 
326  public MeanReversionSymbolData(QCAlgorithm algo, Symbol symbol, int windowSize, Resolution resolution)
327  {
328  // Indicator of price
329  Identity = algo.Identity(symbol, resolution);
330  // Moving average indicator for mean reversion level
331  Sma = algo.SMA(symbol, windowSize, resolution);
332 
333  // Warmup indicator
334  algo.WarmUpIndicator(symbol, Identity, resolution);
335  algo.WarmUpIndicator(symbol, Sma, resolution);
336  }
337 
338  public void Reset()
339  {
340  Identity.Reset();
341  Sma.Reset();
342  }
343 
344  public bool IsReady()
345  {
346  return (Identity.IsReady & Sma.IsReady);
347  }
348  }
349  }
350 }