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

Trading Strategy Performance Report in Python – Part 2

by Stuart Jamieson

This is the second 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.

As long as the equity series (and an optional benchmark equity series) are formatted in the correct manner and dropped into the “data” folder in csv format, it will eventually take no more than a click of a button and we will be able to produce in-depth, interactive strategy performance reports. This will be invaluable when it comes to filtering out the “wheat from the chaffe” in terms of prototype trading strategy backtest results. We wont have to recreate our analysis efforts again and again, rather we just run them through this program and the hard work is done for us.

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 was left as follows:

import os
import pandas as pd
import plotly
import plotly.graph_objs as go
import ffn
from jinja2 import Environment, FileSystemLoader

class PerformanceReport:
""" Report with performance stats for given strategy returns.
"""
    def __init__(self,infilename):
        self.infilename = infilename
        self.get_data()
    def get_data(self):
        basedir = os.path.abspath(os.path.dirname('__file__'))
        data_folder = os.path.join(basedir, 'data')
        data = pd.read_csv(os.path.join(data_folder, 
        self.infilename),index_col='date',
        parse_dates=True,dayfirst=True)
        self.equity_curve = data['equity_curve']
        if len(data.columns) > 1:
            self.benchmark_curve = data['benchmark']
    def generate_html(self):
        env = Environment(loader=FileSystemLoader('.'))
        template = env.get_template("templates/template.html")
        perf_chart = self.plot_performance_chart()
        drawdown_chart = self.plot_drawdown_chart()
        html_out = template.render(perf_chart=perf_chart,drawdown_chart=drawdown_chart)
        return html_out
    def generate_html_report(self):
    """ Returns HTML report with analysis
    """
        html = self.generate_html()
        outputdir="output"
        outfile = os.path.join(outputdir, 'report.html') 
        file = open(outfile,"w") 
        file.write(html)
        file.close() 
    def rebase_series(self,series):
        return (series/series.iloc[0]) * 100 
    def plot_performance_chart(self):
        #plotly combined equity chart
        trace_equity = go.Scatter(
        x=self.equity_curve.index.tolist(),
        y=self.rebase_series(self.equity_curve).values.tolist(),
        name='strategy',
        yaxis='y2',
        line = dict(color = ('rgb(22, 96, 167)')))
        trace_benchmark = go.Scatter(
        x=self.benchmark_curve.index.tolist(),
        y=self.rebase_series(self.benchmark_curve).values.tolist(),
        name='benchmark',
        yaxis='y2',
        line = dict(color = ('rgb(22, 96, 0)')))
        layout = go.Layout(
        autosize=True,
        legend=dict(orientation="h"),
        title='Performance Chart',
        yaxis=dict(
        title='Performance'))
        perf_chart = plotly.offline.plot({"data": [trace_equity,trace_benchmark],
"layout": layout}, include_plotlyjs=False,
output_type='div')
        return (perf_chart)
    def plot_drawdown_chart(self):
        #plotly combined equity chart
        trace_equity_drawdown = go.Scatter(
        x=self.equity_curve.to_drawdown_series().index.tolist(),
        y=self.equity_curve.to_drawdown_series().values.tolist(),
        name='strategy drawdown',
        yaxis='y2',
        line = dict(color = ('rgb(22, 96, 167)')))
        trace_benchmark_drawdown = go.Scatter(
        x=self.benchmark_curve.to_drawdown_series().index.tolist(),
        y=self.benchmark_curve.to_drawdown_series().values.tolist(),
        name='benchmark drawdown',
        yaxis='y2',
        line = dict(color = ('rgb(22, 96, 0)')))
        layout = go.Layout(
        autosize=True,
        legend=dict(orientation="h"),
        title='Drawdown Chart',
        yaxis=dict(
        title='Drawdown'))
        drawdown_chart = plotly.offline.plot({"data": [trace_equity_drawdown,trace_benchmark_drawdown],
"layout": layout}, include_plotlyjs=False,
output_type='div')
        return (drawdown_chart)
if __name__ == "__main__":
    report = PerformanceReport('data.csv')
    report.generate_html_report()

And the “template.html” was as 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">
<h1>Trading Backtest Report</h1>
<div class="row">
<div class="col-sm-6">{{ perf_chart|safe }}</div>
<div class="col-sm-6">{{ drawdown_chart|safe }}</div>
</div>
</div>

