Home Basic Data Analysis Trading Strategy Performance Report in Python – Part 3

Trading Strategy Performance Report in Python – Part 3

by Stuart Jamieson

This is the third part of the current “mini-series” providing a walk-through of how to create a “Report Generation” tool to allow the creation and display of a performance report for our (backtest) strategy equity series/returns.

To recap, the way we left the code and report output at the end of the last blog post is shown below. The “main.py” file looked like this:

import os
import pandas as pd
import numpy as np
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()
        monthly_table = self.create_monthly_table(self.equity_curve.pct_change().dropna(),1)
        equity_curve_ffn_stats = self.get_ffn_stats(self.equity_curve)
        kpi_table = self.create_kpi_table(equity_curve_ffn_stats)
        html_out = template.render(perf_chart=perf_chart,drawdown_chart=drawdown_chart,monthly_table=monthly_table,
                                    kpi_table=kpi_table)
        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 drawdown 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)
    def create_monthly_table(self,return_series,num_of_compenents):
        return_series.rename('weighted rets',inplace=True)
        return_series = (return_series/ float(num_of_compenents))
        returns_df_m = pd.DataFrame((return_series + 1).resample('M').prod() - 1)
        returns_df_m['Month'] = returns_df_m.index.month
        monthly_table = returns_df_m[['weighted rets','Month']].pivot_table(returns_df_m[['weighted rets','Month']], index=returns_df_m.index, columns='Month', aggfunc=np.sum).resample('A')
        monthly_table = monthly_table.aggregate('sum')
        monthly_table.columns = monthly_table.columns.droplevel()
        #replace full date in index column with just the correspnding year
        monthly_table.index = monthly_table.index.year
        monthly_table['YTD'] = ((monthly_table + 1).prod(axis=1) - 1)
        monthly_table = monthly_table * 100
        #Replace integer column headings with MMM format
        monthly_table.columns = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec','YTD']
        return monthly_table.round(2).fillna("").to_html(classes="table table-hover table-bordered table-striped")
    
    def get_ffn_stats(self,equity_series):
        equity_stats = equity_series.calc_stats()
        d = dict(equity_stats.stats)
        return d
    def create_kpi_table(self,ffn_dict):
        kpi_table = pd.DataFrame.from_dict(ffn_dict,orient='index')
        kpi_table.index.name = 'KPI'
        kpi_table.columns = ['Value']
        kpi_table2 = kpi_table.loc[['total_return','cagr','daily_sharpe',
                                   'daily_vol','max_drawdown','avg_drawdown']]#.astype(float)
        kpi_table2['Value'] = pd.Series(["{0:.2f}%".format(val * 100) for val in kpi_table2['Value']], index = kpi_table2.index)
        kpi_table2.loc['avg_drawdown_days'] = kpi_table.loc['avg_drawdown_days'] 
        return kpi_table2.to_html(classes="table table-hover table-bordered table-striped",header=False)
if __name__ == "__main__":
    report = PerformanceReport('data.csv')
    report.generate_html_report()

And the”template.html” looked like this:

    <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>
<hr>
    <div class="row">
        <div class="col-sm-8">{{ perf_chart|safe }}</div>
        <div class="col-sm-4"><br><br>
            <h5>KPI Table</h5>
            {{ kpi_table|safe }}</div>
    </div>
<hr>
    <div class="row">
        <h5>Monthly Returns</h5>
        <div class="col-sm-12">{{ monthly_table|safe }}</div>
    </div>
<hr>
    <div class="row">
        <div class="col-sm-12">{{ drawdown_chart|safe }}</div>
    </div>
</div>

Which creates an HTML report file that renders as follows:

Now I’m going to add a few more KPI tables showing various stats and metrics for both our strategy and our benchmark. For the moment we are just going to do the necessary to get the tables created and injected into the report – we can come back to format them and make them look pretty at a later time, I don’t want to get bogged down in the minutia!

