English Русский Português
preview
Creación de un robot probabilístico y neutral respecto al mercado basado en la distribución de rentabilidades

Creación de un robot probabilístico y neutral respecto al mercado basado en la distribución de rentabilidades

MetaTrader 5 — Probador |
11 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

La mayoría de los traders intentan predecir la dirección del mercado, y la mayoría sale perdiendo. Existe otro enfoque: en lugar de intentar adivinar la dirección, se aprovechan las propiedades estadísticas del propio mercado. Una estrategia neutral respecto al mercado basada en la distribución de rentabilidades funciona precisamente así: coloca órdenes en los puntos en los que es estadísticamente probable que se sitúe el precio, independientemente de la tendencia.


Fundamentos: distribución de rentabilidades

La rentabilidad supone la variación porcentual del precio durante un periodo fijo. Si el precio era de 1,0850 y pasó a ser de 1,0900, la rentabilidad es igual a (1,0900 - 1,0850) / 1,0850 = 0,0046 o el 0,46 %. Si tomamos todas las rentabilidades históricas de los periodos anteriores y elaboramos un histograma, obtenemos una distribución. En el caso de los pares de divisas, esta distribución suele ser cercana a la normal, con un centro próximo a cero y colas gruesas.

Por ejemplo, este es un histograma de la distribución de las rentabilidades en el gráfico horario del EUR/USD durante 11 años, desde enero de 2014:

Y aquí tenemos la distribución acumulativa, en la que ya podemos observar las famosas «colas gruesas» de la distribución —como una de las causas de la asimetría entre el beneficio y el riesgo, así como una de las razones por las que resulta tan complicado predecir el mercado en su conjunto—: los movimientos extremos se producen con mucha más frecuencia de lo que predice la distribución normal. Si las rentabilidades siguieran la clásica curva de campana de Gauss, la probabilidad de que se produjera una variación superior al 1 % en 10 horas resultaría insignificante, pero los datos reales muestran que este tipo de acontecimientos se producen con regularidad.

Precisamente por eso, las estrategias basadas en la hipótesis de la normalidad subestiman sistemáticamente el riesgo de pérdidas catastróficas y sobreestiman la frecuencia de los pequeños movimientos rentables, lo que convierte al arbitraje estadístico en un enfoque más complejo, pero también más honesto: trabajamos con una distribución empírica real, y no con su modelo matemático idealizado.

Idea clave: si conocemos la distribución del 90 % de las rentabilidades observadas en una ventana temporal determinada, podemos predecir no la dirección, sino la probabilidad de que se alcancen niveles de precios concretos. Se trata de una filosofía de negociación totalmente distinta.

En lugar de preguntarnos «¿Hacia dónde se moverá el precio?», nos preguntamos «¿Con qué probabilidad alcanzará el precio un nivel determinado en N periodos?». La diferencia resulta esencial: la primera pregunta requiere conocer el futuro, mientras que la segunda se basa en estadísticas del pasado. Si la distribución empírica muestra que, en el 10 % de los casos, el precio cae un 0,5 % o más en 10 horas, podemos colocar una orden BUY LIMIT en ese nivel, sabiendo que la probabilidad de que se ejecute es de alrededor del 10 %. No sabemos cuándo ocurrirá exactamente, pero sabemos que ocurrirá con cierta frecuencia.

Esta lógica da un giro radical al enfoque tradicional de la gestión del capital. La estrategia clásica asigna grandes volúmenes a las señales «seguras» y pequeños volúmenes a las «dudosas». El enfoque estadístico hace justo lo contrario: asigna volúmenes pequeños a los niveles centrales de alta probabilidad (las órdenes en estos niveles se ejecutan con frecuencia, pero generan pocos beneficios) y volúmenes grandes a los niveles de las colas de baja probabilidad (las órdenes en estos niveles se ejecutan rara vez, pero generan grandes beneficios). Las matemáticas equilibran la frecuencia y la magnitud mediante una ponderación por la inversa de la probabilidad.

