/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using Deedle; using QuantConnect.Orders; using QuantConnect.Brokerages.Backtesting; using QuantConnect.Data; using QuantConnect.Data.Market; using QuantConnect.Interfaces; using QuantConnect.Lean.Engine.DataFeeds; using QuantConnect.Lean.Engine.Results; using QuantConnect.Lean.Engine.Setup; using QuantConnect.Lean.Engine.TransactionHandlers; using QuantConnect.Logging; using QuantConnect.Packets; using QuantConnect.Securities; using QuantConnect.Util; using System; using System.Collections.Generic; using System.Linq; using QuantConnect.Lean.Engine.HistoricalData; namespace QuantConnect.Report { /// /// Runs LEAN to calculate the portfolio at a given time from objects. /// Generates and returns objects that represents /// the holdings and other miscellaneous metrics at a point in time by reprocessing the orders /// as they were filled. /// public class PortfolioLooper : IDisposable { /// /// Default resolution to read. This will affect the granularity of the results generated for FX and Crypto /// private const Resolution _resolution = Resolution.Hour; private SecurityService _securityService; private DataManager _dataManager; private IResultHandler _resultHandler; private IDataCacheProvider _cacheProvider; private IEnumerable _conversionSlices = new List(); /// /// QCAlgorithm derived class that sets up internal data feeds for /// use with crypto and forex data, as well as managing the /// public PortfolioLooperAlgorithm Algorithm { get; protected set; } /// /// Creates an instance of the PortfolioLooper class /// /// Equity curve /// Order events /// Optional parameter to override default resolution (Hourly) /// Optional parameter to override default algorithm configuration private PortfolioLooper(double startingCash, List orders, Resolution resolution = _resolution, AlgorithmConfiguration algorithmConfiguration = null) { // Initialize the providers that the HistoryProvider requires var factorFileProvider = Composer.Instance.GetExportedValueByTypeName("LocalDiskFactorFileProvider"); var mapFileProvider = Composer.Instance.GetExportedValueByTypeName("LocalDiskMapFileProvider"); _cacheProvider = new ZipDataCacheProvider(new DefaultDataProvider(), false); var historyProvider = new SubscriptionDataReaderHistoryProvider(); Algorithm = new PortfolioLooperAlgorithm((decimal)startingCash, orders, algorithmConfiguration); var dataPermissionManager = new DataPermissionManager(); historyProvider.Initialize(new HistoryProviderInitializeParameters(null, null, null, _cacheProvider, mapFileProvider, factorFileProvider, (_) => { }, false, dataPermissionManager, Algorithm.ObjectStore, Algorithm.Settings)); Algorithm.SetHistoryProvider(historyProvider); // Dummy LEAN datafeed classes and initializations that essentially do nothing var job = new BacktestNodePacket(1, 2, "3", null, 9m, $""); var feed = new MockDataFeed(); // Create MHDB and Symbol properties DB instances for the DataManager var marketHoursDatabase = MarketHoursDatabase.FromDataFolder(); var symbolPropertiesDataBase = SymbolPropertiesDatabase.FromDataFolder(); _dataManager = new DataManager(feed, new UniverseSelection( Algorithm, new SecurityService(Algorithm.Portfolio.CashBook, marketHoursDatabase, symbolPropertiesDataBase, Algorithm, RegisteredSecurityDataTypesProvider.Null, new SecurityCacheProvider(Algorithm.Portfolio), algorithm: Algorithm), dataPermissionManager, new DefaultDataProvider()), Algorithm, Algorithm.TimeKeeper, marketHoursDatabase, false, RegisteredSecurityDataTypesProvider.Null, dataPermissionManager); _securityService = new SecurityService(Algorithm.Portfolio.CashBook, marketHoursDatabase, symbolPropertiesDataBase, Algorithm, RegisteredSecurityDataTypesProvider.Null, new SecurityCacheProvider(Algorithm.Portfolio), algorithm: Algorithm); var transactions = new BacktestingTransactionHandler(); _resultHandler = new BacktestingResultHandler(); // Initialize security services and other properties so that we // don't get null reference exceptions during our re-calculation Algorithm.Securities.SetSecurityService(_securityService); Algorithm.SubscriptionManager.SetDataManager(_dataManager); // Initialize the algorithm before adding any securities Algorithm.Initialize(); Algorithm.PostInitialize(); // Initializes all the proper Securities from the orders provided by the user Algorithm.FromOrders(orders); // More initialization, this time with Algorithm and other misc. classes _resultHandler.Initialize(new (job, new Messaging.Messaging(), new Api.Api(), transactions, mapFileProvider)); _resultHandler.SetAlgorithm(Algorithm, Algorithm.Portfolio.TotalPortfolioValue); Algorithm.Transactions.SetOrderProcessor(transactions); transactions.Initialize(Algorithm, new BacktestingBrokerage(Algorithm), _resultHandler); feed.Initialize(Algorithm, job, _resultHandler, null, null, null, _dataManager, null, null); // Begin setting up the currency conversion feed if needed var coreSecurities = Algorithm.Securities.Values.ToList(); BaseSetupHandler.SetupCurrencyConversions(Algorithm, _dataManager.UniverseSelection); var conversionSecurities = Algorithm.Securities.Values.Where(s => !coreSecurities.Contains(s)).ToList(); // Skip the history request if we don't need to convert anything if (conversionSecurities.Any()) { // Point-in-time Slices to convert FX and Crypto currencies to the portfolio currency _conversionSlices = GetHistory(Algorithm, conversionSecurities, resolution); } } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { _dataManager.RemoveAllSubscriptions(); _cacheProvider.DisposeSafely(); _resultHandler.Exit(); } /// /// Internal method to get the history for the given securities /// /// Algorithm /// Securities to get history for /// Resolution to retrieve data in /// History of the given securities /// Method is static because we want to use it from the constructor as well private static IEnumerable GetHistory(IAlgorithm algorithm, List securities, Resolution resolution) { var historyRequests = new List(); var historyRequestFactory = new HistoryRequestFactory(algorithm); // Create the history requests foreach (var security in securities) { var configs = algorithm.SubscriptionManager .SubscriptionDataConfigService .GetSubscriptionDataConfigs(security.Symbol, includeInternalConfigs: true); // we need to order and select a specific configuration type // so the conversion rate is deterministic var configToUse = configs.OrderBy(x => x.TickType).First(); var startTime = historyRequestFactory.GetStartTimeAlgoTz( security.Symbol, 1, resolution, security.Exchange.Hours, configToUse.DataTimeZone, configToUse.Type); var endTime = algorithm.EndDate; historyRequests.Add(historyRequestFactory.CreateHistoryRequest( configToUse, startTime, endTime, security.Exchange.Hours, resolution )); } return algorithm.HistoryProvider.GetHistory(historyRequests, algorithm.TimeZone).ToList(); } /// /// Gets the history for the given symbols from the to the /// /// Symbols to request history for /// Start date of history request /// End date of history request /// Resolution of history request /// Enumerable of slices public static IEnumerable GetHistory(List symbols, DateTime start, DateTime end, Resolution resolution) { // Handles the conversion of Symbol to Security for us. var looper = new PortfolioLooper(0, new List(), resolution); var securities = new List(); looper.Algorithm.SetStartDate(start); looper.Algorithm.SetEndDate(end); foreach (var symbol in symbols) { var configs = looper.Algorithm.SubscriptionManager.SubscriptionDataConfigService.Add(symbol, resolution, false, false); securities.Add(looper.Algorithm.Securities.CreateSecurity(symbol, configs)); } return GetHistory(looper.Algorithm, securities, resolution); } /// /// Gets the point in time portfolio over multiple deployments /// /// Equity curve series /// Orders /// Optional parameter to override default algorithm configuration /// Equity curve series originates from LiveResult /// Enumerable of public static IEnumerable FromOrders(Series equityCurve, IEnumerable orders, AlgorithmConfiguration algorithmConfiguration = null, bool liveSeries = false) { // Don't do anything if we have no orders or equity curve to process if (!orders.Any() || equityCurve.IsEmpty) { yield break; } // Chunk different deployments into separate Lists for separate processing var portfolioDeployments = new List>(); // Orders are guaranteed to start counting from 1. This ensures that we have // no collision at all with the start of a deployment var previousOrderId = 0; var currentDeployment = new List(); // Make use of reference semantics to add new deployments to the list portfolioDeployments.Add(currentDeployment); foreach (var order in orders) { // In case we have two different deployments with only a single // order in the deployments, <= was chosen because it covers duplicate values if (order.Id <= previousOrderId) { currentDeployment = new List(); portfolioDeployments.Add(currentDeployment); } currentDeployment.Add(order); previousOrderId = order.Id; } PortfolioLooper looper = null; PointInTimePortfolio prev = null; foreach (var deploymentOrders in portfolioDeployments) { if (deploymentOrders.Count == 0) { Log.Trace($"PortfolioLooper.FromOrders(): Deployment contains no orders"); continue; } var startTime = deploymentOrders.First().Time; var deployment = equityCurve.Where(kvp => kvp.Key <= startTime); if (deployment.IsEmpty) { Log.Trace($"PortfolioLooper.FromOrders(): Equity series is empty after filtering with upper bound: {startTime}"); continue; } // Skip any deployments that haven't been ran long enough to be generated in live mode if (liveSeries && deploymentOrders.First().Time.Date == deploymentOrders.Last().Time.Date) { Log.Trace("PortfolioLooper.FromOrders(): Filtering deployment because it has not been deployed for more than one day"); continue; } // For every deployment, we want to start fresh. looper = new PortfolioLooper(deployment.LastValue(), deploymentOrders, algorithmConfiguration: algorithmConfiguration); foreach (var portfolio in looper.ProcessOrders(deploymentOrders)) { prev = portfolio; yield return portfolio; } } if (prev != null) { yield return new PointInTimePortfolio(prev, equityCurve.LastKey()); } looper.DisposeSafely(); } /// /// Process the orders /// /// orders /// PointInTimePortfolio private IEnumerable ProcessOrders(IEnumerable orders) { // Portfolio.ProcessFill(...) does not filter out invalid orders. We must do so ourselves foreach (var order in orders) { Algorithm.SetDateTime(order.Time); var orderSecurity = Algorithm.Securities[order.Symbol]; DateTime lastFillTime; if ((order.Type == OrderType.MarketOnOpen || order.Type == OrderType.MarketOnClose) && (order.Status == OrderStatus.Filled || order.Status == OrderStatus.PartiallyFilled) && order.LastFillTime == null) { lastFillTime = order.Time; } else if (order.LastFillTime == null) { Log.Trace($"Order with ID: {order.Id} has been skipped because of null LastFillTime"); continue; } else { lastFillTime = order.LastFillTime.Value; } var tick = new Tick { Quantity = order.Quantity, AskPrice = order.Price, BidPrice = order.Price, Value = order.Price, EndTime = lastFillTime }; var tradeBar = new TradeBar { Open = order.Price, High = order.Price, Low = order.Price, Close = order.Price, Volume = order.Quantity, DataType = MarketDataType.TradeBar, Period = TimeSpan.Zero, Symbol = order.Symbol, Time = lastFillTime, }; // Required for crypto so that the Cache Price is updated accordingly, // since its `Security.Price` implementation explicitly requests TradeBars. // For most asset types this might be enough as well, but there is the // possibility that some trades might get filtered, so we cover that // case by setting the market price via Tick as well. orderSecurity.SetMarketPrice(tradeBar); orderSecurity.SetMarketPrice(tick); // Check if we have a base currency (i.e. forex or crypto that requires currency conversion) // to ensure the proper conversion rate is set for them var baseCurrency = orderSecurity as IBaseCurrencySymbol; if (baseCurrency != null) { // We want slices that apply to either this point in time, or the last most recent point in time var updateSlices = _conversionSlices.Where(x => x.Time <= order.Time).ToList(); // This is put here because there can potentially be no slices if (updateSlices.Count != 0) { var updateSlice = updateSlices.Last(); foreach (var quoteBar in updateSlice.QuoteBars.Values) { Algorithm.Securities[quoteBar.Symbol].SetMarketPrice(quoteBar); } } } // Update our cash holdings before we invalidate the portfolio value // to calculate the proper cash value of other assets the algo owns foreach (var cash in Algorithm.Portfolio.CashBook.Values.Where(x => x.CurrencyConversion != null)) { cash.Update(); } // Securities prices might have been updated, so we need to recalculate how much // money we have in our portfolio, otherwise we risk being out of date and // calculate on stale data. Algorithm.Portfolio.InvalidateTotalPortfolioValue(); var ticket = order.ToOrderTicket(Algorithm.Transactions); var orderEvent = new OrderEvent(order, order.Time, Orders.Fees.OrderFee.Zero) { FillPrice = order.Price, FillQuantity = order.Quantity, Ticket = ticket }; // Process the order Algorithm.Portfolio.ProcessFills(new List { orderEvent }); // Create portfolio statistics and return back to the user yield return new PointInTimePortfolio(order, Algorithm.Portfolio); } } } }