Estratégias de trading de rompimento: análise dos principais métodos
Introdução
As estratégias de rompimento da faixa de abertura (Opening Range Breakout, ORB) partem da ideia de que a faixa inicial de negociação, formada logo após a abertura do mercado, reflete níveis de preço relevantes, quando compradores e vendedores chegam a um acordo sobre o valor. Ao identificar rompimentos de uma determinada faixa para cima ou para baixo, os traders podem aproveitar o momentum que costuma surgir quando a direção do mercado fica mais clara.
Neste artigo, vamos analisar três estratégias ORB adaptadas a partir de artigos da Concretum Group. Primeiro, veremos os pressupostos deste estudo, incluindo os conceitos principais e a metodologia utilizada. Em seguida, explicaremos como cada estratégia funciona, listaremos as regras de uso dos sinais em cada uma delas e faremos uma análise estatística de seu desempenho. Por fim, vamos examiná-las do ponto de vista do uso em portfólio, com atenção especial ao tema da diversificação.
Neste artigo, não vamos nos aprofundar em programação; ao contrário, vamos nos concentrar na pesquisa, incluindo a reprodução, a análise e o teste das estratégias apresentadas nessas três publicações. Isso será útil para leitores que buscam possíveis vantagens no trading ou para aqueles que tenham interesse em entender como essas estratégias foram estudadas e reproduzidas. Apesar disso, todo o código MQL5 desses robôs será apresentado. Os leitores poderão expandir e complementar essa estrutura por conta própria.
Pressupostos do estudo
Esta seção apresenta a metodologia de pesquisa que usaremos para analisar as estratégias, bem como os conceitos principais que serão discutidos mais adiante neste artigo.
A Concretum Group é uma das poucas equipes que conduzem pesquisa acadêmica na área de desenvolvimento de estratégias de trading intradiário. Nos estudos que selecionamos para adaptação, o foco principal está em estratégias de negociação no intervalo entre a abertura e o fechamento do mercado, das 9:30 às 16:00 no horário do leste dos Estados Unidos. Como nossa corretora utiliza o fuso horário UTC+2/3, isso corresponde ao horário do servidor de 18:30 a 24:00. Ao testar, certifique-se de considerar o fuso horário da sua corretora.
No estudo original, negocia-se o QQQ, um ETF que acompanha o índice Nasdaq-100. É importante observar que o índice Nasdaq-100 reflete o desempenho de 100 grandes empresas de tecnologia listadas na Nasdaq. O índice em si, na prática, não é negociado, e o trading é feito apenas por meio de seus derivativos. O QQQ permite que os investidores tenham exposição a essas empresas por meio de um único ativo. Em nossos testes, vamos negociar o USTEC, um CFD sobre o Nasdaq-100, o que permite especular sobre os movimentos de preço sem possuir os ativos subjacentes, muitas vezes com uso de alavancagem para ampliar lucros ou perdas.
Neste artigo, vamos abordar dois indicadores-chave: alfa e beta. No trading, alfa representa o retorno excedente gerado por um investimento em relação a um referencial, como um índice de mercado. Ele mostra se o investimento supera o esperado e, em grande medida, reflete a existência de uma vantagem. Beta mede a sensibilidade do investimento aos movimentos do mercado. Um beta igual a 1 significa que ele acompanha as oscilações do mercado. Um valor acima de 1 indica maior volatilidade, enquanto um valor abaixo de 1 aponta para menor volatilidade. Esses indicadores desempenham um papel importante para entender em que medida sua estratégia se apoia nas tendências do mercado e em que medida depende de suas próprias vantagens específicas. Esse conhecimento ajuda a avaliar o possível viés direcional de ativos tendenciais, como índices ou criptomoedas.
Alfa e beta são calculados da seguinte forma:

Ri é o retorno do investimento, Rf é a taxa livre de risco, frequentemente baseada no rendimento de títulos do Tesouro ou simplesmente desconsiderada, e Rm é o retorno do mercado. A covariância e a variância normalmente são calculadas com base na variação diária.
O principal indicador que será usado mais adiante neste artigo é o preço médio ponderado por volume (Volume Weighted Average Price, VWAP). Ele é calculado da seguinte forma:

