Questo articolo è la seconda parte dell’attuale “mini-serie” che descrivere come implementare un report delle performance di una strategia in Python per verificare i risultati di un backtest. In particolare offre una panoramica su come creare un generatore HTML personalizzabile per un report di una strategia di trading.
Come descritto nel precedente articolo, l’obiettivo è costruire uno strumento grazie al quale è sufficiente un clic per produrre report approfonditi e interattivi relativi alle prestazioni di una strategia. Questo report è fondamentale per analizzare e filtrare il “valore” in termini di risultati del backtest di una strategia di trading. E’ sufficiente inserire nel formato corretto i dati dell’equity della strategia e (facoltativamente) i dati di un’equity benchmark all’interno di un file csv, senza dover ricreare i calcoli e grafici ogni volta.
Il codice descritto nel precedente articolo produce un file “report.html” che contiene i grafici delle equity e dei drawdown.
Indicatori delle prestazioni
Iniziamo con alcune modifiche. Invece di avere due grafici in alto, vogliamo ampliare il “Grafico delle prestazioni” ed aggiungere una tabella “Indicatori delle prestazioni” alla sua destra. In basso vogliamo creare e posizionare una tabella dei rendimenti mensili.
Innanzitutto aggiungiamo la tabella dei rendimenti mensili. Modifichiamo il file main.py
come segue.
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 con le statistiche delle performance stats per una data strategia
"""
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):
""" Restitusice un report HTML con le analisi
"""
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):
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):
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()
monthly_table.index = monthly_table.index.year
monthly_table['YTD'] = ((monthly_table + 1).prod(axis=1) - 1)
monthly_table = monthly_table * 100
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()
template.html
come segue:
App
Trading Backtest Report
{{ perf_chart|safe }}
{{ monthly_table|safe }}
report.html
con un browser visualizziamo la seguente pagina: 
Passiamo al calcolo delle prestazioni della strategia per la tabella KPI che posizioniamo in alto. Per calcolare i KPI usiamo, e quindi importiamo, la libreria FFN descritta in un precedente articolo.
Per produrre la tabella dei KPI dobbiamo aggiungere alla classe PerformanceReport
i nuovi metodi get_ffn_stats
e create_kpi_table
. Inoltre dobbiamo prevedere alcune modifiche al metodo generate_html
per richiamare i due nuovi metodi.
Di seguito riportiamo il codice completo per i 2 file main.py
e template.html
. In questa serie di articoli siamo riportando molti blocchi di codice per mostrare il processo di “costruzione” iterativo che dobbiamo fare quando aggiungiamo funzionalità al report.
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 con le statistiche delle performance stats per una data strategia
"""
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):
""" Restitusice un report HTML con le analisi
"""
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):
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):
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()
monthly_table.index = monthly_table.index.year
monthly_table['YTD'] = ((monthly_table + 1).prod(axis=1) - 1)
monthly_table = monthly_table * 100
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()
App
Trading Backtest Report
{{ perf_chart|safe }}
KPI Table
{{ kpi_table|safe }}
Monthly Returns
{{ monthly_table|safe }}
Dopo aver eseguito lo script otteniamo il seguente report.

Il report finale
Ora aggiungiamo nuovamente il grafico dei drawdown che abbiamo rimosso in precedenza e posizioniamolo sotto la tabella dei rendimenti mensili. Per ora impostiamo la dimensione del grafico pari alla larghezza dello schermo intero, ma possiamo cambiarlo in seguito se necessario. Dato che abbiamo già scritto il codice che crea l’oggetto grafico, e lo abbiamo già iniettato nel template HTML, non ci resta che aggiornare il file “template.html” con un paio di righe di codice.
App
Trading Backtest Report
{{ perf_chart|safe }}
KPI Table
{{ kpi_table|safe }}
Monthly Returns
{{ monthly_table|safe }}
{{ drawdown_chart|safe }}
template.html
ed eseguito nuovamente il file main.py
, otteniamo il seguente report. 
Il report sta iniziando a prendere forma. Siamo riusciti ad aggiungere alcuni dei componenti principali che troviamo in qualsiasi report delle performance di una strategia di trading. A questo punto, dobbiamo scegliere quali elementi/statistiche/grafici vogliamo aggiungere, in modo iterativo passo dopo passo.
Nel prossimo articolo descriviamo come calcolare, creare e inserire rapidamente un’intera gamma di componenti nel report. Dopodiché vediamo alcune funzionalità più complesse come i calcoli per il Var con Monte-Carlo e il bootstrapping del rendimento della strategia.
Codice completo
In questo articolo abbiamo descritto come implementare un report delle performance di una strategia in Python. Per il codice completo riportato in questo articolo, si può consultare il seguente repository di github:
https://github.com/datatrading-info/AnalisiDatiFinanziari