Expected Behavior
A live algorithm should report the same statistics as a backtest covering the equivalent period once enough trading days have elapsed: Probabilistic Sharpe Ratio (PSR), Sharpe, Sortino, Alpha, Beta, Tracking Error, Information Ratio, Treynor, VaR, Compounding Annual Return and Drawdown should reflect the algorithm's full history.
Actual Behavior
After a live algorithm has been running for more than ~2 days, PSR is reported as 0% indefinitely. All of the other listPerformance-derived statistics also collapse to their defaults at the same time (Sharpe Ratio = 0, Sortino Ratio = 0, Alpha = 0, Beta = 0, Tracking Error = 0, Information Ratio = 0, Treynor = 0, Compounding Annual Return = 0%). Drawdown and Drawdown Recovery also become inaccurate (only reflect the last ~2 days). PSR is just the most visibly broken value because 0% is never a plausible result for a running algorithm.
The same algorithm run as a backtest over an equivalent period produces correct non-zero values.
Potential Solution
Root cause: LiveTradingResultHandler.Update() runs a chart-trim pass every 10 minutes that removes every series sample older than 2 days:
// Engine/Results/LiveTradingResultHandler.cs (~L336-L356)
if (utcNow > _nextChartTrimming)
{
var timeLimitUtc = utcNow.AddDays(-2);
lock (ChartLock)
{
foreach (var chart in Charts)
{
foreach (var series in chart.Value.Series)
{
// trim data that's older than 2 days
series.Value.Values =
(from v in series.Value.Values
where v.Time > timeLimitUtc
select v).ToList();
}
}
}
_nextChartTrimming = DateTime.UtcNow.AddMinutes(10);
}
The StrategyEquity/Return and Benchmark/Benchmark series are only sampled once a day (from the midnight Daily Sampling scheduled event registered in AlgorithmManager.Run → BaseResultsHandler.Sample(DateTime)), so after each trim each of those series holds at most ~2 points. The trim is rolling, not a one-time startup window, so running the algorithm longer doesn't help — at steady state these series always cap at ~2 daily points.
StatisticsBuilder.PreprocessPerformanceValues then drops the first 2 entries (Day 0 / Day 1 alignment with backtest):
// Common/Statistics/StatisticsBuilder.cs (~L380)
foreach (var kvp in points.Skip(2))
yield return new KeyValuePair<DateTime, double>(kvp.Key, (double)(kvp.Value / 100));
…leaving listPerformance.Count == 0, which causes PortfolioStatistics to early-return:
// Common/Statistics/PortfolioStatistics.cs (~L231)
if (startingCapital == 0
|| listBenchmark.Count < 2
|| listPerformance.Count < 2)
{
return;
}
So ProbabilisticSharpeRatio (and every metric computed after that line) keeps its default value of 0.
There's also a cascade through the equity series: StatisticsBuilder.Generate derives the analysis period from the (also-trimmed) StrategyEquity/Equity series — firstDate = equity.Keys.First().Date — so even if Return/Benchmark were preserved, ChartPointToDictionary(pointsPerformance, fromDate, toDate) would still clip them to ~2 days. CompoundingAnnualReturn and Drawdown also become wrong because they read from the trimmed equity directly.
Backtest is unaffected: BacktestingResultHandler has no equivalent trim loop.
Proposed fix (smallest scope, live-only): retention buffer + override. Keep a separate, daily-resolution retention buffer of the statistics inputs inside LiveTradingResultHandler, populated from the existing daily Sample(DateTime) pass, and inject it into the (cloned) charts passed to GenerateStatisticsResults. Sketch:
- Make
BaseResultsHandler.GenerateStatisticsResults(Dictionary<string, Chart>, ...) protected virtual (one-keyword change).
- In
LiveTradingResultHandler:
- Add four retention lists (
Equity, Return, Benchmark, PortfolioTurnover) and a lock.
override Sample(DateTime time) — call base, then snapshot series.Values[^1] for each statistics-relevant series (StrategyEquity/Equity, StrategyEquity/Return, Benchmark/Benchmark, PortfolioTurnover/PortfolioTurnover) into the corresponding retention list. Guard against double-capture on identical timestamps.
override GenerateStatisticsResults(...) — dedupe-by-time merge each retention list back into the matching series of the (cloned) charts parameter, then delegate to base.GenerateStatisticsResults.
Memory cost: 4 ChartPoints per day (~tens of bytes/day) added to retention. The existing chart-trim loop keeps doing its job for the streamed/displayed charts. Backtest paths are completely untouched.
Alternative, even smaller fix: skip the charts listed in AlgorithmPerformanceCharts (StrategyEquity, Benchmark) when iterating the trim loop. That property already exists in BaseResultsHandler with the docstring ``Used to calculate the probabilistic sharpe ratio''. Trade-off: StrategyEquity/Equity is intra-day sampled (every `ResamplePeriod = 2s`), so leaving it untrimmed grows ~1 GB/year per algorithm worst case — that's why the retention-buffer approach is preferred for production.
Reproducing the Problem
- Run any algorithm in live mode for at least three days.
- Inspect the algorithm's summary statistics in the result file (or via
self.statistics / Statistics) once 3+ midnight samples have elapsed.
- Observe
Probabilistic Sharpe Ratio = 0%, Sharpe Ratio = 0, Sortino Ratio = 0, Beta = 0, Alpha = 0, Tracking Error = 0, Information Ratio = 0, Compounding Annual Return = 0%, Drawdown ≈ last-2-days drawdown.
- The same algorithm run as a backtest covering an equivalent period produces non-zero values.
Internally observable via:
Charts["Strategy Equity"].Series["Return"].Values.Count stays at ≤ 2 after the first trim pass past day 2.
Charts["Benchmark"].Series["Benchmark"].Values.Count stays at ≤ 2.
System Information
- OS: reproduces on all platforms — root cause is platform-agnostic.
- Branch verified:
master (commit a0be00d47).
- Affected files:
Engine/Results/LiveTradingResultHandler.cs (trim loop), Engine/Results/BaseResultsHandler.cs (GenerateStatisticsResults), Common/Statistics/StatisticsBuilder.cs (PreprocessPerformanceValues, period derivation), Common/Statistics/PortfolioStatistics.cs (early-return at L231).
Checklist
Expected Behavior
A live algorithm should report the same statistics as a backtest covering the equivalent period once enough trading days have elapsed: Probabilistic Sharpe Ratio (PSR), Sharpe, Sortino, Alpha, Beta, Tracking Error, Information Ratio, Treynor, VaR, Compounding Annual Return and Drawdown should reflect the algorithm's full history.
Actual Behavior
After a live algorithm has been running for more than ~2 days, PSR is reported as
0%indefinitely. All of the otherlistPerformance-derived statistics also collapse to their defaults at the same time (Sharpe Ratio = 0,Sortino Ratio = 0,Alpha = 0,Beta = 0,Tracking Error = 0,Information Ratio = 0,Treynor = 0,Compounding Annual Return = 0%).DrawdownandDrawdown Recoveryalso become inaccurate (only reflect the last ~2 days). PSR is just the most visibly broken value because0%is never a plausible result for a running algorithm.The same algorithm run as a backtest over an equivalent period produces correct non-zero values.
Potential Solution
Root cause:
LiveTradingResultHandler.Update()runs a chart-trim pass every 10 minutes that removes every series sample older than 2 days:The
StrategyEquity/ReturnandBenchmark/Benchmarkseries are only sampled once a day (from the midnightDaily Samplingscheduled event registered inAlgorithmManager.Run→BaseResultsHandler.Sample(DateTime)), so after each trim each of those series holds at most ~2 points. The trim is rolling, not a one-time startup window, so running the algorithm longer doesn't help — at steady state these series always cap at ~2 daily points.StatisticsBuilder.PreprocessPerformanceValuesthen drops the first 2 entries (Day 0 / Day 1 alignment with backtest):…leaving
listPerformance.Count == 0, which causesPortfolioStatisticsto early-return:So
ProbabilisticSharpeRatio(and every metric computed after that line) keeps its default value of 0.There's also a cascade through the equity series:
StatisticsBuilder.Generatederives the analysis period from the (also-trimmed)StrategyEquity/Equityseries —firstDate = equity.Keys.First().Date— so even ifReturn/Benchmarkwere preserved,ChartPointToDictionary(pointsPerformance, fromDate, toDate)would still clip them to ~2 days.CompoundingAnnualReturnandDrawdownalso become wrong because they read from the trimmed equity directly.Backtest is unaffected:
BacktestingResultHandlerhas no equivalent trim loop.Proposed fix (smallest scope, live-only): retention buffer + override. Keep a separate, daily-resolution retention buffer of the statistics inputs inside
LiveTradingResultHandler, populated from the existing dailySample(DateTime)pass, and inject it into the (cloned) charts passed toGenerateStatisticsResults. Sketch:BaseResultsHandler.GenerateStatisticsResults(Dictionary<string, Chart>, ...)protected virtual(one-keyword change).LiveTradingResultHandler:Equity,Return,Benchmark,PortfolioTurnover) and a lock.override Sample(DateTime time)— callbase, then snapshotseries.Values[^1]for each statistics-relevant series (StrategyEquity/Equity,StrategyEquity/Return,Benchmark/Benchmark,PortfolioTurnover/PortfolioTurnover) into the corresponding retention list. Guard against double-capture on identical timestamps.override GenerateStatisticsResults(...)— dedupe-by-time merge each retention list back into the matching series of the (cloned)chartsparameter, then delegate tobase.GenerateStatisticsResults.Memory cost: 4
ChartPoints per day (~tens of bytes/day) added to retention. The existing chart-trim loop keeps doing its job for the streamed/displayed charts. Backtest paths are completely untouched.Alternative, even smaller fix: skip the charts listed in
AlgorithmPerformanceCharts(StrategyEquity,Benchmark) when iterating the trim loop. That property already exists inBaseResultsHandlerwith the docstring ``Used to calculate the probabilistic sharpe ratio''. Trade-off:StrategyEquity/Equityis intra-day sampled (every `ResamplePeriod = 2s`), so leaving it untrimmed grows ~1 GB/year per algorithm worst case — that's why the retention-buffer approach is preferred for production.Reproducing the Problem
self.statistics/Statistics) once 3+ midnight samples have elapsed.Probabilistic Sharpe Ratio = 0%,Sharpe Ratio = 0,Sortino Ratio = 0,Beta = 0,Alpha = 0,Tracking Error = 0,Information Ratio = 0,Compounding Annual Return = 0%,Drawdown≈ last-2-days drawdown.Internally observable via:
Charts["Strategy Equity"].Series["Return"].Values.Countstays at ≤ 2 after the first trim pass past day 2.Charts["Benchmark"].Series["Benchmark"].Values.Countstays at ≤ 2.System Information
master(commita0be00d47).Engine/Results/LiveTradingResultHandler.cs(trim loop),Engine/Results/BaseResultsHandler.cs(GenerateStatisticsResults),Common/Statistics/StatisticsBuilder.cs(PreprocessPerformanceValues, period derivation),Common/Statistics/PortfolioStatistics.cs(early-return at L231).Checklist
masterbranch