chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
/*
|
||||
* 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.Threading;
|
||||
using QuantConnect.Util;
|
||||
using QuantConnect.Logging;
|
||||
using QuantConnect.Configuration;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using QuantConnect.Optimizer.Analysis;
|
||||
using QuantConnect.Optimizer.Objectives;
|
||||
using QuantConnect.Optimizer.Parameters;
|
||||
using QuantConnect.Optimizer.Strategies;
|
||||
|
||||
namespace QuantConnect.Optimizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Base Lean optimizer class in charge of handling an optimization job packet
|
||||
/// </summary>
|
||||
public abstract class LeanOptimizer : IDisposable
|
||||
{
|
||||
private readonly int _optimizationUpdateInterval = Config.GetInt("optimization-update-interval", 10);
|
||||
|
||||
private DateTime _startedAt = DateTime.UtcNow;
|
||||
|
||||
private DateTime _lastUpdate;
|
||||
private int _failedBacktest;
|
||||
private int _completedBacktest;
|
||||
private volatile bool _disposed;
|
||||
|
||||
// Per-backtest metrics extracted in NewResult so we don't retain the full backtest JSON.
|
||||
private readonly List<OptimizationBacktestMetrics> _completedBacktests = new();
|
||||
|
||||
/// <summary>
|
||||
/// The total completed backtests count
|
||||
/// </summary>
|
||||
protected int CompletedBacktests => _failedBacktest + _completedBacktest;
|
||||
|
||||
/// <summary>
|
||||
/// Lock to update optimization status
|
||||
/// </summary>
|
||||
private object _statusLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// The current optimization status
|
||||
/// </summary>
|
||||
protected OptimizationStatus Status { get; private set; } = OptimizationStatus.New;
|
||||
|
||||
/// <summary>
|
||||
/// The optimization target
|
||||
/// </summary>
|
||||
protected Target OptimizationTarget { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Collection holding <see cref="ParameterSet"/> for each backtest id we are waiting to finish
|
||||
/// </summary>
|
||||
protected ConcurrentDictionary<string, ParameterSet> RunningParameterSetForBacktest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Collection holding <see cref="ParameterSet"/> for each backtest id we are waiting to launch
|
||||
/// </summary>
|
||||
/// <remarks>We can't launch 1 million backtests at the same time</remarks>
|
||||
protected ConcurrentQueue<ParameterSet> PendingParameterSet { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The optimization strategy being used
|
||||
/// </summary>
|
||||
protected IOptimizationStrategy Strategy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The optimization packet
|
||||
/// </summary>
|
||||
protected OptimizationNodePacket NodePacket { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether optimizer was disposed
|
||||
/// </summary>
|
||||
protected bool Disposed => _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when the optimization work ended
|
||||
/// </summary>
|
||||
public event EventHandler<OptimizationResult> Ended;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance
|
||||
/// </summary>
|
||||
/// <param name="nodePacket">The optimization node packet to handle</param>
|
||||
protected LeanOptimizer(OptimizationNodePacket nodePacket)
|
||||
{
|
||||
if (nodePacket.OptimizationParameters.IsNullOrEmpty())
|
||||
{
|
||||
throw new ArgumentException("Cannot start an optimization job with no parameter to optimize");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(nodePacket.Criterion?.Target))
|
||||
{
|
||||
throw new ArgumentException("Cannot start an optimization job with no target to optimize");
|
||||
}
|
||||
|
||||
NodePacket = nodePacket;
|
||||
OptimizationTarget = NodePacket.Criterion;
|
||||
OptimizationTarget.Reached += (s, e) =>
|
||||
{
|
||||
// we've reached the optimization target
|
||||
TriggerOnEndEvent();
|
||||
};
|
||||
|
||||
Strategy = (IOptimizationStrategy)Activator.CreateInstance(Type.GetType(NodePacket.OptimizationStrategy));
|
||||
|
||||
RunningParameterSetForBacktest = new ConcurrentDictionary<string, ParameterSet>();
|
||||
PendingParameterSet = new ConcurrentQueue<ParameterSet>();
|
||||
|
||||
Strategy.Initialize(OptimizationTarget, nodePacket.Constraints, NodePacket.OptimizationParameters, NodePacket.OptimizationStrategySettings);
|
||||
|
||||
Strategy.NewParameterSet += (s, parameterSet) =>
|
||||
{
|
||||
if (parameterSet == null)
|
||||
{
|
||||
// shouldn't happen
|
||||
Log.Error($"Strategy.NewParameterSet({GetLogDetails()}): generated a null {nameof(ParameterSet)} instance");
|
||||
return;
|
||||
}
|
||||
LaunchLeanForParameterSet(parameterSet);
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the optimization
|
||||
/// </summary>
|
||||
public virtual void Start()
|
||||
{
|
||||
lock (RunningParameterSetForBacktest)
|
||||
{
|
||||
Strategy.PushNewResults(OptimizationResult.Initial);
|
||||
|
||||
// if after we started there are no running parameter sets means we have failed to start
|
||||
if (RunningParameterSetForBacktest.Count == 0)
|
||||
{
|
||||
SetOptimizationStatus(OptimizationStatus.Aborted);
|
||||
throw new InvalidOperationException($"LeanOptimizer.Start({GetLogDetails()}): failed to start");
|
||||
}
|
||||
Log.Trace($"LeanOptimizer.Start({GetLogDetails()}): start ended. Waiting on {RunningParameterSetForBacktest.Count + PendingParameterSet.Count} backtests");
|
||||
}
|
||||
|
||||
SetOptimizationStatus(OptimizationStatus.Running);
|
||||
ProcessUpdate(forceSend: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the optimization job end event
|
||||
/// </summary>
|
||||
protected virtual void TriggerOnEndEvent()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
SetOptimizationStatus(OptimizationStatus.Completed);
|
||||
|
||||
var result = Strategy.Solution;
|
||||
if (result != null)
|
||||
{
|
||||
var constraint = NodePacket.Constraints != null ? $"Constraints: ({string.Join(",", NodePacket.Constraints)})" : string.Empty;
|
||||
Log.Trace($"LeanOptimizer.TriggerOnEndEvent({GetLogDetails()}): Optimization has ended. " +
|
||||
$"Result for {OptimizationTarget}: was reached using ParameterSet: ({result.ParameterSet}) backtestId '{result.BacktestId}'. " +
|
||||
$"{constraint}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Trace($"LeanOptimizer.TriggerOnEndEvent({GetLogDetails()}): Optimization has ended. Result was not reached");
|
||||
}
|
||||
|
||||
// we clean up before we send an update so that the runtime stats are updated
|
||||
CleanUpRunningInstance();
|
||||
|
||||
// Set Analysis before ProcessUpdate so SendUpdate can upload it; guarded so analyzer failure never breaks the optimization.
|
||||
if (result != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Snapshot under the lock so a late NewResult on another thread can't mutate the list mid-enumeration.
|
||||
List<OptimizationBacktestMetrics> backtestsSnapshot;
|
||||
lock (_completedBacktests)
|
||||
{
|
||||
backtestsSnapshot = new List<OptimizationBacktestMetrics>(_completedBacktests);
|
||||
}
|
||||
var parameters = new OptimizationAnalysisRunParameters(
|
||||
backtestsSnapshot,
|
||||
NodePacket.OptimizationParameters);
|
||||
result.Analysis = new OptimizationAnalyzer().Run(parameters);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error running optimization analysis");
|
||||
}
|
||||
}
|
||||
|
||||
ProcessUpdate(forceSend: true);
|
||||
|
||||
Ended?.Invoke(this, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles starting Lean for a given parameter set
|
||||
/// </summary>
|
||||
/// <param name="parameterSet">The parameter set for the backtest to run</param>
|
||||
/// <param name="backtestName">The backtest name to use</param>
|
||||
/// <returns>The new unique backtest id</returns>
|
||||
protected abstract string RunLean(ParameterSet parameterSet, string backtestName);
|
||||
|
||||
/// <summary>
|
||||
/// Get's a new backtest name
|
||||
/// </summary>
|
||||
protected virtual string GetBacktestName(ParameterSet parameterSet)
|
||||
{
|
||||
return "OptimizationBacktest";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a new backtest json result matching a requested backtest id
|
||||
/// </summary>
|
||||
/// <param name="jsonBacktestResult">The backtest json result</param>
|
||||
/// <param name="backtestId">The associated backtest id</param>
|
||||
protected virtual void NewResult(string jsonBacktestResult, string backtestId)
|
||||
{
|
||||
lock (RunningParameterSetForBacktest)
|
||||
{
|
||||
ParameterSet parameterSet;
|
||||
|
||||
// we take a lock so that there is no race condition with launching Lean adding the new backtest id and receiving the backtest result for that id
|
||||
// before it's even in the collection 'ParameterSetForBacktest'
|
||||
|
||||
if (!RunningParameterSetForBacktest.TryRemove(backtestId, out parameterSet))
|
||||
{
|
||||
Interlocked.Increment(ref _failedBacktest);
|
||||
Log.Error(
|
||||
$"LeanOptimizer.NewResult({GetLogDetails()}): Optimization compute job with id '{backtestId}' was not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// we got a new result if there are any pending parameterSet to run we can now trigger 1
|
||||
// we do this before 'Strategy.PushNewResults' so FIFO is respected
|
||||
if (PendingParameterSet.TryDequeue(out var pendingParameterSet))
|
||||
{
|
||||
LaunchLeanForParameterSet(pendingParameterSet);
|
||||
}
|
||||
|
||||
var result = new OptimizationResult(null, parameterSet, backtestId);
|
||||
if (string.IsNullOrEmpty(jsonBacktestResult))
|
||||
{
|
||||
Interlocked.Increment(ref _failedBacktest);
|
||||
Log.Error(
|
||||
$"LeanOptimizer.NewResult({GetLogDetails()}): Got null/empty backtest result for backtest id '{backtestId}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Increment(ref _completedBacktest);
|
||||
result = new OptimizationResult(jsonBacktestResult, parameterSet, backtestId);
|
||||
}
|
||||
|
||||
// Extract metrics now and drop the heavy JSON; null results (invalid parameters) are skipped.
|
||||
var metrics = OptimizationBacktestMetrics.ExtractFrom(backtestId, parameterSet, jsonBacktestResult);
|
||||
if (metrics != null)
|
||||
{
|
||||
// Backtest results can arrive on different threads; guard _completedBacktests with its own lock.
|
||||
lock (_completedBacktests)
|
||||
{
|
||||
_completedBacktests.Add(metrics);
|
||||
}
|
||||
}
|
||||
|
||||
// always notify the strategy
|
||||
Strategy.PushNewResults(result);
|
||||
|
||||
// strategy could of added more
|
||||
if (RunningParameterSetForBacktest.Count == 0)
|
||||
{
|
||||
TriggerOnEndEvent();
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of any resources
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_disposed = true;
|
||||
CleanUpRunningInstance();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current optimization status and strategy estimates
|
||||
/// </summary>
|
||||
public int GetCurrentEstimate()
|
||||
{
|
||||
return Strategy.GetTotalBacktestEstimate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current runtime statistics
|
||||
/// </summary>
|
||||
public Dictionary<string, string> GetRuntimeStatistics()
|
||||
{
|
||||
var completedCount = _completedBacktest;
|
||||
var totalEndedCount = completedCount + _failedBacktest;
|
||||
var runtime = DateTime.UtcNow - _startedAt;
|
||||
var result = new Dictionary<string, string>
|
||||
{
|
||||
{ "Completed", $"{completedCount}"},
|
||||
{ "Failed", $"{_failedBacktest}"},
|
||||
{ "Running", $"{RunningParameterSetForBacktest.Count}"},
|
||||
{ "In Queue", $"{PendingParameterSet.Count}"},
|
||||
{ "Average Length", $"{(totalEndedCount > 0 ? new TimeSpan(runtime.Ticks / totalEndedCount) : TimeSpan.Zero).ToString(@"hh\:mm\:ss", CultureInfo.InvariantCulture)}"},
|
||||
{ "Total Runtime", $"{runtime.ToString(@"hh\:mm\:ss", CultureInfo.InvariantCulture)}" }
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to have pretty more informative logs
|
||||
/// </summary>
|
||||
protected string GetLogDetails()
|
||||
{
|
||||
if (NodePacket.UserId == 0)
|
||||
{
|
||||
return $"OID {NodePacket.OptimizationId}";
|
||||
}
|
||||
return $"UI {NodePacket.UserId} PID {NodePacket.ProjectId} OID {NodePacket.OptimizationId} S {Status}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles stopping Lean process
|
||||
/// </summary>
|
||||
/// <param name="backtestId">Specified backtest id</param>
|
||||
protected abstract void AbortLean(string backtestId);
|
||||
|
||||
/// <summary>
|
||||
/// Sends an update of the current optimization status to the user
|
||||
/// </summary>
|
||||
protected abstract void SendUpdate();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the current optimization status
|
||||
/// </summary>
|
||||
/// <param name="optimizationStatus">The new optimization status</param>
|
||||
protected virtual void SetOptimizationStatus(OptimizationStatus optimizationStatus)
|
||||
{
|
||||
lock (_statusLock)
|
||||
{
|
||||
// we never come back from an aborted/ended status
|
||||
if (Status != OptimizationStatus.Aborted && Status != OptimizationStatus.Completed)
|
||||
{
|
||||
Status = optimizationStatus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any pending or running lean instance
|
||||
/// </summary>
|
||||
private void CleanUpRunningInstance()
|
||||
{
|
||||
PendingParameterSet.Clear();
|
||||
|
||||
lock (RunningParameterSetForBacktest)
|
||||
{
|
||||
foreach (var backtestId in RunningParameterSetForBacktest.Keys)
|
||||
{
|
||||
ParameterSet parameterSet;
|
||||
if (RunningParameterSetForBacktest.TryRemove(backtestId, out parameterSet))
|
||||
{
|
||||
Interlocked.Increment(ref _failedBacktest);
|
||||
try
|
||||
{
|
||||
AbortLean(backtestId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// pass
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will determine if it's right time to trigger an update call
|
||||
/// </summary>
|
||||
/// <param name="forceSend">True will force send, skipping interval, useful on start and end</param>
|
||||
private void ProcessUpdate(bool forceSend = false)
|
||||
{
|
||||
if (!forceSend && Status == OptimizationStatus.New)
|
||||
{
|
||||
// don't send any update until we finish the Start(), will be creating a bunch of backtests don't want to send partial/multiple updates
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (forceSend || (now - _lastUpdate > TimeSpan.FromSeconds(_optimizationUpdateInterval)))
|
||||
{
|
||||
_lastUpdate = now;
|
||||
Log.Debug($"LeanOptimizer.ProcessUpdate({GetLogDetails()}): start sending update...");
|
||||
|
||||
SendUpdate();
|
||||
|
||||
Log.Debug($"LeanOptimizer.ProcessUpdate({GetLogDetails()}): finished sending update successfully.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Failed to send status update");
|
||||
}
|
||||
}
|
||||
|
||||
private void LaunchLeanForParameterSet(ParameterSet parameterSet)
|
||||
{
|
||||
if (_disposed || Status == OptimizationStatus.Completed || Status == OptimizationStatus.Aborted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (RunningParameterSetForBacktest)
|
||||
{
|
||||
if (NodePacket.MaximumConcurrentBacktests != 0 && RunningParameterSetForBacktest.Count >= NodePacket.MaximumConcurrentBacktests)
|
||||
{
|
||||
// we hit the limit on the concurrent backtests
|
||||
PendingParameterSet.Enqueue(parameterSet);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var backtestName = GetBacktestName(parameterSet);
|
||||
var backtestId = RunLean(parameterSet, backtestName);
|
||||
|
||||
if (!string.IsNullOrEmpty(backtestId))
|
||||
{
|
||||
Log.Trace($"LeanOptimizer.LaunchLeanForParameterSet({GetLogDetails()}): launched backtest '{backtestId}' with parameters '{parameterSet}'");
|
||||
RunningParameterSetForBacktest.TryAdd(backtestId, parameterSet);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Increment(ref _failedBacktest);
|
||||
// always notify the strategy
|
||||
Strategy.PushNewResults(new OptimizationResult(null, parameterSet, backtestId));
|
||||
Log.Error($"LeanOptimizer.LaunchLeanForParameterSet({GetLogDetails()}): Initial/null optimization compute job could not be placed into the queue");
|
||||
}
|
||||
|
||||
ProcessUpdate();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"LeanOptimizer.LaunchLeanForParameterSet({GetLogDetails()}): Error encountered while placing optimization message into the queue: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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 Newtonsoft.Json;
|
||||
using QuantConnect.Optimizer.Objectives;
|
||||
using QuantConnect.Optimizer.Parameters;
|
||||
using QuantConnect.Optimizer.Strategies;
|
||||
using QuantConnect.Packets;
|
||||
using QuantConnect.Util;
|
||||
|
||||
namespace QuantConnect.Optimizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Provide a packet type containing information on the optimization compute job.
|
||||
/// </summary>
|
||||
public class OptimizationNodePacket : Packet
|
||||
{
|
||||
/// <summary>
|
||||
/// The optimization name
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The creation time
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.ISOShort, DateFormat.UI)]
|
||||
public DateTime Created { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User Id placing request
|
||||
/// </summary>
|
||||
public int UserId { get; set; }
|
||||
|
||||
/// User API Token
|
||||
public string UserToken { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Project Id of the request
|
||||
/// </summary>
|
||||
public int ProjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique compile id of this optimization
|
||||
/// </summary>
|
||||
public string CompileId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The unique optimization Id of the request
|
||||
/// </summary>
|
||||
public string OptimizationId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Organization Id of the request
|
||||
/// </summary>
|
||||
public string OrganizationId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Limit for the amount of concurrent backtests being run
|
||||
/// </summary>
|
||||
public int MaximumConcurrentBacktests { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optimization strategy name
|
||||
/// </summary>
|
||||
public string OptimizationStrategy { get; set; } = "QuantConnect.Optimizer.Strategies.GridSearchOptimizationStrategy";
|
||||
|
||||
/// <summary>
|
||||
/// Objective settings
|
||||
/// </summary>
|
||||
public Target Criterion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optimization constraints
|
||||
/// </summary>
|
||||
public IReadOnlyList<Constraint> Constraints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user optimization parameters
|
||||
/// </summary>
|
||||
public HashSet<OptimizationParameter> OptimizationParameters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user optimization parameters
|
||||
/// </summary>
|
||||
[JsonProperty(TypeNameHandling = TypeNameHandling.All)]
|
||||
public OptimizationStrategySettings OptimizationStrategySettings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Backtest out of sample maximum end date
|
||||
/// </summary>
|
||||
public DateTime? OutOfSampleMaxEndDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The backtest out of sample day count
|
||||
/// </summary>
|
||||
public int OutOfSampleDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance
|
||||
/// </summary>
|
||||
public OptimizationNodePacket() : this(PacketType.OptimizationNode)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance
|
||||
/// </summary>
|
||||
protected OptimizationNodePacket(PacketType packetType) : base(packetType)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
namespace QuantConnect.Optimizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the result of Lean compute job
|
||||
/// </summary>
|
||||
public class OptimizationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Corresponds to initial result to drive the optimization strategy
|
||||
/// </summary>
|
||||
public static readonly OptimizationResult Initial = new OptimizationResult(null, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// The backtest id that generated this result
|
||||
/// </summary>
|
||||
public string BacktestId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Parameter set Id
|
||||
/// </summary>
|
||||
public int Id => ParameterSet?.Id ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Json Backtest result
|
||||
/// </summary>
|
||||
public string JsonBacktestResult { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The parameter set at which the result was achieved
|
||||
/// </summary>
|
||||
public ParameterSet ParameterSet { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Aggregate diagnostic for the whole optimization; populated only on the final result fired via <see cref="LeanOptimizer.Ended"/>.
|
||||
/// </summary>
|
||||
public OptimizationAnalysis Analysis { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create an instance of <see cref="OptimizationResult"/>
|
||||
/// </summary>
|
||||
/// <param name="jsonBacktestResult">Optimization target value for this backtest</param>
|
||||
/// <param name="parameterSet">Parameter set used in compute job</param>
|
||||
/// <param name="backtestId">The backtest id that generated this result</param>
|
||||
public OptimizationResult(string jsonBacktestResult, ParameterSet parameterSet, string backtestId)
|
||||
{
|
||||
JsonBacktestResult = jsonBacktestResult;
|
||||
ParameterSet = parameterSet;
|
||||
BacktestId = backtestId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace QuantConnect.Optimizer.Parameters
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumerates all possible values for specific optimization parameter
|
||||
/// </summary>
|
||||
public abstract class OptimizationParameterEnumerator<T> : IEnumerator<string> where T: OptimizationParameter
|
||||
{
|
||||
/// <summary>
|
||||
/// The target optimization parameter to enumerate
|
||||
/// </summary>
|
||||
protected T OptimizationParameter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current enumeration state
|
||||
/// </summary>
|
||||
protected int Index { get; set; } = -1;
|
||||
|
||||
protected OptimizationParameterEnumerator(T optimizationParameter)
|
||||
{
|
||||
OptimizationParameter = optimizationParameter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the element in the collection at the current position of the enumerator.
|
||||
/// </summary>
|
||||
/// <returns>The element in the collection at the current position of the enumerator.</returns>
|
||||
public abstract string Current { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current element in the collection.
|
||||
/// </summary>
|
||||
/// <returns>The current element in the collection.</returns>
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose() { }
|
||||
|
||||
/// <summary>
|
||||
/// Advances the enumerator to the next element of the collection.
|
||||
/// </summary>
|
||||
/// <returns> true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection.</returns>
|
||||
/// <exception cref="T:System.InvalidOperationException">The collection was modified after the enumerator was created.</exception>
|
||||
public abstract bool MoveNext();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the enumerator to its initial position, which is before the first element in the collection.
|
||||
/// </summary>
|
||||
/// <exception cref="T:System.InvalidOperationException">The collection was modified after the enumerator was created.</exception>
|
||||
public void Reset()
|
||||
{
|
||||
Index = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
namespace QuantConnect.Optimizer.Parameters
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumerates all possible values for specific optimization parameter
|
||||
/// </summary>
|
||||
public class OptimizationStepParameterEnumerator : OptimizationParameterEnumerator<OptimizationStepParameter>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an instance of <see cref="OptimizationStepParameterEnumerator"/>
|
||||
/// </summary>
|
||||
/// <param name="optimizationParameter">Step-based optimization parameter</param>
|
||||
public OptimizationStepParameterEnumerator(OptimizationStepParameter optimizationParameter) : base(optimizationParameter)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the element in the collection at the current position of the enumerator.
|
||||
/// </summary>
|
||||
/// <returns>The element in the collection at the current position of the enumerator.</returns>
|
||||
public override string Current
|
||||
{
|
||||
get
|
||||
{
|
||||
var value = OptimizationParameter.MinValue + Index * OptimizationParameter.Step;
|
||||
if (Index == -1 || value > OptimizationParameter.MaxValue)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
return value.ToStringInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances the enumerator to the next element of the collection.
|
||||
/// </summary>
|
||||
/// <returns> true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection.</returns>
|
||||
/// <exception cref="T:System.InvalidOperationException">The collection was modified after the enumerator was created.</exception>
|
||||
public override bool MoveNext()
|
||||
{
|
||||
var value = OptimizationParameter.MinValue + (Index + 1) * OptimizationParameter.Step;
|
||||
if (value <= OptimizationParameter.MaxValue)
|
||||
{
|
||||
Index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("QuantConnect.Optimizer")]
|
||||
[assembly: AssemblyProduct("QuantConnect.Optimizer")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("4ecd7df3-a675-4e9e-aca3-2b51c88736ce")]
|
||||
@@ -0,0 +1,50 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<RootNamespace>QuantConnect.Optimizer</RootNamespace>
|
||||
<AssemblyName>QuantConnect.Optimizer</AssemblyName>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<Description>QuantConnect LEAN Engine: Optimizer Project - The Lean optimization engine</Description>
|
||||
<NoWarn>CA1062</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<StartupObject />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Features>flow-analysis</Features>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Common\Properties\SharedAssemblyInfo.cs" Link="Properties\SharedAssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Common\QuantConnect.csproj" />
|
||||
<ProjectReference Include="..\Configuration\QuantConnect.Configuration.csproj" />
|
||||
<ProjectReference Include="..\Logging\QuantConnect.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\LICENSE">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath></PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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;
|
||||
using QuantConnect.Optimizer.Objectives;
|
||||
using QuantConnect.Optimizer.Parameters;
|
||||
|
||||
namespace QuantConnect.Optimizer.Strategies
|
||||
{
|
||||
/// <summary>
|
||||
/// Advanced brute-force strategy with search in-depth for best solution on previous step
|
||||
/// </summary>
|
||||
public class EulerSearchOptimizationStrategy : StepBaseOptimizationStrategy
|
||||
{
|
||||
private object _locker = new object();
|
||||
private readonly HashSet<ParameterSet> _runningParameterSet = new HashSet<ParameterSet>();
|
||||
private int _segmentsAmount = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the strategy using generator, extremum settings and optimization parameters
|
||||
/// </summary>
|
||||
/// <param name="target">The optimization target</param>
|
||||
/// <param name="constraints">The optimization constraints to apply on backtest results</param>
|
||||
/// <param name="parameters">Optimization parameters</param>
|
||||
/// <param name="settings">Optimization strategy settings</param>
|
||||
public override void Initialize(Target target, IReadOnlyList<Constraint> constraints, HashSet<OptimizationParameter> parameters, OptimizationStrategySettings settings)
|
||||
{
|
||||
var stepSettings = settings as StepBaseOptimizationStrategySettings;
|
||||
if (stepSettings == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(settings),
|
||||
"EulerSearchOptimizationStrategy.Initialize: Optimizations Strategy settings are required for this strategy");
|
||||
}
|
||||
|
||||
if (stepSettings.DefaultSegmentAmount != 0)
|
||||
{
|
||||
_segmentsAmount = stepSettings.DefaultSegmentAmount;
|
||||
}
|
||||
|
||||
base.Initialize(target, constraints, parameters, settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether new lean compute job better than previous and run new iteration if necessary.
|
||||
/// </summary>
|
||||
/// <param name="result">Lean compute job result and corresponding parameter set</param>
|
||||
public override void PushNewResults(OptimizationResult result)
|
||||
{
|
||||
if (!Initialized)
|
||||
{
|
||||
throw new InvalidOperationException($"EulerSearchOptimizationStrategy.PushNewResults: strategy has not been initialized yet.");
|
||||
}
|
||||
|
||||
lock (_locker)
|
||||
{
|
||||
if (!ReferenceEquals(result, OptimizationResult.Initial) && string.IsNullOrEmpty(result?.JsonBacktestResult))
|
||||
{
|
||||
// one of the requested backtests failed
|
||||
_runningParameterSet.Remove(result.ParameterSet);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the incoming result is not the initial seed
|
||||
if (result.Id > 0)
|
||||
{
|
||||
_runningParameterSet.Remove(result.ParameterSet);
|
||||
ProcessNewResult(result);
|
||||
}
|
||||
|
||||
if (_runningParameterSet.Count > 0)
|
||||
{
|
||||
// we wait till all backtest end during each euler step
|
||||
return;
|
||||
}
|
||||
|
||||
// Once all running backtests have ended, for the current collection of optimization parameters, for each parameter we determine if
|
||||
// we can create a new smaller/finer optimization scope
|
||||
if (Target.Current.HasValue && OptimizationParameters.OfType<OptimizationStepParameter>().Any(s => s.Step > s.MinStep))
|
||||
{
|
||||
var boundaries = new HashSet<OptimizationParameter>();
|
||||
var parameterSet = Solution.ParameterSet;
|
||||
foreach (var optimizationParameter in OptimizationParameters)
|
||||
{
|
||||
var optimizationStepParameter = optimizationParameter as OptimizationStepParameter;
|
||||
if (optimizationStepParameter != null && optimizationStepParameter.Step > optimizationStepParameter.MinStep)
|
||||
{
|
||||
var newStep = Math.Max(optimizationStepParameter.MinStep.Value, optimizationStepParameter.Step.Value / _segmentsAmount);
|
||||
var fractal = newStep * ((decimal)_segmentsAmount / 2);
|
||||
var parameter = parameterSet.Value.First(s => s.Key == optimizationParameter.Name);
|
||||
boundaries.Add(new OptimizationStepParameter(
|
||||
optimizationParameter.Name,
|
||||
Math.Max(optimizationStepParameter.MinValue, parameter.Value.ToDecimal() - fractal),
|
||||
Math.Min(optimizationStepParameter.MaxValue, parameter.Value.ToDecimal() + fractal),
|
||||
newStep,
|
||||
optimizationStepParameter.MinStep.Value));
|
||||
}
|
||||
else
|
||||
{
|
||||
boundaries.Add(optimizationParameter);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var staticParam in OptimizationParameters.OfType<StaticOptimizationParameter>())
|
||||
{
|
||||
boundaries.Add(staticParam);
|
||||
}
|
||||
OptimizationParameters = boundaries;
|
||||
}
|
||||
else if (!ReferenceEquals(result, OptimizationResult.Initial))
|
||||
{
|
||||
// we ended!
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var parameterSet in Step(OptimizationParameters))
|
||||
{
|
||||
OnNewParameterSet(parameterSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles new parameter set
|
||||
/// </summary>
|
||||
/// <param name="parameterSet">New parameter set</param>
|
||||
protected override void OnNewParameterSet(ParameterSet parameterSet)
|
||||
{
|
||||
_runningParameterSet.Add(parameterSet);
|
||||
base.OnNewParameterSet(parameterSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
namespace QuantConnect.Optimizer.Strategies
|
||||
{
|
||||
/// <summary>
|
||||
/// Find the best solution in first generation
|
||||
/// </summary>
|
||||
public class GridSearchOptimizationStrategy : StepBaseOptimizationStrategy
|
||||
{
|
||||
private object _locker = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether new lean compute job better than previous and run new iteration if necessary.
|
||||
/// </summary>
|
||||
/// <param name="result">Lean compute job result and corresponding parameter set</param>
|
||||
public override void PushNewResults(OptimizationResult result)
|
||||
{
|
||||
if (!Initialized)
|
||||
{
|
||||
throw new InvalidOperationException($"GridSearchOptimizationStrategy.PushNewResults: strategy has not been initialized yet.");
|
||||
}
|
||||
|
||||
lock (_locker)
|
||||
{
|
||||
if (!ReferenceEquals(result, OptimizationResult.Initial) && string.IsNullOrEmpty(result?.JsonBacktestResult))
|
||||
{
|
||||
// one of the requested backtests failed
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the incoming result is not the initial seed
|
||||
if (result.Id > 0)
|
||||
{
|
||||
ProcessNewResult(result);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var parameterSet in Step(OptimizationParameters))
|
||||
{
|
||||
OnNewParameterSet(parameterSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 QuantConnect.Optimizer.Objectives;
|
||||
using QuantConnect.Optimizer.Parameters;
|
||||
|
||||
namespace QuantConnect.Optimizer.Strategies
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the optimization settings, direction, solution and exit, i.e. optimization strategy
|
||||
/// </summary>
|
||||
public interface IOptimizationStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Fires when new parameter set is retrieved
|
||||
/// </summary>
|
||||
event EventHandler<ParameterSet> NewParameterSet;
|
||||
|
||||
/// <summary>
|
||||
/// Best found solution, its value and parameter set
|
||||
/// </summary>
|
||||
OptimizationResult Solution { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the strategy using generator, extremum settings and optimization parameters
|
||||
/// </summary>
|
||||
/// <param name="target">The optimization target</param>
|
||||
/// <param name="constraints">The optimization constraints to apply on backtest results</param>
|
||||
/// <param name="parameters">optimization parameters</param>
|
||||
/// <param name="settings">optimization strategy advanced settings</param>
|
||||
void Initialize(Target target, IReadOnlyList<Constraint> constraints, HashSet<OptimizationParameter> parameters, OptimizationStrategySettings settings);
|
||||
|
||||
/// <summary>
|
||||
/// Callback when lean compute job completed.
|
||||
/// </summary>
|
||||
/// <param name="result">Lean compute job result and corresponding parameter set</param>
|
||||
void PushNewResults(OptimizationResult result);
|
||||
|
||||
/// <summary>
|
||||
/// Estimates amount of parameter sets that can be run
|
||||
/// </summary>
|
||||
int GetTotalBacktestEstimate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
namespace QuantConnect.Optimizer.Strategies
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the specific optimization strategy settings
|
||||
/// </summary>
|
||||
public class OptimizationStrategySettings
|
||||
{
|
||||
/// <summary>
|
||||
/// TODO: implement
|
||||
/// </summary>
|
||||
public TimeSpan MaxRuntime { get; set; } = TimeSpan.MaxValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* 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.Globalization;
|
||||
using System.Linq;
|
||||
using QuantConnect.Optimizer.Objectives;
|
||||
using QuantConnect.Optimizer.Parameters;
|
||||
|
||||
namespace QuantConnect.Optimizer.Strategies
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for any optimization built on top of brute force optimization method
|
||||
/// </summary>
|
||||
public abstract class StepBaseOptimizationStrategy : IOptimizationStrategy
|
||||
{
|
||||
private int _i;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates was strategy initialized or no
|
||||
/// </summary>
|
||||
protected bool Initialized { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optimization parameters
|
||||
/// </summary>
|
||||
protected HashSet<OptimizationParameter> OptimizationParameters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optimization target, i.e. maximize or minimize
|
||||
/// </summary>
|
||||
protected Target Target { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optimization constraints; if it doesn't comply just drop the backtest
|
||||
/// </summary>
|
||||
protected IEnumerable<Constraint> Constraints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Keep the best found solution - lean computed job result and corresponding parameter set
|
||||
/// </summary>
|
||||
public OptimizationResult Solution { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Advanced strategy settings
|
||||
/// </summary>
|
||||
public OptimizationStrategySettings Settings { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Fires when new parameter set is generated
|
||||
/// </summary>
|
||||
public event EventHandler<ParameterSet> NewParameterSet;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the strategy using generator, extremum settings and optimization parameters
|
||||
/// </summary>
|
||||
/// <param name="target">The optimization target</param>
|
||||
/// <param name="constraints">The optimization constraints to apply on backtest results</param>
|
||||
/// <param name="parameters">Optimization parameters</param>
|
||||
/// <param name="settings">Optimization strategy settings</param>
|
||||
public virtual void Initialize(Target target, IReadOnlyList<Constraint> constraints, HashSet<OptimizationParameter> parameters, OptimizationStrategySettings settings)
|
||||
{
|
||||
if (Initialized)
|
||||
{
|
||||
throw new InvalidOperationException($"GridSearchOptimizationStrategy.Initialize: can not be re-initialized.");
|
||||
}
|
||||
|
||||
Target = target;
|
||||
Constraints = constraints;
|
||||
OptimizationParameters = parameters;
|
||||
Settings = settings;
|
||||
|
||||
foreach (var optimizationParameter in OptimizationParameters.OfType<OptimizationStepParameter>())
|
||||
{
|
||||
// if the Step optimization parameter does not provide a step to use, we calculate one based on settings
|
||||
if (!optimizationParameter.Step.HasValue)
|
||||
{
|
||||
var stepSettings = Settings as StepBaseOptimizationStrategySettings;
|
||||
if (stepSettings == null)
|
||||
{
|
||||
throw new ArgumentException($"OptimizationStrategySettings is not of {nameof(StepBaseOptimizationStrategySettings)} type", nameof(settings));
|
||||
}
|
||||
CalculateStep(optimizationParameter, stepSettings.DefaultSegmentAmount);
|
||||
}
|
||||
}
|
||||
|
||||
Initialized = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether new lean compute job better than previous and run new iteration if necessary.
|
||||
/// </summary>
|
||||
/// <param name="result">Lean compute job result and corresponding parameter set</param>
|
||||
public abstract void PushNewResults(OptimizationResult result);
|
||||
|
||||
/// <summary>
|
||||
/// Calculate number of parameter sets within grid
|
||||
/// </summary>
|
||||
/// <returns>Number of parameter sets for given optimization parameters</returns>
|
||||
public int GetTotalBacktestEstimate()
|
||||
{
|
||||
var total = 1;
|
||||
foreach (var arg in OptimizationParameters)
|
||||
{
|
||||
total *= Estimate(arg);
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates number od data points for step based optimization parameter based on min/max and step values
|
||||
/// </summary>
|
||||
private int Estimate(OptimizationParameter parameter)
|
||||
{
|
||||
if (parameter is StaticOptimizationParameter)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var stepParameter = parameter as OptimizationStepParameter;
|
||||
if (stepParameter == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot estimate parameter of type {parameter.GetType().FullName}");
|
||||
}
|
||||
|
||||
if (!stepParameter.Step.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException("Optimization parameter cannot be estimated due to step value is not initialized");
|
||||
}
|
||||
|
||||
return (int)Math.Floor((stepParameter.MaxValue - stepParameter.MinValue) / stepParameter.Step.Value) + 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles new parameter set
|
||||
/// </summary>
|
||||
/// <param name="parameterSet">New parameter set</param>
|
||||
protected virtual void OnNewParameterSet(ParameterSet parameterSet)
|
||||
{
|
||||
NewParameterSet?.Invoke(this, parameterSet);
|
||||
}
|
||||
|
||||
protected virtual void ProcessNewResult(OptimizationResult result)
|
||||
{
|
||||
// check if the incoming result is not the initial seed
|
||||
if (result.Id > 0)
|
||||
{
|
||||
if (Constraints?.All(constraint => constraint.IsMet(result.JsonBacktestResult)) != false)
|
||||
{
|
||||
if (Target.MoveAhead(result.JsonBacktestResult))
|
||||
{
|
||||
Solution = result;
|
||||
Target.CheckCompliance();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerate all possible arrangements
|
||||
/// </summary>
|
||||
/// <param name="args"></param>
|
||||
/// <returns>Collection of possible combinations for given optimization parameters settings</returns>
|
||||
protected IEnumerable<ParameterSet> Step(HashSet<OptimizationParameter> args)
|
||||
{
|
||||
foreach (var step in Recursive(new Queue<OptimizationParameter>(args)))
|
||||
{
|
||||
yield return new ParameterSet(
|
||||
++_i,
|
||||
step.ToDictionary(kvp => kvp.Key, kvp => kvp.Value));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate step and min step values based on default number of fragments
|
||||
/// </summary>
|
||||
private void CalculateStep(OptimizationStepParameter parameter, int defaultSegmentAmount)
|
||||
{
|
||||
if (defaultSegmentAmount < 1)
|
||||
{
|
||||
throw new ArgumentException($"Number of segments should be positive number, but specified '{defaultSegmentAmount}'", nameof(defaultSegmentAmount));
|
||||
}
|
||||
|
||||
parameter.Step = Math.Abs(parameter.MaxValue - parameter.MinValue) / defaultSegmentAmount;
|
||||
parameter.MinStep = parameter.Step / 10;
|
||||
}
|
||||
|
||||
private IEnumerable<Dictionary<string, string>> Recursive(Queue<OptimizationParameter> args)
|
||||
{
|
||||
if (args.Count == 1)
|
||||
{
|
||||
var optimizationParameterLast = args.Dequeue();
|
||||
using (var optimizationParameterLastEnumerator = GetEnumerator(optimizationParameterLast))
|
||||
{
|
||||
while (optimizationParameterLastEnumerator.MoveNext())
|
||||
{
|
||||
yield return new Dictionary<string, string>()
|
||||
{
|
||||
{optimizationParameterLast.Name, optimizationParameterLastEnumerator.Current}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
var optimizationParameter = args.Dequeue();
|
||||
using (var optimizationParameterEnumerator = GetEnumerator(optimizationParameter))
|
||||
{
|
||||
while (optimizationParameterEnumerator.MoveNext())
|
||||
{
|
||||
foreach (var inner in Recursive(new Queue<OptimizationParameter>(args)))
|
||||
{
|
||||
inner.Add(optimizationParameter.Name, optimizationParameterEnumerator.Current);
|
||||
|
||||
yield return inner;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator<string> GetEnumerator(OptimizationParameter parameter)
|
||||
{
|
||||
var staticOptimizationParameter = parameter as StaticOptimizationParameter;
|
||||
if (staticOptimizationParameter != null)
|
||||
{
|
||||
return new List<string> { staticOptimizationParameter.Value }.GetEnumerator();
|
||||
}
|
||||
|
||||
var stepParameter = parameter as OptimizationStepParameter;
|
||||
if (stepParameter == null)
|
||||
{
|
||||
throw new InvalidOperationException("");
|
||||
}
|
||||
|
||||
return new OptimizationStepParameterEnumerator(stepParameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
namespace QuantConnect.Optimizer.Strategies
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the specific optimization strategy settings
|
||||
/// </summary>
|
||||
public class StepBaseOptimizationStrategySettings : OptimizationStrategySettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the default number of segments for the next step
|
||||
/// </summary>
|
||||
[JsonProperty("default-segment-amount")]
|
||||
public int DefaultSegmentAmount { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user