Analisi delle Performance del Backtest

Analisi delle Performance del Backtest del Crossover delle Medie Mobili con Pandas

Sommario

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 del Backtest

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')
				
			
Analisi delle Performance del Backtest
				
					strat['rollhitratio'].plot(grid=True, figsize=(8,5),title='Rolling 1 Year Hit Ratio')
				
			
Analisi delle Performance del Backtest
				
					strat['roll12mret'].plot(grid=True, figsize=(8,5),title='Rolling 1 Year Returns')
				
			
Backtest-Analisi-RendimentiAnnualizzati
				
					strat['Strategy'].plot(grid=True, figsize=(8,5),title='Daily Returns')
				
			
Backtest-Analisi-RendimentiGiornalieri
				
					strat['Strategy'].plot(grid=True, figsize=(8,5),title='Daily Returns')
				
			
Backtest-Analisi-Distribuzione-Rendimentigiornalieri

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:

  1. Rendimento annualizzato
  2. Rendimento ultimi 12 mesi
  3. Volatilità
  4. Sharpe Ratio
  5. Drawdown massimo
  6. Calmar Ratio (Rendimento annualizzato / Drawdown massimo)
  7. Volatilità / Drawdown massimo
  8. Migliore performance mensile
  9. Peggior performance mensile
  10. % di mesi redditizi e % mesi non redditizi
  11. Numero di mesi redditizi/Numero di mesi non redditizi
  12. Profitto mensile medio
  13. Perdita mensile media
  14. Profitto mensile medio/Perdita mensile media

 

Prima di andare avanti, creiamo un nuovo dataframe per raccogliere i dati dei rendimenti della strategia su base mensile invece che su base giornaliera. Questo approccio facilita alcuni calcoli e consente di produrre una tabella dei rendimenti mensili. Per raggiungere questo obiettivo dobbiamo semplicemente “ricampionare” la colonna del dataframe originale dei rendimenti della strategia giornaliera.

 

				
					# 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))
				
			
Backtest-Analisi-ReportMensile-head

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

				
			
E infine usiamo le funzione 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')
				
			
Al fine di facilitare l’elaborazione, riconvertiamo questo oggetto in un “DataFrame” tramite la funzione .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.

Backtest-Analisi-ReportMensileAnnuo

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.

Backtest-Analisi-ReportMensileAnnuo1

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

Benvenuto su DataTrading!

Sono Gianluca, ingegnere software e data scientist. Sono appassionato di coding, finanza e trading. Leggi la mia storia.

Ho creato DataTrading per aiutare le altre persone ad utilizzare nuovi approcci e nuovi strumenti, ed applicarli correttamente al mondo del trading.

DataTrading vuole essere un punto di ritrovo per scambiare esperienze, opinioni ed idee.

SCRIVIMI SU TELEGRAM

Per informazioni, suggerimenti, collaborazioni...

Torna in alto
Scroll to Top