We are going to again leverage the power of the ffn package. It has a “calc_stats” method which is going to do the heavy lifting for us. We’ll create 2 new methods, “create_kpi_table_full” and “split_kpi_table”. The first method does as it says in the name and creates a full KPI table for both our strategy and our benchmark. the second method just splits up the table into 5 smaller tables purely for aesthetic reasons to make it look a bit nice and let our page content “flow” a bit more.

As always there have also been additions to the “generate_html” method to call new methods and create the necessary HTML elements we need to inject into our “template.html” file. This file has also had some changes made to it, as would be expected. The code incorporating these updates for both “main.py” and “template.html” can be seen below:

“main.py”

import os
import pandas as pd
import numpy as np
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()
        monthly_table = self.create_monthly_table(self.equity_curve.pct_change().dropna(),1)
        equity_curve_ffn_stats = self.get_ffn_stats(self.equity_curve)
        benchmark_curve_ffn_stats = self.get_ffn_stats(self.benchmark_curve)
        kpi_table = self.create_kpi_table(equity_curve_ffn_stats)
        kpi_table_full =  self.create_kpi_table_full([equity_curve_ffn_stats,benchmark_curve_ffn_stats])
        kpi_table_1,kpi_table_2,kpi_table_3,kpi_table_4,kpi_table_5 = self.split_kpi_table(kpi_table_full)
        html_out = template.render(perf_chart=perf_chart,drawdown_chart=drawdown_chart,monthly_table=monthly_table,
                                    kpi_table=kpi_table,kpi_table_1=kpi_table_1,kpi_table_2=kpi_table_2,
                                    kpi_table_3=kpi_table_3,kpi_table_4=kpi_table_4,kpi_table_5=kpi_table_5)
        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 drawdown 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)
    def create_monthly_table(self,return_series,num_of_compenents):
        return_series.rename('weighted rets',inplace=True)
        return_series = (return_series/ float(num_of_compenents))
        returns_df_m = pd.DataFrame((return_series + 1).resample('M').prod() - 1)
        returns_df_m['Month'] = returns_df_m.index.month
        monthly_table = returns_df_m[['weighted rets','Month']].pivot_table(returns_df_m[['weighted rets','Month']], index=returns_df_m.index, columns='Month', aggfunc=np.sum).resample('A')
        monthly_table = monthly_table.aggregate('sum')
        monthly_table.columns = monthly_table.columns.droplevel()
        #replace full date in index column with just the correspnding year
        monthly_table.index = monthly_table.index.year
        monthly_table['YTD'] = ((monthly_table + 1).prod(axis=1) - 1)
        monthly_table = monthly_table * 100
        #Replace integer column headings with MMM format
        monthly_table.columns = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec','YTD']
        return monthly_table.round(2).fillna("").to_html(classes="table table-hover table-bordered table-striped")
    
    def get_ffn_stats(self,equity_series):
        equity_stats = equity_series.calc_stats()
        d = dict(equity_stats.stats)
        return d
    def create_kpi_table(self,ffn_dict):
        kpi_table = pd.DataFrame.from_dict(ffn_dict,orient='index')
        kpi_table.index.name = 'KPI'
        kpi_table.columns = ['Value']
        kpi_table2 = kpi_table.loc[['total_return','cagr','daily_sharpe',
                                   'daily_vol','max_drawdown','avg_drawdown']]#.astype(float)
        kpi_table2['Value'] = pd.Series(["{0:.2f}%".format(val * 100) for val in kpi_table2['Value']], index = kpi_table2.index)
        kpi_table2.loc['avg_drawdown_days'] = kpi_table.loc['avg_drawdown_days'] 
        return kpi_table2.to_html(classes="table table-hover table-bordered table-striped",header=False)
    def create_kpi_table_full(self,ffn_dict_list):
        df_list =[pd.DataFrame.from_dict(x,orient='index') for x in ffn_dict_list]
        kpi_table_full = pd.concat(df_list,axis=1)
        return kpi_table_full
    def split_kpi_table(self,kpi_table_full):
        kpi_table_1 = kpi_table_full.iloc[3:16].to_html(classes="table table-hover table-bordered table-striped",header=False)
        kpi_table_2 = kpi_table_full.iloc[16:24].to_html(classes="table table-hover table-bordered table-striped",header=False)
        kpi_table_3 = kpi_table_full.iloc[24:32].to_html(classes="table table-hover table-bordered table-striped",header=False)
        kpi_table_4 = kpi_table_full.iloc[32:40].to_html(classes="table table-hover table-bordered table-striped",header=False)
        kpi_table_5 = kpi_table_full.iloc[40:].to_html(classes="table table-hover table-bordered table-striped",header=False)
        return kpi_table_1,kpi_table_2,kpi_table_3,kpi_table_4,kpi_table_5