A ideia do VWAP é medir o preço médio pelo qual um ativo é negociado, ponderado pelo volume, o que reflete o valor “real” da negociação em um determinado período. Ao contrário de uma média simples, esse indicador atribui mais peso aos preços com maior atividade de negociação, tornando-se uma referência mais justa.
As aplicações mais comuns desse indicador no trading algorítmico incluem:
- Usá-lo como filtro de tendência.
- Usá-lo como trailing stop.
- Usá-lo como gerador de sinais, por exemplo, entrada quando o preço cruza a linha do VWAP.
Em geral, começamos o cálculo do VWAP a partir do primeiro candle na abertura do mercado. Na equação acima, Pi representa o preço do i-ésimo candle, que normalmente é o preço de fechamento, e Vi representa o volume negociado no i-ésimo candle. O volume negociado pode variar entre corretoras de CFD por causa de diferenças entre provedores de liquidez, mas, em geral, o peso relativo deve ser comparável entre todas as corretoras.
Neste artigo, foi implementado um modelo de avaliação dos riscos da alavancagem. Esse método define o risco como uma porcentagem fixa do nosso saldo em cada operação e é acionado quando o nível de stop loss é atingido. A distância do stop loss será definida como uma porcentagem fixa do preço do ativo, compatível com sua variação de preço e volatilidade. O risco por operação será definido com valores redondos para, de forma simples, alcançar um drawdown máximo em torno de 20%. Vamos testar cada estratégia ao longo de um período de cinco anos, de 1º de janeiro de 2020 a 1º de janeiro de 2025, para reunir uma quantidade suficiente de dados recentes e avaliar a lucratividade atual. A análise estatística detalhada incluirá comparações com a abordagem buy and hold com base no percentual de lucro total e em métricas individuais de desempenho.
Estratégia primeira. Direção do candle de abertura
A primeira estratégia que vamos analisar é a estratégia clássica de rompimento do nível de abertura, apresentada no artigo Can Day Trading Really Be Profitable? (O day trading pode realmente ser lucrativo?) da Concretum Group. A relevância das regras de geração de sinais dessa estratégia está em captar o momentum de curto prazo dos preços e, ao mesmo tempo, manter um equilíbrio entre praticidade e gestão de risco para traders intradiários. Os autores optaram por usar a abordagem ORB para aproveitar a maior volatilidade e o momentum direcional que costumam ser observados na abertura do mercado. Esse período é visto como uma janela crítica, na qual a atividade institucional muitas vezes se torna evidente, e os traders de varejo podem usar a direção do preço nesse intervalo como referência para identificar a tendência do dia inteiro.
Depois de analisar o artigo, identificamos várias formas de aprimorar a estratégia original. A abordagem inicial usava a máxima ou a mínima do primeiro candle de cinco minutos como nível de stop loss ou de take profit, com risco de 10R. Apesar de lucrativo, esse método se mostrou pouco prático para traders de varejo que operam em tempo real. Um stop loss rigidamente atrelado ao primeiro candle de cinco minutos aumentava os custos relativos de negociação. Além disso, não havia necessidade de um take profit de 10R, já que todas as operações são encerradas no fim do dia e esse nível raramente era atingido. Por fim, a estratégia original não tinha um filtro de regime, então a inclusão de uma média móvel para cumprir essa função poderia melhorá-la.
As regras de geração de sinais que adotamos foram as seguintes:
- Cinco minutos após a abertura do mercado, compramos se o candle de cinco minutos da abertura for de alta e fechar acima da média móvel de 350 períodos.
- Cinco minutos após a abertura do mercado, vendemos se o candle de cinco minutos da abertura for de baixa e fechar abaixo da média móvel de 350 períodos.
- Cinco minutos antes do fechamento do mercado, fechamos as posições em aberto.
- O stop loss é definido a 1% do preço de entrada.
- Risco de 2% em cada operação.
Código MQL5 completo do robô:
//USTEC-M5 #include <Trade/Trade.mqh> CTrade trade; input int startHour = 18; input int startMinute = 35; input int endHour = 23; input int endMinute = 55; input double risk = 2.0; input double slp = 0.01; input int MaPeriods = 350; input int Magic = 0; int barsTotal = 0; int handleMa; double lastClose=0; double lastOpen = 0; double lot = 0.1; //+------------------------------------------------------------------+ //|Initialization function | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(Magic); handleMa = iMA(_Symbol,PERIOD_CURRENT,MaPeriods,0,MODE_SMA,PRICE_CLOSE); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //|Deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //|On tick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol, PERIOD_CURRENT); if(barsTotal != bars){ barsTotal=bars; double ma[]; CopyBuffer(handleMa,0,1,1,ma); if(MarketOpen()){ lastClose = iClose(_Symbol,PERIOD_CURRENT,1); lastOpen = iOpen(_Symbol,PERIOD_CURRENT,1); if(lastClose<lastOpen&&lastClose<ma[0])executeSell(); if (lastClose>lastOpen&&lastClose>ma[0]) executeBuy(); } if(MarketClose()){ for(int i = PositionsTotal()-1; i>=0; i--){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)trade.PositionClose(pos); } } } } //+------------------------------------------------------------------+ //| Detect if market is opened | //+------------------------------------------------------------------+ bool MarketOpen() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; int currentMinute = timeStruct.min; if (currentHour == startHour &¤tMinute==startMinute)return true; else return false; } //+------------------------------------------------------------------+ //| Detect if market is closed | //+------------------------------------------------------------------+ bool MarketClose() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; int currentMinute = timeStruct.min; if (currentHour == endHour && currentMinute == endMinute)return true; else return false; } //+------------------------------------------------------------------+ //| Sell execution function | //+------------------------------------------------------------------+ void executeSell() { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); double sl = bid*(1+slp); sl = NormalizeDouble(sl, _Digits); lot = calclots(bid*slp); trade.Sell(lot,_Symbol,bid,sl); } //+------------------------------------------------------------------+ //| Buy execution function | //+------------------------------------------------------------------+ void executeBuy() { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); double sl = ask*(1-slp); sl = NormalizeDouble(sl, _Digits); lot = calclots(ask*slp); trade.Buy(lot,_Symbol,ask,sl); } //+------------------------------------------------------------------+ //| Calculate lot size based on risk and stop loss range | //+------------------------------------------------------------------+ double calclots(double slpoints) { double riskAmount = AccountInfoDouble(ACCOUNT_BALANCE) * risk / 100; double ticksize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); double tickvalue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double lotstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); double moneyperlotstep = slpoints / ticksize * tickvalue * lotstep; double lots = MathFloor(riskAmount / moneyperlotstep) * lotstep; lots = MathMin(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX)); lots = MathMax(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)); return lots; }
Uma operação típica será mais ou menos assim:

Resultados do teste com dados históricos:




Sem o filtro da média móvel, as regras da estratégia original gerariam uma operação por dia de negociação. O filtro reduziu o número de operações pela metade. Como o período médio de manutenção da posição abrange toda a sessão de negociação, os resultados refletem, até certo ponto, a tendência macroeconômica: as operações compradas ocorrem com mais frequência e apresentam um percentual maior de operações lucrativas. No geral, a estratégia apresenta um fator de lucro de 1,23 e um índice de Sharpe de 2,81, o que reflete um desempenho robusto. Essas regras simples são menos suscetíveis a sobreajuste, o que sugere que os bons resultados dos testes com dados históricos provavelmente também se manterão no trading real.


O robô supera de forma expressiva a abordagem buy and hold no USTEC ao longo desse período de cinco anos, ao mesmo tempo em que mantém o drawdown máximo em 18%, ou seja, metade do valor de referência. A curva de capital permanece suave, com exceção de um breve período de estagnação entre o fim de 2022 e o início de 2023, quando o índice USTEC enfrentou um drawdown mais acentuado.


Альфа: 1.6017 Бета: 0.0090
Um beta de 0,9% mostra que o retorno intradiário tem correlação de apenas 0,9% com o ativo subjacente, o que indica que a vantagem da estratégia decorre principalmente de suas regras, e não das tendências do mercado. Os drawdowns e os retornos permanecem estáveis, o que aponta resiliência em períodos de condições extremas de mercado, como a crise de 2020 provocada pela pandemia de COVID-19. A maioria dos meses foi lucrativa, e os meses de drawdown foram moderados, e o pior deles foi de 10,2%. No geral, trata-se de uma estratégia viável e lucrativa.
Estratégia segunda. Seguir a tendência com VWAP
A segunda estratégia que vamos analisar é uma estratégia de seguimento de tendência na abertura do mercado, apresentada no artigo Volume Weighted Average Price (VWAP) The Holy Grail for Day Trading Systems (Preço Médio Ponderado por Volume, VWAP: o Santo Graal dos sistemas de day trading). A relevância das regras de geração de sinais nessa estratégia está no uso do VWAP como uma referência clara, ponderada por volume, para identificar tendências intradiárias. A posição comprada é acionada quando o preço se aproxima de um patamar acima do VWAP, e a vendida, quando se aproxima de um patamar abaixo dele, com o objetivo de capturar um momentum confirmado e filtrar o ruído. Essa simplicidade garante aos traders intradiários sinais eficazes e reproduzíveis. Essa abordagem clássica de seguimento de tendência funciona melhor em condições de alta volatilidade, capturando tendências prolongadas e proporcionando uma relação retorno/risco elevada. Durante as cinco horas de funcionamento do mercado acionário após a abertura, o índice passa por oscilações significativas, oferecendo excelente liquidez dentro de uma janela de tempo bem definida e, por consequência, contribuindo para o bom desempenho dessa estratégia.
No artigo original, a negociação era feita no timeframe de um minuto, e os autores afirmavam que, entre os vários timeframes, esse era o mais eficiente. No entanto, os testes que realizei pessoalmente mostraram que, para essa estratégia, o timeframe de 15 minutos é mais adequado, provavelmente por causa dos custos de negociação mais altos dos CFDs em comparação com os ETFs, o que torna as operações frequentes menos viáveis do ponto de vista prático. Além disso, o artigo não informa o nível de stop loss. Em nossa abordagem, vamos incluir esse nível, já que usamos um timeframe mais alto. Esse acréscimo serve como proteção em situações de emergência e fornece uma faixa de controle para o cálculo do risco. Por fim, adicionamos um filtro de tendência com média móvel, como já havíamos feito antes.
As regras de geração de sinais que adotamos foram as seguintes:
- Após a abertura do mercado, compramos se não houver posição aberta e o último fechamento de 15 minutos estiver acima do VWAP e da média móvel de 300 períodos.
- Após a abertura do mercado, vendemos se não houver posições abertas e o último fechamento de 15 minutos estiver abaixo do VWAP e da média móvel de 300 períodos.
- O stop loss é definido a 0,8% do preço de entrada.
- Risco de 2% em cada operação.
Código MQL5 completo do robô:
//USTEC-M15 #include <Trade/Trade.mqh> CTrade trade; input int startHour = 18; input int startMinute = 35; input int endHour = 23; input int endMinute = 45; input double risk = 2.0; input double slp = 0.008; input int MaPeriods = 300; input int Magic = 0; int barsTotal = 0; int handleMa; double lastClose=0; double lot = 0.1; //+------------------------------------------------------------------+ //|Initialization function | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(Magic); handleMa = iMA(_Symbol,PERIOD_CURRENT,MaPeriods,0,MODE_SMA,PRICE_CLOSE); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //|Deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //|On tick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol, PERIOD_CURRENT); if(barsTotal != bars){ barsTotal=bars; bool NotInPosition = true; double ma[]; CopyBuffer(handleMa,0,1,1,ma); if(MarketOpened()&&!MarketClosed()){ lastClose = iClose(_Symbol,PERIOD_CURRENT,1); int startIndex = getSessionStartIndex(); double vwap = getVWAP(startIndex); for(int i = PositionsTotal()-1; i>=0; i--){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol){ if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY&&lastClose<vwap)||(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL&&lastClose>vwap))trade.PositionClose(pos); else NotInPosition=false; } } if(lastClose<vwap&&NotInPosition&&lastClose<ma[0])executeSell(); if(lastClose>vwap&&NotInPosition&&lastClose>ma[0]) executeBuy(); } if(MarketClosed()){ for(int i = PositionsTotal()-1; i>=0; i--){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)trade.PositionClose(pos); } } } } //+------------------------------------------------------------------+ //| Detect if market is opened | //+------------------------------------------------------------------+ bool MarketOpened() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; int currentMinute = timeStruct.min; if (currentHour >= startHour &¤tMinute>=startMinute)return true; else return false; } //+------------------------------------------------------------------+ //| Detect if market is closed | //+------------------------------------------------------------------+ bool MarketClosed() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; int currentMinute = timeStruct.min; if (currentHour >= endHour && currentMinute >= endMinute)return true; else return false; } //+------------------------------------------------------------------+ //| Sell execution function | //+------------------------------------------------------------------+ void executeSell() { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); double sl = bid*(1+slp); sl = NormalizeDouble(sl, _Digits); lot = calclots(bid*slp); trade.Sell(lot,_Symbol,bid,sl); } //+------------------------------------------------------------------+ //| Buy execution function | //+------------------------------------------------------------------+ void executeBuy() { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); double sl = ask*(1-slp); sl = NormalizeDouble(sl, _Digits); lot = calclots(ask*slp); trade.Buy(lot,_Symbol,ask,sl); } //+------------------------------------------------------------------+ //| Get VWAP function | //+------------------------------------------------------------------+ double getVWAP(int startCandle) { double sumPV = 0.0; // Sum of (price * volume) long sumV = 0.0; // Sum of volume // Loop from the starting candle index down to 1 (excluding current candle) for(int i = startCandle; i >= 1; i--) { // Calculate typical price: (High + Low + Close) / 3 double high = iHigh(_Symbol, PERIOD_CURRENT, i); double low = iLow(_Symbol, PERIOD_CURRENT, i); double close = iClose(_Symbol, PERIOD_CURRENT, i); double typicalPrice = (high + low + close) / 3.0; // Get volume and update sums long volume = iVolume(_Symbol, PERIOD_CURRENT, i); sumPV += typicalPrice * volume; sumV += volume; } // Calculate VWAP or return 0 if no volume if(sumV == 0) return 0.0; double vwap = sumPV / sumV; // Plot the dot datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); string objName = "VWAP" + TimeToString(currentBarTime, TIME_MINUTES); ObjectCreate(0, objName, OBJ_ARROW, 0, currentBarTime, vwap); ObjectSetInteger(0, objName, OBJPROP_COLOR, clrGreen); // Green dot ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_DOT); // Dot style ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1); // Size of the dot return vwap; } //+------------------------------------------------------------------+ //| Find the index of the candle corresponding to the session open | //+------------------------------------------------------------------+ int getSessionStartIndex() { int sessionIndex = 1; // Loop over bars until we find the session open for(int i = 1; i <=1000; i++) { datetime barTime = iTime(_Symbol, PERIOD_CURRENT, i); MqlDateTime dt; TimeToStruct(barTime, dt); if(dt.hour == startHour && dt.min == startMinute-5) { sessionIndex = i; break; } } return sessionIndex; } //+------------------------------------------------------------------+ //| Calculate lot size based on risk and stop loss range | //+------------------------------------------------------------------+ double calclots(double slpoints) { double riskAmount = AccountInfoDouble(ACCOUNT_BALANCE) * risk / 100; double ticksize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); double tickvalue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double lotstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); double moneyperlotstep = slpoints / ticksize * tickvalue * lotstep; double lots = MathFloor(riskAmount / moneyperlotstep) * lotstep; lots = MathMin(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX)); lots = MathMax(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)); return lots; }
Uma operação típica será mais ou menos assim:

