I’ve been thinking about the topic for the next series of blog posts, and after a bit of deliberation I’ve decided to create a multi-part series of articles with a walk through of how to create a customisable HTML trading strategy report generator.
That is, once all is done and dusted all that will be required is to create a csv file with your trading strategy equity curve data in one column, and an (optional) benchmark equity series in a second column, place it in a particular folder, click a couple of buttons and “Hey Presto!” out will pop an HTML file which can be rendered in your browser and will contain all sorts of charts, statistics and analysis on your particular strategy performance.
Before we get down to any actual performance analysis and calculation of relevant stats etc, we first need to create a quick “skeleton” report which will contain all the necessary files, modules and logic to generate the most basic of HTML output files, using a simple “placeholder” variable to make sure things are working.
I know at this stage what I am saying may not make much sense, but all will become clear shortly.
Firstly we need to create the necessary folder structure along with some files which we will be using as we go along.
Project
|
+– main.py
|
+– templates
| |
| +– template.html
|
+– data
| |
| +– data.csv
|
+– output
|
+– report.html
|
+– static
|
+– app.css
|
+– app.js
To begin with, just leave the files empty; we can come back to them in due course and make any necessary additions for each stage of this project build.
To try to shed a bit of light on how we are going to go about things, the moving parts will come together as follows:
- The main component of the whole project will be a “PerformanceReport” class that we will build and store in the “main.py” file.
- This PerformanceReport class will leverage the power of jinja2 and it’s “templating” ability so that we can calculate and create all of our charts and stats in the body of the class, and then “inject” those objects as variables into an HTML template.
- The HTML template that we will inject our objects into will be the “template.html” file located in the “templates” folder.
- Jinja2 will be used to convert all of our objects and template into one long html string, which will then be written across into our “report.html” file located in the “output” folder. This “report.html” file will then be ready to open and render in any modern browser for us to inspect our results.
This initial stage is purely about making sure we have the skeleton code correctly in place so that we can successfully instantiate a instance of the PerformanceReport class, set a dummy variable that we will inject into the HTML template and then convert it all into a final output as a report.html file.
Let us begin with the “main.py” file – add the following code to the contents of the file:
import os from jinja2 import Environment, FileSystemLoader class PerformanceReport: """ Report with performance stats for given strategy returns. """ def __init__(self): pass def generate_html(self): env = Environment(loader=FileSystemLoader('.')) template = env.get_template("templates/template.html") placeholder = 'Hello World' html_out = template.render(test_variable=placeholder) return html_out def generate_html_report(self): """ Returns HTML report with analysis """ html = self.generate_html() outputdir="output" outfile = os.path.join(outputdir, 'report.html') file = open(outfile,"w") file.write(html) file.close() if __name__ == "__main__": report = PerformanceReport() report.generate_html_report()
One of the most important points to note regarding the above code is that we are defining a variable “placeholder” and setting it equal to the value of the string “Hello World”. We are then passing this variable as an argument to the “template.render()” function, and referencing it as “test_variable”. The power of Jinja2 now allows us to access that variable directly from the HTML template into which it hasbeen injected.
So to see how we do that, let’s now move on to the “template.html” file in the “templates” folder and add the following:
We can see that the “test_variable” has been accessed and included in the output of the HTML by enclosing the variable name in 2 curly braces either side. This will now be able to access the initial “placeholder” variable we defined in the “PerformanceReport” class, and in turn it’s value of “Hello World”.
<pre lang="html"> <!DOCTYPE HTML> <html> <head> <meta charset="utf-8" /> <title>App</title> <meta name= "viewport"content="width=device-width, initial-scale=1" /> <script charset="utf-8" src="static/app.js"></script> <link rel="stylesheet" href="static/app.css" /> </head> <body> <h1>Trading Backtest Report</h1> <p>{{ test_variable }}</p> </body> </html> </pre>
If the above sections of code have been correctly added, all that’s left to do at this point is to run the “main.py” file and then check our “output” folder for the resulting “report.html” file.
If successful, on opening “report.html” in our browser, we should see the following:

