Lean  $LEAN_TAG$
LeanData.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.Globalization;
19 using System.IO;
20 using System.Linq;
21 using System.Runtime.CompilerServices;
22 using NodaTime;
23 using QuantConnect.Data;
29 using QuantConnect.Logging;
33 using static QuantConnect.StringExtensions;
34 
35 namespace QuantConnect.Util
36 {
37  /// <summary>
38  /// Provides methods for generating lean data file content
39  /// </summary>
40  public static class LeanData
41  {
42  private static readonly HashSet<Type> _strictDailyEndTimesDataTypes = new()
43  {
44  // the underlying could yield auxiliary data which we don't want to change
45  typeof(TradeBar), typeof(QuoteBar), typeof(BaseDataCollection), typeof(OpenInterest)
46  };
47 
48  /// <summary>
49  /// The different <see cref="SecurityType"/> used for data paths
50  /// </summary>
51  /// <remarks>This includes 'alternative'</remarks>
52  public static HashSet<string> SecurityTypeAsDataPath => Enum.GetNames(typeof(SecurityType))
53  .Select(x => x.ToLowerInvariant()).Union(new[] { "alternative" }).ToHashSet();
54 
55  /// <summary>
56  /// Converts the specified base data instance into a lean data file csv line.
57  /// This method takes into account the fake that base data instances typically
58  /// are time stamped in the exchange time zone, but need to be written to disk
59  /// in the data time zone.
60  /// </summary>
61  public static string GenerateLine(IBaseData data, Resolution resolution, DateTimeZone exchangeTimeZone, DateTimeZone dataTimeZone)
62  {
63  var clone = data.Clone();
64  clone.Time = data.Time.ConvertTo(exchangeTimeZone, dataTimeZone);
65  return GenerateLine(clone, clone.Symbol.ID.SecurityType, resolution);
66  }
67 
68  /// <summary>
69  /// Helper method that will parse a given data line in search of an associated date time
70  /// </summary>
71  public static DateTime ParseTime(string line, DateTime date, Resolution resolution)
72  {
73  switch (resolution)
74  {
75  case Resolution.Tick:
76  case Resolution.Second:
77  case Resolution.Minute:
78  var index = line.IndexOf(',', StringComparison.InvariantCulture);
79  return date.AddTicks(Convert.ToInt64(10000 * decimal.Parse(line.AsSpan(0, index))));
80  case Resolution.Hour:
81  case Resolution.Daily:
82  return DateTime.ParseExact(line.AsSpan(0, DateFormat.TwelveCharacter.Length), DateFormat.TwelveCharacter, CultureInfo.InvariantCulture);
83  default:
84  throw new ArgumentOutOfRangeException(nameof(resolution), resolution, null);
85  }
86  }
87 
88  /// <summary>
89  /// Converts the specified base data instance into a lean data file csv line
90  /// </summary>
91  public static string GenerateLine(IBaseData data, SecurityType securityType, Resolution resolution)
92  {
93  var milliseconds = data.Time.TimeOfDay.TotalMilliseconds.ToString(CultureInfo.InvariantCulture);
94  var longTime = data.Time.ToStringInvariant(DateFormat.TwelveCharacter);
95 
96  switch (securityType)
97  {
98  case SecurityType.Equity:
99  switch (resolution)
100  {
101  case Resolution.Tick:
102  var tick = (Tick)data;
103  if (tick.TickType == TickType.Trade)
104  {
105  return ToCsv(milliseconds, Scale(tick.LastPrice), tick.Quantity, tick.ExchangeCode, tick.SaleCondition, tick.Suspicious ? "1" : "0");
106  }
107  if (tick.TickType == TickType.Quote)
108  {
109  return ToCsv(milliseconds, Scale(tick.BidPrice), tick.BidSize, Scale(tick.AskPrice), tick.AskSize, tick.ExchangeCode, tick.SaleCondition, tick.Suspicious ? "1" : "0");
110  }
111  break;
112  case Resolution.Minute:
113  case Resolution.Second:
114  var tradeBar = data as TradeBar;
115  if (tradeBar != null)
116  {
117  return ToCsv(milliseconds, Scale(tradeBar.Open), Scale(tradeBar.High), Scale(tradeBar.Low), Scale(tradeBar.Close), tradeBar.Volume);
118  }
119  var quoteBar = data as QuoteBar;
120  if (quoteBar != null)
121  {
122  return ToCsv(milliseconds,
123  ToScaledCsv(quoteBar.Bid), quoteBar.LastBidSize,
124  ToScaledCsv(quoteBar.Ask), quoteBar.LastAskSize);
125  }
126  break;
127 
128  case Resolution.Hour:
129  case Resolution.Daily:
130  var bigTradeBar = data as TradeBar;
131  if (bigTradeBar != null)
132  {
133  return ToCsv(longTime, Scale(bigTradeBar.Open), Scale(bigTradeBar.High), Scale(bigTradeBar.Low), Scale(bigTradeBar.Close), bigTradeBar.Volume);
134  }
135  var bigQuoteBar = data as QuoteBar;
136  if (bigQuoteBar != null)
137  {
138  return ToCsv(longTime,
139  ToScaledCsv(bigQuoteBar.Bid), bigQuoteBar.LastBidSize,
140  ToScaledCsv(bigQuoteBar.Ask), bigQuoteBar.LastAskSize);
141  }
142  break;
143  }
144  break;
145 
146  case SecurityType.Crypto:
147  case SecurityType.CryptoFuture:
148  switch (resolution)
149  {
150  case Resolution.Tick:
151  var tick = data as Tick;
152  if (tick == null)
153  {
154  throw new ArgumentException($"{securityType} tick could not be created", nameof(data));
155  }
156  if (tick.TickType == TickType.Trade)
157  {
158  return ToCsv(milliseconds, tick.LastPrice, tick.Quantity, tick.Suspicious ? "1" : "0");
159  }
160  if (tick.TickType == TickType.Quote)
161  {
162  return ToCsv(milliseconds, tick.BidPrice, tick.BidSize, tick.AskPrice, tick.AskSize, tick.Suspicious ? "1" : "0");
163  }
164  throw new ArgumentException($"{securityType} tick could not be created");
165  case Resolution.Second:
166  case Resolution.Minute:
167  var quoteBar = data as QuoteBar;
168  if (quoteBar != null)
169  {
170  return ToCsv(milliseconds,
171  ToNonScaledCsv(quoteBar.Bid), quoteBar.LastBidSize,
172  ToNonScaledCsv(quoteBar.Ask), quoteBar.LastAskSize);
173  }
174  var tradeBar = data as TradeBar;
175  if (tradeBar != null)
176  {
177  return ToCsv(milliseconds, tradeBar.Open, tradeBar.High, tradeBar.Low, tradeBar.Close, tradeBar.Volume);
178  }
179  throw new ArgumentException($"{securityType} minute/second bar could not be created", nameof(data));
180 
181  case Resolution.Hour:
182  case Resolution.Daily:
183  var bigQuoteBar = data as QuoteBar;
184  if (bigQuoteBar != null)
185  {
186  return ToCsv(longTime,
187  ToNonScaledCsv(bigQuoteBar.Bid), bigQuoteBar.LastBidSize,
188  ToNonScaledCsv(bigQuoteBar.Ask), bigQuoteBar.LastAskSize);
189  }
190  var bigTradeBar = data as TradeBar;
191  if (bigTradeBar != null)
192  {
193  return ToCsv(longTime,
194  bigTradeBar.Open,
195  bigTradeBar.High,
196  bigTradeBar.Low,
197  bigTradeBar.Close,
198  bigTradeBar.Volume);
199  }
200  throw new ArgumentException($"{securityType} hour/daily bar could not be created", nameof(data));
201  }
202  break;
203  case SecurityType.Forex:
204  case SecurityType.Cfd:
205  switch (resolution)
206  {
207  case Resolution.Tick:
208  var tick = data as Tick;
209  if (tick == null)
210  {
211  throw new ArgumentException("Expected data of type 'Tick'", nameof(data));
212  }
213  return ToCsv(milliseconds, tick.BidPrice, tick.AskPrice);
214 
215  case Resolution.Second:
216  case Resolution.Minute:
217  var bar = data as QuoteBar;
218  if (bar == null)
219  {
220  throw new ArgumentException("Expected data of type 'QuoteBar'", nameof(data));
221  }
222  return ToCsv(milliseconds,
223  ToNonScaledCsv(bar.Bid), bar.LastBidSize,
224  ToNonScaledCsv(bar.Ask), bar.LastAskSize);
225 
226  case Resolution.Hour:
227  case Resolution.Daily:
228  var bigBar = data as QuoteBar;
229  if (bigBar == null)
230  {
231  throw new ArgumentException("Expected data of type 'QuoteBar'", nameof(data));
232  }
233  return ToCsv(longTime,
234  ToNonScaledCsv(bigBar.Bid), bigBar.LastBidSize,
235  ToNonScaledCsv(bigBar.Ask), bigBar.LastAskSize);
236  }
237  break;
238 
239  case SecurityType.Index:
240  switch (resolution)
241  {
242  case Resolution.Tick:
243  var tick = (Tick)data;
244  return ToCsv(milliseconds, tick.LastPrice, tick.Quantity, string.Empty, string.Empty, "0");
245  case Resolution.Second:
246  case Resolution.Minute:
247  var bar = data as TradeBar;
248  if (bar == null)
249  {
250  throw new ArgumentException("Expected data of type 'TradeBar'", nameof(data));
251  }
252  return ToCsv(milliseconds, bar.Open, bar.High, bar.Low, bar.Close, bar.Volume);
253  case Resolution.Hour:
254  case Resolution.Daily:
255  var bigTradeBar = data as TradeBar;
256  return ToCsv(longTime, bigTradeBar.Open, bigTradeBar.High, bigTradeBar.Low, bigTradeBar.Close, bigTradeBar.Volume);
257  }
258  break;
259 
260  case SecurityType.Option:
261  case SecurityType.IndexOption:
262  switch (resolution)
263  {
264  case Resolution.Tick:
265  var tick = (Tick)data;
266  if (tick.TickType == TickType.Trade)
267  {
268  return ToCsv(milliseconds,
269  Scale(tick.LastPrice), tick.Quantity, tick.ExchangeCode, tick.SaleCondition, tick.Suspicious ? "1" : "0");
270  }
271  if (tick.TickType == TickType.Quote)
272  {
273  return ToCsv(milliseconds,
274  Scale(tick.BidPrice), tick.BidSize, Scale(tick.AskPrice), tick.AskSize, tick.ExchangeCode, tick.Suspicious ? "1" : "0");
275  }
276  if (tick.TickType == TickType.OpenInterest)
277  {
278  return ToCsv(milliseconds, tick.Value);
279  }
280  break;
281 
282  case Resolution.Second:
283  case Resolution.Minute:
284  // option and future data can be quote or trade bars
285  var quoteBar = data as QuoteBar;
286  if (quoteBar != null)
287  {
288  return ToCsv(milliseconds,
289  ToScaledCsv(quoteBar.Bid), quoteBar.LastBidSize,
290  ToScaledCsv(quoteBar.Ask), quoteBar.LastAskSize);
291  }
292  var tradeBar = data as TradeBar;
293  if (tradeBar != null)
294  {
295  return ToCsv(milliseconds,
296  Scale(tradeBar.Open), Scale(tradeBar.High), Scale(tradeBar.Low), Scale(tradeBar.Close), tradeBar.Volume);
297  }
298  var openInterest = data as OpenInterest;
299  if (openInterest != null)
300  {
301  return ToCsv(milliseconds, openInterest.Value);
302  }
303  break;
304 
305  case Resolution.Hour:
306  case Resolution.Daily:
307  // option and future data can be quote or trade bars
308  var bigQuoteBar = data as QuoteBar;
309  if (bigQuoteBar != null)
310  {
311  return ToCsv(longTime,
312  ToScaledCsv(bigQuoteBar.Bid), bigQuoteBar.LastBidSize,
313  ToScaledCsv(bigQuoteBar.Ask), bigQuoteBar.LastAskSize);
314  }
315  var bigTradeBar = data as TradeBar;
316  if (bigTradeBar != null)
317  {
318  return ToCsv(longTime, ToScaledCsv(bigTradeBar), bigTradeBar.Volume);
319  }
320  var bigOpenInterest = data as OpenInterest;
321  if (bigOpenInterest != null)
322  {
323  return ToCsv(longTime, bigOpenInterest.Value);
324  }
325  break;
326 
327  default:
328  throw new ArgumentOutOfRangeException(nameof(resolution), resolution, null);
329  }
330  break;
331 
332  case SecurityType.FutureOption:
333  switch (resolution)
334  {
335  case Resolution.Tick:
336  var tick = (Tick)data;
337  if (tick.TickType == TickType.Trade)
338  {
339  return ToCsv(milliseconds,
340  tick.LastPrice, tick.Quantity, tick.ExchangeCode, tick.SaleCondition, tick.Suspicious ? "1" : "0");
341  }
342  if (tick.TickType == TickType.Quote)
343  {
344  return ToCsv(milliseconds,
345  tick.BidPrice, tick.BidSize, tick.AskPrice, tick.AskSize, tick.ExchangeCode, tick.Suspicious ? "1" : "0");
346  }
347  if (tick.TickType == TickType.OpenInterest)
348  {
349  return ToCsv(milliseconds, tick.Value);
350  }
351  break;
352 
353  case Resolution.Second:
354  case Resolution.Minute:
355  // option and future data can be quote or trade bars
356  var quoteBar = data as QuoteBar;
357  if (quoteBar != null)
358  {
359  return ToCsv(milliseconds,
360  ToNonScaledCsv(quoteBar.Bid), quoteBar.LastBidSize,
361  ToNonScaledCsv(quoteBar.Ask), quoteBar.LastAskSize);
362  }
363  var tradeBar = data as TradeBar;
364  if (tradeBar != null)
365  {
366  return ToCsv(milliseconds,
367  tradeBar.Open, tradeBar.High, tradeBar.Low, tradeBar.Close, tradeBar.Volume);
368  }
369  var openInterest = data as OpenInterest;
370  if (openInterest != null)
371  {
372  return ToCsv(milliseconds, openInterest.Value);
373  }
374  break;
375 
376  case Resolution.Hour:
377  case Resolution.Daily:
378  // option and future data can be quote or trade bars
379  var bigQuoteBar = data as QuoteBar;
380  if (bigQuoteBar != null)
381  {
382  return ToCsv(longTime,
383  ToNonScaledCsv(bigQuoteBar.Bid), bigQuoteBar.LastBidSize,
384  ToNonScaledCsv(bigQuoteBar.Ask), bigQuoteBar.LastAskSize);
385  }
386  var bigTradeBar = data as TradeBar;
387  if (bigTradeBar != null)
388  {
389  return ToCsv(longTime, ToNonScaledCsv(bigTradeBar), bigTradeBar.Volume);
390  }
391  var bigOpenInterest = data as OpenInterest;
392  if (bigOpenInterest != null)
393  {
394  return ToCsv(longTime, bigOpenInterest.Value);
395  }
396  break;
397 
398  default:
399  throw new ArgumentOutOfRangeException(nameof(resolution), resolution, null);
400  }
401  break;
402 
403  case SecurityType.Future:
404  switch (resolution)
405  {
406  case Resolution.Tick:
407  var tick = (Tick)data;
408  if (tick.TickType == TickType.Trade)
409  {
410  return ToCsv(milliseconds,
411  tick.LastPrice, tick.Quantity, tick.ExchangeCode, tick.SaleCondition, tick.Suspicious ? "1" : "0");
412  }
413  if (tick.TickType == TickType.Quote)
414  {
415  return ToCsv(milliseconds,
416  tick.BidPrice, tick.BidSize, tick.AskPrice, tick.AskSize, tick.ExchangeCode, tick.Suspicious ? "1" : "0");
417  }
418  if (tick.TickType == TickType.OpenInterest)
419  {
420  return ToCsv(milliseconds, tick.Value);
421  }
422  break;
423 
424  case Resolution.Second:
425  case Resolution.Minute:
426  // option and future data can be quote or trade bars
427  var quoteBar = data as QuoteBar;
428  if (quoteBar != null)
429  {
430  return ToCsv(milliseconds,
431  ToNonScaledCsv(quoteBar.Bid), quoteBar.LastBidSize,
432  ToNonScaledCsv(quoteBar.Ask), quoteBar.LastAskSize);
433  }
434  var tradeBar = data as TradeBar;
435  if (tradeBar != null)
436  {
437  return ToCsv(milliseconds,
438  tradeBar.Open, tradeBar.High, tradeBar.Low, tradeBar.Close, tradeBar.Volume);
439  }
440  var openInterest = data as OpenInterest;
441  if (openInterest != null)
442  {
443  return ToCsv(milliseconds, openInterest.Value);
444  }
445  break;
446 
447  case Resolution.Hour:
448  case Resolution.Daily:
449  // option and future data can be quote or trade bars
450  var bigQuoteBar = data as QuoteBar;
451  if (bigQuoteBar != null)
452  {
453  return ToCsv(longTime,
454  ToNonScaledCsv(bigQuoteBar.Bid), bigQuoteBar.LastBidSize,
455  ToNonScaledCsv(bigQuoteBar.Ask), bigQuoteBar.LastAskSize);
456  }
457  var bigTradeBar = data as TradeBar;
458  if (bigTradeBar != null)
459  {
460  return ToCsv(longTime, ToNonScaledCsv(bigTradeBar), bigTradeBar.Volume);
461  }
462  var bigOpenInterest = data as OpenInterest;
463  if (bigOpenInterest != null)
464  {
465  return ToCsv(longTime, bigOpenInterest.Value);
466  }
467  break;
468 
469  default:
470  throw new ArgumentOutOfRangeException(nameof(resolution), resolution, null);
471  }
472  break;
473 
474  default:
475  throw new ArgumentOutOfRangeException(nameof(securityType), securityType, null);
476  }
477 
478  throw new NotImplementedException(Invariant(
479  $"LeanData.GenerateLine has not yet been implemented for security type: {securityType} at resolution: {resolution}"
480  ));
481  }
482 
483  /// <summary>
484  /// Gets the data type required for the specified combination of resolution and tick type
485  /// </summary>
486  /// <param name="resolution">The resolution, if Tick, the Type returned is always Tick</param>
487  /// <param name="tickType">The <see cref="TickType"/> that primarily dictates the type returned</param>
488  /// <returns>The Type used to create a subscription</returns>
489  public static Type GetDataType(Resolution resolution, TickType tickType)
490  {
491  if (resolution == Resolution.Tick) return typeof(Tick);
492  if (tickType == TickType.OpenInterest) return typeof(OpenInterest);
493  if (tickType == TickType.Quote) return typeof(QuoteBar);
494  return typeof(TradeBar);
495  }
496 
497 
498  /// <summary>
499  /// Determines if the Type is a 'common' type used throughout lean
500  /// This method is helpful in creating <see cref="SubscriptionDataConfig"/>
501  /// </summary>
502  /// <param name="baseDataType">The Type to check</param>
503  /// <returns>A bool indicating whether the type is of type <see cref="TradeBar"/>
504  /// <see cref="QuoteBar"/> or <see cref="OpenInterest"/></returns>
505  public static bool IsCommonLeanDataType(Type baseDataType)
506  {
507  if (baseDataType == typeof(Tick) ||
508  baseDataType == typeof(TradeBar) ||
509  baseDataType == typeof(QuoteBar) ||
510  baseDataType == typeof(OpenInterest))
511  {
512  return true;
513  }
514 
515  return false;
516  }
517 
518  /// <summary>
519  /// Helper method to determine if a configuration set is valid
520  /// </summary>
521  public static bool IsValidConfiguration(SecurityType securityType, Resolution resolution, TickType tickType)
522  {
523  if (securityType == SecurityType.Equity && (resolution == Resolution.Daily || resolution == Resolution.Hour))
524  {
525  return tickType != TickType.Quote;
526  }
527  return true;
528  }
529 
530  /// <summary>
531  /// Generates the full zip file path rooted in the <paramref name="dataDirectory"/>
532  /// </summary>
533  public static string GenerateZipFilePath(string dataDirectory, Symbol symbol, DateTime date, Resolution resolution, TickType tickType)
534  {
535  // we could call 'GenerateRelativeZipFilePath' but we don't to avoid an extra string & path combine we are doing to drop right away
536  return Path.Combine(dataDirectory, GenerateRelativeZipFileDirectory(symbol, resolution), GenerateZipFileName(symbol, date, resolution, tickType));
537  }
538 
539  /// <summary>
540  /// Generates the full zip file path rooted in the <paramref name="dataDirectory"/>
541  /// </summary>
542  public static string GenerateZipFilePath(string dataDirectory, string symbol, SecurityType securityType, string market, DateTime date, Resolution resolution)
543  {
544  return Path.Combine(dataDirectory, GenerateRelativeZipFilePath(symbol, securityType, market, date, resolution));
545  }
546 
547  /// <summary>
548  /// Generates the relative zip directory for the specified symbol/resolution
549  /// </summary>
550  public static string GenerateRelativeZipFileDirectory(Symbol symbol, Resolution resolution)
551  {
552  var isHourOrDaily = resolution == Resolution.Hour || resolution == Resolution.Daily;
553  var securityType = symbol.SecurityType.SecurityTypeToLower();
554 
555  var market = symbol.ID.Market.ToLowerInvariant();
556  var res = resolution.ResolutionToLower();
557  var directory = Path.Combine(securityType, market, res);
558  switch (symbol.ID.SecurityType)
559  {
560  case SecurityType.Base:
561  case SecurityType.Equity:
562  case SecurityType.Index:
563  case SecurityType.Forex:
564  case SecurityType.Cfd:
565  case SecurityType.Crypto:
566  return !isHourOrDaily ? Path.Combine(directory, symbol.Value.ToLowerInvariant()) : directory;
567 
568  case SecurityType.IndexOption:
569  // For index options, we use the canonical option ticker since it can differ from the underlying's ticker.
570  return !isHourOrDaily ? Path.Combine(directory, symbol.ID.Symbol.ToLowerInvariant()) : directory;
571 
572  case SecurityType.Option:
573  // options uses the underlying symbol for pathing.
574  return !isHourOrDaily ? Path.Combine(directory, symbol.Underlying.Value.ToLowerInvariant()) : directory;
575 
576  case SecurityType.FutureOption:
577  // For futures options, we use the canonical option ticker plus the underlying's expiry
578  // since it can differ from the underlying's ticker. We differ from normal futures
579  // because the option chain can be extraordinarily large compared to equity option chains.
580  var futureOptionPath = Path.Combine(symbol.ID.Symbol, symbol.Underlying.ID.Date.ToStringInvariant(DateFormat.EightCharacter))
581  .ToLowerInvariant();
582 
583  return Path.Combine(directory, futureOptionPath);
584 
585  case SecurityType.Future:
586  case SecurityType.CryptoFuture:
587  return !isHourOrDaily ? Path.Combine(directory, symbol.ID.Symbol.ToLowerInvariant()) : directory;
588 
589  case SecurityType.Commodity:
590  default:
591  throw new ArgumentOutOfRangeException();
592  }
593  }
594 
595  /// <summary>
596  /// Generates relative factor file paths for equities
597  /// </summary>
598  public static string GenerateRelativeFactorFilePath(Symbol symbol)
599  {
600  return Path.Combine(Globals.DataFolder,
601  "equity",
602  symbol.ID.Market,
603  "factor_files",
604  symbol.Value.ToLowerInvariant() + ".csv");
605  }
606 
607  /// <summary>
608  /// Generates the relative zip file path rooted in the /Data directory
609  /// </summary>
610  public static string GenerateRelativeZipFilePath(Symbol symbol, DateTime date, Resolution resolution, TickType tickType)
611  {
612  return Path.Combine(GenerateRelativeZipFileDirectory(symbol, resolution), GenerateZipFileName(symbol, date, resolution, tickType));
613  }
614 
615  /// <summary>
616  /// Generates the relative zip file path rooted in the /Data directory
617  /// </summary>
618  public static string GenerateRelativeZipFilePath(string symbol, SecurityType securityType, string market, DateTime date, Resolution resolution)
619  {
620  var directory = Path.Combine(securityType.SecurityTypeToLower(), market.ToLowerInvariant(), resolution.ResolutionToLower());
621  if (resolution != Resolution.Daily && resolution != Resolution.Hour)
622  {
623  directory = Path.Combine(directory, symbol.ToLowerInvariant());
624  }
625 
626  return Path.Combine(directory, GenerateZipFileName(symbol, securityType, date, resolution));
627  }
628 
629  /// <summary>
630  /// Generates the relative directory to the universe files for the specified symbol
631  /// </summary>
632  public static string GenerateRelativeUniversesDirectory(Symbol symbol)
633  {
634  var path = Path.Combine(symbol.SecurityType.SecurityTypeToLower(), symbol.ID.Market, "universes");
635  switch (symbol.SecurityType)
636  {
637  case SecurityType.Option:
638  path = Path.Combine(path, symbol.Underlying.Value.ToLowerInvariant());
639  break;
640 
641  case SecurityType.IndexOption:
642  path = Path.Combine(path, symbol.ID.Symbol.ToLowerInvariant());
643  break;
644 
645  case SecurityType.FutureOption:
646  path = Path.Combine(path,
647  symbol.Underlying.ID.Symbol.ToLowerInvariant(),
648  symbol.Underlying.ID.Date.ToStringInvariant(DateFormat.EightCharacter));
649  break;
650 
651  case SecurityType.Future:
652  path = Path.Combine(path, symbol.ID.Symbol.ToLowerInvariant());
653  break;
654 
655  default:
656  throw new ArgumentOutOfRangeException($"Unsupported security type {symbol.SecurityType}");
657  }
658 
659  return path;
660  }
661 
662  /// <summary>
663  /// Generates the directory to the universe files for the specified symbol
664  /// </summary>
665  public static string GenerateUniversesDirectory(string dataDirectory, Symbol symbol)
666  {
667  return Path.Combine(dataDirectory, GenerateRelativeUniversesDirectory(symbol));
668  }
669 
670  /// <summary>
671  /// Generate's the zip entry name to hold the specified data.
672  /// </summary>
673  public static string GenerateZipEntryName(Symbol symbol, DateTime date, Resolution resolution, TickType tickType)
674  {
675  var formattedDate = date.ToStringInvariant(DateFormat.EightCharacter);
676  var isHourOrDaily = resolution == Resolution.Hour || resolution == Resolution.Daily;
677 
678  switch (symbol.ID.SecurityType)
679  {
680  case SecurityType.Base:
681  case SecurityType.Equity:
682  case SecurityType.Index:
683  case SecurityType.Forex:
684  case SecurityType.Cfd:
685  case SecurityType.Crypto:
686  if (resolution == Resolution.Tick && symbol.SecurityType == SecurityType.Equity)
687  {
688  return Invariant($"{formattedDate}_{symbol.Value.ToLowerInvariant()}_{tickType}_{resolution}.csv");
689  }
690 
691  if (isHourOrDaily)
692  {
693  return $"{symbol.Value.ToLowerInvariant()}.csv";
694  }
695 
696  return Invariant($"{formattedDate}_{symbol.Value.ToLowerInvariant()}_{resolution.ResolutionToLower()}_{tickType.TickTypeToLower()}.csv");
697 
698  case SecurityType.Option:
699  var optionPath = symbol.Underlying.Value.ToLowerInvariant();
700 
701  if (isHourOrDaily)
702  {
703  return string.Join("_",
704  optionPath,
705  tickType.TickTypeToLower(),
706  symbol.ID.OptionStyle.OptionStyleToLower(),
707  symbol.ID.OptionRight.OptionRightToLower(),
708  Scale(symbol.ID.StrikePrice),
709  symbol.ID.Date.ToStringInvariant(DateFormat.EightCharacter)
710  ) + ".csv";
711  }
712 
713  return string.Join("_",
714  formattedDate,
715  optionPath,
716  resolution.ResolutionToLower(),
717  tickType.TickTypeToLower(),
718  symbol.ID.OptionStyle.OptionStyleToLower(),
719  symbol.ID.OptionRight.OptionRightToLower(),
720  Scale(symbol.ID.StrikePrice),
721  symbol.ID.Date.ToStringInvariant(DateFormat.EightCharacter)
722  ) + ".csv";
723 
724  case SecurityType.IndexOption:
725  case SecurityType.FutureOption:
726  // We want the future/index option ticker as the lookup name inside the ZIP file
727  var optionTickerBasedPath = symbol.ID.Symbol.ToLowerInvariant();
728 
729  if (isHourOrDaily)
730  {
731  return string.Join("_",
732  optionTickerBasedPath,
733  tickType.TickTypeToLower(),
734  symbol.ID.OptionStyle.OptionStyleToLower(),
735  symbol.ID.OptionRight.OptionRightToLower(),
736  Scale(symbol.ID.StrikePrice),
737  symbol.ID.Date.ToStringInvariant(DateFormat.EightCharacter)
738  ) + ".csv";
739  }
740 
741  return string.Join("_",
742  formattedDate,
743  optionTickerBasedPath,
744  resolution.ResolutionToLower(),
745  tickType.TickTypeToLower(),
746  symbol.ID.OptionStyle.OptionStyleToLower(),
747  symbol.ID.OptionRight.OptionRightToLower(),
748  Scale(symbol.ID.StrikePrice),
749  symbol.ID.Date.ToStringInvariant(DateFormat.EightCharacter)
750  ) + ".csv";
751 
752  case SecurityType.Future:
753  case SecurityType.CryptoFuture:
754  if (symbol.HasUnderlying)
755  {
756  symbol = symbol.Underlying;
757  }
758 
759  string expirationTag;
760  var expiryDate = symbol.ID.Date;
761  if (expiryDate != SecurityIdentifier.DefaultDate)
762  {
764  var contractYearMonth = expiryDate.AddMonths(monthsToAdd).ToStringInvariant(DateFormat.YearMonth);
765 
766  expirationTag = $"{contractYearMonth}_{expiryDate.ToStringInvariant(DateFormat.EightCharacter)}";
767  }
768  else
769  {
770  expirationTag = "perp";
771  }
772 
773  if (isHourOrDaily)
774  {
775  return string.Join("_",
776  symbol.ID.Symbol.ToLowerInvariant(),
777  tickType.TickTypeToLower(),
778  expirationTag
779  ) + ".csv";
780  }
781 
782  return string.Join("_",
783  formattedDate,
784  symbol.ID.Symbol.ToLowerInvariant(),
785  resolution.ResolutionToLower(),
786  tickType.TickTypeToLower(),
787  expirationTag
788  ) + ".csv";
789 
790  case SecurityType.Commodity:
791  default:
792  throw new ArgumentOutOfRangeException();
793  }
794  }
795 
796  /// <summary>
797  /// Generates the zip file name for the specified date of data.
798  /// </summary>
799  public static string GenerateZipFileName(Symbol symbol, DateTime date, Resolution resolution, TickType tickType)
800  {
801  var tickTypeString = tickType.TickTypeToLower();
802  var formattedDate = date.ToStringInvariant(DateFormat.EightCharacter);
803  var isHourOrDaily = resolution == Resolution.Hour || resolution == Resolution.Daily;
804 
805  switch (symbol.ID.SecurityType)
806  {
807  case SecurityType.Base:
808  case SecurityType.Index:
809  case SecurityType.Equity:
810  case SecurityType.Forex:
811  case SecurityType.Cfd:
812  if (isHourOrDaily)
813  {
814  return $"{symbol.Value.ToLowerInvariant()}.zip";
815  }
816 
817  return $"{formattedDate}_{tickTypeString}.zip";
818  case SecurityType.Crypto:
819  if (isHourOrDaily)
820  {
821  return $"{symbol.Value.ToLowerInvariant()}_{tickTypeString}.zip";
822  }
823 
824  return $"{formattedDate}_{tickTypeString}.zip";
825  case SecurityType.Option:
826  if (isHourOrDaily)
827  {
828  // see TryParsePath: he knows tick type position is 3
829  var optionPath = symbol.Underlying.Value.ToLowerInvariant();
830  return $"{optionPath}_{date.Year}_{tickTypeString}_{symbol.ID.OptionStyle.OptionStyleToLower()}.zip";
831  }
832 
833  return $"{formattedDate}_{tickTypeString}_{symbol.ID.OptionStyle.OptionStyleToLower()}.zip";
834 
835  case SecurityType.IndexOption:
836  case SecurityType.FutureOption:
837  if (isHourOrDaily)
838  {
839  // see TryParsePath: he knows tick type position is 3
840  var optionTickerBasedPath = symbol.ID.Symbol.ToLowerInvariant();
841  return $"{optionTickerBasedPath}_{date.Year}_{tickTypeString}_{symbol.ID.OptionStyle.OptionStyleToLower()}.zip";
842  }
843 
844  return $"{formattedDate}_{tickTypeString}_{symbol.ID.OptionStyle.OptionStyleToLower()}.zip";
845 
846  case SecurityType.Future:
847  case SecurityType.CryptoFuture:
848  if (isHourOrDaily)
849  {
850  return $"{symbol.ID.Symbol.ToLowerInvariant()}_{tickTypeString}.zip";
851  }
852 
853  return $"{formattedDate}_{tickTypeString}.zip";
854 
855  case SecurityType.Commodity:
856  default:
857  throw new ArgumentOutOfRangeException();
858  }
859  }
860 
861  /// <summary>
862  /// Creates the zip file name for a QC zip data file
863  /// </summary>
864  public static string GenerateZipFileName(string symbol, SecurityType securityType, DateTime date, Resolution resolution, TickType? tickType = null)
865  {
866  if (resolution == Resolution.Hour || resolution == Resolution.Daily)
867  {
868  return $"{symbol.ToLowerInvariant()}.zip";
869  }
870 
871  var zipFileName = date.ToStringInvariant(DateFormat.EightCharacter);
872 
873  if (tickType == null)
874  {
875  if (securityType == SecurityType.Forex || securityType == SecurityType.Cfd)
876  {
877  tickType = TickType.Quote;
878  }
879  else
880  {
881  tickType = TickType.Trade;
882  }
883  }
884 
885  var suffix = Invariant($"_{tickType.Value.TickTypeToLower()}.zip");
886  return zipFileName + suffix;
887  }
888 
889  /// <summary>
890  /// Gets the tick type most commonly associated with the specified security type
891  /// </summary>
892  /// <param name="securityType">The security type</param>
893  /// <returns>The most common tick type for the specified security type</returns>
894  public static TickType GetCommonTickType(SecurityType securityType)
895  {
896  if (securityType == SecurityType.Forex || securityType == SecurityType.Cfd || securityType == SecurityType.Crypto)
897  {
898  return TickType.Quote;
899  }
900  return TickType.Trade;
901  }
902 
903  /// <summary>
904  /// Creates a symbol from the specified zip entry name
905  /// </summary>
906  /// <param name="symbol">The root symbol of the output symbol</param>
907  /// <param name="resolution">The resolution of the data source producing the zip entry name</param>
908  /// <param name="zipEntryName">The zip entry name to be parsed</param>
909  /// <returns>A new symbol representing the zip entry name</returns>
910  public static Symbol ReadSymbolFromZipEntry(Symbol symbol, Resolution resolution, string zipEntryName)
911  {
912  var isHourlyOrDaily = resolution == Resolution.Hour || resolution == Resolution.Daily;
913  var parts = zipEntryName.Replace(".csv", string.Empty).Split('_');
914  switch (symbol.ID.SecurityType)
915  {
916  case SecurityType.Option:
917  case SecurityType.FutureOption:
918  case SecurityType.IndexOption:
919  if (isHourlyOrDaily)
920  {
921  var style = parts[2].ParseOptionStyle();
922  var right = parts[3].ParseOptionRight();
923  var strike = Parse.Decimal(parts[4]) / 10000m;
924  var expiry = Parse.DateTimeExact(parts[5], DateFormat.EightCharacter);
925  return Symbol.CreateOption(symbol.Underlying, symbol.ID.Symbol, symbol.ID.Market, style, right, strike, expiry);
926  }
927  else
928  {
929  var style = parts[4].ParseOptionStyle();
930  var right = parts[5].ParseOptionRight();
931  var strike = Parse.Decimal(parts[6]) / 10000m;
932  var expiry = DateTime.ParseExact(parts[7], DateFormat.EightCharacter, CultureInfo.InvariantCulture);
933  return Symbol.CreateOption(symbol.Underlying, symbol.ID.Symbol, symbol.ID.Market, style, right, strike, expiry);
934  }
935 
936  case SecurityType.Future:
937  if (isHourlyOrDaily)
938  {
939  var expiryYearMonth = Parse.DateTimeExact(parts[2], DateFormat.YearMonth);
940  var futureExpiryFunc = FuturesExpiryFunctions.FuturesExpiryFunction(symbol);
941  var futureExpiry = futureExpiryFunc(expiryYearMonth);
942  return Symbol.CreateFuture(parts[0], symbol.ID.Market, futureExpiry);
943  }
944  else
945  {
946  var expiryYearMonth = Parse.DateTimeExact(parts[4], DateFormat.YearMonth);
947  var futureExpiryFunc = FuturesExpiryFunctions.FuturesExpiryFunction(symbol);
948  var futureExpiry = futureExpiryFunc(expiryYearMonth);
949  return Symbol.CreateFuture(parts[1], symbol.ID.Market, futureExpiry);
950  }
951 
952  default:
953  throw new NotImplementedException(Invariant(
954  $"ReadSymbolFromZipEntry is not implemented for {symbol.ID.SecurityType} {symbol.ID.Market} {resolution}"
955  ));
956  }
957  }
958 
959  /// <summary>
960  /// Scale and convert the resulting number to deci-cents int.
961  /// </summary>
962  private static long Scale(decimal value)
963  {
964  return (long)(value * 10000);
965  }
966 
967  /// <summary>
968  /// Create a csv line from the specified arguments
969  /// </summary>
970  private static string ToCsv(params object[] args)
971  {
972  // use culture neutral formatting for decimals
973  for (var i = 0; i < args.Length; i++)
974  {
975  var value = args[i];
976  if (value is decimal)
977  {
978  args[i] = ((decimal)value).Normalize();
979  }
980  }
981 
982  var argsFormatted = args.Select(x => Convert.ToString(x, CultureInfo.InvariantCulture));
983  return string.Join(",", argsFormatted);
984  }
985 
986  /// <summary>
987  /// Creates a scaled csv line for the bar, if null fills in empty strings
988  /// </summary>
989  private static string ToScaledCsv(IBar bar)
990  {
991  if (bar == null)
992  {
993  return ToCsv(string.Empty, string.Empty, string.Empty, string.Empty);
994  }
995 
996  return ToCsv(Scale(bar.Open), Scale(bar.High), Scale(bar.Low), Scale(bar.Close));
997  }
998 
999 
1000  /// <summary>
1001  /// Creates a non scaled csv line for the bar, if null fills in empty strings
1002  /// </summary>
1003  private static string ToNonScaledCsv(IBar bar)
1004  {
1005  if (bar == null)
1006  {
1007  return ToCsv(string.Empty, string.Empty, string.Empty, string.Empty);
1008  }
1009 
1010  return ToCsv(bar.Open, bar.High, bar.Low, bar.Close);
1011  }
1012 
1013  /// <summary>
1014  /// Get the <see cref="TickType"/> for common Lean data types.
1015  /// If not a Lean common data type, return a TickType of Trade.
1016  /// </summary>
1017  /// <param name="type">A Type used to determine the TickType</param>
1018  /// <param name="securityType">The SecurityType used to determine the TickType</param>
1019  /// <returns>A TickType corresponding to the type</returns>
1020  public static TickType GetCommonTickTypeForCommonDataTypes(Type type, SecurityType securityType)
1021  {
1022  if (type == typeof(TradeBar))
1023  {
1024  return TickType.Trade;
1025  }
1026  if (type == typeof(QuoteBar))
1027  {
1028  return TickType.Quote;
1029  }
1030  if (type == typeof(OpenInterest))
1031  {
1032  return TickType.OpenInterest;
1033  }
1034  if (type.IsAssignableTo(typeof(BaseChainUniverseData)))
1035  {
1036  return TickType.Quote;
1037  }
1038  if (type == typeof(Tick))
1039  {
1040  return GetCommonTickType(securityType);
1041  }
1042 
1043  return TickType.Trade;
1044  }
1045 
1046  /// <summary>
1047  /// Matches a data path security type with the <see cref="SecurityType"/>
1048  /// </summary>
1049  /// <remarks>This includes 'alternative'</remarks>
1050  /// <param name="securityType">The data path security type</param>
1051  /// <returns>The matching security type for the given data path</returns>
1052  public static SecurityType ParseDataSecurityType(string securityType)
1053  {
1054  if (securityType.Equals("alternative", StringComparison.InvariantCultureIgnoreCase))
1055  {
1056  return SecurityType.Base;
1057  }
1058  return (SecurityType)Enum.Parse(typeof(SecurityType), securityType, true);
1059  }
1060 
1061  /// <summary>
1062  /// Parses file name into a <see cref="Security"/> and DateTime
1063  /// </summary>
1064  /// <param name="fileName">File name to be parsed</param>
1065  /// <param name="securityType">The securityType as parsed from the fileName</param>
1066  /// <param name="market">The market as parsed from the fileName</param>
1067  public static bool TryParseSecurityType(string fileName, out SecurityType securityType, out string market)
1068  {
1069  securityType = SecurityType.Base;
1070  market = string.Empty;
1071 
1072  try
1073  {
1074  var info = SplitDataPath(fileName);
1075 
1076  // find the securityType and parse it
1077  var typeString = info.Find(x => SecurityTypeAsDataPath.Contains(x.ToLowerInvariant()));
1078  securityType = ParseDataSecurityType(typeString);
1079 
1080  var existingMarkets = Market.SupportedMarkets();
1081  var foundMarket = info.Find(x => existingMarkets.Contains(x.ToLowerInvariant()));
1082  if (foundMarket != null)
1083  {
1084  market = foundMarket;
1085  }
1086  }
1087  catch (Exception e)
1088  {
1089  Log.Error($"LeanData.TryParsePath(): Error encountered while parsing the path {fileName}. Error: {e.GetBaseException()}");
1090  return false;
1091  }
1092 
1093  return true;
1094  }
1095 
1096  /// <summary>
1097  /// Parses file name into a <see cref="Security"/> and DateTime
1098  /// </summary>
1099  /// <param name="filePath">File path to be parsed</param>
1100  /// <param name="symbol">The symbol as parsed from the fileName</param>
1101  /// <param name="date">Date of data in the file path. Only returned if the resolution is lower than Hourly</param>
1102  /// <param name="resolution">The resolution of the symbol as parsed from the filePath</param>
1103  /// <param name="tickType">The tick type</param>
1104  /// <param name="dataType">The data type</param>
1105  public static bool TryParsePath(string filePath, out Symbol symbol, out DateTime date,
1106  out Resolution resolution, out TickType tickType, out Type dataType)
1107  {
1108  symbol = default;
1109  tickType = default;
1110  dataType = default;
1111  date = default;
1112  resolution = default;
1113 
1114  try
1115  {
1116  if (!TryParsePath(filePath, out symbol, out date, out resolution, out var isUniverses))
1117  {
1118  return false;
1119  }
1120 
1121  tickType = GetCommonTickType(symbol.SecurityType);
1122  var fileName = Path.GetFileNameWithoutExtension(filePath);
1123  if (fileName.Contains('_', StringComparison.InvariantCulture))
1124  {
1125  // example: 20140606_openinterest_american.zip
1126  var tickTypePosition = 1;
1127  if (resolution >= Resolution.Hour && symbol.SecurityType.IsOption())
1128  {
1129  // daily and hourly have the year too, example: aapl_2014_openinterest_american.zip
1130  // see GenerateZipFileName he's creating these paths
1131  tickTypePosition = 2;
1132  }
1133  tickType = (TickType)Enum.Parse(typeof(TickType), fileName.Split('_')[tickTypePosition], true);
1134  }
1135 
1136  dataType = isUniverses ? typeof(OptionUniverse) : GetDataType(resolution, tickType);
1137  return true;
1138  }
1139  catch (Exception ex)
1140  {
1141  Log.Debug($"LeanData.TryParsePath(): Error encountered while parsing the path {filePath}. Error: {ex.GetBaseException()}");
1142  }
1143  return false;
1144  }
1145 
1146  /// <summary>
1147  /// Parses file name into a <see cref="Security"/> and DateTime
1148  /// </summary>
1149  /// <param name="fileName">File name to be parsed</param>
1150  /// <param name="symbol">The symbol as parsed from the fileName</param>
1151  /// <param name="date">Date of data in the file path. Only returned if the resolution is lower than Hourly</param>
1152  /// <param name="resolution">The resolution of the symbol as parsed from the filePath</param>
1153  public static bool TryParsePath(string fileName, out Symbol symbol, out DateTime date, out Resolution resolution)
1154  {
1155  return TryParsePath(fileName, out symbol, out date, out resolution, out _);
1156  }
1157 
1158  /// <summary>
1159  /// Parses file name into a <see cref="Security"/> and DateTime
1160  /// </summary>
1161  /// <param name="fileName">File name to be parsed</param>
1162  /// <param name="symbol">The symbol as parsed from the fileName</param>
1163  /// <param name="date">Date of data in the file path. Only returned if the resolution is lower than Hourly</param>
1164  /// <param name="resolution">The resolution of the symbol as parsed from the filePath</param>
1165  /// <param name="isUniverses">Outputs whether the file path represents a universe data file.</param>
1166  public static bool TryParsePath(string fileName, out Symbol symbol, out DateTime date, out Resolution resolution, out bool isUniverses)
1167  {
1168  symbol = null;
1169  resolution = Resolution.Daily;
1170  date = default(DateTime);
1171  isUniverses = default;
1172 
1173  try
1174  {
1175  var info = SplitDataPath(fileName);
1176 
1177  // find where the useful part of the path starts - i.e. the securityType
1178  var startIndex = info.FindIndex(x => SecurityTypeAsDataPath.Contains(x.ToLowerInvariant()));
1179 
1180  if (startIndex == -1)
1181  {
1182  if (Log.DebuggingEnabled)
1183  {
1184  Log.Debug($"LeanData.TryParsePath(): Failed to parse '{fileName}' unexpected SecurityType");
1185  }
1186  // SPDB & MHDB folders
1187  return false;
1188  }
1189  var securityType = ParseDataSecurityType(info[startIndex]);
1190 
1191  var market = Market.USA;
1192  string ticker;
1193 
1194  if (!Enum.TryParse(info[startIndex + 2], true, out resolution))
1195  {
1196  resolution = Resolution.Daily;
1197  isUniverses = info[startIndex + 2].Equals("universes", StringComparison.InvariantCultureIgnoreCase);
1198  if (securityType != SecurityType.Base)
1199  {
1200  if (!isUniverses)
1201  {
1202  if (Log.DebuggingEnabled)
1203  {
1204  Log.Debug($"LeanData.TryParsePath(): Failed to parse '{fileName}' unexpected Resolution");
1205  }
1206  // only acept a failure to parse resolution if we are facing a universes path
1207  return false;
1208  }
1209 
1210  (symbol, date) = ParseUniversePath(info, securityType);
1211  return true;
1212  }
1213  }
1214 
1215  if (securityType == SecurityType.Base)
1216  {
1217  // the last part of the path is the file name
1218  var fileNameNoPath = info[info.Count - 1].Split('_').First();
1219 
1220  if (!DateTime.TryParseExact(fileNameNoPath,
1222  DateTimeFormatInfo.InvariantInfo,
1223  DateTimeStyles.None,
1224  out date))
1225  {
1226  // if parsing the date failed we assume filename is ticker
1227  ticker = fileNameNoPath;
1228  }
1229  else
1230  {
1231  // ticker must be the previous part of the path
1232  ticker = info[info.Count - 2];
1233  }
1234  }
1235  else
1236  {
1237  // Gather components used to create the security
1238  market = info[startIndex + 1];
1239  var components = info[startIndex + 3].Split('_');
1240 
1241  // Remove the ticktype from the ticker (Only exists in Crypto and Future data but causes no issues)
1242  ticker = components[0];
1243 
1244  if (resolution < Resolution.Hour)
1245  {
1246  // Future options are special and have the following format Market/Resolution/Ticker/FutureExpiry/Date
1247  var dateIndex = securityType == SecurityType.FutureOption ? startIndex + 5 : startIndex + 4;
1248  date = Parse.DateTimeExact(info[dateIndex].Substring(0, 8), DateFormat.EightCharacter);
1249  }
1250  // If resolution is Daily or Hour for options and index options, we can only get the year from the path
1251  else if (securityType == SecurityType.Option || securityType == SecurityType.IndexOption)
1252  {
1253  var year = int.Parse(components[1], CultureInfo.InvariantCulture);
1254  date = new DateTime(year, 01, 01);
1255  }
1256  }
1257 
1258  if (securityType == SecurityType.FutureOption)
1259  {
1260  // Future options have underlying FutureExpiry date as the parent dir for the zips, we need this for our underlying
1261  symbol = CreateSymbol(ticker, securityType, market, null, Parse.DateTimeExact(info[startIndex + 4].Substring(0, 8), DateFormat.EightCharacter));
1262  }
1263  else
1264  {
1265  symbol = CreateSymbol(ticker, securityType, market, null, date);
1266  }
1267  }
1268  catch (Exception ex)
1269  {
1270  Log.Debug($"LeanData.TryParsePath(): Error encountered while parsing the path {fileName}. Error: {ex.GetBaseException()}");
1271  return false;
1272  }
1273 
1274  return true;
1275  }
1276 
1277  /// <summary>
1278  /// Parses the universe file path and extracts the corresponding symbol and file date.
1279  /// </summary>
1280  /// <param name="filePathParts">
1281  /// A list of strings representing the file path segments. The expected structure is:
1282  /// <para>General format: ["data", SecurityType, Market, "universes", ...]</para>
1283  /// <para>Examples:</para>
1284  /// <list type="bullet">
1285  /// <item><description>Equity: <c>data/equity/usa/universes/etf/spy/20201130.csv</c></description></item>
1286  /// <item><description>Option: <c>data/option/usa/universes/aapl/20241112.csv</c></description></item>
1287  /// <item><description>Future: <c>data/future/cme/universes/es/20130710.csv</c></description></item>
1288  /// <item><description>Future Option: <c>data/futureoption/cme/universes/20120401/20111230.csv</c></description></item>
1289  /// </list>
1290  /// </param>
1291  /// <param name="securityType">The type of security for which the symbol is being created.</param>
1292  /// <returns>A tuple containing the parsed <see cref="Symbol"/> and the universe processing file date.</returns>
1293  /// <exception cref="ArgumentException">Thrown if the file path does not contain 'universes'.</exception>
1294  /// <exception cref="NotSupportedException">Thrown if the security type is not supported.</exception>
1295  private static (Symbol symbol, DateTime processingDate) ParseUniversePath(IReadOnlyList<string> filePathParts, SecurityType securityType)
1296  {
1297  if (!filePathParts.Contains("universes", StringComparer.InvariantCultureIgnoreCase))
1298  {
1299  throw new ArgumentException($"LeanData.{nameof(ParseUniversePath)}:The file path must contain a 'universes' part, but it was not found.");
1300  }
1301 
1302  var symbol = default(Symbol);
1303  var market = filePathParts[2];
1304  var ticker = filePathParts[^2];
1305  var universeFileDate = DateTime.ParseExact(filePathParts[^1], DateFormat.EightCharacter, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None);
1306  switch (securityType)
1307  {
1308  case SecurityType.Equity:
1309  securityType = SecurityType.Base;
1310  var dataType = filePathParts.Contains("etf", StringComparer.InvariantCultureIgnoreCase) ? typeof(ETFConstituentUniverse) : default;
1311  symbol = CreateSymbol(ticker, securityType, market, dataType, universeFileDate);
1312  break;
1313  case SecurityType.Option:
1314  symbol = CreateSymbol(ticker, securityType, market, null, universeFileDate);
1315  break;
1316  case SecurityType.IndexOption:
1317  symbol = CreateSymbol(ticker, securityType, market, null, default);
1318  break;
1319  case SecurityType.FutureOption:
1320  symbol = CreateSymbol(filePathParts[^3], securityType, market, null, Parse.DateTimeExact(filePathParts[^2], DateFormat.EightCharacter));
1321  break;
1322  case SecurityType.Future:
1323  var mapUnderlyingTicker = OptionSymbol.MapToUnderlying(ticker, securityType);
1324  symbol = Symbol.CreateFuture(mapUnderlyingTicker, market, universeFileDate);
1325  break;
1326  default:
1327  throw new NotSupportedException($"LeanData.{nameof(ParseUniversePath)}:The security type '{securityType}' is not supported for data universe files.");
1328  }
1329 
1330  return (symbol, universeFileDate);
1331  }
1332 
1333  /// <summary>
1334  /// Creates a new Symbol based on parsed data path information.
1335  /// </summary>
1336  /// <param name="ticker">The parsed ticker symbol.</param>
1337  /// <param name="securityType">The parsed type of security.</param>
1338  /// <param name="market">The parsed market or exchange.</param>
1339  /// <param name="dataType">Optional type used for generating the base data SID (applicable only for SecurityType.Base).</param>
1340  /// <param name="mappingResolveDate">The date used in path parsing to create the correct symbol.</param>
1341  /// <returns>A unique security identifier.</returns>
1342  /// <example>
1343  /// <code>
1344  /// path: equity/usa/minute/spwr/20071223_trade.zip
1345  /// ticker: spwr
1346  /// securityType: equity
1347  /// market: usa
1348  /// mappingResolveDate: 2007/12/23
1349  /// </code>
1350  /// </example>
1351  private static Symbol CreateSymbol(string ticker, SecurityType securityType, string market, Type dataType, DateTime mappingResolveDate = default)
1352  {
1353  if (mappingResolveDate != default && (securityType == SecurityType.Equity || securityType == SecurityType.Option))
1354  {
1355  var symbol = new Symbol(SecurityIdentifier.GenerateEquity(ticker, market, mappingResolveDate: mappingResolveDate), ticker);
1356  return securityType == SecurityType.Option ? Symbol.CreateCanonicalOption(symbol) : symbol;
1357  }
1358  else if (securityType == SecurityType.FutureOption)
1359  {
1360  var underlyingTicker = OptionSymbol.MapToUnderlying(ticker, securityType);
1361  // Create our underlying future and then the Canonical option for this future
1362  var underlyingFuture = Symbol.CreateFuture(underlyingTicker, market, mappingResolveDate);
1363  return Symbol.CreateCanonicalOption(underlyingFuture);
1364  }
1365  else if (securityType == SecurityType.IndexOption)
1366  {
1367  var underlyingTicker = OptionSymbol.MapToUnderlying(ticker, securityType);
1368  // Create our underlying index and then the Canonical option
1369  var underlyingIndex = Symbol.Create(underlyingTicker, SecurityType.Index, market);
1370  return Symbol.CreateCanonicalOption(underlyingIndex, ticker, market, null);
1371  }
1372  else
1373  {
1374  return Symbol.Create(ticker, securityType, market, baseDataType: dataType);
1375  }
1376  }
1377 
1378  private static List<string> SplitDataPath(string fileName)
1379  {
1380  var pathSeparators = new[] { '/', '\\' };
1381 
1382  // Removes file extension
1383  fileName = fileName.Replace(fileName.GetExtension(), string.Empty);
1384 
1385  // remove any relative file path
1386  while (fileName.First() == '.' || pathSeparators.Any(x => x == fileName.First()))
1387  {
1388  fileName = fileName.Remove(0, 1);
1389  }
1390 
1391  // split path into components
1392  return fileName.Split(pathSeparators, StringSplitOptions.RemoveEmptyEntries).ToList();
1393  }
1394 
1395  /// <summary>
1396  /// Aggregates a list of second/minute bars at the requested resolution
1397  /// </summary>
1398  /// <param name="bars">List of <see cref="TradeBar"/>s</param>
1399  /// <param name="symbol">Symbol of all tradeBars</param>
1400  /// <param name="resolution">Desired resolution for new <see cref="TradeBar"/>s</param>
1401  /// <returns>List of aggregated <see cref="TradeBar"/>s</returns>
1402  public static IEnumerable<TradeBar> AggregateTradeBars(IEnumerable<TradeBar> bars, Symbol symbol, TimeSpan resolution)
1403  {
1404  return Aggregate(new TradeBarConsolidator(resolution), bars, symbol);
1405  }
1406 
1407  /// <summary>
1408  /// Aggregates a list of second/minute bars at the requested resolution
1409  /// </summary>
1410  /// <param name="bars">List of <see cref="QuoteBar"/>s</param>
1411  /// <param name="symbol">Symbol of all QuoteBars</param>
1412  /// <param name="resolution">Desired resolution for new <see cref="QuoteBar"/>s</param>
1413  /// <returns>List of aggregated <see cref="QuoteBar"/>s</returns>
1414  public static IEnumerable<QuoteBar> AggregateQuoteBars(IEnumerable<QuoteBar> bars, Symbol symbol, TimeSpan resolution)
1415  {
1416  return Aggregate(new QuoteBarConsolidator(resolution), bars, symbol);
1417  }
1418 
1419  /// <summary>
1420  /// Aggregates a list of ticks at the requested resolution
1421  /// </summary>
1422  /// <param name="ticks">List of quote ticks</param>
1423  /// <param name="symbol">Symbol of all ticks</param>
1424  /// <param name="resolution">Desired resolution for new <see cref="QuoteBar"/>s</param>
1425  /// <returns>List of aggregated <see cref="QuoteBar"/>s</returns>
1426  public static IEnumerable<QuoteBar> AggregateTicks(IEnumerable<Tick> ticks, Symbol symbol, TimeSpan resolution)
1427  {
1428  return Aggregate(new TickQuoteBarConsolidator(resolution), ticks, symbol);
1429  }
1430 
1431  /// <summary>
1432  /// Aggregates a list of ticks at the requested resolution
1433  /// </summary>
1434  /// <param name="ticks">List of trade ticks</param>
1435  /// <param name="symbol">Symbol of all ticks</param>
1436  /// <param name="resolution">Desired resolution for new <see cref="TradeBar"/>s</param>
1437  /// <returns>List of aggregated <see cref="TradeBar"/>s</returns>
1438  public static IEnumerable<TradeBar> AggregateTicksToTradeBars(IEnumerable<Tick> ticks, Symbol symbol, TimeSpan resolution)
1439  {
1440  return Aggregate(new TickConsolidator(resolution), ticks, symbol);
1441  }
1442 
1443  /// <summary>
1444  /// Helper method to return the start time and period of a bar the given point time should be part of
1445  /// </summary>
1446  /// <param name="exchangeTimeZoneDate">The point in time we want to get the bar information about</param>
1447  /// <param name="exchange">The associated security exchange</param>
1448  /// <param name="extendedMarketHours">True if extended market hours should be taken into consideration</param>
1449  /// <returns>The calendar information that holds a start time and a period</returns>
1450  public static CalendarInfo GetDailyCalendar(DateTime exchangeTimeZoneDate, SecurityExchange exchange, bool extendedMarketHours)
1451  {
1452  return GetDailyCalendar(exchangeTimeZoneDate, exchange.Hours, extendedMarketHours);
1453  }
1454 
1455  /// <summary>
1456  /// Helper method to return the start time and period of a bar the given point time should be part of
1457  /// </summary>
1458  /// <param name="exchangeTimeZoneDate">The point in time we want to get the bar information about</param>
1459  /// <param name="exchangeHours">The associated exchange hours</param>
1460  /// <param name="extendedMarketHours">True if extended market hours should be taken into consideration</param>
1461  /// <returns>The calendar information that holds a start time and a period</returns>
1462  public static CalendarInfo GetDailyCalendar(DateTime exchangeTimeZoneDate, SecurityExchangeHours exchangeHours, bool extendedMarketHours)
1463  {
1464  var startTime = exchangeHours.GetFirstDailyMarketOpen(exchangeTimeZoneDate, extendedMarketHours);
1465  var endTime = exchangeHours.GetLastDailyMarketClose(startTime, extendedMarketHours);
1466  var period = endTime - startTime;
1467  return new CalendarInfo(startTime, period);
1468  }
1469 
1470  /// <summary>
1471  /// Helper method to get the next daily end time, taking into account strict end times if appropriate
1472  /// </summary>
1473  public static DateTime GetNextDailyEndTime(Symbol symbol, DateTime exchangeTimeZoneDate, SecurityExchangeHours exchangeHours)
1474  {
1475  var nextMidnight = exchangeTimeZoneDate.Date.AddDays(1);
1476  if (!UseStrictEndTime(true, symbol, Time.OneDay, exchangeHours))
1477  {
1478  return nextMidnight;
1479  }
1480 
1481  var nextMarketClose = exchangeHours.GetLastDailyMarketClose(exchangeTimeZoneDate, extendedMarketHours: false);
1482  if (nextMarketClose > nextMidnight)
1483  {
1484  // if exchangeTimeZoneDate is after the previous close, the next close might be tomorrow
1485  if (!exchangeHours.IsOpen(exchangeTimeZoneDate, extendedMarketHours: false))
1486  {
1487  return nextMarketClose;
1488  }
1489  return nextMidnight;
1490  }
1491  return nextMarketClose;
1492  }
1493 
1494  /// <summary>
1495  /// Helper method that defines the types of options that should use scale factor
1496  /// </summary>
1497  [MethodImpl(MethodImplOptions.AggressiveInlining)]
1498  public static bool OptionUseScaleFactor(Symbol symbol)
1499  {
1500  return symbol.SecurityType == SecurityType.Option || symbol.SecurityType == SecurityType.IndexOption;
1501  }
1502 
1503  /// <summary>
1504  /// Helper method to determine if we should use strict end time
1505  /// </summary>
1506  /// <param name="symbol">The associated symbol</param>
1507  /// <param name="increment">The datas time increment</param>
1508  [MethodImpl(MethodImplOptions.AggressiveInlining)]
1509  public static bool UseStrictEndTime(bool dailyStrictEndTimeEnabled, Symbol symbol, TimeSpan increment, SecurityExchangeHours exchangeHours)
1510  {
1511  if (exchangeHours.IsMarketAlwaysOpen
1512  || increment <= Time.OneHour
1513  || symbol.SecurityType == SecurityType.Cfd && symbol.ID.Market == Market.Oanda
1514  || symbol.SecurityType == SecurityType.Forex
1515  || symbol.SecurityType == SecurityType.Base)
1516  {
1517  return false;
1518  }
1519  return dailyStrictEndTimeEnabled;
1520  }
1521 
1522  /// <summary>
1523  /// Helper method to determine if we should use strict end time
1524  /// </summary>
1525  public static bool UseDailyStrictEndTimes(IAlgorithmSettings settings, BaseDataRequest request, Symbol symbol, TimeSpan increment,
1526  SecurityExchangeHours exchangeHours = null)
1527  {
1528  return UseDailyStrictEndTimes(settings, request.DataType, symbol, increment, exchangeHours ?? request.ExchangeHours);
1529  }
1530 
1531  /// <summary>
1532  /// Helper method to determine if we should use strict end time
1533  /// </summary>
1534  public static bool UseDailyStrictEndTimes(IAlgorithmSettings settings, Type dataType, Symbol symbol, TimeSpan increment, SecurityExchangeHours exchangeHours)
1535  {
1536  return UseDailyStrictEndTimes(settings.DailyPreciseEndTime, dataType, symbol, increment, exchangeHours);
1537  }
1538 
1539  /// <summary>
1540  /// Helper method to determine if we should use strict end time
1541  /// </summary>
1542  public static bool UseDailyStrictEndTimes(bool dailyStrictEndTimeEnabled, Type dataType, Symbol symbol, TimeSpan increment, SecurityExchangeHours exchangeHours)
1543  {
1544  return UseDailyStrictEndTimes(dataType) && UseStrictEndTime(dailyStrictEndTimeEnabled, symbol, increment, exchangeHours);
1545  }
1546 
1547  /// <summary>
1548  /// True if this data type should use strict daily end times
1549  /// </summary>
1550  public static bool UseDailyStrictEndTimes(Type dataType)
1551  {
1552  return dataType != null && _strictDailyEndTimesDataTypes.Contains(dataType);
1553  }
1554 
1555  /// <summary>
1556  /// Helper method that if appropiate, will set the Time and EndTime of the given data point to it's daily strict times
1557  /// </summary>
1558  /// <param name="baseData">The target data point</param>
1559  /// <param name="exchange">The associated exchange hours</param>
1560  /// <remarks>This method is used to set daily times on pre existing data, assuming it does not cover extended market hours</remarks>
1561  [MethodImpl(MethodImplOptions.AggressiveInlining)]
1562  public static bool SetStrictEndTimes(IBaseData baseData, SecurityExchangeHours exchange)
1563  {
1564  if (baseData == null)
1565  {
1566  return false;
1567  }
1568 
1569  var dataType = baseData.GetType();
1570  if (!UseDailyStrictEndTimes(dataType))
1571  {
1572  return false;
1573  }
1574 
1575  var dailyCalendar = GetDailyCalendar(baseData.EndTime, exchange, extendedMarketHours: false);
1576  if (dailyCalendar.End < baseData.Time)
1577  {
1578  // this data point we were given is probably from extended market hours which we don't support for daily backtesting data
1579  return false;
1580  }
1581  baseData.Time = dailyCalendar.Start;
1582  baseData.EndTime = dailyCalendar.End;
1583  return true;
1584  }
1585 
1586  /// <summary>
1587  /// Helper to separate filename and entry from a given key for DataProviders
1588  /// </summary>
1589  /// <param name="key">The key to parse</param>
1590  /// <param name="fileName">File name extracted</param>
1591  /// <param name="entryName">Entry name extracted</param>
1592  public static void ParseKey(string key, out string fileName, out string entryName)
1593  {
1594  // Default scenario, no entryName included in key
1595  entryName = null; // default to all entries
1596  fileName = key;
1597 
1598  if (key == null)
1599  {
1600  return;
1601  }
1602 
1603  // Try extracting an entry name; Anything after a # sign
1604  var hashIndex = key.LastIndexOf("#", StringComparison.Ordinal);
1605  if (hashIndex != -1)
1606  {
1607  entryName = key.Substring(hashIndex + 1);
1608  fileName = key.Substring(0, hashIndex);
1609  }
1610  }
1611 
1612  /// <summary>
1613  /// Helper method to determine if the specified data type supports extended market hours
1614  /// </summary>
1615  /// <param name="dataType">The data type</param>
1616  /// <returns>Whether the specified data type supports extended market hours</returns>
1617  public static bool SupportsExtendedMarketHours(Type dataType)
1618  {
1619  return !dataType.IsAssignableTo(typeof(BaseChainUniverseData));
1620  }
1621 
1622  /// <summary>
1623  /// Helper method to aggregate ticks or bars into lower frequency resolutions
1624  /// </summary>
1625  /// <typeparam name="T">Output type</typeparam>
1626  /// <typeparam name="K">Input type</typeparam>
1627  /// <param name="consolidator">The consolidator to use</param>
1628  /// <param name="dataPoints">The data point source</param>
1629  /// <param name="symbol">The symbol to output</param>
1630  private static IEnumerable<T> Aggregate<T, K>(PeriodCountConsolidatorBase<K, T> consolidator, IEnumerable<K> dataPoints, Symbol symbol)
1631  where T : BaseData
1632  where K : BaseData
1633  {
1634  IBaseData lastAggregated = null;
1635  var getConsolidatedBar = () =>
1636  {
1637  if (lastAggregated != consolidator.Consolidated && consolidator.Consolidated != null)
1638  {
1639  // if there's a new aggregated bar we set the symbol & return it
1640  lastAggregated = consolidator.Consolidated;
1641  lastAggregated.Symbol = symbol;
1642  return lastAggregated;
1643  }
1644  return null;
1645  };
1646 
1647  foreach (var dataPoint in dataPoints)
1648  {
1649  consolidator.Update(dataPoint);
1650  var consolidated = getConsolidatedBar();
1651  if (consolidated != null)
1652  {
1653  yield return (T)consolidated;
1654  }
1655  }
1656 
1657  // flush any partial bar
1658  consolidator.Scan(Time.EndOfTime);
1659  var lastConsolidated = getConsolidatedBar();
1660  if (lastConsolidated != null)
1661  {
1662  yield return (T)lastConsolidated;
1663  }
1664 
1665  // cleanup
1666  consolidator.DisposeSafely();
1667  }
1668  }
1669 }