Motor de decisión Multi-IA para MQL5 (Parte 2): Voto ponderado que aprende en cuál IA confiar, más gestión de riesgo
Introducción
En la Parte 1 construimos un motor de decisión multi-IA que le hace la misma pregunta a varios proveedores (OpenAI, Claude, Gemini, DeepSeek), interpreta sus respuestas en un formato común y las combina por votación, ponderada por la confianza que reporta cada modelo. Funcionaba, pero siempre me quedaron dando vueltas dos cosas.
Primero, el sistema de voto confía en todos los modelos por igual, independientemente de la confianza que cada uno se auto-asigna. Una IA puede estar muy segura y equivocada, y la confianza no es lo mismo que el acierto. Después de usarlo un tiempo tenía la sensación de que algunos proveedores eran simplemente más confiables que otros en este símbolo, pero el motor no tenía forma ni de saberlo ni de actuar en consecuencia. Trataba a un modelo que venía errando toda la semana igual que a uno que venía acertando, mientras los dos sonaran igual de seguros.
Segundo, el EA de ejemplo de la Parte 1 abría operaciones con lote fijo y sin stop loss ni take profit. Eso sirve para mostrar el motor, pero no es algo que realmente pondrías a operar.
En esta parte arreglo las dos cosas. Voy a hacer que el motor aprenda en cuál IA confiar, midiendo el acierto real de cada proveedor a lo largo del tiempo y ponderando su voto. Además, voy a agregar gestión de riesgo de verdad: stop loss y take profit derivados del ATR, con un ratio recompensa-riesgo impuesto en el código, y un tamaño de posición basado en la confianza de la decisión. Todo se apoya en la Parte 1. Reusamos proveedores, parseo y estructuras de votación; solo agregamos las piezas nuevas. Si no leíste la Parte 1, el resumen es que ya tenemos CollectVotes() devolviendo un arreglo de AIResponse (proveedor, señal, confianza, válido), y ese es el punto de partida aquí.
Este es un marco educativo, no un sistema rentable. La salida de la IA es orientación, no es consejo financiero. Pruébalo en una cuenta demo.
Por qué el voto estático no alcanza
El voto de la Parte 1 suma la confianza de cada modelo para COMPRA contra VENTA y elige el lado más alto. El detalle es que la confianza es auto-reportada. Un modelo que dice «VENTA, confianza 90» se trata como más autorizado que uno que dice «COMPRA, confianza 60», incluso si, históricamente, el segundo viene acertando mucho más seguido en este símbolo. Los modelos de lenguaje no son máquinas de probabilidad calibradas: un número de confianza alto es un tono, no una garantía.
Lo que en realidad queremos es ponderar a cada modelo por qué tan acertado ha sido, no por qué tan seguro suena. Para eso, el motor necesita memoria: tiene que registrar qué predijo cada IA, esperar a ver qué hizo el mercado, y llevar un puntaje acumulado de cuántas veces acertó cada una. Así, el voto siguiente se apoya en los modelos que se lo ganaron y resta peso a los que viven gritando «lobo».

