//+------------------------------------------------------------------+
//|                               MultiAI_DecisionEngine_Part2.mq5   |
//|                                   @temonba - binaryforexea.com   |
//|  Part 2: weighted voting that learns which AI to trust (per-     |
//|  provider hit rate) + risk management (enforced SL/TP and        |
//|  confidence-based position sizing). Builds on Part 1.            |
//+------------------------------------------------------------------+
#property copyright "@temonba"
#property link      "https://www.mql5.com/en/users/temonba"
#property version   "2.00"
#property strict
#include <Trade/Trade.mqh>

CTrade trade;

//--- 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)

//--- 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;
  };

//--- 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;

//+------------------------------------------------------------------+
//| 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 (from Part 1)                               |
//+------------------------------------------------------------------+
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 (from Part 1)                      |
//+------------------------------------------------------------------+
string JsonEscape(string text)
  {
   StringReplace(text, "\\", "\\\\");
   StringReplace(text, "\"", "\\\"");
   StringReplace(text, "\n", "\\n");
   StringReplace(text, "\r", "");
   return(text);
  }
//+------------------------------------------------------------------+
//| Build the prompt (from Part 1)                                   |
//+------------------------------------------------------------------+
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 (from Part 1)                                       |
//+------------------------------------------------------------------+
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 (from Part 1)                                     |
//+------------------------------------------------------------------+
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 (from Part 1)                                       |
//+------------------------------------------------------------------+
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-sonnet-4-6\",\"max_tokens\":64,\"messages\":[{\"role\":\"user\",\"content\":\"" + JsonEscape(prompt) + "\"}]}";
   return(SendPost(url, headers, body, response));
  }
//+------------------------------------------------------------------+
//| Query Gemini (from Part 1)                                       |
//+------------------------------------------------------------------+
bool QueryGemini(const string apiKey, const string prompt, string &response)
  {
   string url     = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-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 (from Part 1)      |
//+------------------------------------------------------------------+
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));
   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 (from Part 1)             |
//+------------------------------------------------------------------+
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 (from Part 1)             |
//+------------------------------------------------------------------+
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 (from Part 1)                                       |
//+------------------------------------------------------------------+
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 (from Part 1)           |
//+------------------------------------------------------------------+
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);
  }
//+------------------------------------------------------------------+
//| 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 - 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;
     }
  }
//+------------------------------------------------------------------+
//| NEW - drop graded predictions, keep the active ones              |
//+------------------------------------------------------------------+
void CompactPending()
  {
   Prediction keep[];
   for(int i = 0; i < ArraySize(g_pending); i++)
      if(g_pending[i].active)
        {
         int n = ArraySize(keep);
         ArrayResize(keep, n + 1);
         keep[n] = g_pending[i];
        }
   ArrayResize(g_pending, ArraySize(keep));
   for(int i = 0; i < ArraySize(keep); i++)
      g_pending[i] = keep[i];
  }
//+------------------------------------------------------------------+
//| 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();
  }
//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+
//| 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));
  }
//+------------------------------------------------------------------+
//| Read the keys from MQL5\Files\keys.txt (from Part 1)             |
//+------------------------------------------------------------------+
bool LoadKeys()
  {
   int h = FileOpen("keys.txt", FILE_READ | FILE_TXT | FILE_ANSI);
   if(h == INVALID_HANDLE)
     {
      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);
  }
//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   trade.LogLevel(LOG_LEVEL_NO);   // keep the Experts log clean: only our decision prints
   LoadKeys();
   for(int i = 0; i < 4; i++)
     {
      g_hitRate[i]   = 0.5;   // neutral until the AI proves itself
      g_evalCount[i] = 0;
     }
   LoadStats();

   g_atrHandle = iATR(_Symbol, PERIOD_H1, InpAtrPeriod);
   if(g_atrHandle == INVALID_HANDLE)
     {
      Print("Failed to create the ATR handle");
      return(INIT_FAILED);
     }

   EventSetTimer(60);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   SaveStats();
   EventKillTimer();
   if(g_atrHandle != INVALID_HANDLE)
      IndicatorRelease(g_atrHandle);
  }
//+------------------------------------------------------------------+
//| 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]);
  }
//+------------------------------------------------------------------+
