Skip to content

Commit 1f02c99

Browse files
committed
Address review pt 3
1 parent 05cfed5 commit 1f02c99

4 files changed

Lines changed: 37 additions & 39 deletions

File tree

Common/Optimizer/OptimizationBacktestMetrics.cs

Lines changed: 22 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ namespace QuantConnect.Optimizer
3131
public class OptimizationBacktestMetrics : BacktestSummary
3232
{
3333
/// <summary>
34-
/// The backtest's portfolio statistics; null when absent from the backtest result.
34+
/// The backtest's total performance (wraps <see cref="QuantConnect.Statistics.TradeStatistics"/>,
35+
/// <see cref="QuantConnect.Statistics.PortfolioStatistics"/>, and <see cref="QuantConnect.Statistics.Trade"/> list); null when absent from the backtest result.
3536
/// </summary>
36-
public PortfolioStatistics PortfolioStatistics { get; set; }
37+
public AlgorithmPerformance TotalPerformance { get; set; }
3738

3839
/// <summary>
3940
/// Number of orders the backtest produced.
@@ -64,34 +65,40 @@ public static OptimizationBacktestMetrics ExtractFrom(string backtestId, Paramet
6465
return null;
6566
}
6667

67-
ParsedBacktest parsed = null;
68+
AlgorithmPerformance totalPerformance = null;
69+
var totalOrders = 0;
70+
IReadOnlyList<string> analysisNames = System.Array.Empty<string>();
71+
6872
if (!string.IsNullOrEmpty(jsonBacktestResult))
6973
{
7074
try
7175
{
72-
parsed = JsonConvert.DeserializeObject<ParsedBacktest>(jsonBacktestResult);
76+
var jo = JObject.Parse(jsonBacktestResult);
77+
// Use case-insensitive lookups: BacktestingResultHandler serializes with CamelCaseNamingStrategy.
78+
totalPerformance = jo.GetValue("TotalPerformance", System.StringComparison.OrdinalIgnoreCase)?.ToObject<AlgorithmPerformance>();
79+
totalOrders = (jo.GetValue("Orders", System.StringComparison.OrdinalIgnoreCase) as JObject)?.Count ?? 0;
80+
var analyses = jo.GetValue("Analysis", System.StringComparison.OrdinalIgnoreCase)?.ToObject<List<QuantConnect.Analysis>>();
81+
if (analyses != null)
82+
{
83+
analysisNames = analyses
84+
.Where(a => !string.IsNullOrEmpty(a?.Name))
85+
.Select(a => a.Name)
86+
.ToList();
87+
}
7388
}
7489
catch (JsonException ex)
7590
{
7691
Log.Error(ex, $"OptimizationBacktestMetrics.ExtractFrom(): failed to parse backtest result for '{backtestId}'");
7792
}
7893
}
7994

80-
var portfolioStats = parsed?.TotalPerformance?.PortfolioStatistics;
81-
var analysisNames = parsed?.Analysis == null
82-
? (IReadOnlyList<string>)System.Array.Empty<string>()
83-
: parsed.Analysis
84-
.Where(a => !string.IsNullOrEmpty(a?.Name))
85-
.Select(a => a.Name)
86-
.ToList();
87-
8895
return new OptimizationBacktestMetrics
8996
{
9097
BacktestId = backtestId,
9198
Parameters = parameters,
92-
SharpeRatio = portfolioStats?.SharpeRatio ?? 0m,
93-
PortfolioStatistics = portfolioStats,
94-
TotalOrders = parsed?.Orders?.Count ?? 0,
99+
SharpeRatio = totalPerformance?.PortfolioStatistics?.SharpeRatio ?? 0m,
100+
TotalPerformance = totalPerformance,
101+
TotalOrders = totalOrders,
95102
AnalysisNames = analysisNames
96103
};
97104
}
@@ -109,24 +116,5 @@ private static Dictionary<string, decimal> ParseParameterSet(ParameterSet parame
109116
}
110117
return result;
111118
}
112-
113-
// Minimal-shape DTO that binds only the backtest-result fields the analyzer reads.
114-
private sealed class ParsedBacktest
115-
{
116-
[JsonProperty("TotalPerformance")]
117-
public ParsedTotalPerformance TotalPerformance { get; set; }
118-
119-
[JsonProperty("Orders")]
120-
public JObject Orders { get; set; }
121-
122-
[JsonProperty("Analysis")]
123-
public List<QuantConnect.Analysis> Analysis { get; set; }
124-
}
125-
126-
private sealed class ParsedTotalPerformance
127-
{
128-
[JsonProperty("PortfolioStatistics")]
129-
public PortfolioStatistics PortfolioStatistics { get; set; }
130-
}
131119
}
132120
}