This creates the report shown below:

I decided to move things around a little bit, and instead of having two charts at the top, I widened the “Performace Chart” and added a “Key Performance Indicator” table to the right of it. Below it, I am looking to create and place a table of monthly returns. Then we will kick on from there,but first let’s make those changes/additions.

Firstly let’s add the monthly returns table – the “main.py” file changes as follows:

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)
        html_out = template.render(perf_chart=perf_chart,drawdown_chart=drawdown_chart,monthly_table=monthly_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 equity chart
        trace_equity_drawdown = go.Scatter(
                            x=self.equity_curve.to_drawdown_series().index.tolist(),
                            y=self.equity_curve.to_drawdown_series().values.tolist(),
                            name='strategy drawdown',
                            yaxis='y2',
                            line = dict(color = ('rgb(22, 96, 167)')))
        
        trace_benchmark_drawdown = go.Scatter(
                            x=self.benchmark_curve.to_drawdown_series().index.tolist(),
                            y=self.benchmark_curve.to_drawdown_series().values.tolist(),
                            name='benchmark drawdown',
                            yaxis='y2',
                            line = dict(color = ('rgb(22, 96, 0)')))
        
        layout = go.Layout(
                         autosize=True,
                         legend=dict(orientation="h"),
                         title='Drawdown Chart',
                         yaxis=dict(
                             title='Drawdown'))
        
        drawdown_chart = plotly.offline.plot({"data": [trace_equity_drawdown,trace_benchmark_drawdown],
                                     "layout": layout}, include_plotlyjs=False,
                                         output_type='div')
        
        return (drawdown_chart)
    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")
    
if __name__ == "__main__":
    report = PerformanceReport('data.csv')
    report.generate_html_report()

And the “template.html” changes to:

    <meta charset="utf-8">
    <title>App</title>
    <!-- Bootstrap CSS CDN -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
    <!-- Our Custom CSS -->
    <link rel="stylesheet" href="static/app.css">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- Plotly JS -->
    <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
    <!-- Our Custom JS -->
    <script charset="utf-8" src="static/app.js"></script>
   

<div class="container">
    <h1>Trading Backtest Report</h1>
    <div class="row">
        <div class="col-sm-8">{{ perf_chart|safe }}</div>
        <div class="col-sm-4"></div>
    </div>
    <div class="row">
        <div class="col-sm-12">{{ monthly_table|safe }}</div>
    </div>
</div>

This now produces a report that looks like this:

The creation of the KPIs for the KPI table to be located in the top right will be done using the “ffn” module which we will of course need to import.

The main crux of the next set of changes (to get the KPI table) are the creation of two new class methods:
“get_ffn_stats” and “create_kpi_table”. There are of course \lso some changes made to the “generate_html” method (as there always is each iteration of code updates). The full code for the 2 files “main.py” and “template.html” are again shown in full below. (I know I’m pasting a LOT of code blocks but I want to demostrate the iterative, “building up” process we are going through while adding functionality to the report so forgive me).

“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)
        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 equity chart
        trace_equity_drawdown = go.Scatter(
                            x=self.equity_curve.to_drawdown_series().index.tolist(),
                            y=self.equity_curve.to_drawdown_series().values.tolist(),
                            name='strategy drawdown',
                            yaxis='y2',
                            line = dict(color = ('rgb(22, 96, 167)')))
        
        trace_benchmark_drawdown = go.Scatter(
                            x=self.benchmark_curve.to_drawdown_series().index.tolist(),
                            y=self.benchmark_curve.to_drawdown_series().values.tolist(),
                            name='benchmark drawdown',
                            yaxis='y2',
                            line = dict(color = ('rgb(22, 96, 0)')))
        
        layout = go.Layout(
                         autosize=True,
                         legend=dict(orientation="h"),
                         title='Drawdown Chart',
                         yaxis=dict(
                             title='Drawdown'))
        
        drawdown_chart = plotly.offline.plot({"data": [trace_equity_drawdown,trace_benchmark_drawdown],
                                     "layout": layout}, include_plotlyjs=False,
                                         output_type='div')
        
        return (drawdown_chart)
    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 “template.html”:

