English Русский
preview
Criação de um robô market-neutral probabilístico com base na distribuição dos retornos

Criação de um robô market-neutral probabilístico com base na distribuição dos retornos

MetaTrader 5Testador |
29 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

A maioria dos traders tenta prever a direção do mercado, e a maioria perde. Existe outra abordagem: em vez de tentar adivinhar a direção, podemos explorar as propriedades estatísticas do próprio mercado. Uma estratégia market-neutral baseada na distribuição dos retornos funciona exatamente assim: ela posiciona ordens em níveis que, estatisticamente, têm determinada probabilidade de serem alcançados pelo preço, independentemente da tendência.


Fundamento: distribuição dos retornos

O retorno é a variação percentual do preço em um período fixo. Se o preço estava em 1.0850 e passou para 1.0900, o retorno é igual a (1.0900 - 1.0850) / 1.0850 = 0.0046, ou 0,46%. Se tomarmos todos os retornos históricos dos períodos anteriores e construirmos um histograma, obteremos uma distribuição. Para pares de moedas, essa distribuição costuma ser próxima da normal, centrada em torno de zero e com caudas pesadas.

Por exemplo, este é o histograma da distribuição dos retornos no gráfico horário do EURUSD ao longo de 11 anos, desde janeiro de 2014:

E aqui temos a distribuição acumulada, na qual já podemos observar as famosas "caudas pesadas". Elas são uma das causas da assimetria entre lucro e risco e também um dos motivos pelos quais prever o mercado é tão difícil: movimentos extremos ocorrem com frequência muito maior do que a prevista por uma distribuição normal. Se os retornos seguissem a clássica curva gaussiana em forma de sino, a probabilidade de um movimento superior a 1% em 10 horas seria ínfima, mas os dados reais mostram que eventos desse tipo ocorrem regularmente.

É justamente por isso que estratégias baseadas na hipótese de normalidade subestimam sistematicamente o risco de perdas catastróficas e superestimam a frequência de pequenos movimentos lucrativos. Isso torna a arbitragem estatística mais complexa, mas também mais fiel à realidade: trabalhamos com a distribuição empírica observada, e não com seu modelo matemático idealizado.

A ideia central é a seguinte: se conhecemos a distribuição que abrange 90% dos retornos observados em uma determinada janela temporal, podemos prever não a direção do preço, mas a probabilidade de ele atingir níveis específicos. Essa é uma filosofia de trading fundamentalmente diferente.

Em vez de perguntar "Para onde o preço vai?", perguntamos "Qual é a probabilidade de o preço atingir determinado nível em N períodos?". A diferença é fundamental: a primeira pergunta exige conhecer o futuro; a segunda trabalha com estatísticas do passado. Se a distribuição empírica mostra que, em 10% dos casos, o preço cai 0,5% ou mais ao longo de 10 horas, podemos posicionar uma BUY LIMIT nesse nível, sabendo que a probabilidade de execução é de aproximadamente 10%. Não sabemos exatamente quando isso acontecerá, mas sabemos que acontecerá com determinada frequência.

Essa lógica inverte a abordagem tradicional da gestão de capital. Uma estratégia clássica aloca volumes maiores em sinais "confiáveis" e volumes menores nos "duvidosos". A abordagem estatística faz o contrário: volumes menores nos níveis centrais de alta probabilidade (eles são atingidos com frequência, mas geram pouco lucro) e volumes maiores nos níveis de baixa probabilidade situados nas caudas (eles são atingidos raramente, mas oferecem maior potencial de lucro). A matemática equilibra frequência e magnitude por meio da ponderação pelo inverso da probabilidade.

Além disso, ao trabalhar com a distribuição completa, e não com previsões pontuais, a estratégia se torna naturalmente market-neutral. Posicionamos ordens ao longo de todo o espectro de movimentos possíveis, tanto para cima quanto para baixo, com volumes proporcionais à raridade estatística de cada movimento. Isso não é hedging no sentido clássico, mas o reconhecimento da incerteza fundamental quanto à direção do mercado, combinado com o uso da previsibilidade estatística da distribuição.

Começaremos criando uma estrutura para armazenar as estatísticas:

struct ReturnStats {
   double mean;              // Average return
   double stdDev;            // Standard deviation
   double percentiles[10];   // P10, P20, ..., P100
};

