Well it’s time for part 4 of our mini-series outlining how to create a program to generate performance reports in nice, fancy looking HTML format that we 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 “template.html” from the last post into your 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 much.
So I promised at the end of the last post that I would stop adding random charts and tables with additional KPIs and equity curves and what not, and try to add a bit of functionality that one may actually find useful even if it weren’t part of this whole specific performance report creation tutorial. I know many people are interested in the concept of Monte Carlo analysis and the insights it can offer above and beyond those statistics and visuals created from the actual return series of the investment/trading strategy under inspection.
The main reason why people use Monte Carlo methods is in an attempt to model “uncertainty” and try to see things in terms of distributions of possible outcomes, rather than as point estimates. What I mean by that is this – if you think about it, the strategy return series we have entered as our “data” in the csv file, whether it be the output of a simulated backtest, or indeed a live strategy track record, is just one single return path that happened to actually be realised. There were in theory, infinitely many possible paths the return series could have taken along the way; the movements of the underlying markets, as everything in life, are subject to an element of chance and randomness. In fact the chances of your strategy performing the exact same way in the future is exactly zero. You could even say that before it actually happened, the chances that your strategy would produce the exact series of returns it actually did, was also zero.
I know it sounds a bit strange – how could the probability of something happening be zero when it clearly ended up happening? Another way to think about the situation is as follows. If someone were to ask you to predict what the price of the S&P 500 index will be at the end of next year, how would you answer? Take a guess and give them a figure? If you did, you’d be wrong, I can guarantee that.
The logic stems from the fact that financial asset and financial investment strategy returns are considered to be “continuous random variables”. That is to say there are an infinite number of possible values they can take. Your strategy could return 10% in a year….or it could be 10.5%, or actually it could be 10.52%….how about 10.528%? You get the idea, you could in theory keep getting a more and more precise figure so that there are in theory an infinite number of possible outcomes. If there are an infinite number of possible outcomes and values for a variable to take, the chances of it ending up being any of them is zero, although paradoxically it has to end up somewhere.
So this is where probability density functions come into play – they allow us to assign a probability that any value in a continuous set of values might occur. You can read a little more about it here if you are so inclined.
So what does all this have to do with Monte Carlo simulation? Well we use MC simulation to incorporate “uncertainty/randomness” into our model and use that model to simulate a large number of possible paths and outcomes that we could have realistically expected to happen, given the underlying characteristics of our strategy.
These multiple simulations, if modelled properly allow us to produce a “distribution” of outcomes, rather than having to rely on that one single “point outcome” that was produced by the data held in our csv file. From this distribution of outcomes, we can begin to assign probabilities to the chances of ending up within certain ranges of said outcomes.
I’m starting to labour the point somewhat so I’ll get on to a practical example and perhaps it will become a bit clearer as to what I’m rambling on about.
First thing to do is update our code with a new method that takes the equity curve of our strategy as an input and outputs a DataFrame holding a user defined number of Monte Carlo simulation runs which have been generated using a model based on the underlying return and volatility characteristics of the equity series itself.
Once we have the simulation output we can add another method which generates a plot to help us visualise all this. The updated code is as follows:
“main.py”
import os import pandas as pd import numpy as np import random 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()) 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() simulations = 250 periods = 252 monte_carlo_results,monte_carlo_hist = self.run_monte_carlo_parametric(self.equity_curve.pct_change().dropna(),periods,simulations) mc_chart =self.plot_mc_chart(monte_carlo_results) 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,mc_chart=mc_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 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): return_series.rename('weighted rets',inplace=True) 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 monthly_table.replace(0.0,"",inplace=True) #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 run_monte_carlo_parametric(self,returns,trading_days,simulations): df_list = [] result = [] S = 100 T = trading_days mu = returns.mean() vol = returns.std() for i in range(simulations): #create list of daily returns using random normal distribution daily_returns=np.random.normal(mu,vol,T)+1 #set starting price and create price series generated by above random daily returns price_list = [S] for x in daily_returns: price_list.append(price_list[-1]*x) df_list.append(pd.DataFrame(price_list)) result.append(price_list[-1]) df_master = pd.concat(df_list,axis=1) df_master.columns = range(len(df_master.columns)) return df_master,result 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 def get_random_rgb(self): col = 'rgb'+str((random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))) return col def plot_mc_chart(self,monte_carlo_results): mc_trace_list = [] for col in monte_carlo_results.columns: rgb = self.get_random_rgb() trace = go.Scatter( x=monte_carlo_results.index.tolist(), y=monte_carlo_results[col].values.tolist(), name='mc data', yaxis='y2', line = dict(color = (rgb))) mc_trace_list.append(trace) layout_mc= go.Layout( title='Monte Carlo Parametric', yaxis=dict(title='Equity'), autosize=True, height=600, showlegend=False, hovermode = 'closest' ) mc_chart = plotly.offline.plot({"data": mc_trace_list, "layout": layout_mc}, include_plotlyjs=False, output_type='div') return mc_chart 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> <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> <hr> <div class="row"> <div class="col-sm-12">{{ mc_chart|safe }}</div> </div> </div>
This produces a plot that should look relatively similar to the one below:

