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
132 changes: 132 additions & 0 deletions Common/BacktestAnalysisResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* 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;
using QuantConnect.Util;
using System.Collections;
using System.Collections.Generic;

namespace QuantConnect
{
/// <summary>
/// A simple context that holds a single diagnostic sample object.
/// </summary>
public class BacktestAnalysisContext : IBacktestAnalysisContext
{
/// <summary>
/// Gets or sets a representative sample value produced by the analysis.
/// </summary>
public object Sample { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="BacktestAnalysisContext"/> class.
/// </summary>
/// <param name="sample">A representative sample produced by the analysis, or <c>null</c> when no issue was found.</param>
public BacktestAnalysisContext(object sample)
{
Sample = sample;
}
}

/// <summary>
/// A context that represents multiple occurrences of the same diagnostic issue,
/// exposing the first sample and the total occurrence count.
/// </summary>
public class BacktestAnalysisRepeatedContext : BacktestAnalysisContext
{
/// <summary>
/// Gets or sets the total number of matching occurrences found by the analysis.
/// </summary>
public int Occurrences { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="BacktestAnalysisRepeatedContext"/> class.
/// </summary>
/// <param name="samples">All matching sample objects; the first element is stored as <see cref="BacktestAnalysisContext.Sample"/>.</param>
public BacktestAnalysisRepeatedContext(IReadOnlyList<object> samples) : base(samples.Count > 0 ? samples[0] : null)
{
Occurrences = samples.Count;
}
}

/// <summary>
/// A composite context that aggregates several <see cref="IBacktestAnalysisContext"/> instances
/// into a single enumerable context.
/// </summary>
public class BacktestAnalysisAggregateContext : IBacktestAnalysisContext, IEnumerable<IBacktestAnalysisContext>
{
private IReadOnlyList<IBacktestAnalysisContext> _contexts { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="BacktestAnalysisAggregateContext"/> class.
/// </summary>
/// <param name="contexts">The individual contexts to aggregate.</param>
public BacktestAnalysisAggregateContext(IReadOnlyList<IBacktestAnalysisContext> contexts)
{
_contexts = contexts;
}

/// <summary>
/// Returns an enumerator that iterates over each contained context.
/// </summary>
/// <returns>An enumerator for the inner context list.</returns>
public IEnumerator<IBacktestAnalysisContext> GetEnumerator()
{
return _contexts.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

/// <summary>
/// Represents the outcome of a single backtest diagnostic analysis,
/// containing the analysis name, diagnostic context, and a list of potential solutions.
/// </summary>
[JsonConverter(typeof(BacktestAnalysisResultJsonConverter))]
public class BacktestAnalysisResult
{
/// <summary>
/// Gets or sets the name of the analysis that produced this result.
/// </summary>
public string Name { get; set; }

/// <summary>
/// Gets or sets the diagnostic context carrying sample data about the detected issue.
/// </summary>
public IBacktestAnalysisContext Context { get; set; }

/// <summary>
/// Gets or sets human-readable suggestions for resolving the detected issue.
/// </summary>
public IReadOnlyList<string> PotentialSolutions { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="BacktestAnalysisResult"/> class.
/// </summary>
/// <param name="name">The name of the analysis that produced this result.</param>
/// <param name="context">The diagnostic context object describing the detected issue.</param>
/// <param name="potentialSolutions">A list of human-readable remediation suggestions.</param>
public BacktestAnalysisResult(string name, IBacktestAnalysisContext context, IReadOnlyList<string> potentialSolutions)
{
Name = name;
Context = context;
PotentialSolutions = potentialSolutions;
}
}
}
13 changes: 13 additions & 0 deletions Common/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4558,6 +4558,19 @@ public static int GreatestCommonDivisor(this IEnumerable<int> values)
return result.Value;
}

/// <summary>
/// Returns a new sorted list of (v[i] / v[i-1] - 1) values. The first key is dropped.
/// </summary>
public static SortedList<DateTime, decimal> PercentChange(this SortedList<DateTime, decimal> values)
{
var result = new SortedList<DateTime, decimal>();
foreach (var (current, previous) in values.Skip(1).Zip(values, (current, previous) => (current, previous)))
{
result.Add(current.Key, current.Value / previous.Value - 1);
}
return result;
}

/// <summary>
/// Gets the greatest common divisor of two numbers
/// </summary>
Expand Down
25 changes: 25 additions & 0 deletions Common/IBacktestAnalysisResultContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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.
*
*/

namespace QuantConnect
{
/// <summary>
/// Marker interface for context objects attached to a <see cref="BacktestAnalysisResult"/>.
/// </summary>
public interface IBacktestAnalysisContext
{
}
}
7 changes: 7 additions & 0 deletions Common/Packets/BacktestResultPacket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ public class BacktestResult : Result
/// </summary>
public Dictionary<string, AlgorithmPerformance> RollingWindow { get; set; } = new Dictionary<string, AlgorithmPerformance>();

/// <summary>
/// Backtest analysis results.
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public IReadOnlyList<BacktestAnalysisResult> AnalysisResult { get; set; }

/// <summary>
/// Default Constructor
/// </summary>
Expand All @@ -221,6 +227,7 @@ public BacktestResult()
public BacktestResult(BacktestResultParameters parameters) : base(parameters)
{
RollingWindow = parameters.RollingWindow;
AnalysisResult = parameters.AnalysisResult;
}
}
} // End of Namespace:
11 changes: 9 additions & 2 deletions Common/Packets/BacktestResultParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
*
*/

using System;
using QuantConnect.Orders;
using QuantConnect.Statistics;
using System;
using System.Collections.Generic;

namespace QuantConnect.Packets
Expand All @@ -31,6 +31,11 @@ public class BacktestResultParameters : BaseResultParameters
/// </summary>
public Dictionary<string, AlgorithmPerformance> RollingWindow { get; set; }

/// <summary>
/// Backtest analysis results.
/// </summary>
public IReadOnlyList<BacktestAnalysisResult> AnalysisResult { get; set; }

/// <summary>
/// Creates a new instance
/// </summary>
Expand All @@ -43,10 +48,12 @@ public BacktestResultParameters(IDictionary<string, Chart> charts,
List<OrderEvent> orderEvents,
AlgorithmPerformance totalPerformance = null,
AlgorithmConfiguration algorithmConfiguration = null,
IDictionary<string, string> state = null)
IDictionary<string, string> state = null,
IReadOnlyList<BacktestAnalysisResult> analysisResult = null)
: base(charts, orders, profitLoss, statistics, runtimeStatistics, orderEvents, totalPerformance, algorithmConfiguration, state)
{
RollingWindow = rollingWindow;
AnalysisResult = analysisResult;
}
}
}
114 changes: 114 additions & 0 deletions Common/Util/BacktestAnalysisResultJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* 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 Newtonsoft.Json.Linq;

namespace QuantConnect.Util
{
/// <summary>
/// JSON converter for <see cref="BacktestAnalysisResult"/>.
/// Deserializes into a <see cref="BacktestAnalysisResult"/> and detects the concrete
/// <see cref="IBacktestAnalysisContext"/> type from the shape of the JSON:
/// <list type="bullet">
/// <item>JSON array -> <see cref="BacktestAnalysisAggregateContext"/></item>
/// <item>JSON object with an <c>Occurrences</c> property -> <see cref="BacktestAnalysisRepeatedContext"/></item>
/// <item>Any other JSON object -> <see cref="BacktestAnalysisContext"/></item>
/// </list>
/// </summary>
public class BacktestAnalysisResultJsonConverter : JsonConverter
{
/// <summary>
/// Serialization is handled by the default JSON.NET serializer.
/// </summary>
public override bool CanWrite => false;

/// <summary>
/// Not implemented — serialization is delegated to the default serializer.
/// </summary>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}

/// <summary>
/// Deserializes a JSON object into a <see cref="BacktestAnalysisResult"/>, resolving the
/// concrete <see cref="IBacktestAnalysisContext"/> type from the structure of the
/// <c>Context</c> JSON token.
/// </summary>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObject = JObject.Load(reader);

var name = jObject["Name"]?.Value<string>();
var potentialSolutions = jObject["PotentialSolutions"]?.ToObject<List<string>>() ?? [];
var context = DeserializeContext(jObject["Context"]);

return new BacktestAnalysisResult(name, context, potentialSolutions);
}

/// <summary>
/// Determines whether this converter can handle the given type.
/// </summary>
public override bool CanConvert(Type objectType)
{
return typeof(BacktestAnalysisResult).IsAssignableFrom(objectType);
}

/// <summary>
/// Recursively deserializes a context token into the correct
/// <see cref="IBacktestAnalysisContext"/> concrete type.
/// </summary>
private static IBacktestAnalysisContext DeserializeContext(JToken contextToken)
{
if (contextToken == null || contextToken.Type == JTokenType.Null)
{
return null;
}

if (contextToken.Type == JTokenType.Array)
{
var innerContexts = new List<IBacktestAnalysisContext>();
foreach (var item in (JArray)contextToken)
{
innerContexts.Add(DeserializeContext(item));
}
return new BacktestAnalysisAggregateContext(innerContexts);
}

if (contextToken.Type == JTokenType.Object)
{
var jObj = (JObject)contextToken;
var sample = jObj["Sample"]?.ToObject<object>();

if (jObj.ContainsKey("Occurrences"))
{
return new BacktestAnalysisRepeatedContext([])
{
Sample = sample,
Occurrences = jObj["Occurrences"]?.Value<int>() ?? 0
};
}

return new BacktestAnalysisContext(sample);
}

return null;
}
}
}
Loading
Loading