chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:02:50 +08:00
commit 0fc60fdcb1
5008 changed files with 910633 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
/*
* 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 QuantConnect.Logging;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect.Optimizer.Analysis
{
/// <summary>
/// Builds an aggregate <see cref="OptimizationAnalysis"/> from a completed optimization's per-backtest metrics; optimization-side analogue of the Engine ResultsAnalyzer.
/// </summary>
public class OptimizationAnalyzer
{
/// <summary>
/// Runs the full optimization-analysis pipeline.
/// </summary>
/// <param name="parameters">Completed backtest metrics plus the parameter grid spec.</param>
/// <returns>The populated <see cref="OptimizationAnalysis"/>, or null when no usable backtests remain.</returns>
public OptimizationAnalysis Run(OptimizationAnalysisRunParameters parameters)
{
var allBacktests = parameters?.CompletedBacktests ?? new List<OptimizationBacktestMetrics>();
var backtests = allBacktests.Where(b => b?.TotalPerformance?.PortfolioStatistics != null).ToList();
if (backtests.Count == 0)
{
Log.Trace("OptimizationAnalyzer.Run(): no completed backtests with parsable Sharpe ratios; skipping analysis");
return null;
}
var sharpes = backtests.Select(b => b.SharpeRatio).ToList();
var overall = new SharpeSummary
{
Mean = sharpes.Average(),
StdDev = StdDev(sharpes),
Min = sharpes.Min(),
Max = sharpes.Max(),
Median = Median(sharpes)
};
// Sharpe is the universal yardstick regardless of the optimization's Criterion.
var best = backtests.OrderByDescending(b => b.SharpeRatio).First();
var bestSummary = new BacktestSummary
{
BacktestId = best.BacktestId,
Parameters = new Dictionary<string, decimal>(best.Parameters),
SharpeRatio = best.SharpeRatio
};
var paramReports = parameters.OptimizationParameters
.Select(p => OptimizationSlicing.AnalyzeParameter(p, backtests, best))
.ToList();
var clusters = OptimizationClustering.Build(backtests, parameters.OptimizationParameters);
var modes = OptimizationModes.Find(backtests, parameters.OptimizationParameters);
var failed = OptimizationFailedBacktests.Build(allBacktests);
return new OptimizationAnalysis
{
BacktestCountTotal = allBacktests.Count,
BacktestCountUsed = backtests.Count,
OverallSharpe = overall,
Best = bestSummary,
Parameters = paramReports,
Clusters = clusters,
Modes = modes,
FailedBacktests = failed
};
}
// ── Aggregate helpers ────────────────────────────────────────────────────
private static decimal StdDev(IReadOnlyCollection<decimal> values)
{
if (values.Count < 2) return 0m;
var mean = values.Average();
var s = values.Sum(v => (v - mean) * (v - mean));
// System.Math has no decimal Sqrt; cross into double for the root and back.
return (decimal)System.Math.Sqrt((double)(s / (values.Count - 1)));
}
private static decimal Median(IEnumerable<decimal> values)
{
var sorted = values.OrderBy(v => v).ToList();
if (sorted.Count == 0) return 0m;
return sorted.Count % 2 == 1
? sorted[sorted.Count / 2]
: 0.5m * (sorted[sorted.Count / 2 - 1] + sorted[sorted.Count / 2]);
}
}
}
@@ -0,0 +1,259 @@
/*
* 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 QuantConnect.Optimizer.Parameters;
using System;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect.Optimizer.Analysis
{
/// <summary>
/// K-means clustering of backtests in standardized parameter space with k chosen by an elbow heuristic.
/// </summary>
internal static class OptimizationClustering
{
private const int KMin = 2;
private const int KMaxAbsolute = 5;
private const int Seed = 42;
private const int MaxIterations = 100;
private const double PlateauThreshold = 0.7;
public static IReadOnlyList<Cluster> Build(
IReadOnlyList<OptimizationBacktestMetrics> backtests,
IReadOnlyCollection<OptimizationParameter> parameters)
{
var output = new List<Cluster>();
if (backtests == null || parameters == null) return output;
if (backtests.Count < KMin + 1 || parameters.Count == 0) return output;
var paramNames = parameters.Select(p => p.Name).ToArray();
var usable = backtests
.Where(b => paramNames.All(b.Parameters.ContainsKey))
.ToList();
if (usable.Count < KMin + 1) return output;
// Cap k_max at ceil(sqrt(N)) so small N doesn't get carved into too many clusters.
var sqrtCap = (int)Math.Ceiling(Math.Sqrt(usable.Count));
var kMaxEffective = Math.Min(KMaxAbsolute, sqrtCap);
var maxK = Math.Min(kMaxEffective, usable.Count - 1);
if (maxK < KMin) return output;
// K-means math runs in double so we can use Math.Sqrt / distance comparisons;
// we convert back to decimal at the boundary for the centroid output.
var raw = usable
.Select(b => paramNames.Select(n => (double)b.Parameters[n]).ToArray())
.ToArray();
var (normalized, means, stds) = Standardize(raw);
var byK = new Dictionary<int, KMeansResult>();
for (var k = KMin; k <= maxK; k++)
{
byK[k] = KMeans(normalized, k);
}
var bestK = SelectKByElbow(byK);
var pick = byK[bestK];
var centroidsOriginal = pick.Centroids
.Select(c => Denormalize(c, means, stds))
.ToArray();
for (var c = 0; c < bestK; c++)
{
var memberIndices = Enumerable.Range(0, usable.Count)
.Where(i => pick.Labels[i] == c)
.ToList();
if (memberIndices.Count == 0) continue;
var sharpes = memberIndices.Select(i => usable[i].SharpeRatio).ToList();
var centroidDict = new Dictionary<string, decimal>(paramNames.Length);
for (var d = 0; d < paramNames.Length; d++)
{
centroidDict[paramNames[d]] = (decimal)centroidsOriginal[c][d];
}
output.Add(new Cluster
{
Centroid = centroidDict,
MemberCount = memberIndices.Count,
SharpeMean = sharpes.Average(),
SharpeStdDev = StdDev(sharpes),
SharpeMin = sharpes.Min(),
SharpeMax = sharpes.Max()
});
}
// Order by mean Sharpe descending so the best-performing region is first.
return output.OrderByDescending(x => x.SharpeMean).ToList();
}
private static int SelectKByElbow(Dictionary<int, KMeansResult> results)
{
var ks = results.Keys.OrderBy(k => k).ToList();
if (ks.Count == 1) return ks[0];
for (var i = 1; i < ks.Count; i++)
{
var prev = results[ks[i - 1]].Wcss;
var curr = results[ks[i]].Wcss;
if (prev > 0 && curr / prev > PlateauThreshold) return ks[i - 1];
}
return ks[^1];
}
private sealed class KMeansResult
{
public int[] Labels { get; }
public double[][] Centroids { get; }
public double Wcss { get; }
public KMeansResult(int[] labels, double[][] centroids, double wcss)
{
Labels = labels;
Centroids = centroids;
Wcss = wcss;
}
}
private static KMeansResult KMeans(double[][] points, int k)
{
var n = points.Length;
var d = points[0].Length;
var rng = new Random(Seed);
// k-means++ initialization.
var centroids = new double[k][];
centroids[0] = (double[])points[rng.Next(n)].Clone();
for (var c = 1; c < k; c++)
{
var dists = new double[n];
for (var i = 0; i < n; i++)
{
var min = double.MaxValue;
for (var j = 0; j < c; j++)
{
var dd = SquaredDistance(points[i], centroids[j]);
if (dd < min) min = dd;
}
dists[i] = min;
}
var sum = dists.Sum();
var pick = rng.NextDouble() * sum;
double acc = 0;
var chosen = n - 1;
for (var i = 0; i < n; i++)
{
acc += dists[i];
if (acc >= pick) { chosen = i; break; }
}
centroids[c] = (double[])points[chosen].Clone();
}
// Lloyd's iteration.
var labels = new int[n];
for (var iter = 0; iter < MaxIterations; iter++)
{
var changed = false;
for (var i = 0; i < n; i++)
{
var best = 0;
var bestDist = double.MaxValue;
for (var c = 0; c < k; c++)
{
var dd = SquaredDistance(points[i], centroids[c]);
if (dd < bestDist) { bestDist = dd; best = c; }
}
if (labels[i] != best) { labels[i] = best; changed = true; }
}
if (!changed && iter > 0) break;
var sums = new double[k][];
var counts = new int[k];
for (var c = 0; c < k; c++) sums[c] = new double[d];
for (var i = 0; i < n; i++)
{
var c = labels[i];
counts[c]++;
for (var j = 0; j < d; j++) sums[c][j] += points[i][j];
}
for (var c = 0; c < k; c++)
{
if (counts[c] == 0) continue;
for (var j = 0; j < d; j++) centroids[c][j] = sums[c][j] / counts[c];
}
}
double wcss = 0;
for (var i = 0; i < n; i++) wcss += SquaredDistance(points[i], centroids[labels[i]]);
return new KMeansResult(labels, centroids, wcss);
}
private static (double[][] Normalized, double[] Means, double[] Stds) Standardize(double[][] points)
{
var n = points.Length;
var d = points[0].Length;
var means = new double[d];
var stds = new double[d];
for (var j = 0; j < d; j++)
{
double s = 0;
for (var i = 0; i < n; i++) s += points[i][j];
means[j] = s / n;
}
for (var j = 0; j < d; j++)
{
double s = 0;
for (var i = 0; i < n; i++)
{
var t = points[i][j] - means[j];
s += t * t;
}
stds[j] = n > 1 ? Math.Sqrt(s / (n - 1)) : 1.0;
if (stds[j] < 1e-12) stds[j] = 1.0;
}
var normalized = new double[n][];
for (var i = 0; i < n; i++)
{
normalized[i] = new double[d];
for (var j = 0; j < d; j++) normalized[i][j] = (points[i][j] - means[j]) / stds[j];
}
return (normalized, means, stds);
}
private static double[] Denormalize(double[] standardized, double[] means, double[] stds)
{
var d = standardized.Length;
var result = new double[d];
for (var j = 0; j < d; j++) result[j] = standardized[j] * stds[j] + means[j];
return result;
}
private static double SquaredDistance(double[] a, double[] b)
{
double s = 0;
for (var i = 0; i < a.Length; i++) { var d = a[i] - b[i]; s += d * d; }
return s;
}
private static decimal StdDev(IReadOnlyCollection<decimal> values)
{
if (values.Count < 2) return 0m;
var mean = values.Average();
var s = values.Sum(v => (v - mean) * (v - mean));
return (decimal)Math.Sqrt((double)(s / (values.Count - 1)));
}
}
}
@@ -0,0 +1,64 @@
/*
* 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 System;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect.Optimizer.Analysis
{
/// <summary>
/// Counts how many zero-order backtests carry each backtest-level analysis tag; returns null when no zero-order backtests exist.
/// </summary>
internal static class OptimizationFailedBacktests
{
// Cap on inspected backtests; a rough tally is enough.
private const int MaxBacktestsToInspect = 10;
public static FailedBacktestSummary Build(IReadOnlyList<OptimizationBacktestMetrics> backtests)
{
if (backtests == null) return null;
var zeroOrder = backtests.Where(b => b.TotalOrders == 0).ToList();
if (zeroOrder.Count == 0) return null;
var sample = zeroOrder.Take(MaxBacktestsToInspect).ToList();
var nameCount = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var backtest in sample)
{
// De-dupe per backtest so counts are "backtests carrying the tag", not raw occurrences.
var seen = new HashSet<string>(StringComparer.Ordinal);
if (backtest.AnalysisNames != null)
{
foreach (var name in backtest.AnalysisNames)
{
if (string.IsNullOrEmpty(name)) continue;
if (!seen.Add(name)) continue;
nameCount[name] = nameCount.GetValueOrDefault(name, 0) + 1;
}
}
}
return new FailedBacktestSummary
{
ZeroOrderCount = zeroOrder.Count,
InspectedCount = sample.Count,
AnalysisNameCounts = nameCount
};
}
}
}
+108
View File
@@ -0,0 +1,108 @@
/*
* 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 QuantConnect.Optimizer.Parameters;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace QuantConnect.Optimizer.Analysis
{
/// <summary>
/// Detects local maxima of the Sharpe surface; backtests strictly greater than every face-neighbor on the parameter grid.
/// </summary>
internal static class OptimizationModes
{
public static IReadOnlyList<Mode> Find(
IReadOnlyList<OptimizationBacktestMetrics> backtests,
IReadOnlyCollection<OptimizationParameter> parameters)
{
var modes = new List<Mode>();
if (backtests == null || parameters == null) return modes;
if (parameters.Count == 0 || backtests.Count == 0) return modes;
var paramNames = parameters.Select(p => p.Name).ToArray();
// Sorted distinct values per parameter define the grid axes.
var axisValues = new Dictionary<string, List<decimal>>();
foreach (var name in paramNames)
{
axisValues[name] = backtests
.Where(b => b.Parameters.ContainsKey(name))
.Select(b => b.Parameters[name])
.Distinct()
.OrderBy(v => v)
.ToList();
}
// Map each backtest to its grid position.
var indexed = new List<(OptimizationBacktestMetrics Backtest, int[] Indices)>();
foreach (var b in backtests)
{
if (!paramNames.All(b.Parameters.ContainsKey)) continue;
var idx = new int[paramNames.Length];
var ok = true;
for (var d = 0; d < paramNames.Length; d++)
{
idx[d] = axisValues[paramNames[d]].IndexOf(b.Parameters[paramNames[d]]);
if (idx[d] < 0) { ok = false; break; }
}
if (ok) indexed.Add((b, idx));
}
var byTuple = indexed.ToDictionary(p => TupleKey(p.Indices), p => p.Backtest);
foreach (var (backtest, idx) in indexed)
{
var totalNeighbors = 0;
var dominatesAll = true;
for (var d = 0; d < paramNames.Length && dominatesAll; d++)
{
var axisLen = axisValues[paramNames[d]].Count;
foreach (var delta in new[] { -1, 1 })
{
var ni = idx[d] + delta;
if (ni < 0 || ni >= axisLen) continue;
var neighborIdx = (int[])idx.Clone();
neighborIdx[d] = ni;
if (!byTuple.TryGetValue(TupleKey(neighborIdx), out var neighbor)) continue;
totalNeighbors++;
if (neighbor.SharpeRatio >= backtest.SharpeRatio) { dominatesAll = false; break; }
}
}
if (dominatesAll && totalNeighbors > 0)
{
modes.Add(new Mode
{
BacktestId = backtest.BacktestId,
Parameters = new Dictionary<string, decimal>(backtest.Parameters),
SharpeRatio = backtest.SharpeRatio,
NeighborCount = totalNeighbors
});
}
}
return modes.OrderByDescending(m => m.SharpeRatio).ToList();
}
private static string TupleKey(int[] indices)
=> string.Join(",", indices.Select(i => i.ToString(CultureInfo.InvariantCulture)));
}
}
+173
View File
@@ -0,0 +1,173 @@
/*
* 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 QuantConnect.Optimizer.Parameters;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace QuantConnect.Optimizer.Analysis
{
/// <summary>
/// Per-parameter sensitivity analysis via 1-D slices through the backtest cloud with a piecewise linear fit.
/// </summary>
internal static class OptimizationSlicing
{
public static ParameterReport AnalyzeParameter(
OptimizationParameter parameter,
IReadOnlyList<OptimizationBacktestMetrics> backtests,
OptimizationBacktestMetrics best)
{
var name = parameter.Name;
var owning = backtests.Where(b => b.Parameters.ContainsKey(name)).ToList();
var otherParamNames = owning
.SelectMany(b => b.Parameters.Keys)
.Where(k => k != name)
.Distinct()
.OrderBy(k => k, StringComparer.Ordinal)
.ToList();
// Group backtests by other-parameter values; each group is one 1-D slice.
IEnumerable<IGrouping<string, OptimizationBacktestMetrics>> grouped = otherParamNames.Count == 0
? new[] { owning.GroupBy(_ => "").FirstOrDefault() }
.Where(g => g != null)
.Cast<IGrouping<string, OptimizationBacktestMetrics>>()
: owning.GroupBy(b => SliceKey(b, otherParamNames));
var slices = new List<SliceFit>();
foreach (var group in grouped)
{
var slice = BuildSlice(group.ToList(), name, otherParamNames);
if (slice != null) slices.Add(slice);
}
var hasBest = best.Parameters.TryGetValue(name, out var bestValue);
var (searchedMin, searchedMax, step) = ExtractGridSpec(parameter, owning, name);
var bestAtEdge = hasBest && IsAtSearchedEdge(bestValue, searchedMin, searchedMax, step);
var meanRange = slices.Count > 0 ? slices.Average(s => s.SharpeRange) : 0m;
var maxRange = slices.Count > 0 ? slices.Max(s => s.SharpeRange) : 0m;
var maxDerivPerStep = slices.Count > 0
? slices.Max(s => s.MaxAbsDerivative) * (step ?? 1m)
: 0m;
return new ParameterReport
{
Name = name,
SearchedMin = searchedMin,
SearchedMax = searchedMax,
Step = step,
MeanWithinSliceSharpeRange = meanRange,
MaxWithinSliceSharpeRange = maxRange,
MaxAbsDerivativePerStep = maxDerivPerStep,
BestValue = bestValue,
BestAtSearchedEdge = bestAtEdge,
Slices = slices
};
}
private static SliceFit BuildSlice(
List<OptimizationBacktestMetrics> backtests,
string varyingParamName,
IReadOnlyList<string> otherParamNames)
{
// Defensively collapse duplicate parameter values by averaging Sharpes.
var points = backtests
.GroupBy(b => b.Parameters[varyingParamName])
.Select(g => (X: g.Key, Y: g.Average(b => b.SharpeRatio)))
.OrderBy(p => p.X)
.ToList();
if (points.Count == 0) return null;
var xs = points.Select(p => p.X).ToList();
var ys = points.Select(p => p.Y).ToList();
var sharpeRange = ys.Count >= 2 ? ys.Max() - ys.Min() : 0m;
// Piecewise linear: one segment per adjacent pair; slope is sensitivity per parameter unit.
var segments = new List<LinearSegment>();
decimal maxAbsDerivative = 0m;
for (var i = 0; i < points.Count - 1; i++)
{
var dx = xs[i + 1] - xs[i];
var slope = (ys[i + 1] - ys[i]) / dx;
segments.Add(new LinearSegment
{
XLo = xs[i],
XHi = xs[i + 1],
A = ys[i],
B = slope
});
var absSlope = Math.Abs(slope);
if (absSlope > maxAbsDerivative) maxAbsDerivative = absSlope;
}
var fixedParams = new Dictionary<string, decimal>();
if (otherParamNames.Count > 0)
{
var first = backtests[0];
foreach (var p in otherParamNames)
{
if (first.Parameters.TryGetValue(p, out var v)) fixedParams[p] = v;
}
}
return new SliceFit
{
FixedParameters = fixedParams,
SharpeRange = sharpeRange,
MaxAbsDerivative = maxAbsDerivative,
Segments = segments
};
}
private static (decimal Min, decimal Max, decimal? Step) ExtractGridSpec(
OptimizationParameter parameter,
IReadOnlyList<OptimizationBacktestMetrics> owning,
string name)
{
if (parameter is OptimizationStepParameter step)
{
return (step.MinValue, step.MaxValue, step.Step);
}
// Fallback for non-step parameters: infer min/max/step from measured values.
var values = owning.Select(b => b.Parameters[name]).Distinct().OrderBy(v => v).ToList();
if (values.Count == 0) return (0m, 0m, null);
if (values.Count == 1) return (values[0], values[0], null);
var min = values[0];
var max = values[^1];
var gaps = new List<decimal>();
for (var i = 1; i < values.Count; i++) gaps.Add(values[i] - values[i - 1]);
return (min, max, gaps.Min());
}
private static bool IsAtSearchedEdge(decimal value, decimal min, decimal max, decimal? step)
{
var tol = ((step ?? 1m) / 2m) + 1e-9m;
return Math.Abs(value - min) <= tol || Math.Abs(value - max) <= tol;
}
private static string SliceKey(OptimizationBacktestMetrics backtest, IReadOnlyList<string> otherParamNames)
{
return string.Join("|", otherParamNames.Select(p =>
(backtest.Parameters.TryGetValue(p, out var v) ? v.ToString(CultureInfo.InvariantCulture) : "NaN")));
}
}
}