ReturnStats returnStats;
double returns[];
bool statsCalculated = false;

A média indica a tendência geral ao longo da janela (normalmente próxima de zero em períodos curtos); o desvio-padrão representa a volatilidade; os percentis são quantis da distribuição que a dividem em partes com a mesma probabilidade.


Cálculo da distribuição empírica

A função de cálculo carrega os dados históricos e calcula os retornos em uma janela móvel:

bool CalculateReturnDistribution() {
   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   
   int copied = CopyRates(_Symbol, PERIOD_CURRENT, 0, HistoryBars + WindowBars, rates);
   if(copied < HistoryBars + WindowBars) {
      Print("ERROR: Not enough history. Got ", copied, " bars");
      return false;
   }
   
   int numReturns = HistoryBars;
   ArrayResize(returns, numReturns);
   
   for(int i = 0; i < numReturns; i++) {
      double priceStart = rates[i + WindowBars].close;
      double priceEnd = rates[i].close;
      
      if(priceStart > 0) {
         returns[i] = (priceEnd - priceStart) / priceStart;
      } else {
         returns[i] = 0;
      }
   }

Para cada ponto do histórico, são usados o preço de fechamento da barra atual e o preço de fechamento de WindowBars barras atrás. O retorno é calculado como uma variação relativa. Com WindowBars = 10 em um gráfico M5, obtemos o retorno de 50 minutos. Em um gráfico H1, obtemos o retorno de 10 horas.

Depois de calcular todos os retornos, ordenamos o array para extrair os percentis:

double sortedReturns[];
   ArrayResize(sortedReturns, numReturns);
   ArrayCopy(sortedReturns, returns);
   ArraySort(sortedReturns);
   
   returnStats.mean = 0;
   for(int i = 0; i < numReturns; i++) {
      returnStats.mean += returns[i];
   }
   returnStats.mean /= numReturns;
   
   double variance = 0;
   for(int i = 0; i < numReturns; i++) {
      double diff = returns[i] - returnStats.mean;
      variance += diff * diff;
   }
   returnStats.stdDev = MathSqrt(variance / numReturns);
   
   for(int i = 0; i < 10; i++) {
      double percentileLevel = (i + 1) * 0.10;
      int index = (int)(numReturns * percentileLevel);
      if(index >= numReturns) index = numReturns - 1;
      returnStats.percentiles[i] = sortedReturns[index];
   }
   
   statsCalculated = true;
   return true;
}

Os percentis são obtidos por simples indexação do array ordenado. P10 corresponde ao elemento no índice equivalente a 10% do comprimento do array. P90 corresponde ao elemento no índice de 90%. Assim, obtemos os quantis exatos da distribuição empírica, sem fazer qualquer suposição sobre sua forma. Esse ponto é importante: não pressupomos normalidade, usamos a distribuição tal como ela é.


Matemática dos pesos

Se todas as ordens forem posicionadas com o mesmo volume, a estratégia ficará desequilibrada. As ordens próximas ao centro da distribuição (em torno de retorno zero) são executadas com frequência, mas geram pouco lucro por estarem muito próximas do preço atual. Já as ordens nas caudas da distribuição são executadas raramente, mas apresentam maior potencial de lucro.

A solução ideal é atribuir volumes maiores às ordens com baixa probabilidade de execução, para que essas execuções raras compensem os longos períodos de espera. A probabilidade de um nível ser atingido está relacionada ao seu percentil. Se um nível corresponde a P10, isso significa que 10% dos movimentos históricos chegaram a esse nível ou foram além dele. A probabilidade de atingimento é de aproximadamente 90% (100% - 10%). Para P90, a probabilidade de atingir ou ultrapassar o nível é de 10%.

double CalculateProbabilityWeight(int percentileIndex) {
   if(!UseProbabilityWeights) {
      return MathPow(LotMultiplier, percentileIndex);
   }
   
   double probability = (100.0 - (percentileIndex + 1) * 10.0) / 100.0;
   double weight = 1.0 / MathMax(probability, 0.1);
   
   return MathPow(weight, 0.5) * MathPow(LotMultiplier, percentileIndex * 0.3);
}

A fórmula converte a probabilidade em peso de forma inversa: quanto menor a probabilidade, maior o peso. A raiz quadrada suaviza a progressão para evitar volumes extremos nos níveis mais distantes. Um fator adicional baseado em LotMultiplier na potenciação permite ajustar a agressividade da progressão.

