//+------------------------------------------------------------------+
//|                                     MultiAI_DecisionEngine.mq5    |
//|                          Multi-AI decision engine (Part 1)        |
//+------------------------------------------------------------------+
#property strict
#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

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

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

//--- API keys
string g_keyOpenAI, g_keyClaude, g_keyGemini, g_keyDeepSeek;
//+------------------------------------------------------------------+
//| 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");
  }
//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+
//| Escape a text string for JSON                                    |
//+------------------------------------------------------------------+
string JsonEscape(string text)
  {
   StringReplace(text, "\\", "\\\\");
   StringReplace(text, "\"", "\\\"");
   StringReplace(text, "\n", "\\n");
   StringReplace(text, "\r", "");
   return(text);
  }
//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+
//| 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));
  }
//+------------------------------------------------------------------+
//| Query DeepSeek                                                   |
//+------------------------------------------------------------------+
bool QueryDeepSeek(const string apiKey, const string prompt, string &response)
  {
   string url     = "https://api.deepseek.com/v1/chat/completions";
   string headers = "Content-Type: application/json\r\nAuthorization: Bearer " + apiKey + "\r\n";
   string body    = "{\"model\":\"deepseek-chat\",\"messages\":[{\"role\":\"user\",\"content\":\"" + JsonEscape(prompt) + "\"}],\"temperature\":0.2}";
   return(SendPost(url, headers, body, response));
  }
//+------------------------------------------------------------------+
//| Query Claude                                                     |
//+------------------------------------------------------------------+
bool QueryClaude(const string apiKey, const string prompt, string &response)
  {
   string url     = "https://api.anthropic.com/v1/messages";
   string headers = "Content-Type: application/json\r\nx-api-key: " + apiKey + "\r\nanthropic-version: 2023-06-01\r\n";
   string body    = "{\"model\":\"claude-3-5-sonnet-20240620\",\"max_tokens\":64,\"messages\":[{\"role\":\"user\",\"content\":\"" + JsonEscape(prompt) + "\"}]}";
   return(SendPost(url, headers, body, response));
  }
//+------------------------------------------------------------------+
//| Query Gemini                                                     |
//+------------------------------------------------------------------+
bool QueryGemini(const string apiKey, const string prompt, string &response)
  {
   string url     = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=" + apiKey;
   string headers = "Content-Type: application/json\r\n";
   string body    = "{\"contents\":[{\"parts\":[{\"text\":\"" + JsonEscape(prompt) + "\"}]}]}";
   return(SendPost(url, headers, body, response));
  }
//+------------------------------------------------------------------+
//| 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));
  }
//+------------------------------------------------------------------+
//| 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));
  }
//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+