if __name__ == "__main__":
    report = PerformanceReport('data.csv')
    report.generate_html_report()

And “template.html”:

    <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">
        <div class="row">
    <h1>Trading Backtest Report</h1>
    </div>
<hr>
    <div class="row">
        <div class="col-sm-8">{{ perf_chart|safe }}</div>
        <div class="col-sm-4"><br><br>
            <h5>KPI Table</h5>
            {{ kpi_table|safe }}</div>
    </div>
<hr>
    <div class="row"><h5>Monthly Returns</h5></div>
    <div class="row">
        <div class="col-sm-12">
                              {{ monthly_table|safe }}
        </div>
    </div>
<hr>
    <div class="row">
        <div class="col-sm-12">{{ drawdown_chart|safe }}</div>
    </div>
    <hr>
    <div class="row"><h5>KPI Tables</h5></div>
    <div class="row">
       
        <div class="col-sm-6"> 
                              {{ kpi_table_1|safe }}<br>
                              {{ kpi_table_5|safe }}
        </div>
        <div class="col-sm-6">{{ kpi_table_2|safe }}<br>
                              {{ kpi_table_3|safe }}<br>
                              {{ kpi_table_4|safe }}<br>
        </div>
    </div>
</div>

Now our report looks like this:

Let’s now add a histogram showing the distribution of daily returns of both our strategy and the benchmark series – we’ll overlay one over the other and plot them on the same axis. This will allow us to make a direct comparison more easily – we’ll make sure the size of the bins are the same size for both series to help make comparisons more meaningful.

I’ve added a “plot_daily_histogram” method to our “PerformanceReport” class as you will see in the updated code below – this creates the HTML object needed to inject the histogram into our HTML template. As ever, additions have been made to our “generate_html” method to call the new
“plot_daily_histogram” and pass it to our template.

The “template.html” file has had a new row and column div added containing the injected variable. The updated files are shown below:

“main.py”:

import os
import pandas as pd
import numpy as np
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()
        monthly_table = self.create_monthly_table(self.equity_curve.pct_change().dropna(),1)
        equity_curve_ffn_stats = self.get_ffn_stats(self.equity_curve)
        benchmark_curve_ffn_stats = self.get_ffn_stats(self.benchmark_curve)
        kpi_table = self.create_kpi_table(equity_curve_ffn_stats)
        kpi_table_full =  self.create_kpi_table_full([equity_curve_ffn_stats,benchmark_curve_ffn_stats])
        kpi_table_1,kpi_table_2,kpi_table_3,kpi_table_4,kpi_table_5 = self.split_kpi_table(kpi_table_full)
        daily_ret_hist = self.plot_daily_histogram()
        html_out = template.render(perf_chart=perf_chart,drawdown_chart=drawdown_chart,monthly_table=monthly_table,
                                    kpi_table=kpi_table,kpi_table_1=kpi_table_1,kpi_table_2=kpi_table_2,
                                    kpi_table_3=kpi_table_3,kpi_table_4=kpi_table_4,kpi_table_5=kpi_table_5,
                                    daily_ret_hist=daily_ret_hist)
        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 drawdown 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)
    def create_monthly_table(self,return_series,num_of_compenents):
        return_series.rename('weighted rets',inplace=True)
        return_series = (return_series/ float(num_of_compenents))
        returns_df_m = pd.DataFrame((return_series + 1).resample('M').prod() - 1)
        returns_df_m['Month'] = returns_df_m.index.month
        monthly_table = returns_df_m[['weighted rets','Month']].pivot_table(returns_df_m[['weighted rets','Month']], index=returns_df_m.index, columns='Month', aggfunc=np.sum).resample('A')
        monthly_table = monthly_table.aggregate('sum')
        monthly_table.columns = monthly_table.columns.droplevel()
        #replace full date in index column with just the correspnding year
        monthly_table.index = monthly_table.index.year
        monthly_table['YTD'] = ((monthly_table + 1).prod(axis=1) - 1)
        monthly_table = monthly_table * 100
        #Replace integer column headings with MMM format
        monthly_table.columns = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec','YTD']
        return monthly_table.round(2).fillna("").to_html(classes="table table-hover table-bordered table-striped")
    
    def get_ffn_stats(self,equity_series):
        equity_stats = equity_series.calc_stats()
        d = dict(equity_stats.stats)
        return d
    def create_kpi_table(self,ffn_dict):
        kpi_table = pd.DataFrame.from_dict(ffn_dict,orient='index')
        kpi_table.index.name = 'KPI'
        kpi_table.columns = ['Value']
        kpi_table2 = kpi_table.loc[['total_return','cagr','daily_sharpe',
                                   'daily_vol','max_drawdown','avg_drawdown']]#.astype(float)
        kpi_table2['Value'] = pd.Series(["{0:.2f}%".format(val * 100) for val in kpi_table2['Value']], index = kpi_table2.index)
        kpi_table2.loc['avg_drawdown_days'] = kpi_table.loc['avg_drawdown_days'] 
        return kpi_table2.to_html(classes="table table-hover table-bordered table-striped",header=False)
    def create_kpi_table_full(self,ffn_dict_list):
        df_list =[pd.DataFrame.from_dict(x,orient='index') for x in ffn_dict_list]
        kpi_table_full = pd.concat(df_list,axis=1)
        return kpi_table_full
    def split_kpi_table(self,kpi_table_full):
        kpi_table_1 = kpi_table_full.iloc[3:16].to_html(classes="table table-hover table-bordered table-striped",header=False)
        kpi_table_2 = kpi_table_full.iloc[16:24].to_html(classes="table table-hover table-bordered table-striped",header=False)
        kpi_table_3 = kpi_table_full.iloc[24:32].to_html(classes="table table-hover table-bordered table-striped",header=False)
        kpi_table_4 = kpi_table_full.iloc[32:40].to_html(classes="table table-hover table-bordered table-striped",header=False)
        kpi_table_5 = kpi_table_full.iloc[40:].to_html(classes="table table-hover table-bordered table-striped",header=False)
        return kpi_table_1,kpi_table_2,kpi_table_3,kpi_table_4,kpi_table_5
    def plot_daily_histogram(self):
        trace0 = go.Histogram(x=self.equity_curve.pct_change().values,
                              name="Strategy",
                              opacity=0.75,
                              marker=dict(
                              color=('rgb(22, 96, 167)')),
                              xbins=dict(
                              size=0.0025
                                      ))
        trace1 = go.Histogram(x=self.benchmark_curve.pct_change().values,
                              name="Benchmark",
                              opacity=0.75,
                              marker=dict(
                              color=('rgb(22, 96, 0)')),
                              xbins=dict(
                              size=0.0025
                                      ))
        data = [trace0,trace1]
        layout = go.Layout(
                            title='Histogram of Strategy and Benchmark Daily Returns',
                            autosize=True,
                            height=600,
                            hovermode = 'closest',
                            barmode='overlay'
                            )
        daily_ret_hist = plotly.offline.plot({"data": data,"layout": layout}, 
                                              include_plotlyjs=False,
                                              output_type='div')
                    
        return daily_ret_hist
