diff --git a/Common/Statistics/AlgorithmPerformance.cs b/Common/Statistics/AlgorithmPerformance.cs index d9f091acf131..ded3811d89f5 100644 --- a/Common/Statistics/AlgorithmPerformance.cs +++ b/Common/Statistics/AlgorithmPerformance.cs @@ -83,5 +83,16 @@ public AlgorithmPerformance() ClosedTrades = new List(); } + /// + /// Initializes a new instance of the class + /// + /// The performance instance to use as a base + /// The list of closed trades + public AlgorithmPerformance(AlgorithmPerformance other, List trades) + { + TradeStatistics = new TradeStatistics(trades); + PortfolioStatistics = other.PortfolioStatistics; + ClosedTrades = trades; + } } } diff --git a/Common/Statistics/Trade.cs b/Common/Statistics/Trade.cs index 35ef2a35c6bb..a4d20db36d4c 100644 --- a/Common/Statistics/Trade.cs +++ b/Common/Statistics/Trade.cs @@ -26,6 +26,11 @@ public class Trade { private List _symbols; + /// + /// A unique identifier for the trade + /// + internal long Id { get; set; } + /// /// The symbol of the traded instrument /// diff --git a/Common/Statistics/TradeBuilder.cs b/Common/Statistics/TradeBuilder.cs index b0b0cb010c30..f4a3d72da34d 100644 --- a/Common/Statistics/TradeBuilder.cs +++ b/Common/Statistics/TradeBuilder.cs @@ -58,6 +58,7 @@ public Position() private readonly FillMatchingMethod _matchingMethod; private SecurityManager _securities; private bool _liveMode; + private long _nextTradeId = 1; /// /// Initializes a new instance of the class @@ -561,6 +562,7 @@ private void AddNewTrade(Trade trade, OrderEvent fill) ? fill.IsWin(security, trade.ProfitLoss) : trade.ProfitLoss > 0; + trade.Id = _nextTradeId++; _closedTrades.Add(trade); // Due to memory constraints in live mode, we cap the number of trades diff --git a/Engine/Results/BacktestingResultHandler.cs b/Engine/Results/BacktestingResultHandler.cs index 0fedbcde5402..16484bcc4f92 100644 --- a/Engine/Results/BacktestingResultHandler.cs +++ b/Engine/Results/BacktestingResultHandler.cs @@ -197,8 +197,8 @@ private void Update() } //Get the runtime statistics from the user algorithm: - var summary = GenerateStatisticsResults(performanceCharts, estimatedStrategyCapacity: _capacityEstimate).Summary; - var runtimeStatistics = GetAlgorithmRuntimeStatistics(summary, _capacityEstimate); + var statisticsResult = GenerateStatisticsResults(performanceCharts, estimatedStrategyCapacity: _capacityEstimate); + var runtimeStatistics = GetAlgorithmRuntimeStatistics(statisticsResult.Summary, _capacityEstimate); var progress = _progressMonitor.Progress; @@ -225,8 +225,13 @@ private void Update() _nextS3Update = DateTime.UtcNow.AddSeconds(30); } + var deltaTrades = GetDeltaTrades(statisticsResult.TotalPerformance.ClosedTrades, LastDeltaTradePosition, shouldStop: tradeCount => tradeCount >= 50); + // Deliberately skip to the end of trade collection to prevent overloading backtesting UX + LastDeltaTradePosition = statisticsResult.TotalPerformance.ClosedTrades[0].Id; + var algorithmPerformance = new AlgorithmPerformance(statisticsResult.TotalPerformance, deltaTrades); + //2. Backtest Update -> Send the truncated packet to the backtester: - var splitPackets = SplitPackets(deltaCharts, deltaOrders, runtimeStatistics, progress, serverStatistics); + var splitPackets = SplitPackets(deltaCharts, deltaOrders, runtimeStatistics, progress, serverStatistics, algorithmPerformance); foreach (var backtestingPacket in splitPackets) { @@ -245,7 +250,9 @@ private void Update() /// /// Run over all the data and break it into smaller packets to ensure they all arrive at the terminal /// - public virtual IEnumerable SplitPackets(Dictionary deltaCharts, Dictionary deltaOrders, SortedDictionary runtimeStatistics, decimal progress, Dictionary serverStatistics) + public virtual IEnumerable SplitPackets(Dictionary deltaCharts, Dictionary deltaOrders, + SortedDictionary runtimeStatistics, decimal progress, Dictionary serverStatistics, + AlgorithmPerformance algorithmPerformance) { // break the charts into groups var splitPackets = new List(); @@ -267,6 +274,13 @@ public virtual IEnumerable SplitPackets(Dictionary 0) + { + // Add the trades into the charting packet: + splitPackets.Add(new BacktestResultPacket(_job, new BacktestResult { TotalPerformance = algorithmPerformance }, Algorithm.EndDate, Algorithm.StartDate, progress)); + } + //Add any user runtime statistics into the backtest. splitPackets.Add(new BacktestResultPacket(_job, new BacktestResult { ServerStatistics = serverStatistics, RuntimeStatistics = runtimeStatistics }, Algorithm.EndDate, Algorithm.StartDate, progress)); diff --git a/Engine/Results/BaseResultsHandler.cs b/Engine/Results/BaseResultsHandler.cs index 9073484947b8..0cb79e6312b0 100644 --- a/Engine/Results/BaseResultsHandler.cs +++ b/Engine/Results/BaseResultsHandler.cs @@ -14,12 +14,6 @@ * */ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using QuantConnect.Data.Market; @@ -34,6 +28,12 @@ using QuantConnect.Securities.Positions; using QuantConnect.Statistics; using QuantConnect.Util; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; namespace QuantConnect.Lean.Engine.Results { @@ -59,6 +59,9 @@ public abstract class BaseResultsHandler private Bar _currentAlgorithmEquity; + private List _temporaryPerformanceValues; + private List _temporaryBenchmarkValues; + /// /// String message saying: Strategy Equity /// @@ -114,6 +117,11 @@ public abstract class BaseResultsHandler /// protected int LastDeltaOrderPosition { get; set; } + /// + /// The last position consumed from the by + /// + protected long LastDeltaTradePosition { get; set; } + /// /// The last position consumed from the while determining delta order events /// @@ -122,7 +130,7 @@ public abstract class BaseResultsHandler /// /// Serializer settings to use /// - protected JsonSerializerSettings SerializerSettings { get; set; } = new () + protected JsonSerializerSettings SerializerSettings { get; set; } = new() { ContractResolver = new DefaultContractResolver { @@ -446,6 +454,26 @@ protected virtual Dictionary GetDeltaOrders(int orderEventsStartPosi return deltaOrders; } + /// + /// Gets the trades generated starting from the provided position, + /// which is determined by the and the + /// + /// The delta trades + protected virtual List GetDeltaTrades(List trades, long tradesStartId, Func shouldStop) + { + var deltaTrades = new List(); + foreach (var trade in trades.OrderBy(x => x.Id).Where(x => x.Id > tradesStartId)) + { + LastDeltaTradePosition = trade.Id; + deltaTrades.Add(trade); + if (shouldStop(deltaTrades.Count)) + { + break; + } + } + return deltaTrades; + } + /// /// Initialize the result handler with this result packet. /// @@ -466,7 +494,7 @@ public virtual void Initialize(ResultHandlerInitializeParameters parameters) SerializerSettings = new() { - Converters = new [] { new OrderEventJsonConverter(AlgorithmId) }, + Converters = new[] { new OrderEventJsonConverter(AlgorithmId) }, ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy @@ -636,9 +664,7 @@ public virtual void Sample(DateTime time) // Force an update for our values before doing our daily sample UpdatePortfolioValues(time); UpdateBenchmarkValue(time); - - var currentPortfolioValue = GetPortfolioValue(); - var portfolioPerformance = DailyPortfolioValue == 0 ? 0 : Math.Round((currentPortfolioValue - DailyPortfolioValue) * 100 / DailyPortfolioValue, 10); + GetPortfolioPerformance(out var currentPortfolioValue, out var portfolioPerformance); // Update our max portfolio value CumulativeMaxPortfolioValue = Math.Max(currentPortfolioValue, CumulativeMaxPortfolioValue); @@ -659,6 +685,12 @@ public virtual void Sample(DateTime time) DailyPortfolioValue = currentPortfolioValue; } + private void GetPortfolioPerformance(out decimal currentPortfolioValue, out decimal portfolioPerformance) + { + currentPortfolioValue = GetPortfolioValue(); + portfolioPerformance = DailyPortfolioValue == 0 ? 0 : Math.Round((currentPortfolioValue - DailyPortfolioValue) * 100 / DailyPortfolioValue, 10); + } + private void SamplePortfolioMargin(DateTime algorithmUtcTime, decimal currentPortfolioValue) { var state = PortfolioState.Create(Algorithm.Portfolio, algorithmUtcTime, currentPortfolioValue); @@ -959,27 +991,66 @@ protected StatisticsResults GenerateStatisticsResults(Dictionary // make sure we've taken samples for these series before just blindly requesting them if (charts.TryGetValue(StrategyEquityKey, out var strategyEquity) && strategyEquity.Series.TryGetValue(EquityKey, out var equity) && - strategyEquity.Series.TryGetValue(ReturnKey, out var performance) && - charts.TryGetValue(BenchmarkKey, out var benchmarkChart) && - benchmarkChart.Series.TryGetValue(BenchmarkKey, out var benchmark)) + equity.Values.Count > 0) { - var trades = Algorithm.TradeBuilder.ClosedTrades; - - BaseSeries portfolioTurnover; - if (charts.TryGetValue(PortfolioTurnoverKey, out var portfolioTurnoverChart)) + List performanceValues; + List benchmarkValues; + if (strategyEquity.Series.TryGetValue(ReturnKey, out var performance) && + charts.TryGetValue(BenchmarkKey, out var benchmarkChart) && + benchmarkChart.Series.TryGetValue(BenchmarkKey, out var benchmark)) { - portfolioTurnoverChart.Series.TryGetValue(PortfolioTurnoverKey, out portfolioTurnover); + performanceValues = performance.Values; + benchmarkValues = benchmark.Values; + + // Clear temporary values, free memory. We don't need them anymore + _temporaryPerformanceValues = null; + _temporaryBenchmarkValues = null; } else { - portfolioTurnover = new Series(); + // We don't have performance and/or benchmark values sampled, likely because we are on the first day of the algo + // and we only sample at the end of the day. In this case we will create temporary values for performance and benchmark + // so that we can generate statistics and write trades to the result files + + if (_temporaryPerformanceValues == null || _temporaryBenchmarkValues == null) + { + // Let's force update and sample both performance and benchmark at the current time since they need to be aligned + + UpdatePortfolioValues(Algorithm.UtcTime); + UpdateBenchmarkValue(Algorithm.UtcTime); + GetPortfolioPerformance(out _, out var portfolioPerformance); + + if (portfolioPerformance != 0) + { + _temporaryPerformanceValues = new List { new ChartPoint(Algorithm.UtcTime, portfolioPerformance) }; + _temporaryBenchmarkValues = new List { new ChartPoint(Algorithm.UtcTime, GetBechmarkValue(Algorithm.UtcTime)) }; + } + } + + performanceValues = _temporaryPerformanceValues; + benchmarkValues = _temporaryBenchmarkValues; } - statisticsResults = StatisticsBuilder.Generate(trades, profitLoss, equity.Values, performance.Values, benchmark.Values, - portfolioTurnover.Values, StartingPortfolioValue, Algorithm.Portfolio.TotalFees, TotalTradesCount(), - estimatedStrategyCapacity, AlgorithmCurrencySymbol, Algorithm.Transactions, Algorithm.RiskFreeInterestRateModel, - Algorithm.Settings.TradingDaysPerYear.Value // already set in Brokerage|Backtesting-SetupHandler classes - ); + if (performanceValues != null && benchmarkValues != null) + { + var trades = Algorithm.TradeBuilder.ClosedTrades; + + BaseSeries portfolioTurnover; + if (charts.TryGetValue(PortfolioTurnoverKey, out var portfolioTurnoverChart)) + { + portfolioTurnoverChart.Series.TryGetValue(PortfolioTurnoverKey, out portfolioTurnover); + } + else + { + portfolioTurnover = new Series(); + } + + statisticsResults = StatisticsBuilder.Generate(trades, profitLoss, equity.Values, performanceValues, benchmarkValues, + portfolioTurnover.Values, StartingPortfolioValue, Algorithm.Portfolio.TotalFees, TotalTradesCount(), + estimatedStrategyCapacity, AlgorithmCurrencySymbol, Algorithm.Transactions, Algorithm.RiskFreeInterestRateModel, + Algorithm.Settings.TradingDaysPerYear.Value // already set in Brokerage|Backtesting-SetupHandler classes + ); + } } statisticsResults.AddCustomSummaryStatistics(_customSummaryStatistics); @@ -1142,5 +1213,10 @@ protected virtual void UpdateBenchmarkValue(DateTime time, bool force = false) _benchmarkValue = new ReferenceWrapper(Algorithm.Benchmark.Evaluate(time).SmartRounding()); } } + + private decimal GetBechmarkValue(DateTime time) + { + return Algorithm.Benchmark.Evaluate(time).SmartRounding(); + } } } diff --git a/Engine/Results/LiveTradingResultHandler.cs b/Engine/Results/LiveTradingResultHandler.cs index 6ea564e62da2..302c96594084 100644 --- a/Engine/Results/LiveTradingResultHandler.cs +++ b/Engine/Results/LiveTradingResultHandler.cs @@ -219,9 +219,16 @@ private void Update() var statistics = GenerateStatisticsResults(performanceCharts); var runtimeStatistics = GetAlgorithmRuntimeStatistics(statistics.Summary); + AlgorithmPerformance algorithmPerformance; + { + var stopwatch = Stopwatch.StartNew(); + var deltaTrades = GetDeltaTrades(statistics.TotalPerformance.ClosedTrades, LastDeltaTradePosition, shouldStop: _ => stopwatch.ElapsedMilliseconds > 15); + algorithmPerformance = new AlgorithmPerformance(statistics.TotalPerformance, deltaTrades); + } + // since we're sending multiple packets, let's do it async and forget about it // chart data can get big so let's break them up into groups - var splitPackets = SplitPackets(deltaCharts, deltaOrders, holdings, Algorithm.Portfolio.CashBook, runtimeStatistics, serverStatistics, deltaOrderEvents); + var splitPackets = SplitPackets(deltaCharts, deltaOrders, holdings, Algorithm.Portfolio.CashBook, runtimeStatistics, serverStatistics, deltaOrderEvents, algorithmPerformance); foreach (var liveResultPacket in splitPackets) { @@ -463,7 +470,8 @@ private IEnumerable SplitPackets(Dictionary del CashBook cashbook, SortedDictionary runtimeStatistics, Dictionary serverStatistics, - List deltaOrderEvents) + List deltaOrderEvents, + AlgorithmPerformance algorithmPerformance) { // break the charts into groups var current = new Dictionary(); @@ -518,6 +526,12 @@ private IEnumerable SplitPackets(Dictionary del result = result.Concat(new[] { new LiveResultPacket(_job, new LiveResult { Orders = deltaOrders, OrderEvents = deltaOrderEvents }) }); } + // only send trades packet if there is actually any update + if (algorithmPerformance.ClosedTrades.Count > 0) + { + result = result.Concat(new[] { new LiveResultPacket(_job, new LiveResult { TotalPerformance = algorithmPerformance }) }); + } + return result; }