Lean  $LEAN_TAG$
FactorFileGenerator.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 QuantConnect.Data;
19 using System;
20 using System.Collections.Generic;
21 using System.Collections.ObjectModel;
22 using System.IO;
23 using System.IO.Compression;
24 using System.Linq;
25 
26 namespace QuantConnect.ToolBox
27 {
28  /// <summary>
29  /// Generates a factor file from a list of splits and dividends for a specified equity
30  /// </summary>
31  public class FactorFileGenerator
32  {
33  /// <summary>
34  /// Data for this equity at daily resolution
35  /// </summary>
36  private readonly List<TradeBar> _dailyDataForEquity;
37 
38  /// <summary>
39  /// The last date in the _dailyEquityData
40  /// </summary>
41  private readonly DateTime _lastDateFromEquityData;
42 
43  /// <summary>
44  /// The symbol for which the factor file is being generated
45  /// </summary>
46  public Symbol Symbol { get; set; }
47 
48  /// <summary>
49  /// Constructor for the FactorFileGenerator
50  /// </summary>
51  /// <param name="symbol">The equity for which the factor file respresents</param>
52  /// <param name="pathForDailyEquityData">The path to the daily data for the specified equity</param>
53  public FactorFileGenerator(Symbol symbol, string pathForDailyEquityData)
54  {
55  Symbol = symbol;
56  _dailyDataForEquity = ReadDailyEquityData(pathForDailyEquityData);
57  _lastDateFromEquityData = _dailyDataForEquity.Last().Time;
58  }
59 
60  /// <summary>
61  /// Create FactorFile instance
62  /// </summary>
63  /// <param name="dividendSplitList">List of Dividends and Splits</param>
64  /// <returns><see cref="FactorFile"/> instance</returns>
65  public CorporateFactorProvider CreateFactorFile(List<BaseData> dividendSplitList)
66  {
67  var orderedDividendSplitQueue = new Queue<BaseData>(
68  CombineIntraDayDividendSplits(dividendSplitList)
69  .OrderByDescending(x => x.Time));
70 
71  var factorFileRows = new List<CorporateFactorRow>
72  {
73  // First Factor Row is set far into the future and by definition has 1 for both price and split factors
76  priceFactor: 1,
77  splitFactor: 1
78  )
79  };
80 
81  return RecursivlyGenerateFactorFile(orderedDividendSplitQueue, factorFileRows);
82  }
83 
84  /// <summary>
85  /// If dividend and split occur on the same day,
86  /// combine them into IntraDayDividendSplit object
87  /// </summary>
88  /// <param name="splitDividendList">List of split and dividends</param>
89  /// <returns>A list of splits, dividends with intraday split and dividends combined into <see cref="IntraDayDividendSplit"/></returns>
90  private static List<BaseData> CombineIntraDayDividendSplits(List<BaseData> splitDividendList)
91  {
92  var splitDividendCollection = new Collection<BaseData>(splitDividendList);
93 
94  var dateKeysLookup = splitDividendCollection.GroupBy(x => x.Time)
95  .OrderByDescending(x => x.Key)
96  .Select(group => group)
97  .ToList();
98 
99  var baseDataList = new List<BaseData>();
100  foreach (var kvpLookup in dateKeysLookup)
101  {
102  if (kvpLookup.Count() > 1)
103  {
104  // Intraday dividend split found
105  var dividend = kvpLookup.First(x => x.GetType() == typeof(Dividend)) as Dividend;
106  var split = kvpLookup.First(x => x.GetType() == typeof(Split)) as Split;
107  baseDataList.Add(new IntraDayDividendSplit(split, dividend));
108  }
109  else
110  {
111  baseDataList.Add(kvpLookup.First());
112  }
113  }
114 
115  return baseDataList;
116  }
117 
118  /// <summary>
119  /// Recursively generate a <see cref="FactorFile"/>
120  /// </summary>
121  /// <param name="orderedDividendSplits">Queue of dividends and splits ordered by date</param>
122  /// <param name="factorFileRows">The list of factor file rows</param>
123  /// <returns><see cref="FactorFile"/> instance</returns>
124  private CorporateFactorProvider RecursivlyGenerateFactorFile(Queue<BaseData> orderedDividendSplits, List<CorporateFactorRow> factorFileRows)
125  {
126  // If there is no more dividends or splits, return
127  if (!orderedDividendSplits.Any())
128  {
129  factorFileRows.Add(CreateLastFactorFileRow(factorFileRows, _dailyDataForEquity.Last().Close));
130  return new CorporateFactorProvider(Symbol.ID.Symbol, factorFileRows);
131  }
132 
133  var nextEvent = orderedDividendSplits.Dequeue();
134 
135  // If there is no more daily equity data to use, return
136  if (_lastDateFromEquityData > nextEvent.Time)
137  {
138  decimal initialReferencePrice = 1;
139  factorFileRows.Add(CreateLastFactorFileRow(factorFileRows, initialReferencePrice));
140  return new CorporateFactorProvider(Symbol.ID.Symbol, factorFileRows);
141  }
142 
143  var nextFactorFileRow = CalculateNextFactorFileRow(factorFileRows, nextEvent);
144 
145  if (nextFactorFileRow != null)
146  factorFileRows.Add(nextFactorFileRow);
147 
148  return RecursivlyGenerateFactorFile(orderedDividendSplits, factorFileRows);
149  }
150 
151  /// <summary>
152  /// Create the last FileFactorRow.
153  /// Represents the earliest date that the daily equity data contains.
154  /// </summary>
155  /// <param name="factorFileRows">The list of factor file rows</param>
156  /// <returns><see cref="CorporateFactorRow"/></returns>
157  private CorporateFactorRow CreateLastFactorFileRow(List<CorporateFactorRow> factorFileRows, decimal referencePrice)
158  {
159  return new CorporateFactorRow(
160  _dailyDataForEquity.Last().Time.Date,
161  factorFileRows.Last().PriceFactor,
162  factorFileRows.Last().SplitFactor,
163  referencePrice
164  );
165  }
166 
167  /// <summary>
168  /// Calculates the next <see cref="CorporateFactorRow"/>
169  /// </summary>
170  /// <param name="factorFileRows">The current list of factorFileRows</param>
171  /// <param name="nextEvent">The next dividend, split or intradayDividendSplit</param>
172  /// <returns>A single factor file row</returns>
173  private CorporateFactorRow CalculateNextFactorFileRow(List<CorporateFactorRow> factorFileRows, BaseData nextEvent)
174  {
175  CorporateFactorRow nextCorporateFactorRow;
176  var t = nextEvent.GetType();
177 
178  switch (t.Name)
179  {
180  case "Dividend":
181  nextCorporateFactorRow = CalculateNextDividendFactor(nextEvent, factorFileRows.Last());
182  break;
183  case "Split":
184  nextCorporateFactorRow = CalculateNextSplitFactor(nextEvent, factorFileRows.Last());
185  break;
186  case "IntraDayDividendSplit":
187  nextCorporateFactorRow = CalculateIntradayDividendSplit((IntraDayDividendSplit)nextEvent, factorFileRows.Last());
188  break;
189  default:
190  throw new ArgumentException("Unhandled BaseData type for FactorFileGenerator.");
191  }
192 
193  return nextCorporateFactorRow;
194  }
195 
196  /// <summary>
197  /// Generates the <see cref="CorporateFactorRow"/> that represents a intraday dividend split.
198  /// Applies the dividend first.
199  /// </summary>
200  /// <param name="intraDayDividendSplit"><see cref="IntraDayDividendSplit"/> instance that holds the intraday dividend and split information</param>
201  /// <param name="last">The last <see cref="CorporateFactorRow"/> generated recursivly</param>
202  /// <returns><see cref="CorporateFactorRow"/> that represents an intraday dividend and split</returns>
203  private CorporateFactorRow CalculateIntradayDividendSplit(IntraDayDividendSplit intraDayDividendSplit, CorporateFactorRow last)
204  {
205  var row = CalculateNextDividendFactor(intraDayDividendSplit.Dividend, last);
206  return CalculateNextSplitFactor(intraDayDividendSplit.Split, row);
207  }
208 
209  /// <summary>
210  /// Calculates the price factor of a <see cref="Dividend"/>
211  /// </summary>
212  /// <param name="dividend">The next dividend</param>
213  /// <param name="previousCorporateFactorRow">The previous <see cref="CorporateFactorRow"/> generated</param>
214  /// <returns><see cref="CorporateFactorRow"/> that represents the dividend event</returns>
215  private CorporateFactorRow CalculateNextDividendFactor(BaseData dividend, CorporateFactorRow previousCorporateFactorRow)
216  {
217  var eventDayData = GetDailyDataForDate(dividend.Time);
218 
219  // If you don't have the equity data nothing can be calculated
220  if (eventDayData == null)
221  {
222  return null;
223  }
224 
225  TradeBar previousClosingPrice = FindPreviousTradableDayClosingPrice(eventDayData.Time);
226 
227  // adjust the dividend for both price and split factors (!)
228  var priceFactor = previousCorporateFactorRow.PriceFactor *
229  (1 - dividend.Value * previousCorporateFactorRow.SplitFactor / previousClosingPrice.Close);
230 
231  return new CorporateFactorRow(
232  previousClosingPrice.Time,
233  priceFactor.RoundToSignificantDigits(7),
234  previousCorporateFactorRow.SplitFactor,
235  previousClosingPrice.Close
236  );
237  }
238 
239  /// <summary>
240  /// Calculates the split factor of a <see cref="Split"/>
241  /// </summary>
242  /// <param name="split">The next <see cref="Split"/></param>
243  /// <param name="previousCorporateFactorRow">The previous <see cref="CorporateFactorRow"/> generated</param>
244  /// <returns><see cref="CorporateFactorRow"/> that represents the split event</returns>
245  private CorporateFactorRow CalculateNextSplitFactor(BaseData split, CorporateFactorRow previousCorporateFactorRow)
246  {
247  var eventDayData = GetDailyDataForDate(split.Time);
248 
249  // If you don't have the equity data nothing can be done
250  if (eventDayData == null)
251  {
252  return null;
253  }
254 
255  TradeBar previousClosingPrice = FindPreviousTradableDayClosingPrice(eventDayData.Time);
256 
257  return new CorporateFactorRow(
258  previousClosingPrice.Time,
259  previousCorporateFactorRow.PriceFactor,
260  (previousCorporateFactorRow.SplitFactor / split.Value).RoundToSignificantDigits(6),
261  previousClosingPrice.Close
262  );
263  }
264 
265  /// <summary>
266  /// Gets the data for a specified date
267  /// </summary>
268  /// <param name="date">The current specified date</param>
269  /// <returns><see cref="TradeBar"/>representing that date</returns>
270  private TradeBar GetDailyDataForDate(DateTime date)
271  {
272  return _dailyDataForEquity.FirstOrDefault(x => x.Time.Date == date.Date);
273  }
274 
275  /// <summary>
276  /// Gets the data for the previous tradable day
277  /// </summary>
278  /// <param name="date">The current specified date</param>
279  /// <returns>The last tradeble days data</returns>
280  private TradeBar FindPreviousTradableDayClosingPrice(DateTime date)
281  {
282  TradeBar previousDayData = null;
283  var lastDateforData = _dailyDataForEquity.Last();
284 
285  while (previousDayData == null && date > lastDateforData.EndTime)
286  {
287  previousDayData = _dailyDataForEquity.FirstOrDefault(x => x.Time == date.AddDays(-1));
288  date = date.AddDays(-1);
289  }
290 
291  return previousDayData;
292  }
293 
294  /// <summary>
295  /// Read the daily equity date from file
296  /// </summary>
297  /// <param name="pathForDailyEquityData">Path the the daily data</param>
298  /// <returns>A list of <see cref="TradeBar"/> read from file</returns>
299  private static List<TradeBar> ReadDailyEquityData(string pathForDailyEquityData)
300  {
301  var dataReader = new LeanDataReader(pathForDailyEquityData);
302  var bars = dataReader.Parse();
303  return bars.OrderByDescending(x => x.Time)
304  .Select(x => (TradeBar)x)
305  .ToList();
306  }
307 
308  /// <summary>
309  /// Pairs split and dividend data into one type
310  /// </summary>
311  private class IntraDayDividendSplit : BaseData
312  {
313  public Split Split { get; }
314  public Dividend Dividend { get; }
315 
316  public IntraDayDividendSplit(Split split, Dividend dividend)
317  {
318  if (split == null)
319  {
320  throw new ArgumentNullException(nameof(split));
321  }
322 
323  if (dividend == null)
324  {
325  throw new ArgumentNullException(nameof(dividend));
326  }
327 
328  Split = split;
329  Dividend = dividend;
330  Time = Split.Time;
331  }
332  }
333  }
334 }