Lean  $LEAN_TAG$
RandomDataGenerator.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;
20 using System;
21 using System.Collections.Generic;
22 using System.Linq;
24 using QuantConnect.Logging;
25 
27 {
28  /// <summary>
29  /// Generates random data according to the specified parameters
30  /// </summary>
31  public class RandomDataGenerator
32  {
33  private RandomDataGeneratorSettings _settings;
34  private SecurityManager _securityManager;
35 
36  /// <summary>
37  /// Initializes <see cref="RandomDataGenerator"/> instance fields
38  /// </summary>
39  /// <param name="settings">random data generation settings</param>
40  /// <param name="securityManager">security management</param>
41  public void Init(RandomDataGeneratorSettings settings, SecurityManager securityManager)
42  {
43  _settings = settings;
44  _securityManager = securityManager;
45  }
46 
47  /// <summary>
48  /// Starts data generation
49  /// </summary>
50  public void Run()
51  {
52  var tickTypesPerSecurityType = SubscriptionManager.DefaultDataTypes();
53  // can specify a seed value in this ctor if determinism is desired
54  var random = new Random();
55  var randomValueGenerator = new RandomValueGenerator();
56  if (_settings.RandomSeedSet)
57  {
58  random = new Random(_settings.RandomSeed);
59  randomValueGenerator = new RandomValueGenerator(_settings.RandomSeed);
60  }
61 
62  var symbolGenerator = BaseSymbolGenerator.Create(_settings, randomValueGenerator);
63 
64  var maxSymbolCount = symbolGenerator.GetAvailableSymbolCount();
65  if (_settings.SymbolCount > maxSymbolCount)
66  {
67  Log.Error($"RandomDataGenerator.Run(): Limiting Symbol count to {maxSymbolCount}, we don't have more {_settings.SecurityType} tickers for {_settings.Market}");
68  _settings.SymbolCount = maxSymbolCount;
69  }
70 
71  Log.Trace($"RandomDataGenerator.Run(): Begin data generation of {_settings.SymbolCount} randomly generated {_settings.SecurityType} assets...");
72 
73  // iterate over our randomly generated symbols
74  var count = 0;
75  var progress = 0d;
76  var previousMonth = -1;
77 
78  foreach (var (symbolRef, currentSymbolGroup) in symbolGenerator.GenerateRandomSymbols()
79  .GroupBy(s => s.HasUnderlying ? s.Underlying : s)
80  .Select(g => (g.Key, g.OrderBy(s => s.HasUnderlying).ToList())))
81  {
82  Log.Trace($"RandomDataGenerator.Run(): Symbol[{++count}]: {symbolRef} Progress: {progress:0.0}% - Generating data...");
83 
84  var tickGenerators = new List<IEnumerator<Tick>>();
85  var tickHistories = new Dictionary<Symbol, List<Tick>>();
86  Security underlyingSecurity = null;
87  foreach (var currentSymbol in currentSymbolGroup)
88  {
89  if (!_securityManager.TryGetValue(currentSymbol, out var security))
90  {
91  security = _securityManager.CreateSecurity(
92  currentSymbol,
93  new List<SubscriptionDataConfig>(),
94  underlying: underlyingSecurity);
95  _securityManager.Add(security);
96  }
97 
98  underlyingSecurity ??= security;
99 
100  tickGenerators.Add(
101  new TickGenerator(_settings, tickTypesPerSecurityType[currentSymbol.SecurityType].ToArray(), security, randomValueGenerator)
102  .GenerateTicks()
103  .GetEnumerator());
104 
105  tickHistories.Add(
106  currentSymbol,
107  new List<Tick>());
108  }
109 
110  using var sync = new SynchronizingBaseDataEnumerator(tickGenerators);
111  while (sync.MoveNext())
112  {
113  var dataPoint = sync.Current;
114  if (!_securityManager.TryGetValue(dataPoint.Symbol, out var security))
115  {
116  Log.Error($"RandomDataGenerator.Run(): Could not find security for symbol {sync.Current.Symbol}");
117  continue;
118  }
119 
120  tickHistories[security.Symbol].Add(dataPoint as Tick);
121  security.Update(new List<BaseData> { dataPoint }, dataPoint.GetType(), false);
122  }
123 
124  foreach (var (currentSymbol, tickHistory) in tickHistories)
125  {
126  var symbol = currentSymbol;
127 
128  // This is done so that we can update the Symbol in the case of a rename event
129  var delistDate = GetDelistingDate(_settings.Start, _settings.End, randomValueGenerator);
130  var willBeDelisted = randomValueGenerator.NextBool(1.0);
131 
132  // Companies rarely IPO then disappear within 6 months
133  if (willBeDelisted && tickHistory.Select(tick => tick.Time.Month).Distinct().Count() <= 6)
134  {
135  willBeDelisted = false;
136  }
137 
138  var dividendsSplitsMaps = new DividendSplitMapGenerator(
139  symbol,
140  _settings,
141  randomValueGenerator,
142  symbolGenerator,
143  random,
144  delistDate,
145  willBeDelisted);
146 
147  // Keep track of renamed symbols and the time they were renamed.
148  var renamedSymbols = new Dictionary<Symbol, DateTime>();
149 
150  if (_settings.SecurityType == SecurityType.Equity)
151  {
152  dividendsSplitsMaps.GenerateSplitsDividends(tickHistory);
153 
154  if (!willBeDelisted)
155  {
156  dividendsSplitsMaps.DividendsSplits.Add(new CorporateFactorRow(new DateTime(2050, 12, 31), 1m, 1m));
157 
158  if (dividendsSplitsMaps.MapRows.Count > 1)
159  {
160  // Remove the last element if we're going to have a 20501231 entry
161  dividendsSplitsMaps.MapRows.RemoveAt(dividendsSplitsMaps.MapRows.Count - 1);
162  }
163  dividendsSplitsMaps.MapRows.Add(new MapFileRow(new DateTime(2050, 12, 31), dividendsSplitsMaps.CurrentSymbol.Value));
164  }
165 
166  // If the Symbol value has changed, update the current Symbol
167  if (symbol != dividendsSplitsMaps.CurrentSymbol)
168  {
169  // Add all Symbol rename events to dictionary
170  // We skip the first row as it contains the listing event instead of a rename event
171  foreach (var renameEvent in dividendsSplitsMaps.MapRows.Skip(1))
172  {
173  // Symbol.UpdateMappedSymbol does not update the underlying security ID Symbol, which
174  // is used to create the hash code. Create a new equity Symbol from scratch instead.
175  symbol = Symbol.Create(renameEvent.MappedSymbol, SecurityType.Equity, _settings.Market);
176  renamedSymbols.Add(symbol, renameEvent.Date);
177 
178  Log.Trace($"RandomDataGenerator.Run(): Symbol[{count}]: {symbol} will be renamed on {renameEvent.Date}");
179  }
180  }
181  else
182  {
183  // This ensures that ticks will be written for the current Symbol up until 9999-12-31
184  renamedSymbols.Add(symbol, new DateTime(9999, 12, 31));
185  }
186 
187  symbol = dividendsSplitsMaps.CurrentSymbol;
188 
189  // Write Splits and Dividend events to directory factor_files
190  var factorFile = new CorporateFactorProvider(symbol.Value, dividendsSplitsMaps.DividendsSplits, _settings.Start);
191  var mapFile = new MapFile(symbol.Value, dividendsSplitsMaps.MapRows);
192 
193  factorFile.WriteToFile(symbol);
194  mapFile.WriteToCsv(_settings.Market, symbol.SecurityType);
195 
196  Log.Trace($"RandomDataGenerator.Run(): Symbol[{count}]: {symbol} Dividends, splits, and map files have been written to disk.");
197  }
198  else
199  {
200  // This ensures that ticks will be written for the current Symbol up until 9999-12-31
201  renamedSymbols.Add(symbol, new DateTime(9999, 12, 31));
202  }
203 
204  // define aggregators via settings
205  var aggregators = CreateAggregators(_settings, tickTypesPerSecurityType[currentSymbol.SecurityType].ToArray()).ToList();
206  Symbol previousSymbol = null;
207  var currentCount = 0;
208  var monthsTrading = 0;
209 
210  foreach (var renamed in renamedSymbols)
211  {
212  var previousRenameDate = previousSymbol == null ? new DateTime(1, 1, 1) : renamedSymbols[previousSymbol];
213  var previousRenameDateDay = new DateTime(previousRenameDate.Year, previousRenameDate.Month, previousRenameDate.Day);
214  var renameDate = renamed.Value;
215  var renameDateDay = new DateTime(renameDate.Year, renameDate.Month, renameDate.Day);
216 
217  foreach (var tick in tickHistory.Where(tick => tick.Time >= previousRenameDate && previousRenameDateDay != TickDay(tick)))
218  {
219  // Prevents the aggregator from being updated with ticks after the rename event
220  if (TickDay(tick) > renameDateDay)
221  {
222  break;
223  }
224 
225  if (tick.Time.Month != previousMonth)
226  {
227  Log.Trace($"RandomDataGenerator.Run(): Symbol[{count}]: Month: {tick.Time:MMMM}");
228  previousMonth = tick.Time.Month;
229  monthsTrading++;
230  }
231 
232  foreach (var item in aggregators)
233  {
234  tick.Value = tick.Value / dividendsSplitsMaps.FinalSplitFactor;
235  item.Consolidator.Update(tick);
236  }
237 
238  if (monthsTrading >= 6 && willBeDelisted && tick.Time > delistDate)
239  {
240  Log.Trace($"RandomDataGenerator.Run(): Symbol[{count}]: {renamed.Key} delisted at {tick.Time:MMMM yyyy}");
241  break;
242  }
243  }
244 
245  // count each stage as a point, so total points is 2*Symbol-count
246  // and the current progress is twice the current, but less one because we haven't finished writing data yet
247  progress = 100 * (2 * count - 1) / (2.0 * _settings.SymbolCount);
248 
249  Log.Trace($"RandomDataGenerator.Run(): Symbol[{count}]: {renamed.Key} Progress: {progress:0.0}% - Saving data in LEAN format");
250 
251  // persist consolidated data to disk
252  foreach (var item in aggregators)
253  {
254  var writer = new LeanDataWriter(item.Resolution, renamed.Key, Globals.DataFolder, item.TickType);
255 
256  // send the flushed data into the writer. pulling the flushed list is very important,
257  // lest we likely wouldn't get the last piece of data stuck in the consolidator
258  // Filter out the data we're going to write here because filtering them in the consolidator update phase
259  // makes it write all dates for some unknown reason
260  writer.Write(item.Flush().Where(data => data.Time > previousRenameDate && previousRenameDateDay != DataDay(data)));
261  }
262 
263  // update progress
264  progress = 100 * (2 * count) / (2.0 * _settings.SymbolCount);
265  Log.Trace($"RandomDataGenerator.Run(): Symbol[{count}]: {symbol} Progress: {progress:0.0}% - Symbol data generation and output completed");
266 
267  previousSymbol = renamed.Key;
268  currentCount++;
269  }
270  }
271  }
272 
273  Log.Trace("RandomDataGenerator.Run(): Random data generation has completed.");
274 
275  DateTime TickDay(Tick tick) => new(tick.Time.Year, tick.Time.Month, tick.Time.Day);
276  DateTime DataDay(BaseData data) => new(data.Time.Year, data.Time.Month, data.Time.Day);
277  }
278 
279  public static DateTime GetDateMidpoint(DateTime start, DateTime end)
280  {
281  TimeSpan span = end.Subtract(start);
282  int span_time = (int)span.TotalMinutes;
283  double diff_span = -(span_time / 2.0);
284  DateTime start_time = end.AddMinutes(Math.Round(diff_span, 2, MidpointRounding.ToEven));
285 
286  //Returns a DateTime object that is halfway between start and end
287  return start_time;
288  }
289 
290  public static DateTime GetDelistingDate(DateTime start, DateTime end, RandomValueGenerator randomValueGenerator)
291  {
292  var mid_point = GetDateMidpoint(start, end);
293  var delist_Date = randomValueGenerator.NextDate(mid_point, end, null);
294 
295  //Returns a DateTime object that is a random value between the mid_point and end
296  return delist_Date;
297  }
298 
299  public static IEnumerable<TickAggregator> CreateAggregators(RandomDataGeneratorSettings settings, TickType[] tickTypes)
300  {
301  // create default aggregators for tick type/resolution
302  foreach (var tickAggregator in TickAggregator.ForTickTypes(settings.SecurityType, settings.Resolution, tickTypes))
303  {
304  yield return tickAggregator;
305  }
306 
307 
308  // ensure we have a daily consolidator when coarse is enabled
309  if (settings.IncludeCoarse && settings.Resolution != Resolution.Daily)
310  {
311  // prefer trades for coarse - in practice equity only does trades, but leaving this as configurable
312  if (tickTypes.Contains(TickType.Trade))
313  {
314  yield return TickAggregator.ForTickTypes(settings.SecurityType, Resolution.Daily, TickType.Trade).Single();
315  }
316  else
317  {
318  yield return TickAggregator.ForTickTypes(settings.SecurityType, Resolution.Daily, TickType.Quote).Single();
319  }
320  }
321  }
322  }
323 }