Como mencionado em Como Calcular as Bandas de Bollinger em Python, existem diferentes tipos de estratégias baseadas nesse indicador. No artigo de hoje, vamos realizar o backtest da estratégia conhecida como Saudade de Casa popularizada pelo Stormer.
Como funciona o setup Saudade de Casa?
A ideia por trás da estratégia é bastante simples: buscar por possíveis discrepâncias na abertura do mercado, quando o ativo se encontra fora das bandas de Bollinger (nesse caso, fora de "casa") e esperar que ele retorne para dentro da banda.
Para isso, primeiramente devemos calcular as bandas de Bollinger de 20 períodos com desvio padrão igual a 2. Para essa estratégia, que é mais utilizada no day trade, recomenda-se o timeframe de 30 minutos.
O sinal de entrada é configurado se o primeiro candle do dia abrir acima da banda superior (sinal de venda) ou abaixo da banda inferior (sinal de compra). Dado o sinal de venda, se o próximo candle do dia perder a mínima do candle sinal, a venda deverá ser efetuada. No caso da compra, se o segundo candle do dia romper a máxima do candle sinal, deveremos efetuar a compra. Caso a superação ou perda não aconteça no segundo candle do dia, o sinal está cancelado e a estratégia não é executada. Simples, certo?
Para essa estratégia, usualmente o stop é acionado na máxima (no caso da ponta da venda) ou na mínima (na ponta da compra) do candle sinal.
Para o alvo da operação, geralmente existem duas possibilidades: a banda do meio ou a banda oposta.
A figura abaixo mostra dois exemplos da estratégia para PETR4, na ponta da compra (stopada) e na ponta da venda (bem-sucedida).
Formalmente, definimos o sistema operacional com o seguinte conjunto de regras:
Timeframe: 30 minutos
Tipo de operação: compra e venda.
Ponto de entrada: rompimento da máxima ou mínima do candle sinal.
Alvo: banda central ou banda oposta.
Stop: máxima ou mínima do candle sinal.
Importando as bibliotecas e os dados necessários
Como já aprendemos a criar nosso próprio banco de dados e importar os dados do MetaTrader ou do ProfitChart, para esse artigo vamos usar nossos próprios dados. O arquivo que usaremos aqui estará disponível para download no nosso grupo do Telegram.
Vamos importar também as bibliotecas que usaremos para efetuar nosso backtest.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import math
O ativo escolhido foi PETR4 no timeframe de 30 minutos para um intervalo de 3 anos.
Como na estratégia Saudade de Casa estamos interessado no primeiro candle do dia, podemos usar a mesma abordagem que aprendemos no artigo sobre Gap Trap para identificá-los. Para isso, vamos analisar para cada candle, se o anterior é de um dia diferente.
Com o primeiro candle do dia identificado e as bandas de Bollinger calculadas, podemos criar um simples código para identificarmos a quantidade de vezes que o sinal de entrada foi acionado.
Primeiramente vamos identificar se o candle é o primeiro do dia e abriu acima ou abaixo das bandas de Bollinger. Em seguida, criaremos um coluna sinal que retornará:
1 se o segundo candle do dia rompeu a máxima do candle sinal;
-1 se o segundo candle do dia perdeu a mínima do candle sinal;
0 se não houve trade no dia.
# A bullish "missing home" opens below the lower BB
bullish_candle = (df["first of day"] == True) & \
(df["open"] < df['lower band'])
# A bearish "missing home" opens above the upper BB
bearish_candle = (df["first of day"] == True) & \
(df["open"] > df['upper band'])
# Signal is 1 when the next candle exceeds the high of the bullish candle
# Signal is -1 when the next candle has its low below the low of the
# bearish candle.
# If none occurs, then the signal is 0 and no trade is placed.
df["signal"] = np.where(
(bullish_candle == True) & (df["high"].shift(-1) > df["high"]),
1,
np.where(
(bearish_candle == True) & (df["low"].shift(-1) < df["low"]),
-1,
0
)
)
long_trades = df[df["signal"] == 1]
short_trades = df[df["signal"] == -1]
print(f"Total Compras: {len(long_trades)}")
print(f"Total Vendas: {len(short_trades)}")
print(f"Total Trades: {len(long_trades+short_trades)}")
Total Compras: 37 Total Vendas: 43 Total Trades: 80
No período de julho de 2018 até julho de 2021, 80 trades foram realizados pelo setup Saudade de Casa (contudo, vamos ver mais pra frente que nem todas as operações serão válidas para o nosso propósito aqui). Embora um bom balanço exista entre a ponta da compra e a ponta da venda, o número total de vendas foi um pouco superior ao da compra.
Criando o algoritmo para simular a operação
O código que vamos programar agora é baseado no backtest do estocástico lento e na estratégia 123. Contudo algumas mudanças foram feitas e por isso vamos analisá-las passo-a-passo a seguir.
Primeiramente, após inicializarmos a nossa função com o capital total, vamos criar uma lista para salvarmos os nossos lucros/prejuízos. Vamos também definir variáveis que nos auxiliarão a contar o número de ganhos e perdas das operações de compra e de venda.
Antes de começarmos o nosso loop, como feito nos outros backtests, vamos definir uma variável (ongoing) que nos diz se estamos em um trade ou não. Em outras palavras, só poderemos começar um novo trade quando terminarmos o trade anterior. Dessa forma, só entraremos em um trade por vez.
Vamos também definir a mesma variável signal, citada anteriormente, que nos dirá qual é o sinal da operação (compra ou venda).
Por último, vamos definir a variação mínima de uma ação, min_tick.
ongoing = False
signal = 0
min_tick = 0.01
Agora já estamos prontos para iniciarmos o loop. Nele, vamos percorrer todo o DataFrame.
Dentro do loop, vamos dividir nossa operação em duas partes. Se ongoing == False, ou seja, se ainda não estamos em nenhuma operação, vamos buscar pelo sinal de entrada para um trade de compra ou venda:
for i in range(0,len(df)):
# bullish:
if (df["signal"][i] == 1):
signal = 1
entry = df["high"][i] + min_tick
stop = df["low"][i] - min_tick
risk = entry - stop
ongoing = True
# bearish:
elif (df["signal"][i] == -1):
signal = -1
entry = df["low"][i] - min_tick
stop = df["high"][i] + min_tick
risk = entry - stop
ongoing = True
Lembrando que no final de cada condição, devemos setar ongoing = True sinalizando que iniciamos um trade.
Agora o algoritmo fica mais interessante e vamos definir as operações que faremos quando o sinal for dado e estivermos em um trade.
Primeiramente devemos definir o nosso alvo. Como as Bandas de Bollinger variam à medida que novos dados são inseridos, o nosso alvo é variável com o tempo. Por isso, temos que defini-lo dentro do loop. Como estamos interessados em dois alvos diferentes, a banda do meio e a banda oposta, vamos criar a variável target_band e defini-la como um dado de entrada da função. Repare que precisamos saber o sinal da operação para identificar a banda oposta, mas não precisamos para a banda do meio, que é igual em ambos os casos.
target_band = 'middle'
for i in range(0,len(df)):
if (signal == 1 and target_band == 'opposite'):
target = df["upper band"][i]
elif (signal == -1 and target_band == 'opposite'):
target = df["lower band"][i]
elif (target_band == 'middle'):
target = df["middle band"][i]
else:
print('Unknown target')
Lembra que eu comentei anteriormente que nem todas as 80 operações seriam válidas para esse trade? O que acontece é que existem operações em que não sabemos (olhando o timeframe de 30 minutos) se o stop aconteceu antes da entrada. Por simplicidade, vamos desconsiderar essas operações.
for i in range(0,len(df)):
# Ignore signals where you might have been stopped in the same candle once
# we are unable to say what happened first
if (signal == 1 and df["high"][i] >= df["high"][i-1] and df["low"][i] <= stop):
ongoing = False
elif (signal == -1 and df["low"][i] <= df["low"][i-1] and df["high"][i] >= stop):
ongoing = False
Pronto, agora podemos definir se a nossa entrada atingiu o nosso alvo. Novamente vamos separar as operações da ponta da compra e da ponta da venda, calcular o resultado e adicionar à lista de resultads. Vamos também atualizar o capital e usar as variáveis de contagem para sabermos quantas operações foram bem-sucedidas.
# Example with fixed shares
shares = 100
for i in range(0,len(df)):
# bullish:
if (signal == 1 and df["high"][i] >= target):
profit = shares * (target - entry)
all_profits += [profit]
current_capital = total_capital[-1]
total_capital += [current_capital + profit]
number_bullish_gain += 1
ongoing = False
# bearish:
elif (signal == -1 and df["low"][i] <= target):
profit = shares * (target - entry)
all_profits += [profit]
current_capital = total_capital[-1]
total_capital += [current_capital + profit]
number_bearish_gain += 1
ongoing = False
Na última parte do loop, vamos definir o stop. Lembre-se que o stop é acionado na perda da mínima do candle sinal (ponta da compra) ou no rompimento da máxima do candle sinal (ponta da venda). Nesse caso, também calcularemos o resultado e o adicionaremos à lista.
# Example with fixed shares
shares = 100
for i in range(0,len(df)):
# bullish:
if (signal == 1 and df["low"][i] <= stop):
profit = shares * (stop - entry)
all_profits += [profit]
current_capital = total_capital[-1]
total_capital += [current_capital + profit]
number_bullish_loss += 1
ongoing = False
# bearsih:
elif (signal == -1 and df["high"][i] >= stop):
profit = shares * (stop - entry)
all_profits += [profit]
current_capital = total_capital[-1]
total_capital += [current_capital + profit]
number_bearish_loss += 1
ongoing = False
Finalmente, vamos calcular o número total de operações de compra (total_number_bullish) e venda (total_number_bearish) e calcular a porcentagem de ganhos em cada um deles (pct_bullish_gain e pct_bearish_gain). É essa a informação que estamos mais interessados para sabermos se existe maiores chances de se acertar comprado ou vendido nesse trade. Nossa função vai retornar essa porcentagem, a soma dos nossos lucros e também o nosso capital final ao fim da operação.
A versão final do código, juntando todas as partes, ficará assim:
# 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 missing_home_algorithm(
df,
capital_exposure,
initial_capital,
target_band='middle'): # we define the middle band as standard if nothing is provided
# List with the total capital after every operation
total_capital = [initial_capital]
# List with all profits and a couting splitting bullish and bearish operations
all_profits = []
number_bullish_gain = 0
number_bearish_gain = 0
number_bullish_loss = 0
number_bearish_loss = 0
ongoing = False # at the start no trade is ongoing
signal = 0 # initialising signal
min_tick = 0.01 # That's the min variation for this asset
for i in range(0,len(df)):
# start of the trade
if ongoing == False:
# bullish signal
if (df["signal"][i] == 1):
signal = 1
entry = df["high"][i] + min_tick
stop = df["low"][i] - min_tick
risk = entry - stop
shares = round_down(capital_exposure / risk)
ongoing = True
# bearish signal
elif (df["signal"][i] == -1):
signal = -1
entry = df["low"][i] - min_tick
stop = df["high"][i] + min_tick
risk = entry - stop
shares = round_down(capital_exposure / risk)
ongoing = True
else:
# if I am in a trade, my target is variable and updated every candle
if (signal == 1 and target_band == 'opposite'):
target = df["upper band"][i]
elif (signal == -1 and target_band == 'opposite'):
target = df["lower band"][i]
elif (target_band == 'middle'):
target = df["middle band"][i]
else:
print('No target defined/recognised')
break
# operation where we don't know if the stop happened first is NOT considered
if (signal == 1 and df["high"][i] >= df["high"][i-1] and df["low"][i] <= stop):
#buy operation cancelled
ongoing = False
elif (signal == -1 and df["low"][i] <= df["low"][i-1] and df["high"][i] >= stop):
#sell operation cancelled
ongoing = False
# bullish target
elif (signal == 1 and df["high"][i] >= target):
# target was reached
profit = shares * (target - 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]
number_bullish_gain += 1
#buy operation target
ongoing = False
# bearish target
elif (signal == -1 and df["low"][i] <= target):
# target was reached
profit = shares * (target - 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]
number_bearish_gain += 1
#sell operation target
ongoing = False
# stop loss
elif (signal == 1 and df["low"][i] <= stop):
# stop was reached: end of operation
profit = shares * (stop - 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]
number_bullish_loss += 1
#stop in bullish
ongoing = False
elif (signal == -1 and df["high"][i] >= stop):
# stop was reached: end of operation
profit = shares * (stop - 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]
number_bearish_loss += 1
#stop in bearlish
ongoing = False
total_number_bullish = number_bullish_gain + number_bullish_loss
total_number_bearish = number_bearish_gain + number_bearish_loss
pct_bullish_gain = (number_bullish_gain / total_number_bullish) * 100
pct_bearish_gain = (number_bearish_gain / total_number_bearish) * 100
return all_profits, pct_bullish_gain, pct_bearish_gain, total_capital
Calculando a curva de capital e as estatísticas
Para plotarmos a curva de capital e calcularmos as estatísticas da estratégia, vamos nos basear nas funções get_drawdown, strategy_test e capital_plot ja desenvolvidas em artigos anteriores. Apenas algumas poucas mudanças foram feitas, para incluirmos a média de ganhos e perdas e também o cálculo do valor esperado (EV).
Agora que temos todas as funções necessarias programadas e entendidas, podemos finalmente rodar o nosso backtest!
Vamos seguir o critério usados em outros backtests e entrar com um capital fixo de R$100.000 e um risco controlado de R$1.000 (1% do capital).
Alvo 1: banda central
Como nosso idéia é testar o setup para dois alvos diferentes, vamos começar analisando o alvo como sendo a banda de Bollinger do meio. Vamos também transformar o dicionário statistics em um dataframe para melhor visualização.
Muitas informações interessantes podem ser tiradas dos resultados acima. Note que como algumas operações foram canceladas, o número total de operações diminuiu de 80 para 71.
Vemos também que obtivemos nessa estratégia com o alvo na banda do meio, um ganho de 60.5%. Além do mais, podemos observar que separando esses ganhos entre compra e venda, a porcentagem de acerto na venda (74.3%) é maior que a porcentagem de acertos na compra (65.6%). O drawdown da operação ficou em 4.2%, o que pode ser considerado bom.
Contudo, o nosso EV foi relativamente baixo. Se olharmos quando a operação da lucro, a média de ganhos esta em R$804 e quando a operação da prejuízo, a média de perdas é de R$788. Nesse caso, a relação de ganho e perda da estratégia é praticamente 1:1.
E como seria agora se mudarmos o nosso alvo para a banda oposta?
Alvo 2: banda oposta
Vamos rodar novamente o nosso código, dessa vez utilizando target_band='opposite' para definirmos o alvo como sendo a banda superior no caso da ponta da compra, e a banda inferior no caso da ponta da venda.
Note primeiramente que o número de operações diminuiu; foi de 71 para 67. Se o número de sinais é o mesmo e apenas mudamos o nosso alvo, como podemos ter um número de operações diferente?
Acontece que como não estamos definindo um limite no tempo para carregar as operações, as bandas opostas podem demorar mais tempo para serem alcançadas. E como definimos que só podemos realizar uma operação por vez, podemos deixar de entrar em alguma operação se outra já estiver em andamento.
Outra informação importante é que a porcentagem de ganhos diminuiu para 53.7% em comparação aos 60.5% da banda central. Mas assim como no caso anterior, a porcentagem de acerto na ponta da venda foi maior do que na ponta da compra.
O nosso drawdown aumentou levemente para 5.9%. Apenas olhando essas informações poderiamos dizer que o alvo sendo a banda oposta é pior, certo? Mas vamos antes de tirar conclusões olhar as médias de ganhos e perdas assim como o nosso EV.
Nessa nova abordagem, a média de ganhos está em R$1793 enquanto a média de perdas esta em R$994. Isso acabou por geral um EV de R$503 em comparação com o de R$176 obtido anteriormente. Ou seja, embora temos diminuido a nossa taxa de acerto quando mudamos o nosso alvo para a banda oposta ao invés da banda do meio, o nosso lucro é muito maior quando acertamos. Isso faz com que a relação de ganhos e perdas seja de aproximadamente 2:1, o que torna essa estratégia muito interessante.
Conclusão
Nesse backtest da estratégia Saudade de Casa, vimos que realmente existe uma boa possiblidade do ativo voltar para dentro das Bandas de Bollinger quando o mesmo abre fora delas.
Vimos também que quando o alvo é a banda do meio, embora a operação tenha uma boa taxa de acertos, a relação de ganhos e perdas não é tão interessante. Contudo, quando utilizamos as bandas opostas, conseguimos aumentar os nossos ganhos e consequentemente aumentar essa relação.
No artigo de hoje, porém, não limitamos a operação como day trade, mas abrimos a possibilidade de ser um swing trade, ao passo que a operação fica aberta até o preço atingir o alvo ou sermos stopados. Em uma próxima análise, vamos comparar esses dois tipos de estratégias para entendermos qual delas nos dará um maior retorno usando esse setup.
Para acompanhar os próximos artigos e muitos outros backtests de diferentes estratégias, participe do canal QuantBrasil no Telegram e se inscreva na nossa newsletter!