Great stuff – so now we know our basic skeleton framework is working properly! But the above output isn’t exactly particularly interesting, so let’s get down to creating the charts and data that will populate our final report.
So let us now add the code that will allow the “PerformanceReport” class to read in the csv file from the “data” folder, parse it and store it as the relevant strategy equity curve variable, or the benchmark equity curve variable.
The code for our “main.py” file should now be as follows:
import os import pandas as pd from jinja2 import Environment, FileSystemLoader class PerformanceReport: """ Report with performance stats for given strategy returns. """ def __init__(self,infilename): self.infilename = infilename self.get_data() def get_data(self): basedir = os.path.abspath(os.path.dirname('__file__')) data_folder = os.path.join(basedir, 'data') data = pd.read_csv(os.path.join(data_folder, self.infilename),index_col='date', parse_dates=True,dayfirst=True) self.equity_curve = data['equity_curve'] if len(data.columns) > 1: self.benchmark_curve = data['benchmark'] def generate_html(self): env = Environment(loader=FileSystemLoader('.')) template = env.get_template("templates/template.html") equity_curve = self.equity_curve html_out = template.render(equity_curve=equity_curve) return html_out def generate_html_report(self): """ Returns HTML report with analysis """ html = self.generate_html() outputdir="output" outfile = os.path.join(outputdir, 'report.html') file = open(outfile,"w") file.write(html) file.close() if __name__ == "__main__": report = PerformanceReport('data.csv') report.generate_html_report()
The main changes to the code above are the addition of the “get_data()” method which read in and stores our csv data, the addition of the “infilename” argument to the “PerformanceReport” class, and the injection of the “equity_curve” variable into the HTML file, rather than our “placeholder” variable that we used earlier.
We also of course need to actually populate our csv file with some relevant data in the correct format. You can download the data used in this example by clicking this link.
And our “template.html” should look like the below:
<meta charset="utf-8"> <title>App</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <script charset="utf-8" src="static/app.js"></script> <link rel="stylesheet" href="static/app.css"> <h1>Trading Backtest Report</h1> <p>{{ equity_curve }}</p>
The only change to the above code is the variable name has changed to “equity_curve” between the curly braces – this is to correctly reference the variable that has been injected into the template in the “template.render()” function.
When we now run “main.py” and open the “report.html” file in our browser, we should see something similar to this:

This is good as it means we have been able to correctly read in, store and then inject through into the HTML template the data comtained within the csv file; i.e. our strategy (and optional benchmark) equity curve. It still doesn’t look very visually appealing, i’ll agree….but it wasn’t supposed to be visually appealing, it was supposed to highlight how things are working behind the scenes a little bit and how to create and pass variables around from our class object, into our report file.
Right, let’s finally pass something of value to the report generation process! The code for the “main.py” file has had 2 methods added, namely “rebase_series” and “plot_performance_chart”. Some new imports have also been added to allow us to use Plotly charts.
The code inside the “generate_html” has of course also been altered to generate and pass through the correct variable that holds our performance chart.
The “template.html” file has experienced several changes too; some script links were added to be able to correctly render our Plotly charts, and some new row and column divs were added to create a bit of strucure for our page.
The code for both files should now be as below:
“main.py”
import os import pandas as pd import plotly import plotly.graph_objs as go from jinja2 import Environment, FileSystemLoader class PerformanceReport: """ Report with performance stats for given strategy returns. """ def __init__(self,infilename): self.infilename = infilename self.get_data() def get_data(self): basedir = os.path.abspath(os.path.dirname('__file__')) data_folder = os.path.join(basedir, 'data') data = pd.read_csv(os.path.join(data_folder, self.infilename),index_col='date', parse_dates=True,dayfirst=True) self.equity_curve = data['equity_curve'] if len(data.columns) > 1: self.benchmark_curve = data['benchmark'] def generate_html(self): env = Environment(loader=FileSystemLoader('.')) template = env.get_template("templates/template.html") perf_chart = self.plot_performance_chart() html_out = template.render(perf_chart=perf_chart) return html_out def generate_html_report(self): """ Returns HTML report with analysis """ html = self.generate_html() outputdir="output" outfile = os.path.join(outputdir, 'report.html') file = open(outfile,"w") file.write(html) file.close() def rebase_series(self,series): return (series/series.iloc[0]) * 100 def plot_performance_chart(self): #plotly combined equity chart trace_equity = go.Scatter( x=self.equity_curve.index.tolist(), y=self.rebase_series(self.equity_curve).values.tolist(), name='strategy', yaxis='y2', line = dict(color = ('rgb(22, 96, 167)'))) trace_benchmark = go.Scatter( x=self.benchmark_curve.index.tolist(), y=self.rebase_series(self.benchmark_curve).values.tolist(), name='benchmark', yaxis='y2', line = dict(color = ('rgb(22, 96, 0)'))) layout = go.Layout( autosize=True, legend=dict(orientation="h"), title='Performance Chart', yaxis=dict( title='Performance')) perf_chart = plotly.offline.plot({"data": [trace_equity,trace_benchmark], "layout": layout}, include_plotlyjs=False, output_type='div') return (perf_chart) if __name__ == "__main__": report = PerformanceReport('data.csv') report.generate_html_report()
and “template.html” as below:
<meta charset="utf-8"> <title>App</title> <!-- Bootstrap CSS CDN --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous"> <!-- Our Custom CSS --> <link rel="stylesheet" href="static/app.css"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Plotly JS --> <script src="https://cdn.plot.ly/plotly-latest.min.js"></script> <!-- Our Custom JS --> <script charset="utf-8" src="static/app.js"></script> <div class="container"> <h1>Trading Backtest Report</h1> <div class="row"> <div class="col-sm-6">{{ perf_chart|safe }}</div> <div class="col-sm-6"></div> </div> </div>
After running this latest code, the “report.html” should present us with the following in the browser:

We’re now starting to get somewhere – a nice, interactive chart showing relative performance of our strategy and relevant benchmark. Let’s round off this post by quickly adding another chart figure to the rigtht of the one we’ve just created, this time showing the drawdown series of the strategy equity curve and benchmark equity curve respectively.
I won’t go into great detail about the exact code additions made in the following updated files (below) – I shall leave the reader to look through at their leisure and figure out for themselves what exactly has changed, and what new methods have been created etc.
Reading through other people’s code in an attempt to work out exactly what is going on and how the logic and flow works is a great way to develop your own coding skills and abilities.
The updated “main.py” will be as below:
import os import pandas as pd import plotly import plotly.graph_objs as go import ffn from jinja2 import Environment, FileSystemLoader class PerformanceReport: """ Report with performance stats for given strategy returns. """ def __init__(self,infilename): self.infilename = infilename self.get_data() def get_data(self): basedir = os.path.abspath(os.path.dirname('__file__')) data_folder = os.path.join(basedir, 'data') data = pd.read_csv(os.path.join(data_folder, self.infilename),index_col='date', parse_dates=True,dayfirst=True) self.equity_curve = data['equity_curve'] if len(data.columns) > 1: self.benchmark_curve = data['benchmark'] def generate_html(self): env = Environment(loader=FileSystemLoader('.')) template = env.get_template("templates/template.html") perf_chart = self.plot_performance_chart() drawdown_chart = self.plot_drawdown_chart() html_out = template.render(perf_chart=perf_chart,drawdown_chart=drawdown_chart) return html_out def generate_html_report(self): """ Returns HTML report with analysis """ html = self.generate_html() outputdir="output" outfile = os.path.join(outputdir, 'report.html') file = open(outfile,"w") file.write(html) file.close() def rebase_series(self,series): return (series/series.iloc[0]) * 100 def plot_performance_chart(self): #plotly combined equity chart trace_equity = go.Scatter( x=self.equity_curve.index.tolist(), y=self.rebase_series(self.equity_curve).values.tolist(), name='strategy', yaxis='y2', line = dict(color = ('rgb(22, 96, 167)'))) trace_benchmark = go.Scatter( x=self.benchmark_curve.index.tolist(), y=self.rebase_series(self.benchmark_curve).values.tolist(), name='benchmark', yaxis='y2', line = dict(color = ('rgb(22, 96, 0)'))) layout = go.Layout( autosize=True, legend=dict(orientation="h"), title='Performance Chart', yaxis=dict( title='Performance')) perf_chart = plotly.offline.plot({"data": [trace_equity,trace_benchmark], "layout": layout}, include_plotlyjs=False, output_type='div') return (perf_chart) def plot_drawdown_chart(self): #plotly combined equity chart trace_equity_drawdown = go.Scatter( x=self.equity_curve.to_drawdown_series().index.tolist(), y=self.equity_curve.to_drawdown_series().values.tolist(), name='strategy drawdown', yaxis='y2', line = dict(color = ('rgb(22, 96, 167)'))) trace_benchmark_drawdown = go.Scatter( x=self.benchmark_curve.to_drawdown_series().index.tolist(), y=self.benchmark_curve.to_drawdown_series().values.tolist(), name='benchmark drawdown', yaxis='y2', line = dict(color = ('rgb(22, 96, 0)'))) layout = go.Layout( autosize=True, legend=dict(orientation="h"), title='Drawdown Chart', yaxis=dict( title='Drawdown')) drawdown_chart = plotly.offline.plot({"data": [trace_equity_drawdown,trace_benchmark_drawdown], "layout": layout}, include_plotlyjs=False, output_type='div') return (drawdown_chart) if __name__ == "__main__": report = PerformanceReport('data.csv') report.generate_html_report()
And the “template.html” will change to:
<meta charset="utf-8"> <title>App</title> <!-- Bootstrap CSS CDN --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous"> <!-- Our Custom CSS --> <link rel="stylesheet" href="static/app.css"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Plotly JS --> <script src="https://cdn.plot.ly/plotly-latest.min.js"></script> <!-- Our Custom JS --> <script charset="utf-8" src="static/app.js"></script> <div class="container"> <h1>Trading Backtest Report</h1> <div class="row"> <div class="col-sm-6">{{ perf_chart|safe }}</div> <div class="col-sm-6">{{ drawdown_chart|safe }}</div> </div> </div>
Your “report.html” should now display:

I’ll leave it there for now as the post is starting to get quite long. Next post will build on this one and we will begin to calculate some performance metrics and inject them through into some pretty looking tables in our report document.
Further down the line, we will look into adding some Monte-Carlo simulations and analysis of Value at Risk and the like.
Until next time….
5 comments
Interesting series, thank you. Is there any particular reason why you chose this way over the Bokeh package?
Hi there – mainly my choice of Plotly was down to familiarity with the package as I have used it more than any other of the available charting options (e.g. Highcharts, Bokeh etc) although I have used both of the other two mentioned before also.
I remember trying out Bokeh and it just didn’t feel particularly intuitive to me, then when I tried Plotly around the same time I just preferred it more….mainly due to the fact that it caused me fewer headaches and moments of complete frustration!!!
I’ve got nothing against Bokeh though, I might go back and take another look at it at some point to see if it was me that was the problem rather than Bokeh!!!
How about yourself? Do you have a preferred charting package you use? Any reasons behind the choice?
love your articles.but I can not download your sample data.csv. will you send me data file to my e-mail:purplemelan@hotmail.com.many thanks!
[…] own local files and recreate the folder and file structure outline in part 1 (which can be found here), then you should be ready to follow on from here pretty […]
Thank you very much for sharing this excellent series.
I had a problem installing the ffn module.
I read in the github that pandas removed some functions from dataframe.ix and caused problems for ffn.
https://github.com/pmorissette/ffn/issues/89
Do you have any update to keep it working or are you using another module currently?
I would love if you could share something similar that currently works so I can evaluate my trading strategies from a csv.