Backtesting baseado em eventos com Python - Parte VII

Autora:Bem-estar, Criado: 2019-03-26 10:52:49, Atualizado:

No último artigo da série Event-Driven Backtester, consideramos uma hierarquia básica do ExecutionHandler. Neste artigo vamos discutir como avaliar o desempenho de uma estratégia após o backtest usando a curva de equidade DataFrame construída anteriormente no objeto Portfolio.

Métricas de desempenho

O índice Sharpe é calculado através de:img

Em que Ra é o fluxo de rendimentos da curva de participação e Rb é um índice de referência, como uma taxa de juro adequada ou um índice de participação.

A duração máxima e a duração máxima são duas medidas adicionais que os investidores usam frequentemente para avaliar o risco de uma carteira.

Neste artigo, implementaremos o rácio Sharpe, o drawdown máximo e a duração do drawdown como medidas do desempenho do portfólio para uso no pacote de backtesting baseado em Python.

Implementação do Python

A primeira tarefa é criar um novo arquivoperformance.py, que armazena as funções para calcular a taxa de Sharpe e informações de drawdown.

# performance.py

import numpy as np
import pandas as pd

Observe-se que o rácio Sharpe é uma medida do risco/recompensa (de facto, é uma de muitas!) e tem um único parâmetro, o do número de períodos a ajustar quando se escala para o valor anualizado.

Normalmente, este valor é definido em 252, que é o número de dias de negociação nos EUA por ano. No entanto, se sua estratégia opera dentro da hora, você precisa ajustar o Sharpe para anuálizá-lo corretamente. Assim, você precisa definir os períodos em 2526.5=1638, que é o número de horas de negociação dos EUA dentro de um ano. Se você negociar em uma base minuciosa, então esse fator deve ser definido em 2526.560=98280.

A função create_sharpe_ratio opera em um objeto da série pandas chamado retornos e simplesmente calcula a razão da média dos retornos percentuais do período e dos desvios padrão de retornos percentuais do período escalados pelo fator de períodos:

# performance.py

def create_sharpe_ratio(returns, periods=252):
    """
    Create the Sharpe ratio for the strategy, based on a 
    benchmark of zero (i.e. no risk-free rate information).

    Parameters:
    returns - A pandas Series representing period percentage returns.
    periods - Daily (252), Hourly (252*6.5), Minutely(252*6.5*60) etc.
    """
    return np.sqrt(periods) * (np.mean(returns)) / np.std(returns)

Enquanto o rácio Sharpe caracteriza a quantidade de risco (conforme definido pelo desvio padrão do percurso do ativo) assumido por unidade de retorno, o drawdown é definido como a maior queda de pico para fundo ao longo de uma curva de ações.

A função create_drawdowns abaixo realmente fornece tanto o drawdown máximo quanto a duração máxima do drawdown. O primeiro é a maior queda de pico a fundo mencionada, enquanto o último é definido como o número de períodos em que essa queda ocorre.

É necessária alguma sutileza na interpretação da duração do drawdown, uma vez que contabiliza períodos de negociação e, portanto, não é diretamente traduzível numa unidade temporal como dias.

A função começa criando dois objetos da série pandas que representam o drawdown e a duração em cada negociação bar.

O drawdown é então simplesmente a diferença entre o HWM atual e a curva de equidade. Se este valor for negativo, a duração é aumentada para cada barra que isso ocorre até que o próximo HWM seja atingido. A função então simplesmente retorna o máximo de cada uma das duas séries:

# performance.py

def create_drawdowns(equity_curve):
    """
    Calculate the largest peak-to-trough drawdown of the PnL curve
    as well as the duration of the drawdown. Requires that the 
    pnl_returns is a pandas Series.

    Parameters:
    pnl - A pandas Series representing period percentage returns.

    Returns:
    drawdown, duration - Highest peak-to-trough drawdown and duration.
    """

    # Calculate the cumulative returns curve 
    # and set up the High Water Mark
    # Then create the drawdown and duration series
    hwm = [0]
    eq_idx = equity_curve.index
    drawdown = pd.Series(index = eq_idx)
    duration = pd.Series(index = eq_idx)

    # Loop over the index range
    for t in range(1, len(eq_idx)):
        cur_hwm = max(hwm[t-1], equity_curve[t])
        hwm.append(cur_hwm)
        drawdown[t]= hwm[t] - equity_curve[t]
        duration[t]= 0 if drawdown[t] == 0 else duration[t-1] + 1
    return drawdown.max(), duration.max()

Para utilizar estas medidas de desempenho, precisamos de um meio de os calcular depois de um backtest ter sido realizado, ou seja, quando uma curva de equidade adequada estiver disponível!

Dado que as medidas de desempenho são calculadas com base em uma carteira, faz sentido anexar os cálculos de desempenho a um método na hierarquia de classes de carteira que discutimos neste artigo.

A primeira tarefa é abrirportfolio.pycomo discutido no artigo anterior e importar as funções de desempenho:

# portfolio.py

..  # Other imports

from performance import create_sharpe_ratio, create_drawdowns

Como Portfolio é uma classe base abstrata, queremos anexar um método a uma de suas classes derivadas, que neste caso será NaivePortfolio.

O método é simples: utiliza simplesmente as duas medidas de desempenho e aplica-as diretamente à curva de equidade Panda DataFrame, produzindo as estatísticas como uma lista de tuplas de forma amigável ao formato:

# portfolio.py

..
..

class NaivePortfolio(object):

    ..
    ..

    def output_summary_stats(self):
        """
        Creates a list of summary statistics for the portfolio such
        as Sharpe Ratio and drawdown information.
        """
        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)
        max_dd, dd_duration = create_drawdowns(pnl)

        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)]
        return stats

É evidente que esta é uma análise de desempenho muito simples para uma carteira.performance.pye, em seguida, incorporá-los em output_summary_states conforme necessário.


Mais.