Com lote-base de 0.01 e LotMultiplier = 1.5, obtemos o seguinte: P10 (probabilidade de 90%) resulta em peso de aproximadamente 1.2 e volume de 0.012 lote; P50 (probabilidade de 50%) resulta em peso 2.0 e volume de 0.020 lote; P90 (probabilidade de 10%) resulta em peso 5.0 e volume de 0.050 lote. A progressão é não linear, e os níveis mais distantes recebem volumes 4 a 5 vezes maiores que os centrais.


Posicionamento da grade de ordens

A função PlaceAllGridOrders posiciona ordens limit nos níveis correspondentes aos percentis:

void PlaceAllGridOrders() {
   if(!statsCalculated) return;
   
   double currentPrice = (Ask + Bid) / 2.0;
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   
   gridPlacementTime = TimeCurrent();
   
   if(Direction == NEUTRAL || Direction == BUY) {
      for(int i = 0; i < 10; i++) {
         double returnLevel = returnStats.percentiles[i];
         
         if(returnLevel <= 0.01) {
            double orderPrice = NormalizeDouble(currentPrice * (1 + returnLevel), digits);
            double weight = CalculateProbabilityWeight(i);
            double currentLot = NormalizeDouble(LotSize * weight, 2);
            
            Trade.BuyLimit(currentLot, orderPrice, _Symbol, 0, 0, ORDER_TIME_SPECIFIED, 
                          TimeCurrent() + OrderExpirationSeconds, comment);
         }
      }
   }
   
   if(Direction == NEUTRAL || Direction == SELL) {
      for(int i = 0; i < 10; i++) {
         double returnLevel = returnStats.percentiles[i];
         
         if(returnLevel >= -0.01) {
            double orderPrice = NormalizeDouble(currentPrice * (1 + returnLevel), digits);
            double weight = CalculateProbabilityWeight(i);
            double currentLot = NormalizeDouble(LotSize * weight, 2);
            
            Trade.SellLimit(currentLot, orderPrice, _Symbol, 0, 0, ORDER_TIME_SPECIFIED,
                           TimeCurrent() + OrderExpirationSeconds, comment);
         }
      }
   }
}

Para cada percentil, o preço-alvo é calculado pela fórmula preço_atual × (1 + retorno_do_percentil). Se o retorno for negativo, por exemplo, -0.005 ou -0,5%, o preço-alvo ficará abaixo do preço atual e será posicionada uma BUY LIMIT. A lógica é simples: estatisticamente, há determinada probabilidade de o preço cair nessa magnitude, e compramos nessa queda. Se o retorno for positivo, o preço-alvo ficará acima do atual e será posicionada uma SELL LIMIT. Nesse caso, vendemos durante uma alta estatisticamente provável.

O parâmetro Direction define a direção da estratégia. NEUTRAL posiciona ordens nos dois sentidos, correspondendo à estratégia market-neutral clássica. BUY posiciona apenas ordens de compra nos retornos negativos, apostando em uma recuperação após a queda. SELL posiciona apenas ordens de venda nos retornos positivos, apostando em uma correção após a alta.


Expiração da grade

A distribuição estatística muda gradualmente ao longo do tempo. A volatilidade aumenta e diminui. O retorno médio se desloca quando uma tendência começa a se formar. Uma grade posicionada uma hora atrás pode deixar de representar a distribuição atual. A solução é recalcular periodicamente a distribuição e atualizar a grade.

datetime gridPlacementTime = 0;

bool CheckGridExpiration() {
   if(!UseGridExpiration || gridPlacementTime == 0) return false;
   
   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   int copied = CopyRates(_Symbol, PERIOD_CURRENT, 0, WindowBars + 1, rates);
   
   if(copied < WindowBars + 1) return false;
   
   datetime expirationThreshold = rates[WindowBars].time;
   
   if(gridPlacementTime <= expirationThreshold) {
      Print("Grid EXPIRED! Placement:", TimeToString(gridPlacementTime), 
            " Threshold:", TimeToString(expirationThreshold));
      return true;
   }
   
   return false;
}

