Lean  $LEAN_TAG$
DividendSplitMapGenerator.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;
21 
23 {
24  /// <summary>
25  /// Generates random splits, random dividends, and map file
26  /// </summary>
28  {
29  private const double _minimumFinalSplitFactorAllowed = 0.001;
30 
31  /// <summary>
32  /// The final factor to adjust all prices with in order to maintain price continuity.
33  /// </summary>
34  /// <remarks>
35  /// Set default equal to 1 so that we can use it even in the event of no splits
36  /// </remarks>
37  public decimal FinalSplitFactor { get; set; } = 1m;
38 
39  /// <summary>
40  /// Stores <see cref="MapFileRow"/> instances
41  /// </summary>
42  public List<MapFileRow> MapRows { get; set; } = new();
43 
44  /// <summary>
45  /// Stores <see cref="CorporateFactorRow"/> instances
46  /// </summary>
47  public List<CorporateFactorRow> DividendsSplits { get; set; } = new List<CorporateFactorRow>();
48 
49  /// <summary>
50  /// Current Symbol value. Can be renamed
51  /// </summary>
52  public Symbol CurrentSymbol { get; private set; }
53 
54  private readonly RandomValueGenerator _randomValueGenerator;
55  private readonly Random _random;
56  private readonly RandomDataGeneratorSettings _settings;
57  private readonly DateTime _delistDate;
58  private readonly bool _willBeDelisted;
59  private readonly BaseSymbolGenerator _symbolGenerator;
60 
62  Symbol symbol,
64  RandomValueGenerator randomValueGenerator,
65  BaseSymbolGenerator symbolGenerator,
66  Random random,
67  DateTime delistDate,
68  bool willBeDelisted)
69  {
70  CurrentSymbol = symbol;
71  _settings = settings;
72  _randomValueGenerator = randomValueGenerator;
73  _random = random;
74  _delistDate = delistDate;
75  _willBeDelisted = willBeDelisted;
76  _symbolGenerator = symbolGenerator;
77  }
78 
79  /// <summary>
80  /// Generates the splits, dividends, and maps.
81  /// Writes necessary output to public variables
82  /// </summary>
83  /// <param name="tickHistory"></param>
84  public void GenerateSplitsDividends(IEnumerable<Tick> tickHistory)
85  {
86  var previousMonth = -1;
87  var monthsTrading = 0;
88 
89  var hasRename = _randomValueGenerator.NextBool(_settings.HasRenamePercentage);
90  var hasSplits = _randomValueGenerator.NextBool(_settings.HasSplitsPercentage);
91  var hasDividends = _randomValueGenerator.NextBool(_settings.HasDividendsPercentage);
92  var dividendEveryQuarter = _randomValueGenerator.NextBool(_settings.DividendEveryQuarterPercentage);
93 
94  var previousX = _random.NextDouble();
95 
96  // Since the largest equity value we can obtain is 1 000 000, if we want this price divided by the FinalSplitFactor
97  // to be upper bounded by 1 000 000 000 we need to make sure the FinalSplitFactor is lower bounded by 0.001. Therefore,
98  // since in the worst of the cases FinalSplitFactor = (previousSplitFactor)^(2m), where m is the number of months
99  // in the time span, we need to lower bound previousSplitFactor by (0.001)^(1/(2m))
100  //
101  // On the other hand, if the upper bound for the previousSplitFactor is 1, then the FinalSplitFactor will be, in the
102  // worst of the cases as small as the minimum equity value we can obtain
103 
104  var months = (int)Math.Round(_settings.End.Subtract(_settings.Start).Days / (365.25 / 12));
105  months = months != 0 ? months : 1;
106  var minPreviousSplitFactor = GetLowerBoundForPreviousSplitFactor(months);
107  var maxPreviousSplitFactor = 1;
108  var previousSplitFactor = hasSplits ? GetNextPreviousSplitFactor(_random, minPreviousSplitFactor, maxPreviousSplitFactor) : 1;
109  var previousPriceFactor = hasDividends ? (decimal)Math.Tanh(previousX) : 1;
110 
111  var splitDates = new List<DateTime>();
112  var dividendDates = new List<DateTime>();
113 
114  var firstTick = true;
115 
116  // Iterate through all ticks and generate splits and dividend data
117  if (_settings.SecurityType == SecurityType.Equity)
118  {
119  foreach (var tick in tickHistory)
120  {
121  // On the first trading day write relevant starting data to factor and map files
122  if (firstTick)
123  {
124  DividendsSplits.Add(new CorporateFactorRow(tick.Time,
125  previousPriceFactor,
126  previousSplitFactor,
127  tick.Value));
128 
129  MapRows.Add(new MapFileRow(tick.Time, CurrentSymbol.Value));
130  }
131 
132  // Add the split to the DividendsSplits list if we have a pending
133  // split. That way, we can use the correct referencePrice in the split event.
134  if (splitDates.Count != 0)
135  {
136  var deleteDates = new List<DateTime>();
137 
138  foreach (var splitDate in splitDates)
139  {
140  if (tick.Time > splitDate)
141  {
143  splitDate,
144  previousPriceFactor,
145  previousSplitFactor,
146  tick.Value / FinalSplitFactor));
147 
148  FinalSplitFactor *= previousSplitFactor;
149  deleteDates.Add(splitDate);
150  }
151  }
152 
153  // Deletes dates we've already looped over
154  splitDates.RemoveAll(x => deleteDates.Contains(x));
155  }
156 
157  if (dividendDates.Count != 0)
158  {
159  var deleteDates = new List<DateTime>();
160 
161  foreach (var dividendDate in dividendDates)
162  {
163  if (tick.Time > dividendDate)
164  {
166  dividendDate,
167  previousPriceFactor,
168  previousSplitFactor,
169  tick.Value / FinalSplitFactor));
170 
171  deleteDates.Add(dividendDate);
172  }
173  }
174 
175  dividendDates.RemoveAll(x => deleteDates.Contains(x));
176  }
177 
178  if (tick.Time.Month != previousMonth)
179  {
180  // Every quarter, try to generate dividend events
181  if (hasDividends && (tick.Time.Month - 1) % 3 == 0)
182  {
183  // Make it so there's a 10% chance that dividends occur if there is no dividend every quarter
184  if (dividendEveryQuarter || _randomValueGenerator.NextBool(10.0))
185  {
186  do
187  {
188  previousX += _random.NextDouble() / 10;
189  previousPriceFactor = (decimal)Math.Tanh(previousX);
190  } while (previousPriceFactor >= 1.0m || previousPriceFactor <= 0m);
191 
192  dividendDates.Add(_randomValueGenerator.NextDate(tick.Time, tick.Time.AddMonths(1), (DayOfWeek)_random.Next(1, 5)));
193  }
194  }
195  // Have a 5% chance of a split every month
196  if (hasSplits && _randomValueGenerator.NextBool(_settings.MonthSplitPercentage))
197  {
198  // Produce another split factor that is also bounded by the min and max split factors allowed
199  if (_randomValueGenerator.NextBool(5.0)) // Add the possibility of a reverse split
200  {
201  // A reverse split is a split that is smaller than the current previousSplitFactor
202  // Update previousSplitFactor with a smaller value that is still bounded below by minPreviousSplitFactor
203  previousSplitFactor = GetNextPreviousSplitFactor(_random, minPreviousSplitFactor, previousSplitFactor);
204  }
205  else
206  {
207  // Update previousSplitFactor with a higher value that is still bounded by maxPreviousSplitFactor
208  // Usually, the split factor tends to grow across the time span(See /Data/Equity/usa/factor_files/aapl for instance)
209  previousSplitFactor = GetNextPreviousSplitFactor(_random, previousSplitFactor, maxPreviousSplitFactor);
210  }
211 
212  splitDates.Add(_randomValueGenerator.NextDate(tick.Time, tick.Time.AddMonths(1), (DayOfWeek)_random.Next(1, 5)));
213  }
214  // 10% chance of being renamed every month
215  if (hasRename && _randomValueGenerator.NextBool(10.0))
216  {
217  var randomDate = _randomValueGenerator.NextDate(tick.Time, tick.Time.AddMonths(1), (DayOfWeek)_random.Next(1, 5));
218  MapRows.Add(new MapFileRow(randomDate, CurrentSymbol.Value));
219 
220  CurrentSymbol = _symbolGenerator.NextSymbol(_settings.SecurityType, _settings.Market);
221  }
222 
223  previousMonth = tick.Time.Month;
224  monthsTrading++;
225  }
226 
227  if (monthsTrading >= 6 && _willBeDelisted && tick.Time > _delistDate)
228  {
229  MapRows.Add(new MapFileRow(tick.Time, CurrentSymbol.Value));
230  break;
231  }
232 
233  firstTick = false;
234  }
235  }
236  }
237 
238  /// <summary>
239  /// Gets a lower bound that guarantees the FinalSplitFactor, in all the possible
240  /// cases, will never be smaller than the _minimumFinalSplitFactorAllowed (0.001)
241  /// </summary>
242  /// <param name="months">The lower bound for the previous split factor is based on
243  /// the number of months between the start and end date from ticksHistory <see cref="GenerateSplitsDividends(IEnumerable{Tick})"></param>
244  /// <returns>A valid lower bound that guarantees the FinalSplitFactor is always higher
245  /// than the _minimumFinalSplitFactorAllowed</returns>
246  public static decimal GetLowerBoundForPreviousSplitFactor(int months)
247  {
248  return (decimal)(Math.Pow(_minimumFinalSplitFactorAllowed, 1 / (double)(2 * months)));
249  }
250 
251  /// <summary>
252  /// Gets a new valid previousSplitFactor that is still bounded by the given upper and lower
253  /// bounds
254  /// </summary>
255  /// <param name="random">Random number generator</param>
256  /// <param name="lowerBound">Minimum allowed value to obtain</param>
257  /// <param name="upperBound">Maximum allowed value to obtain</param>
258  /// <returns>A new valid previousSplitFactor that is still bounded by the given upper and lower
259  /// bounds</returns>
260  public static decimal GetNextPreviousSplitFactor(Random random, decimal lowerBound, decimal upperBound)
261  {
262  return ((decimal)random.NextDouble()) * (upperBound - lowerBound) + lowerBound;
263  }
264  }
265 }