//+------------------------------------------------------------------+
//| ThongRooEA.mq5 — follows ThongRoo official XAUUSD signals       |
//|                                                                  |
//| SAFETY FIRST: test on a DEMO account for several weeks before    |
//| using real money. Trading CFDs with leverage can lose all capital.|
//|                                                                  |
//| Setup:                                                           |
//|  1. Copy this file to MQL5/Experts and compile in MetaEditor.    |
//|  2. Tools > Options > Expert Advisors > Allow WebRequest for the |
//|     URL of your ThongRoo website (e.g. https://yourdomain.com). |
//|  3. Attach the EA to a GOLD chart (XAUUSD / GOLD / XAUUSDm…).    |
//|  4. Paste your EA key (generated on the Auto Trade page).        |
//|                                                                  |
//| Risk settings (risk %, daily loss stop, max trades, spread,      |
//| trading hours, partial close, breakeven) are taken from your     |
//| account settings on the website and refreshed on every poll.     |
//+------------------------------------------------------------------+
#property copyright "ThongRoo"
#property version   "1.00"
#property strict

#include <Trade/Trade.mqh>

input string InpServerUrl   = "https://thongroo.com"; // Website URL (no trailing slash)
input string InpEaKey       = "";                           // EA key from the Auto Trade page
input long   InpMagic       = 20261103;                     // Magic number
input int    InpPollSeconds = 20;                           // Poll interval (seconds)
input int    InpSlippage    = 30;                           // Max slippage (points)

CTrade trade;
string gLastId = "";
bool   gPartial = true, gBe = true;   // last settings received from the server

//--- tiny JSON helpers (flat values only) ---------------------------
string JsonRaw(const string json, const string key)
  {
   string pat = "\"" + key + "\":";
   int p = StringFind(json, pat);
   if(p < 0) return "";
   p += StringLen(pat);
   while(p < StringLen(json) && StringGetCharacter(json, p) == ' ') p++;
   if(StringGetCharacter(json, p) == '"')
     {
      int e = StringFind(json, "\"", p + 1);
      return e > p ? StringSubstr(json, p + 1, e - p - 1) : "";
     }
   int e = p;
   while(e < StringLen(json))
     {
      ushort c = StringGetCharacter(json, e);
      if(c == ',' || c == '}' || c == ']') break;
      e++;
     }
   string v = StringSubstr(json, p, e - p);
   StringTrimLeft(v); StringTrimRight(v);
   return v;
  }
double JsonNum(const string json, const string key, double def) { string v = JsonRaw(json, key); return (v == "" || v == "null") ? def : StringToDouble(v); }
bool   JsonBool(const string json, const string key, bool def)  { string v = JsonRaw(json, key); if(v == "true") return true; if(v == "false") return false; return def; }
string JsonObj(const string json, const string key)
  {
   string pat = "\"" + key + "\":{";
   int p = StringFind(json, pat);
   if(p < 0) return "";
   p += StringLen(pat) - 1;
   int depth = 0;
   for(int i = p; i < StringLen(json); i++)
     {
      ushort c = StringGetCharacter(json, i);
      if(c == '{') depth++;
      if(c == '}') { depth--; if(depth == 0) return StringSubstr(json, p, i - p + 1); }
     }
   return "";
  }

//--- position helpers -----------------------------------------------
bool FindPosition(ulong &ticket)
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong t = PositionGetTicket(i);
      if(t == 0) continue;
      if(PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagic) { ticket = t; return true; }
     }
   return false;
  }

datetime DayStart() { MqlDateTime d; TimeToStruct(TimeCurrent(), d); d.hour = 0; d.min = 0; d.sec = 0; return StructToTime(d); }

