Lean  $LEAN_TAG$
PortfolioLooper.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 Deedle;
17 using QuantConnect.Orders;
19 using QuantConnect.Data;
26 using QuantConnect.Logging;
27 using QuantConnect.Packets;
29 using QuantConnect.Util;
30 using System;
31 using System.Collections.Generic;
32 using System.Linq;
34 
35 namespace QuantConnect.Report
36 {
37  /// <summary>
38  /// Runs LEAN to calculate the portfolio at a given time from <see cref="Order"/> objects.
39  /// Generates and returns <see cref="PointInTimePortfolio"/> objects that represents
40  /// the holdings and other miscellaneous metrics at a point in time by reprocessing the orders
41  /// as they were filled.
42  /// </summary>
43  public class PortfolioLooper : IDisposable
44  {
45  /// <summary>
46  /// Default resolution to read. This will affect the granularity of the results generated for FX and Crypto
47  /// </summary>
48  private const Resolution _resolution = Resolution.Hour;
49 
50  private SecurityService _securityService;
51  private DataManager _dataManager;
52  private IResultHandler _resultHandler;
53  private IDataCacheProvider _cacheProvider;
54  private IEnumerable<Slice> _conversionSlices = new List<Slice>();
55 
56  /// <summary>
57  /// QCAlgorithm derived class that sets up internal data feeds for
58  /// use with crypto and forex data, as well as managing the <see cref="SecurityPortfolioManager"/>
59  /// </summary>
60  public PortfolioLooperAlgorithm Algorithm { get; protected set; }
61 
62  /// <summary>
63  /// Creates an instance of the PortfolioLooper class
64  /// </summary>
65  /// <param name="startingCash">Equity curve</param>
66  /// <param name="orders">Order events</param>
67  /// <param name="resolution">Optional parameter to override default resolution (Hourly)</param>
68  /// <param name="algorithmConfiguration">Optional parameter to override default algorithm configuration</param>
69  private PortfolioLooper(double startingCash, List<Order> orders, Resolution resolution = _resolution,
70  AlgorithmConfiguration algorithmConfiguration = null)
71  {
72  // Initialize the providers that the HistoryProvider requires
73  var factorFileProvider = Composer.Instance.GetExportedValueByTypeName<IFactorFileProvider>("LocalDiskFactorFileProvider");
74  var mapFileProvider = Composer.Instance.GetExportedValueByTypeName<IMapFileProvider>("LocalDiskMapFileProvider");
75  _cacheProvider = new ZipDataCacheProvider(new DefaultDataProvider(), false);
76  var historyProvider = new SubscriptionDataReaderHistoryProvider();
77 
78  Algorithm = new PortfolioLooperAlgorithm((decimal)startingCash, orders, algorithmConfiguration);
79  var dataPermissionManager = new DataPermissionManager();
80  historyProvider.Initialize(new HistoryProviderInitializeParameters(null, null, null, _cacheProvider, mapFileProvider, factorFileProvider, (_) => { }, false, dataPermissionManager, Algorithm.ObjectStore, Algorithm.Settings));
81  Algorithm.SetHistoryProvider(historyProvider);
82 
83  // Dummy LEAN datafeed classes and initializations that essentially do nothing
84  var job = new BacktestNodePacket(1, 2, "3", null, 9m, $"");
85  var feed = new MockDataFeed();
86 
87  // Create MHDB and Symbol properties DB instances for the DataManager
88  var marketHoursDatabase = MarketHoursDatabase.FromDataFolder();
89  var symbolPropertiesDataBase = SymbolPropertiesDatabase.FromDataFolder();
90  _dataManager = new DataManager(feed,
92  Algorithm,
94  marketHoursDatabase,
95  symbolPropertiesDataBase,
96  Algorithm,
99  algorithm: Algorithm),
100  dataPermissionManager,
101  new DefaultDataProvider()),
102  Algorithm,
104  marketHoursDatabase,
105  false,
107  dataPermissionManager);
108 
109  _securityService = new SecurityService(Algorithm.Portfolio.CashBook,
110  marketHoursDatabase,
111  symbolPropertiesDataBase,
112  Algorithm,
115  algorithm: Algorithm);
116 
117  var transactions = new BacktestingTransactionHandler();
118  _resultHandler = new BacktestingResultHandler();
119 
120  // Initialize security services and other properties so that we
121  // don't get null reference exceptions during our re-calculation
122  Algorithm.Securities.SetSecurityService(_securityService);
124 
125  // Initialize the algorithm before adding any securities
128 
129  // Initializes all the proper Securities from the orders provided by the user
130  Algorithm.FromOrders(orders);
131 
132  // More initialization, this time with Algorithm and other misc. classes
133  _resultHandler.Initialize(new (job, new Messaging.Messaging(), new Api.Api(), transactions, mapFileProvider));
135 
137 
138  transactions.Initialize(Algorithm, new BacktestingBrokerage(Algorithm), _resultHandler);
139  feed.Initialize(Algorithm, job, _resultHandler, null, null, null, _dataManager, null, null);
140 
141  // Begin setting up the currency conversion feed if needed
142  var coreSecurities = Algorithm.Securities.Values.ToList();
143 
145  var conversionSecurities = Algorithm.Securities.Values.Where(s => !coreSecurities.Contains(s)).ToList();
146 
147  // Skip the history request if we don't need to convert anything
148  if (conversionSecurities.Any())
149  {
150  // Point-in-time Slices to convert FX and Crypto currencies to the portfolio currency
151  _conversionSlices = GetHistory(Algorithm, conversionSecurities, resolution);
152  }
153  }
154 
155  /// <summary>
156  /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
157  /// </summary>
158  public void Dispose()
159  {
160  _dataManager.RemoveAllSubscriptions();
161  _cacheProvider.DisposeSafely();
162  _resultHandler.Exit();
163  }
164 
165  /// <summary>
166  /// Internal method to get the history for the given securities
167  /// </summary>
168  /// <param name="algorithm">Algorithm</param>
169  /// <param name="securities">Securities to get history for</param>
170  /// <param name="resolution">Resolution to retrieve data in</param>
171  /// <returns>History of the given securities</returns>
172  /// <remarks>Method is static because we want to use it from the constructor as well</remarks>
173  private static IEnumerable<Slice> GetHistory(IAlgorithm algorithm, List<Security> securities, Resolution resolution)
174  {
175  var historyRequests = new List<Data.HistoryRequest>();
176  var historyRequestFactory = new HistoryRequestFactory(algorithm);
177 
178  // Create the history requests
179  foreach (var security in securities)
180  {
181  var configs = algorithm.SubscriptionManager
183  .GetSubscriptionDataConfigs(security.Symbol, includeInternalConfigs: true);
184 
185  // we need to order and select a specific configuration type
186  // so the conversion rate is deterministic
187  var configToUse = configs.OrderBy(x => x.TickType).First();
188 
189  var startTime = historyRequestFactory.GetStartTimeAlgoTz(
190  security.Symbol,
191  1,
192  resolution,
193  security.Exchange.Hours,
194  configToUse.DataTimeZone,
195  configToUse.Type);
196  var endTime = algorithm.EndDate;
197 
198  historyRequests.Add(historyRequestFactory.CreateHistoryRequest(
199  configToUse,
200  startTime,
201  endTime,
202  security.Exchange.Hours,
203  resolution
204  ));
205  }
206 
207  return algorithm.HistoryProvider.GetHistory(historyRequests, algorithm.TimeZone).ToList();
208  }
209 
210  /// <summary>
211  /// Gets the history for the given symbols from the <paramref name="start"/> to the <paramref name="end"/>
212  /// </summary>
213  /// <param name="symbols">Symbols to request history for</param>
214  /// <param name="start">Start date of history request</param>
215  /// <param name="end">End date of history request</param>
216  /// <param name="resolution">Resolution of history request</param>
217  /// <returns>Enumerable of slices</returns>
218  public static IEnumerable<Slice> GetHistory(List<Symbol> symbols, DateTime start, DateTime end, Resolution resolution)
219  {
220  // Handles the conversion of Symbol to Security for us.
221  var looper = new PortfolioLooper(0, new List<Order>(), resolution);
222  var securities = new List<Security>();
223 
224  looper.Algorithm.SetStartDate(start);
225  looper.Algorithm.SetEndDate(end);
226 
227  foreach (var symbol in symbols)
228  {
229  var configs = looper.Algorithm.SubscriptionManager.SubscriptionDataConfigService.Add(symbol, resolution, false, false);
230  securities.Add(looper.Algorithm.Securities.CreateSecurity(symbol, configs));
231  }
232 
233  return GetHistory(looper.Algorithm, securities, resolution);
234  }
235 
236  /// <summary>
237  /// Gets the point in time portfolio over multiple deployments
238  /// </summary>
239  /// <param name="equityCurve">Equity curve series</param>
240  /// <param name="orders">Orders</param>
241  /// <param name="algorithmConfiguration">Optional parameter to override default algorithm configuration</param>
242  /// <param name="liveSeries">Equity curve series originates from LiveResult</param>
243  /// <returns>Enumerable of <see cref="PointInTimePortfolio"/></returns>
244  public static IEnumerable<PointInTimePortfolio> FromOrders(Series<DateTime, double> equityCurve, IEnumerable<Order> orders,
245  AlgorithmConfiguration algorithmConfiguration = null, bool liveSeries = false)
246  {
247  // Don't do anything if we have no orders or equity curve to process
248  if (!orders.Any() || equityCurve.IsEmpty)
249  {
250  yield break;
251  }
252 
253  // Chunk different deployments into separate Lists for separate processing
254  var portfolioDeployments = new List<List<Order>>();
255 
256  // Orders are guaranteed to start counting from 1. This ensures that we have
257  // no collision at all with the start of a deployment
258  var previousOrderId = 0;
259  var currentDeployment = new List<Order>();
260 
261  // Make use of reference semantics to add new deployments to the list
262  portfolioDeployments.Add(currentDeployment);
263 
264  foreach (var order in orders)
265  {
266  // In case we have two different deployments with only a single
267  // order in the deployments, <= was chosen because it covers duplicate values
268  if (order.Id <= previousOrderId)
269  {
270  currentDeployment = new List<Order>();
271  portfolioDeployments.Add(currentDeployment);
272  }
273 
274  currentDeployment.Add(order);
275  previousOrderId = order.Id;
276  }
277 
278  PortfolioLooper looper = null;
279  PointInTimePortfolio prev = null;
280  foreach (var deploymentOrders in portfolioDeployments)
281  {
282  if (deploymentOrders.Count == 0)
283  {
284  Log.Trace($"PortfolioLooper.FromOrders(): Deployment contains no orders");
285  continue;
286  }
287  var startTime = deploymentOrders.First().Time;
288  var deployment = equityCurve.Where(kvp => kvp.Key <= startTime);
289  if (deployment.IsEmpty)
290  {
291  Log.Trace($"PortfolioLooper.FromOrders(): Equity series is empty after filtering with upper bound: {startTime}");
292  continue;
293  }
294 
295  // Skip any deployments that haven't been ran long enough to be generated in live mode
296  if (liveSeries && deploymentOrders.First().Time.Date == deploymentOrders.Last().Time.Date)
297  {
298  Log.Trace("PortfolioLooper.FromOrders(): Filtering deployment because it has not been deployed for more than one day");
299  continue;
300  }
301 
302  // For every deployment, we want to start fresh.
303  looper = new PortfolioLooper(deployment.LastValue(), deploymentOrders, algorithmConfiguration: algorithmConfiguration);
304 
305  foreach (var portfolio in looper.ProcessOrders(deploymentOrders))
306  {
307  prev = portfolio;
308  yield return portfolio;
309  }
310  }
311 
312  if (prev != null)
313  {
314  yield return new PointInTimePortfolio(prev, equityCurve.LastKey());
315  }
316 
317  looper.DisposeSafely();
318  }
319 
320  /// <summary>
321  /// Process the orders
322  /// </summary>
323  /// <param name="orders">orders</param>
324  /// <returns>PointInTimePortfolio</returns>
325  private IEnumerable<PointInTimePortfolio> ProcessOrders(IEnumerable<Order> orders)
326  {
327  // Portfolio.ProcessFill(...) does not filter out invalid orders. We must do so ourselves
328  foreach (var order in orders)
329  {
330  Algorithm.SetDateTime(order.Time);
331 
332  var orderSecurity = Algorithm.Securities[order.Symbol];
333  DateTime lastFillTime;
334 
335  if ((order.Type == OrderType.MarketOnOpen || order.Type == OrderType.MarketOnClose) &&
336  (order.Status == OrderStatus.Filled || order.Status == OrderStatus.PartiallyFilled) && order.LastFillTime == null)
337  {
338  lastFillTime = order.Time;
339  }
340  else if (order.LastFillTime == null)
341  {
342  Log.Trace($"Order with ID: {order.Id} has been skipped because of null LastFillTime");
343  continue;
344  }
345  else
346  {
347  lastFillTime = order.LastFillTime.Value;
348  }
349 
350  var tick = new Tick { Quantity = order.Quantity, AskPrice = order.Price, BidPrice = order.Price, Value = order.Price, EndTime = lastFillTime };
351  var tradeBar = new TradeBar
352  {
353  Open = order.Price,
354  High = order.Price,
355  Low = order.Price,
356  Close = order.Price,
357  Volume = order.Quantity,
358 
359  DataType = MarketDataType.TradeBar,
360  Period = TimeSpan.Zero,
361  Symbol = order.Symbol,
362  Time = lastFillTime,
363  };
364 
365  // Required for crypto so that the Cache Price is updated accordingly,
366  // since its `Security.Price` implementation explicitly requests TradeBars.
367  // For most asset types this might be enough as well, but there is the
368  // possibility that some trades might get filtered, so we cover that
369  // case by setting the market price via Tick as well.
370  orderSecurity.SetMarketPrice(tradeBar);
371  orderSecurity.SetMarketPrice(tick);
372 
373  // Check if we have a base currency (i.e. forex or crypto that requires currency conversion)
374  // to ensure the proper conversion rate is set for them
375  var baseCurrency = orderSecurity as IBaseCurrencySymbol;
376 
377  if (baseCurrency != null)
378  {
379  // We want slices that apply to either this point in time, or the last most recent point in time
380  var updateSlices = _conversionSlices.Where(x => x.Time <= order.Time).ToList();
381 
382  // This is put here because there can potentially be no slices
383  if (updateSlices.Count != 0)
384  {
385  var updateSlice = updateSlices.Last();
386 
387  foreach (var quoteBar in updateSlice.QuoteBars.Values)
388  {
389  Algorithm.Securities[quoteBar.Symbol].SetMarketPrice(quoteBar);
390  }
391  }
392  }
393 
394  // Update our cash holdings before we invalidate the portfolio value
395  // to calculate the proper cash value of other assets the algo owns
396  foreach (var cash in Algorithm.Portfolio.CashBook.Values.Where(x => x.CurrencyConversion != null))
397  {
398  cash.Update();
399  }
400 
401  // Securities prices might have been updated, so we need to recalculate how much
402  // money we have in our portfolio, otherwise we risk being out of date and
403  // calculate on stale data.
405 
406  var orderEvent = new OrderEvent(order, order.Time, Orders.Fees.OrderFee.Zero) { FillPrice = order.Price, FillQuantity = order.Quantity };
407 
408  // Process the order
409  Algorithm.Portfolio.ProcessFills(new List<OrderEvent> { orderEvent });
410 
411  // Create portfolio statistics and return back to the user
412  yield return new PointInTimePortfolio(order, Algorithm.Portfolio);
413  }
414  }
415  }
416 }