Resultados do teste com dados históricos:




Em comparação com a primeira estratégia de rompimento na abertura, esta estratégia opera com mais frequência, realizando em média mais de uma operação por dia. Esse aumento ocorre porque é permitido reentrar no mercado sempre que o preço cruza novamente o nível de VWAP durante o horário de negociação. A taxa de acerto é menor, 42%, portanto abaixo de 50%, o que é característico de uma abordagem de acompanhamento de tendência com trailing stop dinâmico. Essa configuração favorece operações com uma relação lucro/risco mais alta, mas aumenta a probabilidade de saída da posição. O índice de Sharpe e o fator de lucro são excepcionalmente altos, 3,57 e 1,26, respectivamente.


Essa estratégia supera com folga a abordagem buy and hold, alcançando retorno de 501% em cinco anos. Ao mesmo tempo, o drawdown máximo é de 16%, e o pior período ocorreu no fim de 2021, o que difere da pior fase do índice USTEC e indica ausência de correlação nos indicadores de desempenho.


Альфа: 4.8714 Бета: 0.0985
O beta é equivalente ao da primeira estratégia, o que também indica baixa correlação com o ativo subjacente. Chama atenção o fato de que o alfa desta estratégia é três vezes maior que o da primeira, mantendo ao mesmo tempo um drawdown máximo semelhante. Essa vantagem provavelmente está ligada à maior frequência de negociação, aos períodos de manutenção mais curtos e à diversificação interna mais ampla, decorrente da presença tanto de posições compradas quanto vendidas ao longo do mesmo dia. A tabela com os dados mensais confirma a robustez da estratégia: perdas e ganhos se distribuem de forma equilibrada e estável ao longo dos meses.
Estratégia terceira. Rompimento das bandas Concretum (Concretum Bands Breakout)
A terceira estratégia é uma estratégia de rompimento da faixa de ruído, usada para operar na abertura do mercado. Ela foi apresentada pela primeira vez no artigo Beat the Market An Effective Intraday Momentum Strategy for S&P500 ETF (SPY) (Supere o mercado: uma estratégia intradiária de momentum eficaz para o ETF do S&P 500 (SPY)), e, mais tarde, viralizou no X/Twitter. Os princípios centrais por trás das regras de geração de sinais da estratégia de rompimento das bandas Concretum decorrem da busca por identificar movimentos relevantes de preço causados por desequilíbrios entre oferta e demanda no trading intradiário. A estratégia usa bandas de volatilidade calculadas com base no fechamento do dia anterior ou na abertura do dia atual e ajustadas por um multiplicador de volatilidade para definir a “Zona de Ruído”, onde ocorrem oscilações aleatórias de preço. As regras buscam filtrar o ruído do mercado, capturar movimentos de momentum de alta probabilidade e se adaptar à volatilidade variável, garantindo que as operações coincidam com o início real da tendência, e não oscilações passageiras.
Eis os cálculos dessas bandas.

