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
openhighlowclose
datetime
2020-01-02 09:00:004016.54017.54008.54017.5
2020-01-02 09:15:004017.54024.04016.54019.0
2020-01-02 09:30:004019.04027.04016.04019.0
2020-01-02 09:45:004018.54020.04013.54014.5
2020-01-02 10:00:004015.04015.54008.54012.5
...............
2021-05-27 15:30:005249.05257.05246.55255.5
2021-05-27 15:45:005256.05258.05241.55242.0
2021-05-27 16:00:005242.05251.05241.55249.5
2021-05-27 16:15:005249.55255.05248.05254.0
2021-05-27 16:30:005254.55254.55254.05254.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)
openhighlowclosedayfirst of day
datetime
2020-01-02 09:00:004016.54017.54008.54017.52020-01-02True
2020-01-02 09:15:004017.54024.04016.54019.02020-01-02False
2020-01-02 09:30:004019.04027.04016.04019.02020-01-02False
2020-01-02 09:45:004018.54020.04013.54014.52020-01-02False
2020-01-02 10:00:004015.04015.54008.54012.52020-01-02False

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
openhighlowclosedayfirst of dayclose of dayhigh of daylow of day
datetime
2020-01-02 09:00:004016.54017.54008.54017.52020-01-02True4030.54046.04008.5
2020-01-02 09:15:004017.54024.04016.54019.02020-01-02False4030.54046.04008.5
2020-01-02 09:30:004019.04027.04016.04019.02020-01-02False4030.54046.04008.5
2020-01-02 09:45:004018.54020.04013.54014.52020-01-02False4030.54046.04008.5
2020-01-02 10:00:004015.04015.54008.54012.52020-01-02False4030.54046.04008.5
..............................
2021-05-27 15:30:005249.05257.05246.55255.52021-05-27False5254.05313.55240.5
2021-05-27 15:45:005256.05258.05241.55242.02021-05-27False5254.05313.55240.5
2021-05-27 16:00:005242.05251.05241.55249.52021-05-27False5254.05313.55240.5
2021-05-27 16:15:005249.55255.05248.05254.02021-05-27False5254.05313.55240.5
2021-05-27 16:30:005254.55254.55254.05254.02021-05-27False5254.05313.55240.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:

signaldescrição
1representa um Gap Trap de Compra que foi ativado com a superação da máxima do primeiro candle no candle seguinte
-1representa um Gap Trap de Venda que foi ativado com a perda da mínima do primeiro candle no candle seguinte
0sem 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)
highlowsignalentryclose of daytarget
day
2020-01-274216.04207.0-14206.54209.04209.0
2020-01-304248.54240.5-14240.04244.54244.5
2020-02-034279.04273.0-14272.54253.54253.5
2020-02-134385.54371.5-14371.04353.54353.5
2020-02-284506.54497.5-14497.04497.04497.0
2020-03-125034.04986.0-14985.54801.04801.0
2020-03-235095.05021.0-15020.55148.55148.5
2020-04-075216.05190.015216.55230.55230.5
2020-04-095157.05114.515157.55114.05114.0
2020-04-275582.55536.015583.05657.55657.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
highlowsignalentryclose of daytargetresultacc
day
2020-01-274216.04207.0-14206.54209.04209.0-2.5-2.5
2020-01-304248.54240.5-14240.04244.54244.5-4.5-7.0
2020-02-034279.04273.0-14272.54253.54253.519.012.0
2020-02-134385.54371.5-14371.04353.54353.517.529.5
2020-02-284506.54497.5-14497.04497.04497.0-0.029.5
2020-03-125034.04986.0-14985.54801.04801.0184.5214.0
2020-03-235095.05021.0-15020.55148.55148.5-128.086.0
2020-04-075216.05190.015216.55230.55230.514.0100.0
2020-04-095157.05114.515157.55114.05114.0-43.556.5
2020-04-275582.55536.015583.05657.55657.574.5131.0
2020-04-295479.55443.015480.05334.55334.5-145.5-14.5
2020-05-045622.05559.0-15558.55554.05554.04.5-10.0
2020-05-195705.05686.015705.55762.05762.056.546.5
2020-05-255523.55487.515524.05445.05445.0-79.0-32.5
2020-06-125116.05080.0-15079.55055.55055.524.0-8.5
2020-06-255389.05352.5-15352.05365.05365.0-13.0-21.5
2020-07-025308.55281.015309.05371.55371.562.541.0
2020-07-065289.55268.015290.05364.55364.574.5115.5
2020-07-075408.05378.5-15378.05381.05381.0-3.0112.5
2020-07-155331.05305.515331.55370.05370.038.5151.0
2020-08-045356.05333.0-15332.55288.05288.044.5195.5
2020-08-055268.55241.015269.05299.55299.530.5226.0
2020-09-285533.55518.515534.05659.55659.5125.5351.5
2020-10-015607.55577.515608.05646.55646.538.5390.0
2020-10-275618.05600.515618.55709.55709.591.0481.0
2020-11-065544.05508.515544.55370.05370.0-174.5306.5
2020-11-195386.05353.0-15352.55308.05308.044.5351.0
2020-11-205309.05289.015309.55381.55381.572.0423.0
2020-11-305304.55271.015305.05331.55331.526.5449.5
2020-12-025246.05218.015246.55220.05220.0-26.5423.0
2020-12-165096.55061.015097.05083.05083.0-14.0409.0
2021-01-185314.55282.0-15281.55302.05302.0-20.5388.5
2021-01-215251.55232.515252.05353.05353.0101.0489.5
2021-02-175435.55406.5-15406.05414.05414.0-8.0481.5
2021-02-195472.05460.0-15459.55383.05383.076.5558.0
2021-04-235440.05427.015440.55477.55477.537.0595.0
2021-05-065380.05344.015380.55290.55290.5-90.0505.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:

TipoTradesAcertos% Acerto
Gap Trap Compra221566.67%
Gap Trap Venda16850.00%
Total382359.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!

Subscribe to Rafael Quintanilha

Don’t miss out on the latest articles. Sign up now to get access to the library of members-only articles.
john@example.com
Subscribe