Backtest da Estratégia de Gap Trap de Compra ou Venda
Utilizando Python, aprenda a calcular o retorno dessa famosa estratégia de day-trade.
Todos os nossos backtests até hoje foram realizados como swing trades. Hoje, executaremos a primeira estratégia de day trade, ou seja, com a operação sendo iniciada e finalizada no mesmo dia.
A vantagem do day trade é proteger o seu capital de movimentos abruptos que aconteçam fora do período do pregão. Além disso, é uma forma de rentabilizar o seu caixa, ou um capital que você deixa separado para trades oportunísticos. Por fim, algumas corretoras vão permitir o uso da margem, que permite uma alavancagem que pode ser lucrativa se utilizada com responsabilidade.
Por outro lado, o day trade é extremamente volátil e a taxa de acerto naturalmente será menor que de swing trades. Finalmente, como o trade tem menos tempo pra evoluir (uma vez que ele precisa ser encerrado até o fim do dia), é difícil capturar altas expressivas.
Hoje nós abordaremos uma estratégia que é bastante eficiente no day trade: o Gap Trap de Compra ou Venda.
Entendendo o Gap Trap
O sinal para um Gap Trap de Compra acontece quando o primeiro candle do dia abrir abaixo da mínima do dia anterior e fechar positivo (fechamento maior que abertura).
Analogamente, no Gap Trap de Venda o primeiro candle do dia abre acima da máxima do dia anterior e fecha negativo (fechamento menor que a abertura).
O primeiro candle pode ser em qualquer timeframe, mas classicamente trabalhamos esse modelo no período de 15 minutos. A ideia é trabalhar contra os traders que imaginaram que o papel perderia a mínima e afundaria ou romperia a máxima e dispararia, e portanto ficaram trapped e terão que stopar suas posições, gerando uma pressão na ponta contrária.
A Estratégia
Uma vez que o sinal foi dado, a condição para a entrada é que o candle seguinte supere a máxima do candle anterior (no caso do Gap Trap de Compra) ou perca a mínima do candle anterior (no caso do Gap Trap de Venda). Caso a superação ou perda não aconteça no candle imediatamente posterior, o sinal está cancelado e a estratégia não é executada.
Se o sinal for ativado, existem algumas formas de conduzir a operação:
- Carregar a operação até o final do dia;
- Estabelecer um alvo de 2x o tamanho do candle inicial;
- Definir uma % de ganho e carregar a operação até atingi-la.
Caso os pontos 2 e 3 não aconteçam dentro do dia, a operação é encerrada no fechamento.
Para o stop, também podemos pensar em 3 abordagens diferentes:
- Sem stop (operação necessariamente se encerrará no fechamento do pregão);
- Stop na mínima do candle sinal (Gap Trap de Compra) ou máxima do candle sinal (Gap Trap de Venda);
- Stop percentual (encerrar a operação se ultrapassar x%x\%x% de loss).
Ao longo dessa série de backtests vamos experimentar algumas dessas abordagens. Hoje, trabalharemos a estratégia sem stop e com o alvo sendo o fechamento do dia.
Importando as bibliotecas e os dados necessários
Como de praxe, vamos importar as bibliotecas a serem utilizadas em nossa análise:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
Após, vamos carregar a nossa base de dados. Realizaremos o backtest no mini dólar (WDO), de 2020 até hoje. Caso você esteja interessado na base, ela estará disponível para download no nosso grupo do Telegram.
OBS: Mesmo que o valor da base difira do valor observado, como estamos interessados na diferença entre os preços no intraday, não há impacto no backtest.
df = pd.read_csv("../data/M15/WDO.csv", index_col='datetime')[["open", "high", "low", "close"]]
df
open | high | low | close | |
---|---|---|---|---|
datetime | ||||
2020-01-02 09:00:00 | 4016.5 | 4017.5 | 4008.5 | 4017.5 |
2020-01-02 09:15:00 | 4017.5 | 4024.0 | 4016.5 | 4019.0 |
2020-01-02 09:30:00 | 4019.0 | 4027.0 | 4016.0 | 4019.0 |
2020-01-02 09:45:00 | 4018.5 | 4020.0 | 4013.5 | 4014.5 |
2020-01-02 10:00:00 | 4015.0 | 4015.5 | 4008.5 | 4012.5 |
... | ... | ... | ... | ... |
2021-05-27 15:30:00 | 5249.0 | 5257.0 | 5246.5 | 5255.5 |
2021-05-27 15:45:00 | 5256.0 | 5258.0 | 5241.5 | 5242.0 |
2021-05-27 16:00:00 | 5242.0 | 5251.0 | 5241.5 | 5249.5 |
2021-05-27 16:15:00 | 5249.5 | 5255.0 | 5248.0 | 5254.0 |
2021-05-27 16:30:00 | 5254.5 | 5254.5 | 5254.0 | 5254.0 |
12559 rows × 4 columns
Determinando os sinais de entrada
Agora que temos nossa base carregada, o primeiro passo é identificar o primeiro candle do dia, pois ele é o responsável por caracterizar o sinal. Existem diversas formas de se fazer isso: nós vamos fazer identificando, para cada candle, se o candle seguinte é de um dia diferente.
# The first 10 chars are the date in the YYYY-MM-DD format
df["day"] = df.index.str[:10]
df["first of day"] = df["day"] != df["day"].shift(1)
df.head(5)
open | high | low | close | day | first of day | |
---|---|---|---|---|---|---|
datetime | ||||||
2020-01-02 09:00:00 | 4016.5 | 4017.5 | 4008.5 | 4017.5 | 2020-01-02 | True |
2020-01-02 09:15:00 | 4017.5 | 4024.0 | 4016.5 | 4019.0 | 2020-01-02 | False |
2020-01-02 09:30:00 | 4019.0 | 4027.0 | 4016.0 | 4019.0 | 2020-01-02 | False |
2020-01-02 09:45:00 | 4018.5 | 4020.0 | 4013.5 | 4014.5 | 2020-01-02 | False |
2020-01-02 10:00:00 | 4015.0 | 4015.5 | 4008.5 | 4012.5 | 2020-01-02 | False |
Para definirmos se o candle abriu em gap, precisamos calcular a máxima e a mínima do dia anterior. Vamos aproveitar e calcular também o fechamento de cada dia, uma vez que isso será utilizado na hora de aferirmos o nosso lucro.
Note, porém, que temos vários candles para um mesmo dia, uma vez que estamos trabalhando no gráfico intradiário. Resolveremos esse problema através da função groupby, onde agruparemos nossos dados pela coluna day
:
high_by_day = df.groupby("day").high.max()
low_by_day = df.groupby("day").low.min()
close_by_day = df.groupby("day").close.last()
print("High by", high_by_day.head(5))
print("\n")
print("Low by", low_by_day.head(5))
print("\n")
print("Close by", close_by_day.head(5))
High by day 2020-01-02 4046.0 2020-01-03 4075.5 2020-01-06 4080.5 2020-01-07 4098.5 2020-01-08 4083.5 Name: high, dtype: float64 Low by day 2020-01-02 4008.5 2020-01-03 4037.5 2020-01-06 4053.0 2020-01-07 4061.0 2020-01-08 4045.5 Name: low, dtype: float64 Close by day 2020-01-02 4030.5 2020-01-03 4062.5 2020-01-06 4067.5 2020-01-07 4074.5 2020-01-08 4069.0 Name: close, dtype: float64
Finalmente, com os dados relevantes calculados para cada dia, podemos inseri-los no nosso dataframe original. Dessa vez, utilizaremos a função merge, definindo a coluna day
como a chave comum e adicionando o sufixo of day
nas novas colunas:
# Save index beforehand so we can reset it after merging
index = df.index
df = pd.merge(df, close_by_day, on="day", suffixes=[None, " of day"])
df = pd.merge(df, high_by_day, on="day", suffixes=[None, " of day"])
df = pd.merge(df, low_by_day, on="day", suffixes=[None, " of day"])
# When merging is done, reset to original index
df.index = index
df
open | high | low | close | day | first of day | close of day | high of day | low of day | |
---|---|---|---|---|---|---|---|---|---|
datetime | |||||||||
2020-01-02 09:00:00 | 4016.5 | 4017.5 | 4008.5 | 4017.5 | 2020-01-02 | True | 4030.5 | 4046.0 | 4008.5 |
2020-01-02 09:15:00 | 4017.5 | 4024.0 | 4016.5 | 4019.0 | 2020-01-02 | False | 4030.5 | 4046.0 | 4008.5 |
2020-01-02 09:30:00 | 4019.0 | 4027.0 | 4016.0 | 4019.0 | 2020-01-02 | False | 4030.5 | 4046.0 | 4008.5 |
2020-01-02 09:45:00 | 4018.5 | 4020.0 | 4013.5 | 4014.5 | 2020-01-02 | False | 4030.5 | 4046.0 | 4008.5 |
2020-01-02 10:00:00 | 4015.0 | 4015.5 | 4008.5 | 4012.5 | 2020-01-02 | False | 4030.5 | 4046.0 | 4008.5 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
2021-05-27 15:30:00 | 5249.0 | 5257.0 | 5246.5 | 5255.5 | 2021-05-27 | False | 5254.0 | 5313.5 | 5240.5 |
2021-05-27 15:45:00 | 5256.0 | 5258.0 | 5241.5 | 5242.0 | 2021-05-27 | False | 5254.0 | 5313.5 | 5240.5 |
2021-05-27 16:00:00 | 5242.0 | 5251.0 | 5241.5 | 5249.5 | 2021-05-27 | False | 5254.0 | 5313.5 | 5240.5 |
2021-05-27 16:15:00 | 5249.5 | 5255.0 | 5248.0 | 5254.0 | 2021-05-27 | False | 5254.0 | 5313.5 | 5240.5 |
2021-05-27 16:30:00 | 5254.5 | 5254.5 | 5254.0 | 5254.0 | 2021-05-27 | False | 5254.0 | 5313.5 | 5240.5 |
12559 rows × 9 columns
Nós já temos tudo o necessário para determinar os sinais de entrada. Recapitulando:
Gap Trap de Compra:
- Primeiro candle do dia tem sua abertura abaixo da mínima do dia anterior;
- Primeiro candle do dia tem seu fechamento maior que a abertura;
- Segundo candle do dia supera a máxima do primeiro candle.
Gap Trap de Venda:
- Primeiro candle do dia tem sua abertura acima da máxima do dia anterior;
- Primeiro candle do dia tem seu fechamento menor que a abertura;
- Segundo candle do dia perde a mínima do primeiro candle.
Assim, criaremos uma nova coluna signal
, onde:
signal | descrição | |
---|---|---|
1 | representa um Gap Trap de Compra que foi ativado com a superação da máxima do primeiro candle no candle seguinte | |
-1 | representa um Gap Trap de Venda que foi ativado com a perda da mínima do primeiro candle no candle seguinte | |
0 | sem Gap Trap no dia |
# A bullish gap trap has its open below yesterday's low and closes above its open
bullish_gap_trap = (df["first of day"] == True) & \
(df["open"] < df['low of day'].shift(1)) & \
(df["close"] > df["open"])
# A bearish gap trap has its open above yesterday's high and closes below its open
bearish_gap_trap = (df["first of day"] == True) & \
(df["open"] > df['high of day'].shift(1)) & \
(df["close"] < df["open"])
# Signal is 1 when the next candle exceeds the high of the bulish gap trap
# or signal is -1 when the next candle has its low below the low of the
# bearish gap trap. If none occurs, then signal is 0 and no trade is placed
df["signal"] = np.where(
(bullish_gap_trap == True) & (df["high"].shift(-1) > df["high"]),
1,
np.where(
(bearish_gap_trap == True) & (df["low"].shift(-1) < df["low"]),
-1,
0
)
)
long_trades = df[df["signal"] == 1]
short_trades = df[df["signal"] == -1]
print(f"Total Gap Trap Compras: {len(long_trades)}")
print(f"Total Gap Trap Vendas: {len(short_trades)}")
Total Gap Trap Compras: 21 Total Gap Trap Vendas: 16
Como podemos observar, de 2020-01-02
até 2021-05-27
, houve 37 trades utilizando o modelo no mini dólar, dos quais 21 foram na ponta da compra e 16 na ponta da venda. Veja que temos em média 2 trades por mês utilizando o modelo, ou seja, é um sinal pouco frequente.
Calculando a Rentabilidade e Taxa de Acerto
Com os sinais calculados, já temos todas as informações necessárias pra calcular a taxa de acerto e a rentabilidade da estratégia. No post de hoje, vamos ver como ela teria se saído carregando a operação até o fechamento, sem stop.
Para isso, vamos definir como entrada (entry
) 1 tick acima da máxima ou mínima do candle sinal. Note que num modelo mais fidedigno deveríamos contar com o slippage, ou a diferença entre o preço onde a ordem foi efetivamente executada e o preço que gostaríamos de executar. No entanto, vamos desconsiderar o efeito do slippage por simplicidade.
min_tick = 0.5 # That's the min variation for this asset
df["entry"] = np.where(
df["signal"] == 1,
df["high"] + min_tick,
np.where(
df["signal"] == -1,
df["low"] - min_tick,
np.nan
)
)
trades = df[~np.isnan(df["entry"])][["day", "high", "low", "signal", "entry", "close of day"]]
trades["target"] = trades["close of day"]
trades.set_index("day", inplace=True)
trades.head(10)
high | low | signal | entry | close of day | target | |
---|---|---|---|---|---|---|
day | ||||||
2020-01-27 | 4216.0 | 4207.0 | -1 | 4206.5 | 4209.0 | 4209.0 |
2020-01-30 | 4248.5 | 4240.5 | -1 | 4240.0 | 4244.5 | 4244.5 |
2020-02-03 | 4279.0 | 4273.0 | -1 | 4272.5 | 4253.5 | 4253.5 |
2020-02-13 | 4385.5 | 4371.5 | -1 | 4371.0 | 4353.5 | 4353.5 |
2020-02-28 | 4506.5 | 4497.5 | -1 | 4497.0 | 4497.0 | 4497.0 |
2020-03-12 | 5034.0 | 4986.0 | -1 | 4985.5 | 4801.0 | 4801.0 |
2020-03-23 | 5095.0 | 5021.0 | -1 | 5020.5 | 5148.5 | 5148.5 |
2020-04-07 | 5216.0 | 5190.0 | 1 | 5216.5 | 5230.5 | 5230.5 |
2020-04-09 | 5157.0 | 5114.5 | 1 | 5157.5 | 5114.0 | 5114.0 |
2020-04-27 | 5582.5 | 5536.0 | 1 | 5583.0 | 5657.5 | 5657.5 |
Veja que a entrada está sendo feita sempre 0.50
pontos acima da máxima (Gap Trap de Compra) ou 0.50
pontos abaixo da mínima (Gap Trap de Venda). Nosso target
nada mais é que o fechamento do dia.
Com isso, podemos calcular quantos pontos a estratégia capturou no período:
trades["result"] = (trades["target"] - trades["entry"]) * trades["signal"]
trades["acc"] = trades["result"].cumsum()
trades
high | low | signal | entry | close of day | target | result | acc | |
---|---|---|---|---|---|---|---|---|
day | ||||||||
2020-01-27 | 4216.0 | 4207.0 | -1 | 4206.5 | 4209.0 | 4209.0 | -2.5 | -2.5 |
2020-01-30 | 4248.5 | 4240.5 | -1 | 4240.0 | 4244.5 | 4244.5 | -4.5 | -7.0 |
2020-02-03 | 4279.0 | 4273.0 | -1 | 4272.5 | 4253.5 | 4253.5 | 19.0 | 12.0 |
2020-02-13 | 4385.5 | 4371.5 | -1 | 4371.0 | 4353.5 | 4353.5 | 17.5 | 29.5 |
2020-02-28 | 4506.5 | 4497.5 | -1 | 4497.0 | 4497.0 | 4497.0 | -0.0 | 29.5 |
2020-03-12 | 5034.0 | 4986.0 | -1 | 4985.5 | 4801.0 | 4801.0 | 184.5 | 214.0 |
2020-03-23 | 5095.0 | 5021.0 | -1 | 5020.5 | 5148.5 | 5148.5 | -128.0 | 86.0 |
2020-04-07 | 5216.0 | 5190.0 | 1 | 5216.5 | 5230.5 | 5230.5 | 14.0 | 100.0 |
2020-04-09 | 5157.0 | 5114.5 | 1 | 5157.5 | 5114.0 | 5114.0 | -43.5 | 56.5 |
2020-04-27 | 5582.5 | 5536.0 | 1 | 5583.0 | 5657.5 | 5657.5 | 74.5 | 131.0 |
2020-04-29 | 5479.5 | 5443.0 | 1 | 5480.0 | 5334.5 | 5334.5 | -145.5 | -14.5 |
2020-05-04 | 5622.0 | 5559.0 | -1 | 5558.5 | 5554.0 | 5554.0 | 4.5 | -10.0 |
2020-05-19 | 5705.0 | 5686.0 | 1 | 5705.5 | 5762.0 | 5762.0 | 56.5 | 46.5 |
2020-05-25 | 5523.5 | 5487.5 | 1 | 5524.0 | 5445.0 | 5445.0 | -79.0 | -32.5 |
2020-06-12 | 5116.0 | 5080.0 | -1 | 5079.5 | 5055.5 | 5055.5 | 24.0 | -8.5 |
2020-06-25 | 5389.0 | 5352.5 | -1 | 5352.0 | 5365.0 | 5365.0 | -13.0 | -21.5 |
2020-07-02 | 5308.5 | 5281.0 | 1 | 5309.0 | 5371.5 | 5371.5 | 62.5 | 41.0 |
2020-07-06 | 5289.5 | 5268.0 | 1 | 5290.0 | 5364.5 | 5364.5 | 74.5 | 115.5 |
2020-07-07 | 5408.0 | 5378.5 | -1 | 5378.0 | 5381.0 | 5381.0 | -3.0 | 112.5 |
2020-07-15 | 5331.0 | 5305.5 | 1 | 5331.5 | 5370.0 | 5370.0 | 38.5 | 151.0 |
2020-08-04 | 5356.0 | 5333.0 | -1 | 5332.5 | 5288.0 | 5288.0 | 44.5 | 195.5 |
2020-08-05 | 5268.5 | 5241.0 | 1 | 5269.0 | 5299.5 | 5299.5 | 30.5 | 226.0 |
2020-09-28 | 5533.5 | 5518.5 | 1 | 5534.0 | 5659.5 | 5659.5 | 125.5 | 351.5 |
2020-10-01 | 5607.5 | 5577.5 | 1 | 5608.0 | 5646.5 | 5646.5 | 38.5 | 390.0 |
2020-10-27 | 5618.0 | 5600.5 | 1 | 5618.5 | 5709.5 | 5709.5 | 91.0 | 481.0 |
2020-11-06 | 5544.0 | 5508.5 | 1 | 5544.5 | 5370.0 | 5370.0 | -174.5 | 306.5 |
2020-11-19 | 5386.0 | 5353.0 | -1 | 5352.5 | 5308.0 | 5308.0 | 44.5 | 351.0 |
2020-11-20 | 5309.0 | 5289.0 | 1 | 5309.5 | 5381.5 | 5381.5 | 72.0 | 423.0 |
2020-11-30 | 5304.5 | 5271.0 | 1 | 5305.0 | 5331.5 | 5331.5 | 26.5 | 449.5 |
2020-12-02 | 5246.0 | 5218.0 | 1 | 5246.5 | 5220.0 | 5220.0 | -26.5 | 423.0 |
2020-12-16 | 5096.5 | 5061.0 | 1 | 5097.0 | 5083.0 | 5083.0 | -14.0 | 409.0 |
2021-01-18 | 5314.5 | 5282.0 | -1 | 5281.5 | 5302.0 | 5302.0 | -20.5 | 388.5 |
2021-01-21 | 5251.5 | 5232.5 | 1 | 5252.0 | 5353.0 | 5353.0 | 101.0 | 489.5 |
2021-02-17 | 5435.5 | 5406.5 | -1 | 5406.0 | 5414.0 | 5414.0 | -8.0 | 481.5 |
2021-02-19 | 5472.0 | 5460.0 | -1 | 5459.5 | 5383.0 | 5383.0 | 76.5 | 558.0 |
2021-04-23 | 5440.0 | 5427.0 | 1 | 5440.5 | 5477.5 | 5477.5 | 37.0 | 595.0 |
2021-05-06 | 5380.0 | 5344.0 | 1 | 5380.5 | 5290.5 | 5290.5 | -90.0 | 505.0 |
Ora, mas cada ponto do mini dólar equivale a R$ 10,00. Dessa forma, podemos calcular o resultado financeiro de um trader operando somente essa estratégia, de janeiro de 2020 até hoje, com 5 contratos:
number_of_contracts = 5
money_per_point = 10
trades["profit"] = trades["acc"] * money_per_point * number_of_contracts
plt.title("Curva de Capital")
trades["profit"].plot(figsize=(12, 5))
print(f'Total Profit: {trades["profit"][-1]}')
Total Profit: 25250.0
Nada mal para nossa primeira estratégia! Vamos desmembrar cada trade para identificarmos a taxa de acerto:
shorts = trades[trades["signal"] == -1]
successful_shorts = len(shorts[shorts["result"] > 0])
failed_shorts = len(shorts) - successful_shorts
longs = trades[trades["signal"] == 1]
successful_longs = len(longs[longs["result"] > 0])
failed_longs = len(longs) - successful_longs
print(f"Total longs: {len(longs)}")
print(f"Total shorts: {len(shorts)}")
print(f"Successful trades: {successful_longs + successful_shorts}")
print(f"Successful bullish gap traps: {successful_longs}")
print(f"Successful bearish gap traps: {successful_shorts}")
print(f"Total Accuracy (%): {(successful_longs + successful_shorts) / len(trades):.2%}")
print(f"Bullish Gap Trap Accuracy (%): {(successful_longs) / len(longs):.2%}")
print(f"Bearish Gap Trap Accuracy (%): {(successful_shorts) / len(shorts):.2%}")
Total longs: 21 Total shorts: 16 Successful trades: 22 Successful bullish gap traps: 14 Successful bearish gap traps: 8 Total Accuracy (%): 59.46% Bullish Gap Trap Accuracy (%): 66.67% Bearish Gap Trap Accuracy (%): 50.00%
De onde produzimos a seguinte tabela:
Tipo | Trades | Acertos | % Acerto |
---|---|---|---|
Gap Trap Compra | 22 | 15 | 66.67% |
Gap Trap Venda | 16 | 8 | 50.00% |
Total | 38 | 23 | 59.46% |
Por fim, vamos calcular estatísticas básicas da estratégia:
description = trades["result"].describe()
print(f"Mean: {description['mean']}")
print(f"Median: {description['50%']}")
print(f"Std deviation: {description['std']}")
print(f"Min: {description['min']}")
print(f"Max: {description['max']}")
print(f"25th percentile: {description['25%']}")
print(f"75th percentile: {description['75%']}")
Mean: 13.64864864864865 Median: 19.0 Std deviation: 72.23148213910618 Min: -174.5 Max: 184.5 25th percentile: -13.0 75th percentile: 56.5
Conclusão
A estratégia do Gap Trap de Compra ou Venda é bastante poderosa por sua simplicidade e objetividade. Além disso, a taxa de acerto fica em tornos dos 60%, o que é bem interessante. A assertividade na ponta da compra é de 2/3 dos trades no período calculado.
Observe que na média um trader pode esperar aproximadamente 14 pontos/trade, ou R$ 140,00 ao se operar apenas um contrato. Repare, porém, que o desvio padrão é bem grande, o que nos traduz que o resultado oscila bastante.
Observe que algum percentil pode ser utilizado como stop. No caso, podemos usar os dados para considerar o stop toda vez que o loss ultrapassar 13 pontos. Note, porém, que a estratégia deu poucos sinais, e portanto um teste de confiabilidade estatística se faz necessário.
Idealmente, um trader mais rigoroso poderia separar a base em training e test sets e otimizar os parâmetros de saída no intervalo. No entanto, tal prática pode ocasionar em overfitting. Fique ligado nos próximos posts se quiser saber mais sobre o assunto!
Próximos Passos
Nos próximos posts dessa série vamos explorar a estratégia em diversos ativos, como mini índice e ações, além de testar estratégias diferentes de stop e alvo. Se você se interessa por esse tipo de conteúdo, não deixe de entrar no nosso canal do Telegram e se inscrever na nossa newsletter abaixo!