Como as regras de geração de sinais do artigo original são bem elaboradas, não faremos mudanças significativas nelas neste artigo. Para simplificar, vamos manter o mesmo ativo, o USTEC, e seguir a mesma abordagem de gestão de risco, o que pode levar a resultados diferentes dos apresentados no artigo original. As regras de geração de sinais são as seguintes:
- Após a abertura do mercado, compramos quando um candle de 1 minuto cruzar a banda superior.
- Após a abertura do mercado, vendemos quando um candle de 1 minuto cruzar a banda inferior.
- Encerramos todas as posições no fechamento do mercado.
- O stop loss é definido a 1% do preço de entrada, com o VWAP atuando como trailing stop.
- Risco de 4% em cada operação.
Código MQL5 completo do robô:
//USTEC-M1 #include <Trade/Trade.mqh> CTrade trade; input int startHour = 18; input int startMinute = 35; input int endHour = 23; input int endMinute = 55; input double risk = 4.0; input double slp = 0.01; input int Magic = 0; input int maPeriod = 400; int barsTotal = 0; int handleMa; double lastClose=0; double lastOpen = 0; double lot = 0.1; //+------------------------------------------------------------------+ //|Initialization function | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(Magic); handleMa = iMA(_Symbol,PERIOD_CURRENT,maPeriod,0,MODE_SMA,PRICE_CLOSE); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //|Deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //|On tick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol, PERIOD_CURRENT); if(barsTotal != bars){ barsTotal=bars; bool NotInPosition = true; double ma[]; CopyBuffer(handleMa,0,1,1,ma); if(MarketOpened()&&!MarketClosed()){ lastClose = iClose(_Symbol,PERIOD_CURRENT,1); lastOpen = iOpen(_Symbol,PERIOD_CURRENT,1); int startIndex = getSessionStartIndex(); double vwap = getVWAP(startIndex); for(int i = PositionsTotal()-1; i>=0; i--){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol){ if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY&&lastClose<vwap)||(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL&&lastClose>vwap))trade.PositionClose(pos); else NotInPosition=false; } } double lower = getLowerBand(); double upper = getUpperBand(); if(NotInPosition&&lastOpen>lower&&lastClose<lower&&lastClose<ma[0])executeSell(); if(NotInPosition&&lastOpen<upper&&lastClose>upper&&lastClose>ma[0]) executeBuy(); } if(MarketClosed()){ for(int i = PositionsTotal()-1; i>=0; i--){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)trade.PositionClose(pos); } } } } //+------------------------------------------------------------------+ //| Detect if market is opened | //+------------------------------------------------------------------+ bool MarketOpened() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; int currentMinute = timeStruct.min; if (currentHour >= startHour &¤tMinute>=startMinute)return true; else return false; } //+------------------------------------------------------------------+ //| Detect if market is closed | //+------------------------------------------------------------------+ bool MarketClosed() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; int currentMinute = timeStruct.min; if (currentHour >= endHour && currentMinute >= endMinute)return true; else return false; } //+------------------------------------------------------------------+ //| Sell execution function | //+------------------------------------------------------------------+ void executeSell() { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); double sl = bid*(1+slp); sl = NormalizeDouble(sl, _Digits); lot = calclots(bid*slp); trade.Sell(lot,_Symbol,bid,sl); } //+------------------------------------------------------------------+ //| Buy execution function | //+------------------------------------------------------------------+ void executeBuy() { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); double sl = ask*(1-slp); sl = NormalizeDouble(sl, _Digits); lot = calclots(ask*slp); trade.Buy(lot,_Symbol,ask,sl); } //+------------------------------------------------------------------+ //| Get VWAP function | //+------------------------------------------------------------------+ double getVWAP(int startCandle) { double sumPV = 0.0; // Sum of (price * volume) long sumV = 0.0; // Sum of volume // Loop from the starting candle index down to 1 (excluding current candle) for(int i = startCandle; i >= 1; i--) { // Calculate typical price: (High + Low + Close) / 3 double high = iHigh(_Symbol, PERIOD_CURRENT, i); double low = iLow(_Symbol, PERIOD_CURRENT, i); double close = iClose(_Symbol, PERIOD_CURRENT, i); double typicalPrice = (high + low + close) / 3.0; // Get volume and update sums long volume = iVolume(_Symbol, PERIOD_CURRENT, i); sumPV += typicalPrice * volume; sumV += volume; } // Calculate VWAP or return 0 if no volume if(sumV == 0) return 0.0; double vwap = sumPV / sumV; // Plot the dot datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); string objName = "VWAP" + TimeToString(currentBarTime, TIME_MINUTES); ObjectCreate(0, objName, OBJ_ARROW, 0, currentBarTime, vwap); ObjectSetInteger(0, objName, OBJPROP_COLOR, clrGreen); // Green dot ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_DOT); // Dot style ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1); // Size of the dot return vwap; } //+------------------------------------------------------------------+ //| Find the index of the candle corresponding to the session open | //+------------------------------------------------------------------+ int getSessionStartIndex() { int sessionIndex = 1; // Loop over bars until we find the session open for(int i = 1; i <=1000; i++) { datetime barTime = iTime(_Symbol, PERIOD_CURRENT, i); MqlDateTime dt; TimeToStruct(barTime, dt); if(dt.hour == startHour && dt.min == 30) { sessionIndex = i; break; } } return sessionIndex; } //+------------------------------------------------------------------+ //| Get the number of bars from now to market open | //+------------------------------------------------------------------+ int getBarShiftForTime(datetime day_start, int hour, int minute) { MqlDateTime dt; TimeToStruct(day_start, dt); dt.hour = hour; dt.min = minute; dt.sec = 0; datetime target_time = StructToTime(dt); int shift = iBarShift(_Symbol, PERIOD_M1, target_time, true); return shift; } //+------------------------------------------------------------------+ //| Get the upper Concretum band value | //+------------------------------------------------------------------+ double getUpperBand() { // Get the time of the current bar datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0); MqlDateTime current_dt; TimeToStruct(current_time, current_dt); int current_hour = current_dt.hour; int current_min = current_dt.min; // Find today's opening price at 9:30 AM datetime today_start = iTime(_Symbol, PERIOD_D1, 0); int bar_at_930_today = getBarShiftForTime(today_start, 9, 30); if (bar_at_930_today < 0) return 0; // Return 0 if no 9:30 bar exists double open_930_today = iOpen(_Symbol, PERIOD_M1, bar_at_930_today); if (open_930_today == 0) return 0; // No valid price // Calculate sigma based on the past 14 days double sum_moves = 0; int valid_days = 0; for (int i = 1; i <= 14; i++) { datetime day_start = iTime(_Symbol, PERIOD_D1, i); int bar_at_930 = getBarShiftForTime(day_start, 9, 30); int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min); if (bar_at_930 < 0 || bar_at_HHMM < 0) continue; // Skip if bars don't exist double open_930 = iOpen(_Symbol, PERIOD_M1, bar_at_930); double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM); if (open_930 == 0) continue; // Skip if no valid opening price double move = MathAbs(close_HHMM / open_930 - 1); sum_moves += move; valid_days++; } if (valid_days == 0) return 0; // Return 0 if no valid data double sigma = sum_moves / valid_days; // Calculate the upper band double upper_band = open_930_today * (1 + sigma); // Plot a blue dot at the upper band level string obj_name = "UpperBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS); ObjectCreate(0, obj_name, OBJ_ARROW, 0, current_time, upper_band); ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 159); // Dot symbol ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue); ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 2); return upper_band; } //+------------------------------------------------------------------+ //| Get the lower Concretum band value | //+------------------------------------------------------------------+ double getLowerBand() { // Get the time of the current bar datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0); MqlDateTime current_dt; TimeToStruct(current_time, current_dt); int current_hour = current_dt.hour; int current_min = current_dt.min; // Find today's opening price at 9:30 AM datetime today_start = iTime(_Symbol, PERIOD_D1, 0); int bar_at_930_today = getBarShiftForTime(today_start, 9, 30); if (bar_at_930_today < 0) return 0; // Return 0 if no 9:30 bar exists double open_930_today = iOpen(_Symbol, PERIOD_M1, bar_at_930_today); if (open_930_today == 0) return 0; // No valid price // Calculate sigma based on the past 14 days double sum_moves = 0; int valid_days = 0; for (int i = 1; i <= 14; i++) { datetime day_start = iTime(_Symbol, PERIOD_D1, i); int bar_at_930 = getBarShiftForTime(day_start, 9, 30); int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min); if (bar_at_930 < 0 || bar_at_HHMM < 0) continue; // Skip if bars don't exist double open_930 = iOpen(_Symbol, PERIOD_M1, bar_at_930); double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM); if (open_930 == 0) continue; // Skip if no valid opening price double move = MathAbs(close_HHMM / open_930 - 1); sum_moves += move; valid_days++; } if (valid_days == 0) return 0; // Return 0 if no valid data double sigma = sum_moves / valid_days; // Calculate the lower band double lower_band = open_930_today * (1 - sigma); // Plot a red dot at the lower band level string obj_name = "LowerBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS); ObjectCreate(0, obj_name, OBJ_ARROW, 0, current_time, lower_band); ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 159); // Dot symbol ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrRed); ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 2); return lower_band; } //+------------------------------------------------------------------+ //| Calculate lot size based on risk and stop loss range | //+------------------------------------------------------------------+ double calclots(double slpoints) { double riskAmount = AccountInfoDouble(ACCOUNT_BALANCE) * risk / 100; double ticksize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); double tickvalue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double lotstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); double moneyperlotstep = slpoints / ticksize * tickvalue * lotstep; double lots = MathFloor(riskAmount / moneyperlotstep) * lotstep; lots = MathMin(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX)); lots = MathMax(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)); return lots; }
Uma operação típica será mais ou menos assim:

