Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions Algorithm/QCAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -165,6 +168,7 @@ private SecurityDefinitionSymbolResolver SecurityDefinitionSymbolResolver
private readonly HistoryRequestFactory _historyRequestFactory;

private IApi _api;
private IWalkForwardOptimizationProvider _walkForwardOptimizationProvider = NullWalkForwardOptimizationProvider.Instance;

/// <summary>
/// QCAlgorithm Base Class Constructor - Initialize the underlying QCAlgorithm components.
Expand Down Expand Up @@ -3136,6 +3140,107 @@ public ScheduledEvent Train(IDateRule dateRule, ITimeRule timeRule, Action train
return Schedule.Training(dateRule, timeRule, trainingCode);
}

/// <summary>
/// Schedules a walk-forward optimization using the specified date and time rules.
/// </summary>
/// <param name="dateRule">Specifies what dates the optimization should run</param>
/// <param name="timeRule">Specifies the times on those dates the optimization should run</param>
/// <param name="target">The optimization target used to select the winning parameter set</param>
/// <param name="parameters">The optimization parameters to evaluate</param>
/// <param name="constraints">The optional constraints to apply to optimization results</param>
[DocumentationAttribute(ParameterAndOptimization)]
[DocumentationAttribute(ScheduledEvents)]
public ScheduledEvent Optimize(
IDateRule dateRule,
ITimeRule timeRule,
Target target,
HashSet<OptimizationParameter> parameters,
IReadOnlyList<Constraint> constraints = null)
{
if (target == null)
{
throw new ArgumentNullException(nameof(target));
}

return ScheduleWalkForwardOptimization(dateRule, timeRule, triggerTime =>
new WalkForwardOptimizationRequest(this, triggerTime, target, parameters, constraints));
}

/// <summary>
/// Schedules a walk-forward optimization using the specified date and time rules.
/// </summary>
/// <param name="dateRule">Specifies what dates the optimization should run</param>
/// <param name="timeRule">Specifies the times on those dates the optimization should run</param>
/// <param name="targetSelector">Selects the winning parameter set from optimization backtests</param>
/// <param name="parameters">The optimization parameters to evaluate</param>
/// <param name="constraints">The optional constraints to apply to optimization results</param>
[DocumentationAttribute(ParameterAndOptimization)]
[DocumentationAttribute(ScheduledEvents)]
public ScheduledEvent Optimize(
IDateRule dateRule,
ITimeRule timeRule,
Func<IReadOnlyList<OptimizationBacktest>, ParameterSet> targetSelector,
HashSet<OptimizationParameter> parameters,
IReadOnlyList<Constraint> 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<DateTime, WalkForwardOptimizationRequest> 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<OptimizationBacktest>());
}

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));
}

/// <summary>
/// Event invocator for the <see cref="InsightsGenerated"/> event
/// </summary>
Expand Down Expand Up @@ -3175,6 +3280,16 @@ public void SetApi(IApi api)
_api = api;
}

/// <summary>
/// Sets the walk-forward optimization provider.
/// </summary>
/// <param name="provider">The provider used to run walk-forward optimizations</param>
[DocumentationAttribute(ParameterAndOptimization)]
public void SetWalkForwardOptimizationProvider(IWalkForwardOptimizationProvider provider)
{
_walkForwardOptimizationProvider = provider ?? throw new ArgumentNullException(nameof(provider));
}

/// <summary>
/// Sets the object store
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,12 @@ public void SetFinishedWarmingUp()
/// <param name="historyProvider">Historical data provider</param>
public void SetHistoryProvider(IHistoryProvider historyProvider) => _baseAlgorithm.SetHistoryProvider(historyProvider);

/// <summary>
/// Sets the walk-forward optimization provider.
/// </summary>
/// <param name="provider">Walk-forward optimization provider</param>
public void SetWalkForwardOptimizationProvider(IWalkForwardOptimizationProvider provider) => _baseAlgorithm.SetWalkForwardOptimizationProvider(provider);

/// <summary>
/// Set live mode state of the algorithm run: Public setter for the algorithm property LiveMode.
/// </summary>
Expand Down
121 changes: 121 additions & 0 deletions Api/ApiWalkForwardOptimizationProvider.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Runs scheduled walk-forward optimization using the QuantConnect cloud API optimization endpoints.
/// </summary>
public class ApiWalkForwardOptimizationProvider : IWalkForwardOptimizationProvider
{
private readonly IApi _api;

/// <summary>
/// Creates a new instance using the configured API client.
/// </summary>
public ApiWalkForwardOptimizationProvider()
{
}

/// <summary>
/// Creates a new instance using the specified API client.
/// </summary>
/// <param name="api">The API client</param>
public ApiWalkForwardOptimizationProvider(IApi api)
{
_api = api;
}

/// <summary>
/// Runs a cloud optimization for the specified walk-forward request.
/// </summary>
/// <param name="request">The walk-forward optimization request</param>
/// <returns>The walk-forward optimization result</returns>
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<OptimizationBacktest>();
var parameterSet = request.TargetSelector == null
? WalkForwardOptimizationBacktestSelector.SelectParameterSet(settings.Target, backtests)
: null;

return new WalkForwardOptimizationResult(parameterSet, backtests);
}

/// <summary>
/// Gets the API client used to submit and poll cloud optimizations.
/// </summary>
protected virtual IApi ApiClient => _api ?? Composer.Instance.GetPart<IApi>();

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));
}
}
}
}
}
118 changes: 118 additions & 0 deletions Api/WalkForwardOptimizationBacktestSelector.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Selects the best parameter set from cloud optimization backtests.
/// </summary>
internal static class WalkForwardOptimizationBacktestSelector
{
/// <summary>
/// Selects the best parameter set according to the specified target.
/// </summary>
public static ParameterSet SelectParameterSet(Target target, IReadOnlyList<OptimizationBacktest> 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<string, string> 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<string> 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);
}
}
}
Loading