1414 *
1515*/
1616
17- using System ;
18- using System . Collections . Concurrent ;
19- using System . Collections . Generic ;
20- using System . IO ;
21- using System . Linq ;
22- using System . Threading ;
2317using Newtonsoft . Json ;
2418using Newtonsoft . Json . Serialization ;
2519using QuantConnect . Data . Market ;
3428using QuantConnect . Securities . Positions ;
3529using QuantConnect . Statistics ;
3630using QuantConnect . Util ;
31+ using System ;
32+ using System . Collections . Concurrent ;
33+ using System . Collections . Generic ;
34+ using System . IO ;
35+ using System . Linq ;
36+ using System . Threading ;
3737
3838namespace QuantConnect . Lean . Engine . Results
3939{
@@ -59,6 +59,11 @@ public abstract class BaseResultsHandler
5959
6060 private Bar _currentAlgorithmEquity ;
6161
62+ private List < ISeriesPoint > _temporaryPerformanceValues ;
63+ private List < ISeriesPoint > _temporaryBenchmarkValues ;
64+ private DateTime _temporaryChartsLastSampleTime ;
65+ private object _temporaryChartsLock = new ( ) ;
66+
6267 /// <summary>
6368 /// String message saying: Strategy Equity
6469 /// </summary>
@@ -114,6 +119,11 @@ public abstract class BaseResultsHandler
114119 /// </summary>
115120 protected int LastDeltaOrderPosition { get ; set ; }
116121
122+ /// <summary>
123+ /// The last position consumed from the <see cref="TradeBuilder.ClosedTrades"/> by <see cref="GetDeltaTrades"/>
124+ /// </summary>
125+ protected string LastTradeId { get ; set ; }
126+
117127 /// <summary>
118128 /// The last position consumed from the <see cref="ITransactionHandler.OrderEvents"/> while determining delta order events
119129 /// </summary>
@@ -122,7 +132,7 @@ public abstract class BaseResultsHandler
122132 /// <summary>
123133 /// Serializer settings to use
124134 /// </summary>
125- protected JsonSerializerSettings SerializerSettings { get ; set ; } = new ( )
135+ protected JsonSerializerSettings SerializerSettings { get ; set ; } = new ( )
126136 {
127137 ContractResolver = new DefaultContractResolver
128138 {
@@ -446,6 +456,29 @@ protected virtual Dictionary<int, Order> GetDeltaOrders(int orderEventsStartPosi
446456 return deltaOrders ;
447457 }
448458
459+ /// <summary>
460+ /// Gets the trades generated starting from the provided <see cref="TradeBuilder.ClosedTrades"/> position,
461+ /// which is determined by the <see cref="LastTradeId"/> and the <see cref="Trade.Id"/>
462+ /// </summary>
463+ /// <returns>The delta trades</returns>
464+ protected virtual List < Trade > GetDeltaTrades ( List < Trade > trades , string lastTradeId , Func < int , bool > shouldStop )
465+ {
466+ var lastTradeIndex = trades . FindIndex ( x => x . Id == lastTradeId ) ;
467+ List < Trade > deltaTrades = null ;
468+ foreach ( var trade in trades . Skip ( lastTradeIndex + 1 ) )
469+ {
470+ LastTradeId = trade . Id ;
471+ deltaTrades ??= new List < Trade > ( ) ;
472+ deltaTrades . Add ( trade ) ;
473+ if ( shouldStop ( deltaTrades . Count ) )
474+ {
475+ break ;
476+ }
477+ }
478+
479+ return deltaTrades ;
480+ }
481+
449482 /// <summary>
450483 /// Initialize the result handler with this result packet.
451484 /// </summary>
@@ -466,7 +499,7 @@ public virtual void Initialize(ResultHandlerInitializeParameters parameters)
466499
467500 SerializerSettings = new ( )
468501 {
469- Converters = new [ ] { new OrderEventJsonConverter ( AlgorithmId ) } ,
502+ Converters = new [ ] { new OrderEventJsonConverter ( AlgorithmId ) } ,
470503 ContractResolver = new DefaultContractResolver
471504 {
472505 NamingStrategy = new CamelCaseNamingStrategy
@@ -636,9 +669,8 @@ public virtual void Sample(DateTime time)
636669 // Force an update for our values before doing our daily sample
637670 UpdatePortfolioValues ( time ) ;
638671 UpdateBenchmarkValue ( time ) ;
639-
640672 var currentPortfolioValue = GetPortfolioValue ( ) ;
641- var portfolioPerformance = DailyPortfolioValue == 0 ? 0 : Math . Round ( ( currentPortfolioValue - DailyPortfolioValue ) * 100 / DailyPortfolioValue , 10 ) ;
673+ var portfolioPerformance = GetPortfolioPerformance ( currentPortfolioValue ) ;
642674
643675 // Update our max portfolio value
644676 CumulativeMaxPortfolioValue = Math . Max ( currentPortfolioValue , CumulativeMaxPortfolioValue ) ;
@@ -659,6 +691,11 @@ public virtual void Sample(DateTime time)
659691 DailyPortfolioValue = currentPortfolioValue ;
660692 }
661693
694+ private decimal GetPortfolioPerformance ( decimal currentPortfolioValue )
695+ {
696+ return DailyPortfolioValue == 0 ? 0 : Math . Round ( ( currentPortfolioValue - DailyPortfolioValue ) * 100 / DailyPortfolioValue , 10 ) ;
697+ }
698+
662699 private void SamplePortfolioMargin ( DateTime algorithmUtcTime , decimal currentPortfolioValue )
663700 {
664701 var state = PortfolioState . Create ( Algorithm . Portfolio , algorithmUtcTime , currentPortfolioValue ) ;
@@ -959,27 +996,82 @@ protected StatisticsResults GenerateStatisticsResults(Dictionary<string, Chart>
959996 // make sure we've taken samples for these series before just blindly requesting them
960997 if ( charts . TryGetValue ( StrategyEquityKey , out var strategyEquity ) &&
961998 strategyEquity . Series . TryGetValue ( EquityKey , out var equity ) &&
962- strategyEquity . Series . TryGetValue ( ReturnKey , out var performance ) &&
963- charts . TryGetValue ( BenchmarkKey , out var benchmarkChart ) &&
964- benchmarkChart . Series . TryGetValue ( BenchmarkKey , out var benchmark ) )
999+ equity . Values . Count > 0 )
9651000 {
966- var trades = Algorithm . TradeBuilder . ClosedTrades ;
967-
968- BaseSeries portfolioTurnover ;
969- if ( charts . TryGetValue ( PortfolioTurnoverKey , out var portfolioTurnoverChart ) )
1001+ List < ISeriesPoint > performanceValues = null ;
1002+ List < ISeriesPoint > benchmarkValues = null ;
1003+ if ( strategyEquity . Series . TryGetValue ( ReturnKey , out var performance ) &&
1004+ charts . TryGetValue ( BenchmarkKey , out var benchmarkChart ) &&
1005+ benchmarkChart . Series . TryGetValue ( BenchmarkKey , out var benchmark ) )
9701006 {
971- portfolioTurnoverChart . Series . TryGetValue ( PortfolioTurnoverKey , out portfolioTurnover ) ;
1007+ performanceValues = performance . Values ;
1008+ benchmarkValues = benchmark . Values ;
1009+
1010+ // Clear temporary values, free memory. We don't need them anymore
1011+ if ( _temporaryPerformanceValues != null && _temporaryBenchmarkValues != null )
1012+ {
1013+ lock ( _temporaryChartsLock )
1014+ {
1015+ _temporaryPerformanceValues = null ;
1016+ _temporaryBenchmarkValues = null ;
1017+ }
1018+ }
9721019 }
9731020 else
9741021 {
975- portfolioTurnover = new Series ( ) ;
1022+ lock ( _temporaryChartsLock )
1023+ {
1024+ if ( Algorithm . UtcTime - _temporaryChartsLastSampleTime >= TimeSpan . FromHours ( 1 ) )
1025+ {
1026+ // We don't have performance and/or benchmark values sampled, likely because we are on the first day of the algo
1027+ // and we only sample at the end of the day. In this case we will create temporary values for performance and benchmark
1028+ // so that we can generate statistics and write trades to the result files
1029+
1030+ // Let's force update and sample both performance and benchmark at the current time since they need to be aligned
1031+ var currentPortfolioValue = GetPortfolioValue ( ) ;
1032+ var portfolioPerformance = GetPortfolioPerformance ( currentPortfolioValue ) ;
1033+
1034+ if ( portfolioPerformance != 0 )
1035+ {
1036+ performanceValues = _temporaryPerformanceValues ??= new List < ISeriesPoint > ( ) ;
1037+ performanceValues . Add ( new ChartPoint ( Algorithm . UtcTime , portfolioPerformance ) ) ;
1038+ benchmarkValues = _temporaryBenchmarkValues ??= new List < ISeriesPoint > ( ) ;
1039+ benchmarkValues . Add ( new ChartPoint ( Algorithm . UtcTime , GetBenchmarkValue ( ) ) ) ;
1040+ _temporaryChartsLastSampleTime = Algorithm . UtcTime ;
1041+ }
1042+ }
1043+
1044+ if ( performanceValues != null && benchmarkValues != null )
1045+ {
1046+ performanceValues = [ .. performanceValues ] ;
1047+ benchmarkValues = [ .. benchmarkValues ] ;
1048+ }
1049+ }
9761050 }
9771051
978- statisticsResults = StatisticsBuilder . Generate ( trades , profitLoss , equity . Values , performance . Values , benchmark . Values ,
979- portfolioTurnover . Values , StartingPortfolioValue , Algorithm . Portfolio . TotalFees , TotalTradesCount ( ) ,
980- estimatedStrategyCapacity , AlgorithmCurrencySymbol , Algorithm . Transactions , Algorithm . RiskFreeInterestRateModel ,
981- Algorithm . Settings . TradingDaysPerYear . Value // already set in Brokerage|Backtesting-SetupHandler classes
982- ) ;
1052+ var trades = Algorithm . TradeBuilder . ClosedTrades ;
1053+ if ( performanceValues != null && benchmarkValues != null )
1054+ {
1055+ BaseSeries portfolioTurnover ;
1056+ if ( charts . TryGetValue ( PortfolioTurnoverKey , out var portfolioTurnoverChart ) )
1057+ {
1058+ portfolioTurnoverChart . Series . TryGetValue ( PortfolioTurnoverKey , out portfolioTurnover ) ;
1059+ }
1060+ else
1061+ {
1062+ portfolioTurnover = new Series ( ) ;
1063+ }
1064+
1065+ statisticsResults = StatisticsBuilder . Generate ( trades , profitLoss , equity . Values , performanceValues , benchmarkValues ,
1066+ portfolioTurnover . Values , StartingPortfolioValue , Algorithm . Portfolio . TotalFees , TotalTradesCount ( ) ,
1067+ estimatedStrategyCapacity , AlgorithmCurrencySymbol , Algorithm . Transactions , Algorithm . RiskFreeInterestRateModel ,
1068+ Algorithm . Settings . TradingDaysPerYear . Value // already set in Brokerage|Backtesting-SetupHandler classes
1069+ ) ;
1070+ }
1071+ else
1072+ {
1073+ statisticsResults . TotalPerformance . ClosedTrades = trades ;
1074+ }
9831075 }
9841076
9851077 statisticsResults . AddCustomSummaryStatistics ( _customSummaryStatistics ) ;
0 commit comments