if __name__ == "__main__":
    report = PerformanceReport('data.csv')
    report.generate_html_report()

“template.html”:

    <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">
        <div class="row">
    <h1>Trading Backtest Report</h1>
    </div>
<hr>
    <div class="row">
        <div class="col-sm-8">{{ perf_chart|safe }}</div>
        <div class="col-sm-4"><br><br>
            <h5>KPI Table</h5>
            {{ kpi_table|safe }}</div>
    </div>
<hr>
    <div class="row"><h5>Monthly Returns</h5></div>
    <div class="row">
        <div class="col-sm-12">
                              {{ monthly_table|safe }}
        </div>
    </div>
<hr>
    <div class="row">
        <div class="col-sm-12">{{ drawdown_chart|safe }}</div>
    </div>
    <hr>
    <div class="row"><h5>KPI Tables</h5></div>
    <div class="row">
       
        <div class="col-sm-6"> 
                              {{ kpi_table_1|safe }}<br>
                              {{ kpi_table_5|safe }}
        </div>
        <div class="col-sm-6">{{ kpi_table_2|safe }}<br>
                              {{ kpi_table_3|safe }}<br>
                              {{ kpi_table_4|safe }}<br>
        </div>
    </div>
    <div class="row">
        <div class="col-sm-12">{{ daily_ret_hist|safe }}</div>
    </div>
</div>

Our report now looks like this (although the top of the report is not shown as it is now too long to fit on one screen without scrolling):

I always find boxplots to be a good compliment to histograms when visualising distributions, so lets quickly add a boxplot of daily returns for both our strategy and the benchmark to our report. A new method has been added to “main.py” called “plot_daily_box” and the necessary additions have been made to the “generate_html” method.

The “main.py” file now looks like this: (I’ve made some small cosmetic changes to some of the other code also FYI – just a bit of housekeeping and renaming titles of charts etc.)

