preview
Motor de decisión Multi-IA para MQL5 (Parte 1): Integrar múltiples IA con votación por consenso

Motor de decisión Multi-IA para MQL5 (Parte 1): Integrar múltiples IA con votación por consenso

MetaTrader 5Integración |
56 0
Martin Alejandro Bamonte
Martin Alejandro Bamonte

Introducción

Cuando empecé a trabajar con Expert Advisors, me topé con un límite. Un EA con reglas fijas hace siempre lo mismo: no puede leer el contexto del mercado ni razonar sobre muchos datos a la vez. Yo quería algo que sí pudiera hacerlo, analizar mucha información, con un contexto más complejo, y rápido. Por eso empecé a conectar el EA a modelos de inteligencia artificial.

La primera versión usaba una sola IA. Funcionaba, pero apareció un problema: un solo modelo a veces se contradice o se equivoca, y no hay nada que lo controle. Si esa única respuesta está mal, el EA opera mal.

De ahí salió la idea de este artículo. En vez de confiar en una sola IA, consulto a varias y tomo la decisión por votación. Cuando varios modelos coinciden, hay más certeza en la respuesta. No es seguro al 100%, ninguna IA lo es, pero baja el riesgo de depender de un solo modelo. Además cada modelo tiene lo suyo: algunos son más rápidos, otros más inteligentes, otros analizan distinto. Combinarlos aprovecha lo mejor de cada uno.

Eso sí, conectarse a varias IA no es tan simple como repetir la misma consulta. Cada proveedor tiene su propia API: su propia forma de recibir el pedido y de devolver la respuesta, con distinto formato y distinta manera de conectarse. Hay que adaptar la comunicación a cada uno y después unificar lo que devuelven para poder compararlos.

En esta primera parte vamos a construir, paso a paso, un motor de decisión multi-IA para MetaTrader 5: una capa que conecta el EA a varios proveedores de IA, les hace la misma consulta y combina sus respuestas en una sola decisión por votación, lista para usar en tus propios EAs.


Arquitectura: una capa entre el EA y las IA

La idea central es simple: el EA no le habla a ninguna IA en concreto. Le habla a una sola capa intermedia, que llamo el "AI Manager". El EA le pide una decisión, y el Manager se encarga de todo lo demás.

El Manager tiene configurados varios proveedores (OpenAI, Claude, Gemini, DeepSeek). Cuando el EA pide una decisión, el Manager le hace la misma consulta a cada uno, junta las respuestas y las pasa por el sistema de votación. Recién ahí devuelve una sola decisión final.

La clave está en que cada proveedor sabe hablar con su propia API, pero todos devuelven el resultado en el mismo formato. Así el EA nunca necesita saber con qué IA está hablando: siempre recibe lo mismo.

Arquitectura del motor multi-IA

Para que esto funcione, primero definimos las estructuras básicas que todos los proveedores van a compartir. Empezamos por la dirección de la señal y la lista de proveedores:

//--- Possible decisions returned by an AI
enum ENUM_AI_SIGNAL
  {
   AI_SIGNAL_BUY,
   AI_SIGNAL_SELL,
   AI_SIGNAL_HOLD
  };

//--- Supported providers
enum ENUM_AI_PROVIDER
  {
   AI_OPENAI,
   AI_CLAUDE,
   AI_GEMINI,
   AI_DEEPSEEK
  };

Después definimos una estructura única para la respuesta de cualquier IA. No importa qué proveedor conteste: todos devuelven un AIResponse con la misma forma.

//--- Standardized response
struct AIResponse
  {
   ENUM_AI_PROVIDER  provider;
   ENUM_AI_SIGNAL    signal;
   double            confidence;
   bool              valid;
  };

Esta estructura es la pieza que hace posible todo lo demás. Como las cuatro IA devuelven el mismo AIResponse, después las podemos comparar y votar sin importar de cuál vinieron.


Conectar a los proveedores vía WebRequest

MetaTrader 5 se comunica con el exterior usando la función WebRequest(). Con ella el EA puede mandarle una consulta a la API de una IA y recibir la respuesta. Hay un paso obligatorio antes: cada URL que vayamos a usar hay que permitirla en Herramientas > Opciones > Asesores Expertos > Permitir WebRequest. Si no, la llamada falla.

Acá aparece lo que mencioné en la introducción: cada proveedor tiene su propia API. No todos se conectan igual ni piden los datos en el mismo formato. Esta tabla resume las diferencias:

Proveedor Endpoint Autenticación Cuerpo (body)
OpenAI api.openai.com/v1/chat/completions Authorization: Bearer KEY messages[]
DeepSeek api.deepseek.com/v1/chat/completions Authorization: Bearer KEY messages[] (igual a OpenAI)
Claude api.anthropic.com/v1/messages x-api-key + anthropic-version messages[] + max_tokens
Gemini .../models/MODELO:generateContent?key=KEY la key va en la URL contents[]

Para no repetir código, primero hacemos una función genérica que manda un POST y devuelve la respuesta como texto:

//+------------------------------------------------------------------+
//| Generic POST request                                             |
//+------------------------------------------------------------------+
bool SendPost(const string url, const string headers, const string body, string &response)
  {
   char post[];
   char result[];
   string result_headers;

   StringToCharArray(body, post, 0, WHOLE_ARRAY, CP_UTF8);
   ArrayResize(post, ArraySize(post) - 1);   // remove the trailing zero

   ResetLastError();
   int status = WebRequest("POST", url, headers, 5000, post, result, result_headers);
   if(status == -1)
     {
      PrintFormat("WebRequest error %d. Allow the URL in Tools>Options.", GetLastError());
      return(false);
     }
   response = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8);
   return(status == 200);
  }

Después, cada proveedor tiene su propia función que arma la URL, los headers y el body según la tabla. Este es el caso de OpenAI:

//+------------------------------------------------------------------+
//| Query OpenAI                                                     |
//+------------------------------------------------------------------+
bool QueryOpenAI(const string apiKey, const string prompt, string &response)
  {
   string url     = "https://api.openai.com/v1/chat/completions";
   string headers = "Content-Type: application/json\r\nAuthorization: Bearer " + apiKey + "\r\n";
   string body    = "{\"model\":\"gpt-4o-mini-2024-07-18\",\"messages\":[{\"role\":\"user\",\"content\":\"" + JsonEscape(prompt) + "\"}],\"temperature\":0.2}";
   return(SendPost(url, headers, body, response));
  }

Los otros tres proveedores tienen su propia función (QueryClaude, QueryGemini, QueryDeepSeek) siguiendo la tabla de arriba. Todas hacen lo mismo desde afuera: reciben la key y el prompt, y devuelven la respuesta cruda como texto. Un detalle: si el prompt tiene comillas o saltos de línea, hay que escaparlos para que el JSON sea válido. Lo resolvemos al armar el prompt en la sección siguiente.

Ahora tenemos las respuestas crudas de cada IA, pero cada una viene en un formato distinto. El próximo paso es parsearlas y convertirlas todas en el mismo AIResponse.


Parsear las respuestas: convertir todo en el mismo formato

Ahora tenemos las respuestas crudas, pero hay dos problemas. El primero: cada proveedor envuelve la respuesta en su propio JSON. El segundo: la IA podría contestar en texto libre, y eso es imposible de comparar.

La solución al segundo problema es la más importante, y es simple: le pedimos a la IA, en el prompt, que conteste siempre en un formato fijo. Por ejemplo: SIGNAL=BUY;CONFIDENCE=80. Como todas contestan así, leer la decisión es igual para todas. Esta función extrae un valor de ese formato:

//+------------------------------------------------------------------+
//| Extract the value of a "KEY=value" tag                           |
//+------------------------------------------------------------------+
string ExtractTag(const string text, const string tag)
  {
   int start = StringFind(text, tag + "=");
   if(start < 0)
      return("");
   start += StringLen(tag) + 1;
   int end = StringFind(text, ";", start);
   if(end < 0)
      end = StringLen(text);
   return(StringSubstr(text, start, end - start));
  }

Con eso convertimos el texto de la IA en nuestro AIResponse estandarizado:

//+------------------------------------------------------------------+
//| Convert the AI text into an AIResponse                           |
//+------------------------------------------------------------------+
AIResponse ParseAIText(ENUM_AI_PROVIDER provider, const string aiText)
  {
   AIResponse r;
   r.provider   = provider;
   r.valid      = false;
   r.signal     = AI_SIGNAL_HOLD;
   r.confidence = 0.0;

   string sig  = ExtractTag(aiText, "SIGNAL");
   string conf = ExtractTag(aiText, "CONFIDENCE");
   if(sig == "")
      return(r);

   if(sig == "BUY")
      r.signal = AI_SIGNAL_BUY;
   else
      if(sig == "SELL")
         r.signal = AI_SIGNAL_SELL;
      else
         r.signal = AI_SIGNAL_HOLD;

   r.confidence = (double)StringToInteger(conf);
   r.valid      = true;
   return(r);
  }

Falta el primer problema: sacar el texto de la IA del JSON de cada proveedor. Como el envoltorio cambia según quién responda, usamos un extractor por proveedor:

//+------------------------------------------------------------------+
//| Extract the AI text depending on the provider                    |
//+------------------------------------------------------------------+
string ExtractContent(ENUM_AI_PROVIDER provider, const string raw)
  {
   string key = "\"content\"";
   if(provider == AI_CLAUDE || provider == AI_GEMINI)
      key = "\"text\"";
   int k = StringFind(raw, key);
   if(k < 0)
      return("");
   int colon = StringFind(raw, ":", k + StringLen(key));   // tolerates spaces
   if(colon < 0)
      return("");
   int q1 = StringFind(raw, "\"", colon);
   if(q1 < 0)
      return("");
   int q2 = StringFind(raw, "\"", q1 + 1);
   if(q2 < 0)
      return("");
   return(StringSubstr(raw, q1 + 1, q2 - q1 - 1));
  }

Un detalle que parece menor pero rompe todo: algunos proveedores devuelven el JSON "compacto" ("content":"...") y otros con un espacio después de los dos puntos ("content": "..."). Por eso no buscamos el texto literal "content":": buscamos la clave, después el dos puntos, y recién ahí la primera comilla. Así funciona con cualquiera de los dos formatos. Sigue siendo un extractor liviano, no un parser JSON completo; para algo más robusto (comillas escapadas, anidamiento) conviene usar un parser de verdad, como el que se muestra en el artículo Building AI-Powered Trading Systems in MQL5 (Part 1): JSON Handling for AI APIs.

El resultado: pase lo que pase, las cuatro IA terminan devolviendo el mismo AIResponse. Y eso es justo lo que necesitamos para poder votar.


El prompt estandarizado: misma pregunta para todas

Para que la votación tenga sentido, todas las IA tienen que opinar sobre lo mismo. Por eso usamos un único prompt, que cumple dos funciones: darle a cada modelo el mismo contexto de mercado, y obligarlo a responder en el formato fijo que vimos en la sección anterior.

Esta función arma el prompt con datos del mercado y agrega la instrucción de formato:

//+------------------------------------------------------------------+
//| Build the prompt                                                 |
//+------------------------------------------------------------------+
string BuildPrompt(const string symbol)
  {
   double bid    = SymbolInfoDouble(symbol, SYMBOL_BID);
   double close1 = iClose(symbol, PERIOD_H1, 1);
   double close2 = iClose(symbol, PERIOD_H1, 2);

   string context = StringFormat("Symbol: %s. Price: %.5f. H1 closes: %.5f, %.5f.",
                                 symbol, bid, close1, close2);
   string instruction = "You are a trading analyst. Reply ONLY in this exact format, without explanations: SIGNAL=BUY|SELL|HOLD;CONFIDENCE=0-100";
   return(context + " " + instruction);
  }

El contexto es simple a propósito (precio y dos cierres), para que se entienda la idea. En la práctica le podés sumar todo lo que quieras: RSI, medias móviles, ATR, la hora, etc. Cuanto mejor el contexto, mejor la respuesta. Lo importante es la instrucción final: le exige a la IA responder solo SIGNAL=...;CONFIDENCE=..., sin explicaciones.

Falta un detalle: el prompt tiene comillas y puede tener saltos de línea, así que hay que escaparlo antes de meterlo en el JSON. Esta función lo resuelve:

//+------------------------------------------------------------------+
//| Escape a text string for JSON                                    |
//+------------------------------------------------------------------+
string JsonEscape(string text)
  {
   StringReplace(text, "\\", "\\\\");
   StringReplace(text, "\"", "\\\"");
   StringReplace(text, "\n", "\\n");
   StringReplace(text, "\r", "");
   return(text);
  }

Ahora tenemos todo lo de "ida y vuelta": armamos el prompt, lo mandamos a cada IA, y convertimos cada respuesta en un AIResponse. Llegó el momento del corazón del sistema: la votación.


El motor de votación: de varias opiniones a una decisión

Ya tenemos un AIResponse por cada IA. Ahora hay que combinarlos en una sola decisión. No lo hacemos contando cabezas a secas: usamos votación ponderada por confianza. Una IA que está 90% segura pesa más que otra que está 55%.

La lógica tiene tres reglas: solo cuentan las respuestas válidas; si respondieron menos IA que el mínimo (quórum), no operamos; y gana el lado con más confianza acumulada, siempre que supere un umbral. Si hay empate o nadie llega al umbral, HOLD.

//+------------------------------------------------------------------+
//| Vote and return the final decision                               |
//+------------------------------------------------------------------+
AIResponse VoteDecision(const AIResponse &votes[], int minQuorum, double minConfidence)
  {
   AIResponse decision;
   decision.signal     = AI_SIGNAL_HOLD;
   decision.confidence = 0.0;
   decision.valid      = false;

   double buyScore = 0.0, sellScore = 0.0;
   int buyCount = 0, sellCount = 0, validCount = 0;

   for(int i = 0; i < ArraySize(votes); i++)
     {
      if(!votes[i].valid)
         continue;
      validCount++;
      if(votes[i].signal == AI_SIGNAL_BUY)
        {
         buyScore  += votes[i].confidence;
         buyCount++;
        }
      if(votes[i].signal == AI_SIGNAL_SELL)
        {
         sellScore += votes[i].confidence;
         sellCount++;
        }
     }

   if(validCount < minQuorum)
      return(decision);

   decision.valid = true;

   if(buyScore > sellScore && buyScore >= minConfidence)
     {
      decision.signal     = AI_SIGNAL_BUY;
      decision.confidence = buyScore / buyCount;
     }
   else
      if(sellScore > buyScore && sellScore >= minConfidence)
        {
         decision.signal     = AI_SIGNAL_SELL;
         decision.confidence = sellScore / sellCount;
        }
   return(decision);
  }

Veámoslo con un ejemplo. Supongamos que las cuatro IA responden esto, con minQuorum=2 y minConfidence=100:

IA Señal Confianza
OpenAI BUY 80
Claude BUY 70
Gemini SELL 60
DeepSeek HOLD

Acá buyScore=150 (dos votos), sellScore=60 (un voto). Hay quórum (respondieron 4). El lado comprador supera al vendedor y pasa el umbral de 100, así que la decisión final es BUY con 75% de confianza (150 / 2 votos). Si las IA estuvieran repartidas o inseguras y ningún lado llegara a 100, el sistema devolvería HOLD: ante la duda, no opera.

Ejemplo de votación ponderada



Fiabilidad: qué pasa cuando una IA falla

Las APIs fallan. A veces tardan demasiado, otras devuelven un error o una respuesta rota. Un sistema serio tiene que aguantar eso sin trabarse ni operar a ciegas. La arquitectura que armamos ya resuelve gran parte con tres mecanismos: (1) un timeout en la llamada (en SendPost ya pusimos 5000 ms para que una IA colgada no congele el EA); (2) fallas aisladas (si una IA falla, devolvemos un voto no válido y seguimos con las demás; la votación ignora esos votos); (3) el quórum como red (votamos con las que sí contestaron, y si son menos que el mínimo, HOLD).

Esta función junta los votos manejando las fallas:

//+------------------------------------------------------------------+
//| Invalid vote                                                     |
//+------------------------------------------------------------------+
AIResponse InvalidResponse(ENUM_AI_PROVIDER p)
  {
   AIResponse r;
   r.provider   = p;
   r.signal     = AI_SIGNAL_HOLD;
   r.confidence = 0.0;
   r.valid      = false;
   return(r);
  }
//+------------------------------------------------------------------+
//| Collect the votes from all the providers                         |
//+------------------------------------------------------------------+
int CollectVotes(const string symbol, AIResponse &votes[])
  {
   string prompt = BuildPrompt(symbol);
   string raw;
   int n = 0;
   ArrayResize(votes, 4);

   if(QueryOpenAI(g_keyOpenAI, prompt, raw))
      votes[n++] = ParseAIText(AI_OPENAI, ExtractContent(AI_OPENAI, raw));
   else
      votes[n++] = InvalidResponse(AI_OPENAI);

   if(QueryClaude(g_keyClaude, prompt, raw))
      votes[n++] = ParseAIText(AI_CLAUDE, ExtractContent(AI_CLAUDE, raw));
   else
      votes[n++] = InvalidResponse(AI_CLAUDE);

   if(QueryGemini(g_keyGemini, prompt, raw))
      votes[n++] = ParseAIText(AI_GEMINI, ExtractContent(AI_GEMINI, raw));
   else
      votes[n++] = InvalidResponse(AI_GEMINI);

   if(QueryDeepSeek(g_keyDeepSeek, prompt, raw))
      votes[n++] = ParseAIText(AI_DEEPSEEK, ExtractContent(AI_DEEPSEEK, raw));
   else
      votes[n++] = InvalidResponse(AI_DEEPSEEK);

   return(n);
  }

Un punto práctico importante: consultar cuatro APIs lleva tiempo (es red, no es instantáneo). No conviene hacerlo en cada OnTick, porque trabaría el gráfico. Lo correcto es hacerlo cada cierto intervalo con OnTimer. Así el EA sigue fluido mientras las IA piensan.


Integración en un EA de ejemplo

Con todas las piezas listas, armar el EA es corto. La idea: cada cierto tiempo consultamos a las IA, votamos, y si hay consenso claro, operamos. Si no, no hacemos nada. Usamos OnTimer (no OnTick) por lo que vimos en fiabilidad.

#include <Trade/Trade.mqh>
CTrade trade;

input int    InpQuorum        = 2;     // Minimum number of AIs that must answer
input double InpMinConfidence = 100;   // Minimum accumulated confidence
input double InpLots          = 0.10;  // Volume

//+------------------------------------------------------------------+
//| Convert the signal into readable text (BUY / SELL / HOLD)        |
//+------------------------------------------------------------------+
string SignalToStr(ENUM_AI_SIGNAL s)
  {
   if(s == AI_SIGNAL_BUY)
      return("BUY");
   if(s == AI_SIGNAL_SELL)
      return("SELL");
   return("HOLD");
  }
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   LoadKeys();
   EventSetTimer(60);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
   if(PositionSelect(_Symbol))
      return;

   AIResponse votes[];
   CollectVotes(_Symbol, votes);

   AIResponse decision = VoteDecision(votes, InpQuorum, InpMinConfidence);
   if(!decision.valid || decision.signal == AI_SIGNAL_HOLD)
      return;

   if(decision.signal == AI_SIGNAL_BUY)
      trade.Buy(InpLots, _Symbol);
   else
      if(decision.signal == AI_SIGNAL_SELL)
         trade.Sell(InpLots, _Symbol);

   PrintFormat("Decision: %s, confidence %.1f%%", SignalToStr(decision.signal), decision.confidence);
  }

El flujo completo queda así: OnTimer llama a CollectVotes (consulta a las cuatro IA), después a VoteDecision (vota), y si la decisión es válida y no es HOLD, abre la operación.

Este EA es a propósito mínimo. No tiene stop loss, take profit ni gestión de riesgo, para que el foco quede en el motor multi-IA. En un EA real le agregarías SL/TP, riesgo por operación como % del balance, filtros de horario, etc.

Decisión real del motor multi-IA en el log de MetaTrader 5


Errores comunes que me frenaron (y cómo los resolví)

Te cuento la parte que no aparece en los tutoriales: cuando armé esto por primera vez, no funcionó. El código compilaba sin un solo error, cero warnings, y aun así el EA no operaba. Así que me senté a debuggear en vivo, y los problemas que encontré son justo los que te vas a encontrar vos. Por eso los dejo acá, para ahorrarte las horas que perdí yo.

1. Las cuatro APIs me devolvían HTTP 400. Todas al mismo tiempo, y el código se veía bien. El problema estaba escondido en SendPost: yo armaba el array de bytes con StringLen(body) y después le restaba uno con ArrayResize. Eso me comía el último carácter del JSON, justo la llave de cierre, y el JSON quedaba inválido. La API lo rechazaba sin explicar mucho. La solución fue convertir el texto completo con WHOLE_ARRAY, y recién ahí sacar el cero final. Un solo detalle, y se cae todo.

// Wrong: StringLen + ArrayResize -1 eats the closing } at the end
StringToCharArray(body, post, 0, StringLen(body), CP_UTF8);

// Right: WHOLE_ARRAY and then you remove only the trailing zero
StringToCharArray(body, post, 0, WHOLE_ARRAY, CP_UTF8);
ArrayResize(post, ArraySize(post) - 1);

2. OpenAI me respondía 200, pero yo no parseaba nada. La respuesta llegaba bien, el estado era el correcto, y aun así mi extractor devolvía vacío. Resultó que algunos proveedores te mandan el JSON pegado ("content":"...") y otros con un espacio después de los dos puntos ("content": "..."). Yo buscaba el texto literal "content":" y, con el espacio en el medio, nunca lo encontraba. Por eso el extractor que viste más arriba no busca el texto pegado: busca la clave, después el dos puntos, y recién ahí la primera comilla. Así anda con los dos formatos, te manden lo que te manden.

3. Error 404, "modelo no encontrado". Me pasó con Gemini: tenía puesto el nombre de un modelo que ya no existía. Los proveedores cambian y deprecan modelos bastante seguido, así que si te tira 404, lo primero es ir a la documentación y verificar el nombre exacto del modelo que está vigente hoy. Con OpenAI me pasó algo parecido: según cómo esté configurada tu cuenta, a veces tenés que poner el ID exacto con fecha (por ejemplo, gpt-4o-mini-2024-07-18) en lugar del nombre corto.

Para que no te vuelvas loco como yo, te dejo una tabla con lo que significa cada código que te puede tirar la API, y dónde mirar:

Código Qué pasa Dónde mirar
400 El JSON está mal armado Casi siempre es el body (mirá el punto 1)
401 / 403 Problema con la key o el modelo Key mal copiada, sin permisos, o un modelo que tu cuenta no tiene habilitado
404 El modelo no existe Nombre mal escrito o modelo deprecado
429 Te pasaste del límite Demasiadas consultas por minuto, o cuota agotada
402 Sin saldo No te queda crédito en esa cuenta

Fijate que los dos últimos (429 y 402) no son errores de código, son de la cuenta. Si te aparecen, el EA está bien, lo que falta es saldo o esperar a que se libere el límite.

Y la moraleja, que para mí es la lección más importante de todo esto: que compile sin errores no quiere decir que funcione. El compilador te avisa si escribiste mal el código, pero no tiene idea de si una API te va a contestar bien. La única forma de saber si esto anda es probarlo en vivo y leer el log. A mí me salvó poner un PrintFormat en cada paso para ver exactamente dónde se rompía.


Seguridad y consideraciones

La regla más importante: nunca pongas tus claves API dentro del código del EA. Si la clave queda codificada en el .mq5 y luego compilás y compartís, vendés o pasás el EA, cualquiera puede extraerla. La key tiene que vivir afuera del código.

La solución que uso es simple: guardo las keys en un archivo keys.txt dentro de MQL5\Files, y el EA las lee al arrancar. La primera vez que lo corrés, si el archivo no existe, el EA crea una plantilla vacía con una línea por proveedor; vos solo la abrís y pegás tus keys. Ese archivo queda en la máquina del usuario y nunca viaja con el EA. El formato es una línea por proveedor: openai:sk-..., claude:sk-ant-..., gemini:..., deepseek:sk-...

Y esta es la función que lo lee (la LoadKeys que llamamos en OnInit):

string g_keyOpenAI, g_keyClaude, g_keyGemini, g_keyDeepSeek;
//+------------------------------------------------------------------+
//| Read the keys from MQL5\Files\keys.txt                           |
//+------------------------------------------------------------------+
bool LoadKeys()
  {
   int h = FileOpen("keys.txt", FILE_READ | FILE_TXT | FILE_ANSI);
   if(h == INVALID_HANDLE)
     {
      //--- The file does not exist yet: create an empty template and ask the user to fill it
      int w = FileOpen("keys.txt", FILE_WRITE | FILE_TXT | FILE_ANSI);
      if(w != INVALID_HANDLE)
        {
         FileWrite(w, "openai:");
         FileWrite(w, "claude:");
         FileWrite(w, "gemini:");
         FileWrite(w, "deepseek:");
         FileClose(w);
        }
      Print("keys.txt created in MQL5\\Files. Open it and put your API key after each provider, e.g. openai:YOUR_KEY");
      return(false);
     }
   while(!FileIsEnding(h))
     {
      string line = FileReadString(h);
      int sep = StringFind(line, ":");
      if(sep < 0)
         continue;
      string name = StringSubstr(line, 0, sep);
      string key  = StringSubstr(line, sep + 1);
      if(name == "openai")
         g_keyOpenAI   = key;
      if(name == "claude")
         g_keyClaude   = key;
      if(name == "gemini")
         g_keyGemini   = key;
      if(name == "deepseek")
         g_keyDeepSeek = key;
     }
   FileClose(h);
   return(true);
  }

Además de la seguridad, hay tres puntos a tener en cuenta: (1) costo: cada decisión implica cuatro llamadas a APIs pagas; si consultás muy seguido en varios gráficos, el gasto se acumula; (2) latencia y límites: las llamadas tardan segundos y cada proveedor limita la cantidad por minuto; (3) incertidumbre: el consenso aumenta la certeza, pero no garantiza aciertos; probá siempre en demo y usá gestión de riesgo.

Y no te olvides de permitir las URLs en Herramientas > Opciones > Asesores Expertos > Permitir WebRequest.


Conclusión

En esta primera parte construimos un motor de decisión multi-IA para MetaTrader 5: una estructura única (AIResponse) que estandariza la respuesta de cualquier IA, una capa que conecta a varios proveedores por WebRequest, un parseo que unifica formatos distintos, un prompt único que da el mismo contexto a todos, un motor de votación ponderada por confianza con quórum, y un EA de ejemplo que junta todo.

El resultado es lo que buscábamos al principio. En vez de depender de una sola IA y de sus errores, el EA consulta a varias y decide por consenso. Eso no garantiza acertar, ninguna IA lo hace, pero baja el riesgo de confiar en un solo modelo y aprovecha lo mejor de cada uno.

Todo el código está adjunto para que lo descargues, lo pruebes y lo adaptes a tus propios EAs.

En la Parte 2 vamos a llevar esto más lejos: ponderar a cada IA según qué tan acertada fue en el pasado (que el sistema aprenda en cuál confiar más), sumar más proveedores, y agregar gestión de riesgo real (SL/TP y tamaño de posición según la confianza).

Si lo probás o le hacés mejoras, contámelo en los comentarios. La idea es que esto crezca entre todos.

Archivos adjuntos |
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.
Formulación de un Asesor Experto Multipar Dinámico (Parte 4): Ajuste de volatilidad y riesgo Formulación de un Asesor Experto Multipar Dinámico (Parte 4): Ajuste de volatilidad y riesgo
Esta fase permite ajustar con precisión tu EA multipar para adaptar el tamaño de las operaciones y el riesgo en tiempo real utilizando indicadores de volatilidad como el ATR, lo que mejora la consistencia, la protección y el rendimiento en diversas condiciones de mercado.
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.
De novato a experto: Noticias animadas utilizando MQL5 (VIII) Botones de operación rápida para trading de noticias De novato a experto: Noticias animadas utilizando MQL5 (VIII) Botones de operación rápida para trading de noticias
Aunque los sistemas de trading algorítmico gestionan las operaciones de forma automatizada, muchos traders que operan en función de las noticias y los scalpers prefieren mantener un control activo durante noticias de alto impacto y en condiciones de mercado de ritmo acelerado, lo que exige una rápida ejecución y gestión de las órdenes. Esto pone de relieve la necesidad de contar con herramientas front-end intuitivas que integren fuentes de noticias en tiempo real, datos del calendario económico, análisis de indicadores, análisis basados en inteligencia artificial y controles de trading ágiles y de respuesta inmediata.