Es más, al trabajar con una distribución completa, en lugar de con previsiones puntuales, la estrategia se convierte de forma natural en una estrategia neutral respecto al mercado. Colocamos órdenes en todo el espectro de movimientos posibles, tanto al alza como a la baja, con volúmenes proporcionales a su rareza estadística. No se trata de una cobertura en el sentido clásico, sino del reconocimiento de la incertidumbre fundamental respecto a la dirección, al tiempo que se aprovecha la certeza estadística de la distribución.

Empezaremos por crear una estructura para almacenar las estadísticas:

struct ReturnStats {
   double mean;              // Rentabilidad media
   double stdDev;            // Desviación estándar
   double percentiles[10];   // P10, P20, ..., P100
};

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

La media muestra la tendencia general en la ventana (normalmente es cercana a cero para periodos cortos); la desviación estándar indica la volatilidad; los percentiles son los cuantiles de la distribución que la dividen en partes de igual probabilidad.


Cálculo de la distribución empírica

La función de cálculo carga los datos históricos y calcula las rentabilidades en una ventana móvil:

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 punto de la serie histórica, se toma el precio de cierre de la barra actual y el precio de cierre de la barra situada WindowBars barras atrás. La rentabilidad se calcula como una variación relativa. Si fijamos WindowBars = 10 en el gráfico M5, obtenemos la rentabilidad en 50 minutos. En el gráfico H1, se trata de la rentabilidad en 10 horas.

Una vez calculadas todas las rentabilidades, ordenamos el array para obtener los percentiles:

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;
}

Los percentiles se obtienen simplemente mediante la indexación del array ordenado. P10 es el elemento con un índice equivalente al 10 % de la longitud del array. P90 es el elemento con un índice equivalente al 90 % de la longitud del array. Obtenemos cuantiles exactos de la distribución empírica, sin hacer ningún supuesto sobre su forma. Esto es muy importante: no suponemos que la distribución sea normal, sino que utilizamos los datos tal como son.


Matemáticas de los coeficientes de ponderación

Si se colocan todas las órdenes con el mismo volumen, la estrategia quedará desequilibrada. Las órdenes situadas en el centro de la distribución (cerca de la rentabilidad cero) se ejecutan con frecuencia, pero generan pocos beneficios debido a la escasa distancia entre el nivel de la orden y el precio actual. Las órdenes en las colas de la distribución rara vez se ejecutan, pero ofrecen un gran potencial de ganancias.

La solución óptima: las órdenes con baja probabilidad de ejecutarse deben tener un mayor volumen, para que sus escasas ejecuciones compensen la larga espera. La probabilidad de alcanzar un nivel está relacionada con su percentil. Si el nivel corresponde a P10, significa que el 10 % de los movimientos históricos lo han alcanzado o lo han superado. La probabilidad de alcanzarlo es de aproximadamente el 90 % (100 % - 10 %). Para el P90, la probabilidad de alcanzar o superar ese valor es del 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);
}

La fórmula transforma la probabilidad en una ponderación de volumen mediante una relación inversa: cuanto menor sea la probabilidad, mayor será la ponderación de volumen. La raíz cuadrada suaviza la progresión para evitar volúmenes extremos en los niveles más alejados. El multiplicador adicional LotMultiplier elevado a una potencia permite ajustar el nivel de agresividad.

Con un lote base de 0,01 y LotMultiplier = 1,5, obtenemos: P10 (probabilidad del 90 %) da una ponderación de aproximadamente 1,2 y un volumen de 0,012 lotes, P50 (probabilidad del 50 %) da una ponderación de 2,0 y un volumen de 0,020 lotes, P90 (probabilidad del 10 %) da una ponderación de 5,0 y un volumen de 0,050 lotes. La progresión no es lineal; los volúmenes de los niveles más alejados son entre 4 y 5 veces mayores que los de los niveles centrales.


Colocación de la red de órdenes

La función PlaceAllGridOrders coloca órdenes limitadas en los niveles correspondientes a los percentiles:

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, se calcula el precio objetivo mediante la fórmula: precio_actual × (1 + rentabilidad_del_percentil). Si la rentabilidad es negativa (por ejemplo, -0,005 o -0,5 %), el precio objetivo resulta ser inferior al actual, por lo que se coloca una orden BUY LIMIT. Lógica: estadísticamente, existe una cierta probabilidad de que el precio baje en esa cantidad, por lo que compramos aprovechando esa caída. Si la rentabilidad es positiva, el precio objetivo es superior al actual, por lo que se coloca una orden SELL LIMIT. Vendemos ante una subida estadísticamente probable.

El parámetro «Direction» controla la orientación de la estrategia. NEUTRAL coloca órdenes en ambos sentidos; se trata de una estrategia clásica de neutralidad de mercado. BUY coloca únicamente órdenes de compra en los niveles correspondientes a rentabilidades negativas, apostando por un rebote tras una caída. SELL coloca únicamente órdenes de venta en los niveles correspondientes a rentabilidades positivas, apostando por un retroceso tras una subida.


Expiración de la red de órdenes

La distribución estadística cambia lentamente con el tiempo. La volatilidad sube y baja. La rentabilidad media se desplaza cuando se forma una tendencia. Es posible que la red de órdenes colocada hace una hora no se corresponda con la distribución actual. La solución consiste en recalcular periódicamente la distribución y actualizar la red de órdenes.

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;
}

Esta función compara la hora de colocación de la red de órdenes con el umbral de expiración. El umbral es el instante situado WindowBars barras atrás respecto al momento actual. Si la red de órdenes es anterior a ese umbral, se considera obsoleta. Al llegar la expiración, se eliminan todas las órdenes pendientes, se cierran opcionalmente las posiciones abiertas, se recalcula la distribución y se coloca una nueva red de órdenes. Esto mantiene la estrategia adaptable a los cambios del mercado.


Gestión de beneficios y posiciones

El robot cuenta con tres parámetros independientes de take profit: el objetivo global, el objetivo para las compras y el objetivo para las ventas. Esto permite una gestión flexible del riesgo.

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;
   }
}

La función recorre todas las posiciones abiertas, obtiene el beneficio flotante y el swap, y los agrupa por tipo de posición. En el ciclo principal OnTick se comprueba si se han alcanzado los objetivos:

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();
}

La comprobación se realiza en cada tick. Cuando se alcanza el objetivo global, se eliminan todas las órdenes pendientes y se cierran todas las posiciones. Cuando se alcanza el objetivo de uno de los lados, solo se cierran las posiciones de ese lado; el otro sigue negociando. Esto resulta útil en mercados con tendencia, en los que un lado acumula beneficios rápidamente, mientras que el otro permanece estancado.


Mantenimiento de la red de órdenes

La función ManageGrid se encarga de mantener la red de órdenes actualizada y elimina las órdenes caducadas:

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);
            }
         }
      }
   }
}

Cada 100 ticks se recalcula la distribución para adaptarse al mercado. Si el número de órdenes cae por debajo del mínimo debido a su ejecución, la red de órdenes se vuelve a crear por completo. Las órdenes antiguas se eliminan al cumplirse el tiempo límite. Esto evita que se acumulen órdenes en niveles obsoletos.


Optimización de los parámetros para el instrumento financiero

WindowBars define el horizonte temporal de la estrategia. Una ventana pequeña (5-10 barras) permite una adaptación rápida, pero implica una alta sensibilidad al ruido. Una ventana amplia (50-100 barras) ofrece estadísticas estables, pero una respuesta lenta. Para el gráfico M5 del EUR/USD, lo ideal son entre 10 y 20 barras (50-100 minutos). Para el gráfico H1, de 5 a 10 barras (de 5 a 10 horas).

El valor de HistoryBars debe ser suficiente para obtener estadísticas fiables. Como mínimo, varios cientos de valores de rentabilidad; lo ideal es entre 2.000 y 5.000 barras. Cuantas más barras se utilicen, mejores serán las estadísticas, pero se necesitará más memoria y tiempo de cálculo. Un historial demasiado extenso incluye datos de otro régimen de mercado, lo que distorsiona la distribución actual.

LotMultiplier controla la agresividad de la progresión de los volúmenes. El valor 1,0 indica que no hay progresión; todas las órdenes tienen el mismo volumen. El valor 2,0 da lugar a una progresión rápida; las órdenes más alejadas tienen un volumen mucho mayor que las centrales. El valor óptimo depende de la relación entre los beneficios en los distintos niveles: normalmente, entre 1,3 y 1,8 para lograr un equilibrio entre el riesgo y la rentabilidad.

OrderExpirationSeconds es el tiempo de vigencia de una orden concreta. Debe ser suficiente para permitir la ejecución de la orden, pero no ser infinito. Un tiempo demasiado corto obliga a volver a crear las órdenes constantemente sin que lleguen a ejecutarse. Si es demasiado largo, se acumulan órdenes en niveles obsoletos. Un rango razonable es de 1.800 a 3.600 segundos (30-60 minutos) para el trading intradía.

Para pares volátiles como el GBPJPY, aumente el valor de WindowBars a 15-20 y reduzca el de LotMultiplier a 1,3. Para pares de divisas poco volátiles, como el EURCHF, reduzca el valor de WindowBars a 8-10 y aumente el de LotMultiplier a 2,0. En el caso de las criptomonedas, los parámetros son radicalmente diferentes debido a su elevada volatilidad: WindowBars 5-8, HistoryBars 1000-2000, LotMultiplier 1,2-1,5.


Resultados prácticos

En el EURUSD M5, con los parámetros WindowBars=10, HistoryBars=3000, LotSize=0.01 y LotMultiplier=1.5, la estrategia genera entre 300 y 500 operaciones al trimestre, con un porcentaje de operaciones rentables del 55-65 %, un beneficio medio por operación de 0,50-1,00 USD, un drawdown máximo de 50-100 USD y un beneficio total de 150-300 USD por 0,01 lotes. Factor de beneficio: 1,3-1,8; coeficiente de Sharpe: 0,8-1,2.

El gráfico del patrimonio neto muestra un crecimiento gradual con retrocesos periódicos. Los drawdowns se producen durante tendencias fuertes, cuando las órdenes de un lado de la red de órdenes se ejecutan rápidamente y el otro lado acumula pérdidas no realizadas. El patrimonio neto se recupera cuando el mercado cambia de dirección o entra en una fase lateral. El drawdown relativo máximo suele ser del 15-25 % de los beneficios acumulados.

La principal amenaza son las fluctuaciones extremas que se salen de los límites de la distribución histórica. Una noticia como la decisión del banco central puede hacer que el precio varíe 500 puntos en cuestión de minutos. Todas las órdenes de un mismo lado se ejecutarán y se acumulará una posición enorme con pérdidas. Protección: limitación del número máximo de posiciones, órdenes stop-loss en niveles críticos, cierre de todas las posiciones antes de noticias importantes, capital suficiente para soportar el drawdown.

El cambio de régimen del mercado reduce la eficacia. Si un movimiento lateral volátil da paso a una tendencia fuerte, las órdenes centrales dejan de ejecutarse, mientras que las periféricas se ejecutan con demasiada frecuencia y generan pérdidas. Solución: reoptimización periódica de los parámetros, uso de la expiración de la red de órdenes para adaptarse, seguimiento de los cambios en los parámetros estadísticos y disposición a detener el robot en condiciones desfavorables.

Una estrategia neutral respecto al mercado, basada en la distribución de rentabilidades, es un enfoque cuantitativo de la negociación. En lugar de predecir la dirección, se usan las regularidades estadísticas del comportamiento de los precios. Ventajas clave: fundamentación matemática de cada acción, ausencia de componentes emocionales, adaptación a los cambios del mercado mediante la expiración de la red de órdenes y configuración flexible para diferentes instrumentos financieros. Esta estrategia requiere comprender la estadística y estar dispuesto a aceptar los drawdowns como parte inevitable del proceso. No es el Santo Grial, pero sí una ventaja matemática estable que da resultados a largo plazo.


Procesamiento de casos límite

El trading real está lleno de situaciones que no se tienen en cuenta en la teoría. Primer caso límite: falta de historial en la primera ejecución. Si en la cuenta no hay datos para HistoryBars periodos, la función CalculateReturnDistribution devolverá false y el robot no se ejecutará. Solución: prever una acumulación gradual del historial o utilizar un valor menor para HistoryBars en la fase inicial.

Segundo caso: todos los percentiles quedan del mismo lado de cero. Esto ocurre en un contexto de fuerte tendencia. Si los diez percentiles son positivos, no se colocará ninguna orden BUY, ni siquiera en modo NEUTRAL. El código comprueba las condiciones returnLevel <= 0.01 para BUY y returnLevel >= -0.01 para SELL. En una tendencia extrema, puede que solo se coloquen órdenes en uno de los dos lados. Esto es normal: la estrategia se adapta al mercado, pero hay que tener en cuenta que, en una situación así, se pierde temporalmente la neutralidad frente al mercado.

Tercer caso: la volatilidad extrema da lugar a niveles demasiado alejados. Si P90 se sitúa un 5 % por encima del precio actual, la orden se coloca muy lejos y puede que nunca se ejecute. Solución: añadir un límite a la desviación máxima:

if(Direction == NEUTRAL || Direction == SELL) {
   for(int i = 0; i < 10; i++) {
      double returnLevel = returnStats.percentiles[i];
      
      if(returnLevel >= -0.01 && returnLevel <= 0.03) {  // Desviación máxima del 3 %
         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);
      }
   }
}

Cuarto caso: ejecución simultánea de muchas órdenes del mismo lado. Ante un movimiento brusco provocado por las noticias, pueden ejecutarse simultáneamente entre 5 y 7 órdenes, lo que abriría una posición total de entre 0,20 y 0,30 lotes. Si esto supera el margen disponible, parte de las órdenes no se ejecutarán y se producirá un desequilibrio. Protección: control del volumen total máximo de posiciones en una misma dirección.

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;
}

Utilice esta función antes de colocar nuevas órdenes. Si el volumen actual de las posiciones BUY, sumado al volumen de las nuevas órdenes BUY, supera el límite, no coloque órdenes adicionales de ese lado.

Quinto caso: las órdenes se quedan bloqueadas en estado de ejecución. A veces, el bróker no puede ejecutar una orden de forma inmediata (por falta de liquidez o problemas técnicos), y la orden permanece en el estado ORDER_STATE_STARTED. Estas órdenes no se tienen en cuenta en CountOrdersAndPositions como pendientes, pero tampoco son posiciones. Solución: añadir una comprobación del estado de la orden.

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++;
         }
      }
   }
}

Las propiedades estadísticas del mercado varían en función de la sesión de negociación. La sesión asiática suele ser menos volátil que la europea o la estadounidense. Esto se puede tener en cuenta mediante un ajuste dinámico de los parámetros:

double GetSessionMultiplier() {
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   
   // Horas UTC
   if(dt.hour >= 0 && dt.hour < 7) {
      return 0.7;  // Sesión asiática: reducimos la agresividad
   }
   else if(dt.hour >= 7 && dt.hour < 15) {
      return 1.0;  // Sesión europea: parámetros estándar
   }
   else if(dt.hour >= 15 && dt.hour < 21) {
      return 1.2;  // Sesión estadounidense: aumentamos la agresividad
   }
   else {
      return 0.8;  // A última hora de la noche: reducimos la agresividad
   }
}

Esto permite operar de forma más agresiva en los periodos de alta liquidez y con mayor cautela en las horas de menor actividad.

Recomendación final: lleve un registro detallado de todos los cambios en los parámetros y de los resultados. Anote la fecha, el parámetro modificado, el valor anterior y el nuevo, el motivo del cambio y el resultado durante la semana siguiente. Al cabo de unos meses, este registro se convertirá en una fuente inestimable para comprender qué funciona específicamente para su instrumento financiero y su estilo de negociación. Una estrategia cuantitativa requiere un enfoque cuantitativo para la optimización.

Analicemos el backtest del sistema:

El coeficiente de Sharpe es superior a 3, lo que significa que la estrategia tiene futuro.


Conclusión

Una estrategia neutral respecto al mercado basada en la distribución de las rentabilidades constituye un caso excepcional: un enfoque matemáticamente fundamentado que realmente funciona en la práctica. A diferencia de la mayoría de las estrategias de negociación, basadas en el análisis técnico o en intentos de predecir el futuro, este método aprovecha una propiedad fundamental de los mercados: su naturaleza estadística.

La ventaja clave de la estrategia radica en su honestidad. No promete el oro y el moro ni afirma saber hacia dónde se moverá el precio. En cambio, reconoce la incertidumbre y trabaja con probabilidades. El robot no intenta tener razón, sino ser rentable en términos estadísticos. La diferencia es fundamental.

La puesta en práctica requiere prestar atención a los detalles. El código, que a primera vista parece sencillo, contiene numerosos matices: el cálculo correcto de los percentiles, la ponderación de los volúmenes en función de la probabilidad, la expiración de la red de órdenes obsoleta y el tratamiento de los casos límite. Cada uno de estos detalles es importante. Si pasa por alto uno de ellos, la estrategia dejará de funcionar o empezará a generar pérdidas en determinadas condiciones de mercado.

Es importante comprender las limitaciones. La estrategia no es universal. Funciona mejor en mercados laterales con volatilidad y peor en tendencias marcadas. Requiere una liquidez suficiente y unos spreads razonables. Es vulnerable a los «cisnes negros» y a los movimientos bruscos provocados por noticias. Conocer estas limitaciones no debilita la estrategia; al contrario, permite aplicarla correctamente.

La optimización de los parámetros no es una tarea que se realice una sola vez. Los mercados cambian, la volatilidad sube y baja, y las correlaciones varían. Lo que funcionaba hace tres meses puede que ahora ya no funcione. Es necesario revisar periódicamente los parámetros WindowBars, HistoryBars y LotMultiplier. Lleve un registro de los cambios y los resultados. Dentro de medio año, dispondrá de una base de conocimientos inestimable sobre el comportamiento de la estrategia en su instrumento financiero.

El aspecto psicológico está infravalorado. Incluso con un robot totalmente automático, sentirá la tentación de intervenir. Un drawdown del 20 % de los beneficios hace que surja el deseo de detener el robot. Una serie de operaciones con pérdidas hace que surjan dudas sobre la estrategia. Un periodo sin beneficios genera nerviosismo. Es normal. Es importante recordar que la ventaja estadística se manifiesta en cientos de operaciones, no en decenas. Si no es capaz de soportar con paciencia los drawdowns, esta estrategia no es para usted.

El capital y la gestión de riesgos son fundamentales. La estrategia puede abrir entre 10 y 15 posiciones a la vez en diferentes combinaciones. Asegúrese de que su depósito sea suficiente para ello. La regla es sencilla: si opera con 0,01 lotes y un máximo de 10 posiciones con ponderaciones de volumen de hasta 5x, necesita un depósito mínimo de entre 500 y 1000 dólares para operar en EURUSD. Con menos, corre el riesgo de recibir una llamada de margen si se dan circunstancias desfavorables.

El backtesting es imprescindible, pero no suficiente. La estrategia dará unos resultados con datos históricos y otros en la operativa real. El deslizamiento, los retrasos en la ejecución, las recotizaciones y las variaciones del spread durante las noticias: todo esto está ausente en una prueba retrospectiva ideal. Es imprescindible que realice un periodo de forward testing en una cuenta demo durante al menos dos o tres meses. Solo después de eso, pase a operar con dinero real con lotes mínimos.

Documéntelo todo. Cada colocación de la red de órdenes, cada expiración, cada cambio de parámetros, cada drawdown y cada recuperación. Dentro de un año, estos datos le permitirán comprender patrones que no son visibles en el momento. Verá que la estrategia funciona muy bien los martes y los jueves, pero peor los viernes. Que julio siempre es peor que marzo. Que tras un fuerte aumento de la volatilidad llega un periodo de rentabilidad. Estos conocimientos no tienen precio.

Una estrategia basada en la distribución de las rentabilidades es una herramienta, no magia. Esta herramienta requiere comprensión, configuración, seguimiento y adaptación. En las manos adecuadas y con las expectativas adecuadas, ofrece una ventaja matemática sostenible. Eso es todo lo que se necesita para obtener rentabilidad a largo plazo en los mercados. No es el Santo Grial, ni un sistema para hacerse rico, sino simplemente un arbitraje estadístico honesto que funciona porque se basa en las propiedades reales del precio, en lugar de intentar predecir lo impredecible.

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/19797

Utilizando redes neuronales en MetaTrader Utilizando redes neuronales en MetaTrader
En el artículo se muestra la aplicación de las redes neuronales en los programas de MQL, usando la biblioteca de libre difusión FANN. Usando como ejemplo una estrategia que utiliza el indicador MACD se ha construido un experto que usa el filtrado con red neuronal de las operaciones. Dicho filtrado ha mejorado las características del sistema comercial.
Características del Wizard MQL5 que debe conocer (Parte 82): Uso de patrones del TRIX y del WPR con aprendizaje por refuerzo mediante una red Q profunda (DQN) Características del Wizard MQL5 que debe conocer (Parte 82): Uso de patrones del TRIX y del WPR con aprendizaje por refuerzo mediante una red Q profunda (DQN)
En el último artículo, analizamos la combinación de Ichimoku y ADX en el framework del aprendizaje por inferencia. En este artículo retomamos el tema del aprendizaje por refuerzo cuando se utiliza junto con un par de indicadores que ya analizamos en la «Parte 68». El indicador TRIX y el rango porcentual de Williams. El algoritmo que usaremos en este análisis será la red Q profunda (DQN) con regresión por cuantiles. Como es habitual, presentamos este enfoque como una clase de señal personalizada diseñada para su implementación con el Asistente de MQL5.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Características del Wizard MQL5 que debe conocer (Parte 81): Uso de patrones de Ichimoku y ADX-Wilder con aprendizaje por inferencia mediante β-VAE Características del Wizard MQL5 que debe conocer (Parte 81): Uso de patrones de Ichimoku y ADX-Wilder con aprendizaje por inferencia mediante β-VAE
Este artículo da continuidad a la «Parte 80», en la que examinamos la combinación de Ichimoku y ADX en un framework de aprendizaje por refuerzo. Ahora centraremos nuestra atención en el aprendizaje por inferencia. Como ya hemos comentado, Ichimoku y ADX son complementarios; sin embargo, vamos a repasar las conclusiones del último artículo en relación con el uso de la cadena de procesamiento. Para nuestro aprendizaje por inferencia, utilizaremos el algoritmo beta de un autocodificador variacional (VAE). Además, seguiremos utilizando una implementación de una clase de señal personalizada diseñada para integrarse con el Asistente de MQL5.