Lean  $LEAN_TAG$
CorporateFactorRow.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 System.Globalization;
22 using System.Collections.Generic;
23 using static QuantConnect.StringExtensions;
24 
26 {
27  /// <summary>
28  /// Defines a single row in a factor_factor file. This is a csv file ordered as {date, price factor, split factor, reference price}
29  /// </summary>
31  {
32  private decimal _splitFactor;
33  private decimal _priceFactor;
34 
35  /// <summary>
36  /// Gets the date associated with this data
37  /// </summary>
38  public DateTime Date { get; private set; }
39 
40  /// <summary>
41  /// Gets the price factor associated with this data
42  /// </summary>
43  public decimal PriceFactor
44  {
45  get
46  {
47  return _priceFactor;
48 
49  }
50  set
51  {
52  _priceFactor = value;
53  UpdatePriceScaleFactor();
54  }
55  }
56 
57  /// <summary>
58  /// Gets the split factor associated with the date
59  /// </summary>
60  public decimal SplitFactor
61  {
62  get
63  {
64  return _splitFactor;
65  }
66  set
67  {
68  _splitFactor = value;
69  UpdatePriceScaleFactor();
70  }
71  }
72 
73  /// <summary>
74  /// Gets the combined factor used to create adjusted prices from raw prices
75  /// </summary>
76  public decimal PriceScaleFactor { get; private set; }
77 
78  /// <summary>
79  /// Gets the raw closing value from the trading date before the updated factor takes effect
80  /// </summary>
81  public decimal ReferencePrice { get; private set; }
82 
83  /// <summary>
84  /// Initializes a new instance of the <see cref="CorporateFactorRow"/> class
85  /// </summary>
86  public CorporateFactorRow(DateTime date, decimal priceFactor, decimal splitFactor, decimal referencePrice = 0)
87  {
88  Date = date;
89  ReferencePrice = referencePrice;
90  PriceFactor = priceFactor;
91  SplitFactor = splitFactor;
92  }
93 
94  /// <summary>
95  /// Parses the lines as factor files rows while properly handling inf entries
96  /// </summary>
97  /// <param name="lines">The lines from the factor file to be parsed</param>
98  /// <param name="factorFileMinimumDate">The minimum date from the factor file</param>
99  /// <returns>An enumerable of factor file rows</returns>
100  public static List<CorporateFactorRow> Parse(IEnumerable<string> lines, out DateTime? factorFileMinimumDate)
101  {
102  factorFileMinimumDate = null;
103 
104  var rows = new List<CorporateFactorRow>();
105 
106  // parse factor file lines
107  foreach (var line in lines)
108  {
109  // Exponential notation is treated as inf is because of the loss of precision. In
110  // all cases, the significant part has fewer decimals than the needed for a correct
111  // representation, E.g., 1.6e+6 when the correct factor is 1562500.
112  if (line.Contains("inf") || line.Contains("e+"))
113  {
114  continue;
115  }
116 
117  var row = Parse(line);
118 
119  // ignore zero factor rows
120  if (row.PriceScaleFactor > 0)
121  {
122  rows.Add(row);
123  }
124  }
125 
126  if (rows.Count > 0)
127  {
128  factorFileMinimumDate = rows.Min(ffr => ffr.Date).AddDays(-1);
129  }
130 
131  return rows;
132  }
133 
134  /// <summary>
135  /// Applies the dividend to this factor file row.
136  /// This dividend date must be on or before the factor
137  /// file row date
138  /// </summary>
139  /// <param name="dividend">The dividend to apply with reference price and distribution specified</param>
140  /// <param name="exchangeHours">Exchange hours used for resolving the previous trading day</param>
141  /// <returns>A new factor file row that applies the dividend to this row's factors</returns>
142  public CorporateFactorRow Apply(Dividend dividend, SecurityExchangeHours exchangeHours)
143  {
144  if (dividend.ReferencePrice == 0m)
145  {
146  throw new ArgumentException("Unable to apply dividend with reference price of zero.");
147  }
148 
149  var previousTradingDay = exchangeHours.GetPreviousTradingDay(dividend.Time);
150 
151  // this instance must be chronologically at or in front of the dividend
152  // this is because the factors are defined working from current to past
153  if (Date < previousTradingDay)
154  {
155  throw new ArgumentException(Invariant(
156  $"Factor file row date '{Date:yyy-MM-dd}' is before dividend previous trading date '{previousTradingDay.Date:yyyy-MM-dd}'."
157  ));
158  }
159 
160  // pfi - new price factor pf(i+1) - this price factor D - distribution C - previous close
161  // pfi = pf(i+1) * (C-D)/C
162  var priceFactor = PriceFactor * (dividend.ReferencePrice - dividend.Distribution) / dividend.ReferencePrice;
163 
164  return new CorporateFactorRow(
165  previousTradingDay,
166  priceFactor,
167  SplitFactor,
168  dividend.ReferencePrice
169  );
170  }
171 
172  /// <summary>
173  /// Applies the split to this factor file row.
174  /// This split date must be on or before the factor
175  /// file row date
176  /// </summary>
177  /// <param name="split">The split to apply with reference price and split factor specified</param>
178  /// <param name="exchangeHours">Exchange hours used for resolving the previous trading day</param>
179  /// <returns>A new factor file row that applies the split to this row's factors</returns>
181  {
182  if (split.Type == SplitType.Warning)
183  {
184  throw new ArgumentException("Unable to apply split with type warning. Only actual splits may be applied");
185  }
186 
187  if (split.ReferencePrice == 0m)
188  {
189  throw new ArgumentException("Unable to apply split with reference price of zero.");
190  }
191 
192  var previousTradingDay = exchangeHours.GetPreviousTradingDay(split.Time);
193 
194  // this instance must be chronologically at or in front of the split
195  // this is because the factors are defined working from current to past
196  if (Date < previousTradingDay)
197  {
198  throw new ArgumentException(Invariant(
199  $"Factor file row date '{Date:yyy-MM-dd}' is before split date '{split.Time.Date:yyyy-MM-dd}'."
200  ));
201  }
202 
203  return new CorporateFactorRow(
204  previousTradingDay,
205  PriceFactor,
206  SplitFactor * split.SplitFactor,
207  split.ReferencePrice
208  );
209  }
210 
211  /// <summary>
212  /// Creates a new dividend from this factor file row and the one chronologically in front of it
213  /// This dividend may have a distribution of zero if this row doesn't represent a dividend
214  /// </summary>
215  /// <param name="nextCorporateFactorRow">The next factor file row in time</param>
216  /// <param name="symbol">The symbol to use for the dividend</param>
217  /// <param name="exchangeHours">Exchange hours used for resolving the previous trading day</param>
218  /// <param name="decimalPlaces">The number of decimal places to round the dividend's distribution to, defaulting to 2</param>
219  /// <returns>A new dividend instance</returns>
220  public Dividend GetDividend(CorporateFactorRow nextCorporateFactorRow, Symbol symbol, SecurityExchangeHours exchangeHours, int decimalPlaces=2)
221  {
222  if (nextCorporateFactorRow.PriceFactor == 0m)
223  {
224  throw new InvalidOperationException(Invariant(
225  $"Unable to resolve dividend for '{symbol.ID}' at {Date:yyyy-MM-dd}. Price factor is zero."
226  ));
227  }
228 
229  // find previous trading day
230  var previousTradingDay = exchangeHours.GetNextTradingDay(Date);
231 
232  return Dividend.Create(
233  symbol,
234  previousTradingDay,
236  PriceFactor / nextCorporateFactorRow.PriceFactor,
237  decimalPlaces
238  );
239  }
240 
241  /// <summary>
242  /// Creates a new split from this factor file row and the one chronologically in front of it
243  /// This split may have a split factor of one if this row doesn't represent a split
244  /// </summary>
245  /// <param name="nextCorporateFactorRow">The next factor file row in time</param>
246  /// <param name="symbol">The symbol to use for the split</param>
247  /// <param name="exchangeHours">Exchange hours used for resolving the previous trading day</param>
248  /// <returns>A new split instance</returns>
249  public Split GetSplit(CorporateFactorRow nextCorporateFactorRow, Symbol symbol, SecurityExchangeHours exchangeHours)
250  {
251  if (nextCorporateFactorRow.SplitFactor == 0m)
252  {
253  throw new InvalidOperationException(Invariant(
254  $"Unable to resolve split for '{symbol.ID}' at {Date:yyyy-MM-dd}. Split factor is zero."
255  ));
256  }
257 
258  // find previous trading day
259  var previousTradingDay = exchangeHours.GetNextTradingDay(Date);
260 
261  return new Split(
262  symbol,
263  previousTradingDay,
265  SplitFactor / nextCorporateFactorRow.SplitFactor,
266  SplitType.SplitOccurred
267  );
268  }
269 
270  /// <summary>
271  /// Parses the specified line as a factor file row
272  /// </summary>
273  private static CorporateFactorRow Parse(string line)
274  {
275  var csv = line.Split(',');
276  return new CorporateFactorRow(
277  QuantConnect.Parse.DateTimeExact(csv[0], DateFormat.EightCharacter, DateTimeStyles.None),
278  QuantConnect.Parse.Decimal(csv[1]),
279  QuantConnect.Parse.Decimal(csv[2]),
280  csv.Length > 3 ? QuantConnect.Parse.Decimal(csv[3]) : 0m
281  );
282  }
283 
284  /// <summary>
285  /// Writes factor file row into it's file format
286  /// </summary>
287  /// <remarks>CSV formatted</remarks>
288  public string GetFileFormat(string source = null)
289  {
290  source = source == null ? "" : $",{source}";
291  return $"{Date.ToStringInvariant(DateFormat.EightCharacter)}," +
292  Invariant($"{Math.Round(PriceFactor, 7)},") +
293  Invariant($"{Math.Round(SplitFactor, 8)},") +
294  Invariant($"{Math.Round(ReferencePrice, 4).Normalize()}") +
295  $"{source}";
296  }
297 
298  /// <summary>
299  /// Returns a string that represents the current object.
300  /// </summary>
301  /// <returns>
302  /// A string that represents the current object.
303  /// </returns>
304  /// <filterpriority>2</filterpriority>
305  public override string ToString()
306  {
307  return Invariant($"{Date:yyyy-MM-dd}: {PriceScaleFactor:0.0000} {SplitFactor:0.0000}");
308  }
309 
310  /// <summary>
311  /// For performance we update <see cref="PriceScaleFactor"/> when underlying
312  /// values are updated to avoid decimal multiplication on each get operation.
313  /// </summary>
314  private void UpdatePriceScaleFactor()
315  {
316  PriceScaleFactor = _priceFactor * _splitFactor;
317  }
318  }
319 }