A função compara o momento em que a grade foi posicionada com o limite de expiração. Esse limite corresponde ao horário da barra situada WindowBars barras antes do momento atual. Se a grade for mais antiga do que esse limite, ela será considerada desatualizada. Quando ocorre a expiração, todas as ordens pendentes são removidas, as posições abertas são fechadas se essa opção estiver habilitada, a distribuição é recalculada e uma nova grade é posicionada. Isso mantém a estratégia adaptável às mudanças nas condições de mercado.


Gestão do lucro e das posições

O robô possui três parâmetros independentes de take profit: uma meta global, uma meta para as compras e uma meta para as vendas. Isso permite uma gestão de risco mais flexível.

void CalculateProfits(double &totalProfit, double &buyProfit, double &sellProfit) {
   totalProfit = 0;
   buyProfit = 0;
   sellProfit = 0;

   for(int i = PositionsTotal() - 1; i >= 0; i--) {
      ulong ticket = PositionGetTicket(i);
      if(!PositionSelectByTicket(ticket)) continue;
      
      if(PositionGetString(POSITION_SYMBOL) != _Symbol || 
         PositionGetInteger(POSITION_MAGIC) != OrderMagic) continue;

      double profit = PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
      totalProfit += profit;

      ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      if(type == POSITION_TYPE_BUY)
         buyProfit += profit;
      else if(type == POSITION_TYPE_SELL)
         sellProfit += profit;
   }
}

A função percorre todas as posições abertas, obtém o lucro flutuante e o swap e agrupa os valores por tipo. No laço principal OnTick, são verificadas as condições para o atingimento das metas:

void OnTick() {
   if(!CheckExpiration()) return;
   
   if(CheckGridExpiration()) {
      DeleteAllPendingOrders();
      if(CloseOnGridExpiration) CloseAllPositions();
      CalculateReturnDistribution();
      PlaceAllGridOrders();
      return;
   }
   
   double totalPnL, buyPnL, sellPnL;
   CalculateProfits(totalPnL, buyPnL, sellPnL);
   
   if(totalPnL >= TotalProfitTarget) {
      DeleteAllPendingOrders();
      CloseAllPositions();
      return;
   }
   
   if(buyPnL >= BuyProfitTarget) {
      ClosePositionsByType(POSITION_TYPE_BUY);
   }
   
   if(sellPnL >= SellProfitTarget) {
      ClosePositionsByType(POSITION_TYPE_SELL);
   }
   
   ManageGrid();
}

A verificação é realizada a cada tick. Quando a meta global é atingida, todas as posições são fechadas. Quando a meta de um dos lados é atingida, somente as posições desse lado são fechadas, enquanto o outro continua operando. Isso é útil em mercados com tendência, nos quais um dos lados acumula lucro rapidamente enquanto o outro permanece praticamente estagnado.


Manutenção da grade

A função ManageGrid monitora se a grade continua atualizada e remove ordens expiradas:

void ManageGrid() {
   int pendingOrders, openPositions;
   CountOrdersAndPositions(pendingOrders, openPositions);
   
   static int tickCount = 0;
   if(++tickCount % 100 == 0) {
      CalculateReturnDistribution();
   }
   
   int minOrders = (Direction == NEUTRAL) ? 10 : 5;
   
   if(pendingOrders < minOrders) {
      DeleteAllPendingOrders();
      PlaceAllGridOrders();
   }
   
   for(int i = OrdersTotal() - 1; i >= 0; i--) {
      ulong ticket = OrderGetTicket(i);
      if(OrderSelect(ticket)) {
         if(OrderGetString(ORDER_SYMBOL) == _Symbol && 
            OrderGetInteger(ORDER_MAGIC) == OrderMagic) {
            datetime orderTime = (datetime)OrderGetInteger(ORDER_TIME_SETUP);
            if(TimeCurrent() - orderTime > OrderExpirationSeconds) {
               Trade.OrderDelete(ticket);
            }
         }
      }
   }
}

A cada 100 ticks, a distribuição é recalculada para que a estratégia se adapte às condições do mercado. Se o número de ordens cair abaixo do mínimo, por exemplo, devido às execuções, toda a grade será recriada. Ordens antigas são removidas quando ultrapassam o tempo limite. Isso evita o acúmulo de ordens em níveis que já não representam a distribuição atual.


Otimização dos parâmetros para o instrumento

WindowBars define o horizonte temporal da estratégia. Uma janela curta, de 5 a 10 barras, proporciona adaptação rápida, mas aumenta a sensibilidade ao ruído. Uma janela maior, de 50 a 100 barras, fornece estatísticas mais estáveis, porém reage mais lentamente às mudanças. Para o gráfico M5 do EURUSD, a faixa ideal é de 10 a 20 barras, ou 50 a 100 minutos. Para o gráfico H1, de 5 a 10 barras, ou 5 a 10 horas.

HistoryBars deve ser suficientemente grande para produzir estatísticas confiáveis. O mínimo é de algumas centenas de valores de retorno, enquanto o intervalo ideal fica entre 2000 e 5000 barras. Um histórico maior melhora a qualidade estatística, mas exige mais memória e tempo de cálculo. Por outro lado, um histórico excessivamente longo incorpora dados de outros regimes de mercado, distorcendo a distribuição atual.

LotMultiplier controla a agressividade da progressão dos volumes. O valor 1.0 significa ausência de progressão, com todas as ordens utilizando o mesmo volume. O valor 2.0 produz uma progressão acentuada, fazendo com que as ordens mais distantes tenham volumes muito maiores que as centrais. O valor ideal depende da relação entre o lucro obtido nos diferentes níveis; em geral, fica entre 1.3 e 1.8 para manter um equilíbrio entre risco e retorno.

OrderExpirationSeconds define o tempo de vida de cada ordem. Esse período deve ser longo o bastante para permitir sua execução, mas não deve ser indefinido. Um tempo muito curto provoca recriações constantes da grade sem que as ordens tenham oportunidade de ser executadas. Um tempo excessivamente longo leva ao acúmulo de ordens em níveis já desatualizados. Para operações intraday, uma faixa razoável é de 1800 a 3600 segundos, ou 30 a 60 minutos.

Para pares mais voláteis, como GBPJPY, convém aumentar WindowBars para 15-20 e reduzir LotMultiplier para 1.3. Para pares mais estáveis, como EURCHF, WindowBars pode ser reduzido para 8-10 e LotMultiplier aumentado para 2.0. No caso das criptomoedas, os parâmetros são bastante diferentes devido à alta volatilidade: WindowBars entre 5 e 8, HistoryBars entre 1000 e 2000 e LotMultiplier entre 1.2 e 1.5.


Resultados práticos

No EURUSD M5, com WindowBars=10, HistoryBars=3000, LotSize=0.01 e LotMultiplier=1.5, a estratégia gera entre 300 e 500 operações por trimestre, com 55% a 65% de operações lucrativas, lucro médio de 0.50 a 1.00 USD por operação, drawdown máximo de 50 a 100 USD e lucro total de 150 a 300 USD para um lote-base de 0.01. O fator de lucro fica entre 1.3 e 1.8, e o índice de Sharpe entre 0.8 e 1.2.

A curva de equity apresenta crescimento gradual, intercalado por correções periódicas. Os drawdowns surgem durante tendências fortes, quando um dos lados da grade é executado rapidamente enquanto o outro acumula prejuízo flutuante. O drawdown tende a ser recuperado quando o mercado reverte ou entra em lateralização. O drawdown relativo máximo costuma ficar entre 15% e 25% do lucro acumulado.

A principal ameaça são movimentos extremos que ultrapassam os limites observados na distribuição histórica. Uma notícia como uma decisão de banco central pode deslocar o preço em 500 pontos em questão de minutos. Todas as ordens de um dos lados podem ser executadas, formando uma grande posição agregada com prejuízo elevado. As principais medidas de proteção são limitar o número máximo de posições, utilizar stop losses em níveis críticos, fechar todas as posições antes de notícias importantes e manter capital suficiente para suportar o drawdown.

Mudanças no regime de mercado também reduzem a eficiência da estratégia. Se um mercado lateral volátil der lugar a uma tendência forte, as ordens centrais deixam de ser executadas com a mesma frequência, enquanto as ordens periféricas passam a ser executadas com frequência excessiva e com prejuízo. A solução é reotimizar regularmente os parâmetros, utilizar a expiração da grade como mecanismo de adaptação, monitorar as mudanças nos parâmetros estatísticos e estar preparado para interromper o robô quando as condições se tornarem desfavoráveis.

Uma estratégia market-neutral baseada na distribuição dos retornos é uma abordagem quantitativa de trading. Em vez de prever a direção do mercado, ela explora regularidades estatísticas do comportamento do preço. Entre as principais vantagens estão a fundamentação matemática de cada decisão, a ausência do componente emocional, a adaptação às mudanças do mercado por meio da expiração da grade e a flexibilidade de configuração para diferentes instrumentos. A estratégia exige conhecimento de estatística e disposição para aceitar drawdowns como parte inevitável de seu funcionamento. Não é um Santo Graal, mas oferece uma vantagem matemática consistente que se manifesta no longo prazo.


Tratamento de casos-limite

Na prática, o trading apresenta muitas situações que não são contempladas pela teoria. O primeiro caso-limite é a falta de histórico na primeira execução. Se não houver dados disponíveis para HistoryBars períodos, a função CalculateReturnDistribution retornará false e o robô não será iniciado. A solução é prever o acúmulo gradual do histórico ou utilizar inicialmente um valor menor de HistoryBars.

O segundo caso ocorre quando todos os percentis ficam do mesmo lado de zero. Isso acontece durante tendências fortes. Se todos os dez percentis forem positivos, nenhuma ordem BUY será posicionada, mesmo no modo NEUTRAL. O código verifica as condições returnLevel <= 0.01 para BUY e returnLevel >= -0.01 para SELL. Em uma tendência extrema, podem ser posicionadas ordens de apenas um dos lados. Isso é normal, pois a estratégia se adapta ao mercado, mas é importante entender que, nessa situação, a neutralidade em relação ao mercado é temporariamente perdida.

O terceiro caso é a volatilidade extrema, que pode gerar níveis excessivamente distantes. Se P90 estiver 5% acima do preço atual, a ordem será posicionada muito longe e talvez nunca seja executada. A solução é adicionar um limite para o desvio máximo:

if(Direction == NEUTRAL || Direction == SELL) {
   for(int i = 0; i < 10; i++) {
      double returnLevel = returnStats.percentiles[i];
      
      if(returnLevel >= -0.01 && returnLevel <= 0.03) {  // Max. 3% deviation
         double orderPrice = NormalizeDouble(currentPrice * (1 + returnLevel), digits);
         double weight = CalculateProbabilityWeight(i);
         double currentLot = NormalizeDouble(LotSize * weight, 2);
         
         Trade.SellLimit(currentLot, orderPrice, _Symbol, 0, 0, ORDER_TIME_SPECIFIED,
                        TimeCurrent() + OrderExpirationSeconds, comment);
      }
   }
}

O quarto caso é a execução simultânea de várias ordens do mesmo lado. Durante um movimento brusco provocado por notícias, 5 a 7 ordens podem ser executadas praticamente ao mesmo tempo, formando uma posição agregada de 0.20 a 0.30 lote. Se esse volume exceder a margem disponível, parte das ordens não será executada, causando um desequilíbrio. A proteção consiste em controlar o volume agregado máximo das posições em cada direção.

double GetTotalVolume(ENUM_POSITION_TYPE posType) {
   double totalVol = 0;
   for(int i = PositionsTotal() - 1; i >= 0; i--) {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket)) {
         if(PositionGetString(POSITION_SYMBOL) == _Symbol && 
            PositionGetInteger(POSITION_MAGIC) == OrderMagic &&
            PositionGetInteger(POSITION_TYPE) == posType) {
            totalVol += PositionGetDouble(POSITION_VOLUME);
         }
      }
   }
   return totalVol;
}

Use essa função antes de posicionar novas ordens. Se o volume atual das posições BUY somado ao volume das novas ordens BUY ultrapassar o limite, não posicione ordens adicionais desse lado.

O quinto caso é quando uma ordem fica presa em estado de execução. Às vezes, a corretora não consegue executar uma ordem imediatamente, seja por falta de liquidez ou por problemas técnicos, e ela permanece no estado ORDER_STATE_STARTED. Essas ordens não são contabilizadas por CountOrdersAndPositions como pendentes, mas também ainda não constituem posições. A solução é adicionar uma verificação do estado da ordem.

void CountOrdersAndPositions(int &pending, int &positions) {
   pending = 0;
   positions = 0;

   for(int i = PositionsTotal() - 1; i >= 0; i--) {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket)) {
         if(PositionGetString(POSITION_SYMBOL) == _Symbol && 
            PositionGetInteger(POSITION_MAGIC) == OrderMagic)
            positions++;
      }
   }

   for(int i = OrdersTotal() - 1; i >= 0; i--) {
      ulong ticket = OrderGetTicket(i);
      if(OrderSelect(ticket)) {
         if(OrderGetString(ORDER_SYMBOL) == _Symbol && 
            OrderGetInteger(ORDER_MAGIC) == OrderMagic) {
            ENUM_ORDER_STATE state = (ENUM_ORDER_STATE)OrderGetInteger(ORDER_STATE);
            if(state == ORDER_STATE_PLACED || state == ORDER_STATE_PARTIAL)
               pending++;
         }
      }
   }
}

As propriedades estatísticas do mercado variam de acordo com a sessão de negociação. A sessão asiática costuma apresentar menor volatilidade do que as sessões europeia e americana. Isso pode ser levado em conta por meio de um ajuste dinâmico dos parâmetros:

double GetSessionMultiplier() {
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   
   // UTC hours
   if(dt.hour >= 0 && dt.hour < 7) {
      return 0.7;  // Asian session: reduce aggressiveness
   }
   else if(dt.hour >= 7 && dt.hour < 15) {
      return 1.0;  // European session: standard parameters
   }
   else if(dt.hour >= 15 && dt.hour < 21) {
      return 1.2;  // U.S. session: increase aggressiveness
   }
   else {
      return 0.8;  // Late evening: reduce it
   }
}

Assim, é possível operar de forma mais agressiva nos períodos de maior liquidez e adotar uma postura mais conservadora nos horários de menor atividade.

Recomendação final: mantenha um registro detalhado de todas as alterações de parâmetros e dos resultados obtidos. Anote a data, o parâmetro alterado, o valor anterior e o novo valor, o motivo da mudança e o resultado observado na semana seguinte. Depois de alguns meses, esse registro se tornará uma fonte valiosíssima para entender o que realmente funciona para o seu instrumento e para o seu estilo de trading. Uma estratégia quantitativa exige uma abordagem igualmente quantitativa à otimização.

Vamos analisar o backtest do sistema:

O índice de Sharpe é superior a 3, o que significa que a estratégia tem futuro.


Conclusão

A estratégia market-neutral baseada na distribuição dos retornos representa um caso relativamente raro: uma abordagem matematicamente fundamentada que realmente funciona na prática. Diferentemente da maioria das estratégias de trading baseadas em análise técnica ou em tentativas de prever o futuro, esse método explora uma propriedade fundamental dos mercados: sua natureza estatística.

A principal vantagem da estratégia está em sua honestidade. Ela não promete ganhos extraordinários nem afirma saber para onde o preço irá. Em vez disso, reconhece a incerteza e trabalha com probabilidades. O robô não tenta estar certo; ele busca ser estatisticamente vantajoso. Essa diferença é fundamental.

A implementação prática exige atenção aos detalhes. Um código que parece simples à primeira vista contém muitas nuances: cálculo correto dos percentis, ponderação dos volumes de acordo com a probabilidade, expiração da grade desatualizada e tratamento dos casos-limite. Cada um desses pontos é importante. Ignore um deles e a estratégia pode deixar de funcionar ou começar a perder dinheiro em determinadas condições de mercado.

Também é importante compreender suas limitações. A estratégia não é universal. Ela tende a funcionar melhor em mercados laterais e voláteis e pior durante tendências fortes. Exige liquidez suficiente e spreads razoáveis. Também é vulnerável a cisnes negros e a movimentos bruscos provocados por notícias. Conhecer essas limitações não torna a estratégia mais fraca; ao contrário, permite utilizá-la de maneira adequada.

A otimização dos parâmetros não é uma tarefa pontual. Os mercados mudam, a volatilidade aumenta e diminui, e as correlações se deslocam. O que funcionava três meses atrás pode não funcionar agora. Por isso, é necessário revisar regularmente os parâmetros WindowBars, HistoryBars e LotMultiplier. Mantenha um registro das alterações e dos resultados. Depois de seis meses, você terá uma base de conhecimento extremamente valiosa sobre o comportamento da estratégia no seu instrumento.

O aspecto psicológico costuma ser subestimado. Mesmo com um robô totalmente automatizado, haverá a tentação de intervir. Um drawdown equivalente a 20% do lucro pode gerar vontade de interromper o robô. Uma sequência de operações perdedoras pode fazer você duvidar da estratégia. Um período sem lucro pode causar desconforto. Isso é normal. O importante é lembrar que uma vantagem estatística se manifesta ao longo de centenas de operações, e não de dezenas. Se você não consegue acompanhar os drawdowns com paciência, essa estratégia provavelmente não é adequada para você.

Capital e gestão de risco são fatores críticos. A estratégia pode abrir simultaneamente entre 10 e 15 posições em diferentes combinações. Certifique-se de que o seu depósito seja suficiente para suportar essa exposição. A regra é simples: se você opera com lote-base de 0.01, admite no máximo 10 posições e utiliza pesos de até 5x, é recomendável ter um depósito de pelo menos 500 a 1000 dólares no EURUSD. Com menos do que isso, você corre o risco de sofrer uma chamada de margem em uma combinação desfavorável de circunstâncias.

O backtesting é obrigatório, mas não é suficiente. A estratégia apresentará determinados resultados no histórico e outros na negociação real. Slippage, atrasos na execução, requotes e variações do spread durante a divulgação de notícias não aparecem em um backtest idealizado. É indispensável passar por um período de forward testing em conta demo durante pelo menos dois ou três meses. Somente depois disso passe para uma conta real, operando com os menores lotes possíveis.

Documente tudo. Cada posicionamento da grade, cada expiração, cada alteração de parâmetros, cada drawdown e cada recuperação. Depois de um ano, esses dados permitirão identificar padrões que não são perceptíveis no momento em que ocorrem. Você verá que a estratégia funciona muito bem às terças e quintas-feiras, mas apresenta desempenho inferior às sextas. Que julho é sempre pior que março. Que, após um aumento brusco da volatilidade, segue-se um período de rentabilidade. Esse conhecimento é valiosíssimo.

Uma estratégia baseada na distribuição dos retornos é uma ferramenta, não magia. E uma ferramenta exige compreensão, configuração, monitoramento e adaptação. Nas mãos certas e com expectativas adequadas, ela oferece uma vantagem matemática consistente. Isso é tudo o que se precisa para obter rentabilidade de longo prazo nos mercados. Não é um Santo Graal nem um sistema de enriquecimento, mas simplesmente uma forma honesta de arbitragem estatística que funciona por explorar propriedades reais do comportamento do preço, em vez de tentar prever aquilo que é imprevisível. 

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/19797

Redes neurais em trading: Treinamento de modelos spiking profundos (SEW-ResNet) Redes neurais em trading: Treinamento de modelos spiking profundos (SEW-ResNet)
Convidamos você a conhecer o framework SEW-ResNet, que permite construir modelos spiking profundos sem problemas de degradação e com controle eficiente dos gradientes. Neste artigo, demonstramos como implementar um neurônio spiking básico e seus algoritmos com MQL5.
Redes neurais em trading: Dos transformers aos neurônios spiking (Conclusão) Redes neurais em trading: Dos transformers aos neurônios spiking (Conclusão)
As redes neurais já estão transformando a forma como analisamos os mercados, e novas arquiteturas abrem possibilidades ainda maiores. Neste artigo, concluímos nosso desenvolvimento com o framework SpikingBrain, que nos apresenta novas perspectivas.
Desenvolvendo uma Estratégia de Pullback: Validação Desenvolvendo uma Estratégia de Pullback: Validação
A etapa de validação de um pullback avança com inclinação das EMAs, limites mínimo e máximo de distância por ATR, janela flexível do gatilho ATR e Walk-Forward Efficiency. As baterias comparam componentes e medem continuidade fora da amostra, dependência exclusiva da gestão. O resultado destaca plataformas paramétricas e critérios práticos para selecionar candidatos e evitar sobreajuste.
Criando sistemas de trading com IA em MQL5 (Parte 3): Evoluindo para uma interface rolável voltada para um único chat Criando sistemas de trading com IA em MQL5 (Parte 3): Evoluindo para uma interface rolável voltada para um único chat
Neste artigo, aprimoramos o programa integrado ao ChatGPT em MQL5, adotando uma interface rolável voltada para um único chat e melhorando a exibição do histórico das conversas com marcas de tempo e rolagem dinâmica. O sistema utiliza parsing de JSON para gerenciar mensagens em conversas com múltiplos turnos e oferece modos configuráveis para a barra de rolagem, além de efeitos ao passar o mouse, melhorando a interação do usuário com o sistema.