Motor de decision Multi-IA para MQL5 (Parte 5): backtest del motor en el simulador de estrategias y comparacion contra reglas simples.
Introducción
Esta serie construyó un motor en cuatro pasos. En la Parte 1 conectamos varios proveedores de IA a MetaTrader 5 e hicimos que votaran. En la Parte 2 el motor aprendió a dar más peso a la IA que más acierta, y le pusimos gestión de riesgo real. En la Parte 3 le dimos contexto, régimen de mercado y ventanas de noticias, para que razonara sobre la situación y no sobre el próximo tick. En la Parte 4 lo pusimos a rendir cuentas con un diario y un cuadro de mando.
Todo eso es ingeniería, y funciona. 100% Pero hay una pregunta que las cuatro partes esquivaron, y es la única que de verdad importa: ¿este motor mejora el resultado de la cuenta, o no? El cuadro de mando de la Parte 4 responde algo parecido, no lo mismo. Mide el porcentaje de aciertos en vivo, decisión por decisión. Hacen falta meses para reunir una muestra significativa. Un porcentaje de aciertos, además, no dice si ganas dinero: puedes acertar el 60% de las veces y perder, o acertar el 40% y ganar, según cuánto dejes correr los aciertos y cuánto te cueste cada error.
En esta última parte cambiamos la pregunta. En lugar de medir cuánto acierta cada IA, medimos cuánto rinde el motor completo frente a alternativas que no usan IA. Usamos el mismo período, las mismas barras, el mismo riesgo y los mismos costos del bróker. La respuesta, adelanto, no es la que uno querría, y por eso vale la pena publicarla: un resultado negativo medido con rigor le sirve más al lector que una demostración que aparenta un sistema.
Al terminar vas a llevarte tres cosas concretas. Primero, un método para poner a prueba un asesor experto que consulta una IA, a pesar de que el Simulador de estrategias no permite WebRequest. Segundo, los números del motor de esta serie frente a reglas simples, con separación entre muestra y fuera de muestra. Tercero, y quizá lo más útil, una prueba de diagnóstico de tres líneas que te dice si la IA que agregaste aporta información nueva o solo repite lo que tú mismo le pasaste en el prompt.
Aclaración: este es un artículo de investigación y diagnóstico. No es consejo financiero ni la presentación de un sistema rentable.
El problema: este motor no se puede probar en el Simulador de estrategias
Cualquiera que haya intentado probar un asesor que consulta una API se topa con la misma pared. El Simulador de estrategias no ejecuta WebRequest. Es una decisión razonable de la plataforma, porque un simulador que dependiera de respuestas de red no sería reproducible ni podría correr en paralelo en varios agentes, pero deja al motor de esta serie sin banco de pruebas. Si el asesor no puede preguntar, no decide, y si no decide, no hay nada que medir.
De ahí salen las dos salidas que suele tomar la gente, y ninguna sirve. La primera es dejar el asesor corriendo en demo y esperar. Tres meses después tienes cuarenta operaciones, que no alcanzan para distinguir una estrategia de la suerte, y encima no puedes comparar contra otra variante porque el mercado ya no es el mismo. La segunda es inventar las respuestas de la IA dentro del simulador con alguna regla propia, con lo cual ya no estás midiendo la IA sino tu regla.
La salida real es separar en el tiempo las dos cosas que el simulador junta. Preguntarle a la IA es una operación de red, lenta y con costo, pero se puede hacer una sola vez y guardar el resultado. Decidir y operar con esa respuesta es aritmética pura y se puede repetir mil veces sin costo. Si grabas las respuestas primero, el simulador ya no necesita red: lee un archivo.

El arnés tiene dos programas. El grabador es un asesor que corre en un gráfico normal. Recorre las barras H1 ya cerradas, reconstruye para cada una el prompt exacto que el motor enviaría en vivo, pregunta a los proveedores y guarda cada voto en un archivo CSV de la carpeta común. El reproductor es un asesor pensado para el Simulador de estrategias. Lee ese archivo, aplica el mismo voto, el mismo gating y las mismas reglas de riesgo de las partes anteriores, y opera. El grabador usa la red donde está permitida; el reproductor no la toca en ningún momento.
Este esquema tiene una ventaja que va más allá de nuestro caso: una vez que los votos están en un archivo, puedes probar diez variantes del motor sobre exactamente las mismas opiniones. Cambiar el quórum, el umbral, el peso por acierto o las reglas de salida deja de costar llamadas de API, y las comparaciones se vuelven limpias porque la única diferencia entre dos corridas es el parámetro que moviste.
El grabador: reconstruir el prompt de una barra pasada
La regla de oro del arnés es que el prompt grabado sea idéntico al que el motor manda en vivo. Si cambia una coma, estás midiendo otro sistema. Por eso el grabador no inventa nada: repite la construcción de la Parte 3, pero leyendo los indicadores con desplazamiento en vez de leerlos en la barra actual.
//+------------------------------------------------------------------+ //| The prompt of a past bar: identical to the one sent live (P3) | //+------------------------------------------------------------------+ string PromptAt(int shift) { string regime = RegimeAt(shift); if(regime == "") return(""); double entry = iOpen(_Symbol, PERIOD_H1, shift); double close1 = iClose(_Symbol, PERIOD_H1, shift + 1); double close2 = iClose(_Symbol, PERIOD_H1, shift + 2); string price = StringFormat("Symbol: %s. Price: %.5f. H1 closes: %.5f, %.5f.", _Symbol, entry, close1, close2); string news = "News: no scheduled high-impact window in the near term."; string instruction = "You are a trading analyst. Reason about the market REGIME and the NEWS context above, " "not about the next tick. If a high-impact event is active or imminent, prefer HOLD and set RISK=HIGH. " "Reply ONLY in this exact format, no extra text: " "SIGNAL=BUY|SELL|HOLD;CONFIDENCE=0-100;RISK=LOW|HIGH;REASON=up to 8 words"; return(StringFormat("%s %s %s %s", price, regime, news, instruction)); }
El detalle importante está en el desplazamiento. El precio de referencia es la apertura de la barra que estamos evaluando, y los cierres que se le pasan al modelo son los de las barras anteriores. Nada de lo que entra en el prompt ocurre después del momento que estamos simulando, que es la única forma de que la prueba no se contamine con información del futuro.
La descripción del régimen sigue la misma lógica, leyendo el ADX, la relación entre el ATR y su promedio, y la pendiente de la media móvil, siempre en la barra previa a la que se decide.
//+------------------------------------------------------------------+ //| Market regime of a past bar, as text for the prompt (Part 3) | //+------------------------------------------------------------------+ string RegimeAt(int shift) { double atr[], adx[], ma[]; if(CopyBuffer(g_atrHandle, 0, shift + 1, 50, atr) < 50) return(""); if(CopyBuffer(g_adxHandle, 0, shift + 1, 1, adx) < 1) return(""); if(CopyBuffer(g_maHandle, 0, shift + 1, 6, ma) < 6) return(""); double sum = 0.0; for(int i = 0; i < 50; i++) sum += atr[i]; double avg = sum / 50.0; double ratio = (avg > 0.0) ? atr[ArraySize(atr) - 1] / avg : 1.0; string vol = "normal"; if(ratio >= 1.5) vol = "high"; if(ratio <= 0.7) vol = "low"; double maNow = ma[ArraySize(ma) - 1]; double maPast = ma[0]; if(adx[0] >= InpAdxTrendLevel) { string dir = (maNow >= maPast) ? "up" : "down"; return(StringFormat("Regime: trending %s, %s volatility (ADX %.0f, ATR %.1fx its average).", dir, vol, adx[0], ratio)); } return(StringFormat("Regime: ranging, %s volatility (ADX %.0f, ATR %.1fx its average).", vol, adx[0], ratio)); }
Guarda esta función en la cabeza, porque en la sección de diagnóstico va a volver, y no de la manera que uno esperaría.
Grabar una barra es preguntar a los dos proveedores y anotar lo que contestaron. Las barras que caen dentro de una ventana de noticias se saltan, igual que hace el motor en vivo por el gating de la Parte 3, para que el conjunto grabado corresponda a los momentos en que el motor realmente habría preguntado.
//+------------------------------------------------------------------+ //| Record one bar: build the prompt, ask both providers, store it | //+------------------------------------------------------------------+ void RecordBar(int shift) { datetime when = iTime(_Symbol, PERIOD_H1, shift); if(NewsStateAt(when) >= 1) // the engine does not ask during news: neither do we return; string prompt = PromptAt(shift); if(prompt == "") return; int sig[REC_COUNT], conf[REC_COUNT], risk[REC_COUNT], ok[REC_COUNT]; string raw; ParseVote(QueryOpenAI(prompt, raw) ? ExtractContent(raw) : "", sig[REC_OPENAI], conf[REC_OPENAI], risk[REC_OPENAI], ok[REC_OPENAI]); ParseVote(QueryDeepSeek(prompt, raw) ? ExtractContent(raw) : "", sig[REC_DEEPSEEK], conf[REC_DEEPSEEK], risk[REC_DEEPSEEK], ok[REC_DEEPSEEK]); WriteCacheRow(when, iOpen(_Symbol, PERIOD_H1, shift), sig, conf, risk, ok); }
Una barra por cada evento del temporizador mantiene el terminal utilizable mientras el grabador trabaja. La pausa entre llamadas queda como un parámetro, algo que las APIs agradecen cuando uno les pide miles de respuestas seguidas.
//+------------------------------------------------------------------+ //| Timer: one bar per tick keeps the terminal responsive | //+------------------------------------------------------------------+ void OnTimer() { if(g_shift < 1) { EventKillTimer(); PrintFormat("Cache complete: %d rows written to %s.", g_written, InpCacheFile); Comment(StringFormat("Vote cache complete: %d rows.", g_written)); return; } RecordBar(g_shift); g_shift--; if(g_shift % 25 == 0) { PrintFormat("Recorded %d rows, %d bars to go.", g_written, g_shift); Comment(StringFormat("Recording votes: %d rows written, %d bars to go.", g_written, g_shift)); } }
El caché y una precaución que evita conclusiones falsas
Cada fila del caché lleva la hora de la barra, la apertura de esa barra, y por cada proveedor la señal, la confianza declarada, la marca de riesgo y si la respuesta fue válida. El archivo se escribe en la carpeta común, que es la que el Simulador de estrategias puede leer.
//+------------------------------------------------------------------+ //| Append one cache row: bar time, bar open and every vote | //+------------------------------------------------------------------+ void WriteCacheRow(datetime when, double entry, const int &sig[], const int &conf[], const int &risk[], const int &ok[]) { bool isNew = !FileIsExist(InpCacheFile, FILE_COMMON); int h = FileOpen(InpCacheFile, FILE_READ | FILE_WRITE | FILE_CSV | FILE_ANSI | FILE_COMMON, ','); if(h == INVALID_HANDLE) { Print("Cannot open the cache file for writing."); return; } FileSeek(h, 0, SEEK_END); if(isNew) FileWrite(h, "time", "entry", "openai_sig", "openai_conf", "openai_risk", "openai_valid", "deepseek_sig", "deepseek_conf", "deepseek_risk", "deepseek_valid"); FileWrite(h, TimeToString(when, TIME_DATE | TIME_MINUTES), DoubleToString(entry, 2), sig[REC_OPENAI], conf[REC_OPENAI], risk[REC_OPENAI], ok[REC_OPENAI], sig[REC_DEEPSEEK], conf[REC_DEEPSEEK], risk[REC_DEEPSEEK], ok[REC_DEEPSEEK]); FileClose(h); g_written++; }
La columna de la apertura merece una explicación, porque no está ahí para el voto sino para protegerte de una trampa silenciosa. Los brókeres no comparten la hora del servidor: la barra de las 10:00 de uno puede ser la de las 12:00 de otro, y los precios difieren un poco entre feeds. Si tomas un caché grabado en una cuenta y lo reproduces en otra, cada fila va a encontrar una barra con la misma etiqueta horaria, pero puede ser una barra distinta, y el resultado sería un backtest de aspecto perfecto que no significa nada.
Por eso el reproductor compara, en cada barra, la apertura que trae el caché contra la apertura real de esa barra en el terminal donde está corriendo, y lleva la cuenta de cuántas coinciden. Si la proporción baja del 90%, avisa en el registro y no hay que creerle a los números. En el estudio que resumo más adelante coincidieron 8.649 filas de 8.649, es decir el 100%, porque el caché se grabó en la misma cuenta donde después se simuló.
El reproductor: el mismo voto, dentro del simulador
El reproductor carga el archivo completo en memoria una sola vez, al arrancar. Leerlo barra por barra sería lento y no aportaría nada.
//+------------------------------------------------------------------+ //| Load the whole vote cache into memory (no network in the tester) | //+------------------------------------------------------------------+ bool LoadCache() { int h = FileOpen(InpCacheFile, FILE_READ | FILE_CSV | FILE_ANSI | FILE_COMMON, ','); if(h == INVALID_HANDLE) { PrintFormat("Cache %s not found in the shared folder. Run MultiAI_VoteRecorder first.", InpCacheFile); return(false); } for(int i = 0; i < 10; i++) // skip the header cells FileReadString(h); int cap = 1024; ArrayResize(g_time, cap); ArrayResize(g_entry, cap); ArrayResize(g_sig, cap); ArrayResize(g_conf, cap); ArrayResize(g_risk, cap); ArrayResize(g_ok, cap); while(!FileIsEnding(h)) { string when = FileReadString(h); if(StringLen(when) < 10) break; if(g_rows >= cap) { cap *= 2; ArrayResize(g_time, cap); ArrayResize(g_entry, cap); ArrayResize(g_sig, cap); ArrayResize(g_conf, cap); ArrayResize(g_risk, cap); ArrayResize(g_ok, cap); } g_time[g_rows] = StringToTime(when); g_entry[g_rows] = StringToDouble(FileReadString(h)); for(int p = 0; p < PROV_COUNT; p++) { g_sig[g_rows][p] = (int)StringToInteger(FileReadString(h)); g_conf[g_rows][p] = StringToDouble(FileReadString(h)); g_risk[g_rows][p] = (int)StringToInteger(FileReadString(h)); g_ok[g_rows][p] = (int)StringToInteger(FileReadString(h)); } g_rows++; } FileClose(h); PrintFormat("Cache loaded: %d rows, from %s to %s.", g_rows, TimeToString(g_time[0]), TimeToString(g_time[g_rows - 1])); return(g_rows > 0); }
Como las filas vienen ordenadas por tiempo, encontrar la que corresponde a una barra es una búsqueda binaria. Con miles de filas y una consulta por barra la diferencia contra recorrer el arreglo entero se nota en corridas largas.
//+------------------------------------------------------------------+ //| Binary search: the cache row of a bar time, or -1 if not cached | //+------------------------------------------------------------------+ int FindRow(datetime when) { int lo = 0, hi = g_rows - 1; while(lo <= hi) { int mid = (lo + hi) / 2; if(g_time[mid] == when) return(mid); if(g_time[mid] < when) lo = mid + 1; else hi = mid - 1; } return(-1); }
El voto es el de la serie, con las opiniones leídas del caché en lugar de la red: se suman las confianzas ponderadas por el peso aprendido de cada proveedor, se exige un quórum mínimo de respuestas válidas, se corta si la mitad o más marcaron riesgo alto, y se exige superar un puntaje mínimo para actuar.
//+------------------------------------------------------------------+ //| The combined vote of the cached opinions (Parts 1-3 rules) | //+------------------------------------------------------------------+ int VoteAt(int row, bool useLearning) { double buy = 0.0, sell = 0.0; int valid = 0, high = 0; for(int p = 0; p < PROV_COUNT; p++) { if(g_ok[row][p] == 0) continue; valid++; if(g_risk[row][p] == 1) high++; double score = g_conf[row][p] * ProviderWeight(p, useLearning); if(g_sig[row][p] > 0) buy += score; if(g_sig[row][p] < 0) sell += score; } if(valid < InpQuorum) return(0); if(InpBlockOnAiRisk && high * 2 >= valid) return(0); if(buy > sell && buy >= InpMinScore) return(1); if(sell > buy && sell >= InpMinScore) return(-1); return(0); }
El aprendizaje de la Parte 2 también se replica, y aquí hay un punto de rigor que conviene subrayar: la tasa de acierto de cada proveedor se actualiza después de decidir, nunca antes. Si se actualizara antes, el motor estaría usando el resultado de la barra para decidir en esa misma barra, que es la forma más común de fabricar un backtest brillante e imposible.
//+------------------------------------------------------------------+ //| Grade the cached votes of a bar and update the hit rates (P2) | //+------------------------------------------------------------------+ void LearnFrom(int row, int shift) { double move = iClose(_Symbol, PERIOD_H1, shift) - iOpen(_Symbol, PERIOD_H1, shift); for(int p = 0; p < PROV_COUNT; p++) { if(g_ok[row][p] == 0 || g_sig[row][p] == 0) continue; double outcome = 0.0; if(g_sig[row][p] > 0 && move >= InpLearnThreshold) outcome = 1.0; if(g_sig[row][p] < 0 && move <= -InpLearnThreshold) outcome = 1.0; g_hitRate[p] = (1.0 - InpHitRateAlpha) * g_hitRate[p] + InpHitRateAlpha * outcome; } }
Las reglas del experimento
Para que la comparación signifique algo, todas las estrategias tienen que jugar el mismo partido. Un parámetro selecciona cuál se prueba, y todas las demás condiciones quedan fijas: mismas barras, misma distancia de stop calculada con ATR, misma relación entre objetivo y riesgo, mismo lote, una posición a la vez, y los costos reales del bróker aplicados por el simulador.
//+------------------------------------------------------------------+ //| The direction each strategy wants on this bar: +1, -1 or 0 | //+------------------------------------------------------------------+ int Direction(int row) { switch(InpMode) { case MODE_MULTI_LEARNED: return(VoteAt(row, true)); case MODE_MULTI_EQUAL: return(VoteAt(row, false)); case MODE_SINGLE_OPENAI: return(g_ok[row][P_OPENAI] == 1 ? g_sig[row][P_OPENAI] : 0); case MODE_SINGLE_DEEPSEEK: return(g_ok[row][P_DEEPSEEK] == 1 ? g_sig[row][P_DEEPSEEK] : 0); case MODE_MA_CROSS: return(BufferAt(g_fastHandle, 1) > BufferAt(g_slowHandle, 1) ? 1 : -1); case MODE_RSI: { double r = BufferAt(g_rsiHandle, 1); if(r < 30.0) return(1); if(r > 70.0) return(-1); return(0); } case MODE_RANDOM: if(VoteAt(row, true) == 0) // same bars the engine would have traded return(0); return((MathRand() % 2 == 0) ? 1 : -1); case MODE_ALWAYS_BUY: return(1); case MODE_REGIME_RULE: return(RegimeRule()); } return(0); }
La apertura de posición es la de la Parte 2, con el stop derivado del ATR y el objetivo derivado del stop, de modo que la relación entre ganancia y pérdida esperada no se degrade sola.
//+------------------------------------------------------------------+ //| Open one position with the ATR stop and the enforced R:R (P2) | //+------------------------------------------------------------------+ void OpenTrade(int direction) { double atr = BufferAt(g_atrHandle, 1); if(atr <= 0.0) return; double slDist = atr * InpAtrSLMult; double tpDist = slDist * InpRewardRisk; double price = (direction > 0) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID); double sl = (direction > 0) ? price - slDist : price + slDist; double tp = (direction > 0) ? price + tpDist : price - tpDist; sl = NormalizeDouble(sl, _Digits); tp = NormalizeDouble(tp, _Digits); bool sent = (direction > 0) ? trade.Buy(InpLot, _Symbol, 0.0, sl, tp, "multiai-test") : trade.Sell(InpLot, _Symbol, 0.0, sl, tp, "multiai-test"); if(sent) g_entries++; }
Al terminar cada corrida, el asesor vuelca la lista de operaciones cerradas a un CSV. Con esa lista se puede calcular fuera del terminal cualquier métrica, y sobre todo se puede partir el período en dos y mirar cada mitad por separado, que es lo que distingue una comparación seria de un gráfico bonito.
//+------------------------------------------------------------------+ //| Dump every closed trade so the split can be done outside (P4) | //+------------------------------------------------------------------+ void DumpTrades() { if(!HistorySelect(0, TimeCurrent())) return; string file = StringFormat("multiai_trades_%d.csv", (int)InpMode); int h = FileOpen(file, FILE_WRITE | FILE_CSV | FILE_ANSI | FILE_COMMON, ','); if(h == INVALID_HANDLE) { Print("Cannot write the trade list."); return; } FileWrite(h, "close_time", "direction", "profit"); int rows = 0; int total = HistoryDealsTotal(); for(int i = 0; i < total; i++) { ulong ticket = HistoryDealGetTicket(i); if(ticket == 0) continue; if(HistoryDealGetInteger(ticket, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue; // only closing deals carry the result double net = HistoryDealGetDouble(ticket, DEAL_PROFIT) + HistoryDealGetDouble(ticket, DEAL_SWAP) + HistoryDealGetDouble(ticket, DEAL_COMMISSION); //--- a closing deal is the opposite type of the position it closed int dir = (HistoryDealGetInteger(ticket, DEAL_TYPE) == DEAL_TYPE_SELL) ? 1 : -1; FileWrite(h, TimeToString((datetime)HistoryDealGetInteger(ticket, DEAL_TIME), TIME_DATE | TIME_MINUTES), dir, DoubleToString(net, 2)); rows++; } FileClose(h); PrintFormat("Trade list written: %s (%d closed trades).", file, rows); }
El período usado fue XAUUSD en H1, desde agosto de 2024 hasta febrero de 2026, con 8.649 puntos de decisión y dos proveedores por punto, es decir 17.298 respuestas reales de modelos. La primera mitad, hasta el 30 de junio de 2025, sirve como muestra; desde el 1 de julio de 2025 en adelante es el tramo fuera de muestra. El aprendizaje del motor viene arrastrado desde la primera mitad, tal como le pasaría a un asesor que llevara meses corriendo.
Una precisión sobre cómo se generó ese caché, porque hace a la reproducibilidad. Las 17.298 llamadas se lanzaron en paralelo desde un proceso auxiliar. Ese proceso construye exactamente el mismo prompt que ves en el código del grabador, con los mismos modelos, la misma temperatura y las mismas barras de esta cuenta. El grabador adjunto hace ese trabajo desde el terminal, una barra por cada evento del temporizador, que es la forma cómoda de armar tu propio caché sin salir de MetaTrader. La diferencia es de rendimiento, no de contenido: el archivo que produce tiene el mismo formato y las mismas columnas, y el reproductor no distingue uno de otro. Si prefieres no confiar en esa equivalencia, graba tu propio caché con el grabador y repite el experimento, que es justamente lo que el chequeo de alineación te permite verificar.
Los resultados
Las condiciones fueron las mismas para todas: depósito inicial de 10.000 dólares, lote fijo de 0,10, una posición por vez, y los costos de una cuenta demo de un bróker real, es decir el diferencial vigente en cada momento más el swap y la comisión que aplique el símbolo. El modelado fue por OHLC de un minuto, un punto que retomo en las limitaciones.
La tabla muestra el beneficio neto, la esperanza matemática por operación, el factor de beneficio y la máxima caída, calculados sobre las operaciones reales que devolvió el Simulador de estrategias.
| Estrategia | Operaciones fuera de muestra | Muestra, neto | Fuera de muestra, neto | Esperanza por operación | Factor de beneficio | Máxima caída |
|---|---|---|---|---|---|---|
| Comprar y mantener, referencia | 1 | - | +17.799 | - | - | - |
| Seguir solo a OpenAI | 217 | -1.600 | +11.163 | +51,4 | 1,29 | 5.383 |
| Comprar siempre | 299 | +6.780 | +10.106 | +33,8 | 1,19 | 10.722 |
| La regla de régimen, sin IA | 268 | -713 | +9.241 | +34,5 | 1,20 | 6.697 |
| El motor multi-IA de esta serie | 155 | -1.826 | +7.099 | +45,8 | 1,26 | 3.183 |
| Seguir solo a DeepSeek | 210 | -1.600 | +5.764 | +27,4 | 1,15 | 5.383 |
| Cruce de medias móviles | 319 | +1.022 | +4.626 | +14,5 | 1,08 | 7.965 |
| Voto multi-IA con pesos iguales | 208 | -1.607 | +3.274 | +15,7 | 1,09 | 5.858 |
| Dirección al azar | 176 | +723 | +2.664 | +15,1 | 1,08 | 4.913 |
| RSI 30/70 | 140 | -1.355 | -8.391 | -59,9 | 0,64 | 8.410 |

Conviene leerla despacio, porque dice varias cosas y ninguna es cómoda.
- El motor completo, con voto ponderado por acierto, gating por noticias y por riesgo, y todo el aparato de cuatro partes, queda cuarto entre las nueve estrategias. Le gana el modelo más simple imaginable, comprar siempre, y le gana también seguir a un solo proveedor sin votar nada.
- En el tramo de muestra, todas las variantes con IA perdieron dinero, mientras que comprar siempre ganaba. La rentabilidad fuera de muestra no viene de que el motor haya aprendido algo: viene de que el oro subió con fuerza en ese tramo, y cualquier cosa que estuviera comprada la mayor parte del tiempo terminó en positivo.
- La referencia de comprar y mantener, que no requiere ningún programa, supera a las nueve estrategias.
- El voto con pesos iguales rinde menos que seguir a cualquiera de los dos modelos por separado. Combinar opiniones, en este caso, empeoró el resultado en lugar de mejorarlo.
- La esperanza por operación del motor, 45,8 dólares, es la segunda más alta de la tabla. Eso podría parecer un punto a favor, pero se explica solo: el motor opera mucho menos, 155 veces contra 299 de comprar siempre, porque el quórum y el gating filtran. Gana más por operación y menos en total. Es un dato útil de todos modos, porque muestra por qué la esperanza por operación no debe leerse aislada del número de operaciones.
Hay un dato a favor del motor: su máxima caída fuera de muestra (3.183 dólares) es la más baja de la tabla. Es bastante menor que los 10.722 de comprar siempre. En la relación entre retorno y variabilidad medida con el índice de Sharpe queda en 1,04, apenas por debajo del 1,08 de comprar siempre y del 1,26 de seguir a OpenAI. Eso no lo convierte en rentable, pero indica que el gating y el quórum hacen algo real, filtran entradas y suavizan la curva. El motor no es ruido puro: es un filtro caro alrededor de una idea que no tiene ventaja.
La pregunta que importa: por qué no funciona
Podríamos habernos quedado ahí, con la tabla y una conclusión prudente. Pero un resultado negativo sin explicación es casi tan poco útil como una promesa sin datos. Si el motor no aporta, hay que entender dónde se pierde el aporte, y para eso hay dos mediciones que se pueden hacer sobre el caché que ya está grabado.
La primera es el grado de acuerdo entre los modelos. Todo el argumento de un ensamble descansa en que sus miembros se equivoquen en momentos distintos: si dos votantes dicen siempre lo mismo, el segundo no agrega información, solo duplica el costo. En los 8.649 puntos de decisión, OpenAI y DeepSeek dieron la misma señal en el 99,7% de los casos. Hubo 25 desacuerdos en total.
Vale aclarar que no se trata de un error de grabación, porque las confianzas declaradas sí difieren bastante entre ambos. Uno de los modelos solo usa tres valores de confianza, 60, 70 y 75; el otro reparte entre 30, 40, 50, 60 y 65. Son respuestas distintas de dos empresas distintas que, sin embargo, coinciden casi siempre en la dirección.
La segunda medición explica la primera, y es la que más me sorprendió. Se trata de cruzar la señal que devolvió cada modelo contra el régimen que nosotros mismos habíamos escrito en el prompt.

Cuando el prompt decía "trending up", el modelo respondió COMPRA en el 100% de los casos. Cuando decía "trending down", respondió VENTA en el 99%. Cuando decía "ranging", respondió ESPERAR en el 100%. Mirando solo esa frase, sin conocer nada más, se puede predecir la respuesta del modelo el 99,8% de las veces.
Esto merece decirse sin rodeos. La IA no estaba analizando el mercado: estaba devolviéndonos, en su propio formato, el indicador que nosotros le habíamos calculado en MQL5 y le habíamos servido en una frase. El acierto direccional a una hora fue del 51,1% y del 51,0%, que es lo que da una moneda al aire.
Y ahí se entiende toda la tabla de golpe. Los modelos coinciden porque ambos leen la misma pista y responden lo obvio. El voto no combina opiniones independientes porque no hay opiniones independientes que combinar. El peso por acierto aprendido no distingue a nadie porque todos aciertan lo mismo. Cinco votantes costarían cinco veces más para decir una sola cosa.
La prueba decisiva: la misma regla, sin ninguna IA
Si la hipótesis es correcta, entonces programar directamente la regla que describe el prompt debería dar un resultado parecido, sin llamadas, sin costo y sin latencia. Es una prueba fácil de hacer y difícil de discutir.
//+------------------------------------------------------------------+ //| The regime rule the prompt describes, computed here with no AI | //+------------------------------------------------------------------+ int RegimeRule() { if(BufferAt(g_adxHandle, 1) < InpAdxTrendLevel) return(0); // ranging: the prompt asks for HOLD return(BufferAt(g_regMaHandle, 1) >= BufferAt(g_regMaHandle, 6) ? 1 : -1); }
Son cuatro líneas. En el mismo período, con las mismas barras y las mismas reglas de riesgo, esa regla dio 9.241 dólares fuera de muestra, contra los 7.099 del motor multi-IA completo. No es que empate: le gana, y lo hace sin una sola llamada de API.
Ese es el veredicto, y es el aporte más honesto que esta serie puede dejar. Cinco partes de arquitectura, votación, aprendizaje y contexto terminaron rindiendo menos que las cuatro líneas de MQL5 que describían el contexto que le pasábamos a los modelos.
La prueba del eco, para tu propio sistema
De todo lo anterior sale una herramienta de diagnóstico simple, que vale para cualquier integración de IA en trading, no solo para esta. La llamo la prueba del eco, y consiste en preguntarse: ¿puedo predecir la respuesta del modelo mirando únicamente lo que yo le mandé?
El procedimiento es breve:
- Graba unos cientos de decisiones con su prompt y su respuesta, que es exactamente lo que hace el grabador de este artículo.
- Clasifica los prompts por la característica que domina tu descripción del contexto, sea el régimen, la tendencia o lo que uses.
- Cuenta, dentro de cada grupo, cuál fue la respuesta más frecuente y qué porcentaje representa.
- Si ese porcentaje es alto, digamos por encima del 90%, el modelo está repitiendo tu propio indicador y no está aportando información nueva.
Cuando el eco es alto, hay dos caminos, y los dos son mejores que seguir pagando llamadas. Uno es quedarse con la regla y borrar la IA, con lo que ganas velocidad, ahorras dinero y eliminas una dependencia externa. El otro es cambiar lo que le preguntas, para darle algo que tu código no sepa calcular.
Porque el problema, visto en perspectiva, no es que los modelos sean malos: es que les hicimos la pregunta equivocada. Le pedimos a un modelo de lenguaje que dedujera una dirección a partir de tres números que ya resumían la dirección. Un modelo de lenguaje no tiene ninguna ventaja ahí, y el resultado, 51%, lo confirma.
Donde un modelo de lenguaje sí podría aportar algo que el código no hace es en el terreno del texto: interpretar el comunicado de un banco central, resumir la carga de noticias de una jornada, clasificar el tono de un titular, decidir si dos eventos hablan de lo mismo. Eso no se calcula con un ADX. Si esta serie tuviera una sexta parte, empezaría por ahí, y arrancaría midiendo con este mismo arnés antes de escribir una línea de motor.
Cómo reproducir el experimento
- Compila los dos asesores y ponlos en la carpeta de asesores del terminal. Completa el archivo keys.txt de la carpeta MQL5\Files con tus claves, y autoriza las URLs de los proveedores en Herramientas, Opciones, Asesores expertos.
- Coloca el grabador en un gráfico H1 del símbolo que quieras estudiar, con el número de barras a grabar. Recuerda que graba una barra por cada evento del temporizador y que cada barra consume dos llamadas de API, así que conviene empezar con unos cientos de barras para dimensionar el costo antes de lanzarse a miles.
- Cuando el registro anuncie que el caché está completo, abre el Simulador de estrategias, elige el reproductor, el mismo símbolo y el mismo marco temporal, y el período que cubre tu caché.
- Corre una vez por cada valor del parámetro de estrategia. Al terminar cada corrida revisa el registro: ahí aparece cuántas filas del caché se encontraron y qué porcentaje coincidió en precio con tus barras. Si ese porcentaje no es alto, vuelve a grabar el caché en tu propia cuenta antes de sacar conclusiones.
- Junta los CSV de operaciones de todas las corridas, parte cada lista por la fecha que elijas como corte, y compara las mitades. Lo que importa no es cuál gana en la primera mitad, sino cuál sigue ganando en la segunda.
Limitaciones
- El estudio cubre un símbolo, un marco temporal y un período de año y medio, con un tramo alcista muy marcado en el oro. Otro activo o un mercado lateral podrían dar otro cuadro, aunque el diagnóstico del eco es independiente del resultado del mercado.
- Se usaron dos proveedores, no los cuatro de las partes anteriores. Hubo un tercero en la grabación, pero su plan gratuito limitó la cantidad de peticiones y quedó con muy pocas respuestas válidas, insuficientes para incluirlo. Prefiero decirlo antes que completar el hueco con estimaciones. Un tercer modelo podría haber aportado desacuerdo, aunque el nivel de coincidencia observado, un solo desacuerdo cada trescientas decisiones, hace poco probable que la conclusión cambie.
- El prompt es el de la Parte 3. Otro prompt, más abierto o con información que el código no puede calcular, daría otras respuestas. Justamente por eso la prueba del eco es lo que hay que correr primero, antes de dar por bueno cualquier prompt.
- La simulación abre una posición por vez y usa un lote fijo. Con gestión de posición distinta los importes cambiarían, pero el orden entre estrategias, que es lo que se está comparando, se sostiene porque todas juegan con la misma regla.
- El modelado fue por OHLC de un minuto, no por ticks reales. Como las entradas ocurren en la apertura de una barra horaria y las salidas son stop y objetivo fijos, el efecto sobre el orden de la tabla es menor, pero los importes exactos pueden variar algunas decenas de dólares si repites la prueba con ticks reales. Conviene rehacerla en ese modo antes de tomar cualquier decisión sobre dinero real.
- Los modelos evolucionan. Una versión posterior podría responder distinto, lo que refuerza la idea de tratar cualquier resultado como una medición fechada y no como una verdad permanente.
Conclusión
La serie termina con la respuesta que le faltaba, y no es la que esperaba encontrar cuando empecé a escribirla. El motor multi-IA de las Partes 1 a 4, medido fuera de muestra y con los costos del bróker, no le gana a comprar y mantener, no le gana a comprar siempre, no le gana a seguir a un solo modelo, y no le gana a la regla de cuatro líneas que describe el contexto que le pasábamos a los modelos. La razón está medida, no supuesta: los dos proveedores coinciden en el 99,7% de los casos y su respuesta se predice en el 99,8% a partir del régimen que nosotros mismos escribimos en el prompt.
Respondo la pregunta directa, que es la que corresponde después de todo esto: no pondría dinero real en este motor como sistema autónomo. Lo usaría, y de hecho lo pienso usar, como banco de pruebas, porque el arnés de grabar y reproducir sirve para cualquier idea con IA que uno quiera evaluar antes de arriesgar capital. Y volvería a considerar la parte de IA solo en dos condiciones, ambas verificables: que la prueba del eco dé baja, es decir que el modelo aporte algo que mi código no calcula, y que la ventaja se sostenga fuera de muestra frente a la regla simple equivalente.
Lo que queda en pie de las cinco partes no es el motor: es el método. La arquitectura agnóstica sigue siendo la forma correcta de conectar varios proveedores, el diario y el cuadro de mando siguen siendo la forma correcta de registrar lo que pasa, y el arnés de esta última parte es la pieza que faltaba para que todo eso pueda ser puesto a prueba de verdad. Un motor que no se puede medir es una opinión; con estas dos herramientas, cualquiera puede medir la suya, incluida la mía, y llegar a su propia conclusión con los números delante.
Aclaración final: artículo de investigación y diagnóstico. No es consejo financiero. Los resultados corresponden a un símbolo, un período y dos modelos concretos, y deben tomarse como una medición fechada.
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Utilizando redes neuronales en MetaTrader
Simulación de mercado: La unión hace la fuerza (III)
Particularidades del trabajo con números del tipo double en MQL4
Del básico al intermedio: Sobrecarga de operadores (IV)
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso