Skip to content

Commit ec80400

Browse files
committed
Add optimization analysis
1 parent 261366a commit ec80400

20 files changed

Lines changed: 1850 additions & 1 deletion

Common/Api/Optimization.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System;
1717
using System.Collections.Generic;
1818
using Newtonsoft.Json;
19+
using QuantConnect.Optimizer;
1920
using QuantConnect.Optimizer.Objectives;
2021
using QuantConnect.Util;
2122

@@ -74,6 +75,15 @@ public class Optimization : BaseOptimization
7475
/// </summary>
7576
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.ISOShort, DateFormat.UI)]
7677
public DateTime Requested { get; set; }
78+
79+
/// <summary>
80+
/// Aggregate diagnostic of the optimization (Sharpe distribution, parameter sensitivity
81+
/// slices, clusters in parameter space, local maxima, zero-order failure breakdown).
82+
/// Populated by the optimization analyzer in <c>LeanOptimizer.TriggerOnEndEvent</c>;
83+
/// omitted on optimizations that haven't run the analyzer or had no usable trials.
84+
/// </summary>
85+
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
86+
public OptimizationAnalysis Analysis { get; set; }
7787
}
7888

7989
/// <summary>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*
15+
*/
16+
17+
using QuantConnect.Optimizer.Parameters;
18+
using System.Collections.Generic;
19+
20+
namespace QuantConnect.Optimizer.Analysis
21+
{
22+
/// <summary>
23+
/// Bundles the inputs needed by <see cref="OptimizationAnalyzer"/>: the full list of
24+
/// completed trials from a finished optimization and the parameter grid spec that
25+
/// drove it. The optimization-side analogue of <c>ResultsAnalysisRunParameters</c>
26+
/// (which serves the backtest analyzer in Engine).
27+
/// </summary>
28+
public class OptimizationAnalysisRunParameters
29+
{
30+
/// <summary>
31+
/// All completed trials from the optimization (one per backtest), already mapped
32+
/// to the Common-side <see cref="OptimizationTrial"/> shape that the analyzer reads.
33+
/// </summary>
34+
public IReadOnlyList<OptimizationTrial> CompletedTrials { get; }
35+
36+
/// <summary>
37+
/// The optimization parameter grid spec (used for searched-min/max/step bounds and to
38+
/// drive per-parameter slicing).
39+
/// </summary>
40+
public IReadOnlyCollection<OptimizationParameter> OptimizationParameters { get; }
41+
42+
/// <summary>
43+
/// Initializes a new instance of the <see cref="OptimizationAnalysisRunParameters"/> class.
44+
/// </summary>
45+
/// <param name="completedTrials">The completed trials.</param>
46+
/// <param name="optimizationParameters">The parameter grid spec.</param>
47+
public OptimizationAnalysisRunParameters(
48+
IReadOnlyList<OptimizationTrial> completedTrials,
49+
IReadOnlyCollection<OptimizationParameter> optimizationParameters)
50+
{
51+
CompletedTrials = completedTrials;
52+
OptimizationParameters = optimizationParameters;
53+
}
54+
}
55+
}
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*
15+
*/
16+
17+
using Newtonsoft.Json;
18+
using QuantConnect.Logging;
19+
using QuantConnect.Optimizer.Parameters;
20+
using System.Collections.Generic;
21+
using System.Globalization;
22+
using System.Linq;
23+
24+
namespace QuantConnect.Optimizer.Analysis
25+
{
26+
// Types like OptimizationAnalysis / SharpeSummary / Cluster / Mode / etc. live in
27+
// QuantConnect.Optimizer (the parent namespace); they're referenced unqualified
28+
// because the file is inside QuantConnect.Optimizer.Analysis and the C# compiler
29+
// walks outward through parent namespaces when resolving simple names.
30+
/// <summary>
31+
/// Builds an aggregate diagnostic (<see cref="OptimizationAnalysis"/>) from a completed
32+
/// optimization's compute-job results. Computes the Sharpe distribution, the best trial,
33+
/// per-parameter sensitivity slices, k-means clusters in parameter space, local maxima,
34+
/// and a zero-order failure breakdown. The optimization-side analogue of
35+
/// <see cref="ResultsAnalyzer"/>; invoked from <c>LeanOptimizer.TriggerOnEndEvent</c>.
36+
/// </summary>
37+
public class OptimizationAnalyzer
38+
{
39+
private readonly OptimizationAnalysisRunParameters _parameters;
40+
41+
/// <summary>
42+
/// Initializes a new instance of the <see cref="OptimizationAnalyzer"/> class.
43+
/// </summary>
44+
/// <param name="parameters">The inputs to analyze.</param>
45+
public OptimizationAnalyzer(OptimizationAnalysisRunParameters parameters)
46+
{
47+
_parameters = parameters;
48+
}
49+
50+
/// <summary>
51+
/// Runs the full analysis pipeline and returns the aggregate diagnostic.
52+
/// </summary>
53+
/// <returns>The populated <see cref="OptimizationAnalysis"/>, or <c>null</c> if no usable trials remain.</returns>
54+
public OptimizationAnalysis Run()
55+
{
56+
var allTrials = ExtractTrials(_parameters.CompletedTrials);
57+
var trials = allTrials.Where(t => t.HasSharpe).ToList();
58+
if (trials.Count == 0)
59+
{
60+
Log.Trace("OptimizationAnalyzer.Run(): no completed backtests with parsable Sharpe ratios; skipping analysis");
61+
return null;
62+
}
63+
64+
var sharpes = trials.Select(t => t.Sharpe).ToList();
65+
var overall = new SharpeSummary
66+
{
67+
Mean = sharpes.Average(),
68+
StdDev = StdDev(sharpes),
69+
Min = sharpes.Min(),
70+
Max = sharpes.Max(),
71+
Median = Median(sharpes)
72+
};
73+
74+
// Always maximize Sharpe. The optimization's chosen Criterion may be something else
75+
// but the analyzer uses Sharpe as the universal yardstick for the analysis surface.
76+
var best = trials.OrderByDescending(t => t.Sharpe).First();
77+
var bestSummary = new BestTrialSummary
78+
{
79+
BacktestId = best.BacktestId,
80+
Parameters = new Dictionary<string, double>(best.Parameters),
81+
SharpeRatio = best.Sharpe
82+
};
83+
84+
var paramReports = _parameters.OptimizationParameters
85+
.Select(p => OptimizationSlicing.AnalyzeParameter(p, trials, best))
86+
.ToList();
87+
88+
var clusters = OptimizationClustering.Build(trials, _parameters.OptimizationParameters);
89+
var modes = OptimizationModes.Find(trials, _parameters.OptimizationParameters);
90+
var failed = OptimizationFailedBacktests.Build(allTrials);
91+
92+
return new OptimizationAnalysis
93+
{
94+
TrialCountTotal = allTrials.Count,
95+
TrialCountUsed = trials.Count,
96+
OverallSharpe = overall,
97+
Best = bestSummary,
98+
Parameters = paramReports,
99+
Clusters = clusters,
100+
Modes = modes,
101+
FailedBacktests = failed
102+
};
103+
}
104+
105+
// ── Trial extraction ─────────────────────────────────────────────────────
106+
107+
/// <summary>
108+
/// Parses each completed trial's JSON backtest payload into a typed
109+
/// <see cref="TrialRecord"/>: parameter values + Sharpe + total orders + the backtest's
110+
/// own diagnostic Analysis tags.
111+
/// </summary>
112+
private static List<TrialRecord> ExtractTrials(IReadOnlyList<OptimizationTrial> trialInputs)
113+
{
114+
var trials = new List<TrialRecord>();
115+
if (trialInputs == null) return trials;
116+
117+
foreach (var t in trialInputs)
118+
{
119+
if (t == null || string.IsNullOrEmpty(t.JsonBacktestResult) || t.ParameterSet == null)
120+
{
121+
continue;
122+
}
123+
124+
Dictionary<string, double> paramValues;
125+
try
126+
{
127+
paramValues = ParseParameterSet(t.ParameterSet);
128+
}
129+
catch
130+
{
131+
continue;
132+
}
133+
if (paramValues.Count == 0)
134+
{
135+
continue;
136+
}
137+
138+
ParsedBacktest parsed;
139+
try
140+
{
141+
parsed = JsonConvert.DeserializeObject<ParsedBacktest>(t.JsonBacktestResult);
142+
}
143+
catch
144+
{
145+
continue;
146+
}
147+
if (parsed == null)
148+
{
149+
continue;
150+
}
151+
152+
var hasSharpe = TryReadDouble(parsed.Statistics, "Sharpe Ratio", out var sharpe);
153+
TryReadInt(parsed.Statistics, "Total Orders", out var totalOrders);
154+
155+
var analysisNames = parsed.Analysis == null
156+
? new List<string>()
157+
: parsed.Analysis
158+
.Where(a => !string.IsNullOrEmpty(a?.Name))
159+
.Select(a => a.Name)
160+
.ToList();
161+
162+
trials.Add(new TrialRecord(
163+
backtestId: t.BacktestId,
164+
parameters: paramValues,
165+
sharpe: sharpe,
166+
hasSharpe: hasSharpe,
167+
totalOrders: totalOrders,
168+
analysisNames: analysisNames));
169+
}
170+
return trials;
171+
}
172+
173+
private static Dictionary<string, double> ParseParameterSet(ParameterSet parameterSet)
174+
{
175+
var result = new Dictionary<string, double>();
176+
if (parameterSet?.Value == null) return result;
177+
foreach (var kv in parameterSet.Value)
178+
{
179+
if (double.TryParse(kv.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var d))
180+
{
181+
result[kv.Key] = d;
182+
}
183+
}
184+
return result;
185+
}
186+
187+
private static bool TryReadDouble(IDictionary<string, string> statistics, string key, out double value)
188+
{
189+
value = 0;
190+
if (statistics == null) return false;
191+
if (!statistics.TryGetValue(key, out var raw) || string.IsNullOrEmpty(raw)) return false;
192+
// QC statistics often carry trailing units like "%" — strip them before parsing.
193+
var trimmed = raw.TrimEnd('%').Trim();
194+
return double.TryParse(trimmed, NumberStyles.Any, CultureInfo.InvariantCulture, out value);
195+
}
196+
197+
private static bool TryReadInt(IDictionary<string, string> statistics, string key, out int value)
198+
{
199+
value = 0;
200+
if (!TryReadDouble(statistics, key, out var d)) return false;
201+
value = (int)d;
202+
return true;
203+
}
204+
205+
// ── Aggregate helpers ────────────────────────────────────────────────────
206+
207+
private static double StdDev(IReadOnlyCollection<double> values)
208+
{
209+
if (values.Count < 2) return 0;
210+
var mean = values.Average();
211+
var s = values.Sum(v => (v - mean) * (v - mean));
212+
return System.Math.Sqrt(s / (values.Count - 1));
213+
}
214+
215+
private static double Median(IEnumerable<double> values)
216+
{
217+
var sorted = values.OrderBy(v => v).ToList();
218+
if (sorted.Count == 0) return 0;
219+
return sorted.Count % 2 == 1
220+
? sorted[sorted.Count / 2]
221+
: 0.5 * (sorted[sorted.Count / 2 - 1] + sorted[sorted.Count / 2]);
222+
}
223+
224+
// ── Minimal-shape DTO for JSON deserialization ───────────────────────────
225+
//
226+
// The JsonBacktestResult string carried by OptimizationResult is the Newtonsoft.Json
227+
// serialization of the in-process backtest Result. We only need a few fields; rather
228+
// than depend on the full Result type (with its many nested dependencies), bind a
229+
// minimal shape with the two properties we read.
230+
231+
private sealed class ParsedBacktest
232+
{
233+
[JsonProperty("Statistics")]
234+
public IDictionary<string, string> Statistics { get; set; }
235+
236+
[JsonProperty("Analysis")]
237+
public List<QuantConnect.Analysis> Analysis { get; set; }
238+
}
239+
}
240+
241+
/// <summary>
242+
/// Internal per-trial record used across the optimization-analysis helpers
243+
/// (<see cref="OptimizationClustering"/>, <see cref="OptimizationModes"/>,
244+
/// <see cref="OptimizationSlicing"/>, <see cref="OptimizationFailedBacktests"/>).
245+
/// Public visibility kept low: only the helpers in this namespace need it.
246+
/// </summary>
247+
internal sealed class TrialRecord
248+
{
249+
public string BacktestId { get; }
250+
public IReadOnlyDictionary<string, double> Parameters { get; }
251+
public double Sharpe { get; }
252+
public bool HasSharpe { get; }
253+
public int TotalOrders { get; }
254+
public IReadOnlyList<string> AnalysisNames { get; }
255+
256+
public TrialRecord(
257+
string backtestId,
258+
IReadOnlyDictionary<string, double> parameters,
259+
double sharpe,
260+
bool hasSharpe,
261+
int totalOrders,
262+
IReadOnlyList<string> analysisNames)
263+
{
264+
BacktestId = backtestId;
265+
Parameters = parameters;
266+
Sharpe = sharpe;
267+
HasSharpe = hasSharpe;
268+
TotalOrders = totalOrders;
269+
AnalysisNames = analysisNames;
270+
}
271+
}
272+
}

0 commit comments

Comments
 (0)