<prelang="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">
    <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>
</div>

</prelang="html">

This now produces a report that looks like this:

Let’s now add back the drawdown chart that we removed earlier and place it down below the monthly returns table. For now we will set it as full screen width size, and we can change that later if need be. As we have already written the code that creates the chart object, and we have already injected it into the HTML template, all we need to do is update the “template.html” file with a couple of lines of code 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">
    <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>

Once we update the “template.html” file and run the “main.py” file again, we see the following output generated:

Our report is starting to take shape now, and we’ve already managed to add some of the main components you would expect to find in any comprehensive trading strategy analsysis report. At this point,it’s very much a case of figuring out what elements/statistics/charts we want to add and iteratively add them , step by step.

In the next post I willspend some time quickly calculating, creating and injecting a whole range of components into our report. That will probably take up a full article’s worth of space, after which we can move on to some more complex functionality like Monte-Carlo VaR calculations and strategy return bootstrapping etc.

Until next time!

You may also like

10 comments

Alexander Martens 4 March 2019 - 11:36

Hello, thank you for this great content. I am trying to implement this code for my purposes but I am getting stuck at the basics. I am using VS Code and I getting an error in the output file which makes the DrawDown Graph look strange.

window is stressed and once I hover over it, I get ‘;’ expected. (undefined) module window var window: Window

javascript”>window.addEventListener(“resize”, function(){Plotly.Plots.resize(document.getElementById(“92a07f3f-a621-4298-82f4-d178d45a93a5”));});

I do not understand where this Error comes from.

need some help

best Regards
A.M.

Reply
s666 10 April 2019 - 18:23

Hi Alexander – apologies for the delay in replying, I must have missed this comment notification in my inbox!! I have just seen it now…

I have to admit I haven’t come across that error before but it sounds obviously like there is some issue with the Javascript that is producing the Plotly charts.

Is it just the drawdown chart that is being affected? Did you copy the code above exactly or have you adapted it and made changes?

I can try to help but i will probably need some further information – I shall contact you via email and take it from there.

Cheers,
S666

Reply
nihal 16 June 2019 - 08:54

hello, thanks for the series, it was just what i was looking for, but in this mini series HTML code is not showing properly as i have no idea about html codes cant go further.

can you look into the matter and help solve it.

thank you.

Reply
s666 17 June 2019 - 15:21

Hi Nihal, thanks for your message – I was indeed facing some formatting issues over the last couple of days as I have completed a full migration of the site onto a new server and there were some bugs to squash to get everything correctly ported over.

Can you please check now as the code should be showing correctly now. If you are still facing issues do let me know.

Cheers!

Reply
Luiz Macedo 31 July 2019 - 17:28

Hi S666, pretty awesome thread on performance reporting! Ive been able to follow everything with some level of understanding even though I know nothing about HTML programming or most of the packages used here.

One thing that I believe would be nice to have is to include stats from the benchmark as well on the KPI table. I tried to give a list of ‘ffn instances’ to the create_kpi_table method but apparently it does not accept lists as inputs.

Would you be able to suggest an implementation on that sense? It would be really useful for me here!

Thanks!

Reply
s666 1 August 2019 - 21:46

Hi Luiz,

I have implemented a quick and dirty version of what you have requested – the files in the following folder (link to google drive below) will create 2 KPI tables – one for strategy and one for benchmark ( I appreciate that reaklly you would want to create 1 combined table, but i’m pressed for time.)

let me know if it works for you…cheers

https://drive.google.com/drive/folders/1JEREDYSP0OkP6f1_-llE3MOAOyS7meJ4?usp=sharing

Reply
venpi 6 September 2019 - 13:27

Thank you for sharing this link, Stuart! I helped me double-check my replication and learn a bit more. Much appreciated.

Reply
s666 6 September 2019 - 15:29

No probs – glad to hear it was useful!

Reply
s666 22 September 2019 - 11:11

No problem at all, glad to hear it was helpful!!

Reply
Jenna 12 June 2020 - 10:55

First off, you are incredible! Thank you for sharing this tutorial. It has been hugely helpful. I am sorry if this is quite a basic question, but I am not fully understanding some of the code and wanted to change the Monthly Returns to, for example, 3 monthly returns. Would it be quite easy to edit the code to do this?

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: