From 8762cce4d8c04eee85278b749d7b048a4b80034e Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 19 Jan 2026 17:46:21 -0400 Subject: [PATCH 1/5] Add a temporary benchmark stats sample in live Allow trades to be written to result file on first day of the deployment before the first daily sample is done --- Engine/Results/BaseResultsHandler.cs | 101 ++++++++++++++++++++------- 1 file changed, 76 insertions(+), 25 deletions(-) diff --git a/Engine/Results/BaseResultsHandler.cs b/Engine/Results/BaseResultsHandler.cs index 9073484947b8..c46e330b76fb 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 /// @@ -122,7 +125,7 @@ public abstract class BaseResultsHandler /// /// Serializer settings to use /// - protected JsonSerializerSettings SerializerSettings { get; set; } = new () + protected JsonSerializerSettings SerializerSettings { get; set; } = new() { ContractResolver = new DefaultContractResolver { @@ -466,7 +469,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 +639,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 +660,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 +966,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 +1188,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(); + } } } From 7c0160b9e3c33135ba0b7ce0815e9ae2024c960a Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 20 Jan 2026 13:30:41 -0400 Subject: [PATCH 2/5] Enable trades results streaming --- Common/Statistics/AlgorithmPerformance.cs | 11 ++++++++++ Common/Statistics/Trade.cs | 5 +++++ Common/Statistics/TradeBuilder.cs | 2 ++ Engine/Results/BacktestingResultHandler.cs | 22 +++++++++++++++---- Engine/Results/BaseResultsHandler.cs | 25 ++++++++++++++++++++++ Engine/Results/LiveTradingResultHandler.cs | 18 ++++++++++++++-- 6 files changed, 77 insertions(+), 6 deletions(-) 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 c46e330b76fb..0cb79e6312b0 100644 --- a/Engine/Results/BaseResultsHandler.cs +++ b/Engine/Results/BaseResultsHandler.cs @@ -117,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 /// @@ -449,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. /// 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; } From 1a0f29588113b2665788a39c695989147c47a60c Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 27 Jan 2026 10:25:08 -0400 Subject: [PATCH 3/5] Minor changes --- Common/Statistics/AlgorithmPerformance.cs | 7 ++-- Common/Statistics/Trade.cs | 2 +- Common/Statistics/TradeBuilder.cs | 2 +- Engine/Results/BacktestingResultHandler.cs | 4 +-- Engine/Results/BaseResultsHandler.cs | 40 +++++++++------------- Engine/Results/LiveTradingResultHandler.cs | 4 +-- 6 files changed, 26 insertions(+), 33 deletions(-) diff --git a/Common/Statistics/AlgorithmPerformance.cs b/Common/Statistics/AlgorithmPerformance.cs index ded3811d89f5..5b94c81b710d 100644 --- a/Common/Statistics/AlgorithmPerformance.cs +++ b/Common/Statistics/AlgorithmPerformance.cs @@ -87,12 +87,11 @@ public AlgorithmPerformance() /// 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) + public AlgorithmPerformance(AlgorithmPerformance other) { - TradeStatistics = new TradeStatistics(trades); + TradeStatistics = other.TradeStatistics; PortfolioStatistics = other.PortfolioStatistics; - ClosedTrades = trades; + ClosedTrades = other.ClosedTrades; } } } diff --git a/Common/Statistics/Trade.cs b/Common/Statistics/Trade.cs index a4d20db36d4c..288a2c94c10c 100644 --- a/Common/Statistics/Trade.cs +++ b/Common/Statistics/Trade.cs @@ -29,7 +29,7 @@ public class Trade /// /// A unique identifier for the trade /// - internal long Id { get; set; } + public int Id { get; set; } /// /// The symbol of the traded instrument diff --git a/Common/Statistics/TradeBuilder.cs b/Common/Statistics/TradeBuilder.cs index f4a3d72da34d..2ba2100a98a4 100644 --- a/Common/Statistics/TradeBuilder.cs +++ b/Common/Statistics/TradeBuilder.cs @@ -58,7 +58,7 @@ public Position() private readonly FillMatchingMethod _matchingMethod; private SecurityManager _securities; private bool _liveMode; - private long _nextTradeId = 1; + private int _nextTradeId = 1; /// /// Initializes a new instance of the class diff --git a/Engine/Results/BacktestingResultHandler.cs b/Engine/Results/BacktestingResultHandler.cs index 16484bcc4f92..019db7e6ef50 100644 --- a/Engine/Results/BacktestingResultHandler.cs +++ b/Engine/Results/BacktestingResultHandler.cs @@ -227,8 +227,8 @@ private void Update() 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); + LastDeltaTradePosition = statisticsResult.TotalPerformance.ClosedTrades[^1].Id; + var algorithmPerformance = new AlgorithmPerformance(statisticsResult.TotalPerformance) { ClosedTrades = deltaTrades }; //2. Backtest Update -> Send the truncated packet to the backtester: var splitPackets = SplitPackets(deltaCharts, deltaOrders, runtimeStatistics, progress, serverStatistics, algorithmPerformance); diff --git a/Engine/Results/BaseResultsHandler.cs b/Engine/Results/BaseResultsHandler.cs index 0cb79e6312b0..21bb77444bd5 100644 --- a/Engine/Results/BaseResultsHandler.cs +++ b/Engine/Results/BaseResultsHandler.cs @@ -461,10 +461,11 @@ protected virtual Dictionary GetDeltaOrders(int orderEventsStartPosi /// 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)) + List deltaTrades = null; + foreach (var trade in trades.Where(x => x.Id > tradesStartId)) { LastDeltaTradePosition = trade.Id; + deltaTrades ??= new List(); deltaTrades.Add(trade); if (shouldStop(deltaTrades.Count)) { @@ -664,7 +665,8 @@ public virtual void Sample(DateTime time) // Force an update for our values before doing our daily sample UpdatePortfolioValues(time); UpdateBenchmarkValue(time); - GetPortfolioPerformance(out var currentPortfolioValue, out var portfolioPerformance); + var currentPortfolioValue = GetPortfolioValue(); + var portfolioPerformance = GetPortfolioPerformance(currentPortfolioValue); // Update our max portfolio value CumulativeMaxPortfolioValue = Math.Max(currentPortfolioValue, CumulativeMaxPortfolioValue); @@ -685,10 +687,9 @@ public virtual void Sample(DateTime time) DailyPortfolioValue = currentPortfolioValue; } - private void GetPortfolioPerformance(out decimal currentPortfolioValue, out decimal portfolioPerformance) + private decimal GetPortfolioPerformance(decimal currentPortfolioValue) { - currentPortfolioValue = GetPortfolioValue(); - portfolioPerformance = DailyPortfolioValue == 0 ? 0 : Math.Round((currentPortfolioValue - DailyPortfolioValue) * 100 / DailyPortfolioValue, 10); + return DailyPortfolioValue == 0 ? 0 : Math.Round((currentPortfolioValue - DailyPortfolioValue) * 100 / DailyPortfolioValue, 10); } private void SamplePortfolioMargin(DateTime algorithmUtcTime, decimal currentPortfolioValue) @@ -1012,19 +1013,17 @@ protected StatisticsResults GenerateStatisticsResults(Dictionary // 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); + // Let's force update and sample both performance and benchmark at the current time since they need to be aligned + //var currentPortfolioValue = Algorithm?.Portfolio.TotalPortfolioValue ?? 0; + var currentPortfolioValue = GetPortfolioValue(); + var portfolioPerformance = GetPortfolioPerformance(currentPortfolioValue); - if (portfolioPerformance != 0) - { - _temporaryPerformanceValues = new List { new ChartPoint(Algorithm.UtcTime, portfolioPerformance) }; - _temporaryBenchmarkValues = new List { new ChartPoint(Algorithm.UtcTime, GetBechmarkValue(Algorithm.UtcTime)) }; - } + if (portfolioPerformance != 0) + { + _temporaryPerformanceValues ??= new List(); + _temporaryPerformanceValues.Add(new ChartPoint(Algorithm.UtcTime, portfolioPerformance)); + _temporaryBenchmarkValues ??= new List(); + _temporaryBenchmarkValues.Add(new ChartPoint(Algorithm.UtcTime, GetBenchmarkValue())); } performanceValues = _temporaryPerformanceValues; @@ -1213,10 +1212,5 @@ 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 302c96594084..1f5ce0463f9e 100644 --- a/Engine/Results/LiveTradingResultHandler.cs +++ b/Engine/Results/LiveTradingResultHandler.cs @@ -223,7 +223,7 @@ private void Update() { var stopwatch = Stopwatch.StartNew(); var deltaTrades = GetDeltaTrades(statistics.TotalPerformance.ClosedTrades, LastDeltaTradePosition, shouldStop: _ => stopwatch.ElapsedMilliseconds > 15); - algorithmPerformance = new AlgorithmPerformance(statistics.TotalPerformance, deltaTrades); + algorithmPerformance = new AlgorithmPerformance(statistics.TotalPerformance) { ClosedTrades = deltaTrades }; } // since we're sending multiple packets, let's do it async and forget about it @@ -527,7 +527,7 @@ private IEnumerable SplitPackets(Dictionary del } // only send trades packet if there is actually any update - if (algorithmPerformance.ClosedTrades.Count > 0) + if (algorithmPerformance.ClosedTrades != null && algorithmPerformance.ClosedTrades.Count > 0) { result = result.Concat(new[] { new LiveResultPacket(_job, new LiveResult { TotalPerformance = algorithmPerformance }) }); } From 36c4f5b642ca7855f131adc01c6754da77d8e235 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 27 Jan 2026 11:05:02 -0400 Subject: [PATCH 4/5] Make Trade.Id a Guid --- Common/Statistics/Trade.cs | 2 +- Common/Statistics/TradeBuilder.cs | 3 +-- Engine/Results/BacktestingResultHandler.cs | 4 ++-- Engine/Results/BaseResultsHandler.cs | 12 +++++++----- Engine/Results/LiveTradingResultHandler.cs | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Common/Statistics/Trade.cs b/Common/Statistics/Trade.cs index 288a2c94c10c..6604abe5c8fa 100644 --- a/Common/Statistics/Trade.cs +++ b/Common/Statistics/Trade.cs @@ -29,7 +29,7 @@ public class Trade /// /// A unique identifier for the trade /// - public int Id { get; set; } + public string Id { get; set; } /// /// The symbol of the traded instrument diff --git a/Common/Statistics/TradeBuilder.cs b/Common/Statistics/TradeBuilder.cs index 2ba2100a98a4..a98fc63a9ac9 100644 --- a/Common/Statistics/TradeBuilder.cs +++ b/Common/Statistics/TradeBuilder.cs @@ -58,7 +58,6 @@ public Position() private readonly FillMatchingMethod _matchingMethod; private SecurityManager _securities; private bool _liveMode; - private int _nextTradeId = 1; /// /// Initializes a new instance of the class @@ -562,7 +561,7 @@ private void AddNewTrade(Trade trade, OrderEvent fill) ? fill.IsWin(security, trade.ProfitLoss) : trade.ProfitLoss > 0; - trade.Id = _nextTradeId++; + trade.Id = Guid.NewGuid().ToString(); _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 019db7e6ef50..a9d35542b861 100644 --- a/Engine/Results/BacktestingResultHandler.cs +++ b/Engine/Results/BacktestingResultHandler.cs @@ -225,9 +225,9 @@ private void Update() _nextS3Update = DateTime.UtcNow.AddSeconds(30); } - var deltaTrades = GetDeltaTrades(statisticsResult.TotalPerformance.ClosedTrades, LastDeltaTradePosition, shouldStop: tradeCount => tradeCount >= 50); + var deltaTrades = GetDeltaTrades(statisticsResult.TotalPerformance.ClosedTrades, LastTradeId, shouldStop: tradeCount => tradeCount >= 50); // Deliberately skip to the end of trade collection to prevent overloading backtesting UX - LastDeltaTradePosition = statisticsResult.TotalPerformance.ClosedTrades[^1].Id; + LastTradeId = statisticsResult.TotalPerformance.ClosedTrades[^1].Id; var algorithmPerformance = new AlgorithmPerformance(statisticsResult.TotalPerformance) { ClosedTrades = deltaTrades }; //2. Backtest Update -> Send the truncated packet to the backtester: diff --git a/Engine/Results/BaseResultsHandler.cs b/Engine/Results/BaseResultsHandler.cs index 21bb77444bd5..e502fd818a35 100644 --- a/Engine/Results/BaseResultsHandler.cs +++ b/Engine/Results/BaseResultsHandler.cs @@ -120,7 +120,7 @@ public abstract class BaseResultsHandler /// /// The last position consumed from the by /// - protected long LastDeltaTradePosition { get; set; } + protected string LastTradeId { get; set; } /// /// The last position consumed from the while determining delta order events @@ -456,15 +456,16 @@ protected virtual Dictionary GetDeltaOrders(int orderEventsStartPosi /// /// Gets the trades generated starting from the provided position, - /// which is determined by the and the + /// which is determined by the and the /// /// The delta trades - protected virtual List GetDeltaTrades(List trades, long tradesStartId, Func shouldStop) + protected virtual List GetDeltaTrades(List trades, string lastTradeId, Func shouldStop) { + var lastTradeIndex = trades.FindIndex(x => x.Id == lastTradeId); List deltaTrades = null; - foreach (var trade in trades.Where(x => x.Id > tradesStartId)) + foreach (var trade in trades.Skip(lastTradeIndex + 1)) { - LastDeltaTradePosition = trade.Id; + LastTradeId = trade.Id; deltaTrades ??= new List(); deltaTrades.Add(trade); if (shouldStop(deltaTrades.Count)) @@ -472,6 +473,7 @@ protected virtual List GetDeltaTrades(List trades, long tradesStar break; } } + return deltaTrades; } diff --git a/Engine/Results/LiveTradingResultHandler.cs b/Engine/Results/LiveTradingResultHandler.cs index 1f5ce0463f9e..954c95cd3411 100644 --- a/Engine/Results/LiveTradingResultHandler.cs +++ b/Engine/Results/LiveTradingResultHandler.cs @@ -222,7 +222,7 @@ private void Update() AlgorithmPerformance algorithmPerformance; { var stopwatch = Stopwatch.StartNew(); - var deltaTrades = GetDeltaTrades(statistics.TotalPerformance.ClosedTrades, LastDeltaTradePosition, shouldStop: _ => stopwatch.ElapsedMilliseconds > 15); + var deltaTrades = GetDeltaTrades(statistics.TotalPerformance.ClosedTrades, LastTradeId, shouldStop: _ => stopwatch.ElapsedMilliseconds > 15); algorithmPerformance = new AlgorithmPerformance(statistics.TotalPerformance) { ClosedTrades = deltaTrades }; } From 27c13a589c0d3ded64881080367324d6488b1f6a Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 27 Jan 2026 11:14:28 -0400 Subject: [PATCH 5/5] Cleanup --- Engine/Results/BacktestingResultHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Engine/Results/BacktestingResultHandler.cs b/Engine/Results/BacktestingResultHandler.cs index a9d35542b861..643beafd2265 100644 --- a/Engine/Results/BacktestingResultHandler.cs +++ b/Engine/Results/BacktestingResultHandler.cs @@ -275,7 +275,7 @@ public virtual IEnumerable SplitPackets(Dictionary 0) + if (algorithmPerformance.ClosedTrades != null && algorithmPerformance.ClosedTrades.Count > 0) { // Add the trades into the charting packet: splitPackets.Add(new BacktestResultPacket(_job, new BacktestResult { TotalPerformance = algorithmPerformance }, Algorithm.EndDate, Algorithm.StartDate, progress));