Lean  $LEAN_TAG$
QuantBook.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 Python.Runtime;
19 using QuantConnect.Data;
31 using QuantConnect.Util;
32 using System;
33 using System.Collections.Generic;
34 using System.IO;
35 using System.Linq;
36 using QuantConnect.Packets;
37 using System.Threading.Tasks;
40 
41 namespace QuantConnect.Research
42 {
43  /// <summary>
44  /// Provides access to data for quantitative analysis
45  /// </summary>
46  public class QuantBook : QCAlgorithm
47  {
48  private dynamic _pandas;
49  private IDataCacheProvider _dataCacheProvider;
50  private IDataProvider _dataProvider;
51  private static bool _isPythonNotebook;
52 
53  static QuantBook()
54  {
55  //Determine if we are in a Python Notebook
56  try
57  {
58  PythonEngine.Initialize();
59  using (Py.GIL())
60  {
61  var isPython = PyModule.FromString(Guid.NewGuid().ToString(),
62  "try:\n" +
63  " import IPython\n" +
64  " def IsPythonNotebook():\n" +
65  " return (IPython.get_ipython() != None)\n" +
66  "except:\n" +
67  " print('No IPython installed')\n" +
68  " def IsPythonNotebook():\n" +
69  " return false\n").GetAttr("IsPythonNotebook").Invoke();
70  isPython.TryConvert(out _isPythonNotebook);
71  }
72  }
73  catch
74  {
75  //Default to false
76  _isPythonNotebook = false;
77  Logging.Log.Error("QuantBook failed to determine Notebook kernel language");
78  }
79 
80  RecycleMemory();
81 
82  Logging.Log.Trace($"QuantBook started; Is Python: {_isPythonNotebook}");
83  }
84 
85  /// <summary>
86  /// <see cref = "QuantBook" /> constructor.
87  /// Provides access to data for quantitative analysis
88  /// </summary>
89  public QuantBook() : base()
90  {
91  try
92  {
93  using (Py.GIL())
94  {
95  _pandas = Py.Import("pandas");
96  }
97 
98  // Issue #4892 : Set start time relative to NY time
99  // when the data is available from the previous day
100  var newYorkTime = DateTime.UtcNow.ConvertFromUtc(TimeZones.NewYork);
101  var hourThreshold = Config.GetInt("qb-data-hour", 9);
102 
103  // If it is after our hour threshold; then we can use today
104  if (newYorkTime.Hour >= hourThreshold)
105  {
106  SetStartDate(newYorkTime);
107  }
108  else
109  {
110  SetStartDate(newYorkTime - TimeSpan.FromDays(1));
111  }
112 
113  // Sets PandasConverter
115 
116  // Reset our composer; needed for re-creation of QuantBook
118  var composer = Composer.Instance;
119  Config.Reset();
120 
121  // Create our handlers with our composer instance
122  var systemHandlers = LeanEngineSystemHandlers.FromConfiguration(composer);
123  // init the API
124  systemHandlers.Initialize();
125  var algorithmHandlers = LeanEngineAlgorithmHandlers.FromConfiguration(composer, researchMode: true);
126 ;
127 
128  var algorithmPacket = new BacktestNodePacket
129  {
130  UserToken = Globals.UserToken,
131  UserId = Globals.UserId,
133  OrganizationId = Globals.OrganizationID,
134  Version = Globals.Version
135  };
136 
137  ProjectId = algorithmPacket.ProjectId;
138  systemHandlers.LeanManager.Initialize(systemHandlers,
139  algorithmHandlers,
140  algorithmPacket,
141  new AlgorithmManager(false));
142  systemHandlers.LeanManager.SetAlgorithm(this);
143 
144 
145  algorithmHandlers.DataPermissionsManager.Initialize(algorithmPacket);
146 
147  algorithmHandlers.ObjectStore.Initialize(algorithmPacket.UserId,
148  algorithmPacket.ProjectId,
149  algorithmPacket.UserToken,
150  new Controls
151  {
152  // if <= 0 we disable periodic persistence and make it synchronous
153  PersistenceIntervalSeconds = -1,
154  StorageLimit = Config.GetValue("storage-limit", 10737418240L),
155  StorageFileCount = Config.GetInt("storage-file-count", 10000),
156  StoragePermissions = (FileAccess) Config.GetInt("storage-permissions", (int)FileAccess.ReadWrite)
157  });
158  SetObjectStore(algorithmHandlers.ObjectStore);
159 
160  _dataCacheProvider = new ZipDataCacheProvider(algorithmHandlers.DataProvider);
161  _dataProvider = algorithmHandlers.DataProvider;
162 
163  var symbolPropertiesDataBase = SymbolPropertiesDatabase.FromDataFolder();
164  var registeredTypes = new RegisteredSecurityDataTypesProvider();
165  var securityService = new SecurityService(Portfolio.CashBook,
167  symbolPropertiesDataBase,
168  this,
169  registeredTypes,
171  algorithm: this);
172  Securities.SetSecurityService(securityService);
174  new DataManager(new NullDataFeed(),
175  new UniverseSelection(this, securityService, algorithmHandlers.DataPermissionsManager, algorithmHandlers.DataProvider),
176  this,
177  TimeKeeper,
179  false,
180  registeredTypes,
181  algorithmHandlers.DataPermissionsManager));
182 
183  var mapFileProvider = algorithmHandlers.MapFileProvider;
187  null,
188  null,
189  algorithmHandlers.DataProvider,
190  _dataCacheProvider,
191  mapFileProvider,
192  algorithmHandlers.FactorFileProvider,
193  null,
194  true,
195  algorithmHandlers.DataPermissionsManager,
197  )
198  );
199 
200  SetOptionChainProvider(new CachingOptionChainProvider(new BacktestingOptionChainProvider(_dataCacheProvider, mapFileProvider)));
202 
204  SetDeploymentTarget(Config.GetValue("deployment-target", DeploymentTarget.LocalPlatform));
205  }
206  catch (Exception exception)
207  {
208  throw new Exception("QuantBook.Main(): " + exception);
209  }
210  }
211 
212  /// <summary>
213  /// Python implementation of GetFundamental, get fundamental data for input symbols or tickers
214  /// </summary>
215  /// <param name="input">The symbols or tickers to retrieve fundamental data for</param>
216  /// <param name="selector">Selects a value from the Fundamental data to filter the request output</param>
217  /// <param name="start">The start date of selected data</param>
218  /// <param name="end">The end date of selected data</param>
219  /// <returns>pandas DataFrame</returns>
220  [Obsolete("Please use the 'UniverseHistory()' API")]
221  public PyObject GetFundamental(PyObject input, string selector = null, DateTime? start = null, DateTime? end = null)
222  {
223  //Covert to symbols
224  var symbols = PythonUtil.ConvertToSymbols(input);
225 
226  //Fetch the data
227  var fundamentalData = GetAllFundamental(symbols, selector, start, end);
228 
229  using (Py.GIL())
230  {
231  var data = new PyDict();
232  foreach (var day in fundamentalData.OrderBy(x => x.Key))
233  {
234  var orderedValues = day.Value.OrderBy(x => x.Key.ID.ToString()).ToList();
235  var columns = orderedValues.Select(x => x.Key.ID.ToString());
236  var values = orderedValues.Select(x => x.Value);
237  var row = _pandas.Series(values, columns);
238  data.SetItem(day.Key.ToPython(), row);
239  }
240 
241  return _pandas.DataFrame.from_dict(data, orient:"index");
242  }
243  }
244 
245  /// <summary>
246  /// Get fundamental data from given symbols
247  /// </summary>
248  /// <param name="symbols">The symbols to retrieve fundamental data for</param>
249  /// <param name="selector">Selects a value from the Fundamental data to filter the request output</param>
250  /// <param name="start">The start date of selected data</param>
251  /// <param name="end">The end date of selected data</param>
252  /// <returns>Enumerable collection of DataDictionaries, one dictionary for each day there is data</returns>
253  [Obsolete("Please use the 'UniverseHistory()' API")]
254  public IEnumerable<DataDictionary<dynamic>> GetFundamental(IEnumerable<Symbol> symbols, string selector = null, DateTime? start = null, DateTime? end = null)
255  {
256  var data = GetAllFundamental(symbols, selector, start, end);
257 
258  foreach (var kvp in data.OrderBy(kvp => kvp.Key))
259  {
260  yield return kvp.Value;
261  }
262  }
263 
264  /// <summary>
265  /// Get fundamental data for a given symbol
266  /// </summary>
267  /// <param name="symbol">The symbol to retrieve fundamental data for</param>
268  /// <param name="selector">Selects a value from the Fundamental data to filter the request output</param>
269  /// <param name="start">The start date of selected data</param>
270  /// <param name="end">The end date of selected data</param>
271  /// <returns>Enumerable collection of DataDictionaries, one Dictionary for each day there is data.</returns>
272  [Obsolete("Please use the 'UniverseHistory()' API")]
273  public IEnumerable<DataDictionary<dynamic>> GetFundamental(Symbol symbol, string selector = null, DateTime? start = null, DateTime? end = null)
274  {
275  var list = new List<Symbol>
276  {
277  symbol
278  };
279 
280  return GetFundamental(list, selector, start, end);
281  }
282 
283  /// <summary>
284  /// Get fundamental data for a given set of tickers
285  /// </summary>
286  /// <param name="tickers">The tickers to retrieve fundamental data for</param>
287  /// <param name="selector">Selects a value from the Fundamental data to filter the request output</param>
288  /// <param name="start">The start date of selected data</param>
289  /// <param name="end">The end date of selected data</param>
290  /// <returns>Enumerable collection of DataDictionaries, one dictionary for each day there is data.</returns>
291  [Obsolete("Please use the 'UniverseHistory()' API")]
292  public IEnumerable<DataDictionary<dynamic>> GetFundamental(IEnumerable<string> tickers, string selector = null, DateTime? start = null, DateTime? end = null)
293  {
294  var list = new List<Symbol>();
295  foreach (var ticker in tickers)
296  {
297  list.Add(QuantConnect.Symbol.Create(ticker, SecurityType.Equity, Market.USA));
298  }
299 
300  return GetFundamental(list, selector, start, end);
301  }
302 
303  /// <summary>
304  /// Get fundamental data for a given ticker
305  /// </summary>
306  /// <param name="symbol">The symbol to retrieve fundamental data for</param>
307  /// <param name="selector">Selects a value from the Fundamental data to filter the request output</param>
308  /// <param name="start">The start date of selected data</param>
309  /// <param name="end">The end date of selected data</param>
310  /// <returns>Enumerable collection of DataDictionaries, one Dictionary for each day there is data.</returns>
311  [Obsolete("Please use the 'UniverseHistory()' API")]
312  public dynamic GetFundamental(string ticker, string selector = null, DateTime? start = null, DateTime? end = null)
313  {
314  //Check if its Python; PythonNet likes to convert the strings, but for python we want the DataFrame as the return object
315  //So we must route the function call to the Python version.
316  if (_isPythonNotebook)
317  {
318  return GetFundamental(ticker.ToPython(), selector, start, end);
319  }
320 
321  var symbol = QuantConnect.Symbol.Create(ticker, SecurityType.Equity, Market.USA);
322  var list = new List<Symbol>
323  {
324  symbol
325  };
326 
327  return GetFundamental(list, selector, start, end);
328  }
329 
330  /// <summary>
331  /// Gets <see cref="OptionHistory"/> object for a given symbol, date and resolution
332  /// </summary>
333  /// <param name="symbol">The symbol to retrieve historical option data for</param>
334  /// <param name="targetOption">The target option ticker. This is useful when the option ticker does not match the underlying, e.g. SPX index and the SPXW weekly option. If null is provided will use underlying</param>
335  /// <param name="start">The history request start time</param>
336  /// <param name="end">The history request end time. Defaults to 1 day if null</param>
337  /// <param name="resolution">The resolution to request</param>
338  /// <param name="fillForward">True to fill forward missing data, false otherwise</param>
339  /// <param name="extendedMarketHours">True to include extended market hours data, false otherwise</param>
340  /// <returns>A <see cref="OptionHistory"/> object that contains historical option data.</returns>
341  public OptionHistory OptionHistory(Symbol symbol, string targetOption, DateTime start, DateTime? end = null, Resolution? resolution = null,
342  bool fillForward = true, bool extendedMarketHours = false)
343  {
344  symbol = GetOptionSymbolForHistoryRequest(symbol, targetOption, resolution, fillForward);
345 
346  return OptionHistory(symbol, start, end, resolution, fillForward, extendedMarketHours);
347  }
348 
349  /// <summary>
350  /// Gets <see cref="OptionHistory"/> object for a given symbol, date and resolution
351  /// </summary>
352  /// <param name="symbol">The symbol to retrieve historical option data for</param>
353  /// <param name="targetOption">The target option ticker. This is useful when the option ticker does not match the underlying, e.g. SPX index and the SPXW weekly option. If null is provided will use underlying</param>
354  /// <param name="start">The history request start time</param>
355  /// <param name="end">The history request end time. Defaults to 1 day if null</param>
356  /// <param name="resolution">The resolution to request</param>
357  /// <param name="fillForward">True to fill forward missing data, false otherwise</param>
358  /// <param name="extendedMarketHours">True to include extended market hours data, false otherwise</param>
359  /// <returns>A <see cref="OptionHistory"/> object that contains historical option data.</returns>
360  [Obsolete("Please use the 'OptionHistory()' API")]
361  public OptionHistory GetOptionHistory(Symbol symbol, string targetOption, DateTime start, DateTime? end = null, Resolution? resolution = null,
362  bool fillForward = true, bool extendedMarketHours = false)
363  {
364  return OptionHistory(symbol, targetOption, start, end, resolution, fillForward, extendedMarketHours);
365  }
366 
367  /// <summary>
368  /// Gets <see cref="OptionHistory"/> object for a given symbol, date and resolution
369  /// </summary>
370  /// <param name="symbol">The symbol to retrieve historical option data for</param>
371  /// <param name="start">The history request start time</param>
372  /// <param name="end">The history request end time. Defaults to 1 day if null</param>
373  /// <param name="resolution">The resolution to request</param>
374  /// <param name="fillForward">True to fill forward missing data, false otherwise</param>
375  /// <param name="extendedMarketHours">True to include extended market hours data, false otherwise</param>
376  /// <returns>A <see cref="OptionHistory"/> object that contains historical option data.</returns>
377  public OptionHistory OptionHistory(Symbol symbol, DateTime start, DateTime? end = null, Resolution? resolution = null,
378  bool fillForward = true, bool extendedMarketHours = false)
379  {
380  if (!end.HasValue || end.Value == start)
381  {
382  end = start.AddDays(1);
383  }
384 
385  // Load a canonical option Symbol if the user provides us with an underlying Symbol
386  symbol = GetOptionSymbolForHistoryRequest(symbol, null, resolution, fillForward);
387 
388  IEnumerable<Symbol> symbols;
389  if (symbol.IsCanonical())
390  {
391  // canonical symbol, lets find the contracts
392  var option = Securities[symbol] as Option;
393  var resolutionToUseForUnderlying = resolution ?? SubscriptionManager.SubscriptionDataConfigService
395  .GetHighestResolution();
396  if (!Securities.ContainsKey(symbol.Underlying))
397  {
398  if (symbol.Underlying.SecurityType == SecurityType.Equity)
399  {
400  // only add underlying if not present
401  AddEquity(symbol.Underlying.Value, resolutionToUseForUnderlying, fillForward: fillForward,
402  extendedMarketHours: extendedMarketHours);
403  }
404  else if (symbol.Underlying.SecurityType == SecurityType.Index)
405  {
406  // only add underlying if not present
407  AddIndex(symbol.Underlying.Value, resolutionToUseForUnderlying, fillForward: fillForward);
408  }
409  else if(symbol.Underlying.SecurityType == SecurityType.Future && symbol.Underlying.IsCanonical())
410  {
411  AddFuture(symbol.Underlying.ID.Symbol, resolutionToUseForUnderlying, fillForward: fillForward,
412  extendedMarketHours: extendedMarketHours);
413  }
414  else if (symbol.Underlying.SecurityType == SecurityType.Future)
415  {
416  AddFutureContract(symbol.Underlying, resolutionToUseForUnderlying, fillForward: fillForward,
417  extendedMarketHours: extendedMarketHours);
418  }
419  }
420  var allSymbols = new List<Symbol>();
421  for (var date = start; date < end; date = date.AddDays(1))
422  {
423  if (option.Exchange.DateIsOpen(date))
424  {
425  allSymbols.AddRange(OptionChainProvider.GetOptionContractList(symbol, date));
426  }
427  }
428 
429  var optionFilterUniverse = new OptionFilterUniverse(option);
430  var distinctSymbols = allSymbols.Distinct();
431  symbols = base.History(symbol.Underlying, start, end.Value, resolution)
432  .SelectMany(x =>
433  {
434  // the option chain symbols wont change so we can set 'exchangeDateChange' to false always
435  optionFilterUniverse.Refresh(distinctSymbols, x, x.EndTime);
436  return option.ContractFilter.Filter(optionFilterUniverse);
437  })
438  .Distinct().Concat(new[] { symbol.Underlying });
439  }
440  else
441  {
442  // the symbol is a contract
443  symbols = new List<Symbol>{ symbol };
444  }
445 
446  return new OptionHistory(History(symbols, start, end.Value, resolution, fillForward, extendedMarketHours));
447  }
448 
449  /// <summary>
450  /// Gets <see cref="OptionHistory"/> object for a given symbol, date and resolution
451  /// </summary>
452  /// <param name="symbol">The symbol to retrieve historical option data for</param>
453  /// <param name="start">The history request start time</param>
454  /// <param name="end">The history request end time. Defaults to 1 day if null</param>
455  /// <param name="resolution">The resolution to request</param>
456  /// <param name="fillForward">True to fill forward missing data, false otherwise</param>
457  /// <param name="extendedMarketHours">True to include extended market hours data, false otherwise</param>
458  /// <returns>A <see cref="OptionHistory"/> object that contains historical option data.</returns>
459  [Obsolete("Please use the 'OptionHistory()' API")]
460  public OptionHistory GetOptionHistory(Symbol symbol, DateTime start, DateTime? end = null, Resolution? resolution = null,
461  bool fillForward = true, bool extendedMarketHours = false)
462  {
463  return OptionHistory(symbol, start, end, resolution, fillForward, extendedMarketHours);
464  }
465 
466  /// <summary>
467  /// Gets <see cref="FutureHistory"/> object for a given symbol, date and resolution
468  /// </summary>
469  /// <param name="symbol">The symbol to retrieve historical future data for</param>
470  /// <param name="start">The history request start time</param>
471  /// <param name="end">The history request end time. Defaults to 1 day if null</param>
472  /// <param name="resolution">The resolution to request</param>
473  /// <param name="fillForward">True to fill forward missing data, false otherwise</param>
474  /// <param name="extendedMarketHours">True to include extended market hours data, false otherwise</param>
475  /// <returns>A <see cref="FutureHistory"/> object that contains historical future data.</returns>
476  public FutureHistory FutureHistory(Symbol symbol, DateTime start, DateTime? end = null, Resolution? resolution = null,
477  bool fillForward = true, bool extendedMarketHours = false)
478  {
479  if (!end.HasValue || end.Value == start)
480  {
481  end = start.AddDays(1);
482  }
483 
484  var allSymbols = new HashSet<Symbol>();
485  if (symbol.IsCanonical())
486  {
487  // canonical symbol, lets find the contracts
488  var future = Securities[symbol] as Future;
489 
490  for (var date = start; date < end; date = date.AddDays(1))
491  {
492  if (future.Exchange.DateIsOpen(date))
493  {
494  var allList = FutureChainProvider.GetFutureContractList(future.Symbol, date);
495 
496  allSymbols.UnionWith(future.ContractFilter.Filter(new FutureFilterUniverse(allList, date)));
497  }
498  }
499  }
500  else
501  {
502  // the symbol is a contract
503  allSymbols.Add(symbol);
504  }
505 
506  return new FutureHistory(History(allSymbols, start, end.Value, resolution, fillForward, extendedMarketHours));
507  }
508 
509  /// <summary>
510  /// Gets <see cref="FutureHistory"/> object for a given symbol, date and resolution
511  /// </summary>
512  /// <param name="symbol">The symbol to retrieve historical future data for</param>
513  /// <param name="start">The history request start time</param>
514  /// <param name="end">The history request end time. Defaults to 1 day if null</param>
515  /// <param name="resolution">The resolution to request</param>
516  /// <param name="fillForward">True to fill forward missing data, false otherwise</param>
517  /// <param name="extendedMarketHours">True to include extended market hours data, false otherwise</param>
518  /// <returns>A <see cref="FutureHistory"/> object that contains historical future data.</returns>
519  [Obsolete("Please use the 'FutureHistory()' API")]
520  public FutureHistory GetFutureHistory(Symbol symbol, DateTime start, DateTime? end = null, Resolution? resolution = null,
521  bool fillForward = true, bool extendedMarketHours = false)
522  {
523  return FutureHistory(symbol, start, end, resolution, fillForward, extendedMarketHours);
524  }
525 
526  /// <summary>
527  /// Gets the historical data of an indicator for the specified symbol. The exact number of bars will be returned.
528  /// The symbol must exist in the Securities collection.
529  /// </summary>
530  /// <param name="symbol">The symbol to retrieve historical data for</param>
531  /// <param name="periods">The number of bars to request</param>
532  /// <param name="resolution">The resolution to request</param>
533  /// <param name="selector">Selects a value from the BaseData to send into the indicator, if null defaults to the Value property of BaseData (x => x.Value)</param>
534  /// <returns>pandas.DataFrame of historical data of an indicator</returns>
535  public PyObject Indicator(IndicatorBase<IndicatorDataPoint> indicator, Symbol symbol, int period, Resolution? resolution = null, Func<IBaseData, decimal> selector = null)
536  {
537  var history = History(new[] { symbol }, period, resolution);
538  return Indicator(indicator, history, selector);
539  }
540 
541  /// <summary>
542  /// Gets the historical data of a bar indicator for the specified symbol. The exact number of bars will be returned.
543  /// The symbol must exist in the Securities collection.
544  /// </summary>
545  /// <param name="symbol">The symbol to retrieve historical data for</param>
546  /// <param name="periods">The number of bars to request</param>
547  /// <param name="resolution">The resolution to request</param>
548  /// <param name="selector">Selects a value from the BaseData to send into the indicator, if null defaults to the Value property of BaseData (x => x.Value)</param>
549  /// <returns>pandas.DataFrame of historical data of a bar indicator</returns>
550  public PyObject Indicator(IndicatorBase<IBaseDataBar> indicator, Symbol symbol, int period, Resolution? resolution = null, Func<IBaseData, IBaseDataBar> selector = null)
551  {
552  var history = History(new[] { symbol }, period, resolution);
553  return Indicator(indicator, history, selector);
554  }
555 
556  /// <summary>
557  /// Gets the historical data of a bar indicator for the specified symbol. The exact number of bars will be returned.
558  /// The symbol must exist in the Securities collection.
559  /// </summary>
560  /// <param name="symbol">The symbol to retrieve historical data for</param>
561  /// <param name="periods">The number of bars to request</param>
562  /// <param name="resolution">The resolution to request</param>
563  /// <param name="selector">Selects a value from the BaseData to send into the indicator, if null defaults to the Value property of BaseData (x => x.Value)</param>
564  /// <returns>pandas.DataFrame of historical data of a bar indicator</returns>
565  public PyObject Indicator(IndicatorBase<TradeBar> indicator, Symbol symbol, int period, Resolution? resolution = null, Func<IBaseData, TradeBar> selector = null)
566  {
567  var history = History(new[] { symbol }, period, resolution);
568  return Indicator(indicator, history, selector);
569  }
570 
571  /// <summary>
572  /// Gets the historical data of an indicator for the specified symbol. The exact number of bars will be returned.
573  /// The symbol must exist in the Securities collection.
574  /// </summary>
575  /// <param name="indicator">Indicator</param>
576  /// <param name="symbol">The symbol to retrieve historical data for</param>
577  /// <param name="span">The span over which to retrieve recent historical data</param>
578  /// <param name="resolution">The resolution to request</param>
579  /// <param name="selector">Selects a value from the BaseData to send into the indicator, if null defaults to the Value property of BaseData (x => x.Value)</param>
580  /// <returns>pandas.DataFrame of historical data of an indicator</returns>
581  public PyObject Indicator(IndicatorBase<IndicatorDataPoint> indicator, Symbol symbol, TimeSpan span, Resolution? resolution = null, Func<IBaseData, decimal> selector = null)
582  {
583  var history = History(new[] { symbol }, span, resolution);
584  return Indicator(indicator, history, selector);
585  }
586 
587  /// <summary>
588  /// Gets the historical data of a bar indicator for the specified symbol. The exact number of bars will be returned.
589  /// The symbol must exist in the Securities collection.
590  /// </summary>
591  /// <param name="indicator">Indicator</param>
592  /// <param name="symbol">The symbol to retrieve historical data for</param>
593  /// <param name="span">The span over which to retrieve recent historical data</param>
594  /// <param name="resolution">The resolution to request</param>
595  /// <param name="selector">Selects a value from the BaseData to send into the indicator, if null defaults to the Value property of BaseData (x => x.Value)</param>
596  /// <returns>pandas.DataFrame of historical data of a bar indicator</returns>
597  public PyObject Indicator(IndicatorBase<IBaseDataBar> indicator, Symbol symbol, TimeSpan span, Resolution? resolution = null, Func<IBaseData, IBaseDataBar> selector = null)
598  {
599  var history = History(new[] { symbol }, span, resolution);
600  return Indicator(indicator, history, selector);
601  }
602 
603  /// <summary>
604  /// Gets the historical data of a bar indicator for the specified symbol. The exact number of bars will be returned.
605  /// The symbol must exist in the Securities collection.
606  /// </summary>
607  /// <param name="indicator">Indicator</param>
608  /// <param name="symbol">The symbol to retrieve historical data for</param>
609  /// <param name="span">The span over which to retrieve recent historical data</param>
610  /// <param name="resolution">The resolution to request</param>
611  /// <param name="selector">Selects a value from the BaseData to send into the indicator, if null defaults to the Value property of BaseData (x => x.Value)</param>
612  /// <returns>pandas.DataFrame of historical data of a bar indicator</returns>
613  public PyObject Indicator(IndicatorBase<TradeBar> indicator, Symbol symbol, TimeSpan span, Resolution? resolution = null, Func<IBaseData, TradeBar> selector = null)
614  {
615  var history = History(new[] { symbol }, span, resolution);
616  return Indicator(indicator, history, selector);
617  }
618 
619  /// <summary>
620  /// Gets the historical data of an indicator for the specified symbol. The exact number of bars will be returned.
621  /// The symbol must exist in the Securities collection.
622  /// </summary>
623  /// <param name="indicator">Indicator</param>
624  /// <param name="symbol">The symbol to retrieve historical data for</param>
625  /// <param name="start">The start time in the algorithm's time zone</param>
626  /// <param name="end">The end time in the algorithm's time zone</param>
627  /// <param name="resolution">The resolution to request</param>
628  /// <param name="selector">Selects a value from the BaseData to send into the indicator, if null defaults to the Value property of BaseData (x => x.Value)</param>
629  /// <returns>pandas.DataFrame of historical data of an indicator</returns>
630  public PyObject Indicator(IndicatorBase<IndicatorDataPoint> indicator, Symbol symbol, DateTime start, DateTime end, Resolution? resolution = null, Func<IBaseData, decimal> selector = null)
631  {
632  var history = History(new[] { symbol }, start, end, resolution);
633  return Indicator(indicator, history, selector);
634  }
635 
636  /// <summary>
637  /// Gets the historical data of a bar indicator for the specified symbol. The exact number of bars will be returned.
638  /// The symbol must exist in the Securities collection.
639  /// </summary>
640  /// <param name="indicator">Indicator</param>
641  /// <param name="symbol">The symbol to retrieve historical data for</param>
642  /// <param name="start">The start time in the algorithm's time zone</param>
643  /// <param name="end">The end time in the algorithm's time zone</param>
644  /// <param name="resolution">The resolution to request</param>
645  /// <param name="selector">Selects a value from the BaseData to send into the indicator, if null defaults to the Value property of BaseData (x => x.Value)</param>
646  /// <returns>pandas.DataFrame of historical data of a bar indicator</returns>
647  public PyObject Indicator(IndicatorBase<IBaseDataBar> indicator, Symbol symbol, DateTime start, DateTime end, Resolution? resolution = null, Func<IBaseData, IBaseDataBar> selector = null)
648  {
649  var history = History(new[] { symbol }, start, end, resolution);
650  return Indicator(indicator, history, selector);
651  }
652 
653  /// <summary>
654  /// Gets the historical data of a bar indicator for the specified symbol. The exact number of bars will be returned.
655  /// The symbol must exist in the Securities collection.
656  /// </summary>
657  /// <param name="indicator">Indicator</param>
658  /// <param name="symbol">The symbol to retrieve historical data for</param>
659  /// <param name="start">The start time in the algorithm's time zone</param>
660  /// <param name="end">The end time in the algorithm's time zone</param>
661  /// <param name="resolution">The resolution to request</param>
662  /// <param name="selector">Selects a value from the BaseData to send into the indicator, if null defaults to the Value property of BaseData (x => x.Value)</param>
663  /// <returns>pandas.DataFrame of historical data of a bar indicator</returns>
664  public PyObject Indicator(IndicatorBase<TradeBar> indicator, Symbol symbol, DateTime start, DateTime end, Resolution? resolution = null, Func<IBaseData, TradeBar> selector = null)
665  {
666  var history = History(new[] { symbol }, start, end, resolution);
667  return Indicator(indicator, history, selector);
668  }
669 
670  /// <summary>
671  /// Will return the universe selection data and will optionally perform selection
672  /// </summary>
673  /// <typeparam name="T1">The universe selection universe data type, for example Fundamentals</typeparam>
674  /// <typeparam name="T2">The selection data type, for example Fundamental</typeparam>
675  /// <param name="start">The start date</param>
676  /// <param name="end">Optionally the end date, will default to today</param>
677  /// <param name="func">Optionally the universe selection function</param>
678  /// <returns>Enumerable of universe selection data for each date, filtered if the func was provided</returns>
679  public IEnumerable<IEnumerable<T2>> UniverseHistory<T1, T2>(DateTime start, DateTime? end = null, Func<IEnumerable<T2>, IEnumerable<Symbol>> func = null)
680  where T1 : BaseDataCollection
681  where T2 : IBaseData
682  {
683  var universeSymbol = ((BaseDataCollection)typeof(T1).GetBaseDataInstance()).UniverseSymbol();
684 
685  var symbols = new[] { universeSymbol };
686  var requests = CreateDateRangeHistoryRequests(new[] { universeSymbol }, typeof(T1), start, end ?? DateTime.UtcNow.Date);
687  var history = GetDataTypedHistory<BaseDataCollection>(requests).Select(x => x.Values.Single());
688 
689  HashSet<Symbol> filteredSymbols = null;
690  foreach (var data in history)
691  {
692  var castedType = data.Data.OfType<T2>();
693 
694  if (func != null)
695  {
696  var selection = func(castedType);
697  if (!ReferenceEquals(selection, Universe.Unchanged))
698  {
699  filteredSymbols = selection.ToHashSet();
700  }
701  yield return castedType.Where(x => filteredSymbols == null || filteredSymbols.Contains(x.Symbol));
702  }
703  else
704  {
705  yield return castedType;
706  }
707  }
708  }
709 
710  /// <summary>
711  /// Will return the universe selection data and will optionally perform selection
712  /// </summary>
713  /// <param name="universe">The universe to fetch the data for</param>
714  /// <param name="start">The start date</param>
715  /// <param name="end">Optionally the end date, will default to today</param>
716  /// <returns>Enumerable of universe selection data for each date, filtered if the func was provided</returns>
717  public IEnumerable<IEnumerable<BaseData>> UniverseHistory(Universe universe, DateTime start, DateTime? end = null)
718  {
719  return RunUniverseSelection(universe, start, end);
720  }
721 
722  /// <summary>
723  /// Will return the universe selection data and will optionally perform selection
724  /// </summary>
725  /// <param name="universe">The universe to fetch the data for</param>
726  /// <param name="start">The start date</param>
727  /// <param name="end">Optionally the end date, will default to today</param>
728  /// <param name="func">Optionally the universe selection function</param>
729  /// <returns>Enumerable of universe selection data for each date, filtered if the func was provided</returns>
730  public PyObject UniverseHistory(PyObject universe, DateTime start, DateTime? end = null, PyObject func = null)
731  {
732  if (universe.TryConvert<Universe>(out var convertedUniverse))
733  {
734  if (func != null)
735  {
736  throw new ArgumentException($"When providing a universe, the selection func argument isn't supported. Please provider a universe or a type and a func");
737  }
738  var filteredUniverseSelectionData = RunUniverseSelection(convertedUniverse, start, end);
739 
740  return GetDataFrame(filteredUniverseSelectionData);
741  }
742  // for backwards compatibility
743  if (universe.TryConvert<Type>(out var convertedType) && convertedType.IsAssignableTo(typeof(BaseDataCollection)))
744  {
745  end ??= DateTime.UtcNow.Date;
746  var universeSymbol = ((BaseDataCollection)convertedType.GetBaseDataInstance()).UniverseSymbol();
747  if (func == null)
748  {
749  return History(universe, universeSymbol, start, end.Value);
750  }
751 
752  var requests = CreateDateRangeHistoryRequests(new[] { universeSymbol }, convertedType, start, end.Value);
753  var history = History(requests);
754 
755  return GetDataFrame(GetFilteredSlice(history, func), convertedType);
756  }
757 
758  throw new ArgumentException($"Failed to convert given universe {universe}. Please provider a valid {nameof(Universe)}");
759  }
760 
761  /// <summary>
762  /// Gets Portfolio Statistics from a pandas.DataFrame with equity and benchmark values
763  /// </summary>
764  /// <param name="dataFrame">pandas.DataFrame with the information required to compute the Portfolio statistics</param>
765  /// <returns><see cref="PortfolioStatistics"/> object wrapped in a <see cref="PyDict"/> with the portfolio statistics.</returns>
766  public PyDict GetPortfolioStatistics(PyObject dataFrame)
767  {
768  var dictBenchmark = new SortedDictionary<DateTime, double>();
769  var dictEquity = new SortedDictionary<DateTime, double>();
770  var dictPL = new SortedDictionary<DateTime, double>();
771 
772  using (Py.GIL())
773  {
774  var result = new PyDict();
775 
776  try
777  {
778  // Converts the data from pandas.DataFrame into dictionaries keyed by time
779  var df = ((dynamic)dataFrame).dropna();
780  dictBenchmark = GetDictionaryFromSeries((PyObject)df["benchmark"]);
781  dictEquity = GetDictionaryFromSeries((PyObject)df["equity"]);
782  dictPL = GetDictionaryFromSeries((PyObject)df["equity"].pct_change());
783  }
784  catch (PythonException e)
785  {
786  result.SetItem("Runtime Error", e.Message.ToPython());
787  return result;
788  }
789 
790  // Convert the double into decimal
791  var equity = new SortedDictionary<DateTime, decimal>(dictEquity.ToDictionary(kvp => kvp.Key, kvp => (decimal)kvp.Value));
792  var profitLoss = new SortedDictionary<DateTime, decimal>(dictPL.ToDictionary(kvp => kvp.Key, kvp => double.IsNaN(kvp.Value) ? 0 : (decimal)kvp.Value));
793 
794  // Gets the last value of the day of the benchmark and equity
795  var listBenchmark = CalculateDailyRateOfChange(dictBenchmark);
796  var listPerformance = CalculateDailyRateOfChange(dictEquity);
797 
798  // Gets the startting capital
799  var startingCapital = Convert.ToDecimal(dictEquity.FirstOrDefault().Value);
800 
801  // call method to set tradingDayPerYear for Algorithm (use: backwards compatibility)
803 
804  // Compute portfolio statistics
805  var stats = new PortfolioStatistics(profitLoss, equity, new(), listPerformance, listBenchmark, startingCapital, RiskFreeInterestRateModel,
807 
808  result.SetItem("Average Win (%)", Convert.ToDouble(stats.AverageWinRate * 100).ToPython());
809  result.SetItem("Average Loss (%)", Convert.ToDouble(stats.AverageLossRate * 100).ToPython());
810  result.SetItem("Compounding Annual Return (%)", Convert.ToDouble(stats.CompoundingAnnualReturn * 100m).ToPython());
811  result.SetItem("Drawdown (%)", Convert.ToDouble(stats.Drawdown * 100).ToPython());
812  result.SetItem("Expectancy", Convert.ToDouble(stats.Expectancy).ToPython());
813  result.SetItem("Net Profit (%)", Convert.ToDouble(stats.TotalNetProfit * 100).ToPython());
814  result.SetItem("Sharpe Ratio", Convert.ToDouble(stats.SharpeRatio).ToPython());
815  result.SetItem("Win Rate (%)", Convert.ToDouble(stats.WinRate * 100).ToPython());
816  result.SetItem("Loss Rate (%)", Convert.ToDouble(stats.LossRate * 100).ToPython());
817  result.SetItem("Profit-Loss Ratio", Convert.ToDouble(stats.ProfitLossRatio).ToPython());
818  result.SetItem("Alpha", Convert.ToDouble(stats.Alpha).ToPython());
819  result.SetItem("Beta", Convert.ToDouble(stats.Beta).ToPython());
820  result.SetItem("Annual Standard Deviation", Convert.ToDouble(stats.AnnualStandardDeviation).ToPython());
821  result.SetItem("Annual Variance", Convert.ToDouble(stats.AnnualVariance).ToPython());
822  result.SetItem("Information Ratio", Convert.ToDouble(stats.InformationRatio).ToPython());
823  result.SetItem("Tracking Error", Convert.ToDouble(stats.TrackingError).ToPython());
824  result.SetItem("Treynor Ratio", Convert.ToDouble(stats.TreynorRatio).ToPython());
825 
826  return result;
827  }
828  }
829 
830  /// <summary>
831  /// Helper method to perform selection on the given data and filter it
832  /// </summary>
833  private IEnumerable<Slice> GetFilteredSlice(IEnumerable<Slice> history, dynamic func)
834  {
835  HashSet<Symbol> filteredSymbols = null;
836  foreach (var slice in history)
837  {
838  var filteredData = slice.AllData.OfType<BaseDataCollection>();
839  using (Py.GIL())
840  {
841  using PyObject selection = func(filteredData.SelectMany(baseData => baseData.Data));
842  if (!selection.TryConvert<object>(out var result) || !ReferenceEquals(result, Universe.Unchanged))
843  {
844  filteredSymbols = ((Symbol[])selection.AsManagedObject(typeof(Symbol[]))).ToHashSet();
845  }
846  }
847  yield return new Slice(slice.Time, filteredData.Where(x => {
848  if (filteredSymbols == null)
849  {
850  return true;
851  }
852  x.Data = new List<BaseData>(x.Data.Where(dataPoint => filteredSymbols.Contains(dataPoint.Symbol)));
853  return true;
854  }), slice.UtcTime);
855  }
856  }
857 
858  /// <summary>
859  /// Helper method to perform selection on the given data and filter it using the given universe
860  /// </summary>
861  private IEnumerable<BaseDataCollection> RunUniverseSelection(Universe universe, DateTime start, DateTime? end = null)
862  {
863  var history = History(universe, start, end ?? DateTime.UtcNow.Date);
864 
865  HashSet<Symbol> filteredSymbols = null;
866  foreach (var dataPoint in history)
867  {
868  var utcTime = dataPoint.EndTime.ConvertToUtc(universe.Configuration.ExchangeTimeZone);
869  var selection = universe.SelectSymbols(utcTime, dataPoint);
870  if (!ReferenceEquals(selection, Universe.Unchanged))
871  {
872  filteredSymbols = selection.ToHashSet();
873  }
874  dataPoint.Data = dataPoint.Data.Where(x => filteredSymbols == null || filteredSymbols.Contains(x.Symbol)).ToList();
875  yield return dataPoint;
876  }
877  }
878 
879  /// <summary>
880  /// Converts a pandas.Series into a <see cref="SortedDictionary{DateTime, Double}"/>
881  /// </summary>
882  /// <param name="series">pandas.Series to be converted</param>
883  /// <returns><see cref="SortedDictionary{DateTime, Double}"/> with pandas.Series information</returns>
884  private SortedDictionary<DateTime, double> GetDictionaryFromSeries(PyObject series)
885  {
886  var dictionary = new SortedDictionary<DateTime, double>();
887 
888  var pyDict = new PyDict(((dynamic)series).to_dict());
889  foreach (PyObject item in pyDict.Items())
890  {
891  var key = (DateTime)item[0].AsManagedObject(typeof(DateTime));
892  var value = (double)item[1].AsManagedObject(typeof(double));
893  dictionary.Add(key, value);
894  }
895 
896  return dictionary;
897  }
898 
899  /// <summary>
900  /// Calculates the daily rate of change
901  /// </summary>
902  /// <param name="dictionary"><see cref="IDictionary{DateTime, Double}"/> with prices keyed by time</param>
903  /// <returns><see cref="List{Double}"/> with daily rate of change</returns>
904  private List<double> CalculateDailyRateOfChange(IDictionary<DateTime, double> dictionary)
905  {
906  var daily = dictionary.GroupBy(kvp => kvp.Key.Date)
907  .ToDictionary(x => x.Key, v => v.LastOrDefault().Value)
908  .Values.ToArray();
909 
910  var rocp = new double[daily.Length];
911  for (var i = 1; i < daily.Length; i++)
912  {
913  rocp[i] = (daily[i] - daily[i - 1]) / daily[i - 1];
914  }
915  rocp[0] = 0;
916 
917  return rocp.ToList();
918  }
919 
920  /// <summary>
921  /// Gets the historical data of an indicator and convert it into pandas.DataFrame
922  /// </summary>
923  /// <param name="indicator">Indicator</param>
924  /// <param name="history">Historical data used to calculate the indicator</param>
925  /// <param name="selector">Selects a value from the BaseData to send into the indicator, if null defaults to the Value property of BaseData (x => x.Value)</param>
926  /// <returns>pandas.DataFrame containing the historical data of <param name="indicator"></returns>
927  private PyObject Indicator(IndicatorBase<IndicatorDataPoint> indicator, IEnumerable<Slice> history, Func<IBaseData, decimal> selector = null)
928  {
929  var properties = WireIndicatorProperties(indicator);
930 
931  selector = selector ?? (x => x.Value);
932 
933  history.PushThrough(bar =>
934  {
935  var value = selector(bar);
936  indicator.Update(bar.EndTime, value);
937  });
938 
939  return PandasConverter.GetIndicatorDataFrame(properties);
940  }
941 
942  /// <summary>
943  /// Gets the historical data of an bar indicator and convert it into pandas.DataFrame
944  /// </summary>
945  /// <param name="indicator">Bar indicator</param>
946  /// <param name="history">Historical data used to calculate the indicator</param>
947  /// <param name="selector">Selects a value from the BaseData to send into the indicator, if null defaults to the Value property of BaseData (x => x.Value)</param>
948  /// <returns>pandas.DataFrame containing the historical data of <param name="indicator"></returns>
949  private PyObject Indicator<T>(IndicatorBase<T> indicator, IEnumerable<Slice> history, Func<IBaseData, T> selector = null)
950  where T : IBaseData
951  {
952  var properties = WireIndicatorProperties(indicator);
953 
954  selector = selector ?? (x => (T)x);
955 
956  history.PushThrough(bar => indicator.Update(selector(bar)));
957 
958  return PandasConverter.GetIndicatorDataFrame(properties);
959  }
960 
961  /// <summary>
962  /// Gets a value of a property
963  /// </summary>
964  /// <param name="baseData">Object with the desired property</param>
965  /// <param name="fullName">Property name</param>
966  /// <returns>Property value</returns>
967  private object GetPropertyValue(object baseData, string fullName)
968  {
969  foreach (var name in fullName.Split('.'))
970  {
971  if (baseData == null) return null;
972 
973  // TODO this is expensive and can be cached
974  var info = baseData.GetType().GetProperty(name);
975 
976  baseData = info?.GetValue(baseData, null);
977  }
978 
979  return baseData;
980  }
981 
982  /// <summary>
983  /// Get all fundamental data for given symbols
984  /// </summary>
985  /// <param name="symbols">The symbols to retrieve fundamental data for</param>
986  /// <param name="start">The start date of selected data</param>
987  /// <param name="end">The end date of selected data</param>
988  /// <returns>DataDictionary of Enumerable IBaseData</returns>
989  private Dictionary<DateTime, DataDictionary<dynamic>> GetAllFundamental(IEnumerable<Symbol> symbols, string selector, DateTime? start = null, DateTime? end = null)
990  {
991  //SubscriptionRequest does not except nullable DateTimes, so set a startTime and endTime
992  var startTime = start.HasValue ? (DateTime)start : QuantConnect.Time.Start;
993  var endTime = end.HasValue ? (DateTime) end : DateTime.UtcNow.Date;
994 
995  //Collection to store our results
996  var data = new Dictionary<DateTime, DataDictionary<dynamic>>();
997 
998  //Get all data for each symbol and fill our dictionary
999  foreach (var symbol in symbols)
1000  {
1001  var exchangeHours = MarketHoursDatabase.GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType);
1002  foreach (var date in QuantConnect.Time.EachTradeableDayInTimeZone(exchangeHours, startTime, endTime, TimeZones.NewYork))
1003  {
1004  var currentData = new Fundamental(date, symbol);
1005  var time = currentData.EndTime;
1006  object dataPoint = currentData;
1007  if (!string.IsNullOrWhiteSpace(selector))
1008  {
1009  dataPoint = GetPropertyValue(currentData, selector);
1010  if (BaseFundamentalDataProvider.IsNone(dataPoint))
1011  {
1012  dataPoint = null;
1013  }
1014  }
1015 
1016  if (!data.TryGetValue(time, out var dataAtTime))
1017  {
1018  dataAtTime = data[time] = new DataDictionary<dynamic>(time);
1019  }
1020  dataAtTime.Add(currentData.Symbol, dataPoint);
1021  }
1022  }
1023  return data;
1024  }
1025 
1026  private Symbol GetOptionSymbolForHistoryRequest(Symbol symbol, string targetOption, Resolution? resolution, bool fillForward)
1027  {
1028  // Load a canonical option Symbol if the user provides us with an underlying Symbol
1029  if (!symbol.SecurityType.IsOption())
1030  {
1031  var option = AddOption(symbol, targetOption, resolution, symbol.ID.Market, fillForward);
1032 
1033  // Allow 20 strikes from the money for futures. No expiry filter is applied
1034  // so that any future contract provided will have data returned.
1035  if (symbol.SecurityType == SecurityType.Future && symbol.IsCanonical())
1036  {
1037  throw new ArgumentException("The Future Symbol provided is a canonical Symbol (i.e. a Symbol representing all Futures), which is not supported at this time. " +
1038  "If you are using the Symbol accessible from `AddFuture(...)`, use the Symbol from `AddFutureContract(...)` instead. " +
1039  "You can use `qb.FutureOptionChainProvider(canonicalFuture, datetime)` to get a list of futures contracts for a given date, and add them to your algorithm with `AddFutureContract(symbol, Resolution)`.");
1040  }
1041  if (symbol.SecurityType == SecurityType.Future && !symbol.IsCanonical())
1042  {
1043  option.SetFilter(universe => universe.Strikes(-10, +10));
1044  }
1045 
1046  symbol = option.Symbol;
1047  }
1048 
1049  return symbol;
1050  }
1051 
1052  private Dictionary<string, List<IndicatorDataPoint>> WireIndicatorProperties(IndicatorBase indicator)
1053  {
1054  // Reset the indicator
1055  indicator.Reset();
1056 
1057  // Create a dictionary of the properties
1058  var name = indicator.GetType().Name;
1059 
1060  var properties = indicator.GetType().GetProperties()
1061  .Where(x => x.PropertyType.IsGenericType && x.Name != "Consolidators" && x.Name != "Window")
1062  .ToDictionary(x => x.Name, y => new List<IndicatorDataPoint>());
1063  properties.Add(name, new List<IndicatorDataPoint>());
1064 
1065  indicator.Updated += (s, e) =>
1066  {
1067  if (!indicator.IsReady)
1068  {
1069  return;
1070  }
1071 
1072  foreach (var kvp in properties)
1073  {
1074  var dataPoint = kvp.Key == name ? e : GetPropertyValue(s, kvp.Key + ".Current");
1075  kvp.Value.Add((IndicatorDataPoint)dataPoint);
1076  }
1077  };
1078 
1079  return properties;
1080  }
1081 
1082  private static void RecycleMemory()
1083  {
1084  Task.Delay(TimeSpan.FromSeconds(20)).ContinueWith(_ =>
1085  {
1086  if (Logging.Log.DebuggingEnabled)
1087  {
1088  Logging.Log.Debug($"QuantBook.RecycleMemory(): running...");
1089  }
1090 
1091  GC.Collect();
1092 
1093  RecycleMemory();
1094  }, TaskScheduler.Current);
1095  }
1096  }
1097 }