import os
import pandas as pd
import numpy as np
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()
        monthly_table = self.create_monthly_table(self.equity_curve.pct_change().dropna(),1)
        equity_curve_ffn_stats = self.get_ffn_stats(self.equity_curve)
        benchmark_curve_ffn_stats = self.get_ffn_stats(self.benchmark_curve)
        kpi_table = self.create_kpi_table(equity_curve_ffn_stats)
        kpi_table_full =  self.create_kpi_table_full([equity_curve_ffn_stats,benchmark_curve_ffn_stats])
        kpi_table_1,kpi_table_2,kpi_table_3,kpi_table_4,kpi_table_5 = self.split_kpi_table(kpi_table_full)
        daily_ret_hist = self.plot_daily_histogram()
        daily_ret_box = self.plot_daily_box()
        html_out = template.render(perf_chart=perf_chart,drawdown_chart=drawdown_chart,monthly_table=monthly_table,
                                    kpi_table=kpi_table,kpi_table_1=kpi_table_1,kpi_table_2=kpi_table_2,
                                    kpi_table_3=kpi_table_3,kpi_table_4=kpi_table_4,kpi_table_5=kpi_table_5,
                                    daily_ret_hist=daily_ret_hist,daily_ret_box=daily_ret_box)
        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 drawdown 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',
                            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',
                            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
    def create_monthly_table(self,return_series,num_of_compenents):
        return_series.rename('weighted rets',inplace=True)
        return_series = (return_series/ float(num_of_compenents))
        returns_df_m = pd.DataFrame((return_series + 1).resample('M').prod() - 1)
        returns_df_m['Month'] = returns_df_m.index.month
        monthly_table = returns_df_m[['weighted rets','Month']].pivot_table(returns_df_m[['weighted rets','Month']], index=returns_df_m.index, columns='Month', aggfunc=np.sum).resample('A')
        monthly_table = monthly_table.aggregate('sum')
        monthly_table.columns = monthly_table.columns.droplevel()
        #replace full date in index column with just the correspnding year
        monthly_table.index = monthly_table.index.year
        monthly_table['YTD'] = ((monthly_table + 1).prod(axis=1) - 1)
        monthly_table = monthly_table * 100
        #Replace integer column headings with MMM format
        monthly_table.columns = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec','YTD']
        return monthly_table.round(2).fillna("").to_html(classes="table table-hover table-bordered table-striped")
    
    def get_ffn_stats(self,equity_series):
        equity_stats = equity_series.calc_stats()
        d = dict(equity_stats.stats)
        return d
    def create_kpi_table(self,ffn_dict):
        kpi_table = pd.DataFrame.from_dict(ffn_dict,orient='index')
        kpi_table.index.name = 'KPI'
        kpi_table.columns = ['Value']
        kpi_table2 = kpi_table.loc[['total_return','cagr',
                                   'daily_vol','max_drawdown','avg_drawdown']]#.astype(float)
        kpi_table2['Value'] = pd.Series(["{0:.2f}%".format(val * 100) for val in kpi_table2['Value']], index = kpi_table2.index)
        kpi_table2.loc['avg_drawdown_days'] = kpi_table.loc['avg_drawdown_days'] 
        kpi_table2.loc['daily_sharpe'] = np.round(kpi_table.loc['daily_sharpe'].values[0],2)
        return kpi_table2.to_html(classes="table table-hover table-bordered table-striped",header=False)
    def create_kpi_table_full(self,ffn_dict_list):
        df_list =[pd.DataFrame.from_dict(x,orient='index') for x in ffn_dict_list]
        kpi_table_full = pd.concat(df_list,axis=1)
        return kpi_table_full
    def split_kpi_table(self,kpi_table_full):
        kpi_table_1 = kpi_table_full.iloc[3:16].to_html(classes="table table-hover table-bordered table-striped",header=False)
        kpi_table_2 = kpi_table_full.iloc[16:24].to_html(classes="table table-hover table-bordered table-striped",header=False)
        kpi_table_3 = kpi_table_full.iloc[24:32].to_html(classes="table table-hover table-bordered table-striped",header=False)
        kpi_table_4 = kpi_table_full.iloc[32:40].to_html(classes="table table-hover table-bordered table-striped",header=False)
        kpi_table_5 = kpi_table_full.iloc[40:].to_html(classes="table table-hover table-bordered table-striped",header=False)
        return kpi_table_1,kpi_table_2,kpi_table_3,kpi_table_4,kpi_table_5
    def plot_daily_histogram(self):
        trace0 = go.Histogram(x=self.equity_curve.pct_change().values,
                              name="Strategy",
                              opacity=0.75,
                              marker=dict(
                              color=('rgb(22, 96, 167)')),
                              xbins=dict(
                              size=0.0025
                                      ))
        trace1 = go.Histogram(x=self.benchmark_curve.pct_change().values,
                              name="Benchmark",
                              opacity=0.75,
                              marker=dict(
                              color=('rgb(22, 96, 0)')),
                              xbins=dict(
                              size=0.0025
                                      ))
        data = [trace0,trace1]
        layout = go.Layout(
                            title='Histogram of Strategy and Benchmark Daily Returns',
                            autosize=True,
                            height=600,
                            hovermode = 'closest',
                            barmode='overlay'
                            )
        daily_ret_hist = plotly.offline.plot({"data": data,"layout": layout}, 
                                              include_plotlyjs=False,
                                              output_type='div')
                    
        return daily_ret_hist

    def plot_daily_box(self):
        trace0 = go.Box(y=self.equity_curve.pct_change().values,
                        name="Strategy",
                        marker=dict(
                                     color=('rgb(22, 96, 167)')))
        trace1 = go.Box(y=self.benchmark_curve.pct_change().values,
                        name="Benchmark",
                        marker=dict(
                              color=('rgb(22, 96, 0)')))
        data = [trace0,trace1]
        
        layout = go.Layout(
                         title='Boxplot of Strategy and Benchmark Daily Returns',
                         autosize=True,
                         height=600,
                         yaxis=dict(
                                zeroline=False
                            )
                         )
        
        box_plot = plotly.offline.plot({"data": data,"layout": layout}, include_plotlyjs=False,
                                         output_type='div')
                                        
        return box_plot
