Lean  $LEAN_TAG$
CorporateFactorProvider.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 
17 using System;
18 using System.Linq;
19 using QuantConnect.Util;
20 using QuantConnect.Logging;
23 using System.Collections.Generic;
24 
26 {
27  /// <summary>
28  /// Corporate related factor provider. Factors based on splits and dividends
29  /// </summary>
30  public class CorporateFactorProvider : FactorFile<CorporateFactorRow>
31  {
32  /// <summary>
33  ///Creates a new instance
34  /// </summary>
35  public CorporateFactorProvider(string permtick, IEnumerable<CorporateFactorRow> data, DateTime? factorFileMinimumDate = null) : base(permtick, data, factorFileMinimumDate)
36  {
37  }
38 
39  /// <summary>
40  /// Gets the price scale factor that includes dividend and split adjustments for the specified search date
41  /// </summary>
42  public override decimal GetPriceFactor(DateTime searchDate, DataNormalizationMode dataNormalizationMode, DataMappingMode? dataMappingMode = null, uint contractOffset = 0)
43  {
44  if (dataNormalizationMode == DataNormalizationMode.Raw)
45  {
46  return 0;
47  }
48 
49  var factor = 1m;
50 
51  for (var i = 0; i < ReversedFactorFileDates.Count; i++)
52  {
53  var factorDate = ReversedFactorFileDates[i];
54  if (factorDate.Date < searchDate.Date)
55  {
56  break;
57  }
58 
59  var factorFileRow = SortedFactorFileData[factorDate];
60  switch (dataNormalizationMode)
61  {
62  case DataNormalizationMode.TotalReturn:
63  case DataNormalizationMode.SplitAdjusted:
64  factor = factorFileRow.First().SplitFactor;
65  break;
66  case DataNormalizationMode.Adjusted:
67  case DataNormalizationMode.ScaledRaw:
68  factor = factorFileRow.First().PriceScaleFactor;
69  break;
70  default:
71  throw new ArgumentOutOfRangeException();
72  }
73  }
74 
75  return factor;
76  }
77 
78  /// <summary>
79  /// Gets price and split factors to be applied at the specified date
80  /// </summary>
81  public CorporateFactorRow GetScalingFactors(DateTime searchDate)
82  {
83  var factors = new CorporateFactorRow(searchDate, 1m, 1m, 0m);
84 
85  // Iterate backwards to find the most recent factors
86  foreach (var splitDate in ReversedFactorFileDates)
87  {
88  if (splitDate.Date < searchDate.Date) break;
89  factors = SortedFactorFileData[splitDate][0];
90  }
91 
92  return factors;
93  }
94 
95  /// <summary>
96  /// Returns true if the specified date is the last trading day before a dividend event
97  /// is to be fired
98  /// </summary>
99  /// <remarks>
100  /// NOTE: The dividend event in the algorithm should be fired at the end or AFTER
101  /// this date. This is the date in the file that a factor is applied, so for example,
102  /// MSFT has a 31 cent dividend on 2015.02.17, but in the factor file the factor is applied
103  /// to 2015.02.13, which is the first trading day BEFORE the actual effective date.
104  /// </remarks>
105  /// <param name="date">The date to check the factor file for a dividend event</param>
106  /// <param name="priceFactorRatio">When this function returns true, this value will be populated
107  /// with the price factor ratio required to scale the closing value (pf_i/pf_i+1)</param>
108  /// <param name="referencePrice">When this function returns true, this value will be populated
109  /// with the reference raw price, which is the close of the provided date</param>
110  public bool HasDividendEventOnNextTradingDay(DateTime date, out decimal priceFactorRatio, out decimal referencePrice)
111  {
112  priceFactorRatio = 0;
113  referencePrice = 0;
114  var index = SortedFactorFileData.IndexOfKey(date);
115  if (index > -1 && index < SortedFactorFileData.Count - 1)
116  {
117  // grab the next key to ensure it's a dividend event
118  var thisRow = SortedFactorFileData.Values[index].First();
119  var nextRow = SortedFactorFileData.Values[index + 1].First();
120 
121  // if the price factors have changed then it's a dividend event
122  if (thisRow.PriceFactor != nextRow.PriceFactor)
123  {
124  priceFactorRatio = thisRow.PriceFactor / nextRow.PriceFactor;
125  referencePrice = thisRow.ReferencePrice;
126  return true;
127  }
128  }
129  return false;
130  }
131 
132  /// <summary>
133  /// Returns true if the specified date is the last trading day before a split event
134  /// is to be fired
135  /// </summary>
136  /// <remarks>
137  /// NOTE: The split event in the algorithm should be fired at the end or AFTER this
138  /// date. This is the date in the file that a factor is applied, so for example MSFT
139  /// has a split on 1999.03.29, but in the factor file the split factor is applied on
140  /// 1999.03.26, which is the first trading day BEFORE the actual split date.
141  /// </remarks>
142  /// <param name="date">The date to check the factor file for a split event</param>
143  /// <param name="splitFactor">When this function returns true, this value will be populated
144  /// with the split factor ratio required to scale the closing value</param>
145  /// <param name="referencePrice">When this function returns true, this value will be populated
146  /// with the reference raw price, which is the close of the provided date</param>
147  public bool HasSplitEventOnNextTradingDay(DateTime date, out decimal splitFactor, out decimal referencePrice)
148  {
149  splitFactor = 1;
150  referencePrice = 0;
151  var index = SortedFactorFileData.IndexOfKey(date);
152  if (index > -1 && index < SortedFactorFileData.Count - 1)
153  {
154  // grab the next key to ensure it's a split event
155  var thisRow = SortedFactorFileData.Values[index].First();
156  var nextRow = SortedFactorFileData.Values[index + 1].First();
157 
158  // if the split factors have changed then it's a split event
159  if (thisRow.SplitFactor != nextRow.SplitFactor)
160  {
161  splitFactor = thisRow.SplitFactor / nextRow.SplitFactor;
162  referencePrice = thisRow.ReferencePrice;
163  return true;
164  }
165  }
166  return false;
167  }
168 
169  /// <summary>
170  /// Gets all of the splits and dividends represented by this factor file
171  /// </summary>
172  /// <param name="symbol">The symbol to ues for the dividend and split objects</param>
173  /// <param name="exchangeHours">Exchange hours used for resolving the previous trading day</param>
174  /// <param name="decimalPlaces">The number of decimal places to round the dividend's distribution to, defaulting to 2</param>
175  /// <returns>All splits and dividends represented by this factor file in chronological order</returns>
176  public List<BaseData> GetSplitsAndDividends(Symbol symbol, SecurityExchangeHours exchangeHours, int decimalPlaces = 2)
177  {
178  var dividendsAndSplits = new List<BaseData>();
179  if (SortedFactorFileData.Count == 0)
180  {
181  Log.Trace($"{symbol} has no factors!");
182  return dividendsAndSplits;
183  }
184 
185  var futureFactorFileRow = SortedFactorFileData.Last().Value.First();
186  for (var i = SortedFactorFileData.Count - 2; i >= 0; i--)
187  {
188  var row = SortedFactorFileData.Values[i].First();
189  var dividend = row.GetDividend(futureFactorFileRow, symbol, exchangeHours, decimalPlaces);
190  if (dividend.Distribution != 0m)
191  {
192  dividendsAndSplits.Add(dividend);
193  }
194 
195  var split = row.GetSplit(futureFactorFileRow, symbol, exchangeHours);
196  if (split.SplitFactor != 1m)
197  {
198  dividendsAndSplits.Add(split);
199  }
200 
201  futureFactorFileRow = row;
202  }
203 
204  return dividendsAndSplits.OrderBy(d => d.Time.Date).ToList();
205  }
206 
207  /// <summary>
208  /// Creates a new factor file with the specified data applied.
209  /// Only <see cref="Dividend"/> and <see cref="Split"/> data types
210  /// will be used.
211  /// </summary>
212  /// <param name="data">The data to apply</param>
213  /// <param name="exchangeHours">Exchange hours used for resolving the previous trading day</param>
214  /// <returns>A new factor file that incorporates the specified dividend</returns>
215  public CorporateFactorProvider Apply(List<BaseData> data, SecurityExchangeHours exchangeHours)
216  {
217  if (data.Count == 0)
218  {
219  return this;
220  }
221 
222  var factorFileRows = new List<CorporateFactorRow>();
223  var firstEntry = SortedFactorFileData.First().Value.First();
224  var lastEntry = SortedFactorFileData.Last().Value.First();
225  factorFileRows.Add(lastEntry);
226 
227  var splitsAndDividends = GetSplitsAndDividends(data[0].Symbol, exchangeHours);
228 
229  var combinedData = splitsAndDividends.Concat(data)
230  .DistinctBy(e => $"{e.GetType().Name}{e.Time.ToStringInvariant(DateFormat.EightCharacter)}")
231  .OrderByDescending(d => d.Time.Date);
232 
233  foreach (var datum in combinedData)
234  {
235  CorporateFactorRow nextEntry = null;
236  var split = datum as Split;
237  var dividend = datum as Dividend;
238  if (dividend != null)
239  {
240  nextEntry = lastEntry.Apply(dividend, exchangeHours);
241  lastEntry = nextEntry;
242  }
243  else if (split != null)
244  {
245  nextEntry = lastEntry.Apply(split, exchangeHours);
246  lastEntry = nextEntry;
247  }
248 
249  if (nextEntry != null)
250  {
251  // overwrite the latest entry -- this handles splits/dividends on the same date
252  if (nextEntry.Date == factorFileRows.Last().Date)
253  {
254  factorFileRows[factorFileRows.Count - 1] = nextEntry;
255  }
256  else
257  {
258  factorFileRows.Add(nextEntry);
259  }
260  }
261  }
262 
263  var firstFactorFileRow = new CorporateFactorRow(firstEntry.Date, factorFileRows.Last().PriceFactor, factorFileRows.Last().SplitFactor, firstEntry.ReferencePrice == 0 ? 0 : firstEntry.ReferencePrice);
264  var existing = factorFileRows.FindIndex(row => row.Date == firstFactorFileRow.Date);
265  if (existing == -1)
266  {
267  // only add it if not present
268  factorFileRows.Add(firstFactorFileRow);
269  }
270 
271  return new CorporateFactorProvider(Permtick, factorFileRows, FactorFileMinimumDate);
272  }
273  }
274 }