Ese es todo el ciclo: cada IA vota, registramos cada predicción, la calificamos después de un horizonte, actualizamos el hit-rate de esa IA, y el voto siguiente se pondera por confianza × hit-rate. Nada de la conexión, el parseo o los proveedores de la Parte 1 cambia: estamos agregando una capa de retroalimentación alrededor del voto. Vamos a construirla pieza por pieza.
Los inputs nuevos
Antes del código, estos son los parámetros que sumamos sobre la Parte 1. Se dividen en tres grupos: el voto, el aprendizaje y el riesgo.
//--- voting / quorum input int InpQuorum = 2; // Minimum number of AIs that must answer input double InpMinScore = 100; // Minimum weighted score to act //--- learning input bool InpUseLearning = true; // Weight votes by each AI's past accuracy input int InpEvalSeconds = 3600; // Horizon to judge each prediction (seconds) input int InpEvalThresholdPts = 50; // Min move (points) for a prediction to count as correct input double InpHitRateAlpha = 0.15; // Learning rate of the hit-rate EMA (0-1) //--- risk management input int InpAtrPeriod = 14; // ATR period for SL/TP input double InpAtrSLMult = 1.5; // Stop-loss distance = ATR * this input double InpRewardRisk = 2.0; // Take-profit = SL distance * this (R:R enforced in code) input double InpBaseLot = 0.10; // Base lot (used when Risk% = 0) input double InpRiskPercent = 0.0; // Risk % of balance per trade (0 = use base lot)
Los dos que cambian el carácter del motor son InpUseLearning (activa o desactiva toda la capa de aprendizaje, ideal para comparar) e InpHitRateAlpha (qué tan rápido el motor cambia de opinión sobre un modelo). InpEvalSeconds e InpEvalThresholdPts definen qué cuenta como un acierto, y el grupo de riesgo define el stop, el ratio y el tamaño.
Registrar la predicción de cada IA
Primero necesitamos un lugar donde guardar una predicción que vamos a calificar después, más el puntaje por proveedor que alimenta. Agregamos una estructura pequeña y un poco de estado sobre la Parte 1.
//--- A pending prediction we will grade later (to learn each AI's accuracy) struct Prediction { ENUM_AI_PROVIDER provider; ENUM_AI_SIGNAL signal; double entryPrice; datetime dueTime; bool active; }; //--- state string g_keyOpenAI, g_keyClaude, g_keyGemini, g_keyDeepSeek; double g_hitRate[4]; // EMA hit rate per provider (index = provider) int g_evalCount[4]; // graded predictions per provider Prediction g_pending[]; // predictions waiting to be graded int g_atrHandle = INVALID_HANDLE;
El hit-rate está indexado por proveedor (0 = OpenAI, 1 = Claude, y así). Cada vez que el motor junta los votos, guardamos una predicción pendiente por cada voto direccional: el proveedor, su señal, el precio actual y un «momento de vencimiento» en el que la vamos a calificar.
//+------------------------------------------------------------------+ //| NEW - record one pending prediction per valid directional vote | //+------------------------------------------------------------------+ void RecordPredictions(const AIResponse &votes[]) { double price = SymbolInfoDouble(_Symbol, SYMBOL_BID); datetime due = TimeCurrent() + InpEvalSeconds; for(int i = 0; i < ArraySize(votes); i++) { if(!votes[i].valid || votes[i].signal == AI_SIGNAL_HOLD) continue; int n = ArraySize(g_pending); ArrayResize(g_pending, n + 1); g_pending[n].provider = votes[i].provider; g_pending[n].signal = votes[i].signal; g_pending[n].entryPrice = price; g_pending[n].dueTime = due; g_pending[n].active = true; } }
Fíjate que calificamos la decisión de cada proveedor, no solo el lado que ganó el voto. Ese es el punto: queremos saber qué tan bueno es cada modelo por sí solo, sin importar qué terminamos operando. Un modelo que discrepó en silencio con la mayoría y resultó tener razón merece que lo premiemos la próxima vez.
Calificar predicciones y actualizar el hit-rate
Una vez que pasa el horizonte, miramos qué hizo el precio y actualizamos el puntaje del proveedor. Una predicción cuenta como correcta si el precio se movió al menos InpEvalThresholdPts en la dirección predicha; el umbral evita que el ruido pequeño cuente como un «acierto», así un modelo solo suma cuando el mercado claramente fue para su lado.
//+------------------------------------------------------------------+ //| NEW - grade due predictions and update each AI's hit rate (EMA) | //+------------------------------------------------------------------+ void ResolvePredictions() { double price = SymbolInfoDouble(_Symbol, SYMBOL_BID); double thr = InpEvalThresholdPts * _Point; bool changed = false; for(int i = 0; i < ArraySize(g_pending); i++) { if(!g_pending[i].active || TimeCurrent() < g_pending[i].dueTime) continue; double move = price - g_pending[i].entryPrice; bool correct = (g_pending[i].signal == AI_SIGNAL_BUY) ? (move >= thr) : (move <= -thr); double outcome = correct ? 1.0 : 0.0; int idx = (int)g_pending[i].provider; g_hitRate[idx] = (1.0 - InpHitRateAlpha) * g_hitRate[idx] + InpHitRateAlpha * outcome; g_evalCount[idx]++; g_pending[i].active = false; changed = true; } if(changed) CompactPending(); }
La línea clave es la actualización del hit-rate, una media móvil exponencial (EMA):
hitRate = (1 - alpha) * hitRate + alpha * outcome La evidencia nueva (outcome, 0 o 1) empuja el puntaje, y alpha controla qué tan rápido. Un alpha más alto se adapta más rápido pero es más ruidoso; uno más bajo es más estable pero lento para reaccionar. Encuentro que 0.10-0.20 es un rango sensato. Uso una EMA en vez de un porcentaje simple porque los mercados cambian. Un modelo excelente en un mes tendencial puede degradarse en uno lateral; la EMA permite dar más peso a la evidencia reciente. Un ayudante pequeño, CompactPending(), descarta las predicciones ya calificadas para que el arreglo no crezca indefinidamente; simplemente reconstruye la lista quedándose con las que siguen activas.
Ponderar el voto por el acierto aprendido
Ahora el premio. Cada proveedor recibe un peso a partir de su hit-rate, y el voto usa confianza × peso en vez de confianza sola.
//+------------------------------------------------------------------+ //| NEW - the weight of a provider, from its learned hit rate | //+------------------------------------------------------------------+ double ProviderWeight(ENUM_AI_PROVIDER p) { if(!InpUseLearning) return(1.0); //--- a 0.5 (neutral) hit rate -> weight 0.75; 1.0 -> 1.25; 0.0 -> 0.25 return(0.25 + g_hitRate[(int)p]); } //+------------------------------------------------------------------+ //| NEW - vote, weighting each AI by confidence AND learned accuracy | //+------------------------------------------------------------------+ AIResponse VoteDecisionWeighted(const AIResponse &votes[], int minQuorum, double minScore) { AIResponse decision; decision.signal = AI_SIGNAL_HOLD; decision.confidence = 0.0; decision.valid = false; double buyScore = 0.0, sellScore = 0.0; // weighted double buyRaw = 0.0, sellRaw = 0.0; // raw confidence (for sizing) int buyCount = 0, sellCount = 0, validCount = 0; for(int i = 0; i < ArraySize(votes); i++) { if(!votes[i].valid) continue; validCount++; double w = ProviderWeight(votes[i].provider); double s = votes[i].confidence * w; if(votes[i].signal == AI_SIGNAL_BUY) { buyScore += s; buyRaw += votes[i].confidence; buyCount++; } if(votes[i].signal == AI_SIGNAL_SELL) { sellScore += s; sellRaw += votes[i].confidence; sellCount++; } } if(validCount < minQuorum) return(decision); decision.valid = true; if(buyScore > sellScore && buyScore >= minScore) { decision.signal = AI_SIGNAL_BUY; decision.confidence = buyRaw / buyCount; } else if(sellScore > buyScore && sellScore >= minScore) { decision.signal = AI_SIGNAL_SELL; decision.confidence = sellRaw / sellCount; } return(decision); }
ProviderWeight mantiene un mínimo de 0.25 para que hasta un modelo malo siga contando un poco (de vez en cuando puede acertar), mientras que uno probado llega hasta 1.25. El voto tiene la misma forma que el de la Parte 1, pero ahora el puntaje es confianza × peso. Guardo la confianza cruda del lado ganador por separado (buyRaw/sellRaw), porque la voy a usar para dimensionar la posición: no el puntaje ponderado, que mezclaría el acierto dentro del lote y lo contaría dos veces.
Si InpUseLearning está desactivado, todos los pesos son 1.0 y obtienes exactamente el comportamiento de la Parte 1. Ese interruptor es más que una comodidad: te permite ejecutar el mismo motor con y sin aprendizaje (en corridas separadas) y ver de verdad si el aprendizaje ayuda, que es la forma honesta de juzgarlo.
Un ejemplo con números: viéndolo aprender
Los números lo hacen concreto. Todos arrancan neutros en 0.5, así que el día uno el motor vota igual que la Parte 1. Después de una semana de predicciones calificadas, el log podría leerse más o menos así:
hitrate OpenAI 0.70 Claude 0.40 Gemini 0.55 DeepSeek 0.62
Ahora supongamos que entra un ciclo donde Claude —que viene errando últimamente— grita VENTA con confianza 95, y OpenAI, que viene siendo confiable, dice COMPRA con confianza 70, mientras los otros se abstienen. El voto estático de la Parte 1 elegiría VENTA, porque 95 le gana a 70. Con aprendizaje, la VENTA fuerte de Claude se escala a 95 × (0.25 + 0.40) = 95 × 0.65 = 62, mientras que la COMPRA de OpenAI se vuelve 70 × (0.25 + 0.70) = 70 × 0.95 = 66. El motor ahora se inclina por COMPRA.
Ese único cambio es toda la idea: el modelo más ruidoso ya no gana por defecto; gana el que viene acertando. Y se autocorrige: si OpenAI empieza a fallar, su peso baja solo y el motor deja de escucharlo, sin que toques nada.
Gestión de riesgo I: stop loss y take profit desde el ATR
La Parte 1 operaba sin stops. Hay además una trampa en la que caí al principio: pedirle a la IA el SL y el TP y confiar en ellos. El problema es que el ratio recompensa-riesgo efectivo se va corriendo de operación en operación y, en la práctica, promedia cerca de 1:1, lo que borra sin que te des cuenta la ventaja que un plan 1:2 se suponía que te daba. Así que calculo los niveles en el código a partir de la volatilidad (ATR) e impongo el ratio.

