Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7716046
Initial backtest analyzer commit
jhonabreul Mar 10, 2026
9b24d0e
Changes for cleanup
jhonabreul Mar 10, 2026
3edde8d
Minor fix
jhonabreul Mar 11, 2026
552f1b8
Add class for analysis result context
jhonabreul Mar 11, 2026
34adf4f
Fix more models
jhonabreul Mar 11, 2026
87369ab
More fixes
jhonabreul Mar 12, 2026
bcf84d8
More fixes
jhonabreul Mar 12, 2026
8bc0877
More fixes
jhonabreul Mar 12, 2026
4cfb9c5
More fixes
jhonabreul Mar 12, 2026
119d12c
Cleanup
jhonabreul Mar 12, 2026
44ec8a9
Cleanup
jhonabreul Mar 12, 2026
a8af21c
Add documentation
jhonabreul Mar 13, 2026
403931a
Cleanup and remove some classes
jhonabreul Mar 13, 2026
ed1701a
Write backtest analysis to results file
jhonabreul Mar 13, 2026
4159996
Cleanup
jhonabreul Mar 13, 2026
39ed9fd
Minor fixes
jhonabreul Mar 13, 2026
43b9d5e
Minor fix
jhonabreul Mar 16, 2026
e7b748b
Minor cleanup
jhonabreul Mar 16, 2026
54d93ad
Implement json deserialization for backtest analysis result
jhonabreul Mar 16, 2026
1450fd5
Rename analyzer classes to generalize
jhonabreul Mar 17, 2026
ff1d69d
Add weight to analyses
jhonabreul Mar 18, 2026
b32ce58
Run analyses in order by weight
jhonabreul Mar 18, 2026
71ea601
Cleanup
jhonabreul Mar 18, 2026
2ab7984
Cleanup
jhonabreul Mar 18, 2026
721bedf
Update Issue text
DerekMelchin Mar 18, 2026
05f2aec
Set sub-test weights to 0
DerekMelchin Mar 19, 2026
266506d
Update test weights
DerekMelchin Mar 19, 2026
501f7b1
Add xml docs
jhonabreul Mar 19, 2026
4ded883
Disable result analysis on regression tests
jhonabreul Mar 19, 2026
818b5a3
Refactor analysis naming
jhonabreul Mar 24, 2026
d6ff307
Renaming cleanup
jhonabreul Mar 24, 2026
24c1d78
Simplify analysis results context
jhonabreul Apr 7, 2026
ac21a4f
Log when analyzer reaches max tests or time limit
jhonabreul Apr 7, 2026
363af34
Sample down equity curve to match daily benchmark
jhonabreul Apr 7, 2026
9ff673e
Minor changes
jhonabreul Apr 7, 2026
674f482
Minor fixes
jhonabreul Apr 7, 2026
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
57 changes: 57 additions & 0 deletions Common/Analysis.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

using Newtonsoft.Json;
using System.Collections.Generic;

namespace QuantConnect
{
/// <summary>
/// Represents the outcome of a single backtest diagnostic analysis,
/// containing the analysis name, diagnostic context, and a list of solutions.
/// </summary>
public class Analysis(string name, string issue, object sample, int? count, IReadOnlyList<string> solutions)
{
/// <summary>
/// Gets or sets the name of the analysis that produced this result.
/// </summary>
public string Name { get; set; } = name;

/// <summary>
/// Gets or sets a short description of why the analysis was triggered.
/// </summary>
public string Issue { get; set; } = issue;

/// <summary>
/// Gets or sets a representative sample value of the issue detected by the analysis.
/// It can be something like a log message, an order or an order event.
/// </summary>
public object Sample { get; set; } = sample;

/// <summary>
/// Gets or sets the total number of matching occurrences found by the analysis.
/// If null, the analysis is reporting a single issue with the provided sample;
/// if not null, the sample represents one of multiple occurrences of the same issue.
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public int? Count { get; set; } = count;

/// <summary>
/// Gets or sets human-readable suggestions for resolving the detected issue.
/// </summary>
public IReadOnlyList<string> Solutions { get; set; } = solutions;
}
}
13 changes: 13 additions & 0 deletions Common/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4558,6 +4558,19 @@ public static int GreatestCommonDivisor(this IEnumerable<int> values)
return result.Value;
}

/// <summary>
/// Returns a new sorted list of (v[i] / v[i-1] - 1) values. The first key is dropped.
/// </summary>
public static SortedList<DateTime, decimal> PercentChange(this SortedList<DateTime, decimal> values)
{
var result = new SortedList<DateTime, decimal>();
foreach (var (current, previous) in values.Skip(1).Zip(values, (current, previous) => (current, previous)))
{
result.Add(current.Key, current.Value / previous.Value - 1);
}
return result;
}

/// <summary>
/// Gets the greatest common divisor of two numbers
/// </summary>
Expand Down
7 changes: 4 additions & 3 deletions Common/Packets/BacktestResultParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
*
*/

using System;
using QuantConnect.Orders;
using QuantConnect.Statistics;
using System;
using System.Collections.Generic;

namespace QuantConnect.Packets
Expand All @@ -43,8 +43,9 @@ public BacktestResultParameters(IDictionary<string, Chart> charts,
List<OrderEvent> orderEvents,
AlgorithmPerformance totalPerformance = null,
AlgorithmConfiguration algorithmConfiguration = null,
IDictionary<string, string> state = null)
: base(charts, orders, profitLoss, statistics, runtimeStatistics, orderEvents, totalPerformance, algorithmConfiguration, state)
IDictionary<string, string> state = null,
IReadOnlyList<Analysis> analysisResult = null)
: base(charts, orders, profitLoss, statistics, runtimeStatistics, orderEvents, totalPerformance, algorithmConfiguration, state, analysisResult)
{
RollingWindow = rollingWindow;
}
Expand Down
9 changes: 8 additions & 1 deletion Common/Packets/BaseResultParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ public class BaseResultParameters
/// </summary>
public AlgorithmPerformance TotalPerformance { get; set; }

/// <summary>
/// Backtest analysis results.
/// </summary>
public IReadOnlyList<Analysis> Analysis { get; set; }

/// <summary>
/// Creates a new instance
/// </summary>
Expand All @@ -82,7 +87,8 @@ public BaseResultParameters(IDictionary<string, Chart> charts,
List<OrderEvent> orderEvents,
AlgorithmPerformance totalPerformance = null,
AlgorithmConfiguration algorithmConfiguration = null,
IDictionary<string, string> state = null)
IDictionary<string, string> state = null,
IReadOnlyList<Analysis> analysisResult = null)
{
Charts = charts;
Orders = orders;
Expand All @@ -93,6 +99,7 @@ public BaseResultParameters(IDictionary<string, Chart> charts,
AlgorithmConfiguration = algorithmConfiguration;
State = state;
TotalPerformance = totalPerformance;
Analysis = analysisResult;
}
}
}
5 changes: 3 additions & 2 deletions Common/Packets/LiveResultParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ public LiveResultParameters(IDictionary<string, Chart> charts,
AlgorithmPerformance totalPerformance = null,
IDictionary<string, string> serverStatistics = null,
AlgorithmConfiguration algorithmConfiguration = null,
IDictionary<string, string> state = null)
: base(charts, orders, profitLoss, statistics, runtimeStatistics, orderEvents, totalPerformance, algorithmConfiguration, state)
IDictionary<string, string> state = null,
IReadOnlyList<Analysis> analysisResult = null)
: base(charts, orders, profitLoss, statistics, runtimeStatistics, orderEvents, totalPerformance, algorithmConfiguration, state, analysisResult)
{
Holdings = holdings;
CashBook = cashBook;
Expand Down
7 changes: 7 additions & 0 deletions Common/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ public class Result
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public AlgorithmPerformance TotalPerformance { get; set; }

/// <summary>
/// Backtest analysis results.
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public IReadOnlyList<Analysis> Analysis { get; set; }

/// <summary>
/// Creates new empty instance
/// </summary>
Expand All @@ -111,6 +117,7 @@ public Result(BaseResultParameters parameters)
AlgorithmConfiguration = parameters.AlgorithmConfiguration;
State = parameters.State;
TotalPerformance = parameters.TotalPerformance;
Analysis = parameters.Analysis;
}
}
}
76 changes: 76 additions & 0 deletions Engine/Results/Analysis/Analyses/BaseResultsAnalysis.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
using Python.Runtime;
using System.Collections.Generic;
using System.Linq;

namespace QuantConnect.Lean.Engine.Results.Analysis.Analyses
{
/// <summary>
/// Abstract base class for all backtest diagnostic tests.
/// </summary>
public abstract class BaseResultsAnalysis
{
/// <summary>
/// Gets a short (3–8 word) description of why the analysis was triggered.
/// </summary>
public abstract string Issue { get; }

/// <summary>
/// Gets the severity/impact weight (0–100). Higher values run first and rank higher in results.
/// </summary>
public abstract int Weight { get; }

/// <summary>
/// Runs the analysis against all backtest data provided in <paramref name="parameters"/>.
/// </summary>
public abstract IReadOnlyList<QuantConnect.Analysis> Run(ResultsAnalysisRunParameters parameters);

/// <summary>
/// Wraps a single <see cref="QuantConnect.Analysis"/> in a one-element read-only list.
/// </summary>
protected IReadOnlyList<QuantConnect.Analysis> SingleResponse(object sample, IReadOnlyList<string> solutions = null)
=> SingleResponse(sample, null, solutions);

/// <summary>
/// Wraps a single <see cref="QuantConnect.Analysis"/> in a one-element read-only list.
/// </summary>
protected IReadOnlyList<QuantConnect.Analysis> SingleResponse(object sample, int? count, IReadOnlyList<string> solutions = null)
=> [new(GetType().Name, Issue, sample, count, solutions ?? [])];

/// <summary>
/// Filters <paramref name="responses"/> to those with solutions,
/// prefixes the class name, and returns a flat list.
/// </summary>
protected IReadOnlyList<QuantConnect.Analysis> CreateAggregatedResponse(IEnumerable<QuantConnect.Analysis> responses)
=> responses
.Where(x => x.Solutions.Count > 0)
.Select(x => new QuantConnect.Analysis(GetType().Name + " / " + x.Name, x.Issue, x.Sample, x.Count, x.Solutions))
.ToList();

/// <summary>
/// Formats the specified code string according to the conventions of the given programming language.
/// </summary>
protected static string FormatCode(string code, Language language)
{
return language switch
{
Language.Python => code.ToSnakeCase(),
_ => code
};
}
}
}
Loading
Loading