Nel precedente articolo della serie “Ambiente di Backtesting Event-Driven” è stato descritta la gerarchia della classe ExecutionHandler. In questo articolo si introduce l’implementazione delle metriche per misurare le prestazioni di una strategia usando la curva equity DataFrame precedentemente costruita nell’oggetto Portfolio.
Misurare le Performance
\(\begin{eqnarray*} S_A = \sqrt{N} \frac{\mathbb{E}(R_a – R_b)}{\sqrt{\text{Var} (R_a – R_b)}} \end{eqnarray*}\)
Dove \(R_a\) è il flusso dei rendimenti della curva equity e \(R_b\) è un indice di riferimento, come uno specifico tasso di interesse o un indice azionario. Il massimo drawdown e la durata del drawdown sono due ulteriori misure che gli investitori utilizzano per valutare il rischio in un portafoglio. Il primo rappresenta è più grande discesa, la correzione, da un precedente massimo relativo o massimo assoluto, della curva equity, mentre il secondo è definito come il numero di periodi di trading in cui si verifica. In questo articolo si implementa il Sharpe Ratio, il drawdown massimo e la durata del drawdown come misure delle prestazioni del portafoglio da utilizzare nella suite di Backtesting Event-Driven sviluppato in Python.Implementazione
performance.py
, che memorizzi le funzioni per calcolare il Sharpe Ratio e le informazioni sul drawdown. Come per la maggior parte delle classi che prevedono elevati carichi computanzionali, abbiamo bisogno di importare NumPy e Pandas:
# performance.py
import numpy as np
import pandas as pd
Il Sharpe Ratio è una misura del rischio/rendimento (in realtà è una delle tante!) e prevede un singolo parametro, cioè il numero di periodi da considerare per il ridimensionamento al valore annualizzato.
Di solito questo valore è impostato su 252, ovvero il numero di giorni di negoziazione (mercati aperti) negli Stati Uniti in un anno. Tuttavia, ad esempio, se la strategia apre e chiude posizioni all’interno di un’ora, si deve regolare lo Sharpe per annualizzarlo correttamente. Pertanto, è necessario impostare il periods
come 252 * 6.5 = 1638, ovvero il numero di ore di trading statunitensi in un anno. Se si effettua trading sul minuto, questo fattore deve essere impostato come 252 * 6.5 * 60 = 98280.
La funzione create_sharpe_ratio
opera su un oggetto Serie di Pandas che abbiamo chiamato returns
e calcola semplicemente il rapporto tra la media dei rendimenti percentuali del periodo e le deviazioni standard dei rendimenti percentuali ridimensionato in base al fattore periods
:
# performance.py
def create_sharpe_ratio(returns, periods=252):
"""
Crea il Sharpe ratio per la strategia, basato su a benchmark
pari a zero (ovvero nessuna informazione sui tassi privi di rischio).
Parametri:
returns - Una serie panda che rappresenta i rendimenti percentuali nel periodo.
periods - Giornaliero (252), orario (252 * 6,5), minuto (252 * 6,5 * 60) ecc.
"""
return np.sqrt(periods) * (np.mean(returns)) / np.std(returns)
Mentre il Sharpe Ratio indica il livello di rischio (definito dalla deviazione standard del patrimonio) per unità di rendimento, il “drawdown” è definito come la distanza tra un massimo relativo e un minimo relativo lungo una curva equity.
La funzione create_drawdowns
calcola sia il drawdown massimo che la durata massima di drawdown. Il primo è la discesa più elevata tra un massimo e minimo relativi, mentre il secondo è definito come il numero di periodi in cui questa discesa si verifica.
E’ necessario prestare molta attenzione nell’interpretazione della durata del drawdown in quanto questo fattore identifica i periodi di trading e quindi non è direttamente traducibile in un’unità temporale come “giorni”.
La funzione inizia creando due oggetti Serie di Pandas che rappresentano il drawdown e la durata di ogni “barra” di trading. Quindi viene stabilito l’attuale high water mark (HWM) determinando se la curva di equty supera tutti i picchi precedenti.
Il drawdown è quindi semplicemente la differenza tra l’attuale HWM e la curva di equity. Se questo valore è negativo, la durata viene aumentata per ogni barra che si verifica fino al raggiungimento del prossimo HWM. La funzione restituisce quindi semplicemente il massimo di ciascuna delle due serie:
# performance.py
def create_drawdowns(pnl):
"""
Calcola il massimo drawdown tra il picco e il minimo della curva PnL
così come la durata del drawdown. Richiede che il pnl_returns
sia una serie di pandas.
Parametri:
pnl - Una serie pandas che rappresenta i rendimenti percentuali del periodo.
Restituisce:
Drawdown, duration - Massimo drawdown picco-minimo e relativa durata.
"""
# Calcola la curva cumulativa dei rendimenti
# e imposta un "High Water Mark"
# Quindi crea le serie dei drawdown e relative durate
hwm = [0]
idx = pnl.index
drawdown = pd.Series(index = idx)
duration = pd.Series(index = idx)
# Ciclo sul range dell'indice
for t in range(1, len(idx)):
cur_hwm = max(hwm[t-1], pnl[t])
hwm.append(cur_hwm)
dd = (hwm[t] - pnl[t])
drawdown[t]= dd
duration[t]= (0 if drawdown[t] == 0 else duration[t-1] + 1)
return drawdown, drawdown.max(), duration.max()
Portfolio
, che è stata descritta in questo articolo.
Il primo compito è aprire portfolio.py
e importare le funzioni di performance:
# portfolio.py
.. # Other imports
from performance import create_sharpe_ratio, create_drawdowns
Portfolio
è una classe base astratta, si deve associare un metodo a una delle sue classi derivate, che in questo caso corrisponde a NaivePortfolio
. Quindi si crea un metodo chiamato output_summary_stats
che elabora la curva equity del portafoglio per generare le informazioni relative allo Sharpe e drawdown.
Il metodo è semplice. Utilizza semplicemente le due misure di performance e le applica direttamente al DataFrame Pandas relativo alla curva equity, restituendo le statistiche come una lista di tuple in un formato “user-friendly”:
# portfolio.py
..
..
class NaivePortfolio(object):
..
..
def output_summary_stats(self):
"""
Crea un elenco di statistiche di riepilogo per il portafoglio
come lo Sharpe Ratio e le informazioni sul drowdown.
"""
total_return = self.equity_curve['equity_curve'][-1]
returns = self.equity_curve['returns']
pnl = self.equity_curve['equity_curve']
sharpe_ratio = create_sharpe_ratio(returns)
drawdown, max_dd, dd_duration = create_drawdowns(pnl)
self.equity_curve['drawdown'] = drawdown
stats = [("Total Return", "%0.2f%%" % \
((total_return - 1.0) * 100.0)),
("Sharpe Ratio", "%0.2f" % sharpe_ratio),
("Max Drawdown", "%0.2f%%" % (max_dd * 100.0)),
("Drawdown Duration", "%d" % dd_duration)]
self.equity_curve.to_csv('equity.csv')
return stats
performance.py
e quindi incorporandoli in output_summary_stats
come richiesto.
Per il codice completo riportato in questo articolo, utilizzando il modulo di backtesting event-driven DataBacktest si può consultare il seguente repository di github:
https://github.com/datatrading-info/DataBacktest