In questo articolo descriviamo come effettuare l’analisi delle performance del backtest della strategia di crossover delle medie mobili descritto nell’articolo precedente, tramite gli strumenti del modulo Pandas di Python. In particolare vediamo come analizzare la curva di equity della strategia ed implementare alcuni dei principali indicatori di performance di una strategia e alcuni dati (si spera) interessanti.
Creare la curva di equity
Per completezza, riportiamo tutto il codice python necessario per produrre i risultati del backtest della strategia per effettuare l’analisi insieme al grafico della curva equity, al solo scopo di assicurarci di averlo eseguito correttamente.
import pandas as pd
import numpy as np
from math import sqrt
import matplotlib.pyplot as plt
import yfinance as yf
# scarico di dati da Yahoo Finance in un dataframe e calcolo delle medie mobili
sp500 = yf.download('^GSPC', start='2000-01-01', end='2020-01-01')
sp500['42d'] = np.round(sp500['Close'].rolling(window=42).mean(),2)
sp500['252d'] = np.round(sp500['Close'].rolling(window=252).mean(),2)
# Creo la colonna con la differenza tra le medie mobili
sp500['42-252'] = sp500['42d'] - sp500['252d']
# importo il numero di punti come soglia dello spread tra le medie mobili
# e creo la colonna con lo 'Stance' della strategia
X = 50
sp500['Stance'] = np.where(sp500['42-252'] > X, 1, 0)
sp500['Stance'] = np.where(sp500['42-252'] < -X, -1, sp500['Stance'])
sp500['Stance'].value_counts()
# creo le colonne con i rendimenti logaritmici giornalieri dei prezzi e della strategia
sp500['Market Returns'] = np.log(sp500['Close'] / sp500['Close'].shift(1))
sp500['Strategy'] = sp500['Market Returns'] * sp500['Stance'].shift(1)
# importo l'equity iniziare della strategia a 1 (100%) e genero la curva di equity
sp500['Strategy Equity'] = sp500['Strategy'].cumsum() + 1
# grafico della curva di qquity
sp500['Strategy Equity'].plot()
Analisi delle Performance
Per effettuare l’analisi delle performance del backtest implementiamo i seguenti indicatori e visualizziamo l’andamento grafico:
- Volatilità annualizzata a finestra mobile
- Rapporto di Hit di 1 anno a finestra mobile
- Rendimenti di 1 anno a finestra mobile
- Rendimenti giornalieri
- Istogramma della distribuzione dei rendimenti giornalieri
Creiamo quindi un dataframe Pandas che contiene solo i dati di cui abbiamo bisogno, cioè la curva equity della strategia e i rendimenti giornalieri della strategia.
strat = pd.DataFrame([sp500['Strategy Equity'], sp500['Strategy']]).transpose()
A questo punto dobbiamo costruire un dataframe che raccoglie tutti i dati grezzi di cui avremo bisogno per calcolare e visualizzare le serie di indicatori sopra elencati.
# crea le colonne che identificato i giorni con rendimenti positivi, negativi o flat
strat['win'] = (np.where(strat['Strategy'] > 0, 1,0))
strat['loss'] = (np.where(strat['Strategy'] < 0, 1,0))
strat['scratch'] = (np.where(strat['Strategy'] == 0, 1,0))
# crea le colonne con le somme comulative dei rendimenti giornalieri
strat['wincum'] = (np.where(strat['Strategy'] > 0, 1,0)).cumsum()
strat['losscum'] = (np.where(strat['Strategy'] < 0, 1,0)).cumsum()
strat['scratchcum'] = (np.where(strat['Strategy'] == 0, 1,0)).cumsum()
# crea una colonna che somma i rendimenti dei giorni di trading
# usiamo questa colonna per creare le percentuali
strat['days'] = (strat['wincum'] + strat['losscum'] + strat['scratchcum'])
# crea le colonne che mostra la somma dei giorni positivi, negativi e flat con finestra mobile a 252 giorni
strat['rollwin'] = strat['win'].rolling(window=252).sum()
strat['rollloss'] = strat['loss'].rolling(window=252).sum()
strat['rollscratch'] = strat['scratch'].rolling(window=252).sum()
# crea le colonne con i dati del hit ratio e loss ratio
strat['hitratio'] = strat['wincum'] / (strat['wincum']+strat['losscum'])
strat['lossratio'] = 1 - strat['hitratio']
# crea le colonne con i dati del hit ratio e loss ratio con finestra mobile a 2252 giorni
strat['rollhitratio'] = strat['hitratio'].rolling(window=252).mean()
strat['rolllossratio'] =1 - strat['rollhitratio']
# crea la colonna che i redimenti a finestra mobile di 12 mesi
strat['roll12mret'] = strat['Strategy'].rolling(window=252).sum()
# crea le colonne con le vincite medie, le perdite medie e i redimenti medi giornalieri
strat['averagewin'] = strat['Strategy'][(strat['Strategy'] > 0)].mean()
strat['averageloss'] = strat['Strategy'][(strat['Strategy'] < 0)].mean()
strat['averagedailyret'] = strat['Strategy'].mean()
# crea le colonne con la deviazione standard e la deviazione standard annualizzate
# con finestra mobile a 1 anno
strat['roll12mstdev'] = strat['Strategy'].rolling(window=252).std()
strat['roll12mannualisedvol'] = strat['roll12mstdev'] * sqrt(252)
Dopo aver calcolato questi dati possiamo per tracciare i rispettivi grafici.
strat['roll12mannualisedvol'].plot(grid=True, figsize=(8,5),title='Rolling 1 Year Annualised Volatility')
strat['rollhitratio'].plot(grid=True, figsize=(8,5),title='Rolling 1 Year Hit Ratio')
strat['roll12mret'].plot(grid=True, figsize=(8,5),title='Rolling 1 Year Returns')
strat['Strategy'].plot(grid=True, figsize=(8,5),title='Daily Returns')
strat['Strategy'].plot(grid=True, figsize=(8,5),title='Daily Returns')
Come integrazione possiamo dare un’occhiata alla skewness e kurtosis, descritti in un articolo precedente, della distribuzione dei rendimenti giornalieri.
print("Skew:",round(strat['Strategy'].skew(),4))
print("Kurtosis:",round(strat['Strategy'].kurt(),4))
Skew: -0.0211
Kurtosis: 9.9504
Vediamo che la distribuzione dei rendimenti giornalieri è tutt’altro che normale e mostra un’inclinazione leggermente negativa e un’elevata curtosi (dato che l’inclinazione della distribuzione normale è 0 e la curtosi della distribuzione normale è 3).
Approfondiamo l’analisi e produciamo alcuni indicatori chiave della prestazione (KPI) che troviamo solitamente insieme all’analisi dei rendimenti di qualsiasi strategia di trading. Questi indicatori non sono esaustivi, ma coprono la maggior parte delle principali aree.
Vogliamo implementare i seguenti indicatori:
- Rendimento annualizzato
- Rendimento ultimi 12 mesi
- Volatilità
- Sharpe Ratio
- Drawdown massimo
- Calmar Ratio (Rendimento annualizzato / Drawdown massimo)
- Volatilità / Drawdown massimo
- Migliore performance mensile
- Peggior performance mensile
- % di mesi redditizi e % mesi non redditizi
- Numero di mesi redditizi/Numero di mesi non redditizi
- Profitto mensile medio
- Perdita mensile media
- Profitto mensile medio/Perdita mensile media
# Crea un nuovo DataFrame per i dati mensili e popolalo con i dati della colonna dei rendimenti
# giornalieri del DataFrame originale e sommati per mese
stratm = pd.DataFrame(strat['Strategy'].resample('M').sum())
# Costruisce la curva equity mensile
stratm['Strategy Equity'] = stratm['Strategy'].cumsum()+1
# Crea un indice numerico per i mesi (es. Gen = 1, Feb = 2 etc)
stratm['month'] = stratm.index.month
Stampiamo quindi il primi 15 elementi del dataframe
print(stratm.head(15))
Iniziamo quindi ad elaborare l’elenco dei KPI
print("\n1) Rendimento annualizzato")
days = (strat.index[-1] - strat.index[0]).days
cagr = ((((strat['Strategy Equity'][-1]) / strat['Strategy Equity'][1])) ** (365.0 / days)) - 1
print('CAGR =', str(round(cagr, 4) * 100) + "%")
print("\n2) Rendimenti ultimi 12 mesi")
stratm['last12mret'] = stratm['Strategy'].rolling(window=12, center=False).sum()
last12mret = stratm['last12mret'][-1]
print('last 12 month return =', str(round(last12mret * 100, 2)) + "%")
print("\n3) Volatilità")
voldaily = (strat['Strategy'].std()) * sqrt(252)
volmonthly = (stratm['Strategy'].std()) * sqrt(12)
print('Annualised volatility using daily data =', str(round(voldaily, 4) * 100) + "%")
print('Annualised volatility using monthly data =', str(round(volmonthly, 4) * 100) + "%")
print("\n4) Sharpe Ratio")
dailysharpe = cagr / voldaily
monthlysharpe = cagr / volmonthly
print('daily Sharpe =', round(dailysharpe, 2))
print('monthly Sharpe =', round(monthlysharpe, 2))
print("\n5) Maxdrawdown")
# Funzione per calcolare il drawdown massimo
def max_drawdown(X):
mdd = 0
peak = X[0]
for x in X:
if x > peak:
peak = x
dd = (peak - x) / peak
if dd > mdd:
mdd = dd
return mdd
mdd_daily = max_drawdown(strat['Strategy Equity'])
mdd_monthly = max_drawdown(stratm['Strategy Equity'])
print('max drawdown daily data =', str(round(mdd_daily, 4) * 100) + "%")
print('max drawdown monthly data =', str(round(mdd_monthly, 4) * 100) + "%")
print("\n6) Calmar Ratio")
calmar = cagr / mdd_daily
print('Calmar ratio =', round(calmar, 2))
print("\n7) Volatilitè / Drawdown Massimo")
vol_dd = volmonthly / mdd_daily
print('Volatility / Max Drawdown =', round(vol_dd, 2))
print("\n8) Migliore performance mensile")
bestmonth = max(stratm['Strategy'])
print('Best month =', str(round(bestmonth, 2)) + "%")
print("\n9) Peggior performance mensile")
worstmonth = min(stratm['Strategy'])
print('Worst month =', str(round(worstmonth, 2) * 100) + "%")
print("\n10) % di mesi redditizi e % mesi non redditizi")
positive_months = len(stratm['Strategy'][stratm['Strategy'] > 0])
negative_months = len(stratm['Strategy'][stratm['Strategy'] < 0])
flatmonths = len(stratm['Strategy'][stratm['Strategy'] == 0])
perc_positive_months = positive_months / (positive_months + negative_months + flatmonths)
perc_negative_months = negative_months / (positive_months + negative_months + flatmonths)
print('% of Profitable Months =', str(round(perc_positive_months, 2) * 100) + "%")
print('% of Non-profitable Months =', str(round(perc_negative_months, 2) * 100) + "%")
print("\n11) Numero di mesi redditizi/Numero di mesi non redditizi")
prof_unprof_months = positive_months / negative_months
print('Number of Profitable Months/Number of Non Profitable Months', round(prof_unprof_months, 2))
print("\n12) Profitto mensile medio")
av_monthly_pos = (stratm['Strategy'][stratm['Strategy'] > 0]).mean()
print('Average Monthly Profit =', str(round(av_monthly_pos, 4) * 100) + "%")
print("\n13) Perdita mensile media")
av_monthly_neg = (stratm['Strategy'][stratm['Strategy'] < 0]).mean()
print('Average Monthly Loss =', str(round(av_monthly_neg * 100, 2)) + "%")
print("\n14) Profitto mensile medio/Perdita mensile media")
pos_neg_month = abs(av_monthly_pos / av_monthly_neg)
print('Average Monthly Profit/Average Monthly Loss', round(pos_neg_month, 4))
Otteniamo i seguenti risultati
1) Rendimento annualizzato
CAGR = 2.81%
2) Rendimenti ultimi 12 mesi
last 12 month return = -3.48%
3) Volatilità
Annualised volatility using daily data = 18.22%
Annualised volatility using monthly data = 14.77%
4) Sharpe Ratio
daily Sharpe = 0.15
monthly Sharpe = 0.19
5) Maxdrawdown
max drawdown daily data = 37.059999999999995%
max drawdown monthly data = 33.650000000000006%
6) Calmar Ratio
Calmar ratio = 0.08
7) Volatilitè / Drawdown Massimo
Volatility / Max Drawdown = 0.4
8) Migliore performance mensile
Best month = 0.19%
9) Peggior performance mensile
Worst month = -10.0%
10) % di mesi redditizi e % mesi non redditizi
% of Profitable Months = 53.0%
% of Non-profitable Months = 42.0%
11) Numero di mesi redditizi/Numero di mesi non redditizi
Number of Profitable Months/Number of Non Profitable Months 1.24
12) Profitto mensile medio
Average Monthly Profit = 3.29%
13) Perdita mensile media
Average Monthly Loss = -3.34%
14) Profitto mensile medio/Perdita mensile media
Average Monthly Profit/Average Monthly Loss 0.9855
resample
di Pandas per creare un dataframe dei rendimenti mensili.
Il primo passaggio consiste nel crea una tabella pivot e ricampionarla per ottenre un oggetto noto come pandas.tseries.resample.DatetimeIndexResampler
.
monthly_table = stratm[['Strategy','month']].pivot_table(stratm[['Strategy','month']], index=stratm.index, columns='month', aggfunc=np.sum).resample('A')
.aggregate()
.
monthly_table = monthly_table.aggregate('sum')
Infine procediamo a convertire le date dell’indice in modo che mostrino solo l’anno anziché la data completa, e quindi sostituire le intestazioni delle colonne dei mesi dal formato numerico nell’appropriato formato “MMM”.
Dobbiamo anche eliminare la colonna con uno dei livelli dell’indice identificato dalla parola “Strategy”. In questo modo otteniamo una tabella con un solo indice di colonne che corrisponde alle rappresentazioni numeriche dei mesi.
# Elimina l'indice della colonna di livello superiore che corrispone a "Strategy"
monthly_table.columns = monthly_table.columns.droplevel()
Otteniamo la seguente tabella.
Non ci resta che cambiare l’indice delle date per visualizzare mostrare in un formato annuale (YYYY) e le restanti intestazioni di colonna per mostrare un formato mensile (MMM).
# Sostituisce le date nell'indice con l'anno corrispondente
monthly_table.index = monthly_table.index.year
# Sostituisce l'intero nell'intestazione delle colonne con il formato MMM
monthly_table.columns = ['Gen','Feb','Mar','Apr','Mag','Giu','Lug','Ago','Set','Ott','Nov','Dic']
Otteniamo la seguente tabella dei rendimenti mensili.
Codice completo
In questo articolo abbiamo descritto come effettuare l’analisi delle performance del backtest della strategia di crossover delle medie mobili. Per il codice completo riportato in questo articolo, si può consultare il seguente repository di github:
https://github.com/datatrading-info/Backtest_Strategie