From c5d2d0319d04f56f3718e9e5f643c4a05b912054 Mon Sep 17 00:00:00 2001 From: ScarabSystems Date: Mon, 13 Jul 2026 03:25:42 -0400 Subject: [PATCH 1/3] Add scheduled walk-forward optimization API contract Adds QCAlgorithm Optimize overloads backed by a pluggable walk-forward optimization provider contract in Common, including request/result DTOs and Python wrapper forwarding. Verification: dotnet build Tests/QuantConnect.Tests.csproj /p:Configuration=Debug /v:quiet in the SDS Lean workspace; PYTHONNET_PYDLL=/usr/local/lib/libpython3.13.so dotnet test Tests/bin/Debug/QuantConnect.Tests.dll --filter FullyQualifiedName~AlgorithmWalkForwardOptimizationTests. --- Algorithm/QCAlgorithm.cs | 115 ++++++++++++ .../Python/Wrappers/AlgorithmPythonWrapper.cs | 6 + Common/AlgorithmImports.py | 3 + Common/Interfaces/IAlgorithm.cs | 6 + .../IWalkForwardOptimizationProvider.cs | 32 ++++ .../NullWalkForwardOptimizationProvider.cs | 42 +++++ .../WalkForwardOptimizationRequest.cs | 102 ++++++++++ .../WalkForwardOptimizationResult.cs | 70 +++++++ .../AlgorithmWalkForwardOptimizationTests.cs | 175 ++++++++++++++++++ 9 files changed, 551 insertions(+) create mode 100644 Common/Interfaces/IWalkForwardOptimizationProvider.cs create mode 100644 Common/Optimizer/NullWalkForwardOptimizationProvider.cs create mode 100644 Common/Optimizer/WalkForwardOptimizationRequest.cs create mode 100644 Common/Optimizer/WalkForwardOptimizationResult.cs create mode 100644 Tests/Algorithm/AlgorithmWalkForwardOptimizationTests.cs diff --git a/Algorithm/QCAlgorithm.cs b/Algorithm/QCAlgorithm.cs index daf195a7a1e1..9ed103cfb742 100644 --- a/Algorithm/QCAlgorithm.cs +++ b/Algorithm/QCAlgorithm.cs @@ -58,6 +58,9 @@ using Newtonsoft.Json; using QuantConnect.Securities.Index; using QuantConnect.Api; +using QuantConnect.Optimizer; +using QuantConnect.Optimizer.Objectives; +using QuantConnect.Optimizer.Parameters; using Common.Util; namespace QuantConnect.Algorithm @@ -165,6 +168,7 @@ private SecurityDefinitionSymbolResolver SecurityDefinitionSymbolResolver private readonly HistoryRequestFactory _historyRequestFactory; private IApi _api; + private IWalkForwardOptimizationProvider _walkForwardOptimizationProvider = NullWalkForwardOptimizationProvider.Instance; /// /// QCAlgorithm Base Class Constructor - Initialize the underlying QCAlgorithm components. @@ -3136,6 +3140,107 @@ public ScheduledEvent Train(IDateRule dateRule, ITimeRule timeRule, Action train return Schedule.Training(dateRule, timeRule, trainingCode); } + /// + /// Schedules a walk-forward optimization using the specified date and time rules. + /// + /// Specifies what dates the optimization should run + /// Specifies the times on those dates the optimization should run + /// The optimization target used to select the winning parameter set + /// The optimization parameters to evaluate + /// The optional constraints to apply to optimization results + [DocumentationAttribute(ParameterAndOptimization)] + [DocumentationAttribute(ScheduledEvents)] + public ScheduledEvent Optimize( + IDateRule dateRule, + ITimeRule timeRule, + Target target, + HashSet parameters, + IReadOnlyList constraints = null) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + return ScheduleWalkForwardOptimization(dateRule, timeRule, triggerTime => + new WalkForwardOptimizationRequest(this, triggerTime, target, parameters, constraints)); + } + + /// + /// Schedules a walk-forward optimization using the specified date and time rules. + /// + /// Specifies what dates the optimization should run + /// Specifies the times on those dates the optimization should run + /// Selects the winning parameter set from optimization backtests + /// The optimization parameters to evaluate + /// The optional constraints to apply to optimization results + [DocumentationAttribute(ParameterAndOptimization)] + [DocumentationAttribute(ScheduledEvents)] + public ScheduledEvent Optimize( + IDateRule dateRule, + ITimeRule timeRule, + Func, ParameterSet> targetSelector, + HashSet parameters, + IReadOnlyList constraints = null) + { + if (targetSelector == null) + { + throw new ArgumentNullException(nameof(targetSelector)); + } + + return ScheduleWalkForwardOptimization(dateRule, timeRule, triggerTime => + new WalkForwardOptimizationRequest(this, triggerTime, targetSelector, parameters, constraints)); + } + + private ScheduledEvent ScheduleWalkForwardOptimization( + IDateRule dateRule, + ITimeRule timeRule, + Func requestFactory) + { + if (dateRule == null) + { + throw new ArgumentNullException(nameof(dateRule)); + } + if (timeRule == null) + { + throw new ArgumentNullException(nameof(timeRule)); + } + if (requestFactory == null) + { + throw new ArgumentNullException(nameof(requestFactory)); + } + + var name = $"Optimization: {dateRule.Name}: {timeRule.Name}"; + return Schedule.On(name, dateRule, timeRule, (eventName, triggerTime) => + { + RunWalkForwardOptimization(requestFactory(triggerTime)); + }); + } + + private void RunWalkForwardOptimization(WalkForwardOptimizationRequest request) + { + if (AlgorithmMode == AlgorithmMode.Optimization) + { + Debug("Skipping walk-forward optimization because this algorithm is already running as an optimization child."); + return; + } + + var result = _walkForwardOptimizationProvider.Optimize(request); + var parameterSet = result?.ParameterSet; + if (parameterSet == null && request.TargetSelector != null) + { + parameterSet = request.TargetSelector(result?.Backtests ?? Array.Empty()); + } + + if (parameterSet?.Value == null) + { + Debug("Walk-forward optimization completed without a selected parameter set."); + return; + } + + SetParameters(parameterSet.Value.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); + } + /// /// Event invocator for the event /// @@ -3175,6 +3280,16 @@ public void SetApi(IApi api) _api = api; } + /// + /// Sets the walk-forward optimization provider. + /// + /// The provider used to run walk-forward optimizations + [DocumentationAttribute(ParameterAndOptimization)] + public void SetWalkForwardOptimizationProvider(IWalkForwardOptimizationProvider provider) + { + _walkForwardOptimizationProvider = provider ?? throw new ArgumentNullException(nameof(provider)); + } + /// /// Sets the object store /// diff --git a/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs b/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs index 642f1a1918d0..3e69722262c0 100644 --- a/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs +++ b/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs @@ -1129,6 +1129,12 @@ public void SetFinishedWarmingUp() /// Historical data provider public void SetHistoryProvider(IHistoryProvider historyProvider) => _baseAlgorithm.SetHistoryProvider(historyProvider); + /// + /// Sets the walk-forward optimization provider. + /// + /// Walk-forward optimization provider + public void SetWalkForwardOptimizationProvider(IWalkForwardOptimizationProvider provider) => _baseAlgorithm.SetWalkForwardOptimizationProvider(provider); + /// /// Set live mode state of the algorithm run: Public setter for the algorithm property LiveMode. /// diff --git a/Common/AlgorithmImports.py b/Common/AlgorithmImports.py index 35cda517a1ca..400516362741 100644 --- a/Common/AlgorithmImports.py +++ b/Common/AlgorithmImports.py @@ -41,6 +41,7 @@ from QuantConnect.Storage import * from QuantConnect.Research import * from QuantConnect.Commands import * +from QuantConnect.Optimizer import * from QuantConnect.Algorithm import * from QuantConnect.Statistics import * from QuantConnect.Parameters import * @@ -54,6 +55,7 @@ from QuantConnect.Orders.Fees import * from QuantConnect.Data.Custom import * from QuantConnect.Data.Market import * +from QuantConnect.Optimizer.Objectives import * from QuantConnect.Lean.Engine import * from QuantConnect.Orders.Fills import * from QuantConnect.Configuration import * @@ -70,6 +72,7 @@ from QuantConnect.Data.Consolidators import * from QuantConnect.Orders.TimeInForces import * from QuantConnect.Algorithm.Framework import * +from QuantConnect.Optimizer.Parameters import * from QuantConnect.Algorithm.Selection import * from QuantConnect.Securities.Positions import * from QuantConnect.Orders.OptionExercise import * diff --git a/Common/Interfaces/IAlgorithm.cs b/Common/Interfaces/IAlgorithm.cs index a4221bd440e5..9c4ecdfa4bf8 100644 --- a/Common/Interfaces/IAlgorithm.cs +++ b/Common/Interfaces/IAlgorithm.cs @@ -900,6 +900,12 @@ Security AddSecurity(Symbol symbol, Resolution? resolution = null, bool? fillFor /// Initiated API void SetApi(IApi api); + /// + /// Sets the walk-forward optimization provider. + /// + /// The provider used to run walk-forward optimizations + void SetWalkForwardOptimizationProvider(IWalkForwardOptimizationProvider provider); + /// /// Sets the object store /// diff --git a/Common/Interfaces/IWalkForwardOptimizationProvider.cs b/Common/Interfaces/IWalkForwardOptimizationProvider.cs new file mode 100644 index 000000000000..849e2c07cecc --- /dev/null +++ b/Common/Interfaces/IWalkForwardOptimizationProvider.cs @@ -0,0 +1,32 @@ +/* + * 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; + +namespace QuantConnect.Interfaces +{ + /// + /// Provides walk-forward optimization results for a running algorithm. + /// + public interface IWalkForwardOptimizationProvider + { + /// + /// Runs an optimization for the specified request. + /// + /// The walk-forward optimization request + /// The optimization result + WalkForwardOptimizationResult Optimize(WalkForwardOptimizationRequest request); + } +} diff --git a/Common/Optimizer/NullWalkForwardOptimizationProvider.cs b/Common/Optimizer/NullWalkForwardOptimizationProvider.cs new file mode 100644 index 000000000000..598461a05e10 --- /dev/null +++ b/Common/Optimizer/NullWalkForwardOptimizationProvider.cs @@ -0,0 +1,42 @@ +/* + * 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.Interfaces; + +namespace QuantConnect.Optimizer +{ + /// + /// Default walk-forward optimization provider used when no runtime provider is configured. + /// + public sealed class NullWalkForwardOptimizationProvider : IWalkForwardOptimizationProvider + { + /// + /// Gets the singleton instance. + /// + public static NullWalkForwardOptimizationProvider Instance { get; } = new NullWalkForwardOptimizationProvider(); + + private NullWalkForwardOptimizationProvider() + { + } + + /// + /// Returns an empty result. + /// + public WalkForwardOptimizationResult Optimize(WalkForwardOptimizationRequest request) + { + return WalkForwardOptimizationResult.Empty; + } + } +} diff --git a/Common/Optimizer/WalkForwardOptimizationRequest.cs b/Common/Optimizer/WalkForwardOptimizationRequest.cs new file mode 100644 index 000000000000..470b77101f1d --- /dev/null +++ b/Common/Optimizer/WalkForwardOptimizationRequest.cs @@ -0,0 +1,102 @@ +/* + * 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.Api; +using QuantConnect.Interfaces; +using QuantConnect.Optimizer.Objectives; +using QuantConnect.Optimizer.Parameters; + +namespace QuantConnect.Optimizer +{ + /// + /// Defines a scheduled walk-forward optimization request. + /// + public class WalkForwardOptimizationRequest + { + /// + /// The parent algorithm requesting the optimization. + /// + public IAlgorithm Algorithm { get; } + + /// + /// The UTC time that triggered the scheduled optimization. + /// + public DateTime TriggerTimeUtc { get; } + + /// + /// The optimization target used by provider-selected optimizations. + /// + public Target Target { get; } + + /// + /// The optional selector used to choose a winning parameter set from optimization backtests. + /// + public Func, ParameterSet> TargetSelector { get; } + + /// + /// The optimization parameters to evaluate. + /// + public HashSet Parameters { get; } + + /// + /// The constraints to apply to optimization results. + /// + public IReadOnlyList Constraints { get; } + + /// + /// Creates a new instance for provider-selected target optimization. + /// + public WalkForwardOptimizationRequest( + IAlgorithm algorithm, + DateTime triggerTimeUtc, + Target target, + HashSet parameters, + IReadOnlyList constraints = null) + : this(algorithm, triggerTimeUtc, target, null, parameters, constraints) + { + } + + /// + /// Creates a new instance for selector-based optimization. + /// + public WalkForwardOptimizationRequest( + IAlgorithm algorithm, + DateTime triggerTimeUtc, + Func, ParameterSet> targetSelector, + HashSet parameters, + IReadOnlyList constraints = null) + : this(algorithm, triggerTimeUtc, null, targetSelector, parameters, constraints) + { + } + + private WalkForwardOptimizationRequest( + IAlgorithm algorithm, + DateTime triggerTimeUtc, + Target target, + Func, ParameterSet> targetSelector, + HashSet parameters, + IReadOnlyList constraints) + { + Algorithm = algorithm ?? throw new ArgumentNullException(nameof(algorithm)); + TriggerTimeUtc = triggerTimeUtc; + Target = target; + TargetSelector = targetSelector; + Parameters = parameters ?? throw new ArgumentNullException(nameof(parameters)); + Constraints = constraints ?? Array.Empty(); + } + } +} diff --git a/Common/Optimizer/WalkForwardOptimizationResult.cs b/Common/Optimizer/WalkForwardOptimizationResult.cs new file mode 100644 index 000000000000..482640e43a86 --- /dev/null +++ b/Common/Optimizer/WalkForwardOptimizationResult.cs @@ -0,0 +1,70 @@ +/* + * 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.Api; +using QuantConnect.Optimizer.Parameters; + +namespace QuantConnect.Optimizer +{ + /// + /// Defines the result of a walk-forward optimization request. + /// + public class WalkForwardOptimizationResult + { + /// + /// Empty result used when no optimization was run. + /// + public static WalkForwardOptimizationResult Empty { get; } = new WalkForwardOptimizationResult( + null, + Array.Empty()); + + /// + /// The selected parameter set. + /// + public ParameterSet ParameterSet { get; } + + /// + /// The optimization backtests produced by the provider. + /// + public IReadOnlyList Backtests { get; } + + /// + /// Creates a new result with the selected parameter set. + /// + public WalkForwardOptimizationResult(ParameterSet parameterSet) + : this(parameterSet, Array.Empty()) + { + } + + /// + /// Creates a new result with candidate backtests for selector-based optimization. + /// + public WalkForwardOptimizationResult(IReadOnlyList backtests) + : this(null, backtests) + { + } + + /// + /// Creates a new result with a selected parameter set and candidate backtests. + /// + public WalkForwardOptimizationResult(ParameterSet parameterSet, IReadOnlyList backtests) + { + ParameterSet = parameterSet; + Backtests = backtests ?? Array.Empty(); + } + } +} diff --git a/Tests/Algorithm/AlgorithmWalkForwardOptimizationTests.cs b/Tests/Algorithm/AlgorithmWalkForwardOptimizationTests.cs new file mode 100644 index 000000000000..7f350bb25e75 --- /dev/null +++ b/Tests/Algorithm/AlgorithmWalkForwardOptimizationTests.cs @@ -0,0 +1,175 @@ +/* + * 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.Reflection; +using NUnit.Framework; +using QuantConnect.Api; +using QuantConnect.Algorithm; +using QuantConnect.Interfaces; +using QuantConnect.Lean.Engine; +using QuantConnect.Lean.Engine.RealTime; +using QuantConnect.Optimizer; +using QuantConnect.Optimizer.Objectives; +using QuantConnect.Optimizer.Parameters; +using QuantConnect.Packets; +using QuantConnect.Util.RateLimit; + +namespace QuantConnect.Tests.Algorithm +{ + [TestFixture] + public class AlgorithmWalkForwardOptimizationTests + { + [Test] + public void ScheduledOptimizationAppliesWinningParameterSet() + { + var algorithm = CreateAlgorithm(out var realTimeHandler); + var provider = new FakeWalkForwardOptimizationProvider + { + Result = new WalkForwardOptimizationResult(new ParameterSet(1, new Dictionary + { + { "ema-fast", "20" }, + { "ema-slow", "200" } + })) + }; + algorithm.SetWalkForwardOptimizationProvider(provider); + algorithm.SetParameters(new Dictionary + { + { "ema-fast", "10" }, + { "ema-slow", "100" } + }); + + algorithm.Optimize( + algorithm.DateRules.Today, + algorithm.TimeRules.Now, + new Target("Profit", new Maximization(), null), + new HashSet + { + new OptimizationStepParameter("ema-fast", 10, 20, 10), + new OptimizationStepParameter("ema-slow", 100, 200, 100) + }); + + realTimeHandler.SetTime(algorithm.UtcTime); + + Assert.AreEqual("20", algorithm.GetParameter("ema-fast")); + Assert.AreEqual("200", algorithm.GetParameter("ema-slow")); + Assert.AreEqual(1, provider.Requests.Count); + Assert.AreEqual(algorithm, provider.Requests[0].Algorithm); + Assert.AreEqual(2, provider.Requests[0].Parameters.Count); + Assert.IsNotNull(provider.Requests[0].Target); + } + + [Test] + public void SelectorOptimizationAppliesSelectedBacktestParameterSet() + { + var algorithm = CreateAlgorithm(out var realTimeHandler); + var expectedParameterSet = new ParameterSet(2, new Dictionary + { + { "lookback", "40" } + }); + var provider = new FakeWalkForwardOptimizationProvider + { + Result = new WalkForwardOptimizationResult(new[] + { + new OptimizationBacktest(new ParameterSet(1, new Dictionary + { + { "lookback", "20" } + }), "backtest-1", "candidate-1"), + new OptimizationBacktest(expectedParameterSet, "backtest-2", "candidate-2") + }) + }; + algorithm.SetWalkForwardOptimizationProvider(provider); + algorithm.SetParameters(new Dictionary + { + { "lookback", "20" } + }); + + algorithm.Optimize( + algorithm.DateRules.Today, + algorithm.TimeRules.Now, + backtests => expectedParameterSet, + new HashSet + { + new OptimizationStepParameter("lookback", 20, 40, 20) + }); + + realTimeHandler.SetTime(algorithm.UtcTime); + + Assert.AreEqual("40", algorithm.GetParameter("lookback")); + Assert.AreEqual(1, provider.Requests.Count); + Assert.IsNull(provider.Requests[0].Target); + Assert.IsNotNull(provider.Requests[0].TargetSelector); + } + + [Test] + public void OptimizationChildAlgorithmDoesNotStartNestedOptimization() + { + var algorithm = CreateAlgorithm(out var realTimeHandler); + var provider = new FakeWalkForwardOptimizationProvider + { + Result = new WalkForwardOptimizationResult(new ParameterSet(1, new Dictionary + { + { "ema-fast", "20" } + })) + }; + algorithm.SetWalkForwardOptimizationProvider(provider); + algorithm.SetParameters(new Dictionary + { + { "ema-fast", "10" } + }); + typeof(QCAlgorithm) + .GetField("_algorithmMode", BindingFlags.NonPublic | BindingFlags.Instance) + .SetValue(algorithm, AlgorithmMode.Optimization); + + algorithm.Optimize( + algorithm.DateRules.Today, + algorithm.TimeRules.Now, + new Target("Profit", new Maximization(), null), + new HashSet + { + new OptimizationStepParameter("ema-fast", 10, 20, 10) + }); + + realTimeHandler.SetTime(algorithm.UtcTime); + + Assert.AreEqual("10", algorithm.GetParameter("ema-fast")); + Assert.AreEqual(0, provider.Requests.Count); + } + + private static QCAlgorithm CreateAlgorithm(out BacktestingRealTimeHandler realTimeHandler) + { + var algorithm = new QCAlgorithm(); + realTimeHandler = new BacktestingRealTimeHandler(); + var timeLimitManager = new AlgorithmTimeLimitManager(TokenBucket.Null, TimeSpan.MaxValue); + realTimeHandler.Setup(algorithm, new AlgorithmNodePacket(PacketType.BacktestNode), null, null, timeLimitManager); + algorithm.Schedule.SetEventSchedule(realTimeHandler); + algorithm.SetDateTime(new DateTime(2024, 1, 1)); + return algorithm; + } + + private sealed class FakeWalkForwardOptimizationProvider : IWalkForwardOptimizationProvider + { + public List Requests { get; } = new List(); + public WalkForwardOptimizationResult Result { get; set; } = WalkForwardOptimizationResult.Empty; + + public WalkForwardOptimizationResult Optimize(WalkForwardOptimizationRequest request) + { + Requests.Add(request); + return Result; + } + } + } +} From 52db3d6e910f847e7f99cba8594ab3afe1e986e7 Mon Sep 17 00:00:00 2001 From: ScarabSystems Date: Mon, 13 Jul 2026 03:34:00 -0400 Subject: [PATCH 2/3] Route walk-forward optimization providers through Lean manager Exports the walk-forward optimization provider contract through Composer and injects the configured provider when LocalLeanManager receives the algorithm. Verification: dotnet build Tests/QuantConnect.Tests.csproj /p:Configuration=Debug /v:quiet; PYTHONNET_PYDLL=/usr/local/lib/libpython3.13.so dotnet test Tests/bin/Debug/QuantConnect.Tests.dll --filter FullyQualifiedName~WalkForwardOptimizationProviderRoutingTests. --- .../IWalkForwardOptimizationProvider.cs | 2 + .../NullWalkForwardOptimizationProvider.cs | 5 +- Engine/Server/LocalLeanManager.cs | 5 ++ Launcher/config.json | 5 +- ...ForwardOptimizationProviderRoutingTests.cs | 75 +++++++++++++++++++ 5 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 Tests/Engine/WalkForwardOptimizationProviderRoutingTests.cs diff --git a/Common/Interfaces/IWalkForwardOptimizationProvider.cs b/Common/Interfaces/IWalkForwardOptimizationProvider.cs index 849e2c07cecc..ec84e897217c 100644 --- a/Common/Interfaces/IWalkForwardOptimizationProvider.cs +++ b/Common/Interfaces/IWalkForwardOptimizationProvider.cs @@ -13,6 +13,7 @@ * limitations under the License. */ +using System.ComponentModel.Composition; using QuantConnect.Optimizer; namespace QuantConnect.Interfaces @@ -20,6 +21,7 @@ namespace QuantConnect.Interfaces /// /// Provides walk-forward optimization results for a running algorithm. /// + [InheritedExport(typeof(IWalkForwardOptimizationProvider))] public interface IWalkForwardOptimizationProvider { /// diff --git a/Common/Optimizer/NullWalkForwardOptimizationProvider.cs b/Common/Optimizer/NullWalkForwardOptimizationProvider.cs index 598461a05e10..f05a83ffa77e 100644 --- a/Common/Optimizer/NullWalkForwardOptimizationProvider.cs +++ b/Common/Optimizer/NullWalkForwardOptimizationProvider.cs @@ -27,7 +27,10 @@ public sealed class NullWalkForwardOptimizationProvider : IWalkForwardOptimizati /// public static NullWalkForwardOptimizationProvider Instance { get; } = new NullWalkForwardOptimizationProvider(); - private NullWalkForwardOptimizationProvider() + /// + /// Creates a new instance. + /// + public NullWalkForwardOptimizationProvider() { } diff --git a/Engine/Server/LocalLeanManager.cs b/Engine/Server/LocalLeanManager.cs index 9a7193910dbd..490fe9a2ca6c 100644 --- a/Engine/Server/LocalLeanManager.cs +++ b/Engine/Server/LocalLeanManager.cs @@ -16,7 +16,9 @@ using QuantConnect.Util; using QuantConnect.Packets; using QuantConnect.Commands; +using QuantConnect.Configuration; using QuantConnect.Interfaces; +using QuantConnect.Optimizer; using QuantConnect.Data.UniverseSelection; using QuantConnect.Lean.Engine.DataFeeds.Transport; @@ -67,6 +69,9 @@ public virtual void SetAlgorithm(IAlgorithm algorithm) { Algorithm = algorithm; algorithm.SetApi(SystemHandlers.Api); + algorithm.SetWalkForwardOptimizationProvider( + Composer.Instance.GetExportedValueByTypeName( + Config.Get("walk-forward-optimization-provider", nameof(NullWalkForwardOptimizationProvider)))); RemoteFileSubscriptionStreamReader.SetDownloadProvider((Api.Api)SystemHandlers.Api); } diff --git a/Launcher/config.json b/Launcher/config.json index 03af8dd9c5f9..b7b01814569f 100644 --- a/Launcher/config.json +++ b/Launcher/config.json @@ -343,6 +343,9 @@ "ema-slow": 20 }, + // provider used by QCAlgorithm.Optimize scheduled walk-forward optimization + "walk-forward-optimization-provider": "NullWalkForwardOptimizationProvider", + // specify supported languages when running regression tests "regression-test-languages": [ "CSharp", "Python" ], @@ -818,4 +821,4 @@ "history-provider": [ "BrokerageHistoryProvider", "SubscriptionDataReaderHistoryProvider" ] } } -} \ No newline at end of file +} diff --git a/Tests/Engine/WalkForwardOptimizationProviderRoutingTests.cs b/Tests/Engine/WalkForwardOptimizationProviderRoutingTests.cs new file mode 100644 index 000000000000..06adea624a46 --- /dev/null +++ b/Tests/Engine/WalkForwardOptimizationProviderRoutingTests.cs @@ -0,0 +1,75 @@ +/* + * 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.Reflection; +using Moq; +using NUnit.Framework; +using QuantConnect.Algorithm; +using QuantConnect.Configuration; +using QuantConnect.Interfaces; +using QuantConnect.Lean.Engine; +using QuantConnect.Lean.Engine.Server; +using QuantConnect.Optimizer; +using QuantConnect.Packets; +using QuantConnect.Util; + +namespace QuantConnect.Tests.Engine +{ + [TestFixture] + public class WalkForwardOptimizationProviderRoutingTests + { + [SetUp] + public void SetUp() + { + Config.Set("walk-forward-optimization-provider", nameof(TestWalkForwardOptimizationProvider)); + Composer.Instance.Reset(); + } + + [TearDown] + public void TearDown() + { + Config.Set("walk-forward-optimization-provider", nameof(NullWalkForwardOptimizationProvider)); + Composer.Instance.Reset(); + } + + [Test] + public void LocalLeanManagerInjectsConfiguredWalkForwardOptimizationProvider() + { + var manager = new LocalLeanManager(); + var systemHandlers = new LeanEngineSystemHandlers( + Mock.Of(), + new QuantConnect.Api.Api(), + Mock.Of(), + Mock.Of()); + manager.Initialize(systemHandlers, null, new BacktestNodePacket(), null); + var algorithm = new QCAlgorithm(); + + manager.SetAlgorithm(algorithm); + + var provider = typeof(QCAlgorithm) + .GetField("_walkForwardOptimizationProvider", BindingFlags.NonPublic | BindingFlags.Instance) + .GetValue(algorithm); + Assert.IsInstanceOf(provider); + } + } + + public sealed class TestWalkForwardOptimizationProvider : IWalkForwardOptimizationProvider + { + public WalkForwardOptimizationResult Optimize(WalkForwardOptimizationRequest request) + { + return WalkForwardOptimizationResult.Empty; + } + } +} From 372b67d79a5bcf117632304e7344804df80d349e Mon Sep 17 00:00:00 2001 From: ScarabSystems Date: Mon, 13 Jul 2026 04:14:47 -0400 Subject: [PATCH 3/3] Add walk-forward optimization providers Adds local and cloud walk-forward optimization providers, captures lightweight optimizer backtests for selector flows, and covers target, selector, timeout, abort, and engine-routing behavior with focused tests. Verification: dotnet build Tests/QuantConnect.Tests.csproj /p:Configuration=Debug /v:quiet; dotnet test QuantConnect.Tests.dll --filter WFO provider/algorithm/routing filters; dotnet test QuantConnect.Tests.dll --filter FullyQualifiedName~QuantConnect.Tests.Optimizer. --- Api/ApiWalkForwardOptimizationProvider.cs | 121 ++++++++++++ ...WalkForwardOptimizationBacktestSelector.cs | 118 ++++++++++++ Api/WalkForwardOptimizationCloudSettings.cs | 112 +++++++++++ Launcher/config.json | 2 +- .../LocalWalkForwardOptimizationProvider.cs | 133 +++++++++++++ Optimizer/LeanOptimizer.cs | 55 ++++++ Optimizer/OptimizationResult.cs | 10 +- ...rwardOptimizationProviderSelectionTests.cs | 121 ++++++++++++ ...WalkForwardOptimizationProviderTestBase.cs | 123 ++++++++++++ ...ApiWalkForwardOptimizationProviderTests.cs | 104 ++++++++++ ...ForwardOptimizationProviderRoutingTests.cs | 9 +- ...calWalkForwardOptimizationProviderTests.cs | 181 ++++++++++++++++++ 12 files changed, 1084 insertions(+), 5 deletions(-) create mode 100644 Api/ApiWalkForwardOptimizationProvider.cs create mode 100644 Api/WalkForwardOptimizationBacktestSelector.cs create mode 100644 Api/WalkForwardOptimizationCloudSettings.cs create mode 100644 Optimizer.Launcher/LocalWalkForwardOptimizationProvider.cs create mode 100644 Tests/Api/ApiWalkForwardOptimizationProviderSelectionTests.cs create mode 100644 Tests/Api/ApiWalkForwardOptimizationProviderTestBase.cs create mode 100644 Tests/Api/ApiWalkForwardOptimizationProviderTests.cs create mode 100644 Tests/Optimizer/LocalWalkForwardOptimizationProviderTests.cs diff --git a/Api/ApiWalkForwardOptimizationProvider.cs b/Api/ApiWalkForwardOptimizationProvider.cs new file mode 100644 index 000000000000..b0d88e4b9372 --- /dev/null +++ b/Api/ApiWalkForwardOptimizationProvider.cs @@ -0,0 +1,121 @@ +/* + * 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 System.Threading; +using QuantConnect.Interfaces; +using QuantConnect.Optimizer; +using QuantConnect.Util; + +namespace QuantConnect.Api +{ + /// + /// Runs scheduled walk-forward optimization using the QuantConnect cloud API optimization endpoints. + /// + public class ApiWalkForwardOptimizationProvider : IWalkForwardOptimizationProvider + { + private readonly IApi _api; + + /// + /// Creates a new instance using the configured API client. + /// + public ApiWalkForwardOptimizationProvider() + { + } + + /// + /// Creates a new instance using the specified API client. + /// + /// The API client + public ApiWalkForwardOptimizationProvider(IApi api) + { + _api = api; + } + + /// + /// Runs a cloud optimization for the specified walk-forward request. + /// + /// The walk-forward optimization request + /// The walk-forward optimization result + public WalkForwardOptimizationResult Optimize(WalkForwardOptimizationRequest request) + { + ArgumentNullException.ThrowIfNull(request); + if (request.Algorithm.ProjectId <= 0) + { + throw new InvalidOperationException("Walk-forward cloud optimization requires a configured project id."); + } + + var settings = WalkForwardOptimizationCloudSettings.Read(request); + var optimization = CreateAndWaitForOptimization(request, settings); + var backtests = optimization.Backtests?.Values.ToList() ?? new List(); + var parameterSet = request.TargetSelector == null + ? WalkForwardOptimizationBacktestSelector.SelectParameterSet(settings.Target, backtests) + : null; + + return new WalkForwardOptimizationResult(parameterSet, backtests); + } + + /// + /// Gets the API client used to submit and poll cloud optimizations. + /// + protected virtual IApi ApiClient => _api ?? Composer.Instance.GetPart(); + + private Optimization CreateAndWaitForOptimization(WalkForwardOptimizationRequest request, WalkForwardOptimizationCloudSettings settings) + { + var summary = ApiClient.CreateOptimization( + request.Algorithm.ProjectId, + settings.Name, + settings.Target.Target, + settings.TargetTo, + settings.Target.TargetValue, + settings.Strategy, + settings.CompileId, + request.Parameters, + request.Constraints, + settings.EstimatedCost, + settings.NodeType, + settings.ParallelNodes); + + if (string.IsNullOrWhiteSpace(summary?.OptimizationId)) + { + throw new InvalidOperationException("Walk-forward cloud optimization did not return an optimization id."); + } + + var timeoutUtc = DateTime.UtcNow.AddMinutes(settings.TimeoutMinutes); + while (true) + { + var optimization = ApiClient.ReadOptimization(summary.OptimizationId); + if (optimization?.Status == OptimizationStatus.Completed) + { + return optimization; + } + if (optimization?.Status == OptimizationStatus.Aborted) + { + throw new InvalidOperationException($"Walk-forward cloud optimization '{summary.OptimizationId}' was aborted."); + } + if (settings.TimeoutMinutes >= 0 && DateTime.UtcNow >= timeoutUtc) + { + throw new TimeoutException($"Walk-forward cloud optimization '{summary.OptimizationId}' timed out after {settings.TimeoutMinutes} minutes."); + } + if (settings.PollIntervalSeconds > 0) + { + Thread.Sleep(TimeSpan.FromSeconds(settings.PollIntervalSeconds)); + } + } + } + } +} diff --git a/Api/WalkForwardOptimizationBacktestSelector.cs b/Api/WalkForwardOptimizationBacktestSelector.cs new file mode 100644 index 000000000000..c9cac0557417 --- /dev/null +++ b/Api/WalkForwardOptimizationBacktestSelector.cs @@ -0,0 +1,118 @@ +/* + * 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 System.Text.RegularExpressions; +using QuantConnect.Optimizer.Objectives; +using QuantConnect.Optimizer.Parameters; + +namespace QuantConnect.Api +{ + /// + /// Selects the best parameter set from cloud optimization backtests. + /// + internal static class WalkForwardOptimizationBacktestSelector + { + /// + /// Selects the best parameter set according to the specified target. + /// + public static ParameterSet SelectParameterSet(Target target, IReadOnlyList backtests) + { + OptimizationBacktest bestBacktest = null; + decimal bestValue = 0; + + foreach (var backtest in backtests.Where(backtest => backtest?.ParameterSet != null)) + { + if (!TryGetTargetValue(backtest.Statistics, target.Target, out var targetValue)) + { + continue; + } + if (bestBacktest == null || target.Extremum.Better(bestValue, targetValue)) + { + bestBacktest = backtest; + bestValue = targetValue; + } + } + + return bestBacktest?.ParameterSet; + } + + private static bool TryGetTargetValue(IDictionary statistics, string targetPath, out decimal targetValue) + { + targetValue = 0; + if (statistics == null) + { + return false; + } + + foreach (var targetKey in GetCandidateStatisticKeys(targetPath)) + { + var statistic = statistics.FirstOrDefault(kvp => string.Equals(kvp.Key, targetKey, StringComparison.OrdinalIgnoreCase)); + if (statistic.Key != null && TryParseStatistic(statistic.Value, out targetValue)) + { + return true; + } + + statistic = statistics.FirstOrDefault(kvp => string.Equals(RemoveWhitespace(kvp.Key), RemoveWhitespace(targetKey), StringComparison.OrdinalIgnoreCase)); + if (statistic.Key != null && TryParseStatistic(statistic.Value, out targetValue)) + { + return true; + } + } + + return false; + } + + private static IEnumerable GetCandidateStatisticKeys(string targetPath) + { + yield return targetPath; + + var path = targetPath.Replace("[", string.Empty, StringComparison.Ordinal) + .Replace("]", string.Empty, StringComparison.Ordinal) + .Replace("'", string.Empty, StringComparison.Ordinal) + .Split("."); + var lastPart = path.LastOrDefault(); + if (!string.IsNullOrWhiteSpace(lastPart)) + { + yield return lastPart; + yield return Regex.Replace(lastPart, "([a-z])([A-Z])", "$1 $2"); + } + } + + private static bool TryParseStatistic(string value, out decimal parsed) + { + value = value?.Trim(); + var percentage = value?.EndsWith('%') == true; + if (percentage) + { + value = value.TrimEnd('%'); + } + var success = decimal.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out parsed); + if (success && percentage) + { + parsed /= 100; + } + return success; + } + + private static string RemoveWhitespace(string value) + { + return value == null ? string.Empty : Regex.Replace(value, @"\s+", string.Empty); + } + } +} diff --git a/Api/WalkForwardOptimizationCloudSettings.cs b/Api/WalkForwardOptimizationCloudSettings.cs new file mode 100644 index 000000000000..535eea36d975 --- /dev/null +++ b/Api/WalkForwardOptimizationCloudSettings.cs @@ -0,0 +1,112 @@ +/* + * 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 QuantConnect.Configuration; +using QuantConnect.Optimizer; +using QuantConnect.Optimizer.Objectives; +using QuantConnect.Statistics; +using QuantConnect.Util; + +namespace QuantConnect.Api +{ + /// + /// Configuration used to run walk-forward cloud optimizations. + /// + internal sealed class WalkForwardOptimizationCloudSettings + { + private const string DefaultOptimizationStrategy = "QuantConnect.Optimizer.Strategies.GridSearchOptimizationStrategy"; + + /// + /// Optimization name. + /// + public string Name { get; set; } + + /// + /// Optimization target. + /// + public Target Target { get; set; } + + /// + /// Optimization direction used by the API. + /// + public string TargetTo { get; set; } + + /// + /// Optimization strategy type name. + /// + public string Strategy { get; set; } + + /// + /// Compile id to optimize. + /// + public string CompileId { get; set; } + + /// + /// User-confirmed estimated cost. + /// + public decimal EstimatedCost { get; set; } + + /// + /// Cloud optimization node type. + /// + public string NodeType { get; set; } + + /// + /// Number of parallel nodes. + /// + public int ParallelNodes { get; set; } + + /// + /// Poll interval in seconds. + /// + public int PollIntervalSeconds { get; set; } + + /// + /// Timeout in minutes. + /// + public int TimeoutMinutes { get; set; } + + /// + /// Reads and validates cloud optimization settings for a request. + /// + public static WalkForwardOptimizationCloudSettings Read(WalkForwardOptimizationRequest request) + { + return new WalkForwardOptimizationCloudSettings + { + Name = Config.Get("walk-forward-optimization-name", $"Walk Forward Optimization {request.TriggerTimeUtc:O}"), + Target = request.Target ?? new Target(Config.Get("walk-forward-optimization-selector-target", PerformanceMetrics.NetProfit), new Maximization(), null), + TargetTo = request.Target?.Extremum is Minimization ? "min" : "max", + Strategy = Config.Get("walk-forward-optimization-strategy", DefaultOptimizationStrategy), + CompileId = GetRequiredConfig("walk-forward-optimization-compile-id"), + EstimatedCost = GetRequiredConfig("walk-forward-optimization-estimated-cost").ToDecimal(), + NodeType = GetRequiredConfig("walk-forward-optimization-node-type"), + ParallelNodes = GetRequiredConfig("walk-forward-optimization-parallel-nodes").ToInt32(), + PollIntervalSeconds = Math.Max(0, Config.GetInt("walk-forward-optimization-api-poll-interval-seconds", 10)), + TimeoutMinutes = Config.GetInt("walk-forward-optimization-api-timeout-minutes", 60) + }; + } + + private static string GetRequiredConfig(string key) + { + var value = Config.Get(key); + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidOperationException($"{key} must be configured before running walk-forward cloud optimization."); + } + return value; + } + } +} diff --git a/Launcher/config.json b/Launcher/config.json index b7b01814569f..997a42fb43fe 100644 --- a/Launcher/config.json +++ b/Launcher/config.json @@ -344,7 +344,7 @@ }, // provider used by QCAlgorithm.Optimize scheduled walk-forward optimization - "walk-forward-optimization-provider": "NullWalkForwardOptimizationProvider", + "walk-forward-optimization-provider": "LocalWalkForwardOptimizationProvider", // specify supported languages when running regression tests "regression-test-languages": [ "CSharp", "Python" ], diff --git a/Optimizer.Launcher/LocalWalkForwardOptimizationProvider.cs b/Optimizer.Launcher/LocalWalkForwardOptimizationProvider.cs new file mode 100644 index 000000000000..989193e03113 --- /dev/null +++ b/Optimizer.Launcher/LocalWalkForwardOptimizationProvider.cs @@ -0,0 +1,133 @@ +/* + * 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.Linq; +using System.Threading; +using Newtonsoft.Json; +using QuantConnect.Configuration; +using QuantConnect.Interfaces; +using QuantConnect.Optimizer.Objectives; +using QuantConnect.Optimizer.Strategies; +using QuantConnect.Statistics; +using QuantConnect.Util; + +namespace QuantConnect.Optimizer.Launcher +{ + /// + /// Runs scheduled walk-forward optimization using the local LEAN optimizer launcher. + /// + public class LocalWalkForwardOptimizationProvider : IWalkForwardOptimizationProvider + { + /// + /// Runs a local optimization for the specified walk-forward request. + /// + /// The walk-forward optimization request + /// The walk-forward optimization result + public WalkForwardOptimizationResult Optimize(WalkForwardOptimizationRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + using var endedEvent = new ManualResetEvent(false); + using var optimizer = CreateOptimizer(CreatePacket(request)); + + OptimizationResult result = null; + optimizer.Ended += (sender, optimizationResult) => + { + result = optimizationResult; + endedEvent.Set(); + }; + + optimizer.Start(); + + var timeoutMinutes = Config.GetInt("walk-forward-optimization-timeout-minutes"); + if (timeoutMinutes > 0) + { + if (!endedEvent.WaitOne(TimeSpan.FromMinutes(timeoutMinutes))) + { + throw new TimeoutException($"Walk-forward optimization timed out after {timeoutMinutes} minutes."); + } + } + else + { + endedEvent.WaitOne(); + } + + var backtests = result?.Backtests ?? optimizer.CompletedOptimizationBacktests; + var parameterSet = request.TargetSelector == null ? result?.ParameterSet : null; + return new WalkForwardOptimizationResult(parameterSet, backtests); + } + + /// + /// Creates the optimizer instance for the optimization packet. + /// + /// The optimization packet + /// The optimizer instance + protected virtual LeanOptimizer CreateOptimizer(OptimizationNodePacket packet) + { + var optimizerType = Config.Get( + "walk-forward-optimization-launcher", + Config.Get("optimization-launcher", nameof(ConsoleLeanOptimizer))); + + return (LeanOptimizer)Activator.CreateInstance(Composer.Instance + .GetExportedTypes() + .Single(type => type.Name == optimizerType || type.FullName == optimizerType), packet); + } + + private static OptimizationNodePacket CreatePacket(WalkForwardOptimizationRequest request) + { + return new OptimizationNodePacket + { + Name = Config.Get("walk-forward-optimization-name", $"Walk Forward Optimization {request.TriggerTimeUtc:O}"), + Created = request.TriggerTimeUtc, + ProjectId = request.Algorithm.ProjectId, + OptimizationId = Config.Get("walk-forward-optimization-id", Guid.NewGuid().ToString()), + OptimizationStrategy = Config.Get( + "walk-forward-optimization-strategy", + Config.Get("optimization-strategy", typeof(GridSearchOptimizationStrategy).FullName)), + OptimizationStrategySettings = GetStrategySettings(), + Criterion = request.Target ?? GetSelectorTarget(), + Constraints = request.Constraints, + OptimizationParameters = request.Parameters, + MaximumConcurrentBacktests = Config.GetInt( + "walk-forward-optimization-maximum-concurrent-backtests", + Config.GetInt("maximum-concurrent-backtests", Math.Max(1, Environment.ProcessorCount / 2))), + Channel = Config.Get("data-channel") + }; + } + + private static OptimizationStrategySettings GetStrategySettings() + { + var settings = Config.Get( + "walk-forward-optimization-strategy-settings", + Config.Get("optimization-strategy-settings", + "{\"$type\":\"QuantConnect.Optimizer.Strategies.StepBaseOptimizationStrategySettings, QuantConnect.Optimizer\"}")); + return (OptimizationStrategySettings)JsonConvert.DeserializeObject( + settings, + new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }); + } + + private static Target GetSelectorTarget() + { + return new Target( + Config.Get("walk-forward-optimization-selector-target", PerformanceMetrics.NetProfit), + new Maximization(), + null); + } + } +} diff --git a/Optimizer/LeanOptimizer.cs b/Optimizer/LeanOptimizer.cs index 28bac18bf537..2755ea4b4bdb 100644 --- a/Optimizer/LeanOptimizer.cs +++ b/Optimizer/LeanOptimizer.cs @@ -14,8 +14,11 @@ */ using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System.Threading; using QuantConnect.Util; +using QuantConnect.Api; using QuantConnect.Logging; using QuantConnect.Configuration; using System.Collections.Concurrent; @@ -44,12 +47,27 @@ public abstract class LeanOptimizer : IDisposable // Per-backtest metrics extracted in NewResult so we don't retain the full backtest JSON. private readonly List _completedBacktests = new(); + private readonly List _completedOptimizationBacktests = new(); /// /// The total completed backtests count /// protected int CompletedBacktests => _failedBacktest + _completedBacktest; + /// + /// The lightweight backtest summaries completed by this optimization. + /// + public IReadOnlyList CompletedOptimizationBacktests + { + get + { + lock (_completedOptimizationBacktests) + { + return new List(_completedOptimizationBacktests); + } + } + } + /// /// Lock to update optimization status /// @@ -199,6 +217,10 @@ protected virtual void TriggerOnEndEvent() { backtestsSnapshot = new List(_completedBacktests); } + lock (_completedOptimizationBacktests) + { + result.Backtests = new List(_completedOptimizationBacktests); + } var parameters = new OptimizationAnalysisRunParameters( backtestsSnapshot, NodePacket.OptimizationParameters); @@ -284,6 +306,15 @@ protected virtual void NewResult(string jsonBacktestResult, string backtestId) } } + var optimizationBacktest = CreateOptimizationBacktest(backtestId, parameterSet, jsonBacktestResult); + if (optimizationBacktest != null) + { + lock (_completedOptimizationBacktests) + { + _completedOptimizationBacktests.Add(optimizationBacktest); + } + } + // always notify the strategy Strategy.PushNewResults(result); @@ -481,5 +512,29 @@ private void LaunchLeanForParameterSet(ParameterSet parameterSet) } } } + + private static OptimizationBacktest CreateOptimizationBacktest(string backtestId, ParameterSet parameterSet, string jsonBacktestResult) + { + if (string.IsNullOrEmpty(jsonBacktestResult)) + { + return null; + } + + try + { + var result = JObject.Parse(jsonBacktestResult); + var statistics = result["Statistics"] ?? result["statistics"]; + return new OptimizationBacktest(parameterSet, backtestId, "OptimizationBacktest") + { + Progress = 1, + Statistics = statistics?.ToObject>() + }; + } + catch (JsonException ex) + { + Log.Error(ex, $"LeanOptimizer.CreateOptimizationBacktest(): failed to parse backtest result for '{backtestId}'"); + return null; + } + } } } diff --git a/Optimizer/OptimizationResult.cs b/Optimizer/OptimizationResult.cs index 7d7e97e1e376..1d1f59bda600 100644 --- a/Optimizer/OptimizationResult.cs +++ b/Optimizer/OptimizationResult.cs @@ -13,6 +13,9 @@ * limitations under the License. */ +using System; +using System.Collections.Generic; +using QuantConnect.Api; using QuantConnect.Optimizer.Parameters; namespace QuantConnect.Optimizer @@ -48,10 +51,15 @@ public class OptimizationResult public ParameterSet ParameterSet { get; } /// - /// Aggregate diagnostic for the whole optimization; populated only on the final result fired via . + /// Aggregate analysis for the whole optimization; populated only on the final result fired via . /// public OptimizationAnalysis Analysis { get; set; } + /// + /// Lightweight backtest summaries produced during the optimization. + /// + public IReadOnlyList Backtests { get; set; } = Array.Empty(); + /// /// Create an instance of /// diff --git a/Tests/Api/ApiWalkForwardOptimizationProviderSelectionTests.cs b/Tests/Api/ApiWalkForwardOptimizationProviderSelectionTests.cs new file mode 100644 index 000000000000..e3ac79fc9b6d --- /dev/null +++ b/Tests/Api/ApiWalkForwardOptimizationProviderSelectionTests.cs @@ -0,0 +1,121 @@ +/* + * 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 Moq; +using NUnit.Framework; +using QuantConnect.Interfaces; +using QuantConnect.Optimizer; +using QuantConnect.Optimizer.Objectives; +using QuantConnect.Optimizer.Parameters; +using QuantConnect.Optimizer.Strategies; +using QuantConnect.Statistics; +using ApiOptimization = QuantConnect.Api.Optimization; +using ApiOptimizationNodes = QuantConnect.Api.OptimizationNodes; +using ApiOptimizationSummary = QuantConnect.Api.OptimizationSummary; +using ApiWalkForwardOptimizationProvider = QuantConnect.Api.ApiWalkForwardOptimizationProvider; + +namespace QuantConnect.Tests.API +{ + [TestFixture] + [NonParallelizable] + public class ApiWalkForwardOptimizationProviderSelectionTests : ApiWalkForwardOptimizationProviderTestBase + { + [Test] + public void CreatesCloudOptimizationAndReturnsWinningParameterSet() + { + var request = CreateTargetRequest(); + var api = CreateApiForCompletedOptimization(request, "['Statistics'].['Net Profit']", "max", CreateCompletedOptimization()); + var provider = new ApiWalkForwardOptimizationProvider(api.Object); + + var result = provider.Optimize(request); + + Assert.AreEqual("20", result.ParameterSet.Value["ema-fast"]); + Assert.AreEqual(2, result.Backtests.Count); + api.Verify(apiClient => apiClient.ReadOptimization("optimization-1"), Times.Exactly(2)); + } + + [Test] + public void MinimizationTargetsSelectLowestBacktestStatistic() + { + var request = CreateTargetRequest(new Target(PerformanceMetrics.Drawdown, new Minimization(), null)); + var api = CreateApiForCompletedOptimization( + request, + "['Statistics'].['Drawdown']", + "min", + CreateCompletedOptimization(PerformanceMetrics.Drawdown, 10, 5), + includeRunningRead: false); + var provider = new ApiWalkForwardOptimizationProvider(api.Object); + + var result = provider.Optimize(request); + + Assert.AreEqual("20", result.ParameterSet.Value["ema-fast"]); + } + + [Test] + public void SelectorRequestsReturnBacktestsWithoutPreselectedParameterSet() + { + var request = CreateSelectorRequest(); + var api = CreateApiForCompletedOptimization(request, "['Statistics'].['Net Profit']", "max", CreateCompletedOptimization(), false); + var provider = new ApiWalkForwardOptimizationProvider(api.Object); + + var result = provider.Optimize(request); + + Assert.IsNull(result.ParameterSet); + Assert.AreEqual(2, result.Backtests.Count); + Assert.AreEqual("20", request.TargetSelector(result.Backtests).Value["ema-fast"]); + } + + private static Mock CreateApiForCompletedOptimization( + WalkForwardOptimizationRequest request, + string target, + string targetTo, + ApiOptimization completedOptimization, + bool includeRunningRead = true) + { + var api = new Mock(MockBehavior.Strict); + api.Setup(apiClient => apiClient.CreateOptimization( + request.Algorithm.ProjectId, + It.Is(name => name.StartsWith("Walk Forward Optimization", StringComparison.Ordinal)), + target, + targetTo, + null, + typeof(GridSearchOptimizationStrategy).FullName, + CompileId, + request.Parameters, + request.Constraints, + 0.25m, + ApiOptimizationNodes.O2_8, + 2)) + .Returns(new ApiOptimizationSummary { OptimizationId = "optimization-1" }); + + if (includeRunningRead) + { + api.SetupSequence(apiClient => apiClient.ReadOptimization("optimization-1")) + .Returns(new ApiOptimization { Status = OptimizationStatus.Running }) + .Returns(completedOptimization); + } + else + { + api.Setup(apiClient => apiClient.ReadOptimization("optimization-1")) + .Returns(completedOptimization); + } + + return api; + } + } +} diff --git a/Tests/Api/ApiWalkForwardOptimizationProviderTestBase.cs b/Tests/Api/ApiWalkForwardOptimizationProviderTestBase.cs new file mode 100644 index 000000000000..6784bff327e7 --- /dev/null +++ b/Tests/Api/ApiWalkForwardOptimizationProviderTestBase.cs @@ -0,0 +1,123 @@ +/* + * 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 NUnit.Framework; +using QuantConnect.Algorithm; +using QuantConnect.Configuration; +using QuantConnect.Optimizer; +using QuantConnect.Optimizer.Objectives; +using QuantConnect.Optimizer.Parameters; +using QuantConnect.Optimizer.Strategies; +using QuantConnect.Statistics; +using QuantConnect.Util; +using ApiOptimization = QuantConnect.Api.Optimization; +using ApiOptimizationBacktest = QuantConnect.Api.OptimizationBacktest; + +namespace QuantConnect.Tests.API +{ + public abstract class ApiWalkForwardOptimizationProviderTestBase + { + protected const string CompileId = "compile-1"; + + [SetUp] + public void SetUp() + { + Config.Set("walk-forward-optimization-compile-id", CompileId); + Config.Set("walk-forward-optimization-estimated-cost", "0.25"); + Config.Set("walk-forward-optimization-node-type", QuantConnect.Api.OptimizationNodes.O2_8); + Config.Set("walk-forward-optimization-parallel-nodes", "2"); + Config.Set("walk-forward-optimization-api-poll-interval-seconds", "0"); + Config.Set("walk-forward-optimization-api-timeout-minutes", "1"); + Config.Set("walk-forward-optimization-strategy", typeof(GridSearchOptimizationStrategy).FullName); + } + + [TearDown] + public void TearDown() + { + SetUp(); + } + + protected static WalkForwardOptimizationRequest CreateTargetRequest(Target target = null) + { + return new WalkForwardOptimizationRequest( + CreateAlgorithm(), + new DateTime(2026, 1, 1), + target ?? new Target(PerformanceMetrics.NetProfit, new Maximization(), null), + CreateParameters()); + } + + protected static WalkForwardOptimizationRequest CreateSelectorRequest() + { + return new WalkForwardOptimizationRequest( + CreateAlgorithm(), + new DateTime(2026, 1, 1), + backtests => backtests + .OrderByDescending(backtest => backtest.Statistics[PerformanceMetrics.NetProfit].ToDecimal()) + .First() + .ParameterSet, + CreateParameters()); + } + + protected static HashSet CreateParameters() + { + return new HashSet + { + new OptimizationStepParameter("ema-fast", 10, 20, 10) + }; + } + + protected static ApiOptimization CreateCompletedOptimization( + string statistic = PerformanceMetrics.NetProfit, + decimal firstValue = 10, + decimal secondValue = 20) + { + return new ApiOptimization + { + Status = OptimizationStatus.Completed, + Backtests = new Dictionary + { + { "backtest-1", CreateBacktest(1, "10", statistic, firstValue) }, + { "backtest-2", CreateBacktest(2, "20", statistic, secondValue) } + } + }; + } + + private static QCAlgorithm CreateAlgorithm() + { + return new QCAlgorithm { ProjectId = 123 }; + } + + private static ApiOptimizationBacktest CreateBacktest( + int id, + string parameterValue, + string statistic, + decimal statisticValue) + { + return new ApiOptimizationBacktest( + new ParameterSet(id, new Dictionary { { "ema-fast", parameterValue } }), + $"backtest-{id}", + $"candidate-{id}") + { + Statistics = new Dictionary + { + { statistic, statisticValue.ToStringInvariant() } + } + }; + } + } +} diff --git a/Tests/Api/ApiWalkForwardOptimizationProviderTests.cs b/Tests/Api/ApiWalkForwardOptimizationProviderTests.cs new file mode 100644 index 000000000000..d5383b85c7ff --- /dev/null +++ b/Tests/Api/ApiWalkForwardOptimizationProviderTests.cs @@ -0,0 +1,104 @@ +/* + * 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 Moq; +using NUnit.Framework; +using QuantConnect.Configuration; +using QuantConnect.Interfaces; +using QuantConnect.Optimizer; +using QuantConnect.Optimizer.Objectives; +using QuantConnect.Optimizer.Parameters; +using ApiOptimization = QuantConnect.Api.Optimization; +using ApiOptimizationSummary = QuantConnect.Api.OptimizationSummary; +using ApiWalkForwardOptimizationProvider = QuantConnect.Api.ApiWalkForwardOptimizationProvider; + +namespace QuantConnect.Tests.API +{ + [TestFixture] + [NonParallelizable] + public class ApiWalkForwardOptimizationProviderTests : ApiWalkForwardOptimizationProviderTestBase + { + [Test] + public void RequiresExplicitCloudResourceConfiguration() + { + Config.Set("walk-forward-optimization-compile-id", string.Empty); + var api = new Mock(MockBehavior.Strict); + var provider = new ApiWalkForwardOptimizationProvider(api.Object); + + Assert.Throws(() => provider.Optimize(CreateTargetRequest())); + + api.Verify( + apiClient => apiClient.CreateOptimization( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Test] + public void ThrowsWhenCloudOptimizationIsAborted() + { + var api = CreateApiForSingleRead(new ApiOptimization { Status = OptimizationStatus.Aborted }); + var provider = new ApiWalkForwardOptimizationProvider(api.Object); + + Assert.Throws(() => provider.Optimize(CreateTargetRequest())); + } + + [Test] + public void ThrowsWhenCloudOptimizationTimesOut() + { + Config.Set("walk-forward-optimization-api-timeout-minutes", "0"); + var api = CreateApiForSingleRead(new ApiOptimization { Status = OptimizationStatus.Running }); + var provider = new ApiWalkForwardOptimizationProvider(api.Object); + + Assert.Throws(() => provider.Optimize(CreateTargetRequest())); + api.Verify(apiClient => apiClient.ReadOptimization("optimization-1"), Times.Once); + } + + private static Mock CreateApiForSingleRead(ApiOptimization optimization) + { + var api = new Mock(MockBehavior.Strict); + api.Setup(apiClient => apiClient.CreateOptimization( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(new ApiOptimizationSummary { OptimizationId = "optimization-1" }); + api.Setup(apiClient => apiClient.ReadOptimization("optimization-1")) + .Returns(optimization); + + return api; + } + } +} diff --git a/Tests/Engine/WalkForwardOptimizationProviderRoutingTests.cs b/Tests/Engine/WalkForwardOptimizationProviderRoutingTests.cs index 06adea624a46..9660577a4f8f 100644 --- a/Tests/Engine/WalkForwardOptimizationProviderRoutingTests.cs +++ b/Tests/Engine/WalkForwardOptimizationProviderRoutingTests.cs @@ -13,6 +13,7 @@ * limitations under the License. */ +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Moq; using NUnit.Framework; @@ -45,12 +46,14 @@ public void TearDown() } [Test] + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "LeanEngineSystemHandlers owns and disposes the API instance.")] public void LocalLeanManagerInjectsConfiguredWalkForwardOptimizationProvider() { - var manager = new LocalLeanManager(); - var systemHandlers = new LeanEngineSystemHandlers( + using var manager = new LocalLeanManager(); + var api = new QuantConnect.Api.Api(); + using var systemHandlers = new LeanEngineSystemHandlers( Mock.Of(), - new QuantConnect.Api.Api(), + api, Mock.Of(), Mock.Of()); manager.Initialize(systemHandlers, null, new BacktestNodePacket(), null); diff --git a/Tests/Optimizer/LocalWalkForwardOptimizationProviderTests.cs b/Tests/Optimizer/LocalWalkForwardOptimizationProviderTests.cs new file mode 100644 index 000000000000..89add1a0f370 --- /dev/null +++ b/Tests/Optimizer/LocalWalkForwardOptimizationProviderTests.cs @@ -0,0 +1,181 @@ +/* + * 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 System.Threading.Tasks; +using NUnit.Framework; +using QuantConnect.Algorithm; +using QuantConnect.Api; +using QuantConnect.Optimizer; +using QuantConnect.Optimizer.Launcher; +using QuantConnect.Optimizer.Objectives; +using QuantConnect.Optimizer.Parameters; +using QuantConnect.Statistics; +using QuantConnect.Util; + +namespace QuantConnect.Tests.Optimizer +{ + [TestFixture] + public class LocalWalkForwardOptimizationProviderTests + { + [Test] + public void RunsOptimizerAndReturnsWinningParameterSet() + { + var provider = new TestLocalWalkForwardOptimizationProvider(); + var parameters = new HashSet + { + new OptimizationStepParameter("ema-fast", 10, 20, 10) + }; + var target = new Target("Profit", new Maximization(), null); + + var result = provider.Optimize(new WalkForwardOptimizationRequest( + new QCAlgorithm(), + new DateTime(2026, 1, 1), + target, + parameters)); + + Assert.AreEqual("20", result.ParameterSet.Value["ema-fast"]); + Assert.AreSame(target, provider.Packet.Criterion); + Assert.AreSame(parameters, provider.Packet.OptimizationParameters); + Assert.AreEqual(2, result.Backtests.Count); + } + + [Test] + public void SelectorRequestsReturnBacktestsWithoutPreselectedParameterSet() + { + var provider = new TestLocalWalkForwardOptimizationProvider(); + var parameters = new HashSet + { + new OptimizationStepParameter("ema-fast", 10, 20, 10) + }; + + var result = provider.Optimize(new WalkForwardOptimizationRequest( + new QCAlgorithm(), + new DateTime(2026, 1, 1), + backtests => backtests + .OrderByDescending(backtest => backtest.Statistics[PerformanceMetrics.NetProfit].ToDecimal()) + .First() + .ParameterSet, + parameters)); + + Assert.IsNull(result.ParameterSet); + Assert.AreEqual(2, result.Backtests.Count); + Assert.AreEqual("20", result.Backtests + .OrderByDescending(backtest => backtest.Statistics[PerformanceMetrics.NetProfit].ToDecimal()) + .First() + .ParameterSet + .Value["ema-fast"]); + Assert.AreEqual( + "Statistics." + PerformanceMetrics.NetProfit, + provider.Packet.Criterion.Target + .Replace("['", string.Empty, StringComparison.Ordinal) + .Replace("']", string.Empty, StringComparison.Ordinal)); + } + + [Test] + public void SelectorRequestsReturnBacktestsWhenDefaultTargetDoesNotSelectSolution() + { + var provider = new TestLocalWalkForwardOptimizationProvider(includeNetProfit: false); + var parameters = new HashSet + { + new OptimizationStepParameter("ema-fast", 10, 20, 10) + }; + + var result = provider.Optimize(new WalkForwardOptimizationRequest( + new QCAlgorithm(), + new DateTime(2026, 1, 1), + backtests => backtests + .OrderByDescending(backtest => backtest.Statistics["Profit"].ToDecimal()) + .First() + .ParameterSet, + parameters)); + + Assert.IsNull(result.ParameterSet); + Assert.AreEqual(2, result.Backtests.Count); + Assert.AreEqual("20", result.Backtests + .OrderByDescending(backtest => backtest.Statistics["Profit"].ToDecimal()) + .First() + .ParameterSet + .Value["ema-fast"]); + } + + private sealed class TestLocalWalkForwardOptimizationProvider : LocalWalkForwardOptimizationProvider + { + private readonly bool _includeNetProfit; + + public TestLocalWalkForwardOptimizationProvider(bool includeNetProfit = true) + { + _includeNetProfit = includeNetProfit; + } + + public OptimizationNodePacket Packet { get; private set; } + + protected override LeanOptimizer CreateOptimizer(OptimizationNodePacket packet) + { + Packet = packet; + return new ImmediateLeanOptimizer(packet, _includeNetProfit); + } + } + + private sealed class ImmediateLeanOptimizer : LeanOptimizer + { + private readonly bool _includeNetProfit; + + public ImmediateLeanOptimizer(OptimizationNodePacket nodePacket, bool includeNetProfit) + : base(nodePacket) + { + _includeNetProfit = includeNetProfit; + } + + protected override string RunLean(ParameterSet parameterSet, string backtestName) + { + var backtestId = parameterSet.Id.ToStringInvariant(); + Task.Delay(10).ContinueWith(_ => + { + var netProfit = parameterSet.Value["ema-fast"].ToDecimal(); + NewResult(CreateBacktestJson(netProfit, _includeNetProfit), backtestId); + }, TaskScheduler.Default); + return backtestId; + } + + protected override void AbortLean(string backtestId) + { + } + + protected override void SendUpdate() + { + } + + private static string CreateBacktestJson(decimal netProfit, bool includeNetProfit) + { + var statistics = new Dictionary + { + { "Profit", netProfit.ToStringInvariant() } + }; + if (includeNetProfit) + { + statistics[PerformanceMetrics.NetProfit] = netProfit.ToStringInvariant(); + } + + return Newtonsoft.Json.JsonConvert.SerializeObject(new + { + Statistics = statistics + }); + } + } + } +}