Questo articolo è la quarta parte della “mini-serie” che descrivere come implementare un report delle performance di una strategia in Python per verificare i risultati di un backtest. La serie offre una panoramica per creare un programma che genera un report delle prestazioni in un formato HTML piacevole e dall’aspetto elegante, da visualizzare in un brower. Partiamo dal codice descritto nell‘articolo precedente nei file main.py
e template.html
e ricreiamo la struttura delle cartelle e dei file previsti dal progetto, come descritto nella prima parte di serie. Siamo pronti a proseguire con ulteriori modifiche.
Come anticipato nel precedente articolo, vediamo come aggiungere un po’ di funzionalità avanzate e molto utili per la creazione di report delle prestazioni di una strategia. Molti trader sistematici sono interessati alle analisi Monte Carlo e alle informazioni che può offrire, oltre a quelle statistiche create dalla serie di rendimenti effettivi della strategia di investimento/trading in esame.
Le simulazioni Monte Carlo
Il motivo principale per cui usiamo i metodi Monte Carlo è provare a modellare la “incertezza” e vedere le distribuzioni di risultati possibili ed alternativi, invece che le stime puntuali. In altre parole, la serie di rendimenti inseriti nel file “data.csv” è un possibile andamento dei rendimenti effettivi, sia che si tratti dell’output di un backtest simulato, o addirittura di un track record di strategia dal vivo. In teoria ci sono infiniti possibili andamenti che la serie dei rendimenti avrebbe potuto avere lungo il tempo. I movimenti dei mercati sottostanti, come per ogni cosa nella vita, sono soggetti a un elemento di rumore e casualità. In effetti, le probabilità che la strategia funzioni esattamente allo stesso modo in futuro sono esattamente pari a zero.
Questo concetto potrebbe essere difficile da capire. Perché la probabilità che qualcosa accada è pari a zero quando è evidente che è già accaduto in passato? Un altro modo di pensare è il seguente. Se qualcuno vi chiedesse di prevedere quale sarà il prezzo dell’indice S&P 500 alla fine del prossimo anno, come rispondereste? Prova a indovinare e dai loro una cifra? Se lo fai ti sbaglieresti, te lo garantisco.
La logica deriva dal fatto che i rendimenti degli asset finanziari e delle strategie di investimento sono considerati “variabili aleatorie continue”. Vale a dire che possono assumere un numero infinito di possibili valori. La strategia potrebbe restituire il 10% in un anno… o potrebbe essere il 10,5%, o in realtà potrebbe essere il 10,52%… che ne dici del 10,528%? Possiamo continuare ed ottenere una cifra sempre più precisa quindi in teoria ci sono un numero infinito di possibili risultati. Se esiste un numero infinito di possibili risultati per una variabile, le possibilità che riusciamo ad individuare il risultato finale sono pari a zero.
In questo scenario entrano in gioco le funzioni di densità di probabilità. Queste funzioni permettono di assegnare una probabilità che un qualsiasi valore, di un insieme continuo di valori, possa verificarsi. Puoi consultare questa documentazione se vuoi approfondire i concetti matematici. Cosa c’entra tutto questo con la simulazione Monte Carlo? Vogliamo usare la simulazione MC per incorporare “incertezza/casualità” nel modello. Possiamo usare questo modello per simulare un gran numero di possibili risultati che potrebbero verificarsi realisticamente, a partire dalle caratteristiche alla base della strategia.
Queste molteplici simulazioni, se modellate correttamente, permetto di produrre una “distribuzione” dei risultati, invece di fare affidamento su un unico “risultato puntuale” che è stato prodotto dai dati contenuti nel file csv. Da questa distribuzione dei risultati, possiamo iniziare ad assegnare le probabilità alle possibilità di finire entro determinati intervalli di detti risultati.
Calcolare e visualizzare le simulazioni
Iniziamo a lavorare sul codice con un esempio pratico in modo da rendere più chiaro l’approccio per costruire un report delle performance di una strategia in Python. La prima cosa da fare è aggiornare il nostro codice con un nuovo metodo che prende l’equity della strategia come input e restituisce un DataFrame contenente un numero specifico di simulazioni Monte Carlo. Le simulazioni sono generate tramite un modello basato sul rendimento sottostante e sulle caratteristiche di volatilità della stessa serie di equity.
Dopo aver ottenuto l’output della simulazione, possiamo aggiungere un altro metodo per generare un grafico e visualizzare i risultati.
Il codice aggiornato per il file main.py
è il seguente.
import os
import math
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 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())
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):
""" 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',
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()
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)
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):
daily_returns = np.random.normal(mu / T, vol / math.sqrt(T), T) + 1
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()
template.html
è aggiornato come segue.
App