@@ -150,7 +150,7 @@ public void DailySampleValueBasedOnMarketHour(bool extendedMarketHoursEnabled)
150150 using var messagging = new QuantConnect . Messaging . Messaging ( ) ;
151151 var referenceDate = new DateTime ( 2020 , 11 , 25 ) ;
152152 var resultHandler = new LiveTradingResultHandler ( ) ;
153- resultHandler . Initialize ( new ( new LiveNodePacket ( ) , messagging , api , new BacktestingTransactionHandler ( ) , null ) ) ;
153+ resultHandler . Initialize ( new ( new LiveNodePacket ( ) , messagging , api , new BacktestingTransactionHandler ( ) , null ) ) ;
154154
155155 try
156156 {
@@ -222,6 +222,102 @@ public void MessagesArePrefixedWithAlgorithmTime()
222222 Assert . That ( messages , Has . All . StartsWith ( algorithmTimePrefix ) ) ;
223223 }
224224
225+ [ Test ]
226+ public void TrimChartsKeepsDailySampleOfStatisticsSeries ( )
227+ {
228+ var handler = new TestableLiveTradingResultHandler ( ) ;
229+ var utcNow = new DateTime ( 2020 , 11 , 25 , 12 , 0 , 0 , DateTimeKind . Utc ) ;
230+
231+ var benchmarkChart = new Chart ( BaseResultsHandler . BenchmarkKey ) ;
232+ benchmarkChart . Series . Add ( BaseResultsHandler . BenchmarkKey , new Series ( BaseResultsHandler . BenchmarkKey ) ) ;
233+ handler . Charts [ BaseResultsHandler . BenchmarkKey ] = benchmarkChart ;
234+
235+ var customChart = new Chart ( "MyCustomChart" ) ;
236+ customChart . Series . Add ( "MyMetric" , new Series ( "MyMetric" ) ) ;
237+ handler . Charts [ "MyCustomChart" ] = customChart ;
238+
239+ var returnSeries = handler . Charts [ BaseResultsHandler . StrategyEquityKey ] . Series [ BaseResultsHandler . ReturnKey ] ;
240+ var equitySeries = handler . Charts [ BaseResultsHandler . StrategyEquityKey ] . Series [ BaseResultsHandler . EquityKey ] ;
241+ var benchmarkSeries = benchmarkChart . Series [ BaseResultsHandler . BenchmarkKey ] ;
242+ var customSeries = customChart . Series [ "MyMetric" ] ;
243+
244+ // Return and Benchmark: one point per day, going beyond 2 years
245+ for ( var i = 800 ; i >= 1 ; i -- )
246+ {
247+ var t = utcNow . AddDays ( - i ) ;
248+ returnSeries . Values . Add ( new ChartPoint ( t , i ) ) ;
249+ benchmarkSeries . Values . Add ( new ChartPoint ( t , i ) ) ;
250+ }
251+
252+ // Equity: several points per day for older days, with varying OHLC so the high and low come from intraday candles
253+ foreach ( var day in new [ ] { 5 , 4 , 3 } )
254+ {
255+ var date = utcNow . AddDays ( - day ) . Date ;
256+ equitySeries . Values . Add ( new Candlestick ( date . AddHours ( 10 ) , 100 , 105 , 98 , 101 ) ) ;
257+ equitySeries . Values . Add ( new Candlestick ( date . AddHours ( 14 ) , 101 , 120 , 99 , 102 ) ) ;
258+ equitySeries . Values . Add ( new Candlestick ( date . AddHours ( 16 ) , 102 , 106 , 85 , 103 ) ) ;
259+ }
260+ // Two recent points within the 2 day window
261+ equitySeries . Values . Add ( new Candlestick ( utcNow . AddHours ( - 5 ) , 200 , 210 , 195 , 205 ) ) ;
262+ equitySeries . Values . Add ( new Candlestick ( utcNow . AddHours ( - 1 ) , 205 , 215 , 200 , 211 ) ) ;
263+
264+ // Custom chart: not a statistics series, so no daily sample
265+ for ( var i = 5 ; i >= 1 ; i -- )
266+ {
267+ customSeries . Values . Add ( new ChartPoint ( utcNow . AddDays ( - i ) , i ) ) ;
268+ }
269+
270+ handler . PublicTrimCharts ( utcNow ) ;
271+
272+ // Return and Benchmark keep one point per day, up to 2 years
273+ var dailyStatsCutoff = utcNow . AddDays ( - 730 ) ;
274+ Assert . IsTrue ( returnSeries . Values . All ( v => v . Time > dailyStatsCutoff ) ) ;
275+ Assert . IsTrue ( benchmarkSeries . Values . All ( v => v . Time > dailyStatsCutoff ) ) ;
276+ Assert . AreEqual ( 729 , returnSeries . Values . Count ) ;
277+ Assert . AreEqual ( 729 , benchmarkSeries . Values . Count ) ;
278+
279+ // Equity keeps all recent points and one aggregated candlestick per day for older ones
280+ Assert . AreEqual ( 5 , equitySeries . Values . Count ) ;
281+ foreach ( var day in new [ ] { 5 , 4 , 3 } )
282+ {
283+ var date = utcNow . AddDays ( - day ) . Date ;
284+ var samplesForDay = equitySeries . Values . Where ( v => v . Time . Date == date ) . Cast < Candlestick > ( ) . ToList ( ) ;
285+ Assert . AreEqual ( 1 , samplesForDay . Count ) ;
286+ // The whole day OHLC is aggregated, not just the last candle
287+ var candle = samplesForDay [ 0 ] ;
288+ Assert . AreEqual ( 100 , candle . Open ) ;
289+ Assert . AreEqual ( 120 , candle . High ) ;
290+ Assert . AreEqual ( 85 , candle . Low ) ;
291+ Assert . AreEqual ( 103 , candle . Close ) ;
292+ }
293+
294+ // Recent points are kept at full resolution
295+ var recent = equitySeries . Values . Where ( v => v . Time > utcNow . AddDays ( - 2 ) ) . Cast < Candlestick > ( ) . ToList ( ) ;
296+ Assert . AreEqual ( 2 , recent . Count ) ;
297+ Assert . AreEqual ( 205 , recent [ 0 ] . Close ) ;
298+ Assert . AreEqual ( 211 , recent [ 1 ] . Close ) ;
299+
300+ // Custom chart keeps only the last 2 days
301+ var defaultCutoff = utcNow . AddDays ( - 2 ) ;
302+ Assert . IsTrue ( customSeries . Values . All ( v => v . Time > defaultCutoff ) ) ;
303+ Assert . AreEqual ( 1 , customSeries . Values . Count ) ;
304+
305+ // Trimming runs repeatedly in production, so a second pass must leave the already trimmed series unchanged
306+ var equitySnapshot = equitySeries . Values . Cast < Candlestick > ( )
307+ . Select ( v => ( v . Time , v . Open , v . High , v . Low , v . Close ) ) . ToList ( ) ;
308+ handler . PublicTrimCharts ( utcNow ) ;
309+ Assert . AreEqual ( 729 , returnSeries . Values . Count ) ;
310+ Assert . AreEqual ( 729 , benchmarkSeries . Values . Count ) ;
311+ Assert . AreEqual ( 1 , customSeries . Values . Count ) ;
312+ CollectionAssert . AreEqual ( equitySnapshot , equitySeries . Values . Cast < Candlestick > ( )
313+ . Select ( v => ( v . Time , v . Open , v . High , v . Low , v . Close ) ) . ToList ( ) ) ;
314+ }
315+
316+ private class TestableLiveTradingResultHandler : LiveTradingResultHandler
317+ {
318+ public void PublicTrimCharts ( DateTime utcNow ) => TrimCharts ( utcNow ) ;
319+ }
320+
225321 private class TestDataFeed : IDataFeed
226322 {
227323 public bool IsActive { get ; }
0 commit comments