Optimizer/Analysis/OptimizationAnalyzer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public class OptimizationAnalyzer
3333
public OptimizationAnalysis Run(OptimizationAnalysisRunParameters parameters)
3434
{
3535
var allBacktests = parameters?.CompletedBacktests ?? new List<OptimizationBacktestMetrics>();
36-
var backtests = allBacktests.Where(b => b?.PortfolioStatistics != null).ToList();
36+
var backtests = allBacktests.Where(b => b?.TotalPerformance?.PortfolioStatistics != null).ToList();
3737
if (backtests.Count == 0)
3838
{
3939
Log.Trace("OptimizationAnalyzer.Run(): no completed backtests with parsable Sharpe ratios; skipping analysis");

Optimizer/LeanOptimizer.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,14 @@ protected virtual void TriggerOnEndEvent()
193193
{
194194
try
195195
{
196+
// Snapshot under the lock so a late NewResult on another thread can't mutate the list mid-enumeration.
197+
List<OptimizationBacktestMetrics> backtestsSnapshot;
198+
lock (_completedBacktests)
199+
{
200+
backtestsSnapshot = new List<OptimizationBacktestMetrics>(_completedBacktests);
201+
}
196202
var parameters = new OptimizationAnalysisRunParameters(
197-
_completedBacktests,
203+
backtestsSnapshot,
198204
NodePacket.OptimizationParameters);
199205
result.Analysis = new OptimizationAnalyzer().Run(parameters);
200206
}
@@ -271,7 +277,11 @@ protected virtual void NewResult(string jsonBacktestResult, string backtestId)
271277
var metrics = OptimizationBacktestMetrics.ExtractFrom(backtestId, parameterSet, jsonBacktestResult);
272278
if (metrics != null)
273279
{
274-
_completedBacktests.Add(metrics);
280+
// Backtest results can arrive on different threads; guard _completedBacktests with its own lock.
281+
lock (_completedBacktests)
282+
{
283+
_completedBacktests.Add(metrics);
284+
}
275285
}
276286

277287
// always notify the strategy

Tests/Optimizer/Analysis/OptimizationAnalyzerTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,9 @@ public void ExtractFrom_ParsesSharpeAndAnalysisNamesFromBacktestJson()
162162
var metrics = OptimizationBacktestMetrics.ExtractFrom("bt-0", parameterSet, json);
163163

164164
Assert.NotNull(metrics);
165-
Assert.NotNull(metrics.PortfolioStatistics);
165+
Assert.NotNull(metrics.TotalPerformance?.PortfolioStatistics);
166166
Assert.AreEqual(0.75m, metrics.SharpeRatio);
167-
Assert.AreEqual(0.75m, metrics.PortfolioStatistics.SharpeRatio);
167+
Assert.AreEqual(0.75m, metrics.TotalPerformance.PortfolioStatistics.SharpeRatio);
168168
Assert.AreEqual(12, metrics.TotalOrders);
169169
CollectionAssert.AreEqual(new[] { "FlatEquityCurveAnalysis" }, metrics.AnalysisNames.ToArray());
170170
Assert.AreEqual(1m, metrics.Parameters["x"]);

0 commit comments

Comments
 (0)