Lean  $LEAN_TAG$
BasePairsTradingAlphaModel.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 QuantConnect.Data;
24 using static System.FormattableString;
25 
27 {
28  /// <summary>
29  /// This alpha model is designed to accept every possible pair combination
30  /// from securities selected by the universe selection model
31  /// This model generates alternating long ratio/short ratio insights emitted as a group
32  /// </summary>
34  {
35  private readonly int _lookback;
36  private readonly Resolution _resolution;
37  private readonly TimeSpan _predictionInterval;
38  private readonly decimal _threshold;
39  private readonly Dictionary<Tuple<Symbol, Symbol>, PairData> _pairs;
40 
41  /// <summary>
42  /// List of security objects present in the universe
43  /// </summary>
44  public HashSet<Security> Securities { get; }
45 
46  /// <summary>
47  /// Initializes a new instance of the <see cref="BasePairsTradingAlphaModel"/> class
48  /// </summary>
49  /// <param name="lookback">Lookback period of the analysis</param>
50  /// <param name="resolution">Analysis resolution</param>
51  /// <param name="threshold">The percent [0, 100] deviation of the ratio from the mean before emitting an insight</param>
53  int lookback = 1,
54  Resolution resolution = Resolution.Daily,
55  decimal threshold = 1m
56  )
57  {
58  _lookback = lookback;
59  _resolution = resolution;
60  _threshold = threshold;
61  _predictionInterval = _resolution.ToTimeSpan().Multiply(_lookback);
62  _pairs = new Dictionary<Tuple<Symbol, Symbol>, PairData>();
63 
64  Securities = new HashSet<Security>();
65  Name = Invariant($"{nameof(BasePairsTradingAlphaModel)}({_lookback},{_resolution},{_threshold.Normalize()})");
66  }
67 
68  /// <summary>
69  /// Updates this alpha model with the latest data from the algorithm.
70  /// This is called each time the algorithm receives data for subscribed securities
71  /// </summary>
72  /// <param name="algorithm">The algorithm instance</param>
73  /// <param name="data">The new data available</param>
74  /// <returns>The new insights generated</returns>
75  public override IEnumerable<Insight> Update(QCAlgorithm algorithm, Slice data)
76  {
77  var insights = new List<Insight>();
78 
79  foreach (var kvp in _pairs)
80  {
81  insights.AddRange(kvp.Value.GetInsightGroup());
82  }
83 
84  return insights;
85  }
86 
87  /// <summary>
88  /// Event fired each time the we add/remove securities from the data feed
89  /// </summary>
90  /// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
91  /// <param name="changes">The security additions and removals from the algorithm</param>
92  public override void OnSecuritiesChanged(QCAlgorithm algorithm, SecurityChanges changes)
93  {
95 
96  UpdatePairs(algorithm);
97 
98  // Remove pairs that has assets that were removed from the universe
99  foreach (var security in changes.RemovedSecurities)
100  {
101  var symbol = security.Symbol;
102  var keys = _pairs.Keys.Where(k => k.Item1 == symbol || k.Item2 == symbol).ToList();
103 
104  foreach (var key in keys)
105  {
106  var pair = _pairs[key];
107  pair.Dispose();
108  _pairs.Remove(key);
109  }
110  }
111  }
112 
113  /// <summary>
114  /// Check whether the assets pass a pairs trading test
115  /// </summary>
116  /// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
117  /// <param name="asset1">The first asset's symbol in the pair</param>
118  /// <param name="asset2">The second asset's symbol in the pair</param>
119  /// <returns>True if the statistical test for the pair is successful</returns>
120  public virtual bool HasPassedTest(QCAlgorithm algorithm, Symbol asset1, Symbol asset2) => true;
121 
122  private void UpdatePairs(QCAlgorithm algorithm)
123  {
124  var assets = Securities.Select(x => x.Symbol).ToArray();
125 
126  for (var i = 0; i < assets.Length; i++)
127  {
128  var assetI = assets[i];
129 
130  for (var j = i + 1; j < assets.Length; j++)
131  {
132  var assetJ = assets[j];
133 
134  var pairSymbol = Tuple.Create(assetI, assetJ);
135  var invert = Tuple.Create(assetJ, assetI);
136 
137  if (_pairs.ContainsKey(pairSymbol) || _pairs.ContainsKey(invert))
138  {
139  continue;
140  }
141 
142  if (!HasPassedTest(algorithm, assetI, assetJ))
143  {
144  continue;
145  }
146 
147  var pairData = new PairData(algorithm, assetI, assetJ, _predictionInterval, _threshold);
148  _pairs.Add(pairSymbol, pairData);
149  }
150  }
151  }
152 
153  private class PairData : IDisposable
154  {
155  private enum State
156  {
157  ShortRatio,
158  FlatRatio,
159  LongRatio
160  };
161 
162  private State _state = State.FlatRatio;
163 
164  private readonly QCAlgorithm _algorithm;
165  private readonly Symbol _asset1;
166  private readonly Symbol _asset2;
167 
168  private readonly IDataConsolidator _identityConsolidator1;
169  private readonly IDataConsolidator _identityConsolidator2;
170 
171  private readonly IndicatorBase<IndicatorDataPoint> _asset1Price;
172  private readonly IndicatorBase<IndicatorDataPoint> _asset2Price;
173  private readonly IndicatorBase<IndicatorDataPoint> _ratio;
174  private readonly IndicatorBase<IndicatorDataPoint> _mean;
175  private readonly IndicatorBase<IndicatorDataPoint> _upperThreshold;
176  private readonly IndicatorBase<IndicatorDataPoint> _lowerThreshold;
177  private readonly TimeSpan _predictionInterval;
178 
179  /// <summary>
180  /// Create a new pair
181  /// </summary>
182  /// <param name="algorithm">The algorithm instance that experienced the change in securities</param>
183  /// <param name="asset1">The first asset's symbol in the pair</param>
184  /// <param name="asset2">The second asset's symbol in the pair</param>
185  /// <param name="period">Period over which this insight is expected to come to fruition</param>
186  /// <param name="threshold">The percent [0, 100] deviation of the ratio from the mean before emitting an insight</param>
187  public PairData(QCAlgorithm algorithm, Symbol asset1, Symbol asset2, TimeSpan period, decimal threshold)
188  {
189  _algorithm = algorithm;
190  _asset1 = asset1;
191  _asset2 = asset2;
192 
193  // Created the Identity indicator for a given Symbol and
194  // the consolidator it is registered to. The consolidator reference
195  // will be used to remove it from SubscriptionManager
196  (Identity, IDataConsolidator) CreateIdentityIndicator(Symbol symbol)
197  {
198  var resolution = algorithm.SubscriptionManager
199  .SubscriptionDataConfigService
200  .GetSubscriptionDataConfigs(symbol)
201  .Min(x => x.Resolution);
202 
203  var name = algorithm.CreateIndicatorName(symbol, "close", resolution);
204  var identity = new Identity(name);
205 
206  var consolidator = algorithm.ResolveConsolidator(symbol, resolution);
207  algorithm.RegisterIndicator(symbol, identity, consolidator);
208 
209  return (identity, consolidator);
210  }
211 
212  (_asset1Price, _identityConsolidator1) = CreateIdentityIndicator(asset1);
213  (_asset2Price, _identityConsolidator2) = CreateIdentityIndicator(asset2);
214 
215  _ratio = _asset1Price.Over(_asset2Price);
216  _mean = new ExponentialMovingAverage(500).Of(_ratio);
217 
218  var upper = new ConstantIndicator<IndicatorDataPoint>("ct", 1 + threshold / 100m);
219  _upperThreshold = _mean.Times(upper, "UpperThreshold");
220 
221  var lower = new ConstantIndicator<IndicatorDataPoint>("ct", 1 - threshold / 100m);
222  _lowerThreshold = _mean.Times(lower, "LowerThreshold");
223 
224  _predictionInterval = period;
225  }
226 
227  /// <summary>
228  /// On disposal, remove the consolidators from the subscription manager
229  /// </summary>
230  public void Dispose()
231  {
232  _algorithm.SubscriptionManager.RemoveConsolidator(_asset1, _identityConsolidator1);
233  _algorithm.SubscriptionManager.RemoveConsolidator(_asset2, _identityConsolidator2);
234  }
235 
236  /// <summary>
237  /// Gets the insights group for the pair
238  /// </summary>
239  /// <returns>Insights grouped by an unique group id</returns>
240  public IEnumerable<Insight> GetInsightGroup()
241  {
242  if (!_mean.IsReady)
243  {
244  return Enumerable.Empty<Insight>();
245  }
246 
247  // don't re-emit the same direction
248  if (_state != State.LongRatio && _ratio > _upperThreshold)
249  {
250  _state = State.LongRatio;
251 
252  // asset1/asset2 is more than 2 std away from mean, short asset1, long asset2
253  var shortAsset1 = Insight.Price(_asset1, _predictionInterval, InsightDirection.Down);
254  var longAsset2 = Insight.Price(_asset2, _predictionInterval, InsightDirection.Up);
255 
256  // creates a group id and set the GroupId property on each insight object
257  return Insight.Group(shortAsset1, longAsset2);
258  }
259 
260  // don't re-emit the same direction
261  if (_state != State.ShortRatio && _ratio < _lowerThreshold)
262  {
263  _state = State.ShortRatio;
264 
265  // asset1/asset2 is less than 2 std away from mean, long asset1, short asset2
266  var longAsset1 = Insight.Price(_asset1, _predictionInterval, InsightDirection.Up);
267  var shortAsset2 = Insight.Price(_asset2, _predictionInterval, InsightDirection.Down);
268 
269  // creates a group id and set the GroupId property on each insight object
270  return Insight.Group(longAsset1, shortAsset2);
271  }
272 
273  return Enumerable.Empty<Insight>();
274  }
275  }
276  }
277 }