//+------------------------------------------------------------------+ //| NEW - ATR-based SL/TP with the reward:risk ratio enforced | //+------------------------------------------------------------------+ bool ComputeSLTP(ENUM_AI_SIGNAL sig, double price, double &sl, double &tp) { double atr[]; if(CopyBuffer(g_atrHandle, 0, 1, 1, atr) < 1 || atr[0] <= 0.0) return(false); double slDist = atr[0] * InpAtrSLMult; double tpDist = slDist * InpRewardRisk; // TP is derived from SL -> R:R can't collapse if(sig == AI_SIGNAL_BUY) { sl = price - slDist; tp = price + tpDist; } else { sl = price + slDist; tp = price - tpDist; } sl = NormalizeDouble(sl, _Digits); tp = NormalizeDouble(tp, _Digits); return(true); }
La distancia del stop es ATR × un multiplicador, así que respira con la volatilidad: stops más anchos en un mercado rápido, más ajustados en uno tranquilo. La distancia del take profit es la distancia del stop × el ratio recompensa-riesgo que pidas. Como el TP se deriva del SL, el ratio es exactamente el que fijaste: pides 1:2 y obtienes 1:2, siempre. No puede volverse 1:1 sin que lo notes. Leemos el ATR de la barra que ya cerró (shift 1) para que el valor no se repinte en medio de la barra.
Gestión de riesgo II: tamaño de posición según la confianza
Una decisión que apenas pasa el umbral y una fuerte no deberían arriesgar el mismo dinero. Así que el lote escala con la confianza de la decisión, sobre un dimensionamiento de riesgo correcto.
//+------------------------------------------------------------------+ //| NEW - normalize a lot to the symbol constraints | //+------------------------------------------------------------------+ double NormalizeLot(double lot) { double mn = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double mx = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); double st = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); if(st > 0) lot = MathRound(lot / st) * st; if(lot < mn) lot = mn; if(lot > mx) lot = mx; return(lot); } //+------------------------------------------------------------------+ //| NEW - position size: risk-based, then scaled by confidence | //+------------------------------------------------------------------+ double LotByConfidence(double confidence, double slDistance) { double lot = InpBaseLot; if(InpRiskPercent > 0.0 && slDistance > 0.0) { double tickVal = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); if(tickSize > 0.0 && tickVal > 0.0) { double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * InpRiskPercent / 100.0; double lossPerLot = (slDistance / tickSize) * tickVal; if(lossPerLot > 0.0) lot = riskMoney / lossPerLot; } } //--- scale: 0.5x at 0% confidence up to 1.0x at 100% double c = MathMin(MathMax(confidence, 0.0), 100.0); lot *= (0.5 + 0.5 * c / 100.0); return(NormalizeLot(lot)); }
Si configuras InpRiskPercent, el lote base se calcula para que tocar el stop pierda ese porcentaje del balance: dimensionamiento de riesgo correcto que se mantiene entre símbolos, porque pasa por el valor del tick y el tamaño del tick. Después lo escalamos por la confianza (cerca de 0.5× con confianza baja hasta 1.0× al máximo), y lo normalizamos al paso de volumen del símbolo. Deja InpRiskPercent en 0 para usar un lote base fijo en su lugar. La confianza aquí es el promedio crudo del lado ganador, así que una decisión unánime y segura pone más tamaño que una marginal.
Juntando todo (y recordando lo aprendido)
El timer ata todo: califica las predicciones viejas, les pregunta a las IA, registra las nuevas predicciones, toma la decisión ponderada y, si es accionable, opera con el SL/TP y el lote calculados.
//+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer() { //--- 1) grade past predictions so the engine keeps learning ResolvePredictions(); if(PositionSelect(_Symbol)) return; //--- 2) ask every AI and remember each one's call to grade later AIResponse votes[]; CollectVotes(_Symbol, votes); RecordPredictions(votes); //--- 3) weighted decision (confidence x learned accuracy) AIResponse decision = VoteDecisionWeighted(votes, InpQuorum, InpMinScore); if(!decision.valid || decision.signal == AI_SIGNAL_HOLD) return; //--- 4) risk management: enforced SL/TP and confidence-based lot double price = (decision.signal == AI_SIGNAL_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID); double sl, tp; if(!ComputeSLTP(decision.signal, price, sl, tp)) return; double lot = LotByConfidence(decision.confidence, MathAbs(price - sl)); if(decision.signal == AI_SIGNAL_BUY) trade.Buy(lot, _Symbol, 0, sl, tp, "MultiAI P2"); else trade.Sell(lot, _Symbol, 0, sl, tp, "MultiAI P2"); PrintFormat("Decision %s | conf %.1f | lot %.2f | hitrate O%.2f C%.2f G%.2f D%.2f", SignalToStr(decision.signal), decision.confidence, lot, g_hitRate[0], g_hitRate[1], g_hitRate[2], g_hitRate[3]); }
Fíjate en el orden: calificamos antes de votar, así el acierto más fresco ya está incorporado a los pesos de este ciclo. El Print del final es tu ventana al motor: muestra la decisión, la confianza, el lote y el hit-rate en vivo de los cuatro modelos, para que veas el aprendizaje suceder línea por línea.
Para que el motor no se olvide de todo en cada reinicio, persistimos los hit-rates en un archivo pequeño y los cargamos al arrancar.
//+------------------------------------------------------------------+ //| NEW - persist the learned hit rates between sessions | //+------------------------------------------------------------------+ void SaveStats() { int h = FileOpen("multiai_stats.txt", FILE_WRITE | FILE_TXT | FILE_ANSI); if(h == INVALID_HANDLE) return; for(int i = 0; i < 4; i++) FileWrite(h, StringFormat("%d:%.4f:%d", i, g_hitRate[i], g_evalCount[i])); FileClose(h); } //+------------------------------------------------------------------+ //| NEW - load the learned hit rates on start | //+------------------------------------------------------------------+ void LoadStats() { int h = FileOpen("multiai_stats.txt", FILE_READ | FILE_TXT | FILE_ANSI); if(h == INVALID_HANDLE) return; ushort sep = StringGetCharacter(":", 0); while(!FileIsEnding(h)) { string line = FileReadString(h); string parts[]; if(StringSplit(line, sep, parts) == 3) { int idx = (int)StringToInteger(parts[0]); if(idx >= 0 && idx < 4) { g_hitRate[idx] = StringToDouble(parts[1]); g_evalCount[idx] = (int)StringToInteger(parts[2]); } } } FileClose(h); }
Llamamos a SaveStats() en el deinit y a LoadStats() en el init (después de sembrar cada hit-rate en un 0.5 neutro). Las predicciones que quedaron en vuelo se pierden en un reinicio, lo cual está bien: lo que importa es el acierto acumulado, y eso sobrevive.
Cómo usarlo
- Completa keys.txt como en la Parte 1, y agrega las cuatro URLs de las API a la lista de permitidas de WebRequest (Tools > Options > Expert Advisors).
- Adjunta el EA a un gráfico; corre con un timer de 60 segundos.
- Al principio, todos los proveedores están en un 0.5 neutro, así que el motor vota igual que la Parte 1. A medida que las predicciones se califican, los pesos se separan y empieza a apoyarse en los modelos que de verdad vienen acertando.
- Mira el registro (log) de Asesores Expertos: cada decisión imprime la señal, la confianza, el lote y el hit-rate actual de cada proveedor, así lo ves aprender literalmente.
- Para juzgar si el aprendizaje ayuda, ejecútalo dos veces —una con InpUseLearning activado y otra desactivado— y compara.
Resultado esperado

Limitaciones
- El aprendizaje necesita tiempo y operaciones. Al principio los hit-rates son neutros y no informan nada; hacen falta muchas predicciones calificadas antes de que signifiquen algo, y en un símbolo lento eso puede tardar.
- La calificación es deliberadamente simple (un horizonte fijo más un umbral de movimiento). Una versión más rica podría calificar contra el resultado real de SL/TP de cada decisión, o usar varios horizontes.
- Los modelos todavía razonan solo sobre el contexto de precio del prompt de la Parte 1. Darles el régimen de mercado y las noticias es donde está la ventaja de verdad, y eso es justamente la Parte 3.
- Este es un marco educativo, no es consejo financiero. El punto es la arquitectura, no una promesa de ganancia.
Conclusión
Cerramos el ciclo "señal → memoria → evaluación → actualización → nuevo voto" y lo enlazamos con un módulo de ejecución con riesgo reproducible. En concreto: el motor guarda la predicción de cada proveedor, la juzga tras un horizonte fijo y un umbral de movimiento, actualiza su hit-rate con una EMA y, en el ciclo siguiente, pondera cada voto por confianza × hit-rate aprendido. Las órdenes se abren con SL/TP derivados del ATR (SL = ATR × InpAtrSLMult; TP = SL × InpRewardRisk), de modo que el ratio recompensa:riesgo es exactamente el indicado, y el tamaño se calcula desde InpRiskPercent (o InpBaseLot si Risk% = 0), escalado por la confianza cruda del lado ganador y normalizado a las restricciones del símbolo. Los hit-rates persisten entre sesiones en multiai_stats.txt, y el interruptor InpUseLearning permite comparar en corridas separadas el mismo motor con y sin aprendizaje.
Convertimos así el motor estático de la Parte 1 en uno que aprende en cuál IA confiar según el acierto real: se adapta con el tiempo, se corrige solo cuando un modelo se enfría, y es algo que de verdad puedes ejecutar en una demo en vez de solo demostrar. En la Parte 3 les voy a dar a los modelos algo genuinamente digno de razonar: el régimen de mercado y el calendario económico, que es donde realmente vive la ventaja de un modelo de lenguaje.
Aclaración: educativo y de diagnóstico. No es consejo financiero.
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
Introducción a MQL5 (Parte 21): Automatización de la detección de patrones armónicos
Particularidades del trabajo con números del tipo double en MQL4
De novato a experto: Implementación de estrategias de Fibonacci en el trading posterior al NFP
- 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