diff --git a/Common/Python/PythonInitializer.cs b/Common/Python/PythonInitializer.cs index b967c630256f..8b3692cbda61 100644 --- a/Common/Python/PythonInitializer.cs +++ b/Common/Python/PythonInitializer.cs @@ -14,6 +14,7 @@ * */ +using System; using Python.Runtime; using QuantConnect.Logging; diff --git a/Configuration/QuantConnect.Configuration.csproj b/Configuration/QuantConnect.Configuration.csproj index a2ab7ccf645b..dc714150a290 100644 --- a/Configuration/QuantConnect.Configuration.csproj +++ b/Configuration/QuantConnect.Configuration.csproj @@ -77,6 +77,7 @@ + diff --git a/Configuration/ReportArgumentParser.cs b/Configuration/ReportArgumentParser.cs new file mode 100644 index 000000000000..8bf1cd6e5416 --- /dev/null +++ b/Configuration/ReportArgumentParser.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Microsoft.Extensions.CommandLineUtils; + +namespace QuantConnect.Configuration +{ + /// + /// Command Line arguments parser for Report Creator + /// + public static class ReportArgumentParser + { + private const string ApplicationName = "Report Creator"; + + private const string ApplicationDescription = + "LEAN Report Creator generates beautiful PDF reports from your backtesting strategies for sharing with prospective partners."; + + private const string ApplicationHelpText = + "If you are looking for help, please go to https://www.quantconnect.com/docs"; + + private static readonly List Options = new List + { + new CommandLineOption("strategy-name", CommandOptionType.SingleValue, "Strategy name"), + new CommandLineOption("strategy-description", CommandOptionType.SingleValue, "Strategy description"), + new CommandLineOption("live-data-source-file", CommandOptionType.SingleValue, "Live source data json file"), + new CommandLineOption("backtest-data-source-file", CommandOptionType.SingleValue, "Backtest source data json file"), + new CommandLineOption("report-destination", CommandOptionType.SingleValue, "Destination of processed report file") + }; + + /// + /// Parse and construct the args. + /// + public static Dictionary ParseArguments(string[] args) + { + return ApplicationParser.Parse(ApplicationName, ApplicationDescription, ApplicationHelpText, args, Options); + } + } +} \ No newline at end of file diff --git a/QuantConnect.Lean.sln b/QuantConnect.Lean.sln index a0931e90501d..7d8f3361cb27 100644 --- a/QuantConnect.Lean.sln +++ b/QuantConnect.Lean.sln @@ -46,7 +46,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantConnect.Algorithm.Pyth EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantConnect.ToolBox", "ToolBox\QuantConnect.ToolBox.csproj", "{AC9A142C-B485-44D7-91FF-015C22C43D05}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantConnect.VisualStudioPlugin", "VisualStudioPlugin\QuantConnect.VisualStudio17Plugin.csproj", "{5326A9FB-0270-4647-8C43-20C5607DB529}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantConnect.VisualStudio17Plugin", "VisualStudioPlugin\QuantConnect.VisualStudio17Plugin.csproj", "{5326A9FB-0270-4647-8C43-20C5607DB529}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantConnect.Jupyter", "Jupyter\QuantConnect.Jupyter.csproj", "{9561D14A-467E-40AD-928E-EE9F758D7D98}" EndProject @@ -56,6 +56,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantConnect.Algorithm.Fram EndProject Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "QuantConnect.PythonToolbox", "PythonToolbox\QuantConnect.PythonToolbox.pyproj", "{1C47F4DB-2AFF-4FAE-9142-B33BE654A516}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantConnect.Report", "Report\QuantConnect.Report.csproj", "{2431419F-8BC6-4F59-944E-9A1CD28982DF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -208,8 +210,19 @@ Global {1C47F4DB-2AFF-4FAE-9142-B33BE654A516}.Debug|x64.ActiveCfg = Debug|Any CPU {1C47F4DB-2AFF-4FAE-9142-B33BE654A516}.Release|Any CPU.ActiveCfg = Release|Any CPU {1C47F4DB-2AFF-4FAE-9142-B33BE654A516}.Release|x64.ActiveCfg = Release|Any CPU + {2431419F-8BC6-4F59-944E-9A1CD28982DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2431419F-8BC6-4F59-944E-9A1CD28982DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2431419F-8BC6-4F59-944E-9A1CD28982DF}.Debug|x64.ActiveCfg = Debug|Any CPU + {2431419F-8BC6-4F59-944E-9A1CD28982DF}.Debug|x64.Build.0 = Debug|Any CPU + {2431419F-8BC6-4F59-944E-9A1CD28982DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2431419F-8BC6-4F59-944E-9A1CD28982DF}.Release|Any CPU.Build.0 = Release|Any CPU + {2431419F-8BC6-4F59-944E-9A1CD28982DF}.Release|x64.ActiveCfg = Release|Any CPU + {2431419F-8BC6-4F59-944E-9A1CD28982DF}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {67BACDB0-1FDB-4AF0-A199-88CF436FB470} + EndGlobalSection EndGlobal diff --git a/QuantConnect.Lean.sln.DotSettings b/QuantConnect.Lean.sln.DotSettings index 5cac1ac0e7f9..6306ae513568 100644 --- a/QuantConnect.Lean.sln.DotSettings +++ b/QuantConnect.Lean.sln.DotSettings @@ -1,14 +1,20 @@ CSharp60 INSIDE + NEVER True CHOP_IF_LONG True <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> + True True True + True True + True + True True + True True True \ No newline at end of file diff --git a/Report/App.config b/Report/App.config new file mode 100644 index 000000000000..8227adb98927 --- /dev/null +++ b/Report/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/Report/Program.cs b/Report/Program.cs new file mode 100644 index 000000000000..5284a14e84d7 --- /dev/null +++ b/Report/Program.cs @@ -0,0 +1,82 @@ +/* + * 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.IO; +using Newtonsoft.Json; +using QuantConnect.Configuration; +using QuantConnect.Logging; +using QuantConnect.Orders; +using QuantConnect.Packets; + +namespace QuantConnect.Report +{ + /// + /// Lean Report creates a PDF strategy summary from the backtest and live json objects. + /// + class Program + { + static void Main(string[] args) + { + // Parse report arguments and merge with config to use in report creator: + if (args.Length > 0) + { + Config.MergeCommandLineArgumentsWithConfiguration(ReportArgumentParser.ParseArguments(args)); + } + var name = Config.Get("strategy-name"); + var description = Config.Get("strategy-description"); + var version = Config.Get("strategy-version"); + var backtestDataFile = Config.Get("backtest-data-source-file"); + var liveDataFile = Config.Get("live-data-source-file"); + var destination = Config.Get("report-destination"); + + //Set the Order Parser For JSON: + JsonConvert.DefaultSettings = () => new JsonSerializerSettings + { + Converters = { new OrderJsonConverter() } + }; + + // Parse content from source files into result objects + Log.Trace($"QuantConnect.Report.Main(): Parsing source files...{backtestDataFile}, {liveDataFile}"); + var backtest = JsonConvert.DeserializeObject(File.ReadAllText(backtestDataFile)); + + LiveResult live = null; + if (liveDataFile != string.Empty) + { + live = JsonConvert.DeserializeObject(File.ReadAllText(liveDataFile)); + } + + //Create a new report + Log.Trace("QuantConnect.Report.Main(): Instantiating report..."); + var report = new Report(name, description, version, backtest, live); + + // Generate the html content + Log.Trace("QuantConnect.Report.Main(): Starting content compile..."); + var html = report.Compile(); + + //Write it to target destination. + if (destination != string.Empty) + { + Log.Trace($"QuantConnect.Report.Main(): Writing content to file {destination}"); + File.WriteAllText(destination, html); + } + else + { + Console.Write(html); + } + Log.Trace("QuantConnect.Report.Main(): Completed."); + Console.ReadKey(); + } + } +} diff --git a/Report/Properties/AssemblyInfo.cs b/Report/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..fb87df56844a --- /dev/null +++ b/Report/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("QuantConnect.Report")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("QuantConnect.Report")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("2431419f-8bc6-4f59-944e-9a1cd28982df")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Report/PyConverter.cs b/Report/PyConverter.cs new file mode 100644 index 000000000000..0f1cde678335 --- /dev/null +++ b/Report/PyConverter.cs @@ -0,0 +1,567 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Linq; + +namespace Python.Runtime +{ + /// + /// use PyClrType to convert between python object and clr object. + /// From: https://raw.githubusercontent.com/yagweb/pythonnetLab/master/pynetLab/PyConverter.cs + /// + public class PyConverter + { + public PyConverter() + { + this.Converters = new List(); + this.PythonConverters = new Dictionary>(); + this.ClrConverters = new Dictionary>(); + } + + private List Converters; + + private Dictionary> PythonConverters; + + private Dictionary> ClrConverters; + + public void Add(PyClrTypeBase converter) + { + this.Converters.Add(converter); + + Dictionary py_converters; + var state = this.PythonConverters.TryGetValue(converter.PythonType.Handle, out py_converters); + if (!state) + { + py_converters = new Dictionary(); + this.PythonConverters.Add(converter.PythonType.Handle, py_converters); + } + py_converters.Add(converter.ClrType, converter); + + Dictionary clr_converters; + state = this.ClrConverters.TryGetValue(converter.ClrType, out clr_converters); + if (!this.ClrConverters.ContainsKey(converter.ClrType)) + { + clr_converters = new Dictionary(); + this.ClrConverters.Add(converter.ClrType, clr_converters); + } + clr_converters.Add(converter.PythonType.Handle, converter); + } + + public void AddObjectType(PyObject pyType, PyConverter converter = null) + { + if (converter == null) + { + converter = this; + } + this.Add(new ObjectType(pyType, converter)); + } + + public void AddListType(PyConverter converter = null) + { + this.AddListType(converter); + } + + public void AddListType(PyConverter converter = null) + { + if (converter == null) + { + converter = this; + } + this.Add(new PyListType(converter)); + } + + public void AddDictType(PyConverter converter = null) + { + if (converter == null) + { + converter = this; + } + this.Add(new PyDictType(converter)); + } + + public T ToClr(PyObject obj) + { + return (T)ToClr(obj, typeof(T)); + } + + public object ToClr(PyObject obj, Type t = null) + { + if (obj == null) + { + return null; + } + PyObject type = obj.GetPythonType(); + Dictionary converters; + var state = PythonConverters.TryGetValue(type.Handle, out converters); + if (!state) + { + throw new Exception($"Type {type.ToString()} not recognized"); + } + if (t == null || !converters.ContainsKey(t)) + { + return converters.Values.First().ToClr(obj); + } + else + { + return converters[t].ToClr(obj); + } + } + + public PyObject ToPython(object clrObj, IntPtr? t = null) + { + if (clrObj == null) + { + return null; + } + Type type = clrObj.GetType(); + Dictionary converters; + var state = ClrConverters.TryGetValue(type, out converters); + if (!state) + { + throw new Exception($"Type {type.ToString()} not recognized"); + } + if (t == null || !converters.ContainsKey(t.Value)) + { + return converters.Values.First().ToPython(clrObj); + } + else + { + return converters[t.Value].ToPython(clrObj); + } + } + } + + public abstract class PyClrTypeBase + { + protected PyClrTypeBase(string pyType, Type clrType) + { + this.PythonType = PythonEngine.Eval(pyType); + this.ClrType = clrType; + } + + protected PyClrTypeBase(PyObject pyType, Type clrType) + { + this.PythonType = pyType; + this.ClrType = clrType; + } + + public PyObject PythonType + { + get; + private set; + } + + public Type ClrType + { + get; + private set; + } + + public abstract object ToClr(PyObject pyObj); + + public abstract PyObject ToPython(object clrObj); + } + + public class PyClrType : PyClrTypeBase + { + public PyClrType(PyObject pyType, Type clrType, + Func py2clr, Func clr2py) + : base(pyType, clrType) + { + this.Py2Clr = py2clr; + this.Clr2Py = clr2py; + } + + private Func Py2Clr; + + private Func Clr2Py; + + public override object ToClr(PyObject pyObj) + { + return this.Py2Clr(pyObj); + } + + public override PyObject ToPython(object clrObj) + { + return this.Clr2Py(clrObj); + } + } + + public class StringType : PyClrTypeBase + { + public StringType() + : base("str", typeof(string)) + { + } + + public override object ToClr(PyObject pyObj) + { + return pyObj.As(); + } + + public override PyObject ToPython(object clrObj) + { + return new PyString(Convert.ToString(clrObj)); + } + } + + public class BooleanType : PyClrTypeBase + { + public BooleanType() + : base("bool", typeof(bool)) + { + } + + public override object ToClr(PyObject pyObj) + { + return pyObj.As(); + } + + public override PyObject ToPython(object clrObj) + { + //return new PyBoolean(Convert.ToString(clrObj)); + throw new NotImplementedException(); + } + } + + public class Int32Type : PyClrTypeBase + { + public Int32Type() + : base("int", typeof(int)) + { + } + + public override object ToClr(PyObject pyObj) + { + return pyObj.As(); + } + + public override PyObject ToPython(object clrObj) + { + return new PyInt(Convert.ToInt32(clrObj)); + } + } + + public class Int64Type : PyClrTypeBase + { + public Int64Type() + : base("int", typeof(long)) + { + } + + public override object ToClr(PyObject pyObj) + { + return pyObj.As(); + } + + public override PyObject ToPython(object clrObj) + { + return new PyInt(Convert.ToInt64(clrObj)); + } + } + + public class FloatType : PyClrTypeBase + { + public FloatType() + : base("float", typeof(float)) + { + } + + public override object ToClr(PyObject pyObj) + { + return pyObj.As(); + } + + public override PyObject ToPython(object clrObj) + { + return new PyFloat(Convert.ToSingle(clrObj)); + } + } + + public class DoubleType : PyClrTypeBase + { + public DoubleType() + : base("float", typeof(double)) + { + } + + public override object ToClr(PyObject pyObj) + { + return pyObj.As(); + } + + public override PyObject ToPython(object clrObj) + { + return new PyFloat(Convert.ToDouble(clrObj)); + } + } + + public class PyPropetryAttribute : Attribute + { + public PyPropetryAttribute() + { + this.Name = null; + } + + public PyPropetryAttribute(string name, string py_type = null) + { + this.Name = name; + this.PythonTypeName = py_type; + } + + public string Name + { + get; + private set; + } + + public string PythonTypeName + { + get; + private set; + } + + public IntPtr? PythonType + { + get; + set; + } + } + + abstract class ClrMemberInfo + { + public string PyPropertyName; + + public IntPtr? PythonType; + + public string ClrPropertyName; + + public Type ClrType; + + public PyConverter Converter; + + public abstract void SetPyObjAttr(PyObject pyObj, object clrObj); + + public abstract void SetClrObjAttr(object clrObj, PyObject pyObj); + } + + class ClrPropertyInfo : ClrMemberInfo + { + public ClrPropertyInfo(PropertyInfo info, PyPropetryAttribute py_info, PyConverter converter) + { + this.PropertyInfo = info; + this.ClrPropertyName = info.Name; + this.ClrType = info.PropertyType; + this.PyPropertyName = py_info.Name; + if (string.IsNullOrEmpty(this.PyPropertyName)) + { + this.PyPropertyName = info.Name; + } + //this.PythonType = converter.Get(); + this.Converter = converter; + } + + public PropertyInfo PropertyInfo + { + get; + private set; + } + + public override void SetPyObjAttr(PyObject pyObj, object clrObj) + { + var clr_value = this.PropertyInfo.GetValue(clrObj, null); + var py_value = this.Converter.ToPython(clr_value); + pyObj.SetAttr(this.PyPropertyName, py_value); + } + + public override void SetClrObjAttr(object clrObj, PyObject pyObj) + { + var py_value = pyObj.GetAttr(this.PyPropertyName); + var clr_value = this.Converter.ToClr(py_value); + this.PropertyInfo.SetValue(clrObj, clr_value, null); + } + } + + class ClrFieldInfo : ClrMemberInfo + { + public ClrFieldInfo(FieldInfo info, PyPropetryAttribute py_info, PyConverter converter) + { + this.FieldInfo = info; + this.ClrPropertyName = info.Name; + this.ClrType = info.FieldType; + this.PyPropertyName = py_info.Name; + if (string.IsNullOrEmpty(this.PyPropertyName)) + { + this.PyPropertyName = info.Name; + } + //this.PythonType = converter.Get(); + this.Converter = converter; + } + + public FieldInfo FieldInfo; + + public override void SetPyObjAttr(PyObject pyObj, object clrObj) + { + var clr_value = this.FieldInfo.GetValue(clrObj); + var py_value = this.Converter.ToPython(clr_value); + pyObj.SetAttr(this.PyPropertyName, py_value); + } + + public override void SetClrObjAttr(object clrObj, PyObject pyObj) + { + var py_value = pyObj.GetAttr(this.PyPropertyName); + var clr_value = this.Converter.ToClr(py_value); + this.FieldInfo.SetValue(clrObj, clr_value); + } + } + + /// + /// Convert between Python object and clr object + /// + public class ObjectType : PyClrTypeBase + { + public ObjectType(PyObject pyType, PyConverter converter) + : base(pyType, typeof(T)) + { + this.Converter = converter; + this.Properties = new List(); + + // Get all attributes + foreach (var property in this.ClrType.GetProperties()) + { + var attr = property.GetCustomAttributes(typeof(PyPropetryAttribute), true); + if (attr.Length == 0) + { + continue; + } + var py_info = attr[0] as PyPropetryAttribute; + this.Properties.Add(new ClrPropertyInfo(property, py_info, this.Converter)); + } + + foreach (var field in this.ClrType.GetFields()) + { + var attr = field.GetCustomAttributes(typeof(PyPropetryAttribute), true); + if (attr.Length == 0) + { + continue; + } + var py_info = attr[0] as PyPropetryAttribute; + this.Properties.Add(new ClrFieldInfo(field, py_info, this.Converter)); + } + } + + private PyConverter Converter; + + private List Properties; + + public override object ToClr(PyObject pyObj) + { + var clrObj = Activator.CreateInstance(this.ClrType); + foreach (var item in this.Properties) + { + item.SetClrObjAttr(clrObj, pyObj); + } + return clrObj; + } + + public override PyObject ToPython(object clrObj) + { + var pyObj = this.PythonType.Invoke(); + foreach (var item in this.Properties) + { + item.SetPyObjAttr(pyObj, clrObj); + } + return pyObj; + } + } + + public class PyListType : PyClrTypeBase + { + public PyListType(PyConverter converter) + : base("list", typeof(List)) + { + this.Converter = converter; + } + + private PyConverter Converter; + + public override object ToClr(PyObject pyObj) + { + var dict = this._ToClr(new PyList(pyObj)); + return dict; + } + + private object _ToClr(PyList pyList) + { + var list = new List(); + foreach (PyObject item in pyList) + { + var _item = this.Converter.ToClr(item); + list.Add(_item); + } + return list; + } + + public override PyObject ToPython(object clrObj) + { + return this._ToPython(clrObj as List); + } + + public PyObject _ToPython(List clrObj) + { + var pyList = new PyList(); + foreach (var item in clrObj) + { + PyObject _item = this.Converter.ToPython(item); + pyList.Append(_item); + } + return pyList; + } + } + + public class PyDictType : PyClrTypeBase + { + public PyDictType(PyConverter converter) + : base("dict", typeof(Dictionary)) + { + this.Converter = converter; + } + + private PyConverter Converter; + + public override object ToClr(PyObject pyObj) + { + var dict = this._ToClr(new PyDict(pyObj)); + return dict; + } + + private object _ToClr(PyDict pyDict) + { + var dict = new Dictionary(); + foreach (PyObject key in pyDict.Keys()) + { + var _key = this.Converter.ToClr(key); + var _value = this.Converter.ToClr(pyDict[key]); + dict.Add(_key, _value); + } + return dict; + } + + public override PyObject ToPython(object clrObj) + { + return this._ToPython(clrObj as Dictionary); + } + + public PyObject _ToPython(Dictionary clrObj) + { + var pyDict = new PyDict(); + foreach (var item in clrObj) + { + PyObject _key = this.Converter.ToPython(item.Key); + PyObject _value = this.Converter.ToPython(item.Value); + pyDict[_key] = _value; + } + return pyDict; + } + } +} \ No newline at end of file diff --git a/Report/QuantConnect.Report.csproj b/Report/QuantConnect.Report.csproj new file mode 100644 index 000000000000..23d99ffd7cb9 --- /dev/null +++ b/Report/QuantConnect.Report.csproj @@ -0,0 +1,181 @@ + + + + + Debug + AnyCPU + {2431419F-8BC6-4F59-944E-9A1CD28982DF} + Exe + QuantConnect.Report + QuantConnect.Report + v4.5.2 + 512 + true + + + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + bin\Debug\QuantConnect.Report.xml + false + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + QuantConnect.Report.Program + + + + ..\packages\morelinq.3.2.0\lib\net451\MoreLinq.dll + + + + ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll + + + ..\packages\QuantConnect.pythonnet.1.0.5.29\lib\osx\Python.Runtime.dll + + + + ..\packages\System.Collections.Immutable.1.5.0\lib\netstandard2.0\System.Collections.Immutable.dll + + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Threading.Tasks.Extensions.4.5.1\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll + + + ..\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll + True + + + + + + + + + + false + true + false + true + false + true + + + + + + + + + + + + + ..\packages\QuantConnect.pythonnet.1.0.5.29\lib\win\Python.Runtime.dll + + + + + + + ..\packages\QuantConnect.pythonnet.1.0.5.29\lib\linux\Python.Runtime.dll + + + + + + + ..\packages\QuantConnect.pythonnet.1.0.5.29\lib\osx\Python.Runtime.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + PreserveNewest + + + + + + {2545C0B4-FABB-49C9-8DD1-9AD7EE23F86B} + QuantConnect + + + {0aeb4ea3-28c8-476e-89fd-926f06590b4c} + QuantConnect.Configuration + + + {01911409-86BE-4E7D-9947-DF714138610D} + QuantConnect.Logging + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/Report/Report.cs b/Report/Report.cs new file mode 100644 index 000000000000..7b24db8ec665 --- /dev/null +++ b/Report/Report.cs @@ -0,0 +1,85 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System.Collections.Generic; +using System.IO; +using Python.Runtime; +using QuantConnect.Logging; +using QuantConnect.Packets; +using QuantConnect.Report.ReportElements; + +namespace QuantConnect.Report +{ + internal class Report + { + private const string _template = "template.html"; + private readonly IReadOnlyCollection _elements; + + /// + /// Creating beautiful HTML and PDF Reports based on backtest and live data. + /// + /// Name of the strategy + /// Description of the strategy + /// Version number of the strategy + /// Backtest result object + /// Live result object + public Report(string name, string description, string version, BacktestResult backtest, LiveResult live) + { + _elements = new List + { + //Basics + new TextReportElement("strategy name", ReportKey.StrategyName, name), + new TextReportElement("description", ReportKey.StrategyDescription, description), + new TextReportElement("version", ReportKey.StrategyVersion, version), + + //KPI's Backtest: + new EstimatedCapacityReportElement("estimated capacity kpi", ReportKey.EstimatedCapacity, backtest, live), + new CAGRReportElement("cagr kpi", ReportKey.CAGR, backtest, live), + new TurnoverReportElement("turnover kpi", ReportKey.Turnover, backtest, live), + new MaxDrawdownReportElement("max drawdown kpi", ReportKey.MaxDrawdown, backtest, live), + new KellyEstimateReportElement("kelly estimate kpi", ReportKey.KellyEstimate, backtest, live), + new SharpeRatioReportElement("sharpe kpi", ReportKey.SharpeRatio, backtest, live), + new PSRReportElement("psr kpi", ReportKey.PSR, backtest, live), + new InformationRatioReportElement("psr kpi", ReportKey.InformationRatio, backtest, live), + new MarketsReportElement("markets kpi", ReportKey.Markets, backtest, live), + new TradesPerDayReportElement("trades per day kpi", ReportKey.TradesPerDay, backtest, live), + + // Generate and insert plots MonthlyReturnsReportElement + new MonthlyReturnsReportElement("monthly return plot", ReportKey.MonthlyReturns, backtest, live), + + // Array of Crisis Plots: + new CrisisReportElement("crisis plots", ReportKey.CrisisPlots, backtest, live) + }; + } + + /// + /// Compile the backtest data into a report + /// + /// + public string Compile() + { + var html = File.ReadAllText(_template); + + // Render the output and replace the report section + foreach (var element in _elements) + { + Log.Trace($"QuantConnect.Report.Compile(): Rendering {element.Name}..."); + html = html.Replace(element.Key, element.Render()); + } + + return html; + } + } +} \ No newline at end of file diff --git a/Report/ReportChartTests.py b/Report/ReportChartTests.py new file mode 100644 index 000000000000..2336a50aa4c8 --- /dev/null +++ b/Report/ReportChartTests.py @@ -0,0 +1,166 @@ +import numpy as np +import pandas as pd +from datetime import datetime +from ReportCharts import ReportCharts + +charts = ReportCharts() + +## Test GetReturnsPerTrade +backtest = list(np.random.normal(0, 1, 1000)) +live = list(np.random.normal(0.5, 1, 400)) +result = charts.GetReturnsPerTrade([], []) +result = charts.GetReturnsPerTrade(backtest, []) +result = charts.GetReturnsPerTrade(backtest, live) + +## Test GetCumulativeReturnsPlot +time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2012-10-01T00:00:00', periods=365)] +strategy = np.linspace(1, 25, 365) +benchmark = np.linspace(2, 26, 365) +backtest = [time, strategy, benchmark] + +time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2013-10-01T00:00:00', periods=50)] +strategy = np.linspace(25, 29, 50) +benchmark = np.linspace(26, 30, 50) +live = [time, strategy, benchmark] +empty = [[], [], []] +result = charts.GetCumulativeReturns(empty, empty) +result = charts.GetCumulativeReturns(backtest, empty) +result = charts.GetCumulativeReturns(backtest, live) + +## Test GetDailyReturnsPlot +time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2012-10-01T00:00:00', periods=365)] +data = list(np.random.normal(0, 1, 365)) +backtest = [time, data] + +time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2013-10-01T00:00:00', periods=120)] +data = list(np.random.normal(0.5, 1.5, 120)) +live = [time, data] + +empty = [[], []] +result = charts.GetDailyReturns(empty, empty) +result = charts.GetDailyReturns(backtest, empty) +result = charts.GetDailyReturns(backtest, live) + +## Test GetMonthlyReturnsPlot +backtest = {'2016': [0.5, 0.7, 0.2, 0.23, 1.3, 1.45, 1.67, -2.3, -0.5, 1.23, 1.23, -3.5], + '2017': [0.5, 0.7, 0.2, 0.23, 1.3, 1.45, 1.67, -2.3, -0.5, 1.23, 1.23, -3.5][::-1]} + +live = {'2018': [0.5, 0.7, 0.2, 0.23, 1.3, 1.45, 1.67, -2.3, -0.5, 1.23, 1.23, -3.5], + '2019': [1.5, 2.7, -3.2, -0.23, 4.3, -2.45, -1.67, 2.3, np.nan, np.nan, np.nan, np.nan]} + +result = charts.GetMonthlyReturns({}, {}) +result = charts.GetMonthlyReturns(backtest, pd.DataFrame()) +result = charts.GetMonthlyReturns(backtest, live) + +## Test GetAnnualReturnsPlot +time = ['2012', '2013', '2014', '2015', '2016'] +strategy = list(np.random.normal(0, 1, 5)) +backtest = [time, strategy] + +time = ['2017', '2018'] +strategy = list(np.random.normal(0.5, 1.5, 2)) +live = [time, strategy] + +empty = [[], []] +result = charts.GetAnnualReturns(empty, empty) +result = charts.GetAnnualReturns(backtest, empty) +result = charts.GetAnnualReturns(backtest, live) + +## Test GetDrawdownPlot +time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2012-10-01', periods=365)] +data = list(np.random.uniform(-5, 0, 365)) +backtest = [time, data] +time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2013-10-01', periods=100)] +data = list(np.random.uniform(-5, 0, 100)) +live = [time, data] +worst = [{'Begin': datetime(2012, 10, 1), 'End': datetime(2012, 10, 11)}, + {'Begin': datetime(2012, 12, 1), 'End': datetime(2012, 12, 11)}, + {'Begin': datetime(2013, 3, 1), 'End': datetime(2013, 3, 11)}, + {'Begin': datetime(2013, 4, 1), 'End': datetime(2013, 4, 1)}, + {'Begin': datetime(2013, 6, 1), 'End': datetime(2013, 6, 11)}] +empty = [[], []] +result = charts.GetDrawdown(empty, empty, {}) +result = charts.GetDrawdown(backtest, empty, worst) +result = charts.GetDrawdown(backtest, live, worst) + +## Test GetCrisisPlots (backtest only) +equity = list(np.linspace(1, 25, 365)) +benchmark = list(np.linspace(2, 26, 365)) +time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2012-10-01 00:00:00', periods=365)] +backtest = [time, equity, benchmark] + +empty = [[], [], []] +result = charts.GetCrisisEventsPlots(empty, 'empty_crisis') +result = charts.GetCrisisEventsPlots(backtest, 'dummy_crisis') + +## Test GetRollingBetaPlot +empty = [[], [], []] +twelve = [np.nan for x in range(180)] + list(np.random.uniform(-1, 1, 185)) +six = list(np.random.uniform(-1, 1, 365)) +time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2012-10-01 00:00:00', periods=365)] +backtest = [time, six, twelve] + +result = charts.GetRollingBeta(empty, empty) +result = charts.GetRollingBeta([time, six,[]], empty) +result = charts.GetRollingBeta(backtest, empty) + +twelve = [np.nan for x in range(180)] + list(np.random.uniform(-1, 1, 185)) +six = list(np.random.uniform(-1, 1, 365)) +time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2013-10-01 00:00:00', periods=365)] +live = [time, six, twelve] + +result = charts.GetRollingBeta(backtest, [time, six,[]]) +result = charts.GetRollingBeta(backtest, live) + +## Test GetRollingSharpeRatioPlot +data = list(np.random.uniform(1, 3, 365 * 2)) +time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2012-10-01 00:00:00', periods=365 * 2)] +backtest = [time, data] + +data = list(np.random.uniform(1, 3, 365)) +time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2014-10-01 00:00:00', periods=365)] +live = [time, data] + +empty = [[], []] +result = charts.GetRollingSharpeRatio(empty, empty) +result = charts.GetRollingSharpeRatio(backtest, empty) +result = charts.GetRollingSharpeRatio(backtest, live) + +## Test GetAssetAllocationPlot +backtest = [['SPY', 'IBM', 'NFLX', 'AAPL'], [0.50, 0.25, 0.125, 0.125]] +live = [['SPY', 'IBM', 'AAPL'], [0.4, 0.4, 0.2]] +empty = [[], []] +result = charts.GetAssetAlloction(empty, empty) +result = charts.GetAssetAlloction(backtest, empty) +result = charts.GetAssetAlloction(backtest, live) + +## Test GetLeveragePlot +backtest = [[pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2014-10-01', periods=365)], + list(np.random.uniform(0.5, 1.5, 365))] +live = [[pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2015-10-01', periods=100)], + list(np.random.uniform(0.5, 2, 100))] +empty = [[], []] +result = charts.GetLeverage(empty, empty) +result = charts.GetLeverage(backtest, empty) +result = charts.GetLeverage(backtest, live) + +## Test GetExposurePlot +time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2014-10-01', periods=365)] +long_securities = ['Equity'] +short_securities = ['Forex'] +long = [np.random.uniform(0, 0.5, 365)] +short = [np.random.uniform(-0.5, 0, 365)] + +live_time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2015-10-01', periods=100)] +live_long = [np.random.uniform(0, 0.5, 100)] +live_short = [np.random.uniform(-0.5, -0, 100)] +live_long_securities = ['Equity'] +live_short_securities = ['Forex'] + +result = charts.GetExposure() +result = charts.GetExposure(time, long_securities = long_securities, long_data=long) +result = charts.GetExposure(time, short_securities = short_securities, short_data=short) +result = charts.GetExposure(time, long_securities, short_securities, long, short) +result = charts.GetExposure(time, long_securities, short_securities, long, short, + live_time, live_long_securities, live_short_securities, + live_long, live_short) diff --git a/Report/ReportCharts.py b/Report/ReportCharts.py new file mode 100644 index 000000000000..9dc6f44d5eb3 --- /dev/null +++ b/Report/ReportCharts.py @@ -0,0 +1,684 @@ +# 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. + +import re +import matplotlib +import numpy as np +import pandas as pd +from base64 import b64encode +from datetime import date, datetime, timedelta +from pandas.plotting import register_matplotlib_converters +register_matplotlib_converters() + +matplotlib.use('Agg') +font = {'family': 'DejaVu Sans'} +matplotlib.rc('font',**font) +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker +import matplotlib.colors as mcolors +from matplotlib.dates import DateFormatter +from matplotlib.ticker import MaxNLocator, NullFormatter, ScalarFormatter, FormatStrFormatter +la = matplotlib.font_manager.FontManager() +lu = matplotlib.font_manager.FontProperties(family = "Open Sans Condensed") + +class ReportCharts: + + def fig_to_base64(self, filename = '', fig = None, dpi = 200): + base64 = 'data:image/png;base64,' + if fig is not None: + fig.savefig(filename, dpi=dpi, bbox_inches='tight') + with open(filename, "rb") as fp: + base64 += b64encode(fp.read()).decode('utf-8').replace('\n', '') + return base64 + + def GetReturnsPerTrade(self, returns_per_trade = [], live_returns_per_trade = [], + name = "returns-per-trade.png", width = 7, height = 5, + live_color = "#ff9914", backtest_color = "#71c3fc"): + + if len(returns_per_trade) == 0: + fig = plt.figure() + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + if len(live_returns_per_trade) > 0: + width = 11.5 + height = 5 + plt.figure() + fig, ax = plt.subplots(1, 2, tight_layout=True) + ax[0].hist(returns_per_trade, bins=75, color=backtest_color) + ax[1].hist(live_returns_per_trade, bins=25, color=live_color) + for i in range(2): + if i == 0: + ax[i].set_ylabel('Backtest', fontweight='demibold') + ax[i].axvline(x=np.median(returns_per_trade), color="red", ls="dashed", label="median", linewidth=0.5) + else: + ax[i].set_ylabel('Live', fontweight='demibold') + ax[i].axvline(x=np.median(live_returns_per_trade), color="red", ls="dashed", label="median", + linewidth=0.5) + ax[i].tick_params(labelsize=8) + ax[i].spines['right'].set_visible(False) + ax[i].spines['top'].set_visible(False) + else: + fig = plt.figure() + plt.hist(returns_per_trade, bins=75, color=backtest_color) + plt.xticks(fontsize=8) + plt.gca().spines['right'].set_visible(False) + plt.gca().spines['top'].set_visible(False) + plt.gca().axvline(x=np.median(returns_per_trade), color="red", ls="dashed", label="median", linewidth=0.5) + plt.ylabel('') + + plt.xlabel('') + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + def GetCumulativeReturns(self, data = [[],[],[]], live_data = [[],[],[]], + name = "cumulative-return.png", width = 11.5, height = 2.5, live_color = "#ff9914", + backtest_color = "#71c3fc", gray = "#b3bcc0"): + if len(data[0]) == 0: + fig = plt.figure() + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + plt.figure() + ax = plt.gca() + labels = ['Backtest', 'Benchmark'] + rectangles = [] + colors = [backtest_color, gray] + values = [data[1], data[2]] + for i, array in enumerate(values): + ax.plot(data[0], array, linewidth = 0.5, color = colors[i]) + rectangles.append(plt.Rectangle((0, 0), 1, 1, fc=colors[i])) + + if len(live_data[0]) > 0: + colors =[live_color, gray] + labels.append('Live') + values = [live_data[1], live_data[2]] + for i, array in enumerate(values): + ax.plot(live_data[0], array, linewidth=0.5, color=colors[i]) + rectangles.append(plt.Rectangle((0, 0), 1, 1, fc=colors[i])) + + ax.legend(rectangles, labels, handlelength=0.8, handleheight=0.8, + frameon=False, fontsize=8, ncol=len(labels)) + fig = ax.get_figure() + plt.xticks(rotation=0, ha='center', fontsize=8) + plt.yticks(fontsize=8) + ax.xaxis.set_major_formatter(DateFormatter("%b %Y")) + ax.yaxis.set_major_formatter(FormatStrFormatter('%.0f')) + ax.yaxis.set_major_locator(MaxNLocator(6)) + plt.axhline(y=0, color='#d5d5d5', zorder=1) + plt.setp(ax.spines.values(), color='#d5d5d5') + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') + plt.ylabel("") + plt.xlabel("") + ax.yaxis.grid(True, color="#ececec") + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + def GetDailyReturns(self, returns = [[],[]], live_returns = [[],[]], + name = "daily-returns.png", width = 11.5, height = 2.5, + live_color = "#ff9914", backtest_color = "#71c3fc", gray = "#b3bcc0"): + if len(returns[0]) == 0: + fig = plt.figure() + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + plt.figure() + ax = plt.gca() + + # Backtest + ax.bar(returns[0][:min(len(returns[0]),len(returns[1]))], returns[1], color = backtest_color, zorder = 2) + # Live + ax.bar(live_returns[0][:min(len(live_returns[0]),len(live_returns[1]))], live_returns[1], color=live_color,zorder=2) + + # Need to handle this since we don't use a legend if it is only backtesting + if len(live_returns[0]) > 0: + rectangles = [plt.Rectangle((0, 0), 1, 1, fc=backtest_color), plt.Rectangle((0, 0), 1, 1, fc=live_color)] + ax.legend(rectangles, [label for label in ['Backtest', "Live"]], handlelength=0.8, handleheight=0.8, + frameon=False, fontsize=8) + + fig = ax.get_figure() + ax.xaxis_date() + plt.xticks(rotation = 0,ha = 'center', fontsize = 8) + plt.yticks(fontsize = 8) + plt.ylabel("") + plt.xlabel("") + ax.xaxis.set_major_formatter(DateFormatter("%b %Y")) + plt.axhline(y = 0, color = '#d5d5d5') + plt.setp(ax.spines.values(), color='#d5d5d5') + plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + ax.set_axisbelow(True) + ax.yaxis.grid(True, color = "#ececec") + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + return base64 + + def GetMonthlyReturns(self, returns = {}, live_returns = {}): + live_returns = {} + width = 7 + height = 5 + name = "monthly-returns.png" + months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + print(returns) + + returns = dict(returns) + + + if len(returns) == 0: + print("No monthly returns found") + fig = plt.figure() + fig.set_size_inches(width, height) + base64 = fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + # Make data frame + returns = pd.DataFrame(returns, index = months).transpose() + + def make_colormap(seq): + seq = [(None,) * 3, 0.0] + list(seq) + [1.0, (None,) * 3] + cdict = {'red': [], 'green': [], 'blue': []} + for i, item in enumerate(seq): + if isinstance(item, float): + r1, g1, b1 = seq[i - 1] + r2, g2, b2 = seq[i + 1] + cdict['red'].append([item, r1, r2]) + cdict['green'].append([item, g1, g2]) + cdict['blue'].append([item, b1, b2]) + return mcolors.LinearSegmentedColormap('CustomMap', cdict) + + c = mcolors.ColorConverter().to_rgb + c_map = make_colormap([c('#CC0000'), 0.1, c('#FF0000'), 0.2, c('#FF3333'), + 0.3, c('#FF9933'), 0.4, c('#FFFF66'), 0.5, c('#FFFF99'), + 0.6, c('#B2FF66'), 0.7, c('#99FF33'), 0.8, + c('#00FF00'), 0.9, c('#00CC00')]) + + if len(live_returns) > 0: + live_returns = pd.DataFrame(live_returns, index=months).transpose() + liveC = mcolors.ColorConverter().to_rgb + live_c_map = make_colormap([liveC('#CC0000'), 0.1, liveC('#FF0000'), 0.2, c('#FF3333'), + 0.3, liveC('#FF9933'), 0.4, liveC('#FFFF66'), 0.5, liveC('#FFFF99'), + 0.6, liveC('#B2FF66'), 0.7, liveC('#99FF33'), 0.8, + liveC('#00FF00'), 0.9, liveC('#00CC00')]) + + fig, ax = plt.subplots(2, 1, gridspec_kw={'height_ratios': [6, 1]}) + ax[0].matshow(returns, aspect='auto', cmap=c_map, interpolation='none', vmin=-10, vmax=10) + ax[1].matshow(live_returns, aspect='auto', cmap=live_c_map, interpolation='none') + ax[0].xaxis.set_major_locator(ticker.MaxNLocator(min(12, len(returns.columns)))) + ax[0].yaxis.set_major_locator(ticker.MaxNLocator(len(returns.index.values))) + ax[0].set_yticklabels([''] + list(returns.index.values)) + ax[0].set_xticklabels([''] + [x for x in returns.columns]) + ax[0].tick_params(labelsize=8, bottom=True, labelbottom=True, top=False, labeltop=False) + ax[0].set_ylabel('Backtest', rotation='vertical', fontweight='black') + for (j, i), label in np.ndenumerate(returns): + if j == 0: + ax[0].text(i, j + 0.1, round(label, 1), ha='center', va='top', fontsize=7) + elif j == (returns.shape[0] - 1): + ax[0].text(i, j - 0.1, round(label, 1), ha='center', va='bottom', fontsize=7) + else: + ax[0].text(i, j, round(label, 1), ha='center', va='center', fontsize=7) + + ax[1].xaxis.set_major_locator(ticker.MaxNLocator(min(12, len(live_returns.columns)))) + ax[1].yaxis.set_major_locator(ticker.MaxNLocator(len(live_returns.index.values))) + ax[1].set_xticklabels([''] + [x for x in live_returns.columns]) ## will need to be fixed for more than 1 year + ax[1].set_yticklabels([''] + list(live_returns.index.values)) + ax[1].tick_params(labelsize=8, bottom=True, labelbottom=True, top=False, labeltop=False) + ax[1].set_ylabel('Live', rotation='vertical', fontweight='black') + for (j, i), label in np.ndenumerate(live_returns): + ax[1].text(i, j, round(label, 1), ha='center', va='center', fontsize=7) + + else: + ax = plt.imshow(returns, aspect='auto', cmap=c_map, interpolation='none', vmin=-10, vmax=10) + fig = ax.get_figure() + plt.xlabel('') + plt.ylabel('') + plt.yticks(range(len(returns.index.values)), returns.index.values, fontsize=8) + plt.xticks(range(12), ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]) + for (j, i), label in np.ndenumerate(returns): + if j == 0: + plt.text(i, j + 0.1, round(label, 1), ha='center', va='top', fontsize=7) + elif j == (returns.shape[0] - 1): + plt.text(i, j - 0.1, round(label, 1), ha='center', va='bottom', fontsize=7) + else: + plt.text(i, j, round(label, 1), ha='center', va='center', fontsize=7) + + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + def GetAnnualReturns(self, data = [[],[]], live_data = [[],[]], name = "annual-returns.png",width = 3.5*2, height = 2.5*2): + if len(data[0]) == 0: + fig = plt.figure() + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + time = data[0] + live_data[0] + returns = data[1] + live_data[1] + + plt.figure() + ax = plt.gca() + ax.barh(time, returns, color = ["#428BCA"]) + fig = ax.get_figure() + plt.xticks(rotation=0, ha='center', fontsize=8) + plt.yticks(fontsize=8) + plt.axvline(x=0, color='#d5d5d5', linewidth=0.5) + vline = plt.axvline(x=np.mean(returns), color="red", ls="dashed", label="mean", linewidth=0.5) + plt.legend([vline], ["mean"], loc='upper right', frameon=False, fontsize=8) + plt.setp(ax.spines.values(), color='#d5d5d5') + plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + plt.xlabel("") + plt.ylabel("") + ax.xaxis.grid(True) + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + def GetDrawdown(self, data = [[],[]], live_data = [[],[]], worst = {}, name = "drawdowns.png", + width = 11.5, height = 2.5, gray = "#b3bcc0"): + if (len(data[0]) == 0) or (len(worst) == 0): + fig = plt.figure() + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + time = data[0] + drawdown = data[1] + colors = ["#FFCCCCCC", "#FFE5CCCC", "#FFFFCCCC", "#E5FFCCCC", "#CCFFCCCC"] + labels = ["1st Worst", "2nd Worst", "3rd Worst", "4th Worst", "5th Worst"] + plt.figure() + ax = plt.gca() + ax.xaxis.set_major_formatter(DateFormatter("%b %Y")) + + # Backtest + ax.plot(time, drawdown, color=gray, zorder=2) + ax.fill_between(time, drawdown, 0, color=gray, zorder=3) + for index, values in enumerate(worst): + start = values['Begin'] + end = values['End'] + if values['Begin'] == values['End']: + worst_point = values['Begin'] + else: + sub_data = drawdown[time.index(values['Begin']):time.index(values['End'])] + worst_point = time[drawdown.index(min(sub_data))] + plt.axvspan(start, end, 0, 0.95, color = colors[index], zorder = 1) + plt.axvline(worst_point, 0, 0.95, ls = 'dashed', color = 'black', zorder = 4, linewidth = 0.5) + plt.text(worst_point, min(drawdown) * 0.75, labels[index], rotation = 90, zorder = 4) + + # Live + live_time = live_data[0] + live_drawdown = live_data[1] + ax.plot(live_time[:min(len(live_drawdown), len(live_time))], live_drawdown, color = gray, zorder = 2) + ax.fill_between(live_time[:min(len(live_drawdown), len(live_time))], live_drawdown, 0, color = gray, zorder = 3) + plt.axvline(live_time[0], 0, 0.95, ls='dotted', color='red', zorder=4) if len(live_time) > 0 else None + plt.text(live_time[0], min(min(drawdown), min(live_drawdown)) * 0.75, "Live Trading", rotation=90, zorder=4, fontsize=7) if len(live_time) > 0 else None + + fig = ax.get_figure() + plt.xticks(rotation=0, ha='center', fontsize=8) + plt.yticks(fontsize=8) + plt.ylabel("") + plt.xlabel("") + plt.axhline(y=0, color='#d5d5d5', zorder=1) + plt.setp(ax.spines.values(), color='#d5d5d5') + plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + ax.yaxis.grid(True, color="#ececec") + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + def GetCrisisEventsPlots(self, data = [[],[],[]], name = '', width = 7, height = 5, + backtest_color = "#71c3fc", gray = "#b3bcc0"): + if len(data[0]) == 0: + fig = plt.figure() + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(f'{name}.png', fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + plt.figure() + ax = plt.gca() + fig = ax.get_figure() + ax.xaxis.set_major_formatter(DateFormatter("%Y-%m-%d")) + colors = [backtest_color, gray] + for j, values in enumerate(data[1:]): + ax.plot(data[0][:min(len(data[0]),len(values))], values, color=colors[j], linewidth=0.5, zorder=2) + labels = ['Backtest', 'Benchmark'] + rectangles = [plt.Rectangle((0, 0), 1, 1, fc=backtest_color), plt.Rectangle((0, 0), 1, 1, fc=gray)] + leg = ax.legend(rectangles, labels, handlelength=0.8, handleheight=0.8, + frameon=False, fontsize=8, ncol=len(labels)) + for line in leg.get_lines(): line.set_linewidth(3) + plt.axhline(y=0, color= gray, zorder=1) + plt.setp(ax.spines.values(), color='#d5d5d5') + ax.tick_params(axis='x', labelsize=8, labelrotation=45) + plt.yticks(fontsize=8) + plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + plt.xlabel("") + plt.ylabel("") + ax.yaxis.grid(True, color="#ececec") + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(f'{name}.png', fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + def GetRollingBeta(self, data = [[],[],[]], live_data = [[],[],[]], name = "rolling-portfolio-beta-to-equity.png", + width = 11.5, height = 2.5): + if len(data[0]) == 0: + fig = plt.figure() + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + + labels = ['6 mo.', '12 mo.'] + rectangles = [plt.Rectangle((0, 0), 1, 1, fc="#71c3fc"), plt.Rectangle((0, 0), 1, 1, fc="#1d7dc1")] + if len(live_data[0]) > 0: + labels += ['Live 6 mo.', 'Live 12 mo.'] + rectangles += [plt.Rectangle((0, 0), 1, 1, fc="#ff9914"), plt.Rectangle((0, 0), 1, 1, fc="#ffd700")] + + plt.figure() + ax = plt.gca() + fig = ax.get_figure() + ax.xaxis.set_major_formatter(DateFormatter("%b %Y")) + + # Backtest + ax.plot(data[0][:min(len(data[0]),len(data[1]))], data[1], linewidth = 0.5, color = "#71c3fc") + ax.plot(data[0][:min(len(data[0]),len(data[2]))], data[2], linewidth = 0.5, color = "#1d7dc1") + + # Live + ax.plot(live_data[0][:min(len(live_data[0]), len(live_data[1]))], live_data[1], linewidth=0.5, color="#ff9914") + ax.plot(live_data[0][:min(len(live_data[0]), len(live_data[2]))], live_data[2], linewidth=0.5, color="#ffd700") + + leg = ax.legend(rectangles, labels, handlelength=0.8, handleheight=0.8, + frameon=False, fontsize=8, ncol=2) + for line in leg.get_lines(): line.set_linewidth(3) + plt.axhline(y=0, color='#d5d5d5', zorder=1) + plt.setp(ax.spines.values(), color='#d5d5d5') + ax.tick_params(axis='both', labelsize=8, labelrotation=0) + plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') + plt.xlabel("") + plt.ylabel("") + ax.set_axisbelow(True) + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + ax.yaxis.grid(True, color="#ececec") + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + def GetRollingSharpeRatio(self, data = [[],[]], live_data = [[],[]], name = "rolling-sharpe-ratio.png", + width = 11.5, height = 2.5, live_color = "#ff9914", backtest_color = "#71c3fc"): + if len(data[0]) == 0: + fig = plt.figure() + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + labels = ['6 mo.'] + rectangles = [plt.Rectangle((0, 0), 1, 1, fc=backtest_color)] + if len(live_data[0]) > 0: # Need to handle this... + rectangles += [plt.Rectangle((0, 0,), 1, 1, fc=live_color)] + labels += ["Live 6 mo."] + + plt.figure() + ax = plt.gca() + ax.xaxis.set_major_formatter(DateFormatter("%b %Y")) + + # Backtest + ax.plot(data[0][:min(len(data[0]),len(data[1]))], data[1], color = backtest_color, linewidth = 0.5, zorder = 2) + + # Live + ax.plot(live_data[0][:min(len(live_data[0]), len(live_data[1]))], live_data[1], color=live_color, linewidth=0.5, zorder=2) + + leg = ax.legend(rectangles, labels, handlelength=0.8, handleheight=0.8, + frameon=False, fontsize=8) + for line in leg.get_lines(): line.set_linewidth(3) + plt.axhline(y=0, color='#d5d5d5', zorder=1) + plt.setp(ax.spines.values(), color='#d5d5d5') + ax.tick_params(axis='both', labelsize=8, labelrotation=0) + plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') + plt.ylabel("") + plt.xlabel("") + ax.set_axisbelow(True) + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + ax.yaxis.grid(True, color="#ececec") + fig = ax.get_figure() + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + def GetAssetAlloction(self, data = [[],[]], live_data = [[],[]], + width = 7, height = 5): + if len(data[0]) == 0: + fig = plt.figure() + fig.set_size_inches(width, height) + base64 = self.fig_to_base64("asset-allocation.png", fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + symbols = [data[0], live_data[0]] + data = [data[1], live_data[1]] + colors = ['#fce0bd', '#fcd6a7', '#fbcd92', '#fac37c', '#f8af53', '#f79b31', '#de8b2c', "#dde1e3"] + pies = {} + + for i in range(len(data)): + symbols_to_use, to_label = symbols[i], data[i] + + # No need to plot if there are no symbols/data -- necessary as we don't want to return a dictionary + # with even a blank plot for live if only using a backtest + if len(symbols_to_use) == 0: + continue + + to_label = to_label[:7] + symbols_to_use = symbols_to_use[:7] + if sum(to_label) < 1: + to_label.append(1 - sum(to_label)) + symbols_to_use.append('Others') + + labels = [f'{symbol}\n{value*100}%' for symbol, value in zip(symbols_to_use, to_label)] + + fig = plt.figure() + plt.pie(to_label, colors = colors) + plt.legend(labels, frameon = False, fontsize = 8, loc = 'center left', bbox_to_anchor=(0, 0.5)) + plt.axis('equal') + fig.set_size_inches(width, height) + if i == 0: + pies.update({"Backtest Asset Allocation": self.fig_to_base64(f"asset-allocation-backtest.png", fig)}) + else: + pies.update({"Live Asset Allocation": self.fig_to_base64(f"asset-allocation-live.png", fig)}) + plt.cla() + plt.clf() + plt.close('all') + return pies + + def GetLeverage(self, data = [[],[]], live_data = [[],[]], name = "leverage.png",width = 11.5, + height = 2.5, backtest_color = "#71c3fc", live_color = "#ff9914",): + if len(data[0]) == 0: + fig = plt.figure() + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + plt.figure() + ax = plt.gca() + fig = ax.get_figure() + + # Backtest + ax.plot(data[0][:min(len(data[0]),len(data[1]))], data[1], color = backtest_color, alpha = 0.75) + ax.fill_between(data[0][:min(len(data[0]),len(data[1]))], 0, data[1], color = backtest_color, alpha = 0.75) + + # Live + ax.plot(live_data[0][:min(len(live_data[0]),len(live_data[1]))], live_data[1], color = live_color, alpha = 0.75) + ax.fill_between(live_data[0][:min(len(live_data[0]),len(live_data[1]))], 0, live_data[1], color=live_color, alpha=0.75, step = 'pre') + + rectangles = [plt.Rectangle((0, 0), 1, 1, fc=backtest_color), plt.Rectangle((0, 0), 1, 1, fc=live_color)] + ax.legend(rectangles, [label for label in ['Backtest', "Live"]], handlelength=0.8, handleheight=0.8, + frameon=False, fontsize=8) + ax.set_xticklabels(ax.get_xticklabels(), rotation=0, ha='center') + ax.tick_params(axis='both', labelsize=8, labelrotation=0) + ax.xaxis.set_major_formatter(DateFormatter("%b %Y")) + plt.axhline(y=0, color='#d5d5d5') + plt.setp(ax.spines.values(), color='#d5d5d5') + plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') + plt.ylabel("") + plt.xlabel("") + ax.set_axisbelow(True) + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + ax.yaxis.grid(True, color="#ececec") + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + def GetExposure(self, time = [], long_securities = [], short_securities = [], long_data = [[]], short_data = [[]], + live_time = [], live_long_securities = [], live_short_securities = [], live_long_data = [[]], + live_short_data = [[]], name = "exposure.png", width = 11.5, height = 2.5): + if len(time) == 0: + fig = plt.figure() + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 + + color_map = {'Equity': "#71c3fc", 'Option':'#A0522D', 'Commodity':'#4B0082', + 'Forex':'#0000FF', 'Future':'#6B8E23', 'Cfd':'#FF8C00', 'Crypto':'#BDB76B'} + live_color_map = {'Equity': "#ff9914" , 'Option': '#DAA520', 'Commodity': '#9400D3', + 'Forex':'#6495ED', 'Future':'#808000', 'Cfd':'#FFD700', 'Crypto':'#FFDAB9'} + labels = long_securities + short_securities + live_labels = live_long_securities + live_short_securities + + ax = plt.gca() + + # No need to check if live is empty or not, this will handle it, just needs to plot whichever has the longer time index first + if max([len(x) for x in long_data]) > max([len(x) for x in short_data]): + ax.stackplot(time[:max([len(x) for x in long_data])], np.vstack(long_data), + color = [color_map[security] for security in long_securities], alpha = 0.75) + ax.stackplot(time[:max([len(x) for x in short_data])], np.vstack(short_data), + color=[color_map[security] for security in short_securities], alpha=0.75) + else: + ax.stackplot(time[:max([len(x) for x in short_data])], np.vstack(short_data), + color=[color_map[security] for security in short_securities], alpha=0.75) + ax.stackplot(time[:max([len(x) for x in long_data])], np.vstack(long_data), + color=[color_map[security] for security in long_securities], alpha=0.75) + + if max([len(x) for x in live_long_data]) > max([len(x) for x in live_short_data]): + ax.stackplot(live_time[:max([len(x) for x in live_long_data])], np.vstack(live_long_data), + color=[live_color_map[security] for security in live_long_securities], alpha = 0.75) + ax.stackplot(live_time[:max([len(x) for x in live_short_data])], np.vstack(live_short_data), + color=[live_color_map[security] for security in live_short_securities], alpha = 0.75) + else: + ax.stackplot(live_time[:max([len(x) for x in live_short_data])], np.vstack(live_short_data), + color=[live_color_map[security] for security in live_short_securities], alpha=0.75) + ax.stackplot(live_time[:max([len(x) for x in live_long_data])], np.vstack(live_long_data), + color=[live_color_map[security] for security in live_long_securities], alpha=0.75) + + labels = list(set(labels)) + live_labels = list(set(live_labels)) + rectangles = [plt.Rectangle((0, 0), 1, 1, fc=color_map[lab]) for lab in labels] + live_rectangles = [plt.Rectangle((0, 0), 1, 1, fc=live_color_map[lab]) for lab in live_labels] + ax.legend(rectangles + live_rectangles, labels + [f'{lab} - Live' for lab in live_labels], handlelength=0.8, + handleheight=0.8, frameon=False, fontsize=8, ncol=len(labels), loc='upper right') + fig = ax.get_figure() + plt.xticks(rotation = 0,ha = 'center', fontsize = 8) + plt.yticks(fontsize = 8) + plt.xlabel("") + ax.axhline(y=0, color = 'black', linewidth = 0.5) + ax.xaxis.set_major_formatter(DateFormatter("%b %Y")) + plt.setp(ax.spines.values(), color='#d5d5d5') + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + plt.setp([ax.get_xticklines(), ax.get_yticklines()], color='#d5d5d5') + plt.ylabel("") + plt.xlabel("") + ax.set_axisbelow(True) + ax.yaxis.grid(True, color = "#ececec") + fig.set_size_inches(width, height) + base64 = self.fig_to_base64(name, fig) + plt.cla() + plt.clf() + plt.close('all') + return base64 \ No newline at end of file diff --git a/Report/ReportElements/CAGRReportElement.cs b/Report/ReportElements/CAGRReportElement.cs new file mode 100644 index 000000000000..a16567a6a6cd --- /dev/null +++ b/Report/ReportElements/CAGRReportElement.cs @@ -0,0 +1,48 @@ +/* + * 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.Packets; + +namespace QuantConnect.Report.ReportElements +{ + internal sealed class CAGRReportElement : ReportElement + { + private LiveResult _live; + private BacktestResult _backtest; + + /// + /// Estimate the CAGR of the strategy. + /// + /// Name of the widget + /// Location of injection + /// Backtest result object + /// Live result object + public CAGRReportElement(string name, string key, BacktestResult backtest, LiveResult live) + { + _live = live; + _backtest = backtest; + Name = name; + Key = key; + } + + /// + /// The generated output string to be injected + /// + public override string Render() + { + return _backtest.TotalPerformance.PortfolioStatistics.CompoundingAnnualReturn.ToString("P1"); + } + } +} \ No newline at end of file diff --git a/Report/ReportElements/ChartReportElement.cs b/Report/ReportElements/ChartReportElement.cs new file mode 100644 index 000000000000..5798fbac8741 --- /dev/null +++ b/Report/ReportElements/ChartReportElement.cs @@ -0,0 +1,71 @@ +/* + * 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.Runtime.CompilerServices; +using Python.Runtime; +using QuantConnect.Python; + + +namespace QuantConnect.Report.ReportElements +{ + internal abstract class ChartReportElement : ReportElement + { + internal static dynamic Charting; + + /// + /// Charting base class report element + /// + protected ChartReportElement() + { + PythonInitializer.Initialize(); + + using (Py.GIL()) + { + dynamic module = PythonEngine.ImportModule("ReportCharts"); + + var classObj = module.ReportCharts; + + Charting = classObj.Invoke(); + } + } + + public PyConverter DictionaryConverter() + { + using (Py.GIL()) + { + var converter = new PyConverter(); //create a instance of PyConverter + converter.AddListType(); + converter.Add(new StringType()); + converter.Add(new Int64Type()); + converter.Add(new Int32Type()); + converter.Add(new FloatType()); + converter.Add(new PyListType(DoubleListConverter())); + converter.AddDictType(); + return converter; + } + } + + public PyConverter DoubleListConverter() + { + using (Py.GIL()) + { + var converter = new PyConverter(); + converter.AddListType(); + converter.Add(new DoubleType()); + return converter; + } + } + } +} \ No newline at end of file diff --git a/Report/ReportElements/CrisisReportElement.cs b/Report/ReportElements/CrisisReportElement.cs new file mode 100644 index 000000000000..4e4d1f13d104 --- /dev/null +++ b/Report/ReportElements/CrisisReportElement.cs @@ -0,0 +1,51 @@ +/* + * 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.IO; +using QuantConnect.Packets; + +namespace QuantConnect.Report.ReportElements +{ + internal sealed class CrisisReportElement : ReportElement + { + private LiveResult _live; + private BacktestResult _backtest; + private string _template; + + /// + /// Create a new array of crisis event plots + /// + /// Name of the widget + /// Location of injection + /// Backtest result object + /// Live result object + public CrisisReportElement(string name, string key, BacktestResult backtest, LiveResult live) + { + _live = live; + _backtest = backtest; + _template = File.ReadAllText("template.crisis.html"); + Name = name; + Key = key; + } + + /// + /// The generated output string to be injected + /// + public override string Render() + { + return ""; + } + } +} \ No newline at end of file diff --git a/Report/ReportElements/EstimatedCapacityReportElement.cs b/Report/ReportElements/EstimatedCapacityReportElement.cs new file mode 100644 index 000000000000..80397a686150 --- /dev/null +++ b/Report/ReportElements/EstimatedCapacityReportElement.cs @@ -0,0 +1,69 @@ +/* + * 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.IO; +using System.Linq; +using QuantConnect.Packets; + +namespace QuantConnect.Report.ReportElements +{ + internal sealed class EstimatedCapacityReportElement : ReportElement + { + private LiveResult _live; + private BacktestResult _backtest; + + /// + /// Create a new capacity estimate + /// + /// Name of the widget + /// Location of injection + /// Backtest result object + /// Live result object + public EstimatedCapacityReportElement(string name, string key, BacktestResult backtest, LiveResult live) + { + _live = live; + _backtest = backtest; + Name = name; + Key = key; + } + + /// + /// The generated output string to be injected + /// + public override string Render() + { + var unit = ""; + var capacity = _backtest.Orders.Values.Sum(x => x.Value); + + if (capacity > 1e9m) + { + unit = "B"; + capacity /= 1e9m; + } + else if (capacity > 1e6m) + { + unit = "M"; + capacity /= 1e6m; + } + else if (capacity > 1e4m) + { + unit = "K"; + capacity /= 1e3m; + } + + return $"{capacity:C0}{unit}"; + } + } +} \ No newline at end of file diff --git a/Report/ReportElements/IReportElement.cs b/Report/ReportElements/IReportElement.cs new file mode 100644 index 000000000000..35949a8e2432 --- /dev/null +++ b/Report/ReportElements/IReportElement.cs @@ -0,0 +1,37 @@ +/* + * 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.Report.ReportElements +{ + /// + /// Common interface for template elements of the report + /// + internal interface IReportElement + { + /// + /// Name of this report element + /// + string Name { get; } + + /// + /// Template key code. + /// + string Key { get; } + + /// + /// The generated output string to be injected + /// + string Render(); + } +} \ No newline at end of file diff --git a/Report/ReportElements/InformationRatioReportElement.cs b/Report/ReportElements/InformationRatioReportElement.cs new file mode 100644 index 000000000000..6985761f61c3 --- /dev/null +++ b/Report/ReportElements/InformationRatioReportElement.cs @@ -0,0 +1,48 @@ +/* + * 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.Packets; + +namespace QuantConnect.Report.ReportElements +{ + internal sealed class InformationRatioReportElement : ReportElement + { + private LiveResult _live; + private BacktestResult _backtest; + + /// + /// Estimate the information ratio of the strategy. + /// + /// Name of the widget + /// Location of injection + /// Backtest result object + /// Live result object + public InformationRatioReportElement(string name, string key, BacktestResult backtest, LiveResult live) + { + _live = live; + _backtest = backtest; + Name = name; + Key = key; + } + + /// + /// The generated output string to be injected + /// + public override string Render() + { + return _backtest.TotalPerformance.PortfolioStatistics.InformationRatio.ToString("F1"); + } + } +} \ No newline at end of file diff --git a/Report/ReportElements/KellyEstimateReportElement.cs b/Report/ReportElements/KellyEstimateReportElement.cs new file mode 100644 index 000000000000..5ccfaa572956 --- /dev/null +++ b/Report/ReportElements/KellyEstimateReportElement.cs @@ -0,0 +1,49 @@ +/* + * 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.Packets; + +namespace QuantConnect.Report.ReportElements +{ + internal sealed class KellyEstimateReportElement : ReportElement + { + private LiveResult _live; + private BacktestResult _backtest; + + /// + /// Estimate the kelly estimate of the strategy. + /// + /// Name of the widget + /// Location of injection + /// Backtest result object + /// Live result object + public KellyEstimateReportElement(string name, string key, BacktestResult backtest, LiveResult live) + { + _live = live; + _backtest = backtest; + Name = name; + Key = key; + } + + /// + /// The generated output string to be injected + /// + public override string Render() + { + var kelly = _backtest.AlphaRuntimeStatistics.KellyCriterionEstimate; + return $"{kelly:F1}"; + } + } +} \ No newline at end of file diff --git a/Report/ReportElements/MarketsReportElement.cs b/Report/ReportElements/MarketsReportElement.cs new file mode 100644 index 000000000000..cad77092c762 --- /dev/null +++ b/Report/ReportElements/MarketsReportElement.cs @@ -0,0 +1,54 @@ +/* + * 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 MoreLinq; +using System.Linq; +using System.Runtime; +using QuantConnect.Packets; + +namespace QuantConnect.Report.ReportElements +{ + internal sealed class MarketsReportElement : ReportElement + { + private LiveResult _live; + private BacktestResult _backtest; + + /// + /// Get the markets of the strategy. + /// + /// Name of the widget + /// Location of injection + /// Backtest result object + /// Live result object + public MarketsReportElement(string name, string key, BacktestResult backtest, LiveResult live) + { + _live = live; + _backtest = backtest; + Name = name; + Key = key; + } + + /// + /// The generated output string to be injected + /// + public override string Render() + { + var orders = _backtest.Orders.Values.Union(_live.Orders.Values); + + var securityTypes = orders.DistinctBy(o => o.SecurityType).Select(s => s.SecurityType.ToString()).ToList(); + + return string.Join(",", securityTypes); + } + } +} \ No newline at end of file diff --git a/Report/ReportElements/MaxDrawdownReportElement.cs b/Report/ReportElements/MaxDrawdownReportElement.cs new file mode 100644 index 000000000000..284295566444 --- /dev/null +++ b/Report/ReportElements/MaxDrawdownReportElement.cs @@ -0,0 +1,48 @@ +/* + * 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.Packets; + +namespace QuantConnect.Report.ReportElements +{ + internal sealed class MaxDrawdownReportElement : ReportElement + { + private LiveResult _live; + private BacktestResult _backtest; + + /// + /// Estimate the max drawdown of the strategy. + /// + /// Name of the widget + /// Location of injection + /// Backtest result object + /// Live result object + public MaxDrawdownReportElement(string name, string key, BacktestResult backtest, LiveResult live) + { + _live = live; + _backtest = backtest; + Name = name; + Key = key; + } + + /// + /// The generated output string to be injected + /// + public override string Render() + { + return _backtest.TotalPerformance.PortfolioStatistics.Drawdown.ToString("P1"); + } + } +} \ No newline at end of file diff --git a/Report/ReportElements/MonthlyReturnsReportElement.cs b/Report/ReportElements/MonthlyReturnsReportElement.cs new file mode 100644 index 000000000000..3aa535d8e10d --- /dev/null +++ b/Report/ReportElements/MonthlyReturnsReportElement.cs @@ -0,0 +1,80 @@ +/* + * 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.IO; +using System.Linq; +using Python.Runtime; +using QuantConnect.Packets; + +namespace QuantConnect.Report.ReportElements +{ + internal sealed class MonthlyReturnsReportElement : ChartReportElement + { + private LiveResult _live; + private BacktestResult _backtest; + + /// + /// Create a new array of crisis event plots + /// + /// Name of the widget + /// Location of injection + /// Backtest result object + /// Live result object + public MonthlyReturnsReportElement(string name, string key, BacktestResult backtest, LiveResult live) + { + _live = live; + _backtest = backtest; + Name = name; + Key = key; + } + + /// + /// Generate the monthly returns plot using the python libraries. + /// + public override string Render() + { + var result = new Dictionary>(); + var backtestReturns = EquityReturns(EquityPoints(_backtest)); + + var returnsByMonth = backtestReturns.Select(day => new {day.Key.Year, day.Key.Month, day.Value}).GroupBy( + y => new {y.Year, y.Month}, + (key, group) => new + { + Year = key.Year.ToString(), + Month = key.Month, + Returns = group.Sum(day => day.Value) + }); + + foreach (var a in returnsByMonth) + { + if (!result.ContainsKey(a.Year)) + { + result.Add(a.Year, new List()); + } + result[a.Year].Add(a.Returns); + } + + var base64 = ""; + using (Py.GIL()) + { + base64 = Charting.GetMonthlyReturns(result); + } + + return base64; + } + } +} \ No newline at end of file diff --git a/Report/ReportElements/PSRReportElement.cs b/Report/ReportElements/PSRReportElement.cs new file mode 100644 index 000000000000..c1f361fe3c5c --- /dev/null +++ b/Report/ReportElements/PSRReportElement.cs @@ -0,0 +1,55 @@ +/* + * 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.Packets; + +namespace QuantConnect.Report.ReportElements +{ + internal sealed class PSRReportElement : ReportElement + { + private LiveResult _live; + private BacktestResult _backtest; + + /// + /// Estimate the PSR of the strategy. + /// + /// Name of the widget + /// Location of injection + /// Backtest result object + /// Live result object + public PSRReportElement(string name, string key, BacktestResult backtest, LiveResult live) + { + _live = live; + _backtest = backtest; + Name = name; + Key = key; + } + + /// + /// The generated output string to be injected + /// + public override string Render() + { + var psr = _backtest.TotalPerformance.PortfolioStatistics.ProbabilisticSharpeRatio; + + if (psr > 0) + { + return $"{psr:P0}"; + } + + return "-"; + } + } +} \ No newline at end of file diff --git a/Report/ReportElements/ReportElement.cs b/Report/ReportElements/ReportElement.cs new file mode 100644 index 000000000000..cb9401f9b42b --- /dev/null +++ b/Report/ReportElements/ReportElement.cs @@ -0,0 +1,83 @@ +/* + * 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.Packets; + +namespace QuantConnect.Report.ReportElements +{ + /// + /// Common interface for template elements of the report + /// + internal abstract class ReportElement : IReportElement + { + /// + /// Name of this report element + /// + public virtual string Name { get; protected set; } + + /// + /// Template key code. + /// + public virtual string Key { get; protected set; } + + /// + /// The generated output string to be injected + /// + public abstract string Render(); + + /// + /// Get the equity chart points + /// + /// Result object to extract the chart points + /// + public SortedList EquityPoints(Result result) + { + var points = new SortedList(); + + foreach (var point in result.Charts["Strategy Equity"].Series["Equity"].Values) + { + points[Time.UnixTimeStampToDateTime(point.x)] = Convert.ToDouble(point.y); + } + + return points; + } + + /// + /// Convert cumulative return to daily returns percentage + /// + /// + /// + public SortedList EquityReturns(SortedList chart) + { + var returns = new SortedList(); + double previous = 0; + foreach (var point in chart) + { + if (returns.Count == 0) + { + returns.Add(point.Key, 0); + previous = point.Value; + continue; + } + + var delta = (point.Value / previous) - 1; + returns.Add(point.Key, delta); + } + return returns; + } + } +} \ No newline at end of file diff --git a/Report/ReportElements/SharpeRatioReportElement.cs b/Report/ReportElements/SharpeRatioReportElement.cs new file mode 100644 index 000000000000..2bc2276c8533 --- /dev/null +++ b/Report/ReportElements/SharpeRatioReportElement.cs @@ -0,0 +1,48 @@ +/* + * 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.Packets; + +namespace QuantConnect.Report.ReportElements +{ + internal sealed class SharpeRatioReportElement : ReportElement + { + private LiveResult _live; + private BacktestResult _backtest; + + /// + /// Estimate the sharpe ratio of the strategy. + /// + /// Name of the widget + /// Location of injection + /// Backtest result object + /// Live result object + public SharpeRatioReportElement(string name, string key, BacktestResult backtest, LiveResult live) + { + _live = live; + _backtest = backtest; + Name = name; + Key = key; + } + + /// + /// The generated output string to be injected + /// + public override string Render() + { + return _backtest.TotalPerformance.PortfolioStatistics.SharpeRatio.ToString("F1"); + } + } +} \ No newline at end of file diff --git a/Report/ReportElements/TextReportElement.cs b/Report/ReportElements/TextReportElement.cs new file mode 100644 index 000000000000..9a203a101d6b --- /dev/null +++ b/Report/ReportElements/TextReportElement.cs @@ -0,0 +1,44 @@ +/* + * 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.Report.ReportElements +{ + internal sealed class TextReportElement : ReportElement + { + private readonly string _content; + + /// + /// Text place holder report element + /// + /// Name of this text field + /// Report injection point + /// Content for injection + public TextReportElement(string name, string key, string content) + { + Name = name; + Key = key; + _content = content; + } + + /// + /// Render the element contents + /// + /// + public override string Render() + { + return _content; + } + } +} \ No newline at end of file diff --git a/Report/ReportElements/TradesPerDayReportElement.cs b/Report/ReportElements/TradesPerDayReportElement.cs new file mode 100644 index 000000000000..62955a10021a --- /dev/null +++ b/Report/ReportElements/TradesPerDayReportElement.cs @@ -0,0 +1,62 @@ +/* + * 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.Linq; +using QuantConnect.Packets; + +namespace QuantConnect.Report.ReportElements +{ + internal sealed class TradesPerDayReportElement : ReportElement + { + private LiveResult _live; + private BacktestResult _backtest; + + /// + /// Estimate the trades per day of the strategy. + /// + /// Name of the widget + /// Location of injection + /// Backtest result object + /// Live result object + public TradesPerDayReportElement(string name, string key, BacktestResult backtest, LiveResult live) + { + _live = live; + _backtest = backtest; + Name = name; + Key = key; + } + + /// + /// Generate trades per day + /// + public override string Render() + { + var orders = _backtest.Orders.Values.Union(_live.Orders.Values); + + var equity = EquityPoints(_backtest).Select(x => x.Value); + + var days = 1; + + var tradesPerDay = orders.Count() / days; + + if (tradesPerDay > 9) + { + return $"{tradesPerDay:F0}"; + } + + return $"{tradesPerDay:F1}"; + } + } +} \ No newline at end of file diff --git a/Report/ReportElements/TurnoverReportElement.cs b/Report/ReportElements/TurnoverReportElement.cs new file mode 100644 index 000000000000..92ab55fa5038 --- /dev/null +++ b/Report/ReportElements/TurnoverReportElement.cs @@ -0,0 +1,50 @@ +/* + * 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.Packets; + +namespace QuantConnect.Report.ReportElements +{ + internal sealed class TurnoverReportElement : ReportElement + { + private LiveResult _live; + private BacktestResult _backtest; + + /// + /// Estimate the turnover of the strategy. + /// + /// Name of the widget + /// Location of injection + /// Backtest result object + /// Live result object + public TurnoverReportElement(string name, string key, BacktestResult backtest, LiveResult live) + { + _live = live; + _backtest = backtest; + Name = name; + Key = key; + } + + /// + /// The generated output string to be injected + /// + public override string Render() + { + var turnover = _backtest.AlphaRuntimeStatistics.PortfolioTurnover; + + return $"{turnover:P0}"; + } + } +} \ No newline at end of file diff --git a/Report/ReportKey.cs b/Report/ReportKey.cs new file mode 100644 index 000000000000..7d292dfaeaf7 --- /dev/null +++ b/Report/ReportKey.cs @@ -0,0 +1,51 @@ +/* + * 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.Report +{ + /// + /// Helper shortcuts for report injection points. + /// + internal static class ReportKey + { + public const string StrategyName = "{{$TEXT-STRATEGY-NAME}}"; + public const string StrategyDescription = "{{$TEXT-STRATEGY-DESCRIPTION}}"; + public const string StrategyVersion = "{{$TEXT-STRATEGY-VERSION}}"; + + public const string CAGR = "{{$KPI-CAGR}}"; + public const string Turnover = "{{$KPI-TURNOVER}}"; + public const string MaxDrawdown = "{{$KPI-DRAWDOWN}}"; + public const string KellyEstimate = "{{$KPI-KELLY-ESTIMATE}}"; + public const string SharpeRatio = "{{$KPI-SHARPE}}"; + public const string EstimatedCapacity = "{{$KPI-CAPACITY}}"; + public const string InformationRatio = "{{$KPI-INFORMATION-RATIO}}"; + public const string TradesPerDay = "{{$KPI-TRADES-PER-DAY}}"; + public const string Markets = "{{$KPI-MARKETS}}"; + public const string PSR = "{{$KPI-PSR}}"; + + public const string MonthlyReturns = "{{$PLOT-MONTHLY-RETURNS}}"; + public const string CumulativeReturns = "{{$PLOT-CUMULATIVE-RETURNS}}"; + public const string AnnualReturns = "{{$PLOT-ANNUAL-RETURNS}}"; + public const string ReturnsPerTrade = "{{$PLOT-RETURNS-PER-TRADE}}"; + public const string AssetAllocation = "{{$PLOT-ASSET-ALLOCATION}}"; + public const string Drawdown = "{{$PLOT-DRAWDOWN}}"; + public const string DailyReturns = "{{$PLOT-DAILY-RETURNS}}"; + public const string RollingBeta = "{{$PLOT-BETA}}"; + public const string RollingSharpe = "{{$PLOT-SHARPE}}"; + public const string LeverageUtilization = "{{$PLOT-LEVERAGE}}"; + public const string Exposure = "{{$PLOT-EXPOSURE}}"; + public const string CrisisPlots = "{{$HTML-CRISIS-PLOTS}}"; + } +} diff --git a/Report/config.json b/Report/config.json new file mode 100644 index 000000000000..01942b08ecae --- /dev/null +++ b/Report/config.json @@ -0,0 +1,11 @@ +{ + "pdf-configuration-path": "C:\\Program Files\\wkhtmltopdf\\bin\\wkhtmltopdf.exe", + "data-folder": "C:/Server/Lean/Data", + "strategy-name": "Fixed Income Model", + "strategy-version": "v1.0.2", + "strategy-description": "This is a relative value trade. It is looking to move money continuously into the most attractive global equity and fixed income assets. The strategy is long only and makes daily predictions. The algorithm decides how much exposure to take on to each asset class and can be totally risk on or risk off on a given day. The capacity of the trade is around 50mm. We have been running a different version of this strategy for around 1.5 years. The returns from the live have closely matched the expected returns from the backtests. We are looking to license the strategy at a 2% annual fee for the capacity. The raw version of the alpha is posted and can be traded as is or with a spy hedge to reduce monthly beta exposure.", + "live-data-source-file": "FixedIncomeModel-Live.json", + "backtest-data-source-file": "FixedIncomeModel.json", + "report-destination": "output.html", + "generate-pdf": true +} \ No newline at end of file diff --git a/Report/css/report.css b/Report/css/report.css new file mode 100644 index 000000000000..db8eba4c2a0b --- /dev/null +++ b/Report/css/report.css @@ -0,0 +1,531 @@ +body { + width: 100%; + margin: 0; + padding: 0; + font-size: 12px; + background-color: #666666; + box-shadow: #0A0A0A; + font-family: "Roboto"; +} + +.table.qc-table.compact thead > tr { + height: 1em; +} + +.page .content .text-center { + text-align: center; +} + +.page .content .text-left { + text-align: left; +} + +.page .content .text-right { + text-align: right; +} + +.page { + width: 993px; + height: 1405px; + overflow: hidden; + position: relative; + margin: 10px auto; + background-color: #fff; +} + + +.text-h1, +h1 { + font-size: 26px; + width: 100%; + font-family: 'Titillium Web', Helvetica, Arial, sans-serif !important; + font-weight: bold; +} + +.footer { + bottom: 0; + left: 0; + right: 0; + height: 80px; + position: absolute; + color: #222; + font-family: 'Titillium Web', Helvetica, Arial, sans-serif; + text-align: center; + line-height: 50px; +} + +.footer .footer-page { + position: absolute; + top: 0; + right: 0; + line-height: 50px; + margin-right: 40px; +} + +.footer .footer-id { + position: absolute; + top: 0; + left: 0; + line-height: 50px; + margin-left: 40px; +} + +.header { + position: absolute; + left: 0; + right: 0; + top: 0; + height: 120px; + padding-left: 40px; + padding-right: 40px; + color: #222; + font-family: 'Titillium Web', Helvetica, Arial, sans-serif; +} + +.header .header-left { + float: left; +} + +.header .header-left img { + height: 58px; + padding-top: 25px; +} + +.header .header-right { + margin-top: 40px; + line-height: 30px; + float: right; +} + +.page .content { + position: absolute; + left: 40px; + right: 40px; + top: 80px; + bottom: 80px; + border-top: 1px solid #222; + border-bottom: 1px solid #222; + padding-top: 6px; +} + +table.table { + width: 100%; +} + +.table.qc-table { + border-spacing: 0; + border-collapse: separate; + margin-bottom: 0; +} + + .table.qc-table thead th { + padding: 5px; + border-top: solid 1px #677080; + border-left: solid 1px #677080; + border-bottom: solid 1px #677080; + color: #ffffff; + font-family: 'Titillium Web', Helvetica, Arial, sans-serif; + font-weight: bold; + } + +.table.qc-table thead tr { + background-color: #677080; +} + +.table.qc-table thead th:last-child { + border-right: solid 1px #677080; +} + +.table.qc-table tbody tr td { + border-top: none; + border-bottom: solid 1px #e9edf1; + border-left: solid 1px #e9edf1; + padding: 15px; + margin: 0; + word-wrap: break-word; +} + +.table.qc-table.compact tbody tr td { + padding: 5px; +} + +.table.qc-table tbody tr { + background-color: #fff; +} + +.table.qc-table tbody tr:nth-child(odd) { + background-color: #f8f9fa; +} + +.table.qc-table tbody tr td:last-child { + border-right: solid 1px #e9edf1; +} + + .table.qc-table a, + .table.qc-table span, + .table.qc-table div, + .table.qc-table p { + font-family: 'Titillium Web', Helvetica, Arial, sans-serif; + font-weight: bold; + } + + .table.qc-table.table-itemized tbody tr td:first-child, + .table.qc-table.table-itemized tbody tr td:first-child a, + .table.qc-table.table-itemized tbody tr td:first-child span, + .table.qc-table.table-itemized tbody tr td:first-child div, + .table.qc-table.table-itemized tbody tr td:first-child p { + font-family: 'Titillium Web', Helvetica, Arial, sans-serif; + font-weight: bold; + } + +.table.qc-table a, +.table.qc-table a span, +.table.qc-table a div, +.table.qc-table a p { + color: #f5ae29; +} + +.table.v-top tr td { + vertical-align: top; +} + +.table.col-2 td { + width: 50% +} + +.table.col-3 td { + width: 33.3% +} + +.table.col-4 td { + width: 25% +} + +.table.col-5 td { + width: 20% +} + +.no-padding { + padding: 0 !important; +} + +.no-margin { + margin: 0 !important; +} + +.split-text { + -webkit-column-count: 2; + -moz-column-count: 2; + column-count: 2; +} + +.profile img { + max-height: 150px; + max-width: 100%; + width: auto; + display: block; + margin: auto auto 12px; +} + +.page .content .text-justify, +.page .content p { + text-align: justify; +} + +@media print { + body { + background-color: #ffffff; + } + + .hide-print { + display: none; + } + + .page { + border: none; + margin: 0; + } +} + +.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { + padding: 6px; +} + +.container-row { + width: 100%; + display: block; +} + +.container-row > div:last-child { + padding-right: 0; +} + +.container-row > div:first-child { + padding-left: 0; +} + +#table-summary .fa-times { + color: #d9534f; +} + +#author-metadata tr > td:first-child, +#project-metadata tr > td:first-child { + width: 66%; +} + +#project-metadata tr > td:last-child { + text-align: center; +} + +#key-statistics tr > td:last-child { + text-align: right; +} + +#key-statistics tr > td, +#key-characteristics tr > td, +table.table tr > td { + vertical-align: middle; +} + +table.table.align-top tr > td { + vertical-align: top; +} + +table.table { + height: 230px; +} + +table img { + width: 100%; + height: 100%; + max-height: 225px; +} + +.col-xs-12 table > tbody > tr > td { + text-align: center; +} + +table.table > thead > tr > th { + border-bottom: none; + font-weight: bold; + font-size: 18px; + padding-top: 15px; + font-family: 'Titillium Web', sans-serif; +} + +table.table > tbody > tr > td { + border-top: none; + font-size: 15px; + padding-top: 10px; + padding-bottom: 10px; +} + +table#key-characteristics { + width: calc(100% - 30px); +} + +table#key-characteristics > thead > tr > th { + font-size: 14px; + width: 12.5%; + text-align: center; +} + + table#key-characteristics > thead > tr > th.title { + font-size: 18px; + width: 25%; + text-align: left; + } + +table#key-characteristics > tbody > tr { + border-bottom: 1px solid #cbd1d4; +} + +table#key-characteristics > tbody > tr > td { + position: relative; + text-align: center; +} + + table#key-characteristics > tbody > tr > td.title { + text-align: left; + } + +table#key-characteristics > tbody > tr:first-child { + border-top: 1px solid #9c9c9c; +} + +table#key-characteristics > tbody > tr > td > span.markets { + background: #8f9ca3; + font-size: 11px; + color: #fff; + padding: 8px 14px; + border-radius: 4px; +} + +.col-xs-4:nth-child(2) table#key-characteristics > tbody > tr > td:last-child { + text-align: right; +} + +table#key-characteristics > tbody > tr > td:first-child { + border-top: #c3cace; +} + +table#description-box { + word-wrap: break-word; + min-height: 225px; +} + + table#description-box > thead > tr > th > p { + color: #f5ae29; + font-size: 24px; + } + + table#description-box > thead > tr > th > p > span { + font-weight: 100; + } + + table#description-box > thead > tr > th > p > span { + margin-right: 10px; + width: 1px; + height: 24px; + background: #f5ae29; + } + +.page { + width: 1200px; + height: 1698px; +} + + .page .content { + top: 80px; + left: 110px; + right: 110px; + border-top: 1px solid #888888; + border-bottom: none; + padding: 0; + } + + .page .header { + height: 80px; + left: 110px; + right: 110px; + padding: 0; + } + +.header .header-left img { + width: 230px; + height: auto; + padding-top: 0; + margin-top: 25px; +} + +.header .header-right { + font-family: 'Titillium Web', sans-serif; + font-weight: bold; + margin-top: 40px; + line-height: 23px; + float: right; + font-size: 18px; + max-width: 70%; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.container-row { + height: auto; + overflow: auto; + border-bottom: 1px solid #b8b8b8; +} + + .container-row.first-row .col-xs-4:last-child { + text-align: right; + } + + .container-row.first-row .col-xs-4:last-child p { + font-family: 'Titillium Web', sans-serif; + position: absolute; + transform: rotate(-90deg); + font-size: 12px; + } + + .container-row.first-row .col-xs-4:last-child p:first-child { + left: -13px; + top: 128px; + } + + .container-row.first-row .col-xs-4:last-child p:nth-child(2) { + bottom: 36px; + } + + .container-row.first-row .col-xs-4:last-child table img { + width: calc(100% - 25px); + } + + .container-row:empty { + border: none; + } + +span.checkmark, span.exmark { + border-radius: 50%; + position: absolute; + border: none; + top: 10px; + right: 36%; +} + + span.checkmark.half, span.exmark.half { + right: 44%; + } + +span.checkmark { + background-color: #46bd6a; + height: 20px; + width: 20px; +} + + span.checkmark:after { + content: ""; + position: absolute; + left: 8px; + top: 3px; + width: 5px; + height: 10px; + border: solid #fff; + border-width: 0 1px 1px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); + } + +span.exmark { + background-color: #bc4143; + color: white; + font-size: 12px; + padding: 4px 5px; + padding-top: 4px; + line-height: 1; +} + +p#strategy-description { + overflow: hidden; + max-height: 130px; + margin-bottom: 0; + word-break: break-word; +} + +.kpi-live { + display: none; +} + +@media print { + body { + margin: 0; + box-shadow: 0; + padding: 0; + background-color: #ffffff; + } + + .page { + margin: 0; + } +} \ No newline at end of file diff --git a/Report/packages.config b/Report/packages.config new file mode 100644 index 000000000000..bdddadf4b615 --- /dev/null +++ b/Report/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Report/template.crisis.html b/Report/template.crisis.html new file mode 100644 index 000000000000..90f0bcbac7a1 --- /dev/null +++ b/Report/template.crisis.html @@ -0,0 +1,16 @@ + + + + + {{$TEXT-CRISIS-TITLE}} + + + + + + + + + + + \ No newline at end of file diff --git a/Report/template.html b/Report/template.html new file mode 100644 index 000000000000..98a9243b3665 --- /dev/null +++ b/Report/template.html @@ -0,0 +1,303 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Strategy Report: {{$TEXT-STRATEGY-NAME}} {{$TEXT-STRATEGY-VERSION}} + + + + + + + + + + | Strategy Description + + + + + + + + + {{$TEXT-STRATEGY-DESCRIPTION}} + + + + + + + + + + + + + Key StatisticsBacktestLiveBacktestLive + + + + + Estimated Capacity{{$KPI-CAPACITY}}{{$KPI-LIVE-CAPACITY}} + CAGR{{$KPI-CAGR}}{{$KPI-LIVE-CAGR}} + + + Turnover{{$KPI-TURNOVER}}{{$KPI-LIVE-TURNOVER}} + Drawdown{{$KPI-DRAWDOWN}}{{$KPI-LIVE-DRAWDOWN}} + + + Kelly Estimate{{$KPI-KELLY-ESTIMATE}}{{$KPI-LIVE-KELLY-ESTIMATE}} + Sharpe Ratio{{$KPI-SHARPE}}{{$KPI-LIVE-SHARPE}} + + + Probabilistic SR{{$KPI-PSR}}{{$KPI-LIVE-PSR}} + Information Ratio{{$KPI-INFORMATION-RATIO}}{{$KPI-LIVE-INFORMATION-RATIO}} + + + Markets {{$KPI-MARKETS}}{{$KPI-MARKETS}} + Trades Per Day{{$KPI-TRADES-PER-DAY}}{{$KPI-LIVE-TRADES-PER-DAY}} + + + + + + + + + Monthly Returns + + + + + + + + + + + + + + + + + + Cumulative Returns + + + + + + + + + + + + + + + + + + Annual Returns + + + + + + + + + + + + + + + + Returns Per Trade + + + + + + + + + + + + + + + + Asset Allocation + + + + + + + + + + + + + + + + + + Drawdown + + + + + + + + + + + + + + + + + + + + Strategy Report Summary: {{$TEXT-STRATEGY-NAME}} {{$TEXT-STRATEGY-VERSION}} + + + + + + + + Daily Returns + + + + + + + + + + + + + + + + + + Rolling Portfolio Beta to Equity + + + + + + + + + + + + + + + + + + Rolling Sharpe Ratio (6 Months) + + + + + + + + + + + + + + + + + + Leverage + + + + + + + + + + + + + + + + + + Exposure + + + + + + + + + + + + + + + + + + + + Strategy Report Summary: {{$TEXT-STRATEGY-NAME}} {{$TEXT-STRATEGY-VERSION}} + + + + {{$HTML-CRISIS-PLOTS}} + + + + + \ No newline at end of file
+ | Strategy Description +
+ {{$TEXT-STRATEGY-DESCRIPTION}} +