Resultados do teste com dados históricos:




A estratégia opera com frequência semelhante à da primeira estratégia ORB, com média de cerca de uma operação a cada dois dias de negociação. Ela não opera todos os dias, porque em alguns casos os movimentos de preço permanecem dentro da faixa de ruído e não conseguem rompê-la. O percentual de operações lucrativas fica abaixo de 50%, o que resulta do uso do VWAP como trailing stop dinâmico. O fator de lucro, de 1,3, e o índice de Sharpe, de 5,9, indicam alto retorno em relação ao drawdown.


Essa estratégia supera ligeiramente a abordagem buy and hold, ao mesmo tempo em que mantém metade do drawdown máximo. No entanto, nela os períodos de drawdown relevante ocorreram com mais frequência do que na estratégia anterior. Isso mostra que, apesar do excelente desempenho, essa estratégia costuma passar por fases prolongadas de queda antes de alcançar novos máximos de capital.


Альфа: 1.6562 Бета: -0.1183
O beta desta estratégia é de -11%, o que indica uma pequena correlação negativa com o ativo subjacente. Esse é um resultado favorável para traders que buscam vantagem em movimentos contrários às tendências do mercado. Em comparação com as outras duas estratégias, esta apresenta mais meses de drawdown, cerca de 50%, mas entrega retornos mais altos nos meses lucrativos. Esse modelo sugere que, no trading real, os traders devem se preparar para períodos prolongados de drawdown e aguardar com paciência as fases de maior retorno. Com uma amostra significativa ao longo de um período extenso, essa estratégia continua lucrativa.
Considerações
Em nosso artigo anterior, estudamos a construção de sistemas-modelo, e não de estratégias isoladas. Aplicamos a mesma ideia neste artigo. As três estratégias se baseiam em rompimentos de intervalos temporais na abertura do mercado acionário, com variações que demonstraram lucratividade. Além disso, compartilhamos ideias sobre como buscar vantagem estratégica ao adaptar artigos científicos com apoio do nosso próprio conhecimento e da nossa intuição. Essa abordagem é uma excelente forma de identificar conceitos robustos de trading e ampliar nossa capacidade de compreensão.
Agora que temos três estratégias lucrativas, precisamos examinar a perspectiva de portfólio. Antes de operá-las simultaneamente, precisamos analisar seus resultados conjuntos, suas correlações e o drawdown máximo agregado. Na negociação algorítmica, a diversificação é o verdadeiro Santo Graal. Ela ajuda a compensar perdas de estratégias diferentes em períodos diferentes. Até certo ponto, seu retorno máximo é limitado pelo drawdown que você está disposto a tolerar. A combinação de estratégias distintas permite aumentar o volume investido mantendo um nível de drawdown semelhante, e isso eleva a rentabilidade. No entanto, não é possível ampliar o risco indefinidamente, porque o risco mínimo sempre será maior do que o risco de operações individuais.
Algumas formas comuns de alcançar diversificação incluem:
- Operar o mesmo modelo estratégico e distribuí-lo entre diferentes ativos não correlacionados.
- Operar modelos estratégicos diferentes sobre o mesmo ativo.
- Distribuir o capital entre diferentes abordagens de trading, como opções, arbitragem e seleção de ações.
É importante entender que mais diversificação nem sempre é melhor; o que importa é a diversificação não correlacionada. Por exemplo, aplicar a mesma estratégia em todos os mercados de cripto não é a opção ideal, porque a maioria dos criptoativos apresenta alto grau de correlação em um contexto mais amplo. Além disso, também pode ser um erro confiar apenas na diversificação obtida por meio de testes com dados históricos, porque a correlação depende do período analisado, por exemplo, retornos diários ou mensais. Ao mesmo tempo, quando há mudanças significativas no regime de mercado, as correlações entre estratégias podem se alterar e se desalinhar de forma inesperada. É por isso que alguns traders preferem usar as correlações dos resultados reais de trading em vez das correlações obtidas em testes com dados históricos, para avaliar se a vantagem de suas estratégias diminuiu.
Considerando essas informações, apresentamos abaixo as estatísticas do teste de desempenho conjunto das três estratégias.