if __name__ == "__main__":
    report = PerformanceReport('data.csv')
    report.generate_html_report()

And the “template.html” looks as follows:

    <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">
        <div class="row">
    <h1>Trading Backtest Report</h1>
    </div>
<hr>
    <div class="row">
        <div class="col-sm-8">{{ perf_chart|safe }}</div>
        <div class="col-sm-4"><br><br>
            <h5>KPI Table</h5>
            {{ kpi_table|safe }}</div>
    </div>
<hr>
    <div class="row"><h5>Monthly Returns</h5></div>
    <div class="row">
        <div class="col-sm-12">
                              {{ monthly_table|safe }}
        </div>
    </div>
<hr>
    <div class="row">
        <div class="col-sm-12">{{ drawdown_chart|safe }}</div>
    </div>
    <hr>
    <div class="row"><h5>KPI Tables</h5></div>
    <div class="row">
       
        <div class="col-sm-6"> 
                              {{ kpi_table_1|safe }}<br>
                              {{ kpi_table_5|safe }}
        </div>
        <div class="col-sm-6">{{ kpi_table_2|safe }}<br>
                              {{ kpi_table_3|safe }}<br>
                              {{ kpi_table_4|safe }}<br>
        </div>
    </div>
    <hr>
    <div class="row">
        <div class="col-sm-12">{{ daily_ret_hist|safe }}</div>
    </div>
    <hr>
    <div class="row">
            <div class="col-sm-12">{{ daily_ret_box|safe }}</div>
    </div>
</div>

This will tag the boxplot onto the end of our report as shown below:

I think this is a good place to leave it for now – we’ve added some nice stats tables to our report, along with a histogram and boxplot of daily returns for both our strategy and the specified benchmark.

It should be becoming clear now the process by which we add functionality to our PerformanceReport class in the way of a new method, which returns an object with the desired information/chart etc which we then convert into HTML (if it’s not already in HTML format) and inject into our report template through the use of arguments to the “template.render” method.

Pretty much any Python object can be injected into our template so the possibilities for our report are vast. I’m not going to repeat the same process of creating and injecting simple statistics, tables or chart objects as there’s significantly diminishing value to you the reader of seeing me do the same thing over and over.

What I’ll try to do in the next article (which may or may not be the last in this series, depending on how much I get through), is try to add a bit of functionality that’s hopefully interesting in it’s own right, even without the actual report generation process. I was thinking something along the lines of incorporating Monte Carlo simulations to allow us to calculate our strategies Value at Risk (VaR) and shed some light on best/worst case scenarios and estimates of possible future maximum drawdowns and the like. I will look to use several methods to generate our simulations – namely Monte Carlo simulation that generates possible outcomes by sampling from a theoretical distribution with predefined parameters, and “resampling with replacement” which is also known as “bootstrapping”. I’ll explain more about these methods in the next article…coming soon!

Until next time!

You may also like

3 comments

Alex 28 January 2019 - 22:37

Very helpful series of articles, it all seems so easy when I’m reading your code but I know first hand the struggles involved.
Thanks for all your efforts in putting this together as I will certainly get use out of this in the future.

Reply
s666 29 January 2019 - 11:46

Hi Alex, not a problem! Glad someone is finding the series useful…If there’s anything specific you would like to see implemented in the report then do let me know and ill see if I can add it in the next post 😀

Reply
Trading Strategy Performance Report in Python – Part 4 - Python For Finance 3 February 2019 - 16:46

[…] can render in our browser and interact with (to a certain extent). The previous post can be found here. If you copy and paste the last iteration of the code for “main.py” and […]

Reply

Leave a Reply

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More

%d bloggers like this: