Lean  $LEAN_TAG$
Report.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
17 using System.Collections.Generic;
18 using System.IO;
19 using System.Linq;
20 using Deedle;
21 using Newtonsoft.Json;
23 using QuantConnect.Logging;
24 using QuantConnect.Packets;
26 using QuantConnect.Orders;
27 using System.Text.RegularExpressions;
28 
29 namespace QuantConnect.Report
30 {
31  /// <summary>
32  /// Report class
33  /// </summary>
34  public class Report
35  {
36  private string _template;
37  private readonly List<IReportElement> _elements;
38 
39  /// <summary>
40  /// File name for statistics
41  /// </summary>
42  public const string StatisticsFileName = "report-statistics.json";
43 
44  /// <summary>
45  /// Create beautiful HTML and PDF Reports based on backtest and live data.
46  /// </summary>
47  /// <param name="name">Name of the strategy</param>
48  /// <param name="description">Description of the strategy</param>
49  /// <param name="version">Version number of the strategy</param>
50  /// <param name="backtest">Backtest result object</param>
51  /// <param name="live">Live result object</param>
52  /// <param name="pointInTimePortfolioDestination">Point in time portfolio json output base filename</param>
53  /// <param name="cssOverride">CSS file that overrides some of the default rules defined in report.css</param>
54  /// <param name="htmlCustom">Custom HTML file to replace the default template</param>
55  public Report(string name, string description, string version, BacktestResult backtest, LiveResult live, string pointInTimePortfolioDestination = null, string cssOverride = null, string htmlCustom = null)
56  {
57  _template = htmlCustom ?? File.ReadAllText("template.html");
58  var crisisHtmlContent = GetRegexInInput(@"<!--crisis(\r|\n)*((\r|\n|.)*?)crisis-->", _template);
59  var parametersHtmlContent = GetRegexInInput(@"<!--parameters(\r|\n)*((\r|\n|.)*?)parameters-->", _template);
60 
61  var backtestCurve = new Series<DateTime, double>(ResultsUtil.EquityPoints(backtest));
62  var liveCurve = new Series<DateTime, double>(ResultsUtil.EquityPoints(live));
63 
64  var backtestOrders = backtest?.Orders?.Values.ToList() ?? new List<Order>();
65  var liveOrders = live?.Orders?.Values.ToList() ?? new List<Order>();
66 
67  var backtestConfiguration = backtest?.AlgorithmConfiguration;
68  var liveConfiguration = live?.AlgorithmConfiguration;
69 
70  // Earlier we use constant's value tradingDaysPerYear = 252
71  // backtestConfiguration?.TradingDaysPerYear equal liveConfiguration?.TradingDaysPerYear
72  var tradingDayPerYear = backtestConfiguration?.TradingDaysPerYear ?? 252;
73 
74  Log.Trace($"QuantConnect.Report.Report(): Processing backtesting orders");
75  var backtestPortfolioInTime = PortfolioLooper.FromOrders(backtestCurve, backtestOrders, backtestConfiguration).ToList();
76  Log.Trace($"QuantConnect.Report.Report(): Processing live orders");
77  var livePortfolioInTime = PortfolioLooper.FromOrders(liveCurve, liveOrders, liveConfiguration, liveSeries: true).ToList();
78 
79  var destination = pointInTimePortfolioDestination ?? Config.Get("report-destination");
80  if (!string.IsNullOrWhiteSpace(destination))
81  {
82  if (backtestPortfolioInTime.Count != 0)
83  {
84  var dailyBacktestPortfolioInTime = backtestPortfolioInTime
85  .Select(x => new PointInTimePortfolio(x, x.Time.Date).NoEmptyHoldings())
86  .GroupBy(x => x.Time.Date)
87  .Select(kvp => kvp.Last())
88  .OrderBy(x => x.Time)
89  .ToList();
90 
91  var outputFile = destination.Replace(".html", string.Empty) + "-backtesting-portfolio.json";
92  Log.Trace($"Report.Report(): Writing backtest point-in-time portfolios to JSON file: {outputFile}");
93  var backtestPortfolioOutput = JsonConvert.SerializeObject(dailyBacktestPortfolioInTime);
94  File.WriteAllText(outputFile, backtestPortfolioOutput);
95  }
96  if (livePortfolioInTime.Count != 0)
97  {
98  var dailyLivePortfolioInTime = livePortfolioInTime
99  .Select(x => new PointInTimePortfolio(x, x.Time.Date).NoEmptyHoldings())
100  .GroupBy(x => x.Time.Date)
101  .Select(kvp => kvp.Last())
102  .OrderBy(x => x.Time)
103  .ToList();
104 
105  var outputFile = destination.Replace(".html", string.Empty) + "-live-portfolio.json";
106  Log.Trace($"Report.Report(): Writing live point-in-time portfolios to JSON file: {outputFile}");
107  var livePortfolioOutput = JsonConvert.SerializeObject(dailyLivePortfolioInTime);
108  File.WriteAllText(outputFile, livePortfolioOutput);
109  }
110  }
111 
112  _elements = new List<IReportElement>
113  {
114  //Basics
115  new TextReportElement("strategy name", ReportKey.StrategyName, name),
116  new TextReportElement("description", ReportKey.StrategyDescription, description),
117  new TextReportElement("version", ReportKey.StrategyVersion, version),
118  new TextReportElement("stylesheet", ReportKey.Stylesheet, File.ReadAllText("css/report.css") + (cssOverride)),
119  new TextReportElement("live marker key", ReportKey.LiveMarker, live == null ? string.Empty : "Live "),
120 
121  //KPI's Backtest:
122  new RuntimeDaysReportElement("runtime days kpi", ReportKey.BacktestDays, backtest, live),
123  new CAGRReportElement("cagr kpi", ReportKey.CAGR, backtest, live),
124  new TurnoverReportElement("turnover kpi", ReportKey.Turnover, backtest, live),
125  new MaxDrawdownReportElement("max drawdown kpi", ReportKey.MaxDrawdown, backtest, live),
126  new SharpeRatioReportElement("sharpe kpi", ReportKey.SharpeRatio, backtest, live, tradingDayPerYear),
127  new SortinoRatioReportElement("sortino kpi", ReportKey.SortinoRatio, backtest, live, tradingDayPerYear),
128  new PSRReportElement("psr kpi", ReportKey.PSR, backtest, live, tradingDayPerYear),
129  new InformationRatioReportElement("ir kpi", ReportKey.InformationRatio, backtest, live),
130  new MarketsReportElement("markets kpi", ReportKey.Markets, backtest, live),
131  new TradesPerDayReportElement("trades per day kpi", ReportKey.TradesPerDay, backtest, live),
132  new EstimatedCapacityReportElement("estimated algorithm capacity", ReportKey.StrategyCapacity, backtest, live),
133 
134  // Generate and insert plots MonthlyReturnsReportElement
135  new MonthlyReturnsReportElement("monthly return plot", ReportKey.MonthlyReturns, backtest, live),
136  new CumulativeReturnsReportElement("cumulative returns", ReportKey.CumulativeReturns, backtest, live),
137  new AnnualReturnsReportElement("annual returns", ReportKey.AnnualReturns, backtest, live),
138  new ReturnsPerTradeReportElement("returns per trade", ReportKey.ReturnsPerTrade, backtest, live),
139  new AssetAllocationReportElement("asset allocation over time pie chart", ReportKey.AssetAllocation, backtest, live, backtestPortfolioInTime, livePortfolioInTime),
140  new DrawdownReportElement("drawdown plot", ReportKey.Drawdown, backtest, live),
141  new DailyReturnsReportElement("daily returns plot", ReportKey.DailyReturns, backtest, live),
142  new RollingPortfolioBetaReportElement("rolling beta to equities plot", ReportKey.RollingBeta, backtest, live, tradingDayPerYear),
143  new RollingSharpeReportElement("rolling sharpe ratio plot", ReportKey.RollingSharpe, backtest, live, tradingDayPerYear),
144  new LeverageUtilizationReportElement("leverage plot", ReportKey.LeverageUtilization, backtest, live, backtestPortfolioInTime, livePortfolioInTime),
145  new ExposureReportElement("exposure plot", ReportKey.Exposure, backtest, live, backtestPortfolioInTime, livePortfolioInTime)
146  };
147 
148  // Include Algorithm Parameters
149  if (parametersHtmlContent != null)
150  {
151  _elements.Add(new ParametersReportElement("parameters page", ReportKey.ParametersPageStyle, backtestConfiguration, liveConfiguration, parametersHtmlContent));
152  _elements.Add(new ParametersReportElement("parameters", ReportKey.Parameters, backtestConfiguration, liveConfiguration, parametersHtmlContent));
153  }
154 
155  // Array of Crisis Plots:
156  if (crisisHtmlContent != null)
157  {
158  _elements.Add(new CrisisReportElement("crisis page", ReportKey.CrisisPageStyle, backtest, live, crisisHtmlContent));
159  _elements.Add(new CrisisReportElement("crisis plots", ReportKey.CrisisPlots, backtest, live, crisisHtmlContent));
160  }
161 
162  }
163 
164  /// <summary>
165  /// Compile the backtest data into a report
166  /// </summary>
167  /// <returns></returns>
168  public void Compile(out string html, out string reportStatistics)
169  {
170  html = _template;
171  var statistics = new Dictionary<string, object>();
172 
173  // Render the output and replace the report section
174  foreach (var element in _elements)
175  {
176  Log.Trace($"QuantConnect.Report.Compile(): Rendering {element.Name}...");
177  html = html.Replace(element.Key, element.Render());
178 
179  if (element is TextReportElement || element is CrisisReportElement || element is ParametersReportElement ||(element as ReportElement) == null)
180  {
181  continue;
182  }
183 
184  var reportElement = element as ReportElement;
185  statistics[reportElement.JsonKey] = reportElement.Result;
186  }
187 
188  reportStatistics = JsonConvert.SerializeObject(statistics, Formatting.None);
189  }
190 
191  /// <summary>
192  /// Gets the regex pattern in the given input string
193  /// </summary>
194  /// <param name="pattern">Regex pattern to be find the input string</param>
195  /// <param name="input">Input string that may contain the regex pattern</param>
196  /// <returns>The regex pattern in the input string if found. Otherwise, null</returns>
197  public static string GetRegexInInput(string pattern, string input)
198  {
199  var regex = new Regex(pattern);
200  var match = regex.Match(input);
201  var regexWithinInput = match.Success ? match.Groups[2].Value : null;
202  return regexWithinInput;
203  }
204  }
205 }