void TodayStats(double &pnl, int &entries)
  {
   pnl = 0; entries = 0;
   if(!HistorySelect(DayStart(), TimeCurrent() + 60)) return;
   for(int i = HistoryDealsTotal() - 1; i >= 0; i--)
     {
      ulong d = HistoryDealGetTicket(i);
      if(HistoryDealGetInteger(d, DEAL_MAGIC) != InpMagic || HistoryDealGetString(d, DEAL_SYMBOL) != _Symbol) continue;
      pnl += HistoryDealGetDouble(d, DEAL_PROFIT) + HistoryDealGetDouble(d, DEAL_SWAP) + HistoryDealGetDouble(d, DEAL_COMMISSION);
      if(HistoryDealGetInteger(d, DEAL_ENTRY) == DEAL_ENTRY_IN) entries++;
     }
  }

double NormalizeVolume(double lots)
  {
   double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   double vmin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double vmax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   if(step <= 0) step = 0.01;
   lots = MathFloor(lots / step) * step;
   if(lots < vmin) return 0;           // too small for the configured risk -> skip, never round up risk
   return MathMin(lots, vmax);
  }

//--- manage open position: partial close at TP1 + breakeven ---------
void ManagePosition(const ulong ticket, const bool partial, const bool breakeven)
  {
   if(!PositionSelectByTicket(ticket)) return;
   string key = "AE_TP1_" + (string)ticket, doneKey = "AE_DONE_" + (string)ticket;
   if(!GlobalVariableCheck(key) || GlobalVariableCheck(doneKey)) return;
   double tp1 = GlobalVariableGet(key);
   long   type = PositionGetInteger(POSITION_TYPE);
   double open = PositionGetDouble(POSITION_PRICE_OPEN);
   double vol  = PositionGetDouble(POSITION_VOLUME);
   double px   = type == POSITION_TYPE_BUY ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   bool hit = type == POSITION_TYPE_BUY ? px >= tp1 : px <= tp1;
   if(!hit) return;
   if(partial)
     {
      double half = NormalizeVolume(vol / 2.0);
      if(half > 0 && vol - half >= SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)) trade.PositionClosePartial(ticket, half);
     }
   if(breakeven && PositionSelectByTicket(ticket))
      trade.PositionModify(ticket, NormalizeDouble(open, _Digits), PositionGetDouble(POSITION_TP));
   GlobalVariableSet(doneKey, 1);
  }

//--- main poll ------------------------------------------------------
void Poll()
  {
   if(InpEaKey == "") { Comment("ThongRoo: please enter your EA key"); return; }
   char post[], result[]; string headers;
   string req = "X-EA-Key: " + InpEaKey + "\r\nAccept: application/json\r\n";
   ResetLastError();
   int code = WebRequest("GET", InpServerUrl + "/api/ea/signal", req, 8000, post, result, headers);
   if(code == -1) { Comment("ThongRoo: WebRequest failed (", GetLastError(), "). Allow the URL in Tools > Options > Expert Advisors."); return; }
   string json = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8);
   if(code != 200) { Comment("ThongRoo: server error ", code, " ", json); return; }

   string cfg = JsonObj(json, "settings");
   double riskPct   = JsonNum(cfg, "riskPct", 1.0);
   double maxLoss   = JsonNum(cfg, "maxDailyLossPct", 3.0);
   int    maxTrades = (int)JsonNum(cfg, "maxTradesPerDay", 3);
   int    maxSpread = (int)JsonNum(cfg, "maxSpreadPoints", 40);
   double minConf   = JsonNum(cfg, "minConfidence", 65);
   bool   partial   = JsonBool(cfg, "partialAtTp1", true);
   bool   be        = JsonBool(cfg, "breakevenAtTp1", true);
   gPartial = partial; gBe = be;

   ulong ticket = 0;
   bool hasPos = FindPosition(ticket);
   if(hasPos) ManagePosition(ticket, partial, be);   // always manage, even outside session

   bool active = JsonBool(json, "active", false);
   double pnl; int entries; TodayStats(pnl, entries);
   double balance = AccountInfoDouble(ACCOUNT_BALANCE);
   string status = StringFormat("ThongRoo | active:%s | today P/L: %.2f | trades: %d/%d", active ? "yes" : "no", pnl, entries, maxTrades);

   if(!active)                                   { Comment(status, " | ", JsonRaw(json, "reason")); return; }
   if(hasPos)                                    { Comment(status, " | position open"); return; }
   if(pnl < 0 && -pnl >= balance * maxLoss / 100.0) { Comment(status, " | daily loss limit reached — stopped for today"); return; }
   if(entries >= maxTrades)                      { Comment(status, " | max trades reached"); return; }

   string sig = JsonObj(json, "signal");
   if(sig == "") { Comment(status, " | waiting for a fresh signal"); return; }
   string id = JsonRaw(sig, "id");
   if(id == "" || id == gLastId || GlobalVariableCheck("AE_SIG_" + id)) { Comment(status, " | signal already handled"); return; }
   string dir  = JsonRaw(sig, "direction");
   double conf = JsonNum(sig, "confidence", 0);
   double e = JsonNum(sig, "entry", 0), sl = JsonNum(sig, "sl", 0), tp1 = JsonNum(sig, "tp1", 0), tp2 = JsonNum(sig, "tp2", 0);
   if(conf < minConf || e <= 0 || sl <= 0) { Comment(status, " | signal below minimum confidence"); MarkHandled(id); return; }

   long spread = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
   if(spread > maxSpread) { Comment(status, " | spread too wide: ", spread); return; } // retry next poll

   // Broker prices differ slightly from the reference feed, so use DISTANCES from the signal
   double slDist = MathAbs(e - sl), tp1Dist = MathAbs(tp1 - e), tp2Dist = MathAbs(tp2 - e);
   bool buy = dir == "BUY";
   double price = buy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double slPx  = NormalizeDouble(buy ? price - slDist : price + slDist, _Digits);
   double tp1Px = NormalizeDouble(buy ? price + tp1Dist : price - tp1Dist, _Digits);
   double tp2Px = NormalizeDouble(buy ? price + tp2Dist : price - tp2Dist, _Digits);

   // position size from risk %
   double lossPerLot = 0;
   if(!OrderCalcProfit(buy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, _Symbol, 1.0, price, slPx, lossPerLot) || lossPerLot >= 0)
     { Comment(status, " | cannot calculate risk"); return; }
   double lots = NormalizeVolume((balance * riskPct / 100.0) / MathAbs(lossPerLot));
   if(lots <= 0) { Comment(status, " | account too small for this risk — skipped"); MarkHandled(id); return; }

   trade.SetExpertMagicNumber(InpMagic);
   trade.SetDeviationInPoints(InpSlippage);
   bool ok = buy ? trade.Buy(lots, _Symbol, price, slPx, tp2Px, "AE#" + id) : trade.Sell(lots, _Symbol, price, slPx, tp2Px, "AE#" + id);
   if(ok && trade.ResultRetcode() == TRADE_RETCODE_DONE)
     {
      MarkHandled(id);
      ulong t = 0;
      if(FindPosition(t)) GlobalVariableSet("AE_TP1_" + (string)t, tp1Px);
      Comment(status, StringFormat(" | opened %s %.2f lots SL %.2f TP1 %.2f TP2 %.2f", dir, lots, slPx, tp1Px, tp2Px));
     }
   else Comment(status, " | order failed: ", trade.ResultRetcodeDescription());
  }

void MarkHandled(const string id) { gLastId = id; GlobalVariableSet("AE_SIG_" + id, 1); }

int OnInit()
  {
   if(MQLInfoInteger(MQL_TESTER)) { Print("ThongRooEA uses live web signals and cannot run in the Strategy Tester."); return INIT_FAILED; }
   trade.SetExpertMagicNumber(InpMagic);
   EventSetTimer(MathMax(5, InpPollSeconds));
   Poll();
   return INIT_SUCCEEDED;
  }
void OnDeinit(const int reason) { EventKillTimer(); Comment(""); }
void OnTimer() { Poll(); }
void OnTick()  { ulong t; if(FindPosition(t)) { /* fast TP1 management between polls */ ManagePosition(t, gPartial, gBe); } }
//+------------------------------------------------------------------+