As curvas de capital e de drawdown mostram com clareza como diferentes estratégias compensam os drawdowns umas das outras em períodos distintos. Agora, o drawdown máximo fica em torno de 10%, bem abaixo dos drawdowns máximos das estratégias individuais, cada um superior a 15%.


Os drawdowns e os retornos se distribuem de forma uniforme ao longo dos meses, o que indica ausência de períodos de condições extremas de mercado que exerçam influência desproporcional, positiva ou negativa, sobre os resultados do teste com dados históricos. Isso faz sentido, considerando que há mais de 3000 observações e uma alocação consistente de risco em cada operação.

A correlação mede o quanto as curvas de capital obtidas nos testes com dados históricos de cada estratégia se assemelham. Ela varia de -1, para comportamentos opostos, a 1, para comportamentos idênticos, normalmente ao comparar dois objetos. Nós a calculamos usando x para o eixo temporal da curva de capital e y para o eixo de retorno.

Conclusão
Neste artigo, analisamos três estratégias de rompimento da faixa intradiária de abertura apresentadas em publicações científicas da Concretum Group. Começamos com uma breve exposição da relevância do estudo, explicando os principais conceitos e as metodologias nele utilizadas. Em seguida, examinamos a lógica por trás das três estratégias, identificamos pontos de melhoria, apresentamos regras claras de geração de sinais, o código MQL5 e a análise estatística dos testes com dados históricos. Por fim, apresentamos algumas reflexões sobre a metodologia adotada e introduzimos a diversificação ao analisar os resultados conjuntos.
O artigo oferece uma visão da real confiabilidade do desenvolvimento da estratégia. Uma análise estatística mais aprofundada amplia essa perspectiva sobre a eficácia da estratégia e de seu papel no funcionamento do portfólio. Todo o esforço está voltado para aprofundar a compreensão e reforçar a confiança antes do início do trading real. Os leitores são convidados a reproduzir a investigação e, com a estrutura apresentada aqui, desenvolver robôs.
Tabela de arquivos
| Nome do arquivo | Uso do arquivo |
|---|---|
| ORB1.mq5. | Código MQL5 do robô para a primeira estratégia |
| ORB2.mq5 | Código MQL5 do robô para a segunda estratégia |
| ORB3.mq5 | Código MQL5 do robô para a terceira estratégia |
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/17745
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
De Iniciante a Especialista: Indicador de Força de Suporte e Resistência (SRSI)
Como simplificar o teste manual de estratégias com MQL5: construindo seu próprio conjunto de ferramentas
Está chegando o novo MetaTrader 5 e MQL5
Uma Nova Abordagem para Critérios Personalizados em Otimizações (Parte 1): Exemplos de Funções de Ativação
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso
Isso só funciona em futuros, como o NQ.
Isso só funciona em futuros, como o NQ.