Hopefully it’s a bit clearer now what I meant by “generating a range of outcomes” – we can clearly see that even with the same expected return and volatility inputs, once we model the effect of randomness the chances of being able to predict where we will end up exactly becomes smaller and smaller. If we look at the range of values on the y-axis, there are return series that reach as high as 180 (starting at an index of 100) and others that reach as low as 60. That’s a pretty big range for what is supposed to be the “same” strategy.
Well that’s kinda the whole point of this exercise – to allow us to quantify the chances of ending up at different ending points within that “pretty big” range.
It’s a bit tough to see what’s really going on amongst all those lines on the above chart – so we should probably think of a better way to visualise the distribution of ending values. The best way to do that would be with a histogram or distribution plot. I’ll create one of each now for demonstration purposes.
The updated code as as follows- for “main.py”
import os import pandas as pd import numpy as np import random import plotly import plotly.graph_objs as go import plotly.figure_factory as ff 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()) 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() simulations = 250 periods = 252 monte_carlo_results,monte_carlo_hist = self.run_monte_carlo_parametric(self.equity_curve.pct_change().dropna(),periods,simulations) mc_chart =self.plot_mc_chart(monte_carlo_results) mc_dist_chart = self.plot_mc_dist_chart(monte_carlo_hist) mc_hist_chart = self.plot_mc_hist_chart(monte_carlo_hist) 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,mc_chart=mc_chart, mc_dist_chart=mc_dist_chart,mc_hist_chart=mc_hist_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 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): return_series.rename('weighted rets',inplace=True) 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 monthly_table.replace(0.0,"",inplace=True) #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 run_monte_carlo_parametric(self,returns,trading_days,simulations): df_list = [] result = [] S = 100 T = trading_days mu = returns.mean() vol = returns.std() for i in range(simulations): #create list of daily returns using random normal distribution daily_returns=np.random.normal(mu,vol,T)+1 #set starting price and create price series generated by above random daily returns price_list = [S] for x in daily_returns: price_list.append(price_list[-1]*x) df_list.append(pd.DataFrame(price_list)) result.append(price_list[-1]) df_master = pd.concat(df_list,axis=1) df_master.columns = range(len(df_master.columns)) return df_master,result 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 def get_random_rgb(self): col = 'rgb'+str((random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))) return col def plot_mc_chart(self,monte_carlo_results): mc_trace_list = [] for col in monte_carlo_results.columns: rgb = self.get_random_rgb() trace = go.Scatter( x=monte_carlo_results.index.tolist(), y=monte_carlo_results[col].values.tolist(), name='mc data', yaxis='y2', line = dict(color = (rgb))) mc_trace_list.append(trace) layout_mc= go.Layout( title='Monte Carlo Parametric', yaxis=dict(title='Equity'), autosize=True, height=600, showlegend=False, hovermode = 'closest' ) mc_chart = plotly.offline.plot({"data": mc_trace_list, "layout": layout_mc}, include_plotlyjs=False, output_type='div') return mc_chart def plot_mc_dist_chart(self,monte_carlo_hist): fig = ff.create_distplot([monte_carlo_hist],group_labels=['Strategy Monte Carlo Returns'],show_rug=False) fig['layout'].update(autosize=True,height=600,title="Parametric Monte Carlo Simulations <br> (Distribution Plot - Ending Equity)",showlegend=False) dist_plot_MC_parametric = plotly.offline.plot(fig, include_plotlyjs=False, output_type='div') return dist_plot_MC_parametric def plot_mc_hist_chart(self,monte_carlo_hist): layout_mc = go.Layout( title="Parametric Monte Carlo Simulations <br> (Histogram - Ending Equity)", autosize=True, height=600, showlegend=False, hovermode = 'closest' ) data_mc_hist = [go.Histogram(x=monte_carlo_hist, histnorm='probability')] mc_hist = plotly.offline.plot({"data": data_mc_hist,"layout": layout_mc}, include_plotlyjs=False, output_type='div') return mc_hist 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> <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> <hr> <div class="row"> <div class="col-sm-12">{{ mc_chart|safe }}</div> </div> <hr> <div class="row"> <div class="col-sm-6">{{ mc_dist_chart|safe }}</div> <div class="col-sm-6">{{ mc_hist_chart|safe }}</div> </div> <hr> </div>
You should now see the following 2 charts at the bottom of your report…

From the results of these simulated runs we can identify the various quantiles of returns that signify the % chances of occuring. For example, there is in theory a 5% that the result will end up higher than the
value found at the 95th quantile or lower than that at the 5th quantile. A 10% chance that it will end up higher than the 90th quantile or lower than the 10th quantile, etc etc.
Along with extracting these values from the distribution of ending values, I have extracted the best and worst runs from the simulations and presented their KPIs and stats. These are all shown below the histogram and distribution plot.
The code for “main.py” should now be as follows:
import os import pandas as pd import numpy as np import random import datetime import plotly import plotly.graph_objs as go import plotly.figure_factory as ff 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()) 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() simulations = 250 periods = 252 monte_carlo_results,monte_carlo_hist,mc_max_dd_list = self.run_monte_carlo_parametric(self.equity_curve.pct_change().dropna(),periods,simulations) mc_chart =self.plot_mc_chart(monte_carlo_results) mc_dist_chart = self.plot_mc_dist_chart(monte_carlo_hist) mc_hist_chart = self.plot_mc_hist_chart(monte_carlo_hist) mc_best_worst_df = self.extract_best_worst(monte_carlo_results,periods) best_df_ffn_stats = self.get_ffn_stats(mc_best_worst_df['Best']) worst_df_ffn_stats = self.get_ffn_stats(mc_best_worst_df['Worst']) best_df_kpi = self.create_kpi_table(best_df_ffn_stats) worst_df_kpi = self.create_kpi_table(worst_df_ffn_stats) # mc_5perc,mc_95perc = self.calc_mc_var(monte_carlo_results,5) mc_dict_perf = self.mc_perf_probs(monte_carlo_results) mc_dict_dd = self.mc_dd_probs(mc_max_dd_list) 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,mc_chart=mc_chart, mc_dist_chart=mc_dist_chart,mc_hist_chart=mc_hist_chart,best_df_kpi=best_df_kpi, worst_df_kpi=worst_df_kpi,mc_dict_perf=mc_dict_perf,mc_dict_dd=mc_dict_dd) 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): return_series.rename('weighted rets',inplace=True) 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 monthly_table.replace(0.0,"",inplace=True) #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 run_monte_carlo_parametric(self,returns,trading_days,simulations): df_list = [] result = [] S = 100 T = trading_days mu = returns.mean() vol = returns.std() dd_result = [] for i in range(simulations): #create list of daily returns using random normal distribution daily_returns=np.random.normal(mu,vol,T)+1 #set starting price and create price series generated by above random daily returns price_list = [S] for x in daily_returns: price_list.append(price_list[-1]*x) df = pd.DataFrame(price_list) max_dd = ffn.calc_max_drawdown(df) dd_result.append(max_dd) df_list.append(df) result.append(price_list[-1]) df_master = pd.concat(df_list,axis=1) df_master.columns = range(len(df_master.columns)) return df_master,result,dd_result def extract_best_worst(self,monte_carlo_results,trading_days): today = datetime.datetime.today().strftime('%d/%m/%Y') date_range = pd.bdate_range(end=today, periods=trading_days+1, freq='B',dayfirst=True) monte_carlo_results.columns = range(len(monte_carlo_results.columns)) last_row = monte_carlo_results.iloc[-1] max_col = last_row.idxmax(axis=1) best_df = monte_carlo_results[max_col] min_col = last_row.idxmin(axis=1) worst_df = monte_carlo_results[min_col] best_df = best_df.to_frame().set_index(date_range) worst_df = worst_df.to_frame().set_index(date_range) mc_best_worst_df = pd.concat([best_df,worst_df],axis=1) mc_best_worst_df.columns = ['Best','Worst'] return mc_best_worst_df def calc_mc_var(self,monte_carlo_results,confidence): mc_as_array = np.array(monte_carlo_results) mc_low_perc = round(((np.percentile(mc_as_array, 100-confidence) /100) - 1) * 100,2) mc_high_perc = round(((np.percentile(mc_as_array, confidence) /100) - 1) * 100,2) return mc_low_perc,mc_high_perc def mc_perf_probs(self,monte_carlo_results): mc_as_array = np.array(monte_carlo_results) mc_5perc = round(((np.percentile(mc_as_array, 5) /100) - 1) * 100,2) mc_95perc = round(((np.percentile(mc_as_array, 95) /100) - 1) * 100,2) mc_1perc = round(((np.percentile(mc_as_array, 1) /100) - 1) * 100,2) mc_10perc = round(((np.percentile(mc_as_array, 10) /100) - 1) * 100,2) mc_20perc = round(((np.percentile(mc_as_array, 20) /100) - 1) * 100,2) mc_30perc = round(((np.percentile(mc_as_array, 30) /100) - 1) * 100,2) mc_40perc = round(((np.percentile(mc_as_array, 40) /100) - 1) * 100,2) mc_50perc = round(((np.percentile(mc_as_array, 50) /100) - 1) * 100,2) mc_60perc = round(((np.percentile(mc_as_array, 60) /100) - 1) * 100,2) mc_70perc = round(((np.percentile(mc_as_array, 70) /100) - 1) * 100,2) mc_80perc = round(((np.percentile(mc_as_array, 80) /100) - 1) * 100,2) mc_90perc = round(((np.percentile(mc_as_array, 90) /100) - 1) * 100,2) mc_99perc = round(((np.percentile(mc_as_array, 99) /100) - 1) * 100,2) mc_dict_perf = { 'mc_1perc':mc_1perc, 'mc_5perc':mc_5perc, 'mc_10perc':mc_10perc, 'mc_20perc':mc_20perc, 'mc_30perc':mc_30perc, 'mc_40perc':mc_40perc, 'mc_50perc':mc_50perc, 'mc_60perc':mc_60perc, 'mc_70perc':mc_70perc, 'mc_80perc':mc_80perc, 'mc_90perc':mc_90perc, 'mc_95perc':mc_95perc, 'mc_99perc':mc_99perc } return mc_dict_perf def mc_dd_probs(self,mc_max_dd_list): mc_as_array_dd = np.array(mc_max_dd_list) mc_5perc_dd = round((np.percentile(mc_as_array_dd, 5)) * 100,2) mc_95perc_dd = round((np.percentile(mc_as_array_dd, 95)) * 100,2) mc_1perc_dd = round((np.percentile(mc_as_array_dd, 1)) * 100,2) mc_10perc_dd = round((np.percentile(mc_as_array_dd, 10)) * 100,2) mc_20perc_dd = round((np.percentile(mc_as_array_dd, 20)) * 100,2) mc_30perc_dd = round((np.percentile(mc_as_array_dd, 30)) * 100,2) mc_40perc_dd = round((np.percentile(mc_as_array_dd, 40)) * 100,2) mc_50perc_dd = round((np.percentile(mc_as_array_dd, 50)) * 100,2) mc_60perc_dd = round((np.percentile(mc_as_array_dd, 60)) * 100,2) mc_70perc_dd = round((np.percentile(mc_as_array_dd, 70)) * 100,2) mc_80perc_dd = round((np.percentile(mc_as_array_dd, 80)) * 100,2) mc_90perc_dd = round((np.percentile(mc_as_array_dd, 90)) * 100,2) mc_99perc_dd = round((np.percentile(mc_as_array_dd, 99)) * 100,2) mc_dict_dd = { 'mc_1perc_dd':mc_1perc_dd, 'mc_5perc_dd':mc_5perc_dd, 'mc_10perc_dd':mc_10perc_dd, 'mc_20perc_dd':mc_20perc_dd, 'mc_30perc_dd':mc_30perc_dd, 'mc_40perc_dd':mc_40perc_dd, 'mc_50perc_dd':mc_50perc_dd, 'mc_60perc_dd':mc_60perc_dd, 'mc_70perc_dd':mc_70perc_dd, 'mc_80perc_dd':mc_80perc_dd, 'mc_90perc_dd':mc_90perc_dd, 'mc_95perc_dd':mc_95perc_dd, 'mc_99perc_dd':mc_99perc_dd } return mc_dict_dd 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 def get_random_rgb(self): col = 'rgb'+str((random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))) return col def plot_mc_chart(self,monte_carlo_results): mc_trace_list = [] for col in monte_carlo_results.columns: rgb = self.get_random_rgb() trace = go.Scatter( x=monte_carlo_results.index.tolist(), y=monte_carlo_results[col].values.tolist(), name='mc data', yaxis='y2', line = dict(color = (rgb))) mc_trace_list.append(trace) layout_mc= go.Layout( title='Monte Carlo Parametric', yaxis=dict(title='Equity'), autosize=True, height=600, showlegend=False, hovermode = 'closest' ) mc_chart = plotly.offline.plot({"data": mc_trace_list, "layout": layout_mc}, include_plotlyjs=False, output_type='div') return mc_chart def plot_mc_dist_chart(self,monte_carlo_hist): fig = ff.create_distplot([monte_carlo_hist],group_labels=['Strategy Monte Carlo Returns'],show_rug=False) fig['layout'].update(autosize=True,height=600,title="Parametric Monte Carlo Simulations <br> (Distribution Plot - Ending Equity)",showlegend=False) dist_plot_MC_parametric = plotly.offline.plot(fig, include_plotlyjs=False, output_type='div') return dist_plot_MC_parametric def plot_mc_hist_chart(self,monte_carlo_hist): layout_mc = go.Layout( title="Parametric Monte Carlo Simulations <br> (Histogram - Ending Equity)", autosize=True, height=600, showlegend=False, hovermode = 'closest' ) data_mc_hist = [go.Histogram(x=monte_carlo_hist, histnorm='probability')] mc_hist = plotly.offline.plot({"data": data_mc_hist,"layout": layout_mc}, include_plotlyjs=False, output_type='div') return mc_hist if __name__ == "__main__": report = PerformanceReport('data.csv') report.generate_html_report()
And “template.html” should now be:
<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> <hr> <div class="row"> <div class="col-sm-12">{{ mc_chart|safe }}</div> </div> <hr> <div class="row"> <div class="col-sm-6">{{ mc_dist_chart|safe }}</div> <div class="col-sm-6">{{ mc_hist_chart|safe }}</div> </div> <hr> <div class="row"> <div class="col-sm-6"><h5>Best MC Run KPI Table</h5>{{ best_df_kpi|safe }}</div> <div class="col-sm-6"><h5>Worst MC Run KPI Table</h5>{{ worst_df_kpi|safe }}</div> </div> <hr> <div class="row"> <div class="col-sm-6"> <b><center>Monte Carlo (Sampled) Confidence Levels</center></b> <table data-toggle="table" class="table table-hover table-bordered table-striped"> <colgroup><col style="width:33%"> <col style="width:33%"> <col style="width:33%"> </colgroup><thead> <tr> <th>KPI</th> <th data-cell-style="cellStyle">Annual Return(%)</th> <th data-cell-style="cellStyle">Maximum Drawdown (%)</th> </tr> </thead> <tbody> <tr> <td><b>1%<b></b></b></td> <td>{{mc_dict_perf.mc_99perc}}</td> <td>{{mc_dict_dd.mc_99perc_dd}}</td> </tr> <tr> <td><b>5%<b></b></b></td> <td>{{mc_dict_perf.mc_95perc}}</td> <td>{{mc_dict_dd.mc_95perc_dd}}</td> </tr> <tr> <td><b>10%<b></b></b></td> <td>{{mc_dict_perf.mc_90perc}}</td> <td>{{mc_dict_dd.mc_90perc_dd}}</td> </tr> <tr> <td><b>20%<b></b></b></td> <td>{{mc_dict_perf.mc_80perc}}</td> <td>{{mc_dict_dd.mc_80perc_dd}}</td> </tr> <tr> <td><b>30%<b></b></b></td> <td>{{mc_dict_perf.mc_70perc}}</td> <td>{{mc_dict_dd.mc_70perc_dd}}</td> </tr> <tr> <td><b>40%</b></td> <td>{{mc_dict_perf.mc_60perc}}</td> <td>{{mc_dict_dd.mc_60perc_dd}}</td> </tr> <tr> <td><b>50%</b></td> <td>{{mc_dict_perf.mc_50perc}}</td> <td>{{mc_dict_dd.mc_50perc_dd}}</td> </tr> <tr> <td><b>60%</b></td> <td>{{mc_dict_perf.mc_40perc}}</td> <td>{{mc_dict_dd.mc_40perc_dd}}</td> </tr> <tr> <td><b>70%</b></td> <td>{{mc_dict_perf.mc_30perc}}</td> <td>{{mc_dict_dd.mc_30perc_dd}}</td> </tr> <tr> <td><b>80%</b></td> <td>{{mc_dict_perf.mc_20perc}}</td> <td>{{mc_dict_dd.mc_20perc_dd}}</td> </tr> <tr> <td><b>90%</b></td> <td>{{mc_dict_perf.mc_10perc}}</td> <td>{{mc_dict_dd.mc_10perc_dd}}</td> </tr> <tr> <td><b>95%</b></td> <td>{{mc_dict_perf.mc_5perc}}</td> <td>{{mc_dict_dd.mc_5perc_dd}}</td> </tr> <tr> <td><b>99%</b></td> <td>{{mc_dict_perf.mc_1perc}}</td> <td>{{mc_dict_dd.mc_1perc_dd}}</td> </tr> </tbody> </table> </div> </div> </div>
This creates three tables as below:

I’ll keep this post (relatively) short and leave it here for now. Any comments or questions feel free to use the comments section below.
I’ll use the next post most likely to just to clean things up and finish things off with this report script – I think I’ve covered most of what I was planning to and hopefully you can now see how to create and add various plots and stats tables etc.
Until next time…
3 comments
As a suggestion, please can you highlight in some way the code you have just added. Not really an issue if the code isn’t too long but in this post it took me some time to understand what was new. Another very helpful post.
Hi Alex – yeah I did think that as I was posting to be honest…have you managed to work out what has changed, or do you think it would be useful for me to go back into the post and add comments etc?
Man, i havent seen so much high quality material on a blog in a while! I covered all 4 parts of this tutorial in two afternoons and i must say you saved me weeks of work. Obviously im going essentially line by line on the codes provided, taking a few lines as given but managing to understand most part. Thank you for the effort on writing on this blog. These are all very helpful!