Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Engine/Results/BaseResultsHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -986,7 +986,7 @@ protected Dictionary<string, string> GetAlgorithmState(DateTime? endTime = null)
/// <summary>
/// Will generate the statistics results and update the provided runtime statistics
/// </summary>
protected StatisticsResults GenerateStatisticsResults(Dictionary<string, Chart> charts,
protected virtual StatisticsResults GenerateStatisticsResults(Dictionary<string, Chart> charts,
SortedDictionary<DateTime, decimal> profitLoss = null, CapacityEstimate estimatedStrategyCapacity = null)
{
var statisticsResults = new StatisticsResults();
Expand Down
122 changes: 122 additions & 0 deletions Engine/Results/LiveTradingResultHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ public class LiveTradingResultHandler : BaseResultsHandler, IResultHandler
private bool _userExchangeIsOpen;
private DateTime _lastChartSampleLogicCheck;
private readonly Dictionary<string, SecurityExchangeHours> _exchangeHours;
private readonly object _statisticsRetentionLock = new();
private readonly Dictionary<string, Dictionary<string, RetainedStatisticsSeries>> _statisticsSeriesRetention = new();

private static readonly IReadOnlyList<Tuple<string, string>> StatisticsSeries = new[]
{
Tuple.Create(StrategyEquityKey, EquityKey),
Tuple.Create(StrategyEquityKey, ReturnKey),
Tuple.Create(BenchmarkKey, BenchmarkKey),
Tuple.Create(PortfolioTurnoverKey, PortfolioTurnoverKey)
};


/// <summary>
Expand All @@ -95,6 +105,18 @@ public LiveTradingResultHandler()
_streamedChartGroupSize = Config.GetInt("streamed-chart-group-size", 3);
}

/// <summary>
/// Samples portfolio equity, benchmark, and daily performance
/// Called by scheduled event every night at midnight algorithm time
/// </summary>
/// <param name="time">Current UTC time in the AlgorithmManager loop</param>
public override void Sample(DateTime time)
{
base.Sample(time);

RetainStatisticsSamples();
}

/// <summary>
/// Initialize the result handler with this result packet.
/// </summary>
Expand Down Expand Up @@ -645,6 +667,106 @@ protected override void Sample(string chartName, string seriesName, int seriesIn
}
}

/// <summary>
/// Will generate the statistics results and update the provided runtime statistics
/// </summary>
protected override StatisticsResults GenerateStatisticsResults(Dictionary<string, Chart> charts,
SortedDictionary<DateTime, decimal> profitLoss = null, CapacityEstimate estimatedStrategyCapacity = null)
{
MergeRetainedStatisticsSamples(charts);

return base.GenerateStatisticsResults(charts, profitLoss, estimatedStrategyCapacity);
}

private void RetainStatisticsSamples()
{
lock (ChartLock)
{
foreach (var retainedSeries in StatisticsSeries)
{
if (Charts.TryGetValue(retainedSeries.Item1, out var chart) &&
chart.Series.TryGetValue(retainedSeries.Item2, out var series) &&
series.Values.Count > 0)
{
RetainStatisticsSample(retainedSeries.Item1, retainedSeries.Item2, series);
}
}
}
}

private void RetainStatisticsSample(string chartName, string seriesName, BaseSeries series)
{
var value = series.Values.Last();

lock (_statisticsRetentionLock)
{
if (!_statisticsSeriesRetention.TryGetValue(chartName, out var chartRetention))
{
chartRetention = new Dictionary<string, RetainedStatisticsSeries>();
_statisticsSeriesRetention[chartName] = chartRetention;
}

if (!chartRetention.TryGetValue(seriesName, out var seriesRetention))
{
seriesRetention = new RetainedStatisticsSeries(series.Clone(empty: true));
chartRetention[seriesName] = seriesRetention;
}

var values = seriesRetention.Values;
if (values.Count > 0 && values[values.Count - 1].Time == value.Time)
{
values[values.Count - 1] = value.Clone();
}
else
{
values.Add(value.Clone());
}
}
}

private void MergeRetainedStatisticsSamples(Dictionary<string, Chart> charts)
{
lock (_statisticsRetentionLock)
{
foreach (var chartRetention in _statisticsSeriesRetention)
{
if (!charts.TryGetValue(chartRetention.Key, out var chart))
{
chart = new Chart(chartRetention.Key);
charts[chartRetention.Key] = chart;
}

foreach (var seriesRetention in chartRetention.Value)
{
if (!chart.Series.TryGetValue(seriesRetention.Key, out var series))
{
series = seriesRetention.Value.Template.Clone(empty: true);
chart.Series[seriesRetention.Key] = series;
}

series.Values = series.Values
.Concat(seriesRetention.Value.Values.Select(x => x.Clone()))
.GroupBy(x => x.Time)
.Select(x => x.Last())
.OrderBy(x => x.Time)
.ToList();
}
}
}
}

private class RetainedStatisticsSeries
{
public RetainedStatisticsSeries(BaseSeries template)
{
Template = template;
}

public BaseSeries Template { get; }

public List<ISeriesPoint> Values { get; } = new();
}

/// <summary>
/// Add a range of samples from the users algorithms to the end of our current list.
/// </summary>
Expand Down
46 changes: 46 additions & 0 deletions Tests/Engine/Results/LiveTradingResultHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using QuantConnect.Packets;
Expand Down Expand Up @@ -190,6 +191,43 @@ public void DailySampleValueBasedOnMarketHour(bool extendedMarketHoursEnabled)
}
}

[Test]
public void RetainsDailyStatisticsSamplesAfterChartTrimming()
{
var resultHandler = new TestableLiveTradingResultHandler();
var algorithm = new AlgorithmStub(createDataManager: false);
algorithm.SetStartDate(2026, 04, 30);
algorithm.SetFinishedWarmingUp();
algorithm.PostInitialize();
resultHandler.SetAlgorithm(algorithm, 100000);

for (var i = 0; i < 5; i++)
{
algorithm.Portfolio.CashBook["USD"].AddAmount(1000);
algorithm.Portfolio.InvalidateTotalPortfolioValue();
resultHandler.Sample(new DateTime(2026, 05, 01 + i, 0, 0, 0, DateTimeKind.Utc));
}

var charts = new Dictionary<string, Chart>
{
[BaseResultsHandler.StrategyEquityKey] = resultHandler.Charts[BaseResultsHandler.StrategyEquityKey].Clone(),
[BaseResultsHandler.BenchmarkKey] = resultHandler.Charts[BaseResultsHandler.BenchmarkKey].Clone()
};

foreach (var series in charts.Values.SelectMany(chart => chart.Series.Values))
{
series.Values = series.Values.TakeLast(2).ToList();
}

resultHandler.CallGenerateStatisticsResults(charts);

Assert.AreEqual(5, charts[BaseResultsHandler.StrategyEquityKey].Series[BaseResultsHandler.EquityKey].Values.Count);
Assert.AreEqual(5, charts[BaseResultsHandler.StrategyEquityKey].Series[BaseResultsHandler.ReturnKey].Values.Count);
Assert.AreEqual(5, charts[BaseResultsHandler.BenchmarkKey].Series[BaseResultsHandler.BenchmarkKey].Values.Count);
Assert.IsTrue(charts.ContainsKey(BaseResultsHandler.PortfolioTurnoverKey));
Assert.AreEqual(5, charts[BaseResultsHandler.PortfolioTurnoverKey].Series[BaseResultsHandler.PortfolioTurnoverKey].Values.Count);
}

private class TestDataFeed : IDataFeed
{
public bool IsActive { get; }
Expand Down Expand Up @@ -218,5 +256,13 @@ public void Exit()
{
}
}

private class TestableLiveTradingResultHandler : LiveTradingResultHandler
{
public void CallGenerateStatisticsResults(Dictionary<string, Chart> charts)
{
GenerateStatisticsResults(charts);
}
}
}
}