Skip to content

PSR (and other risk metrics) stay at 0% in live mode due to chart trimming #9477

@AlexCatarino

Description

@AlexCatarino

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.RunBaseResultsHandler.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:

  1. Make BaseResultsHandler.GenerateStatisticsResults(Dictionary<string, Chart>, ...) protected virtual (one-keyword change).
  2. 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

  1. Run any algorithm in live mode for at least three days.
  2. Inspect the algorithm's summary statistics in the result file (or via self.statistics / Statistics) once 3+ midnight samples have elapsed.
  3. 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.
  4. 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

  • I have completely filled out this template
  • I have confirmed that this issue exists on the current master branch
  • I have confirmed that this is not a duplicate issue by searching issues
  • I have provided detailed steps to reproduce the issue

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions