O padrão gráfico 123 é caracterizado por três candles em sequência, onde o segundo apresenta a menor mínima entre eles. Veja pela imagem a seguir, onde um padrão 123 foi formado em BBDC4 no dia 13 de Maio:
Nesse primeiro backtest realizaremos o sistema operacional clássico para essa estratégia, como demostrado na figura acima, com o seguinte conjunto de regras:
Timeframe: diário;
Tipo de operação: ponta da compra;
Ponto de entrada: padrão 123 formado no Éden dos Traders, a entrada ocorrerá no rompimento da máxima do candle sinal;
Alvo: amplitude do 123, isto é, a diferença entre a menor mínima e a maior máxima do conjunto dos 3 candles, projetada a partir do rompimento do candle sinal (terceiro candle);
Stop: abaixo da mínima do conjunto de 3 candles.
Motivação
A ideia por trás do 123 é comprar um fundo em uma tendência de alta. Um fundo é caracterizado justamente por uma mínima menor que a dos candles vizinhos. Já a condição do "Éden dos Traders" é o que configura a tendência.
Como estamos em tendência de alta, podemos esperar uma perna de alta de tamanho igual ou maior que a perna de baixa. Daí derivamos a amplitude do 123 como alvo. Por fim, o stop é colocado logo abaixo do fundo, pois uma tendência de alta pressupõe fundos mais altos e topos mais altos.
Agora que já desmembramos a estratégia, vamos à análise!
Importando os dados necessários
Como de praxe, importaremos de uma vez todas as bibliotecas necessárias para o código:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
Avaliaremos o desempenho da estratégia nos últimos 5 anos de BBDC4.
Os dados a seguir foram extraídos do MetaTrader e o arquivo estará disponível no nosso grupo do Telegram. Basta baixá-lo em seu computador e, através da função read_csv, parseá-lo em um dataframe.
Com a base de dados em dataframe, o segundo passo é identificar todos os padrões 123 formados nesses últimos 5 anos.
Definindo o candle sinal
Para um candle sinal, sua mínima deve ser maior que a mínima do candle anterior (condition_1) que, por sua vez, deve ter a mínima menor que a do candle anterior a ele (condition_2).
Lembrando que a função shift do pandas seleciona o valor deslocado pelo número de períodos passado como argumento.
Logo, se ambas condições forem respeitadas, o candle é considerado um candle sinal. Armazenaremos essas informações em uma coluna, que será utilizada mais tarde para definirmos o preço de compra das operações.
Bem simples, não? Porém, não podemos esquecer que nem todos os padrões 123 identificados gerarão uma entrada, uma vez que, além de ter a máxima do candle sinal rompida, eles precisam estar no Éden dos Traders. Então vamos calculá-lo.
OBS: se você ainda não sabe o que é o "Éden dos Traders", leia esse artigo.
Calculando o Éden dos Traders
A função utilizada para o cálculo das médias móveis exponenciais é a mesma do artigo mencionado acima (ewm), com a diferença que especificaremos o número mínimo de candles necessários para o cálculo da média (min_periods).
Por conta disso, removeremos todos os valores NaN através da função dropna. Note, então, que os primeiros 80 data points do nosso intervalo serão descartados.
Para definirmos o preço de compra, observaremos os dados da perspectiva do candle de entrada (candle seguinte ao candle sinal).
Dessa forma, nós só entraremos em uma operação caso três condições sejam respeitadas:
condition_1: o candle anterior tem que ser um candle sinal;
condition_2: o candle sinal tem que estar no éden dos traders;
condition_3: sua máxima tem que ser maior do que a máxima do candle sinal.
Além disso, temos que considerar também que o candle pode abrir em gap (acima do ponto de entrada).
Portanto, o preço de compra será armazenado em uma nova coluna buy_price através da função where com a seguinte lógica:
se as três condições a cima forem respeitadas;
e se o candle de entrada abrir acima da máxima do candle sinal (df["open"] > df["high"].shift(1)), o preço de compra será igual ao preço de abertura (df["open"]), caso contrário o preço de compra será igual à máxima acrescido de 1 centavo (df["high"].shift(1) + tick), caracterizando o rompimento da máxima do candle sinal;
caso nenhuma das condições anteriores forem respeitadas, o valor será preenchido por NaN (np.nan)
Em sequência, vamos definir o preço de saída da operação.
Definindo o alvo
O primeiro passo para calcular o alvo é isolar a menor mínima e a maior máxima de uma janela de 3 períodos e calcular a diferença entre eles. Faremos isso combinando a função rolling com max para isolar o valor máximo, armazenando-o a variável max_high, e min para calcular o valor mínimo (min_low).
O nosso alvo (target) portanto, será a diferença entre esses valores (amplitude) projetada a partir do ponto de entrada (entry), ou seja, somado ao rompimento da máxima do candle sinal.
Por fim, temos que estabelecer o stop da operação.
Definindo o stop
O stop será definido no rompimento da menor mínimo do conjunto dos 3 candles, que nesse caso representa a mínima do segundo candle do padrão 123.
df["stop"] = df["low"].shift(2) - tick
df.tail()
open
high
low
close
signal
mme8
mme80
eden
buy_price
target
stop
datetime
2021-07-08 00:00:00
24.49
24.77
24.24
24.59
False
25.129472
24.889629
False
NaN
25.97
24.42
2021-07-12 00:00:00
24.72
25.36
24.69
25.21
True
25.147367
24.897539
True
NaN
25.51
24.49
2021-07-13 00:00:00
25.01
25.40
24.81
25.19
False
25.156841
24.904760
True
25.37
26.48
24.23
2021-07-14 00:00:00
25.50
25.99
25.11
25.30
False
25.188654
24.914519
True
NaN
26.56
24.68
2021-07-15 00:00:00
25.22
25.36
24.70
24.89
False
25.122287
24.913914
False
NaN
27.29
24.80
Pronto! Estabelecemos os principais pontos da nossa estratégia: o preço de compra, o alvo e o stop. Com esses três preços definidos, podemos escrever o algoritmo que irá simular as operações.
o stop e o alvo serão iguais ao valor de sua respectiva coluna;
ignoraremos operações que deram entrada e bateram no stop no mesmo dia (já que não sabemos o que aconteceu primeiro). Sendo assim, só continuaremos na operação se a mínima do candle de entrada for maior que o stop (df["low"][i] > stop).
import math
# Create a function to round any number to the smalles multiple of 100
def round_down(x):
return int(math.floor(x / 100.0)) * 100
def backtest_algorithm(
df,
capital_exposure,
initial_capital):
# List with the total capital after every operation
total_capital = [initial_capital]
# List with profits for every operation
all_profits = []
ongoing = False
for i in range(0,len(df)):
if ongoing == True:
if (df["open"][i] >= target) | (df["open"][i] <= stop):
exit = df["open"][i]
profit = shares * (exit - entry)
# Append profit to list and create a new entry with the capital
# after the operation is complete
all_profits += [profit]
current_capital = total_capital[-1] # current capital is the last entry in the list
total_capital += [current_capital + profit]
ongoing = False
elif df["low"][i] <= stop:
exit = stop
profit = shares * (exit - entry)
# Append profit to list and create a new entry with the capital
# after the operation is complete
all_profits += [profit]
current_capital = total_capital[-1] # current capital is the last entry in the list
total_capital += [current_capital + profit]
ongoing = False
elif df["high"][i] >= target:
exit = target
profit = shares * (exit - entry)
# Append profit to list and create a new entry with the capital
# after the operation is complete
all_profits += [profit]
current_capital = total_capital[-1] # current capital is the last entry in the list
total_capital += [current_capital + profit]
ongoing = False
else:
if ~(np.isnan(df["buy_price"][i])):
entry = df["buy_price"][i]
stop = df["stop"][i]
if df["low"][i] > stop:
ongoing = True
risk = entry - stop
target = df["target"][i]
shares = round_down(capital_exposure / risk)
return all_profits, total_capital
Calculando a estatística e a curva de capital
Além do algoritmo acima, utilizaremos também as funções get_drawdown, strategy_test e capital_plot para calcular a estatística e plotar a evolução do capital durante o backtest.
Se você não sabe o que é drawdown, temos um artigo específico sobre que vale a leitura.
Todas as funções a seguir foram elaboradas ao longo dos backtests aqui desenvolvidos.
def capital_plot(total_capital, all_profits):
all_profits = [0] + all_profits # make sure both lists are the same size
cap_evolution = pd.DataFrame({'Capital': total_capital, 'Profit': all_profits})
plt.title("Curva de Capital")
plt.xlabel("Total Operações")
cap_evolution['Capital'].plot()
Uma vez que já temos todas as funções definidas, vamos ao que interessa: rodar o backtest!
Realizando o backtest
No backtest a seguir, entraremos sempre com um capital fixo de R$100.000 (initial_capital) e um risco controlado de R$1.000 (capital_exposure).
O gráfico não ficou dos melhores, mas pela estatística conseguimos observar que a estratégia obteve uma boa taxa de acerto (54%) e um drawdown aceitável (9.52%). Entretanto, o lucro por operação não foi nada satisfatório (0.07%).
Vamos calular o valor esperado por operação da nossa estratégia para entender esses números. Essa métrica foi dissecada no artigo mencionado na introdução.
Calculando o valor esperado por operação
O EV apresenta a seguinte fórmula matemática:
EV=Pgain×payoff+Ploss×risk
Onde Pgain é a probabilidade de acerto (pct_gains) e Ploss é a probabilidade de erro (pct_losses).
Criaremos uma função expected_value para calculá-lo, que irá receber apenas um argumento (all_profits) e retornar um dataframe com os resutados mais importantes: média de ganho, perda e o valor esperado.
def expected_value(all_profits):
all_positives = [x for x in all_profits if x >= 0]
average_gain = sum(all_positives) / len(all_positives)
all_negatives = [x for x in all_profits if x < 0]
average_loss = sum(all_negatives) / len(all_negatives)
num_operations = len(all_profits)
pct_gains = (len(all_positives) / num_operations)
pct_losses = 1 - pct_gains
expected_value = (average_gain * pct_gains) + (average_loss * pct_losses)
result = pd.DataFrame.from_dict({
"average_gain": average_gain,
"average_loss": average_loss,
"pct_gains": pct_gains,
"pct_losses": pct_losses,
"expected_value": expected_value
}, orient="index")
result.columns = ["result"]
return result
ev = expected_value(all_profits)
ev
result
average_gain
965.269231
average_loss
-988.409091
pct_gains
0.541667
pct_losses
0.458333
expected_value
69.833333
Como podemos observar pelos valores acima, a relação de ganho e perda da estratégia é praticamente 1:1. Isto é, quando a operação vai pro lucro, ganhamos em média R$965,27 ao passo que quando a operação vai pro prejuízo, perdemos em média R$988,41.
Ao final, obtivemos um EV positivo mas baixo onde o risco-retorno é insatisfatório.
Sendo assim, tentaremos otimizar esse resultado filtrando ainda mais a nossa entrada: entraremos em um operação de padrão 123 somente se o candle sinal for um inside bar.
Backtest com o candle sinal sendo um inside bar
Um candle é dito inside bar quando está contido no candle anterior. Assim, o candle sinal deve apresentar não só mínima menor do que do candle anterior (condition_1), mas também máxima menor (condition_3). Perceba pela figura a seguir (as linhas em azul sinalizam a máxima e mínima do candle sinal):
Portanto, adicionaremos a condition_3 ao candle sinal:
capital_plot(total_capital, all_profits)
ev = expected_value(all_profits)
ev
result
average_gain
1216.625000
average_loss
-1296.750000
pct_gains
0.666667
pct_losses
0.333333
expected_value
378.833333
Comparando os resultados
A tabela a seguir reproduz as duas estratégias:
123 clássico
123 + inside bar
número de operações
48
12
acerto (%)
54
67
erro (%)
46
33
lucro total (R$)
3352
4546
drawdown (%)
9.52
2.20
lucro por operação (%)
0.07
0.38
média de ganho (R$)
965.27
1216.62
média de prejuízo (R$)
-988.41
-1296.75
EV por operação (R$)
69.83
378.83
De maneira satisfatória, conseguimos otimizar tanto a taxa de acerto (de 54% para 67%) como o drawdown (de 9.52% para 2.20%) ao adicionarmos a condição de inside bar na estratégia 123.
Apesar de apresentar um lucro por operação 5 vezes maior que o 123 clássico, a estratégia com inside bar teve um número de operações muito baixo (12 em pouco menos de 5 anos). Assim, para que esse sistema operacional seja estatisticamente relevante, seria necessário aplicá-lo em múltiplos ativos simultaneamente.
Em relação ao valor esperado, tivemos uma melhora significativa. Mesmo a média de prejuízo sendo ligeiramente maior que a de ganho, acertamos 2/3 dos trades, o que configura um excelente risco-retorno.
Conclusão
A estratégia 123 tem fundamento mas precisa de melhorias. Ao adicionar a condição do inside bar, encurtamos o nosso alvo e, portanto, aumentamos as chances da operação ir para o ganho.
Como o alvo da estratégia é a amplitude do conjunto dos 3 candles projetada a partir do candle sinal, ele é diretamente influenciado pelo tamanho das barras. Isto é, se o candle sinal for uma barra longa, o alvo fica distante e as chances de sermos estopados na operação aumenta.
Um ponto a se observar na estratégia 123 é que, em casos de abertura em gap, paga-se mais caro para entrar no trade, o que naturalmente diminui o retorno e o valor esperado por operação.
Dessa forma, há algumas variações que serão exploradas em backtests futuros:
medir a amplitude do candle sinal e, caso ele seja uma barra longa, estabelecer o stop na sua mínima;
no caso de abertura em gap, estabelecer o ponto de entrada no meio do candle sinal e esperar que o candle de entrada o atinja (caso contrário não entramos na operação);
calcular a estatística da quantidade de candle sinais acionados e caso esse número seja alto, definir o ponto de entrada no fechamento do terceiro candle (em vez de esperar o rompimento).
Esse foi o primeiro post de uma série de backtests que faremos em cima da estratégia 123. Para acompanhar os próximos artigos, participe do canal QuantBrasil no Telegram e não se esqueça de se inscrever na nossa newsletter!